diff --git a/src/index.ts b/src/index.ts index facad99..da574cb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,57 +1,51 @@ /*! micro-eth-signer - MIT License (c) 2021 Paul Miller (paulmillr.com) */ import { secp256k1 } from '@noble/curves/secp256k1'; import { keccak_256 } from '@noble/hashes/sha3'; -import { bytesToHex } from '@noble/hashes/utils'; +import { bytesToHex, concatBytes } from '@noble/hashes/utils'; import { UnwrapCoder } from 'micro-packed'; import { addr } from './address.js'; // prettier-ignore import { TxType, TxVersions, TxCoder, RawTx, decodeLegacyV, removeSig, sortRawData, validateFields, + AuthorizationItem, AuthorizationRequest, authorizationRequest } from './tx.js'; +import { RLP } from './rlp.js'; // prettier-ignore import { - amounts, astr, add0x, ethHex, ethHexNoLeadingZero, strip0x, weieth, weigwei, cloneDeep, + amounts, astr, ethHex, ethHexNoLeadingZero, strip0x, weieth, weigwei, cloneDeep, } from './utils.js'; export { addr, weigwei, weieth }; // The file exports Transaction, but actual (RLP) parsing logic is done in `./tx` /** - * Basic message signing & verification. Matches ethers and etherscan behavior. - * TODO: research whether EIP-191 and EIP-712 are popular, add them. + * EIP-7702 Authorizations */ -export const messenger = { - sign(msg: string, privateKey: string, extraEntropy = undefined) { - astr(msg); +export const authorization = { + _getHash(req: AuthorizationRequest) { + const msg = RLP.encode(authorizationRequest.decode(req)); + return keccak_256(concatBytes(new Uint8Array([0x05]), msg)); + }, + sign(req: AuthorizationRequest, privateKey: string): AuthorizationItem { astr(privateKey); - const hash = keccak_256(msg); - const sig = secp256k1.sign(hash, ethHex.decode(privateKey), { extraEntropy }); - const end = sig.recovery === 0 ? '1b' : '1c'; - return add0x(sig.toCompactHex() + end); + const sig = secp256k1.sign(this._getHash(req), ethHex.decode(privateKey)); + return { ...req, r: sig.r, s: sig.s, yParity: sig.recovery }; }, - verify(signature: string, msg: string, address: string) { - astr(signature); - astr(msg); - astr(address); - signature = strip0x(signature); - if (signature.length !== 65 * 2) throw new Error('invalid signature length'); - const sigh = signature.slice(0, -2); - const end = signature.slice(-2); - if (!['1b', '1c'].includes(end)) throw new Error('invalid recovery bit'); - const sig = secp256k1.Signature.fromCompact(sigh).addRecoveryBit(end === '1b' ? 0 : 1); - const hash = keccak_256(msg); - const pub = sig.recoverPublicKey(hash).toHex(false); - const recoveredAddr = addr.fromPublicKey(pub); - return recoveredAddr === address && secp256k1.verify(sig, hash, pub); + getAuthority(item: AuthorizationItem) { + const { r, s, yParity, ...req } = item; + const hash = this._getHash(req); + const sig = new secp256k1.Signature(r, s).addRecoveryBit(yParity); + const point = sig.recoverPublicKey(hash); + return addr.fromPublicKey(point.toHex(false)); }, }; - // Transaction-related utils. // 4 fields are required. Others are pre-filled with default values. const DEFAULTS = { accessList: [], // needs to be .slice()-d to create new reference + authorizationList: [], chainId: 1n, // mainnet data: '', gasLimit: 21000n, // TODO: investigate if limit is smaller in eip4844 txs @@ -128,7 +122,7 @@ export class Transaction { for (const f in DEFAULTS) { if (f !== 'type' && fields.has(f)) { raw[f] = DEFAULTS[f as DefaultField]; - if (f === 'accessList') raw[f] = cloneDeep(raw[f]); + if (['accessList', 'authorizationList'].includes(f)) raw[f] = cloneDeep(raw[f]); } } // Copy all fields, so we can validate unexpected ones. diff --git a/src/net/archive.ts b/src/net/archive.ts index 02dc065..58f5037 100644 --- a/src/net/archive.ts +++ b/src/net/archive.ts @@ -1,6 +1,6 @@ import { IWeb3Provider, Web3CallArgs, hexToNumber, amounts } from '../utils.js'; import { Transaction } from '../index.js'; -import { TxVersions, legacySig } from '../tx.js'; +import { TxVersions, legacySig, AccessList } from '../tx.js'; import { ContractInfo, createContract, events, ERC20, WETH } from '../abi/index.js'; /* @@ -104,7 +104,7 @@ export type TxInfo = { blockHash: string; blockNumber: number; hash: string; - accessList?: [string, string[]][]; + accessList?: AccessList; transactionIndex: number; type: number; nonce: bigint; @@ -263,9 +263,6 @@ function fixTxInfo(info: TxInfo) { ] as const) { if (info[i] !== undefined && info[i] !== null) info[i] = BigInt(info[i]!); } - // Same API as Transaction, so we can re-create easily - if (info.accessList) - info.accessList = info.accessList.map((i: any) => [i.address, i.storageKeys]); return info; } diff --git a/src/tx.ts b/src/tx.ts index ae611be..bdee6da 100644 --- a/src/tx.ts +++ b/src/tx.ts @@ -1,7 +1,7 @@ import * as P from 'micro-packed'; import { addr } from './address.js'; import { RLP } from './rlp.js'; -import { isBytes, amounts, ethHex } from './utils.js'; +import { isObject, amounts, ethHex, isBytes } from './utils.js'; // Transaction parsers @@ -64,43 +64,15 @@ function assertYParityValid(elm: number) { } // We don't know chainId when specific field coded yet. const addrCoder = ethHex; - -// Parses eip2930 access lists: -// ["0xde0b295669a9fd93d5f28d9ec85e40f4cb697bae", [ -// "0x0000000000000000000000000000000000000000000000000000000000000003", -// "0x0000000000000000000000000000000000000000000000000000000000000007" -// ]] -export type AccessList = [string, string[]][]; -export type BytesAccessList = [Uint8Array, Uint8Array[]][]; -function accessListParser(coder: (a: T) => K, mapper: (a: T) => K) { - return (data: [T, T[]][]) => { - if (!Array.isArray(data)) throw new Error('access list must be an array'); - return data.map((pair) => { - if (!Array.isArray(pair) || pair.length !== 2) - throw new Error('access list must have 2 elements'); - return [coder(pair[0]), pair[1].map(mapper)]; - }); - }; -} - -// Parses eip4844 blobs: -// ["0x0000000000000000000000000000000000000000000000000000000000000003"...] -function blobParser(fn: (item: T) => K) { - return (data: T[]) => { - if (!Array.isArray(data)) throw new Error('blobVersionedHashes must be an array'); - return data.map(fn); - }; -} - -function ensure32(b: any & { length: number }): T { - if (b.length !== 32) throw new Error('slot must be 32 bytes'); +// Bytes32: VersionedHash, AccessListKey +function ensure32(b: Uint8Array): Uint8Array { + if (!isBytes(b) || b.length !== 32) throw new Error('expected 32 bytes'); return b; } -function ensureBlob(hash: Uint8Array): Uint8Array { - if (!isBytes(hash) || hash.length !== 32) - throw new Error('blobVersionedHashes must contain 32-byte Uint8Array-s'); - return hash; -} +const Bytes32: P.Coder = { + encode: (from) => ethHex.encode(ensure32(from)), + decode: (to) => ensure32(ethHex.decode(to)), +}; type VRS = Partial<{ v: bigint; r: bigint; s: bigint }>; type YRS = Partial<{ chainId: bigint; yParity: number; r: bigint; s: bigint }>; @@ -155,6 +127,72 @@ export const legacySig = { const U64BE = P.coders.reverse(P.bigint(8, false, false, false)); const U256BE = P.coders.reverse(P.bigint(32, false, false, false)); + +// Small coder utils +// TODO: seems generic enought for packed? or RLP (seems useful for structured encoding/decoding of RLP stuff) +// Basic array coder +const array = (coder: P.Coder): P.Coder => ({ + encode(from: F[]) { + if (!Array.isArray(from)) throw new Error('expected array'); + return from.map((i) => coder.encode(i)); + }, + decode(to: T[]) { + if (!Array.isArray(to)) throw new Error('expected array'); + return to.map((i) => coder.decode(i)); + }, +}); +// tuple -> struct +const struct = < + Fields extends Record>, + FromTuple extends { + [K in keyof Fields]: Fields[K] extends P.Coder ? F : never; + }[keyof Fields][], + ToObject extends { [K in keyof Fields]: Fields[K] extends P.Coder ? T : never }, +>( + fields: Fields +): P.Coder => ({ + encode(from: FromTuple) { + if (!Array.isArray(from)) throw new Error('expected array'); + const fNames = Object.keys(fields); + if (from.length !== fNames.length) throw new Error('wrong array length'); + return Object.fromEntries(fNames.map((f, i) => [f, fields[f].encode(from[i])])) as ToObject; + }, + decode(to: ToObject): FromTuple { + const fNames = Object.keys(fields); + if (!isObject(to)) throw new Error('wrong struct object'); + return fNames.map((i) => fields[i].decode(to[i])) as FromTuple; + }, +}); + +// U256BE in geth. But it is either 0 or 1. TODO: is this good enough? +const yParityCoder = P.coders.reverse( + P.validate(P.int(1, false, false, false), (elm) => { + assertYParityValid(elm); + return elm; + }) +); +type CoderOutput = F extends P.Coder ? T : never; + +const accessListItem = struct({ address: addrCoder, storageKeys: array(Bytes32) }); +export type AccessList = CoderOutput[]; + +export const authorizationRequest = struct({ + chainId: U256BE, + address: addrCoder, + nonce: U64BE, +}); +// [chain_id, address, nonce, y_parity, r, s] +const authorizationItem = struct({ + chainId: U256BE, + address: addrCoder, + nonce: U64BE, + yParity: yParityCoder, + r: U256BE, + s: U256BE, +}); +export type AuthorizationItem = CoderOutput; +export type AuthorizationRequest = CoderOutput; + /** * Field types, matching geth. Either u64 or u256. */ @@ -168,25 +206,14 @@ const coders = { to: addrCoder, value: U256BE, // "Decimal" coder can be used, but it's harder to work with data: ethHex, - accessList: { - decode: accessListParser(addrCoder.decode, (k) => ensure32(ethHex.decode(k))), - encode: accessListParser(addrCoder.encode, (k) => ethHex.encode(ensure32(k))), - } as P.Coder, + accessList: array(accessListItem), maxFeePerBlobGas: U256BE, - blobVersionedHashes: { - decode: blobParser((b) => ensureBlob(ethHex.decode(b))), - encode: blobParser((b) => ethHex.encode(ensureBlob(b))), - } as P.Coder, - // U256BE in geth. But it is either 0 or 1. TODO: is this good enough? - yParity: P.coders.reverse( - P.validate(P.int(1, false, false, false), (elm) => { - assertYParityValid(elm); - return elm; - }) - ), + blobVersionedHashes: array(Bytes32), + yParity: yParityCoder, v: U256BE, r: U256BE, s: U256BE, + authorizationList: array(authorizationItem), }; type Coders = typeof coders; type CoderName = keyof Coders; @@ -222,10 +249,13 @@ const txStruct = }, { [K in ST[number]]: FieldType }> > => { + const allFields = reqf.concat(optf); // Check that all fields have known coders - reqf.concat(optf).forEach((f) => { + allFields.forEach((f) => { if (!coders.hasOwnProperty(f)) throw new Error(`coder for field ${f} is not defined`); }); + const reqS = struct(Object.fromEntries(reqf.map((i) => [i, coders[i]]))); + const allS = struct(Object.fromEntries(allFields.map((i) => [i, coders[i]]))); // e.g. eip1559 txs have valid lengths of 9 or 12 (unsigned / signed) const reql = reqf.length; const optl = reql + optf.length; @@ -235,13 +265,10 @@ const txStruct = ) { - // @ts-ignore TODO: fix type - const values = reqf.map((f) => coders[f].decode(raw[f])); // If at least one optional key is present, we add whole optional block - if (optf.some((f) => raw.hasOwnProperty(f))) - // @ts-ignore TODO: fix type - optf.forEach((f) => values.push(coders[f].decode(raw[f]))); - RLP.encodeStream(w, values); + const hasOptional = optf.some((f) => raw.hasOwnProperty(f)); + const sCoder = hasOptional ? allS : reqS; + RLP.encodeStream(w, sCoder.decode(raw)); }, decodeStream(r): Record { const decoded = RLP.decodeStream(r); @@ -249,26 +276,17 @@ const txStruct = [f, coders[f].encode(decoded[i])]) - ); - if (length === optl) { - if (optf.every((_, i) => isEmpty(decoded[optFieldAt(i)]))) - throw new Error('all optional fields empty'); - const rawSig = Object.fromEntries( - // @ts-ignore TODO: fix type - optf.map((f, i) => [f, coders[f].encode(decoded[optFieldAt(i)])]) - ); - Object.assign(raw, rawSig); // mutate raw - } - return raw; + const sCoder = length === optl ? allS : reqS; + if (length === optl && optf.every((_, i) => isEmpty(decoded[optFieldAt(i)]))) + throw new Error('all optional fields empty'); + // @ts-ignore TODO: fix type (there can be null in RLP) + return sCoder.encode(decoded); }, }); fcoder.fields = reqf; fcoder.optionalFields = optf; - fcoder.setOfAllFields = new Set(reqf.concat(optf, ['type'] as any)); + fcoder.setOfAllFields = new Set(allFields.concat(['type'] as any)); return fcoder; }; @@ -316,12 +334,18 @@ const eip4844 = txStruct([ 'chainId', 'nonce', 'maxPriorityFeePerGas', 'maxFeePerGas', 'gasLimit', 'to', 'value', 'data', 'accessList', 'maxFeePerBlobGas', 'blobVersionedHashes'] as const, ['yParity', 'r', 's'] as const); +// prettier-ignore +const eip7702 = txStruct([ + 'chainId', 'nonce', 'maxPriorityFeePerGas', 'maxFeePerGas', 'gasLimit', 'to', 'value', 'data', 'accessList', + 'authorizationList'] as const, + ['yParity', 'r', 's'] as const); export const TxVersions = { legacy, // 0x00 (kinda) eip2930, // 0x01 eip1559, // 0x02 eip4844, // 0x03 + eip7702, // 0x04 }; export const RawTx = P.apply(createTxMap(TxVersions), { @@ -331,7 +355,12 @@ export const RawTx = P.apply(createTxMap(TxVersions), { data.data.to = addr.addChecksum(data.data.to); if (data.type !== 'legacy' && data.data.accessList) { for (const item of data.data.accessList) { - item[0] = addr.addChecksum(item[0]); + item.address = addr.addChecksum(item.address); + } + } + if (data.type === 'eip7702' && data.data.authorizationList) { + for (const item of data.data.authorizationList) { + item.address = addr.addChecksum(item.address); } } return data; @@ -424,10 +453,19 @@ const validators: Record= 1 and <= 2**32-1'); }, - accessList(list: [string, string[]][]) { + accessList(list: AccessList) { // NOTE: we cannot handle this validation in coder, since it requires chainId to calculate correct checksum - for (const [address, _] of list) { + for (const { address } of list) { + if (!addr.isValid(address)) throw new Error('address checksum does not match'); + } + }, + authorizationList(list: AuthorizationItem[], opts: ValidationOpts) { + for (const { address, nonce, chainId } of list) { if (!addr.isValid(address)) throw new Error('address checksum does not match'); + // chainId in authorization list can be zero (==allow any chain) + abig(chainId); + if (opts.strict) minmax(chainId, 0n, amounts.maxChainId, '>= 0 and <= 2**32-1'); + this.nonce(nonce, opts); } }, }; @@ -494,7 +532,7 @@ export function validateFields( const sortedFieldOrder = [ 'to', 'value', 'nonce', 'maxFeePerGas', 'maxFeePerBlobGas', 'maxPriorityFeePerGas', 'gasPrice', 'gasLimit', - 'accessList', 'blobVersionedHashes', 'chainId', 'data', 'type', + 'accessList', 'authorizationList', 'blobVersionedHashes', 'chainId', 'data', 'type', 'r', 's', 'yParity', 'v' ] as const; diff --git a/src/utils.ts b/src/utils.ts index 66e00f2..57e3df0 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -100,7 +100,9 @@ export function astr(str: unknown) { } export function cloneDeep(obj: T): T { - if (Array.isArray(obj)) { + if (obj instanceof Uint8Array) { + return Uint8Array.from(obj) as T; + } else if (Array.isArray(obj)) { return obj.map(cloneDeep) as unknown as T; } else if (typeof obj === 'bigint') { return BigInt(obj) as unknown as T; diff --git a/test/net.test.js b/test/net.test.js index ba9f175..67226f4 100644 --- a/test/net.test.js +++ b/test/net.test.js @@ -1,13 +1,7 @@ import { deepStrictEqual } from 'node:assert'; import { describe, should } from 'micro-should'; import { tokenFromSymbol } from '../esm/abi/index.js'; -import { - Web3Provider, - calcTransfersDiff, - ENS, - Chainlink, - UniswapV3, -} from '../esm/net/index.js'; +import { Web3Provider, calcTransfersDiff, ENS, Chainlink, UniswapV3 } from '../esm/net/index.js'; import * as mftch from 'micro-ftch'; import { weieth, numberTo0xHex } from '../esm/utils.js'; // These real network responses from real nodes, captured by replayable @@ -29,6 +23,15 @@ function initProv(replayJson) { const archive = new Web3Provider(provider); return archive; } +// API change workaround +const fixTx = (tx) => { + if (tx.info.accessList) + tx.info.accessList = tx.info.accessList.map(([address, storageKeys]) => ({ + address, + storageKeys, + })); + return tx; +}; describe('Network', () => { should('ENS', async () => { @@ -115,7 +118,7 @@ describe('Network', () => { ); deepStrictEqual( await tx.txInfo('0xba296ea35b5ff390b8c180ae8f536159dc8723871b43ed7f80e0c218cf171a05'), - NET_TX_VECTORS.blobTx + fixTx(NET_TX_VECTORS.blobTx) ); deepStrictEqual( await tx.txInfo('0x86c5a4350c973cd990105ae461522d01aa313fecbe0a67727e941cd9cee28997'), @@ -218,9 +221,7 @@ describe('Network', () => { offline: true, }); const ftch = mftch.ftch(replay, { concurrencyLimit: 1 }); - const archive = new Web3Provider( - mftch.jsonrpc(ftch, 'http://SOME_NODE/', { batchSize: 5 }) - ); + const archive = new Web3Provider(mftch.jsonrpc(ftch, 'http://SOME_NODE/', { batchSize: 5 })); // 2061s 566 (faster than without batch) const transfers = ( await archive.transfers(addr, { diff --git a/test/tx.test.js b/test/tx.test.js index 68f40b2..8664235 100644 --- a/test/tx.test.js +++ b/test/tx.test.js @@ -1,9 +1,9 @@ import { deepStrictEqual, throws } from 'node:assert'; import { inspect } from 'node:util'; import { describe, should } from 'micro-should'; -import { addr, Transaction, messenger } from '../esm/index.js'; +import { addr, Transaction, messenger, authorization } from '../esm/index.js'; import { RawTx, RlpTx, __tests } from '../esm/tx.js'; -import { add0x, createDecimal, ethHex, formatters } from '../esm/utils.js'; +import { add0x, createDecimal, ethHex, formatters, weieth } from '../esm/utils.js'; import { default as TX_VECTORS } from './vectors/transactions.json' with { type: 'json' }; import { default as EIP155_VECTORS } from './vectors/eips/eip155.json' with { type: 'json' }; import * as ethTests from './vectors/eth-tests-tx-vectors.js'; @@ -194,6 +194,75 @@ const convertTx = (raw) => { }; describe('Transactions', () => { + describe('EIP7702', () => { + should('basic', () => { + const tx = `0x04f8e3018203118080809470997970c51812dc3a010c7d01b50e0d17dc79c8880de0b6b3a764000080c0f8baf85c0194fba3912ca04dd458c843e2ee08967fc04f3579c28201a480a060fdd29ff912ce880cd3edaf9f932dc61d3dae823ea77e0323f94adb9f6a72fea060fdd29ff912ce880cd3edaf9f932dc61d3dae823ea77e0323f94adb9f6a72fef85a0a9400000000000000000000000000000000000000004501a060fdd29ff912ce880cd3edaf9f932dc61d3dae823ea77e0323f94adb9f6a72fea060fdd29ff912ce880cd3edaf9f932dc61d3dae823ea77e0323f94adb9f6a72fe`; + const authList = [ + { + address: '0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2', + chainId: 1n, + nonce: 420n, + r: 0x60fdd29ff912ce880cd3edaf9f932dc61d3dae823ea77e0323f94adb9f6a72fen, + s: 0x60fdd29ff912ce880cd3edaf9f932dc61d3dae823ea77e0323f94adb9f6a72fen, + yParity: 0, + }, + { + address: '0x0000000000000000000000000000000000000000', + chainId: 10n, + nonce: 69n, + r: 0x60fdd29ff912ce880cd3edaf9f932dc61d3dae823ea77e0323f94adb9f6a72fen, + s: 0x60fdd29ff912ce880cd3edaf9f932dc61d3dae823ea77e0323f94adb9f6a72fen, + yParity: 1, + }, + ]; + // Parse actual tx + const parsed = Transaction.fromHex(tx); + deepStrictEqual(parsed.type, 'eip7702'); + deepStrictEqual(parsed.raw.to.toLowerCase(), '0x70997970c51812dc3a010c7d01b50e0d17dc79c8'); + deepStrictEqual(parsed.raw.value, weieth.decode('1')); + deepStrictEqual(parsed.raw.nonce, 785n); + deepStrictEqual(parsed.raw.authorizationList, authList); + // Re-create tx from raw value + const created = Transaction.prepare( + { + type: 'eip7702', + to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + value: weieth.decode('1'), + nonce: 785n, + maxFeePerGas: 0n, + gasLimit: 0n, + maxPriorityFeePerGas: 0n, + authorizationList: authList, + }, + false + ); + deepStrictEqual(created.toHex(false), tx); + // Emulate signing + created.raw.r = 0x60fdd29ff912ce880cd3edaf9f932dc61d3dae823ea77e0323f94adb9f6a72fen; + created.raw.s = 0x60fdd29ff912ce880cd3edaf9f932dc61d3dae823ea77e0323f94adb9f6a72fen; + created.raw.yParity = 1; + created.isSigned = true; + deepStrictEqual( + created.toHex(true), + `0x04f90126018203118080809470997970c51812dc3a010c7d01b50e0d17dc79c8880de0b6b3a764000080c0f8baf85c0194fba3912ca04dd458c843e2ee08967fc04f3579c28201a480a060fdd29ff912ce880cd3edaf9f932dc61d3dae823ea77e0323f94adb9f6a72fea060fdd29ff912ce880cd3edaf9f932dc61d3dae823ea77e0323f94adb9f6a72fef85a0a9400000000000000000000000000000000000000004501a060fdd29ff912ce880cd3edaf9f932dc61d3dae823ea77e0323f94adb9f6a72fea060fdd29ff912ce880cd3edaf9f932dc61d3dae823ea77e0323f94adb9f6a72fe01a060fdd29ff912ce880cd3edaf9f932dc61d3dae823ea77e0323f94adb9f6a72fea060fdd29ff912ce880cd3edaf9f932dc61d3dae823ea77e0323f94adb9f6a72fe` + ); + created.raw.yParity = 0; + deepStrictEqual( + created.toHex(true), + `0x04f90126018203118080809470997970c51812dc3a010c7d01b50e0d17dc79c8880de0b6b3a764000080c0f8baf85c0194fba3912ca04dd458c843e2ee08967fc04f3579c28201a480a060fdd29ff912ce880cd3edaf9f932dc61d3dae823ea77e0323f94adb9f6a72fea060fdd29ff912ce880cd3edaf9f932dc61d3dae823ea77e0323f94adb9f6a72fef85a0a9400000000000000000000000000000000000000004501a060fdd29ff912ce880cd3edaf9f932dc61d3dae823ea77e0323f94adb9f6a72fea060fdd29ff912ce880cd3edaf9f932dc61d3dae823ea77e0323f94adb9f6a72fe80a060fdd29ff912ce880cd3edaf9f932dc61d3dae823ea77e0323f94adb9f6a72fea060fdd29ff912ce880cd3edaf9f932dc61d3dae823ea77e0323f94adb9f6a72fe` + ); + }); + should('sign authorization', () => { + const privateKey = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80'; + const auth = { + address: '0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2', + chainId: 1n, + nonce: 0n, + }; + const signed = authorization.sign(auth, privateKey); + deepStrictEqual(authorization.getAuthority(signed), addr.fromPrivateKey(privateKey)); + }); + }); describe('Utils', () => { should('legacySig', () => { const { legacySig } = __tests; @@ -491,7 +560,6 @@ describe('Transactions', () => { d.maxFeePerBlobGas = 0n; if (['eip4844'].includes(etx.type) && d.blobVersionedHashes === undefined) d.blobVersionedHashes = []; - if (d.accessList) d.accessList = d.accessList.map((i) => [i.address, i.storageKeys]); const c = __tests.TxVersions[etx.type]; // remove fields from wrong version for (const k in d) { @@ -501,7 +569,7 @@ describe('Transactions', () => { if (d.chainId === 30n) { d.to = d.to.toLowerCase(); if (d.accessList) { - for (const item of d.accessList) item[0] = item[0].toLowerCase(); + for (const item of d.accessList) item.address = item.address.toLowerCase(); } } const preparedTx = Transaction.prepare(d, false); @@ -557,7 +625,6 @@ describe('Transactions', () => { d.maxFeePerBlobGas = 0n; if (['eip4844'].includes(etx.type) && d.blobVersionedHashes === undefined) d.blobVersionedHashes = []; - if (d.accessList) d.accessList = d.accessList.map((i) => [i.address, i.storageKeys]); const preparedTx = Transaction.prepare(d, false); deepStrictEqual(preparedTx.toHex(false), unsigned); const sig = etx.signBy(vtx.privateKey);