Added EIP-2930 support (#1364).

This commit is contained in:
Richard Moore 2021-03-26 16:16:56 -04:00
parent 1db4ce12d4
commit c47d2eba4d
No known key found for this signature in database
GPG Key ID: 665176BE8E9DC651
13 changed files with 487 additions and 40 deletions

@ -4,7 +4,7 @@ import { BigNumber, BigNumberish } from "@ethersproject/bignumber";
import { BytesLike, isHexString } from "@ethersproject/bytes"; import { BytesLike, isHexString } from "@ethersproject/bytes";
import { Network } from "@ethersproject/networks"; import { Network } from "@ethersproject/networks";
import { Deferrable, Description, defineReadOnly } from "@ethersproject/properties"; import { Deferrable, Description, defineReadOnly } from "@ethersproject/properties";
import { Transaction } from "@ethersproject/transactions"; import { AccessListish, Transaction } from "@ethersproject/transactions";
import { OnceBlockable } from "@ethersproject/web"; import { OnceBlockable } from "@ethersproject/web";
import { Logger } from "@ethersproject/logger"; import { Logger } from "@ethersproject/logger";
@ -26,6 +26,9 @@ export type TransactionRequest = {
data?: BytesLike, data?: BytesLike,
value?: BigNumberish, value?: BigNumberish,
chainId?: number chainId?: number
type?: number;
accessList?: AccessListish;
} }
export interface TransactionResponse extends Transaction { export interface TransactionResponse extends Transaction {

@ -10,7 +10,7 @@ import { version } from "./_version";
const logger = new Logger(version); const logger = new Logger(version);
const allowedTransactionKeys: Array<string> = [ const allowedTransactionKeys: Array<string> = [
"chainId", "data", "from", "gasLimit", "gasPrice", "nonce", "to", "value" "accessList", "chainId", "data", "from", "gasLimit", "gasPrice", "nonce", "to", "type", "value"
]; ];
const forwardErrors = [ const forwardErrors = [

@ -390,6 +390,8 @@ export function splitSignature(signature: SignatureLike): Signature {
if (result.recoveryParam == null) { if (result.recoveryParam == null) {
if (result.v == null) { if (result.v == null) {
logger.throwArgumentError("signature missing v and recoveryParam", "signature", signature); logger.throwArgumentError("signature missing v and recoveryParam", "signature", signature);
} else if (result.v === 0 || result.v === 1) {
result.recoveryParam = result.v;
} else { } else {
result.recoveryParam = 1 - (result.v % 2); result.recoveryParam = 1 - (result.v % 2);
} }

@ -9,7 +9,8 @@
"@ethersproject/bytes": "^5.0.9", "@ethersproject/bytes": "^5.0.9",
"@ethersproject/constants": "^5.0.8", "@ethersproject/constants": "^5.0.8",
"@ethersproject/logger": "^5.0.8", "@ethersproject/logger": "^5.0.8",
"@ethersproject/properties": "^5.0.7" "@ethersproject/properties": "^5.0.7",
"@ethersproject/transactions": "^5.0.11"
}, },
"description": "Contract abstraction meta-class for ethers.", "description": "Contract abstraction meta-class for ethers.",
"ethereum": "donations.ethers.eth", "ethereum": "donations.ethers.eth",

@ -7,7 +7,7 @@ import { getAddress, getContractAddress } from "@ethersproject/address";
import { BigNumber, BigNumberish } from "@ethersproject/bignumber"; import { BigNumber, BigNumberish } from "@ethersproject/bignumber";
import { arrayify, BytesLike, concat, hexlify, isBytes, isHexString } from "@ethersproject/bytes"; import { arrayify, BytesLike, concat, hexlify, isBytes, isHexString } from "@ethersproject/bytes";
import { Deferrable, defineReadOnly, deepCopy, getStatic, resolveProperties, shallowCopy } from "@ethersproject/properties"; import { Deferrable, defineReadOnly, deepCopy, getStatic, resolveProperties, shallowCopy } from "@ethersproject/properties";
// @TOOD remove dependences transactions import { AccessList, accessListify, AccessListish } from "@ethersproject/transactions";
import { Logger } from "@ethersproject/logger"; import { Logger } from "@ethersproject/logger";
import { version } from "./_version"; import { version } from "./_version";
@ -18,6 +18,8 @@ export interface Overrides {
gasLimit?: BigNumberish | Promise<BigNumberish>; gasLimit?: BigNumberish | Promise<BigNumberish>;
gasPrice?: BigNumberish | Promise<BigNumberish>; gasPrice?: BigNumberish | Promise<BigNumberish>;
nonce?: BigNumberish | Promise<BigNumberish>; nonce?: BigNumberish | Promise<BigNumberish>;
type?: number;
accessList?: AccessListish;
}; };
export interface PayableOverrides extends Overrides { export interface PayableOverrides extends Overrides {
@ -45,6 +47,9 @@ export interface PopulatedTransaction {
data?: string; data?: string;
value?: BigNumber; value?: BigNumber;
chainId?: number; chainId?: number;
type?: number;
accessList?: AccessList;
}; };
export type EventFilter = { export type EventFilter = {
@ -94,7 +99,8 @@ export interface ContractTransaction extends TransactionResponse {
/////////////////////////////// ///////////////////////////////
const allowedTransactionKeys: { [ key: string ]: boolean } = { const allowedTransactionKeys: { [ key: string ]: boolean } = {
chainId: true, data: true, from: true, gasLimit: true, gasPrice:true, nonce: true, to: true, value: true chainId: true, data: true, from: true, gasLimit: true, gasPrice:true, nonce: true, to: true, value: true,
type: true, accessList: true,
} }
async function resolveName(resolver: Signer | Provider, nameOrPromise: string | Promise<string>): Promise<string> { async function resolveName(resolver: Signer | Provider, nameOrPromise: string | Promise<string>): Promise<string> {
@ -212,6 +218,9 @@ async function populateTransaction(contract: Contract, fragment: FunctionFragmen
if (ro.gasPrice != null) { tx.gasPrice = BigNumber.from(ro.gasPrice); } if (ro.gasPrice != null) { tx.gasPrice = BigNumber.from(ro.gasPrice); }
if (ro.from != null) { tx.from = ro.from; } if (ro.from != null) { tx.from = ro.from; }
if (ro.type != null) { tx.type = ro.type; }
if (ro.accessList != null) { tx.accessList = accessListify(ro.accessList); }
// If there was no "gasLimit" override, but the ABI specifies a default, use it // If there was no "gasLimit" override, but the ABI specifies a default, use it
if (tx.gasLimit == null && fragment.gas != null) { if (tx.gasLimit == null && fragment.gas != null) {
// Conmpute the intrinisic gas cost for this transaction // Conmpute the intrinisic gas cost for this transaction
@ -247,6 +256,9 @@ async function populateTransaction(contract: Contract, fragment: FunctionFragmen
delete overrides.from; delete overrides.from;
delete overrides.value; delete overrides.value;
delete overrides.type;
delete overrides.accessList;
// Make sure there are no stray overrides, which may indicate a // Make sure there are no stray overrides, which may indicate a
// typo or using an unsupported key. // typo or using an unsupported key.
const leftovers = Object.keys(overrides).filter((key) => ((<any>overrides)[key] != null)); const leftovers = Object.keys(overrides).filter((key) => ((<any>overrides)[key] != null));

@ -39,14 +39,30 @@ function ethDefaultProvider(network: string | Network): Renetworkable {
} }
if (providers.AlchemyProvider) { if (providers.AlchemyProvider) {
// These networks are currently faulty on Alchemy as their
// network does not handle the Berlin hardfork, which is
// live on these ones.
// @TODO: This goes away once AlchemyAPI has upgraded their nodes
const skip = [ "goerli", "ropsten", "rinkeby" ];
try { try {
providerList.push(new providers.AlchemyProvider(network, options.alchemy)); const provider = new providers.AlchemyProvider(network, options.alchemy);
if (provider.network && skip.indexOf(provider.network.name) === -1) {
providerList.push(provider);
}
} catch(error) { } } catch(error) { }
} }
if (providers.PocketProvider) { if (providers.PocketProvider) {
// These networks are currently faulty on Alchemy as their
// network does not handle the Berlin hardfork, which is
// live on these ones.
// @TODO: This goes away once Pocket has upgraded their nodes
const skip = [ "goerli", "ropsten", "rinkeby" ];
try { try {
providerList.push(new providers.PocketProvider(network)); const provider = new providers.PocketProvider(network);
if (provider.network && skip.indexOf(provider.network.name) === -1) {
providerList.push(provider);
}
} catch(error) { } } catch(error) { }
} }

@ -1122,6 +1122,15 @@ export class BaseProvider extends Provider implements EnsProvider {
tx[key] = Promise.resolve(values[key]).then((v) => (v ? BigNumber.from(v): null)); tx[key] = Promise.resolve(values[key]).then((v) => (v ? BigNumber.from(v): null));
}); });
["type"].forEach((key) => {
if (values[key] == null) { return; }
tx[key] = Promise.resolve(values[key]).then((v) => ((v != null) ? v: null));
});
if (values.accessList) {
tx.accessList = this.formatter.accessList(values.accessList);
}
["data"].forEach((key) => { ["data"].forEach((key) => {
if (values[key] == null) { return; } if (values[key] == null) { return; }
tx[key] = Promise.resolve(values[key]).then((v) => (v ? hexlify(v): null)); tx[key] = Promise.resolve(values[key]).then((v) => (v ? hexlify(v): null));
@ -1175,6 +1184,7 @@ export class BaseProvider extends Provider implements EnsProvider {
const params = await resolveProperties({ const params = await resolveProperties({
transaction: this._getTransactionRequest(transaction) transaction: this._getTransactionRequest(transaction)
}); });
const result = await this.perform("estimateGas", params); const result = await this.perform("estimateGas", params);
try { try {
return BigNumber.from(result); return BigNumber.from(result);

@ -20,10 +20,14 @@ function getTransactionPostData(transaction: TransactionRequest): Record<string,
const result: Record<string, string> = { }; const result: Record<string, string> = { };
for (let key in transaction) { for (let key in transaction) {
if ((<any>transaction)[key] == null) { continue; } if ((<any>transaction)[key] == null) { continue; }
let value = hexlify((<any>transaction)[key]); let value = (<any>transaction)[key];
// Quantity-types require no leading zero, unless 0 // Quantity-types require no leading zero, unless 0
if ((<any>{ gasLimit: true, gasPrice: true, nonce: true, value: true })[key]) { if ((<any>{ type: true, gasLimit: true, gasPrice: true, nonce: true, value: true })[key]) {
value = hexValue(value); value = hexValue(hexlify(value));
} else if (key === "accessList") {
value = value;
} else {
value = hexlify(value);
} }
result[key] = value; result[key] = value;
} }
@ -293,6 +297,13 @@ export class EtherscanProvider extends BaseProvider{
case "call": { case "call": {
if (params.transaction.type != null) {
logger.throwError("Etherscan does not currently support Berlin", Logger.errors.UNSUPPORTED_OPERATION, {
operation: "call",
transaction: params.transaction
});
}
if (params.blockTag !== "latest") { if (params.blockTag !== "latest") {
throw new Error("EtherscanProvider does not support blockTag for call"); throw new Error("EtherscanProvider does not support blockTag for call");
} }
@ -310,6 +321,13 @@ export class EtherscanProvider extends BaseProvider{
} }
case "estimateGas": { case "estimateGas": {
if (params.transaction.type != null) {
logger.throwError("Etherscan does not currently support Berlin", Logger.errors.UNSUPPORTED_OPERATION, {
operation: "estimateGas",
transaction: params.transaction
});
}
const postData = getTransactionPostData(params.transaction); const postData = getTransactionPostData(params.transaction);
postData.module = "proxy"; postData.module = "proxy";
postData.action = "eth_estimateGas"; postData.action = "eth_estimateGas";

@ -6,7 +6,7 @@ import { BigNumber } from "@ethersproject/bignumber";
import { hexDataLength, hexDataSlice, hexValue, hexZeroPad, isHexString } from "@ethersproject/bytes"; import { hexDataLength, hexDataSlice, hexValue, hexZeroPad, isHexString } from "@ethersproject/bytes";
import { AddressZero } from "@ethersproject/constants"; import { AddressZero } from "@ethersproject/constants";
import { shallowCopy } from "@ethersproject/properties"; import { shallowCopy } from "@ethersproject/properties";
import { parse as parseTransaction } from "@ethersproject/transactions"; import { AccessList, accessListify, parse as parseTransaction } from "@ethersproject/transactions";
import { Logger } from "@ethersproject/logger"; import { Logger } from "@ethersproject/logger";
import { version } from "./_version"; import { version } from "./_version";
@ -51,6 +51,9 @@ export class Formatter {
formats.transaction = { formats.transaction = {
hash: hash, hash: hash,
type: Formatter.allowNull(number, null),
accessList: Formatter.allowNull(this.accessList.bind(this), null),
blockHash: Formatter.allowNull(hash, null), blockHash: Formatter.allowNull(hash, null),
blockNumber: Formatter.allowNull(number, null), blockNumber: Formatter.allowNull(number, null),
transactionIndex: Formatter.allowNull(number, null), transactionIndex: Formatter.allowNull(number, null),
@ -83,6 +86,8 @@ export class Formatter {
to: Formatter.allowNull(address), to: Formatter.allowNull(address),
value: Formatter.allowNull(bigNumber), value: Formatter.allowNull(bigNumber),
data: Formatter.allowNull(strictData), data: Formatter.allowNull(strictData),
type: Formatter.allowNull(number),
accessList: Formatter.allowNull(this.accessList.bind(this), null),
}; };
formats.receiptLog = { formats.receiptLog = {
@ -162,6 +167,10 @@ export class Formatter {
return formats; return formats;
} }
accessList(accessList: Array<any>): AccessList {
return accessListify(accessList || []);
}
// Requires a BigNumberish that is within the IEEE754 safe integer range; returns a number // Requires a BigNumberish that is within the IEEE754 safe integer range; returns a number
// Strict! Used on input. // Strict! Used on input.
number(number: any): number { number(number: any): number {
@ -308,28 +317,9 @@ export class Formatter {
transaction.creates = this.contractAddress(transaction); transaction.creates = this.contractAddress(transaction);
} }
// @TODO: use transaction.serialize? Have to add support for including v, r, and s... if (transaction.type === 1 && transaction.accessList == null) {
/* transaction.accessList = [ ];
if (!transaction.raw) {
// Very loose providers (e.g. TestRPC) do not provide a signature or raw
if (transaction.v && transaction.r && transaction.s) {
let raw = [
stripZeros(hexlify(transaction.nonce)),
stripZeros(hexlify(transaction.gasPrice)),
stripZeros(hexlify(transaction.gasLimit)),
(transaction.to || "0x"),
stripZeros(hexlify(transaction.value || "0x")),
hexlify(transaction.data || "0x"),
stripZeros(hexlify(transaction.v || "0x")),
stripZeros(hexlify(transaction.r)),
stripZeros(hexlify(transaction.s)),
];
transaction.raw = rlpEncode(raw);
}
} }
*/
const result: TransactionResponse = Formatter.check(this.formats.transaction, transaction); const result: TransactionResponse = Formatter.check(this.formats.transaction, transaction);

@ -10,6 +10,7 @@ import { _TypedDataEncoder } from "@ethersproject/hash";
import { Network, Networkish } from "@ethersproject/networks"; import { Network, Networkish } from "@ethersproject/networks";
import { checkProperties, deepCopy, Deferrable, defineReadOnly, getStatic, resolveProperties, shallowCopy } from "@ethersproject/properties"; import { checkProperties, deepCopy, Deferrable, defineReadOnly, getStatic, resolveProperties, shallowCopy } from "@ethersproject/properties";
import { toUtf8Bytes } from "@ethersproject/strings"; import { toUtf8Bytes } from "@ethersproject/strings";
import { AccessList, accessListify } from "@ethersproject/transactions";
import { ConnectionInfo, fetchJson, poll } from "@ethersproject/web"; import { ConnectionInfo, fetchJson, poll } from "@ethersproject/web";
import { Logger } from "@ethersproject/logger"; import { Logger } from "@ethersproject/logger";
@ -264,7 +265,8 @@ class UncheckedJsonRpcSigner extends JsonRpcSigner {
} }
const allowedTransactionKeys: { [ key: string ]: boolean } = { const allowedTransactionKeys: { [ key: string ]: boolean } = {
chainId: true, data: true, gasLimit: true, gasPrice:true, nonce: true, to: true, value: true chainId: true, data: true, gasLimit: true, gasPrice:true, nonce: true, to: true, value: true,
type: true, accessList: true
} }
export class JsonRpcProvider extends BaseProvider { export class JsonRpcProvider extends BaseProvider {
@ -529,7 +531,7 @@ export class JsonRpcProvider extends BaseProvider {
// before this is called // before this is called
// @TODO: This will likely be removed in future versions and prepareRequest // @TODO: This will likely be removed in future versions and prepareRequest
// will be the preferred method for this. // will be the preferred method for this.
static hexlifyTransaction(transaction: TransactionRequest, allowExtra?: { [key: string]: boolean }): { [key: string]: string } { static hexlifyTransaction(transaction: TransactionRequest, allowExtra?: { [key: string]: boolean }): { [key: string]: string | AccessList } {
// Check only allowed properties are given // Check only allowed properties are given
const allowed = shallowCopy(allowedTransactionKeys); const allowed = shallowCopy(allowedTransactionKeys);
if (allowExtra) { if (allowExtra) {
@ -537,12 +539,13 @@ export class JsonRpcProvider extends BaseProvider {
if (allowExtra[key]) { allowed[key] = true; } if (allowExtra[key]) { allowed[key] = true; }
} }
} }
checkProperties(transaction, allowed); checkProperties(transaction, allowed);
const result: { [key: string]: string } = {}; const result: { [key: string]: string | AccessList } = {};
// Some nodes (INFURA ropsten; INFURA mainnet is fine) do not like leading zeros. // Some nodes (INFURA ropsten; INFURA mainnet is fine) do not like leading zeros.
["gasLimit", "gasPrice", "nonce", "value"].forEach(function(key) { ["gasLimit", "gasPrice", "type", "nonce", "value"].forEach(function(key) {
if ((<any>transaction)[key] == null) { return; } if ((<any>transaction)[key] == null) { return; }
const value = hexValue((<any>transaction)[key]); const value = hexValue((<any>transaction)[key]);
if (key === "gasLimit") { key = "gas"; } if (key === "gasLimit") { key = "gas"; }
@ -554,6 +557,10 @@ export class JsonRpcProvider extends BaseProvider {
result[key] = hexlify((<any>transaction)[key]); result[key] = hexlify((<any>transaction)[key]);
}); });
if ((<any>transaction).accessList) {
result["accessList"] = accessListify((<any>transaction).accessList);
}
return result; return result;
} }
} }

@ -247,6 +247,58 @@ const blockchainData: { [ network: string ]: TestCases } = {
}, },
], ],
transactions: [ transactions: [
{
hash: "0x48bff7b0e603200118a672f7c622ab7d555a28f98938edb8318803eed7ea7395",
type: 1,
accessList: [
{
address: "0x0000000000000000000000000000000000000000",
storageKeys: []
}
],
blockHash: "0x378e24bcd568bd24cf1f54d38f13f038ee28d89e82af4f2a0d79c1f88dcd8aac",
blockNumber: 9812343,
from: "0x32162F3581E88a5f62e8A61892B42C46E2c18f7b",
gasPrice: bnify("0x65cf89a0"),
gasLimit: bnify("0x5b68"),
to: "0x32162F3581E88a5f62e8A61892B42C46E2c18f7b",
value: bnify("0"),
nonce: 13,
data: "0x",
r: "0x9659cba42376dbea1433cd6afc9c8ffa38dbeff5408ffdca0ebde6207281a3ec",
s: "0x27efbab3e6ed30b088ce0a50533364778e101c9e52acf318daec131da64e7758",
v: 0,
creates: null,
chainId: 3
},
{
hash: "0x1675a417e728fd3562d628d06955ef35b913573d9e417eb4e6a209998499c9d3",
type: 1,
accessList: [
{
address: "0x0000000000000000000000000000000000000000",
storageKeys: [
"0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
"0x0000000000111111111122222222223333333333444444444455555555556666",
"0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"
]
}
],
blockHash: "0x7565688256f5801768237993b47ca0608796b3ace0c4b8b6e623c6092bef14b8",
blockNumber: 9812365,
from: "0x32162F3581E88a5f62e8A61892B42C46E2c18f7b",
gasPrice: bnify("0x65cf89a0"),
gasLimit: bnify("0x71ac"),
to: "0x32162F3581E88a5f62e8A61892B42C46E2c18f7b",
value: bnify("0"),
nonce: 14,
data: "0x",
r: "0xb0646756f89817d70cdb40aa2ae8b5f43ef65d0926dcf71a7dca5280c93763df",
s: "0x4d32dbd9a44a2c5639b8434b823938202f75b0a8459f3fcd9f37b2495b7a66a6",
v: 0,
creates: null,
chainId: 3
}
], ],
transactionReceipts: [ transactionReceipts: [
{ {
@ -630,6 +682,9 @@ Object.keys(blockchainData).forEach((network) => {
tests.transactions.forEach((test) => { tests.transactions.forEach((test) => {
addObjectTest(`fetches transaction ${ test.hash }`, async (provider: ethers.providers.Provider) => { addObjectTest(`fetches transaction ${ test.hash }`, async (provider: ethers.providers.Provider) => {
const tx = await provider.getTransaction(test.hash); const tx = await provider.getTransaction(test.hash);
//console.log("TX");
//console.dir(test, { depth: null })
//console.dir(tx, { depth: null })
// This changes with every block // This changes with every block
assert.equal(typeof(tx.confirmations), "number", "confirmations is a number"); assert.equal(typeof(tx.confirmations), "number", "confirmations is a number");
@ -640,7 +695,12 @@ Object.keys(blockchainData).forEach((network) => {
return tx; return tx;
}, test, (provider: string, network: string, test: TestDescription) => { }, test, (provider: string, network: string, test: TestDescription) => {
return (provider === "EtherscanProvider"); if (network === "ropsten" && (provider === "AlchemyProvider" || provider === "PocketProvider")) {
console.log(`Skipping ${ provider }; incomplete Berlin support`);
return true;
}
return false; //(provider === "EtherscanProvider");
}); });
}); });
@ -721,6 +781,9 @@ testFunctions.push({
extras: [ "funding" ], // We need funding to the funWallet extras: [ "funding" ], // We need funding to the funWallet
timeout: 300, // 5 minutes timeout: 300, // 5 minutes
networks: [ "ropsten" ], // Only test on Ropsten networks: [ "ropsten" ], // Only test on Ropsten
checkSkip: (provider: string, network: string, test: TestDescription) => {
return (provider === "PocketProvider");
},
execute: async (provider: ethers.providers.Provider) => { execute: async (provider: ethers.providers.Provider) => {
const wallet = fundWallet.connect(provider); const wallet = fundWallet.connect(provider);
@ -741,6 +804,41 @@ testFunctions.push({
} }
}); });
testFunctions.push({
name: "sends an EIP-2930 transaction",
extras: [ "funding" ], // We need funding to the funWallet
timeout: 300, // 5 minutes
networks: [ "ropsten" ], // Only test on Ropsten
checkSkip: (provider: string, network: string, test: TestDescription) => {
return (provider === "PocketProvider" || provider === "EtherscanProvider" || provider === "AlchemyProvider");
},
execute: async (provider: ethers.providers.Provider) => {
const wallet = fundWallet.connect(provider);
const addr = "0x8210357f377E901f18E45294e86a2A32215Cc3C9";
const b0 = await provider.getBalance(wallet.address);
assert.ok(b0.gt(ethers.constants.Zero), "balance is non-zero");
const tx = await wallet.sendTransaction({
type: 1,
accessList: {
"0x8ba1f109551bD432803012645Ac136ddd64DBA72": [
"0x0000000000000000000000000000000000000000000000000000000000000000",
"0x0000000000000000000000000000000000000000000000000000000000000042",
]
},
to: addr,
value: 123
});
await tx.wait();
const b1 = await provider.getBalance(wallet.address);
assert.ok(b0.gt(b1), "balance is decreased");
}
});
describe("Test Provider Methods", function() { describe("Test Provider Methods", function() {
let fundReceipt: Promise<ethers.providers.TransactionReceipt> = null; let fundReceipt: Promise<ethers.providers.TransactionReceipt> = null;
const faucet = "0x8210357f377E901f18E45294e86a2A32215Cc3C9"; const faucet = "0x8210357f377E901f18E45294e86a2A32215Cc3C9";
@ -821,6 +919,9 @@ describe("Test Provider Methods", function() {
} catch (attemptError) { } catch (attemptError) {
console.log(`*** Failed attempt ${ attempt + 1 }: ${ attemptError.message }`); console.log(`*** Failed attempt ${ attempt + 1 }: ${ attemptError.message }`);
error = attemptError; error = attemptError;
// On failure, wait 5s
await waiter(5000);
} }
} }
throw error; throw error;

@ -787,3 +787,128 @@ describe("EIP-712", function() {
}); });
}); });
/*
type EIP2930Test = {
hash: string,
data:
};
*/
function _deepEquals(a: any, b: any, path: string): string {
if (Array.isArray(a)) {
if (!Array.isArray(b)) {
return `{ path }:!isArray(b)`;
}
if (a.length !== b.length) {
return `{ path }:a.length[${ a.length }]!=b.length[${ b.length }]`;
}
for (let i = 0; i < a.length; i++) {
const reason = _deepEquals(a[i], b[i], `${ path }:${ i }`);
if (reason != null) { return reason; }
}
return null;
}
if (a.eq) {
if (!b.eq) { return `${ path }:typeof(b)!=BigNumber`; }
return a.eq(b) ? null: `${ path }:!a.eq(b)`;
}
if (a != null && typeof(a) === "object") {
if (b != null && typeof(b) !== "object") { return `${ path }:typeof(b)!=object`; }
const keys = Object.keys(a), otherKeys = Object.keys(b);
keys.sort();
otherKeys.sort();
if (keys.length !== otherKeys.length) { return `${ path }:keys(a)[${ keys.join(",") }]!=keys(b)[${ otherKeys.join(",") }]`; }
for (const key in a) {
const reason = _deepEquals(a[key], b[key], `${ path }:${ key }`);
if (reason != null) { return reason; }
}
return null;
}
if (a !== b) { return `${ path }[${ a } != ${ b }]`; }
return null;
}
function deepEquals(a: any, b: any): string {
return _deepEquals(a, b, "");
}
describe("EIP-2930", function() {
const Tests = [
{
hash: "0x48bff7b0e603200118a672f7c622ab7d555a28f98938edb8318803eed7ea7395",
data: "0x01f87c030d8465cf89a0825b689432162f3581e88a5f62e8a61892b42c46e2c18f7b8080d7d6940000000000000000000000000000000000000000c080a09659cba42376dbea1433cd6afc9c8ffa38dbeff5408ffdca0ebde6207281a3eca027efbab3e6ed30b088ce0a50533364778e101c9e52acf318daec131da64e7758",
preimage: "0x01f839030d8465cf89a0825b689432162f3581e88a5f62e8a61892b42c46e2c18f7b8080d7d6940000000000000000000000000000000000000000c0",
tx: {
hash: "0x48bff7b0e603200118a672f7c622ab7d555a28f98938edb8318803eed7ea7395",
type: 1,
chainId: 3,
nonce: 13,
gasPrice: ethers.BigNumber.from("0x65cf89a0"),
gasLimit: ethers.BigNumber.from("0x5b68"),
to: "0x32162F3581E88a5f62e8A61892B42C46E2c18f7b",
value: ethers.BigNumber.from("0"),
data: "0x",
accessList: [
{
address: "0x0000000000000000000000000000000000000000",
storageKeys: []
}
],
v: 0,
r: "0x9659cba42376dbea1433cd6afc9c8ffa38dbeff5408ffdca0ebde6207281a3ec",
s: "0x27efbab3e6ed30b088ce0a50533364778e101c9e52acf318daec131da64e7758",
from: "0x32162F3581E88a5f62e8A61892B42C46E2c18f7b",
}
},
{
hash: "0x1675a417e728fd3562d628d06955ef35b913573d9e417eb4e6a209998499c9d3",
data: "0x01f8e2030e8465cf89a08271ac9432162f3581e88a5f62e8a61892b42c46e2c18f7b8080f87cf87a940000000000000000000000000000000000000000f863a0deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefa00000000000111111111122222222223333333333444444444455555555556666a0deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef80a0b0646756f89817d70cdb40aa2ae8b5f43ef65d0926dcf71a7dca5280c93763dfa04d32dbd9a44a2c5639b8434b823938202f75b0a8459f3fcd9f37b2495b7a66a6",
preimage: "0x01f89f030e8465cf89a08271ac9432162f3581e88a5f62e8a61892b42c46e2c18f7b8080f87cf87a940000000000000000000000000000000000000000f863a0deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefa00000000000111111111122222222223333333333444444444455555555556666a0deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
tx: {
hash: "0x1675a417e728fd3562d628d06955ef35b913573d9e417eb4e6a209998499c9d3",
type: 1,
chainId: 3,
nonce: 14,
gasPrice: ethers.BigNumber.from("0x65cf89a0"),
gasLimit: ethers.BigNumber.from("0x71ac"),
to: "0x32162F3581E88a5f62e8A61892B42C46E2c18f7b",
value: ethers.BigNumber.from("0"),
data: "0x",
accessList: [
{
address: "0x0000000000000000000000000000000000000000",
storageKeys: [
"0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
"0x0000000000111111111122222222223333333333444444444455555555556666",
"0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"
]
}
],
v: 0,
r: "0xb0646756f89817d70cdb40aa2ae8b5f43ef65d0926dcf71a7dca5280c93763df",
s: "0x4d32dbd9a44a2c5639b8434b823938202f75b0a8459f3fcd9f37b2495b7a66a6",
from: "0x32162F3581E88a5f62e8A61892B42C46E2c18f7b",
}
},
];
Tests.forEach((test) => {
it(`tx:${ test.hash }`, function() {
const tx = ethers.utils.parseTransaction(test.data);
assert.equal(tx.hash, test.hash);
const reason = deepEquals(tx, test.tx);
assert.ok(reason == null, reason);
const preimageData = ethers.utils.serializeTransaction(<any>(test.tx));
assert.equal(preimageData, test.preimage);
const data = ethers.utils.serializeTransaction(<any>(test.tx), test.tx);
assert.equal(data, test.data);
});
});
});

@ -2,7 +2,7 @@
import { getAddress } from "@ethersproject/address"; import { getAddress } from "@ethersproject/address";
import { BigNumber, BigNumberish } from "@ethersproject/bignumber"; import { BigNumber, BigNumberish } from "@ethersproject/bignumber";
import { arrayify, BytesLike, DataOptions, hexDataSlice, hexlify, hexZeroPad, isBytesLike, SignatureLike, splitSignature, stripZeros, } from "@ethersproject/bytes"; import { arrayify, BytesLike, DataOptions, hexConcat, hexDataLength, hexDataSlice, hexlify, hexZeroPad, isBytesLike, SignatureLike, splitSignature, stripZeros, } from "@ethersproject/bytes";
import { Zero } from "@ethersproject/constants"; import { Zero } from "@ethersproject/constants";
import { keccak256 } from "@ethersproject/keccak256"; import { keccak256 } from "@ethersproject/keccak256";
import { checkProperties } from "@ethersproject/properties"; import { checkProperties } from "@ethersproject/properties";
@ -16,6 +16,13 @@ const logger = new Logger(version);
/////////////////////////////// ///////////////////////////////
// Exported Types // Exported Types
export type AccessList = Array<{ address: string, storageKeys: Array<string> }>;
// Input allows flexibility in describing an access list
export type AccessListish = AccessList |
Array<[ string, Array<string> ]> |
Record<string, Array<string>>;
export type UnsignedTransaction = { export type UnsignedTransaction = {
to?: string; to?: string;
nonce?: number; nonce?: number;
@ -26,11 +33,14 @@ export type UnsignedTransaction = {
data?: BytesLike; data?: BytesLike;
value?: BigNumberish; value?: BigNumberish;
chainId?: number; chainId?: number;
// Typed-Transaction features
type?: number | null;
accessList?: AccessListish;
} }
export interface Transaction { export interface Transaction {
hash?: string; hash?: string;
type?: number | null;
to?: string; to?: string;
from?: string; from?: string;
@ -46,6 +56,12 @@ export interface Transaction {
r?: string; r?: string;
s?: string; s?: string;
v?: number; v?: number;
// Typed-Transaction features
type?: number | null;
// EIP-2930; Type 1
accessList?: AccessList;
} }
/////////////////////////////// ///////////////////////////////
@ -60,6 +76,7 @@ function handleNumber(value: string): BigNumber {
return BigNumber.from(value); return BigNumber.from(value);
} }
// Legacy Transaction Fields
const transactionFields = [ const transactionFields = [
{ name: "nonce", maxLength: 32, numeric: true }, { name: "nonce", maxLength: 32, numeric: true },
{ name: "gasPrice", maxLength: 32, numeric: true }, { name: "gasPrice", maxLength: 32, numeric: true },
@ -82,8 +99,78 @@ export function recoverAddress(digest: BytesLike, signature: SignatureLike): str
return computeAddress(recoverPublicKey(arrayify(digest), signature)); return computeAddress(recoverPublicKey(arrayify(digest), signature));
} }
function formatNumber(value: BigNumberish, name: string): Uint8Array {
const result = stripZeros(BigNumber.from(value).toHexString());
if (result.length > 32) {
logger.throwArgumentError("invalid length for " + name, ("transaction:" + name), value);
}
return result;
}
export function serialize(transaction: UnsignedTransaction, signature?: SignatureLike): string { function accessSetify(addr: string, storageKeys: Array<string>): { address: string,storageKeys: Array<string> } {
return {
address: getAddress(addr),
storageKeys: (storageKeys || []).map((storageKey, index) => {
if (hexDataLength(storageKey) !== 32) {
logger.throwArgumentError("invalid access list storageKey", `accessList[${ addr }:${ index }]`, storageKey)
}
return storageKey.toLowerCase();
})
};
}
export function accessListify(value: AccessListish): AccessList {
if (Array.isArray(value)) {
return (<Array<[ string, Array<string>] | { address: string, storageKeys: Array<string>}>>value).map((set, index) => {
if (Array.isArray(set)) {
if (set.length > 2) {
logger.throwArgumentError("access list expected to be [ address, storageKeys[] ]", `value[${ index }]`, set);
}
return accessSetify(set[0], set[1])
}
return accessSetify(set.address, set.storageKeys);
});
}
const result: Array<{ address: string, storageKeys: Array<string> }> = Object.keys(value).map((addr) => {
const storageKeys: Record<string, true> = value[addr].reduce((accum, storageKey) => {
accum[storageKey] = true;
return accum;
}, <Record<string, true>>{ });
return accessSetify(addr, Object.keys(storageKeys).sort())
});
result.sort((a, b) => (a.address.localeCompare(b.address)));
return result;
}
function formatAccessList(value: AccessListish): Array<[ string, Array<string> ]> {
return accessListify(value).map((set) => [ set.address, set.storageKeys ]);
}
function _serializeEip2930(transaction: UnsignedTransaction, signature?: SignatureLike): string {
const fields: any = [
formatNumber(transaction.chainId || 0, "chainId"),
formatNumber(transaction.nonce || 0, "nonce"),
formatNumber(transaction.gasPrice || 0, "gasPrice"),
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([ "0x01", RLP.encode(fields)]);
}
// Legacy Transactions and EIP-155
function _serialize(transaction: UnsignedTransaction, signature?: SignatureLike): string {
checkProperties(transaction, allowedTransactionKeys); checkProperties(transaction, allowedTransactionKeys);
const raw: Array<string | Uint8Array> = []; const raw: Array<string | Uint8Array> = [];
@ -163,6 +250,69 @@ export function serialize(transaction: UnsignedTransaction, signature?: Signatur
return RLP.encode(raw); return RLP.encode(raw);
} }
export function serialize(transaction: UnsignedTransaction, signature?: SignatureLike): string {
// Legacy and EIP-155 Transactions
if (transaction.type == null) { return _serialize(transaction, signature); }
// Typed Transactions (EIP-2718)
switch (transaction.type) {
case 1:
return _serializeEip2930(transaction, signature);
default:
break;
}
return logger.throwError(`unsupported transaction type: ${ transaction.type }`, Logger.errors.UNSUPPORTED_OPERATION, {
operation: "serializeTransaction",
transactionType: transaction.type
});
}
function _parseEip2930(payload: Uint8Array): Transaction {
const transaction = RLP.decode(payload.slice(1));
if (transaction.length !== 8 && transaction.length !== 11) {
logger.throwArgumentError("invalid component count for transaction type: 1", "payload", hexlify(payload));
}
const tx: Transaction = {
type: 1,
chainId: handleNumber(transaction[0]).toNumber(),
nonce: handleNumber(transaction[1]).toNumber(),
gasPrice: handleNumber(transaction[2]),
gasLimit: handleNumber(transaction[3]),
to: handleAddress(transaction[4]),
value: handleNumber(transaction[5]),
data: transaction[6],
accessList: accessListify(transaction[7]),
};
// 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);
return tx;
}
// Legacy Transactions and EIP-155
function _parse(rawTransaction: Uint8Array): Transaction { function _parse(rawTransaction: Uint8Array): Transaction {
const transaction = RLP.decode(rawTransaction); const transaction = RLP.decode(rawTransaction);
@ -231,9 +381,21 @@ function _parse(rawTransaction: Uint8Array): Transaction {
return tx; return tx;
} }
export function parse(rawTransaction: BytesLike): Transaction { export function parse(rawTransaction: BytesLike): Transaction {
const payload = arrayify(rawTransaction); const payload = arrayify(rawTransaction);
// Legacy and EIP-155 Transactions
if (payload[0] > 0x7f) { return _parse(payload); } if (payload[0] > 0x7f) { return _parse(payload); }
// Typed Transaction (EIP-2718)
switch (payload[0]) {
case 1:
return _parseEip2930(payload);
default:
break;
}
return logger.throwError(`unsupported transaction type: ${ payload[0] }`, Logger.errors.UNSUPPORTED_OPERATION, { return logger.throwError(`unsupported transaction type: ${ payload[0] }`, Logger.errors.UNSUPPORTED_OPERATION, {
operation: "parseTransaction", operation: "parseTransaction",
transactionType: payload[0] transactionType: payload[0]