Initial EIP-1559 support (#1610).

This commit is contained in:
Richard Moore 2021-05-30 17:47:04 -04:00
parent d395d16fa3
commit 7a12216cfb
No known key found for this signature in database
GPG Key ID: 665176BE8E9DC651
6 changed files with 253 additions and 26 deletions

@ -29,6 +29,9 @@ export type TransactionRequest = {
type?: number;
accessList?: AccessListish;
maxPriorityFeePerGas?: BigNumberish;
maxFeePerGas?: BigNumberish;
}
export interface TransactionResponse extends Transaction {
@ -67,6 +70,8 @@ interface _Block {
miner: string;
extraData: string;
baseFee?: null | BigNumber;
}
export interface Block extends _Block {

@ -10,7 +10,7 @@ import { version } from "./_version";
const logger = new Logger(version);
const allowedTransactionKeys: Array<string> = [
"accessList", "chainId", "data", "from", "gasLimit", "gasPrice", "nonce", "to", "type", "value"
"accessList", "chainId", "data", "from", "gasLimit", "gasPrice", "maxFeePerGas", "maxPriorityFeePerGas", "nonce", "to", "type", "value"
];
const forwardErrors = [
@ -19,6 +19,12 @@ const forwardErrors = [
Logger.errors.REPLACEMENT_UNDERPRICED,
];
export interface FeeData {
maxFeePerGas: null | BigNumber;
maxPriorityFeePerGas: null | BigNumber;
gasPrice: null | BigNumber;
}
// EIP-712 Typed Data
// See: https://eips.ethereum.org/EIPS/eip-712
@ -139,6 +145,25 @@ export abstract class Signer {
return await this.provider.getGasPrice();
}
async getFeeData(): Promise<FeeData> {
this._checkProvider("getFeeStats");
const { block, gasPrice } = await resolveProperties({
block: this.provider.getBlock(-1),
gasPrice: this.provider.getGasPrice()
});
let maxFeePerGas = null, maxPriorityFeePerGas = null;
if (block && block.baseFee) {
maxFeePerGas = block.baseFee.mul(2);
maxPriorityFeePerGas = BigNumber.from("1000000000");
}
return { maxFeePerGas, maxPriorityFeePerGas, gasPrice };
}
async resolveName(name: string): Promise<string> {
this._checkProvider("resolveName");
return await this.provider.resolveName(name);
@ -146,7 +171,6 @@ export abstract class Signer {
// Checks a transaction does not contain invalid keys and if
// no "from" is provided, populates it.
// - does NOT require a provider
@ -167,6 +191,7 @@ export abstract class Signer {
if (tx.from == null) {
tx.from = this.getAddress();
} else {
// Make sure any provided address matches this signer
tx.from = Promise.all([
@ -187,6 +212,9 @@ export abstract class Signer {
// this Signer. Should be used by sendTransaction but NOT by signTransaction.
// By default called from: (overriding these prevents it)
// - sendTransaction
//
// Notes:
// - We allow gasPrice for EIP-1559 as long as it matches maxFeePerGas
async populateTransaction(transaction: Deferrable<TransactionRequest>): Promise<TransactionRequest> {
const tx: Deferrable<TransactionRequest> = await resolveProperties(this.checkTransaction(transaction))
@ -201,7 +229,93 @@ export abstract class Signer {
return address;
});
}
if (tx.gasPrice == null) { tx.gasPrice = this.getGasPrice(); }
if ((tx.type === 2 || tx.type == null) && (tx.maxFeePerGas != null && tx.maxPriorityFeePerGas != null)) {
// Fully-formed EIP-1559 transaction
// Check the gasPrice == maxFeePerGas
if (tx.gasPrice != null && !BigNumber.from(tx.gasPrice).eq(<BigNumberish>(tx.maxFeePerGas))) {
logger.throwArgumentError("gasPrice/maxFeePerGas mismatch", "transaction", transaction);
}
tx.type = 2;
} else if (tx.type === -1 || tx.type === 1) {
// Explicit EIP-2930 or Legacy transaction
// Do not allow EIP-1559 properties
if (tx.maxFeePerGas != null || tx.maxPriorityFeePerGas != null) {
logger.throwArgumentError(`transaction type ${ tx.type } does not support eip-1559 keys`, "transaction", transaction);
}
if (tx.gasPrice == null) { tx.gasPrice = this.getGasPrice(); }
tx.type = (tx.accessList ? 1: -1);
} else {
// We need to get fee data to determine things
const feeData = await this.getFeeData();
if (tx.type == null) {
// We need to auto-detect the intended type of this transaction...
if (feeData.maxFeePerGas != null && feeData.maxPriorityFeePerGas != null) {
// The network supports EIP-1559!
if (tx.gasPrice != null && tx.maxFeePerGas == null && tx.maxPriorityFeePerGas == null) {
// Legacy or EIP-2930 transaction, but without its type set
tx.type = (tx.accessList ? 1: -1);
} else {
// Use EIP-1559; no gas price or one EIP-1559 property was specified
// Check that gasPrice == maxFeePerGas
if (tx.gasPrice != null) {
// The first condition fails only if gasPrice and maxPriorityFeePerGas
// were specified, which is a weird thing to do
if (tx.maxFeePerGas == null || !BigNumber.from(tx.gasPrice).eq(<BigNumberish>(tx.maxFeePerGas))) {
logger.throwArgumentError("gasPrice/maxFeePerGas mismatch", "transaction", transaction);
}
}
tx.type = 2;
if (tx.maxFeePerGas == null) { tx.maxFeePerGas = feeData.maxFeePerGas; }
if (tx.maxPriorityFeePerGas == null) { tx.maxPriorityFeePerGas = feeData.maxPriorityFeePerGas; }
}
} else if (feeData.gasPrice != null) {
// Network doesn't support EIP-1559...
// ...but they are trying to use EIP-1559 properties
if (tx.maxFeePerGas != null || tx.maxPriorityFeePerGas != null) {
logger.throwError("network does not support EIP-1559", Logger.errors.UNSUPPORTED_OPERATION, {
operation: "populateTransaction"
});
}
tx.gasPrice = feeData.gasPrice;
tx.type = (tx.accessList ? 1: -1);
} else {
// getFeeData has failed us.
logger.throwError("failed to get consistent fee data", Logger.errors.UNSUPPORTED_OPERATION, {
operation: "signer.getFeeData"
});
}
} else if (tx.type === 2) {
// Explicitly using EIP-1559
// Check gasPrice == maxFeePerGas
if (tx.gasPrice != null && !BigNumber.from(tx.gasPrice).eq(<BigNumberish>(tx.maxFeePerGas))) {
logger.throwArgumentError("gasPrice/maxFeePerGas mismatch", "transaction", transaction);
}
if (tx.maxFeePerGas == null) { tx.maxFeePerGas = feeData.maxFeePerGas; }
if (tx.maxPriorityFeePerGas == null) { tx.maxPriorityFeePerGas = feeData.maxPriorityFeePerGas; }
}
}
if (tx.nonce == null) { tx.nonce = this.getTransactionCount("pending"); }
if (tx.gasLimit == null) {

@ -17,7 +17,7 @@ import { checkProperties, deepCopy, defineReadOnly, getStatic, resolveProperties
import * as RLP from "@ethersproject/rlp";
import { computePublicKey, recoverPublicKey, SigningKey } from "@ethersproject/signing-key";
import { formatBytes32String, nameprep, parseBytes32String, _toEscapedUtf8String, toUtf8Bytes, toUtf8CodePoints, toUtf8String, Utf8ErrorFuncs } from "@ethersproject/strings";
import { accessListify, computeAddress, parse as parseTransaction, recoverAddress, serialize as serializeTransaction } from "@ethersproject/transactions";
import { accessListify, computeAddress, parse as parseTransaction, recoverAddress, serialize as serializeTransaction, TransactionTypes } from "@ethersproject/transactions";
import { commify, formatEther, parseEther, formatUnits, parseUnits } from "@ethersproject/units";
import { verifyMessage, verifyTypedData } from "@ethersproject/wallet";
import { _fetchData, fetchJson, poll } from "@ethersproject/web";
@ -153,6 +153,7 @@ export {
accessListify,
parseTransaction,
serializeTransaction,
TransactionTypes,
getJsonWalletAddress,

@ -62,7 +62,12 @@ export class Formatter {
from: address,
gasPrice: bigNumber,
// either (gasPrice) or (maxPriorityFeePerGas + maxFeePerGas)
// must be set
gasPrice: Formatter.allowNull(bigNumber),
maxPriorityFeePerGas: Formatter.allowNull(bigNumber),
maxFeePerGas: Formatter.allowNull(bigNumber),
gasLimit: bigNumber,
to: Formatter.allowNull(address, null),
value: bigNumber,
@ -83,6 +88,8 @@ export class Formatter {
nonce: Formatter.allowNull(number),
gasLimit: Formatter.allowNull(bigNumber),
gasPrice: Formatter.allowNull(bigNumber),
maxPriorityFeePerGas: Formatter.allowNull(bigNumber),
maxFeePerGas: Formatter.allowNull(bigNumber),
to: Formatter.allowNull(address),
value: Formatter.allowNull(bigNumber),
data: Formatter.allowNull(strictData),
@ -135,6 +142,8 @@ export class Formatter {
extraData: data,
transactions: Formatter.allowNull(Formatter.arrayOf(hash)),
baseFee: Formatter.allowNull(bigNumber)
};
formats.blockWithTransactions = shallowCopy(formats.block);
@ -323,6 +332,12 @@ export class Formatter {
const result: TransactionResponse = Formatter.check(this.formats.transaction, transaction);
if (result.type === 2) {
if (result.gasPrice == null) {
result.gasPrice = result.maxFeePerGas;
}
}
if (transaction.chainId != null) {
let chainId = transaction.chainId;

@ -592,7 +592,7 @@ export class JsonRpcProvider extends BaseProvider {
const result: { [key: string]: string | AccessList } = {};
// Some nodes (INFURA ropsten; INFURA mainnet is fine) do not like leading zeros.
["gasLimit", "gasPrice", "type", "nonce", "value"].forEach(function(key) {
["gasLimit", "gasPrice", "type", "maxFeePerGas", "maxPriorityFeePerGas", "nonce", "value"].forEach(function(key) {
if ((<any>transaction)[key] == null) { return; }
const value = hexValue((<any>transaction)[key]);
if (key === "gasLimit") { key = "gas"; }

@ -23,6 +23,12 @@ export type AccessListish = AccessList |
Array<[ string, Array<string> ]> |
Record<string, Array<string>>;
export const TransactionTypes: Record<string, number> = Object.freeze({
legacy: -1,
eip2930: 1,
eip1559: 2,
});
export type UnsignedTransaction = {
to?: string;
nonce?: number;
@ -36,7 +42,13 @@ export type UnsignedTransaction = {
// Typed-Transaction features
type?: number | null;
// EIP-2930; Type 1 & EIP-1559; Type 2
accessList?: AccessListish;
// EIP-1559; Type 2
maxPriorityFeePerGas?: BigNumberish;
maxFeePerGas?: BigNumberish;
}
export interface Transaction {
@ -60,8 +72,12 @@ export interface Transaction {
// Typed-Transaction features
type?: number | null;
// EIP-2930; Type 1
// EIP-2930; Type 1 & EIP-1559; Type 2
accessList?: AccessList;
// EIP-1559; Type 2
maxPriorityFeePerGas?: BigNumber;
maxFeePerGas?: BigNumber;
}
///////////////////////////////
@ -147,6 +163,42 @@ function formatAccessList(value: AccessListish): Array<[ string, Array<string> ]
return accessListify(value).map((set) => [ set.address, set.storageKeys ]);
}
function _serializeEip1559(transaction: UnsignedTransaction, signature?: SignatureLike): string {
// If there is an explicit gasPrice, make sure it matches the
// EIP-1559 fees; otherwise they may not understand what they
// think they are setting in terms of fee.
if (transaction.gasPrice != null) {
const gasPrice = BigNumber.from(transaction.gasPrice);
const maxFeePerGas = BigNumber.from(transaction.maxFeePerGas || 0);
if (!gasPrice.eq(maxFeePerGas)) {
logger.throwArgumentError("mismatch EIP-1559 gasPrice != maxFeePerGas", "tx", {
gasPrice, maxFeePerGas
});
}
}
const fields: any = [
formatNumber(transaction.chainId || 0, "chainId"),
formatNumber(transaction.nonce || 0, "nonce"),
formatNumber(transaction.maxPriorityFeePerGas || 0, "maxPriorityFeePerGas"),
formatNumber(transaction.maxFeePerGas || 0, "maxFeePerGas"),
formatNumber(transaction.gasLimit || 0, "gasLimit"),
((transaction.to != null) ? getAddress(transaction.to): "0x"),
formatNumber(transaction.value || 0, "value"),
(transaction.data || "0x"),
(formatAccessList(transaction.accessList || []))
];
if (signature) {
const sig = splitSignature(signature);
fields.push(formatNumber(sig.recoveryParam, "recoveryParam"));
fields.push(stripZeros(sig.r));
fields.push(stripZeros(sig.s));
}
return hexConcat([ "0x02", RLP.encode(fields)]);
}
function _serializeEip2930(transaction: UnsignedTransaction, signature?: SignatureLike): string {
const fields: any = [
formatNumber(transaction.chainId || 0, "chainId"),
@ -252,7 +304,7 @@ function _serialize(transaction: UnsignedTransaction, signature?: SignatureLike)
export function serialize(transaction: UnsignedTransaction, signature?: SignatureLike): string {
// Legacy and EIP-155 Transactions
if (transaction.type == null) {
if (transaction.type == null || transaction.type === -1) {
if (transaction.accessList != null) {
logger.throwArgumentError("untyped transactions do not support accessList; include type: 1", "transaction", transaction);
}
@ -263,6 +315,8 @@ export function serialize(transaction: UnsignedTransaction, signature?: Signatur
switch (transaction.type) {
case 1:
return _serializeEip2930(transaction, signature);
case 2:
return _serializeEip1559(transaction, signature);
default:
break;
}
@ -273,6 +327,58 @@ export function serialize(transaction: UnsignedTransaction, signature?: Signatur
});
}
function _parseEipSignature(tx: Transaction, fields: Array<string>, serialize: (tx: UnsignedTransaction) => string): void {
try {
const recid = handleNumber(fields[0]).toNumber();
if (recid !== 0 && recid !== 1) { throw new Error("bad recid"); }
tx.v = recid;
} catch (error) {
logger.throwArgumentError("invalid v for transaction type: 1", "v", fields[0]);
}
tx.r = hexZeroPad(fields[1], 32);
tx.s = hexZeroPad(fields[2], 32);
try {
const digest = keccak256(serialize(tx));
tx.from = recoverAddress(digest, { r: tx.r, s: tx.s, recoveryParam: tx.v });
} catch (error) {
console.log(error);
}
}
function _parseEip1559(payload: Uint8Array): Transaction {
const transaction = RLP.decode(payload.slice(1));
if (transaction.length !== 9 && transaction.length !== 12) {
logger.throwArgumentError("invalid component count for transaction type: 2", "payload", hexlify(payload));
}
const maxPriorityFeePerGas = handleNumber(transaction[2]);
const maxFeePerGas = handleNumber(transaction[3]);
const tx: Transaction = {
type: 2,
chainId: handleNumber(transaction[0]).toNumber(),
nonce: handleNumber(transaction[1]).toNumber(),
maxPriorityFeePerGas: maxPriorityFeePerGas,
maxFeePerGas: maxFeePerGas,
gasPrice: maxPriorityFeePerGas.add(maxFeePerGas),
gasLimit: handleNumber(transaction[4]),
to: handleAddress(transaction[5]),
value: handleNumber(transaction[6]),
data: transaction[7],
accessList: accessListify(transaction[8]),
hash: keccak256(payload)
};
// Unsigned EIP-1559 Transaction
if (transaction.length === 9) { return tx; }
_parseEipSignature(tx, transaction.slice(9), _serializeEip2930);
return null;
}
function _parseEip2930(payload: Uint8Array): Transaction {
const transaction = RLP.decode(payload.slice(1));
@ -290,29 +396,13 @@ function _parseEip2930(payload: Uint8Array): Transaction {
value: handleNumber(transaction[5]),
data: transaction[6],
accessList: accessListify(transaction[7]),
hash: keccak256(payload)
};
// Unsigned EIP-2930 Transaction
if (transaction.length === 8) { return tx; }
try {
const recid = handleNumber(transaction[8]).toNumber();
if (recid !== 0 && recid !== 1) { throw new Error("bad recid"); }
tx.v = recid;
} catch (error) {
logger.throwArgumentError("invalid v for transaction type: 1", "v", transaction[8]);
}
tx.r = hexZeroPad(transaction[9], 32);
tx.s = hexZeroPad(transaction[10], 32);
try {
const digest = keccak256(_serializeEip2930(tx));
tx.from = recoverAddress(digest, { r: tx.r, s: tx.s, recoveryParam: tx.v });
} catch (error) {
console.log(error);
}
tx.hash = keccak256(payload);
_parseEipSignature(tx, transaction.slice(8), _serializeEip2930);
return tx;
}
@ -397,6 +487,8 @@ export function parse(rawTransaction: BytesLike): Transaction {
switch (payload[0]) {
case 1:
return _parseEip2930(payload);
case 2:
return _parseEip1559(payload);
default:
break;
}