Added EIP-2930 support (#1364).
This commit is contained in:
parent
1db4ce12d4
commit
c47d2eba4d
@ -4,7 +4,7 @@ import { BigNumber, BigNumberish } from "@ethersproject/bignumber";
|
||||
import { BytesLike, isHexString } from "@ethersproject/bytes";
|
||||
import { Network } from "@ethersproject/networks";
|
||||
import { Deferrable, Description, defineReadOnly } from "@ethersproject/properties";
|
||||
import { Transaction } from "@ethersproject/transactions";
|
||||
import { AccessListish, Transaction } from "@ethersproject/transactions";
|
||||
import { OnceBlockable } from "@ethersproject/web";
|
||||
|
||||
import { Logger } from "@ethersproject/logger";
|
||||
@ -26,6 +26,9 @@ export type TransactionRequest = {
|
||||
data?: BytesLike,
|
||||
value?: BigNumberish,
|
||||
chainId?: number
|
||||
|
||||
type?: number;
|
||||
accessList?: AccessListish;
|
||||
}
|
||||
|
||||
export interface TransactionResponse extends Transaction {
|
||||
|
@ -10,7 +10,7 @@ import { version } from "./_version";
|
||||
const logger = new Logger(version);
|
||||
|
||||
const allowedTransactionKeys: Array<string> = [
|
||||
"chainId", "data", "from", "gasLimit", "gasPrice", "nonce", "to", "value"
|
||||
"accessList", "chainId", "data", "from", "gasLimit", "gasPrice", "nonce", "to", "type", "value"
|
||||
];
|
||||
|
||||
const forwardErrors = [
|
||||
|
@ -390,6 +390,8 @@ export function splitSignature(signature: SignatureLike): Signature {
|
||||
if (result.recoveryParam == null) {
|
||||
if (result.v == null) {
|
||||
logger.throwArgumentError("signature missing v and recoveryParam", "signature", signature);
|
||||
} else if (result.v === 0 || result.v === 1) {
|
||||
result.recoveryParam = result.v;
|
||||
} else {
|
||||
result.recoveryParam = 1 - (result.v % 2);
|
||||
}
|
||||
|
@ -9,7 +9,8 @@
|
||||
"@ethersproject/bytes": "^5.0.9",
|
||||
"@ethersproject/constants": "^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.",
|
||||
"ethereum": "donations.ethers.eth",
|
||||
|
@ -7,7 +7,7 @@ import { getAddress, getContractAddress } from "@ethersproject/address";
|
||||
import { BigNumber, BigNumberish } from "@ethersproject/bignumber";
|
||||
import { arrayify, BytesLike, concat, hexlify, isBytes, isHexString } from "@ethersproject/bytes";
|
||||
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 { version } from "./_version";
|
||||
@ -18,6 +18,8 @@ export interface Overrides {
|
||||
gasLimit?: BigNumberish | Promise<BigNumberish>;
|
||||
gasPrice?: BigNumberish | Promise<BigNumberish>;
|
||||
nonce?: BigNumberish | Promise<BigNumberish>;
|
||||
type?: number;
|
||||
accessList?: AccessListish;
|
||||
};
|
||||
|
||||
export interface PayableOverrides extends Overrides {
|
||||
@ -45,6 +47,9 @@ export interface PopulatedTransaction {
|
||||
data?: string;
|
||||
value?: BigNumber;
|
||||
chainId?: number;
|
||||
|
||||
type?: number;
|
||||
accessList?: AccessList;
|
||||
};
|
||||
|
||||
export type EventFilter = {
|
||||
@ -94,7 +99,8 @@ export interface ContractTransaction extends TransactionResponse {
|
||||
///////////////////////////////
|
||||
|
||||
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> {
|
||||
@ -212,6 +218,9 @@ async function populateTransaction(contract: Contract, fragment: FunctionFragmen
|
||||
if (ro.gasPrice != null) { tx.gasPrice = BigNumber.from(ro.gasPrice); }
|
||||
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 (tx.gasLimit == null && fragment.gas != null) {
|
||||
// Conmpute the intrinisic gas cost for this transaction
|
||||
@ -247,6 +256,9 @@ async function populateTransaction(contract: Contract, fragment: FunctionFragmen
|
||||
delete overrides.from;
|
||||
delete overrides.value;
|
||||
|
||||
delete overrides.type;
|
||||
delete overrides.accessList;
|
||||
|
||||
// Make sure there are no stray overrides, which may indicate a
|
||||
// typo or using an unsupported key.
|
||||
const leftovers = Object.keys(overrides).filter((key) => ((<any>overrides)[key] != null));
|
||||
|
@ -39,14 +39,30 @@ function ethDefaultProvider(network: string | Network): Renetworkable {
|
||||
}
|
||||
|
||||
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 {
|
||||
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) { }
|
||||
}
|
||||
|
||||
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 {
|
||||
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) { }
|
||||
}
|
||||
|
||||
|
@ -1122,6 +1122,15 @@ export class BaseProvider extends Provider implements EnsProvider {
|
||||
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) => {
|
||||
if (values[key] == null) { return; }
|
||||
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({
|
||||
transaction: this._getTransactionRequest(transaction)
|
||||
});
|
||||
|
||||
const result = await this.perform("estimateGas", params);
|
||||
try {
|
||||
return BigNumber.from(result);
|
||||
|
@ -20,10 +20,14 @@ function getTransactionPostData(transaction: TransactionRequest): Record<string,
|
||||
const result: Record<string, string> = { };
|
||||
for (let key in transaction) {
|
||||
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
|
||||
if ((<any>{ gasLimit: true, gasPrice: true, nonce: true, value: true })[key]) {
|
||||
value = hexValue(value);
|
||||
if ((<any>{ type: true, gasLimit: true, gasPrice: true, nonce: true, value: true })[key]) {
|
||||
value = hexValue(hexlify(value));
|
||||
} else if (key === "accessList") {
|
||||
value = value;
|
||||
} else {
|
||||
value = hexlify(value);
|
||||
}
|
||||
result[key] = value;
|
||||
}
|
||||
@ -293,6 +297,13 @@ export class EtherscanProvider extends BaseProvider{
|
||||
|
||||
|
||||
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") {
|
||||
throw new Error("EtherscanProvider does not support blockTag for call");
|
||||
}
|
||||
@ -310,6 +321,13 @@ export class EtherscanProvider extends BaseProvider{
|
||||
}
|
||||
|
||||
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);
|
||||
postData.module = "proxy";
|
||||
postData.action = "eth_estimateGas";
|
||||
|
@ -6,7 +6,7 @@ import { BigNumber } from "@ethersproject/bignumber";
|
||||
import { hexDataLength, hexDataSlice, hexValue, hexZeroPad, isHexString } from "@ethersproject/bytes";
|
||||
import { AddressZero } from "@ethersproject/constants";
|
||||
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 { version } from "./_version";
|
||||
@ -51,6 +51,9 @@ export class Formatter {
|
||||
formats.transaction = {
|
||||
hash: hash,
|
||||
|
||||
type: Formatter.allowNull(number, null),
|
||||
accessList: Formatter.allowNull(this.accessList.bind(this), null),
|
||||
|
||||
blockHash: Formatter.allowNull(hash, null),
|
||||
blockNumber: Formatter.allowNull(number, null),
|
||||
transactionIndex: Formatter.allowNull(number, null),
|
||||
@ -83,6 +86,8 @@ export class Formatter {
|
||||
to: Formatter.allowNull(address),
|
||||
value: Formatter.allowNull(bigNumber),
|
||||
data: Formatter.allowNull(strictData),
|
||||
type: Formatter.allowNull(number),
|
||||
accessList: Formatter.allowNull(this.accessList.bind(this), null),
|
||||
};
|
||||
|
||||
formats.receiptLog = {
|
||||
@ -162,6 +167,10 @@ export class Formatter {
|
||||
return formats;
|
||||
}
|
||||
|
||||
accessList(accessList: Array<any>): AccessList {
|
||||
return accessListify(accessList || []);
|
||||
}
|
||||
|
||||
// Requires a BigNumberish that is within the IEEE754 safe integer range; returns a number
|
||||
// Strict! Used on input.
|
||||
number(number: any): number {
|
||||
@ -308,28 +317,9 @@ export class Formatter {
|
||||
transaction.creates = this.contractAddress(transaction);
|
||||
}
|
||||
|
||||
// @TODO: use transaction.serialize? Have to add support for including v, r, and s...
|
||||
/*
|
||||
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);
|
||||
if (transaction.type === 1 && transaction.accessList == null) {
|
||||
transaction.accessList = [ ];
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
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 { checkProperties, deepCopy, Deferrable, defineReadOnly, getStatic, resolveProperties, shallowCopy } from "@ethersproject/properties";
|
||||
import { toUtf8Bytes } from "@ethersproject/strings";
|
||||
import { AccessList, accessListify } from "@ethersproject/transactions";
|
||||
import { ConnectionInfo, fetchJson, poll } from "@ethersproject/web";
|
||||
|
||||
import { Logger } from "@ethersproject/logger";
|
||||
@ -264,7 +265,8 @@ class UncheckedJsonRpcSigner extends JsonRpcSigner {
|
||||
}
|
||||
|
||||
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 {
|
||||
@ -529,7 +531,7 @@ export class JsonRpcProvider extends BaseProvider {
|
||||
// before this is called
|
||||
// @TODO: This will likely be removed in future versions and prepareRequest
|
||||
// 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
|
||||
const allowed = shallowCopy(allowedTransactionKeys);
|
||||
if (allowExtra) {
|
||||
@ -537,12 +539,13 @@ export class JsonRpcProvider extends BaseProvider {
|
||||
if (allowExtra[key]) { allowed[key] = true; }
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
["gasLimit", "gasPrice", "nonce", "value"].forEach(function(key) {
|
||||
["gasLimit", "gasPrice", "type", "nonce", "value"].forEach(function(key) {
|
||||
if ((<any>transaction)[key] == null) { return; }
|
||||
const value = hexValue((<any>transaction)[key]);
|
||||
if (key === "gasLimit") { key = "gas"; }
|
||||
@ -554,6 +557,10 @@ export class JsonRpcProvider extends BaseProvider {
|
||||
result[key] = hexlify((<any>transaction)[key]);
|
||||
});
|
||||
|
||||
if ((<any>transaction).accessList) {
|
||||
result["accessList"] = accessListify((<any>transaction).accessList);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
@ -247,6 +247,58 @@ const blockchainData: { [ network: string ]: TestCases } = {
|
||||
},
|
||||
],
|
||||
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: [
|
||||
{
|
||||
@ -630,6 +682,9 @@ Object.keys(blockchainData).forEach((network) => {
|
||||
tests.transactions.forEach((test) => {
|
||||
addObjectTest(`fetches transaction ${ test.hash }`, async (provider: ethers.providers.Provider) => {
|
||||
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
|
||||
assert.equal(typeof(tx.confirmations), "number", "confirmations is a number");
|
||||
@ -640,7 +695,12 @@ Object.keys(blockchainData).forEach((network) => {
|
||||
|
||||
return tx;
|
||||
}, 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
|
||||
timeout: 300, // 5 minutes
|
||||
networks: [ "ropsten" ], // Only test on Ropsten
|
||||
checkSkip: (provider: string, network: string, test: TestDescription) => {
|
||||
return (provider === "PocketProvider");
|
||||
},
|
||||
execute: async (provider: ethers.providers.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() {
|
||||
let fundReceipt: Promise<ethers.providers.TransactionReceipt> = null;
|
||||
const faucet = "0x8210357f377E901f18E45294e86a2A32215Cc3C9";
|
||||
@ -821,6 +919,9 @@ describe("Test Provider Methods", function() {
|
||||
} catch (attemptError) {
|
||||
console.log(`*** Failed attempt ${ attempt + 1 }: ${ attemptError.message }`);
|
||||
error = attemptError;
|
||||
|
||||
// On failure, wait 5s
|
||||
await waiter(5000);
|
||||
}
|
||||
}
|
||||
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 { 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 { keccak256 } from "@ethersproject/keccak256";
|
||||
import { checkProperties } from "@ethersproject/properties";
|
||||
@ -16,6 +16,13 @@ const logger = new Logger(version);
|
||||
///////////////////////////////
|
||||
// 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 = {
|
||||
to?: string;
|
||||
nonce?: number;
|
||||
@ -26,11 +33,14 @@ export type UnsignedTransaction = {
|
||||
data?: BytesLike;
|
||||
value?: BigNumberish;
|
||||
chainId?: number;
|
||||
|
||||
// Typed-Transaction features
|
||||
type?: number | null;
|
||||
accessList?: AccessListish;
|
||||
}
|
||||
|
||||
export interface Transaction {
|
||||
hash?: string;
|
||||
type?: number | null;
|
||||
|
||||
to?: string;
|
||||
from?: string;
|
||||
@ -46,6 +56,12 @@ export interface Transaction {
|
||||
r?: string;
|
||||
s?: string;
|
||||
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);
|
||||
}
|
||||
|
||||
// Legacy Transaction Fields
|
||||
const transactionFields = [
|
||||
{ name: "nonce", 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));
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
const raw: Array<string | Uint8Array> = [];
|
||||
@ -163,6 +250,69 @@ export function serialize(transaction: UnsignedTransaction, signature?: Signatur
|
||||
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 {
|
||||
const transaction = RLP.decode(rawTransaction);
|
||||
|
||||
@ -231,9 +381,21 @@ function _parse(rawTransaction: Uint8Array): Transaction {
|
||||
return tx;
|
||||
}
|
||||
|
||||
|
||||
export function parse(rawTransaction: BytesLike): Transaction {
|
||||
const payload = arrayify(rawTransaction);
|
||||
|
||||
// Legacy and EIP-155 Transactions
|
||||
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, {
|
||||
operation: "parseTransaction",
|
||||
transactionType: payload[0]
|
||||
|
Loading…
Reference in New Issue
Block a user