Initial EIP-1559 support (#1610).
This commit is contained in:
parent
d395d16fa3
commit
7a12216cfb
@ -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;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user