diff --git a/.changeset/frame-rpc-fields.md b/.changeset/frame-rpc-fields.md new file mode 100644 index 00000000..d34bf633 --- /dev/null +++ b/.changeset/frame-rpc-fields.md @@ -0,0 +1,5 @@ +--- +"ox": patch +--- + +Aligned frame RPC quantities, signature placeholders, and receipt gas totals with the execution API definitions. diff --git a/src/core/Frame.ts b/src/core/Frame.ts index 6f4937f9..de1e318c 100644 --- a/src/core/Frame.ts +++ b/src/core/Frame.ts @@ -45,13 +45,13 @@ export type Rpc = { /** Frame calldata. */ data: Hex.Hex /** Execution gas budget. */ - executionGasLimit: Hex.Hex + executionGas: Hex.Hex /** Approval scope and batching bits. */ - flags: number + flags: Hex.Hex /** Execution mode. */ - mode: number + mode: Hex.Hex /** State gas budget. */ - stateGasLimit: Hex.Hex + stateGas: Hex.Hex /** Target address; absent for the transaction sender. */ target?: Address.Address | null | undefined /** Value transferred in wei. */ @@ -205,10 +205,10 @@ export declare namespace from { * * const frame = Frame.fromRpc({ * data: '0x', - * executionGasLimit: '0xc350', - * flags: 3, - * mode: 1, - * stateGasLimit: '0x0', + * executionGas: '0xc350', + * flags: '0x3', + * mode: '0x1', + * stateGas: '0x0', * value: '0x0' * }) * ``` @@ -219,17 +219,20 @@ export declare namespace from { export function fromRpc(frame: Rpc): Frame { return from({ data: frame.data, - executionGas: Hex.toBigInt(frame.executionGasLimit), - flags: frame.flags, - mode: frame.mode, - stateGas: Hex.toBigInt(frame.stateGasLimit), + executionGas: Hex.toBigInt(frame.executionGas), + flags: Hex.toNumber(frame.flags), + mode: Hex.toNumber(frame.mode), + stateGas: Hex.toBigInt(frame.stateGas), ...(frame.target == null ? {} : { to: frame.target }), value: Hex.toBigInt(frame.value), }) } export declare namespace fromRpc { - type ErrorType = from.ErrorType | Hex.toBigInt.ErrorType + type ErrorType = + | from.ErrorType + | Hex.toBigInt.ErrorType + | Hex.toNumber.ErrorType } /** @@ -255,10 +258,10 @@ export function toRpc(frame: toRpc.Input): Rpc { const { flags: flags_ = 0, mode = 0 } = frame return { data: frame.data ?? '0x', - executionGasLimit: Quantity.fromNumberish(frame.executionGas ?? 0n), - flags: typeof flags_ === 'string' ? flags[flags_] : flags_, - mode: typeof mode === 'string' ? modes[mode] : mode, - stateGasLimit: Quantity.fromNumberish(frame.stateGas ?? 0n), + executionGas: Quantity.fromNumberish(frame.executionGas ?? 0n), + flags: Hex.fromNumber(typeof flags_ === 'string' ? flags[flags_] : flags_), + mode: Hex.fromNumber(typeof mode === 'string' ? modes[mode] : mode), + stateGas: Quantity.fromNumberish(frame.stateGas ?? 0n), ...(frame.to === undefined ? {} : { target: frame.to }), value: Quantity.fromNumberish(frame.value ?? 0n), } diff --git a/src/core/FrameReceipt.ts b/src/core/FrameReceipt.ts index 362006e4..9031a433 100644 --- a/src/core/FrameReceipt.ts +++ b/src/core/FrameReceipt.ts @@ -2,13 +2,21 @@ import type * as Errors from './Errors.js' import * as Hex from './Hex.js' import * as Quantity from './internal/quantity.js' import type * as Log from './Log.js' +import type { Compute, ExactPartial } from './internal/types.js' -/** Receipt for one frame. Frame logs contain only address, data, and topics. */ +type FrameLog = Compute< + Pick & + ExactPartial> +> + +/** Receipt for one frame. */ export type FrameReceipt = { /** Execution gas used before transaction-level refunds. */ + executionGasUsed: bigintType + /** Total execution and state gas used. */ gasUsed: bigintType /** Logs emitted by this frame. */ - logs: readonly Pick[] + logs: readonly FrameLog[] /** Final state gas after refills and rollbacks. */ stateGasUsed: bigintType /** Frame execution result. */ @@ -19,8 +27,10 @@ export type FrameReceipt = { export type Rpc = { /** Execution gas used. */ executionGasUsed: Hex.Hex + /** Total execution and state gas used. */ + gasUsed: Hex.Hex /** Logs emitted by this frame. */ - logs: readonly Pick[] + logs: readonly FrameLog[] /** Final state gas used. */ stateGasUsed: Hex.Hex /** Zero for failure, one for success, or two for a skipped frame. */ @@ -30,21 +40,21 @@ export type Rpc = { /** Frame execution status. */ export type Status = 'reverted' | 'skipped' | 'success' -/** Numeric JSON-RPC frame execution status. */ -export type RpcStatus = 0 | 1 | 2 +/** Hex JSON-RPC frame execution status. */ +export type RpcStatus = '0x0' | '0x1' | '0x2' /** RPC status to status mapping. */ export const fromRpcStatus = { - 0: 'reverted', - 1: 'success', - 2: 'skipped', + '0x0': 'reverted', + '0x1': 'success', + '0x2': 'skipped', } as const /** Status to RPC status mapping. */ export const toRpcStatus = { - reverted: 0, - skipped: 2, - success: 1, + reverted: '0x0', + skipped: '0x2', + success: '0x1', } as const /** @@ -58,9 +68,10 @@ export const toRpcStatus = { * * const receipt = FrameReceipt.fromRpc({ * executionGasUsed: '0x5208', + * gasUsed: '0x5208', * logs: [], * stateGasUsed: '0x0', - * status: 1 + * status: '0x1' * }) * ``` * @@ -69,7 +80,8 @@ export const toRpcStatus = { */ export function fromRpc(receipt: Rpc): FrameReceipt { return { - gasUsed: Hex.toBigInt(receipt.executionGasUsed), + executionGasUsed: Hex.toBigInt(receipt.executionGasUsed), + gasUsed: Hex.toBigInt(receipt.gasUsed), logs: receipt.logs, stateGasUsed: Hex.toBigInt(receipt.stateGasUsed), status: fromRpcStatus[receipt.status], @@ -91,19 +103,21 @@ export declare namespace fromRpc { * * const receipt = FrameReceipt.fromRpc({ * executionGasUsed: '0x5208', + * gasUsed: '0x5208', * logs: [], * stateGasUsed: '0x0', - * status: 1 + * status: '0x1' * }) * const rpc = FrameReceipt.toRpc(receipt) * ``` * * @param receipt - The frame receipt. Gas accepts hex, bigint, or number values. - * @returns The RPC frame receipt with hex gas and a numeric status. + * @returns The RPC frame receipt with hex gas and a hex status. */ export function toRpc(receipt: toRpc.Input): Rpc { return { - executionGasUsed: Quantity.fromNumberish(receipt.gasUsed), + executionGasUsed: Quantity.fromNumberish(receipt.executionGasUsed), + gasUsed: Quantity.fromNumberish(receipt.gasUsed), logs: receipt.logs, stateGasUsed: Quantity.fromNumberish(receipt.stateGasUsed), status: toRpcStatus[receipt.status], diff --git a/src/core/FrameSignature.ts b/src/core/FrameSignature.ts index 375a9df3..f3af5064 100644 --- a/src/core/FrameSignature.ts +++ b/src/core/FrameSignature.ts @@ -67,11 +67,11 @@ export type FrameSignature = Arbitrary | Secp256k1 | P256 /** JSON-RPC representation of a frame signature. */ export type Rpc = { /** Explicit digest, or empty bytes for the transaction signing hash. */ - msg: Hex.Hex + msg?: Hex.Hex | undefined /** Signature verification scheme. */ - scheme: 0 | 1 | 2 + scheme: '0x0' | '0x1' | '0x2' /** Encoded signature bytes. */ - signature: Hex.Hex + signature?: Hex.Hex | undefined /** Signer address; absent for the transaction sender. */ signer?: Address.Address | null | undefined } @@ -348,7 +348,7 @@ export declare namespace from { * * const entry = FrameSignature.fromRpc({ * msg: '0x', - * scheme: 0, + * scheme: '0x0', * signature: '0xdeadbeef' * }) * ``` @@ -358,15 +358,20 @@ export declare namespace from { */ export function fromRpc(entry: Rpc): FrameSignature { return fromTuple([ - entry.scheme === 0 ? '0x' : Hex.fromNumber(entry.scheme, { size: 1 }), + entry.scheme === '0x0' + ? '0x' + : Hex.fromNumber(Hex.toNumber(entry.scheme), { size: 1 }), entry.signer ?? '0x', - entry.msg, - entry.signature, + entry.msg ?? '0x', + entry.signature ?? '0x', ]) } export declare namespace fromRpc { - type ErrorType = fromTuple.ErrorType | Hex.fromNumber.ErrorType + type ErrorType = + | fromTuple.ErrorType + | Hex.fromNumber.ErrorType + | Hex.toNumber.ErrorType } /** @@ -389,8 +394,10 @@ export function toRpc(entry: FrameSignature): Rpc { const [scheme, signer, msg, signature] = toTuple(entry) return { msg, - scheme: (scheme === '0x' ? 0 : Hex.toNumber(scheme)) as Rpc['scheme'], - signature, + scheme: (scheme === '0x' + ? '0x0' + : Hex.fromNumber(Hex.toNumber(scheme))) as Rpc['scheme'], + ...(signature === '0x' && scheme !== '0x' ? {} : { signature }), ...(signer === '0x' ? {} : { signer }), } } diff --git a/src/core/Transaction.ts b/src/core/Transaction.ts index 3a7a3d99..43e349c3 100644 --- a/src/core/Transaction.ts +++ b/src/core/Transaction.ts @@ -6,7 +6,12 @@ import * as Frame from './Frame.js' import * as FrameSignature from './FrameSignature.js' import * as Hex from './Hex.js' import * as Quantity from './internal/quantity.js' -import type { Compute, OneOf, UnionCompute } from './internal/types.js' +import type { + Compute, + ExactPartial, + OneOf, + UnionCompute, +} from './internal/types.js' import * as Signature from './Signature.js' /** @@ -197,29 +202,35 @@ export type Eip8141< > = Compute< Omit< Base, - 'chainId' | 'r' | 's' | 'v' | 'yParity' - > & { - /** Versioned blob hashes. */ - blobVersionedHashes: readonly Hex.Hex[] - /** Chain ID, retaining bigint precision when needed. */ - chainId: numberType | bigintType - /** Frames in execution order. */ - frames: readonly ([bigintType] extends [Hex.Hex] - ? Frame.Rpc - : Frame.Frame)[] - /** Effective gas price. */ - gasPrice?: bigintType | undefined - /** Maximum fee per blob gas. */ - maxFeePerBlobGas: bigintType - /** Maximum fee per gas. */ - maxFeePerGas: bigintType - /** Maximum priority fee per gas. */ - maxPriorityFeePerGas: bigintType - /** Frame signature entries. */ - signatures: readonly ([bigintType] extends [Hex.Hex] - ? FrameSignature.Rpc - : FrameSignature.FrameSignature)[] - } + 'chainId' | 'gas' | 'input' | 'to' | 'value' | 'r' | 's' | 'v' | 'yParity' + > & + ExactPartial< + Pick< + Base, + 'gas' | 'input' | 'to' | 'value' + > + > & { + /** Versioned blob hashes. */ + blobVersionedHashes: readonly Hex.Hex[] + /** Chain ID, retaining bigint precision when needed. */ + chainId: numberType | bigintType + /** Frames in execution order. */ + frames: readonly ([bigintType] extends [Hex.Hex] + ? Frame.Rpc + : Frame.Frame)[] + /** Effective gas price. */ + gasPrice?: bigintType | undefined + /** Maximum fee per blob gas. */ + maxFeePerBlobGas: bigintType + /** Maximum fee per gas. */ + maxFeePerGas: bigintType + /** Maximum priority fee per gas. */ + maxPriorityFeePerGas: bigintType + /** Frame signature entries. */ + signatures: readonly ([bigintType] extends [Hex.Hex] + ? FrameSignature.Rpc + : FrameSignature.FrameSignature)[] + } > /** An EIP-8141 RPC transaction. */ diff --git a/src/core/_test/AbiConstructor.test.ts b/src/core/_test/AbiConstructor.test.ts index 7e593d7e..7ca809c9 100644 --- a/src/core/_test/AbiConstructor.test.ts +++ b/src/core/_test/AbiConstructor.test.ts @@ -136,6 +136,8 @@ describe('decode', () => { params: [hash], }))! + if (input === undefined) throw new Error('Missing transaction input') + expect( AbiConstructor.decode(abiConstructor, { bytecode: Constructor.bytecode.object, diff --git a/src/core/_test/Frame.test.ts b/src/core/_test/Frame.test.ts index 7a676537..28a6381f 100644 --- a/src/core/_test/Frame.test.ts +++ b/src/core/_test/Frame.test.ts @@ -247,10 +247,10 @@ describe('fromRpc', () => { expect( Frame.fromRpc({ data: '0xdeadbeef', - executionGasLimit: '0x20000000000001', - flags: 0, - mode: 2, - stateGasLimit: '0xc', + executionGas: '0x20000000000001', + flags: '0x0', + mode: '0x2', + stateGas: '0xc', target: null, value: '0x1', }), @@ -273,10 +273,10 @@ describe('toRpc', () => { ), ).toEqual({ data: '0x', - executionGasLimit: '0x0', - flags: 3, - mode: 1, - stateGasLimit: '0x0', + executionGas: '0x0', + flags: '0x3', + mode: '0x1', + stateGas: '0x0', value: '0x0', }) }) @@ -285,10 +285,10 @@ describe('toRpc', () => { Frame.toRpc({ executionGas: '0xc350', stateGas: 12, value: 0n }), ).toEqual({ data: '0x', - executionGasLimit: '0xc350', - flags: 0, - mode: 0, - stateGasLimit: '0xc', + executionGas: '0xc350', + flags: '0x0', + mode: '0x0', + stateGas: '0xc', value: '0x0', }) }) diff --git a/src/core/_test/FrameReceipt.test.ts b/src/core/_test/FrameReceipt.test.ts index 6f9a2c08..7854b191 100644 --- a/src/core/_test/FrameReceipt.test.ts +++ b/src/core/_test/FrameReceipt.test.ts @@ -14,13 +14,14 @@ test('exports', () => { describe('fromRpc', () => { test.each([ - [0, 'reverted'], - [1, 'success'], - [2, 'skipped'], + ['0x0', 'reverted'], + ['0x1', 'success'], + ['0x2', 'skipped'], ] as const)('status %s', (status, expected) => { expect( FrameReceipt.fromRpc({ executionGasUsed: '0x20000000000001', + gasUsed: '0x20000000000003', logs: [ { address: '0x1111111111111111111111111111111111111111', @@ -32,7 +33,8 @@ describe('fromRpc', () => { status, }), ).toEqual({ - gasUsed: 9007199254740993n, + executionGasUsed: 9007199254740993n, + gasUsed: 9007199254740995n, logs: [ { address: '0x1111111111111111111111111111111111111111', @@ -48,18 +50,20 @@ describe('fromRpc', () => { describe('toRpc', () => { test.each([ - ['reverted', 0], - ['success', 1], - ['skipped', 2], + ['reverted', '0x0'], + ['success', '0x1'], + ['skipped', '0x2'], ] as const)('status %s', (status, expected) => { const receipt = FrameReceipt.fromRpc({ executionGasUsed: '0x20000000000001', + gasUsed: '0x20000000000003', logs: [], stateGasUsed: '0x2', status: expected, }) expect(FrameReceipt.toRpc({ ...receipt, status })).toEqual({ executionGasUsed: '0x20000000000001', + gasUsed: '0x20000000000003', logs: [], stateGasUsed: '0x2', status: expected, @@ -69,7 +73,8 @@ describe('toRpc', () => { test('numberish gas', () => { expect( FrameReceipt.toRpc({ - gasUsed: '0x5208', + executionGasUsed: '0x5208', + gasUsed: '0x520a', logs: [], stateGasUsed: 2, status: 'success', @@ -77,9 +82,10 @@ describe('toRpc', () => { ).toMatchInlineSnapshot(` { "executionGasUsed": "0x5208", + "gasUsed": "0x520a", "logs": [], "stateGasUsed": "0x2", - "status": 1, + "status": "0x1", } `) }) diff --git a/src/core/_test/FrameSignature.test.ts b/src/core/_test/FrameSignature.test.ts index 2ecb4a7e..4eb298c9 100644 --- a/src/core/_test/FrameSignature.test.ts +++ b/src/core/_test/FrameSignature.test.ts @@ -455,7 +455,7 @@ describe('fromRpc', () => { expect( FrameSignature.fromRpc({ msg: '0x', - scheme: 0, + scheme: '0x0', signature: '0xaabb', signer: null, }), @@ -463,7 +463,11 @@ describe('fromRpc', () => { }) test('secp256k1 signature', () => { expect( - FrameSignature.fromRpc({ msg: '0x', scheme: 1, signature: secpBytes }), + FrameSignature.fromRpc({ + msg: '0x', + scheme: '0x1', + signature: secpBytes, + }), ).toEqual( FrameSignature.from({ scheme: 'secp256k1', @@ -478,24 +482,24 @@ describe('fromRpc', () => { test('P-256 signature', () => { const entry = FrameSignature.fromRpc({ msg: '0x', - scheme: 2, + scheme: '0x2', signature: p256Bytes, }) expect(entry).toMatchObject({ payload: '0x', publicKey, scheme: 'p256' }) expect(FrameSignature.toRpc(entry)).toEqual({ msg: '0x', - scheme: 2, + scheme: '0x2', signature: p256Bytes, }) }) test('unsigned placeholder', () => { - expect( - FrameSignature.fromRpc({ msg: '0x', scheme: 1, signature: '0x' }), - ).toEqual(FrameSignature.from({ scheme: 'secp256k1' })) + expect(FrameSignature.fromRpc({ scheme: '0x1' })).toEqual( + FrameSignature.from({ scheme: 'secp256k1' }), + ) }) test('malformed protocol signature', () => { expect(() => - FrameSignature.fromRpc({ msg: '0x', scheme: 1, signature: '0x01' }), + FrameSignature.fromRpc({ msg: '0x', scheme: '0x1', signature: '0x01' }), ).toThrowErrorMatchingInlineSnapshot( `[FrameSignature.InvalidError: Invalid frame signature. @@ -505,6 +509,11 @@ Details: Invalid protocol signature length.]`, }) describe('toRpc', () => { + test('omits an unsigned protocol signature', () => { + expect( + FrameSignature.toRpc(FrameSignature.from({ scheme: 'secp256k1' })), + ).toEqual({ msg: '0x', scheme: '0x1' }) + }) test('preserves explicit payload and signer', () => { const entry = FrameSignature.from({ payload, @@ -514,7 +523,7 @@ describe('toRpc', () => { }) expect(FrameSignature.toRpc(entry)).toEqual({ msg: payload, - scheme: 1, + scheme: '0x1', signature: secpBytes, signer: '0x1111111111111111111111111111111111111111', }) diff --git a/src/core/_test/TransactionReceipt.test.ts b/src/core/_test/TransactionReceipt.test.ts index e4b28b1f..7856df21 100644 --- a/src/core/_test/TransactionReceipt.test.ts +++ b/src/core/_test/TransactionReceipt.test.ts @@ -469,6 +469,7 @@ describe('frame receipts', () => { frameReceipts: [ { executionGasUsed: '0x20000000000001', + gasUsed: '0x20000000000003', logs: [ { address: '0x1111111111111111111111111111111111111111', @@ -477,10 +478,22 @@ describe('frame receipts', () => { }, ], stateGasUsed: '0x2', - status: 1, + status: '0x1', + }, + { + executionGasUsed: '0x3', + gasUsed: '0x3', + logs: [], + stateGasUsed: '0x0', + status: '0x0', + }, + { + executionGasUsed: '0x0', + gasUsed: '0x0', + logs: [], + stateGasUsed: '0x0', + status: '0x2', }, - { executionGasUsed: '0x3', logs: [], stateGasUsed: '0x0', status: 0 }, - { executionGasUsed: '0x0', logs: [], stateGasUsed: '0x0', status: 2 }, ], from: '0x1111111111111111111111111111111111111111', gasUsed: '0x20000000000001', @@ -498,13 +511,26 @@ describe('frame receipts', () => { expect(receipt.payer).toBe(rpc.payer) expect(receipt.frameReceipts).toEqual([ { - gasUsed: 9007199254740993n, + executionGasUsed: 9007199254740993n, + gasUsed: 9007199254740995n, logs: rpc.frameReceipts[0]!.logs, stateGasUsed: 2n, status: 'success', }, - { gasUsed: 3n, logs: [], stateGasUsed: 0n, status: 'reverted' }, - { gasUsed: 0n, logs: [], stateGasUsed: 0n, status: 'skipped' }, + { + executionGasUsed: 3n, + gasUsed: 3n, + logs: [], + stateGasUsed: 0n, + status: 'reverted', + }, + { + executionGasUsed: 0n, + gasUsed: 0n, + logs: [], + stateGasUsed: 0n, + status: 'skipped', + }, ]) expect(TransactionReceipt.toRpc(receipt)).toMatchObject(rpc) expect( diff --git a/src/core/_test/TransactionRequest.test.ts b/src/core/_test/TransactionRequest.test.ts index 02565047..df9691f9 100644 --- a/src/core/_test/TransactionRequest.test.ts +++ b/src/core/_test/TransactionRequest.test.ts @@ -474,8 +474,9 @@ describe('frame transactions', () => { const rpc = TransactionRequest.toRpc(request) expect(rpc.chainId).toBe('0x1fcd') expect(rpc.type).toBe('0x6') + expect(rpc.frames?.[0]?.executionGas).toBe('0xc350') expect(rpc.signatures).toEqual([ - { msg: '0x', scheme: 0, signature: '0xaabb' }, + { msg: '0x', scheme: '0x0', signature: '0xaabb' }, ]) const decoded = TransactionRequest.fromRpc(rpc) expect(decoded.chainId).toBe(8141) diff --git a/src/core/_test/TxEnvelopeEip8141.test.ts b/src/core/_test/TxEnvelopeEip8141.test.ts index 6cd4c05f..c6f233f6 100644 --- a/src/core/_test/TxEnvelopeEip8141.test.ts +++ b/src/core/_test/TxEnvelopeEip8141.test.ts @@ -643,18 +643,11 @@ describe('serialize', () => { ], from: sender, signatures: [{ scheme: 'secp256k1' }], - // Nethermind's simulation mapping requires an outer recipient before processing frames. - to: sender, type: 'eip8141', }) - // TODO: remove once migrated to reth or anvil. - // Nethermind validates signatures when filling gas, before the transaction can be signed. - await expect( - rpc.request({ method: 'eth_fillTransaction', params: [request] }), - ).rejects.toThrow('frame transaction signature has the wrong length') const { tx } = await rpc.request({ method: 'eth_fillTransaction', - params: [{ ...request, gas: Hex.fromNumber(100_000n) }], + params: [request], }) expect(tx.type).toBe('0x6') if (tx.type !== '0x6' || !tx.frames) @@ -663,7 +656,6 @@ describe('serialize', () => { expect(tx.nonce).toBe(nonce) expect(tx.frames).toEqual(request.frames) expect(tx.signatures).toEqual(request.signatures) - expect(Hex.toBigInt(tx.gas)).toBe(100_000n) expect(Hex.toBigInt(tx.maxFeePerGas)).toBeGreaterThan(0n) expect(Hex.toBigInt(tx.maxPriorityFeePerGas)).toBeGreaterThanOrEqual(0n) expect(Hex.toBigInt(tx.maxFeePerGas)).toBeGreaterThanOrEqual( @@ -766,14 +758,12 @@ describe('serialize', () => { }) const request = TransactionRequest.toRpc({ ...TransactionEnvelope.toTransactionRequest(signed), - // Nethermind's simulation mapping requires an outer recipient before processing frames. - to: sender, }) const result = await rpc.request({ method, params: [request, 'latest'], }) - // Frame return data is not exposed by Nethermind's transaction-level call result. + // Transaction-level calls do not expose individual frame return data. if (method === 'eth_call') expect(result).toBe('0x') else { // Estimation includes all signed frame budgets plus intrinsic gas. @@ -784,11 +774,14 @@ describe('serialize', () => { rpc.request({ method, params: [ - { ...request, frames: [{ ...request.frames![0]!, mode: 255 }] }, + { + ...request, + frames: [{ ...request.frames![0]!, mode: '0xff' }], + }, 'latest', ], }), - ).rejects.toThrow('frame mode') + ).rejects.toThrow('Invalid params') expect( await rpc.request({ method: 'eth_getBalance', @@ -954,18 +947,18 @@ describe('serialize', () => { chainId: 8141, frames: [ Frame.from({ - executionGas: 50_000n, + executionGas: 20_000n, flags: 'approveExecution', mode: 'verify', }), Frame.from({ - executionGas: 50_000n, + executionGas: 20_000n, flags: 'approvePayment', mode: 'verify', to: accounts[1].address, }), Frame.from({ - executionGas: 50_000n, + executionGas: 20_000n, mode: 'sender', to: accounts[1].address, value: 1n, @@ -1181,7 +1174,7 @@ describe('serialize', () => { ], }), ).rejects.toThrowErrorMatchingInlineSnapshot( - `[RpcResponse.InvalidInputError: transaction invalid, frame transaction SECP256K1 signer does not match the recovered address]`, + `[RpcResponse.InvalidInputError: EIP-8141 public mempool policy: validation prefix execution failed]`, ) }) @@ -1191,12 +1184,12 @@ describe('serialize', () => { frames: [ Frame.from({ data: '0x0000000000000001', - executionGas: 50_000n, + executionGas: 20_000n, mode: 'verify', to: '0x0000000000000000000000000000000000008141', }), Frame.from({ - executionGas: 50_000n, + executionGas: 20_000n, flags: 'approveExecutionAndPayment', mode: 'verify', }), @@ -1228,7 +1221,7 @@ describe('serialize', () => { params: [TxEnvelopeEip8141.serialize(signed)], }), ).rejects.toThrowErrorMatchingInlineSnapshot( - `[RpcResponse.InvalidInputError: frame transaction expired]`, + `[RpcResponse.InvalidInputError: EIP-8141 public mempool policy: validation prefix execution failed]`, ) }) @@ -1753,10 +1746,10 @@ describe('toRpc', () => { frames: [ { data: '0x', - executionGasLimit: '0x0', - flags: 0, - mode: 0, - stateGasLimit: '0x0', + executionGas: '0x0', + flags: '0x0', + mode: '0x0', + stateGas: '0x0', value: '0x0', }, ], diff --git a/src/zod/Frame.ts b/src/zod/Frame.ts index b5da1324..ee1fffe8 100644 --- a/src/zod/Frame.ts +++ b/src/zod/Frame.ts @@ -25,10 +25,10 @@ export const Decoded: z.ZodMiniType = z export const Rpc = z .object({ data: z_Hex.Hex, - executionGasLimit: z_Hex.Hex, - flags: z.number(), - mode: z.number(), - stateGasLimit: z_Hex.Hex, + executionGas: z_Hex.Hex, + flags: z_Hex.Hex, + mode: z_Hex.Hex, + stateGas: z_Hex.Hex, target: z.optional(z.nullable(z_Address.Address)), value: z_Hex.Hex, }) diff --git a/src/zod/FrameReceipt.ts b/src/zod/FrameReceipt.ts index c7ea15e0..651fe1d2 100644 --- a/src/zod/FrameReceipt.ts +++ b/src/zod/FrameReceipt.ts @@ -11,17 +11,26 @@ import * as z from 'zod/mini' const frameLog = z.object({ address: z_Address.Address, + blockHash: z.optional(z_Hex.Hex), + blockNumber: z.optional(quantityHex()), + blockTimestamp: z.optional(quantityHex()), data: z_Hex.Hex, + logIndex: z.optional(quantityHex()), + removed: z.optional(z.boolean()), topics: z.array(z_Hex.Hex), + transactionHash: z.optional(z_Hex.Hex), + transactionIndex: z.optional(quantityHex()), }) const frameReceiptRpc = z.object({ executionGasUsed: quantityHex(), + gasUsed: quantityHex(), logs: z.readonly(z.array(frameLog)), stateGasUsed: quantityHex(), - status: z.union([z.literal(0), z.literal(1), z.literal(2)]), + status: z.enum(['0x0', '0x1', '0x2']), }) const frameReceiptDecoded = z.object({ + executionGasUsed: uintBigint(), gasUsed: uintBigint(), logs: z.readonly(z.array(frameLog)), stateGasUsed: uintBigint(), @@ -38,6 +47,7 @@ export const FrameReceipt = z.codec(frameReceiptRpc, frameReceiptDecoded, { export const FrameReceiptToRpc = z.codec( frameReceiptRpc, z.extend(frameReceiptDecoded, { + executionGasUsed: uintBigintNumberish(), gasUsed: uintBigintNumberish(), stateGasUsed: uintBigintNumberish(), }), diff --git a/src/zod/FrameSignature.ts b/src/zod/FrameSignature.ts index d892c739..3037e6bf 100644 --- a/src/zod/FrameSignature.ts +++ b/src/zod/FrameSignature.ts @@ -58,9 +58,9 @@ export const Decoded: z.ZodMiniType< /** RPC frame signature schema. */ export const Rpc = z .object({ - msg: z_Hex.Hex, - scheme: z.union([z.literal(0), z.literal(1), z.literal(2)]), - signature: z_Hex.Hex, + msg: z.optional(z_Hex.Hex), + scheme: z.enum(['0x0', '0x1', '0x2']), + signature: z.optional(z_Hex.Hex), signer: z.optional(z.nullable(z_Address.Address)), }) .check( diff --git a/src/zod/Transaction.ts b/src/zod/Transaction.ts index 37a28594..c2d95ce7 100644 --- a/src/zod/Transaction.ts +++ b/src/zod/Transaction.ts @@ -179,18 +179,18 @@ function eip8141Fields< data: z.optional(z_Hex.Hex), frames: z.readonly(z.array(frame)), from: z_Address.Address, - gas: uint, + gas: z.optional(uint), gasPrice: z.optional(uint), hash: z_Hex.Hex, - input: z_Hex.Hex, + input: z.optional(z_Hex.Hex), maxFeePerBlobGas: uint, maxFeePerGas: uint, maxPriorityFeePerGas: uint, nonce: uint, signatures: z.readonly(z.array(z_FrameSignature.FrameSignature)), - to: z.nullable(z_Address.Address), + to: z.optional(z.nullable(z_Address.Address)), type: Eip8141Type, - value: uint, + value: z.optional(uint), } } diff --git a/src/zod/_test/Frame.test.ts b/src/zod/_test/Frame.test.ts index 9877a6bb..92f2288b 100644 --- a/src/zod/_test/Frame.test.ts +++ b/src/zod/_test/Frame.test.ts @@ -13,10 +13,10 @@ describe('Frame', () => { const rpc = z.encode(z.Frame.Frame, frame) expect(rpc).toEqual({ data: '0x', - executionGasLimit: '0xc350', - flags: 3, - mode: 1, - stateGasLimit: '0x0', + executionGas: '0xc350', + flags: '0x3', + mode: '0x1', + stateGas: '0x0', target: '0x1111111111111111111111111111111111111111', value: '0x0', }) @@ -39,7 +39,7 @@ describe('Frame', () => { test('encodes numberish values', () => { expect( z.encode(z.Frame.FrameToRpc, { executionGas: '0xc350', stateGas: 0 }) - .executionGasLimit, + .executionGas, ).toBe('0xc350') }) }) diff --git a/src/zod/_test/FrameReceipt.test.ts b/src/zod/_test/FrameReceipt.test.ts index c265668d..b60a6e8b 100644 --- a/src/zod/_test/FrameReceipt.test.ts +++ b/src/zod/_test/FrameReceipt.test.ts @@ -7,13 +7,15 @@ describe('FrameReceipt', () => { expect( z.safeDecode(z.FrameReceipt.FrameReceipt, { executionGasUsed: '0x', + gasUsed: '0x', logs: [], stateGasUsed: '0x0', - status: 1, + status: '0x1', }).success, ).toBe(false) expect( z.safeEncode(z.FrameReceipt.FrameReceipt, { + executionGasUsed: 0n, gasUsed: -1n, logs: [], stateGasUsed: 0n, @@ -23,6 +25,7 @@ describe('FrameReceipt', () => { for (const gasUsed of [-1, Number.MAX_SAFE_INTEGER + 1, '0x'] as const) expect( z.safeEncode(z.FrameReceipt.FrameReceiptToRpc, { + executionGasUsed: 0n, gasUsed, logs: [], stateGasUsed: 0n, @@ -34,9 +37,10 @@ describe('FrameReceipt', () => { test('roundtrip', () => { const rpc = { executionGasUsed: '0x20000000000001', + gasUsed: '0x20000000000001', logs: [], stateGasUsed: '0x0', - status: 2, + status: '0x2', } as const const receipt = FrameReceipt.fromRpc(rpc) expect(z.decode(z.FrameReceipt.FrameReceipt, rpc)).toEqual(receipt) @@ -47,10 +51,11 @@ describe('FrameReceipt', () => { expect( z.safeDecode(z.FrameReceipt.FrameReceipt, { executionGasUsed: '0x0', + gasUsed: '0x0', logs: [], stateGasUsed: '0x0', // @ts-expect-error Invalid RPC status. - status: 3, + status: '0x3', }).success, ).toBe(false) }) @@ -60,6 +65,7 @@ describe('FrameReceiptToRpc', () => { test('numberish gas', () => { expect( z.encode(z.FrameReceipt.FrameReceiptToRpc, { + executionGasUsed: 21000, gasUsed: 21000, logs: [], stateGasUsed: '0x0', @@ -67,9 +73,10 @@ describe('FrameReceiptToRpc', () => { }), ).toEqual({ executionGasUsed: '0x5208', + gasUsed: '0x5208', logs: [], stateGasUsed: '0x0', - status: 1, + status: '0x1', }) }) }) diff --git a/src/zod/_test/FrameSignature.test.ts b/src/zod/_test/FrameSignature.test.ts index 648576ba..6bde2037 100644 --- a/src/zod/_test/FrameSignature.test.ts +++ b/src/zod/_test/FrameSignature.test.ts @@ -8,7 +8,7 @@ describe('FrameSignature', () => { expect( z.safeDecode(z.FrameSignature.FrameSignature, { msg: '0x', - scheme: 1, + scheme: '0x1', signature: '0x01', }).success, ).toBe(false) @@ -20,27 +20,27 @@ describe('FrameSignature', () => { scheme: 'arbitrary', signature: '0xaabb', }), - ).toEqual({ msg: '0x', scheme: 0, signature: '0xaabb' }) + ).toEqual({ msg: '0x', scheme: '0x0', signature: '0xaabb' }) expect( z.encode(z.FrameSignature.FrameSignature, { scheme: 'secp256k1', }), - ).toEqual({ msg: '0x', scheme: 1, signature: '0x' }) + ).toEqual({ msg: '0x', scheme: '0x1' }) expect( z.encode(z.FrameSignature.FrameSignature, { scheme: 'p256', }), - ).toEqual({ msg: '0x', scheme: 2, signature: '0x' }) + ).toEqual({ msg: '0x', scheme: '0x2' }) }) test('arbitrary bytes', () => { expect( z.encode(z.FrameSignature.FrameSignature, FrameSignature.from('0xaabb')), - ).toEqual({ msg: '0x', scheme: 0, signature: '0xaabb' }) + ).toEqual({ msg: '0x', scheme: '0x0', signature: '0xaabb' }) expect( z.decode(z.FrameSignature.FrameSignature, { msg: '0x', - scheme: 0, + scheme: '0x0', signature: '0xaabb', }), ).toEqual(FrameSignature.from('0xaabb')) @@ -54,7 +54,7 @@ describe('FrameSignature', () => { }), }) const rpc = z.encode(z.FrameSignature.FrameSignature, entry) - expect(rpc.scheme).toBe(1) + expect(rpc.scheme).toBe('0x1') expect(z.decode(z.FrameSignature.FrameSignature, rpc)).toEqual(entry) }) test('P-256 signature', () => { @@ -68,7 +68,7 @@ describe('FrameSignature', () => { }), }) const rpc = z.encode(z.FrameSignature.FrameSignature, entry) - expect(rpc.scheme).toBe(2) + expect(rpc.scheme).toBe('0x2') expect(z.decode(z.FrameSignature.FrameSignature, rpc)).toEqual({ ...entry, signature: { r: entry.signature.r, s: entry.signature.s }, diff --git a/src/zod/_test/TxEnvelopeEip8141.test.ts b/src/zod/_test/TxEnvelopeEip8141.test.ts index 4355cdad..08b637c1 100644 --- a/src/zod/_test/TxEnvelopeEip8141.test.ts +++ b/src/zod/_test/TxEnvelopeEip8141.test.ts @@ -78,7 +78,7 @@ describe('TxEnvelopeEip8141', () => { frames: [{ executionGas: 50_000 }], sender: accounts[0].address, type: 'eip8141', - }).frames[0]!.executionGasLimit, + }).frames[0]!.executionGas, ).toBe('0xc350') }) }) @@ -107,7 +107,7 @@ describe('invalid envelopes', () => { test('rejects malformed nested RPC signatures without throwing', () => { const rpc = { ...TxEnvelopeEip8141.toRpc(envelope), - signatures: [{ msg: '0x', scheme: 1, signature: '0x01' }], + signatures: [{ msg: '0x', scheme: '0x1', signature: '0x01' }], } as const expect( z.safeDecode(z.TxEnvelopeEip8141.TxEnvelopeEip8141, rpc).success, diff --git a/test/frames/chainspec.json b/test/frames/chainspec.json index a6df7916..f5585cb9 100644 --- a/test/frames/chainspec.json +++ b/test/frames/chainspec.json @@ -1,114 +1,58 @@ { - "name": "Ox frames", - "dataDir": "frames", - "engine": { - "NethDev": { - "params": {} - } - }, - "params": { - "gasLimitBoundDivisor": "0x0400", - "accountStartNonce": "0x0", - "maximumExtraDataSize": "0x20", - "minGasLimit": "0x1388", - "maxCodeSize": "0x6000", - "maxCodeSizeTransition": "0x0", - "chainID": "0x1fcd", - "networkID": "0x1fcd", - "eip140Transition": "0x0", - "eip145Transition": "0x0", - "eip150Transition": "0x0", - "eip155Transition": "0x0", - "eip160Transition": "0x0", - "eip161abcTransition": "0x0", - "eip161dTransition": "0x0", - "eip211Transition": "0x0", - "eip214Transition": "0x0", - "eip658Transition": "0x0", - "eip1014Transition": "0x0", - "eip1052Transition": "0x0", - "eip1283Transition": "0x0", - "eip1283DisableTransition": "0x0", - "eip152Transition": "0x0", - "eip1108Transition": "0x0", - "eip1344Transition": "0x0", - "eip1884Transition": "0x0", - "eip2028Transition": "0x0", - "eip2200Transition": "0x0", - "eip2537Transition": "0x0", - "eip2565Transition": "0x0", - "eip2929Transition": "0x0", - "eip2930Transition": "0x0", - "eip1559Transition": "0x0", - "eip3198Transition": "0x0", - "eip3529Transition": "0x0", - "eip3541Transition": "0x0", - "eip3651TransitionTimestamp": "0x0", - "eip3855TransitionTimestamp": "0x0", - "eip3860TransitionTimestamp": "0x0", - "eip4895TransitionTimestamp": "0x0", - "eip1153TransitionTimestamp": "0x0", - "eip4844TransitionTimestamp": "0x0", - "eip5656TransitionTimestamp": "0x0", - "eip6780TransitionTimestamp": "0x0", - "eip2537TransitionTimestamp": "0x0", - "eip7702TransitionTimestamp": "0x0", - "eip7623TransitionTimestamp": "0x0", - "blobSchedule": [ - { - "name": "prague", - "timestamp": "0x0", - "target": 6, - "max": 9, - "baseFeeUpdateFraction": "0x4c6964" - } - ], - "eip2780TransitionTimestamp": "0x0", - "eip7778TransitionTimestamp": "0x0", - "eip7825TransitionTimestamp": "0x0", - "eip7976TransitionTimestamp": "0x0", - "eip8037TransitionTimestamp": "0x0", - "eip7708TransitionTimestamp": "0x0", - "eip8141TransitionTimestamp": "0x0", - "eip7951TransitionTimestamp": "0x0", - "eip7594TransitionTimestamp": "0x0" + "config": { + "chainId": 8141, + "homesteadBlock": 0, + "daoForkSupport": true, + "eip150Block": 0, + "eip155Block": 0, + "eip158Block": 0, + "byzantiumBlock": 0, + "constantinopleBlock": 0, + "petersburgBlock": 0, + "istanbulBlock": 0, + "muirGlacierBlock": 0, + "berlinBlock": 0, + "londonBlock": 0, + "arrowGlacierBlock": 0, + "grayGlacierBlock": 0, + "shanghaiTime": 0, + "cancunTime": 0, + "terminalTotalDifficulty": "0x0", + "terminalTotalDifficultyPassed": true, + "pragueTime": 0, + "osakaTime": 0, + "amsterdamTime": 0, + "bogotaTime": 0 }, - "genesis": { - "seal": { - "ethereum": { - "nonce": "0x0000000000000042", - "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000" - } - }, - "difficulty": "0x1", - "author": "0x0000000000000000000000000000000000000000", - "timestamp": "0x0", - "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000", - "extraData": "0x", - "gasLimit": "0x1c9c380" - }, - "nodes": [], - "accounts": { - "0000000000000000000000000000000000008141": { + "nonce": "0x0", + "timestamp": "0x0", + "extraData": "0x00", + "gasLimit": "0x1c9c380", + "difficulty": "0x0", + "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "coinbase": "0x0000000000000000000000000000000000000000", + "alloc": { + "0x0000000000000000000000000000000000008141": { "balance": "0", "nonce": "1", "code": "0x60083614600a575f5ffd5b5f3560c01c4211601657005b5f5ffd" }, - "f39fd6e51aad88f6f4ce6ab8827279cfffb92266": { + "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266": { "balance": "1000000000000000000000" }, - "70997970c51812dc3a010c7d01b50e0d17dc79c8": { + "0x70997970c51812dc3a010c7d01b50e0d17dc79c8": { "balance": "1000000000000000000000" }, - "0000000000000000000000000000000000001000": { + "0x0000000000000000000000000000000000001000": { "balance": "0", "code": "0x5f5ffd", "nonce": "1" }, - "1ecba262e4510f333fb5051743e2a53a765debd0": { + "0x1ecba262e4510f333fb5051743e2a53a765debd0": { "balance": "1000000000000000000000", "code": "0x5f5fb4301460015fb46002141660025fb415166019575f5ffd5b60035f5faa", "nonce": "1" } - } + }, + "number": "0x0" } diff --git a/test/frames/prool.test.ts b/test/frames/prool.test.ts index 0a8fe037..c73faa8a 100644 --- a/test/frames/prool.test.ts +++ b/test/frames/prool.test.ts @@ -5,10 +5,10 @@ import { rpcUrl } from './prool.js' const rpc = RpcTransport.fromHttp(rpcUrl) -describe('nethermind', () => { +describe('reth', () => { test('starts a funded development chain', async () => { expect(await rpc.request({ method: 'web3_clientVersion' })).toContain( - 'Nethermind/', + 'reth/', ) expect(await rpc.request({ method: 'eth_chainId' })).toMatchInlineSnapshot( '"0x1fcd"', diff --git a/test/frames/prool.ts b/test/frames/prool.ts index bf274448..5c20bec4 100644 --- a/test/frames/prool.ts +++ b/test/frames/prool.ts @@ -9,17 +9,18 @@ import { export const port = Number(import.meta.env.VITE_FRAMES_PORT ?? 3001) export const rpcUrl = `http://localhost:${port}/${Number(import.meta.env.VITEST_POOL_ID ?? 1) + Math.floor(Math.random() * 10_000)}` -export const nethermind = Instance.define(() => { +export const reth = Instance.define(() => { let container: StartedTestContainer | undefined return { - name: 'nethermind', + name: 'reth', host: 'localhost', port: 8545, async start({ port = 8545 }, { emitter }) { container = await new GenericContainer( - 'ghcr.io/wevm/nethermind-frames@sha256:1f8e5b5698e18af41849fd95016271a96f8d7fd192014270c08d0306283dc07a', + 'ghcr.io/wevm/reth:sha-0acab10e8123', ) + .withPlatform('linux/amd64') .withExposedPorts({ container: 8545, host: port }) .withCopyFilesToContainer([ { @@ -28,22 +29,18 @@ export const nethermind = Instance.define(() => { }, ]) .withCommand([ - '--config', - 'spaceneth', - '--Init.ChainSpecPath', + 'node', + '--chain', '/tmp/frames.json', - '--Init.EnableUnsecuredDevWallet', - 'false', - '--Init.LogDirectory', - '/tmp/logs', - '--TxPool.BlobsSupport', - 'InMemory', - '--JsonRpc.Host', + '--dev', + '--http', + '--http.addr', '0.0.0.0', - '--JsonRpc.EnabledModules', - 'Eth,Net,Web3', + '--http.api', + 'eth,net,web3', + '--ipcdisable', ]) - .withWaitStrategy(Wait.forLogMessage('Initialization Completed')) + .withWaitStrategy(Wait.forLogMessage('RPC HTTP server started')) .withStartupTimeout(120_000) .withLogConsumer((stream) => { stream.on('data', (data) => { @@ -63,5 +60,5 @@ export const nethermind = Instance.define(() => { }) export function createServer() { - return Server.create({ instance: nethermind(), port }) + return Server.create({ instance: reth(), port }) }