From fcf7c0c4182917596aa77bfb9f98ec2213167d4e Mon Sep 17 00:00:00 2001 From: ch4r10t33r Date: Thu, 2 Apr 2026 16:41:00 +0100 Subject: [PATCH 1/9] eip8141: add Frame Transaction support Implements EIP-8141 (type 0x06) frame transaction encoding and decoding. - Add Frame, FrameMode, TransactionEIP8141, TransactionRequestEIP8141, TransactionSerializableEIP8141, and TransactionSerializedEIP8141 types - Add assertTransactionEIP8141 (validates chain ID, sender, frames, fees) - Add serializeTransactionEIP8141 (RLP: chainId, nonce, sender, frames, maxPriorityFeePerGas, maxFeePerGas, maxFeePerBlobGas, blobVersionedHashes) - Add parseTransactionEIP8141 (inverse of serialize) - Wire 0x06 type byte into getSerializedTransactionType - Wire frames-field inference into getTransactionType - Map RPC hex 0x6 -> 'eip8141' in formatters/transaction - Export all new types and assertTransactionEIP8141 from main index --- src/index.ts | 8 ++ src/types/transaction.ts | 134 ++++++++++++++++++ src/utils/formatters/transaction.ts | 1 + src/utils/transaction/assertTransaction.ts | 39 +++++ .../getSerializedTransactionType.ts | 7 + src/utils/transaction/getTransactionType.ts | 11 ++ src/utils/transaction/parseTransaction.ts | 87 ++++++++++++ src/utils/transaction/serializeTransaction.ts | 55 +++++++ 8 files changed, 342 insertions(+) diff --git a/src/index.ts b/src/index.ts index 227838e307..e225fffaef 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1214,12 +1214,15 @@ export type { } from './types/stateOverride.js' export type { AccessList, + Frame, + FrameMode, Transaction, TransactionBase, TransactionEIP1559, TransactionEIP2930, TransactionEIP4844, TransactionEIP7702, + TransactionEIP8141, TransactionLegacy, TransactionReceipt, TransactionRequest, @@ -1228,6 +1231,7 @@ export type { TransactionRequestEIP2930, TransactionRequestEIP4844, TransactionRequestEIP7702, + TransactionRequestEIP8141, TransactionRequestGeneric, TransactionRequestLegacy, TransactionSerializable, @@ -1236,6 +1240,7 @@ export type { TransactionSerializableEIP2930, TransactionSerializableEIP4844, TransactionSerializableEIP7702, + TransactionSerializableEIP8141, TransactionSerializableGeneric, TransactionSerializableLegacy, TransactionSerialized, @@ -1243,6 +1248,7 @@ export type { TransactionSerializedEIP2930, TransactionSerializedEIP4844, TransactionSerializedEIP7702, + TransactionSerializedEIP8141, TransactionSerializedGeneric, TransactionSerializedLegacy, TransactionType, @@ -1871,9 +1877,11 @@ export { export { type AssertTransactionEIP1559ErrorType, type AssertTransactionEIP2930ErrorType, + type AssertTransactionEIP8141ErrorType, type AssertTransactionLegacyErrorType, assertTransactionEIP1559, assertTransactionEIP2930, + assertTransactionEIP8141, assertTransactionLegacy, } from './utils/transaction/assertTransaction.js' export { diff --git a/src/types/transaction.ts b/src/types/transaction.ts index ce860a4eb1..fb11ca59bf 100644 --- a/src/types/transaction.ts +++ b/src/types/transaction.ts @@ -33,8 +33,35 @@ export type TransactionType = | 'eip2930' | 'eip4844' | 'eip7702' + | 'eip8141' | (string & {}) +/** EIP-8141 frame execution mode (lower 8 bits of `mode` field). */ +export type FrameMode = + | 0 // DEFAULT: execute as ENTRY_POINT (address 0xaa) + | 1 // VERIFY: read-only validation; must call APPROVE opcode + | 2 // SENDER: execute as tx.sender (requires prior approval) + +/** + * A single frame in an EIP-8141 Frame Transaction. + * Each frame captures one unit of execution, validation, or payment. + */ +export type Frame = { + /** + * Execution mode plus flag bits. + * Bits 0-7: FrameMode (0=DEFAULT, 1=VERIFY, 2=SENDER). + * Bits 8-9: approval scope constraint. + * Bit 10: atomic batch flag (SENDER mode only). + */ + mode: number + /** Target address for this frame, or `null` to use `tx.sender`. */ + target: Address | null + /** Gas allocated exclusively to this frame. */ + gasLimit: bigint + /** Input data passed to the frame. */ + data: Hex +} + export type TransactionReceipt< quantity = bigint, index = number, @@ -194,6 +221,49 @@ export type TransactionEIP7702< type: type } & FeeValuesEIP1559 +/** + * EIP-8141 Frame Transaction as returned by `eth_getTransactionByHash`. + * Authorization is embedded in frame data rather than a top-level ECDSA signature, + * so `r`, `s`, `v`, `yParity`, `to`, `value`, and `input` are absent. + */ +export type TransactionEIP8141< + quantity = bigint, + index = number, + isPending extends boolean = boolean, + type = 'eip8141', +> = { + /** Hash of block containing this transaction, or `null` if pending. */ + blockHash: isPending extends true ? null : Hash + /** Number of block containing this transaction, or `null` if pending. */ + blockNumber: isPending extends true ? null : quantity + /** Chain ID that this transaction is valid on. */ + chainId: index + /** List of versioned blob hashes (empty when no blobs are attached). */ + blobVersionedHashes: readonly Hex[] + /** Transaction sender address (explicit in the EIP-8141 envelope). */ + from: Address + /** Maximum fee per blob gas unit (EIP-4844 blob fee field). */ + maxFeePerBlobGas: quantity + /** Maximum total fee per gas unit. */ + maxFeePerGas: quantity + /** Maximum priority fee per gas unit (miner tip). */ + maxPriorityFeePerGas: quantity + /** Hash of this transaction. */ + hash: Hash + /** Unique number identifying this transaction. */ + nonce: index + /** Explicit sender address committed to in the transaction envelope. */ + sender: Address + /** Ordered list of execution frames. */ + frames: readonly Frame[] + /** Index of this transaction in the block, or `null` if pending. */ + transactionIndex: isPending extends true ? null : index + /** The type represented as hex. */ + typeHex: Hex | null + /** Transaction type identifier. */ + type: type +} + export type Transaction< quantity = bigint, index = number, @@ -204,6 +274,7 @@ export type Transaction< | TransactionEIP1559 | TransactionEIP4844 | TransactionEIP7702 + | TransactionEIP8141 > //////////////////////////////////////////////////////////////////////////////////////////// @@ -286,12 +357,43 @@ export type TransactionRequestEIP7702< authorizationList?: AuthorizationList | undefined } +/** + * EIP-8141 Frame Transaction request (for `eth_sendRawTransaction`). + * Authorization is carried inside frame data, so there is no top-level ECDSA + * signature and no `to`/`value`/`data` fields at the envelope level. + */ +export type TransactionRequestEIP8141< + quantity = bigint, + index = number, + type = 'eip8141', +> = { + /** Transaction type identifier. */ + type?: type | undefined + /** Chain ID that this transaction is valid on. */ + chainId?: number | undefined + /** Unique number identifying this transaction. */ + nonce?: index | undefined + /** Explicit sender address committed to in the transaction envelope. */ + sender: Address + /** Ordered list of execution frames. */ + frames: readonly Frame[] + /** Maximum priority fee per gas unit. */ + maxPriorityFeePerGas?: quantity | undefined + /** Maximum total fee per gas unit. */ + maxFeePerGas?: quantity | undefined + /** Maximum fee per blob gas unit (use 0 / omit when no blobs). */ + maxFeePerBlobGas?: quantity | undefined + /** List of versioned blob hashes (omit or leave empty when no blobs). */ + blobVersionedHashes?: readonly Hex[] | undefined +} + export type TransactionRequest = OneOf< | TransactionRequestLegacy | TransactionRequestEIP2930 | TransactionRequestEIP1559 | TransactionRequestEIP4844 | TransactionRequestEIP7702 + | TransactionRequestEIP8141 > export type TransactionRequestGeneric< @@ -316,6 +418,7 @@ export type TransactionSerializedEIP1559 = `0x02${string}` export type TransactionSerializedEIP2930 = `0x01${string}` export type TransactionSerializedEIP4844 = `0x03${string}` export type TransactionSerializedEIP7702 = `0x04${string}` +export type TransactionSerializedEIP8141 = `0x06${string}` export type TransactionSerializedLegacy = Branded<`0x${string}`, 'legacy'> export type TransactionSerializedGeneric = `0x${string}` export type TransactionSerialized< @@ -325,6 +428,7 @@ export type TransactionSerialized< | (type extends 'eip2930' ? TransactionSerializedEIP2930 : never) | (type extends 'eip4844' ? TransactionSerializedEIP4844 : never) | (type extends 'eip7702' ? TransactionSerializedEIP7702 : never) + | (type extends 'eip8141' ? TransactionSerializedEIP8141 : never) | (type extends 'legacy' ? TransactionSerializedLegacy : never), > = IsNever extends true ? TransactionSerializedGeneric : result @@ -403,12 +507,42 @@ export type TransactionSerializableEIP7702< yParity?: number | undefined } +/** + * EIP-8141 Frame Transaction ready for RLP serialization. + * Does not extend `TransactionSerializableBase` because there is no ECDSA + * signature envelope; authorization lives inside the VERIFY frame data. + */ +export type TransactionSerializableEIP8141< + quantity = bigint, + index = number, +> = { + /** Chain ID that this transaction is valid on. */ + chainId: number + /** Unique number identifying this transaction. */ + nonce?: index | undefined + /** Explicit sender address committed to in the transaction envelope. */ + sender: Address + /** Ordered list of execution frames (1 to MAX_FRAMES = 1000). */ + frames: readonly Frame[] + /** Maximum priority fee per gas unit. */ + maxPriorityFeePerGas?: quantity | undefined + /** Maximum total fee per gas unit. */ + maxFeePerGas?: quantity | undefined + /** Maximum fee per blob gas unit (must be 0 when no blobs are included). */ + maxFeePerBlobGas?: quantity | undefined + /** Versioned blob hashes (must be empty when `maxFeePerBlobGas` is 0). */ + blobVersionedHashes?: readonly Hex[] | undefined + /** Transaction type discriminant. */ + type?: 'eip8141' | undefined +} + export type TransactionSerializable = OneOf< | TransactionSerializableLegacy | TransactionSerializableEIP2930 | TransactionSerializableEIP1559 | TransactionSerializableEIP4844 | TransactionSerializableEIP7702 + | TransactionSerializableEIP8141 > export type TransactionSerializableGeneric< diff --git a/src/utils/formatters/transaction.ts b/src/utils/formatters/transaction.ts index 6f3ff04ddf..7809b56f42 100644 --- a/src/utils/formatters/transaction.ts +++ b/src/utils/formatters/transaction.ts @@ -41,6 +41,7 @@ export const transactionType = { '0x2': 'eip1559', '0x3': 'eip4844', '0x4': 'eip7702', + '0x6': 'eip8141', } as const satisfies Record export type FormatTransactionErrorType = ErrorType diff --git a/src/utils/transaction/assertTransaction.ts b/src/utils/transaction/assertTransaction.ts index 03b18dcdbe..7ed8311244 100644 --- a/src/utils/transaction/assertTransaction.ts +++ b/src/utils/transaction/assertTransaction.ts @@ -29,6 +29,7 @@ import type { TransactionSerializableEIP2930, TransactionSerializableEIP4844, TransactionSerializableEIP7702, + TransactionSerializableEIP8141, TransactionSerializableLegacy, } from '../../types/transaction.js' import { type IsAddressErrorType, isAddress } from '../address/isAddress.js' @@ -36,6 +37,44 @@ import { size } from '../data/size.js' import { slice } from '../data/slice.js' import { hexToNumber } from '../encoding/fromHex.js' +export type AssertTransactionEIP8141ErrorType = + | InvalidAddressErrorType + | InvalidChainIdErrorType + | BaseErrorType + | ErrorType + +export function assertTransactionEIP8141( + transaction: TransactionSerializableEIP8141, +) { + const { chainId, sender, frames, maxFeePerGas, maxPriorityFeePerGas } = + transaction + if (chainId <= 0) throw new InvalidChainIdError({ chainId }) + if (!isAddress(sender)) throw new InvalidAddressError({ address: sender }) + if (!frames || frames.length === 0) + throw new BaseError('`frames` must contain at least one frame.') + if (frames.length > 1000) + throw new BaseError( + '`frames` must not exceed MAX_FRAMES (1000) per EIP-8141.', + ) + for (const frame of frames) { + const execMode = frame.mode & 0xff + if (execMode > 2) + throw new BaseError( + `Invalid frame execution mode ${execMode}. Must be 0 (DEFAULT), 1 (VERIFY), or 2 (SENDER).`, + ) + if (frame.target !== null && !isAddress(frame.target)) + throw new InvalidAddressError({ address: frame.target }) + } + if (maxFeePerGas && maxFeePerGas > maxUint256) + throw new FeeCapTooHighError({ maxFeePerGas }) + if ( + maxPriorityFeePerGas && + maxFeePerGas && + maxPriorityFeePerGas > maxFeePerGas + ) + throw new TipAboveFeeCapError({ maxFeePerGas, maxPriorityFeePerGas }) +} + export type AssertTransactionEIP7702ErrorType = | AssertTransactionEIP1559ErrorType | InvalidAddressErrorType diff --git a/src/utils/transaction/getSerializedTransactionType.ts b/src/utils/transaction/getSerializedTransactionType.ts index d6284b8f17..51561335a5 100644 --- a/src/utils/transaction/getSerializedTransactionType.ts +++ b/src/utils/transaction/getSerializedTransactionType.ts @@ -10,6 +10,7 @@ import type { TransactionSerializedEIP2930, TransactionSerializedEIP4844, TransactionSerializedEIP7702, + TransactionSerializedEIP8141, TransactionSerializedGeneric, TransactionSerializedLegacy, TransactionType, @@ -34,6 +35,9 @@ export type GetSerializedTransactionType< | (serializedTransaction extends TransactionSerializedEIP7702 ? 'eip7702' : never) + | (serializedTransaction extends TransactionSerializedEIP8141 + ? 'eip8141' + : never) | (serializedTransaction extends TransactionSerializedLegacy ? 'legacy' : never), @@ -56,6 +60,9 @@ export function getSerializedTransactionType< ): GetSerializedTransactionType { const serializedType = sliceHex(serializedTransaction, 0, 1) + if (serializedType === '0x06') + return 'eip8141' as GetSerializedTransactionType + if (serializedType === '0x04') return 'eip7702' as GetSerializedTransactionType diff --git a/src/utils/transaction/getTransactionType.ts b/src/utils/transaction/getTransactionType.ts index 777c7e9eab..889a0d6ee2 100644 --- a/src/utils/transaction/getTransactionType.ts +++ b/src/utils/transaction/getTransactionType.ts @@ -13,6 +13,7 @@ import type { TransactionSerializableEIP2930, TransactionSerializableEIP4844, TransactionSerializableEIP7702, + TransactionSerializableEIP8141, TransactionSerializableGeneric, } from '../../types/transaction.js' import type { Assign, ExactPartial, IsNever, OneOf } from '../../types/utils.js' @@ -27,6 +28,7 @@ export type GetTransactionType< | (transaction extends EIP2930Properties ? 'eip2930' : never) | (transaction extends EIP4844Properties ? 'eip4844' : never) | (transaction extends EIP7702Properties ? 'eip7702' : never) + | (transaction extends EIP8141Properties ? 'eip8141' : never) | (transaction['type'] extends TransactionSerializableGeneric['type'] ? Extract : never), @@ -48,6 +50,12 @@ export function getTransactionType< if (transaction.type) return transaction.type as GetTransactionType + if ( + typeof (transaction as TransactionSerializableEIP8141).frames !== + 'undefined' + ) + return 'eip8141' as any + if (typeof transaction.authorizationList !== 'undefined') return 'eip7702' as any @@ -132,3 +140,6 @@ type EIP7702Properties = Assign< authorizationList: TransactionSerializableEIP7702['authorizationList'] } > +type EIP8141Properties = { + frames: TransactionSerializableEIP8141['frames'] +} diff --git a/src/utils/transaction/parseTransaction.ts b/src/utils/transaction/parseTransaction.ts index 8432398d44..8667c95230 100644 --- a/src/utils/transaction/parseTransaction.ts +++ b/src/utils/transaction/parseTransaction.ts @@ -16,6 +16,7 @@ import type { import type { Hex, Signature } from '../../types/misc.js' import type { AccessList, + Frame, TransactionRequestEIP2930, TransactionRequestLegacy, TransactionSerializable, @@ -23,12 +24,14 @@ import type { TransactionSerializableEIP2930, TransactionSerializableEIP4844, TransactionSerializableEIP7702, + TransactionSerializableEIP8141, TransactionSerializableLegacy, TransactionSerialized, TransactionSerializedEIP1559, TransactionSerializedEIP2930, TransactionSerializedEIP4844, TransactionSerializedEIP7702, + TransactionSerializedEIP8141, TransactionSerializedGeneric, TransactionType, } from '../../types/transaction.js' @@ -53,11 +56,13 @@ import { type AssertTransactionEIP2930ErrorType, type AssertTransactionEIP4844ErrorType, type AssertTransactionEIP7702ErrorType, + type AssertTransactionEIP8141ErrorType, type AssertTransactionLegacyErrorType, assertTransactionEIP1559, assertTransactionEIP2930, assertTransactionEIP4844, assertTransactionEIP7702, + assertTransactionEIP8141, assertTransactionLegacy, } from './assertTransaction.js' import { @@ -77,6 +82,7 @@ export type ParseTransactionReturnType< ? TransactionSerializableEIP4844 : never) | (type extends 'eip7702' ? TransactionSerializableEIP7702 : never) + | (type extends 'eip8141' ? TransactionSerializableEIP8141 : never) | (type extends 'legacy' ? TransactionSerializableLegacy : never) : TransactionSerializable @@ -86,6 +92,7 @@ export type ParseTransactionErrorType = | ParseTransactionEIP2930ErrorType | ParseTransactionEIP4844ErrorType | ParseTransactionEIP7702ErrorType + | ParseTransactionEIP8141ErrorType | ParseTransactionLegacyErrorType export function parseTransaction< @@ -113,11 +120,91 @@ export function parseTransaction< serializedTransaction as TransactionSerializedEIP7702, ) as ParseTransactionReturnType + if (type === 'eip8141') + return parseTransactionEIP8141( + serializedTransaction as TransactionSerializedEIP8141, + ) as ParseTransactionReturnType + return parseTransactionLegacy( serializedTransaction, ) as ParseTransactionReturnType } +type ParseTransactionEIP8141ErrorType = + | ToTransactionArrayErrorType + | AssertTransactionEIP8141ErrorType + | HexToBigIntErrorType + | HexToNumberErrorType + | InvalidSerializedTransactionErrorType + | IsHexErrorType + | ErrorType + +function parseTransactionEIP8141( + serializedTransaction: TransactionSerializedEIP8141, +): TransactionSerializableEIP8141 { + const transactionArray = toTransactionArray(serializedTransaction) + + const [ + chainId, + nonce, + sender, + framesArray, + maxPriorityFeePerGas, + maxFeePerGas, + maxFeePerBlobGas, + blobVersionedHashes, + ] = transactionArray + + if (transactionArray.length !== 8) + throw new InvalidSerializedTransactionError({ + attributes: { + chainId, + nonce, + sender, + frames: framesArray, + maxPriorityFeePerGas, + maxFeePerGas, + maxFeePerBlobGas, + blobVersionedHashes, + }, + serializedTransaction, + type: 'eip8141', + }) + + const frames: Frame[] = (framesArray as RecursiveArray[]).map( + (frameArray) => { + const [mode, target, gasLimit, data] = frameArray as Hex[] + return { + mode: mode === '0x' ? 0 : hexToNumber(mode), + target: isHex(target) && target !== '0x' ? target : null, + gasLimit: gasLimit === '0x' ? 0n : hexToBigInt(gasLimit), + data: isHex(data) && data !== '0x' ? data : '0x', + } + }, + ) + + const transaction: TransactionSerializableEIP8141 = { + chainId: hexToNumber(chainId as Hex), + sender: sender as Hex, + frames, + type: 'eip8141', + } + + if (isHex(nonce)) transaction.nonce = nonce === '0x' ? 0 : hexToNumber(nonce) + if (isHex(maxFeePerGas) && maxFeePerGas !== '0x') + transaction.maxFeePerGas = hexToBigInt(maxFeePerGas) + if (isHex(maxPriorityFeePerGas) && maxPriorityFeePerGas !== '0x') + transaction.maxPriorityFeePerGas = hexToBigInt(maxPriorityFeePerGas) + if (isHex(maxFeePerBlobGas) && maxFeePerBlobGas !== '0x') + transaction.maxFeePerBlobGas = hexToBigInt(maxFeePerBlobGas) + if (Array.isArray(blobVersionedHashes) && blobVersionedHashes.length > 0) + transaction.blobVersionedHashes = blobVersionedHashes as Hex[] + + assertTransactionEIP8141(transaction) + + return transaction +} + type ParseTransactionEIP7702ErrorType = | ToTransactionArrayErrorType | AssertTransactionEIP7702ErrorType diff --git a/src/utils/transaction/serializeTransaction.ts b/src/utils/transaction/serializeTransaction.ts index 060eb8a6bb..503a38ae50 100644 --- a/src/utils/transaction/serializeTransaction.ts +++ b/src/utils/transaction/serializeTransaction.ts @@ -15,6 +15,7 @@ import type { TransactionSerializableEIP2930, TransactionSerializableEIP4844, TransactionSerializableEIP7702, + TransactionSerializableEIP8141, TransactionSerializableGeneric, TransactionSerializableLegacy, TransactionSerialized, @@ -22,6 +23,7 @@ import type { TransactionSerializedEIP2930, TransactionSerializedEIP4844, TransactionSerializedEIP7702, + TransactionSerializedEIP8141, TransactionSerializedLegacy, TransactionType, } from '../../types/transaction.js' @@ -60,11 +62,13 @@ import { type AssertTransactionEIP2930ErrorType, type AssertTransactionEIP4844ErrorType, type AssertTransactionEIP7702ErrorType, + type AssertTransactionEIP8141ErrorType, type AssertTransactionLegacyErrorType, assertTransactionEIP1559, assertTransactionEIP2930, assertTransactionEIP4844, assertTransactionEIP7702, + assertTransactionEIP8141, assertTransactionLegacy, } from './assertTransaction.js' import { @@ -103,6 +107,7 @@ export type SerializeTransactionErrorType = | SerializeTransactionEIP2930ErrorType | SerializeTransactionEIP4844ErrorType | SerializeTransactionEIP7702ErrorType + | SerializeTransactionEIP8141ErrorType | SerializeTransactionLegacyErrorType | ErrorType @@ -140,12 +145,62 @@ export function serializeTransaction< signature, ) as SerializedTransactionReturnType + if (type === 'eip8141') + return serializeTransactionEIP8141( + transaction as TransactionSerializableEIP8141, + ) as SerializedTransactionReturnType + return serializeTransactionLegacy( transaction as TransactionSerializableLegacy, signature as SignatureLegacy, ) as SerializedTransactionReturnType } +type SerializeTransactionEIP8141ErrorType = + | AssertTransactionEIP8141ErrorType + | ConcatHexErrorType + | NumberToHexErrorType + | ToRlpErrorType + | ErrorType + +function serializeTransactionEIP8141( + transaction: TransactionSerializableEIP8141, +): TransactionSerializedEIP8141 { + const { + chainId, + nonce, + sender, + frames, + maxPriorityFeePerGas, + maxFeePerGas, + maxFeePerBlobGas, + blobVersionedHashes, + } = transaction + + assertTransactionEIP8141(transaction) + + const serializedFrames = frames.map((frame) => [ + numberToHex(frame.mode), + frame.target ?? '0x', + numberToHex(frame.gasLimit), + frame.data ?? '0x', + ]) + + return concatHex([ + '0x06', + toRlp([ + numberToHex(chainId), + nonce ? numberToHex(nonce) : '0x', + sender, + serializedFrames, + maxPriorityFeePerGas ? numberToHex(maxPriorityFeePerGas) : '0x', + maxFeePerGas ? numberToHex(maxFeePerGas) : '0x', + maxFeePerBlobGas ? numberToHex(maxFeePerBlobGas) : '0x', + blobVersionedHashes ?? [], + ]), + ]) as TransactionSerializedEIP8141 +} + type SerializeTransactionEIP7702ErrorType = | AssertTransactionEIP7702ErrorType | SerializeAuthorizationListErrorType From 07756f41297b79b49fad9ad4676f3bf20ec84e71 Mon Sep 17 00:00:00 2001 From: ch4r10t33r Date: Wed, 15 Apr 2026 18:57:27 +0100 Subject: [PATCH 2/9] eip8141: fix spec compliance and add tests - Add missing `flags` field to Frame type (spec requires 5-element tuple: [mode, flags, target, gas_limit, data]) - Fix MAX_FRAMES from 1000 to 64 per spec - Add spec-required validations: flags < 8, VERIFY frames need non-zero APPROVE scope, atomic batch flag constraints, gas_limit bounds (2^63-1), nonce bounds (2^64), zero-address sender rejection - Update serialization and parsing for 5-element frame tuples - Add 46 tests covering roundtrips, all assertion rules, spec examples, and edge cases --- src/types/transaction.ts | 14 +- .../transaction/assertTransaction.test.ts | 103 +++ src/utils/transaction/assertTransaction.ts | 57 +- src/utils/transaction/eip8141.test.ts | 760 ++++++++++++++++++ .../getSerializedTransactionType.test.ts | 6 + .../transaction/getTransactionType.test.ts | 15 + src/utils/transaction/parseTransaction.ts | 3 +- src/utils/transaction/serializeTransaction.ts | 1 + 8 files changed, 945 insertions(+), 14 deletions(-) create mode 100644 src/utils/transaction/eip8141.test.ts diff --git a/src/types/transaction.ts b/src/types/transaction.ts index fb11ca59bf..4cda4df7b9 100644 --- a/src/types/transaction.ts +++ b/src/types/transaction.ts @@ -47,13 +47,15 @@ export type FrameMode = * Each frame captures one unit of execution, validation, or payment. */ export type Frame = { + /** Execution mode: 0=DEFAULT, 1=VERIFY, 2=SENDER. */ + mode: FrameMode /** - * Execution mode plus flag bits. - * Bits 0-7: FrameMode (0=DEFAULT, 1=VERIFY, 2=SENDER). - * Bits 8-9: approval scope constraint. - * Bit 10: atomic batch flag (SENDER mode only). + * Flag bits configuring execution constraints. + * Bits 0-1: approval scope (APPROVE_SCOPE_MASK = 0x03). + * Bit 2: atomic batch flag (SENDER mode only). + * Bits 3-7: reserved, must be zero. */ - mode: number + flags: number /** Target address for this frame, or `null` to use `tx.sender`. */ target: Address | null /** Gas allocated exclusively to this frame. */ @@ -522,7 +524,7 @@ export type TransactionSerializableEIP8141< nonce?: index | undefined /** Explicit sender address committed to in the transaction envelope. */ sender: Address - /** Ordered list of execution frames (1 to MAX_FRAMES = 1000). */ + /** Ordered list of execution frames (1 to MAX_FRAMES = 64). */ frames: readonly Frame[] /** Maximum priority fee per gas unit. */ maxPriorityFeePerGas?: quantity | undefined diff --git a/src/utils/transaction/assertTransaction.test.ts b/src/utils/transaction/assertTransaction.test.ts index 6d0c88ebbd..5618106efe 100644 --- a/src/utils/transaction/assertTransaction.test.ts +++ b/src/utils/transaction/assertTransaction.test.ts @@ -6,9 +6,112 @@ import { assertTransactionEIP2930, assertTransactionEIP4844, assertTransactionEIP7702, + assertTransactionEIP8141, assertTransactionLegacy, } from './assertTransaction.js' +const sender = '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266' as const + +describe('eip8141', () => { + const validTx = { + chainId: 1, + sender, + frames: [ + { mode: 1 as const, flags: 0x03, target: null, gasLimit: 50000n, data: '0xab' as const }, + ], + } + + test('valid transaction passes', () => { + expect(() => assertTransactionEIP8141(validTx)).not.toThrow() + }) + + test('zero-address sender rejected', () => { + expect(() => + assertTransactionEIP8141({ + ...validTx, + sender: '0x0000000000000000000000000000000000000000', + }), + ).toThrow('zero address') + }) + + test('MAX_FRAMES is 64', () => { + const frames = Array.from({ length: 65 }, () => ({ + mode: 0 as const, + flags: 0, + target: sender, + gasLimit: 1n, + data: '0x' as const, + })) + expect(() => + assertTransactionEIP8141({ ...validTx, frames }), + ).toThrow('MAX_FRAMES (64)') + }) + + test('VERIFY with zero scope rejected', () => { + expect(() => + assertTransactionEIP8141({ + ...validTx, + frames: [ + { mode: 1, flags: 0x00, target: null, gasLimit: 1n, data: '0x' as const }, + ], + }), + ).toThrow('non-zero APPROVE scope') + }) + + test('reserved flag bits rejected', () => { + expect(() => + assertTransactionEIP8141({ + ...validTx, + frames: [ + { mode: 2, flags: 0x08, target: null, gasLimit: 1n, data: '0x' as const }, + ], + }), + ).toThrow('reserved') + }) + + test('atomic batch on DEFAULT rejected', () => { + expect(() => + assertTransactionEIP8141({ + ...validTx, + frames: [ + { mode: 0, flags: 0x04, target: sender, gasLimit: 1n, data: '0x' as const }, + { mode: 2, flags: 0x00, target: null, gasLimit: 1n, data: '0x' as const }, + ], + }), + ).toThrow('only valid with SENDER') + }) + + test('gas limit per frame bounded to 2^63-1', () => { + expect(() => + assertTransactionEIP8141({ + ...validTx, + frames: [ + { mode: 1, flags: 0x03, target: null, gasLimit: 2n ** 63n, data: '0x' as const }, + ], + }), + ).toThrow('gasLimit') + }) + + test('fee cap too high', () => { + expect(() => + assertTransactionEIP8141({ + ...validTx, + maxFeePerGas: maxUint256 + 1n, + }), + ).toThrow() + }) + + test('tip above fee cap', () => { + expect(() => + assertTransactionEIP8141({ + ...validTx, + maxFeePerGas: parseGwei('1'), + maxPriorityFeePerGas: parseGwei('2'), + }), + ).toThrow() + }) +}) + describe('eip7702', () => { test('invalid chainId', () => { expect(() => diff --git a/src/utils/transaction/assertTransaction.ts b/src/utils/transaction/assertTransaction.ts index 7ed8311244..2634d03141 100644 --- a/src/utils/transaction/assertTransaction.ts +++ b/src/utils/transaction/assertTransaction.ts @@ -43,27 +43,70 @@ export type AssertTransactionEIP8141ErrorType = | BaseErrorType | ErrorType +const maxUint64 = 2n ** 64n - 1n +const maxInt64 = 2n ** 63n - 1n +const MAX_FRAMES = 64 +const VERIFY = 1 +const SENDER = 2 +const APPROVE_SCOPE_MASK = 0x03 +const ATOMIC_BATCH_FLAG = 0x04 + export function assertTransactionEIP8141( transaction: TransactionSerializableEIP8141, ) { - const { chainId, sender, frames, maxFeePerGas, maxPriorityFeePerGas } = + const { chainId, sender, frames, nonce, maxFeePerGas, maxPriorityFeePerGas } = transaction if (chainId <= 0) throw new InvalidChainIdError({ chainId }) if (!isAddress(sender)) throw new InvalidAddressError({ address: sender }) + if (sender === '0x0000000000000000000000000000000000000000') + throw new BaseError('`sender` must not be the zero address.') + if (typeof nonce === 'number' && BigInt(nonce) > maxUint64) + throw new BaseError('`nonce` must be less than 2^64.') if (!frames || frames.length === 0) throw new BaseError('`frames` must contain at least one frame.') - if (frames.length > 1000) + if (frames.length > MAX_FRAMES) throw new BaseError( - '`frames` must not exceed MAX_FRAMES (1000) per EIP-8141.', + `\`frames\` must not exceed MAX_FRAMES (${MAX_FRAMES}) per EIP-8141.`, ) - for (const frame of frames) { - const execMode = frame.mode & 0xff - if (execMode > 2) + let totalFrameGas = 0n + for (let i = 0; i < frames.length; i++) { + const frame = frames[i] + if (frame.mode > 2) + throw new BaseError( + `Invalid frame mode ${frame.mode}. Must be 0 (DEFAULT), 1 (VERIFY), or 2 (SENDER).`, + ) + if (frame.flags >= 8) throw new BaseError( - `Invalid frame execution mode ${execMode}. Must be 0 (DEFAULT), 1 (VERIFY), or 2 (SENDER).`, + `Invalid frame flags ${frame.flags}. Bits 3-7 are reserved and must be zero.`, ) + if ( + frame.mode === VERIFY && + (frame.flags & APPROVE_SCOPE_MASK) === 0 + ) + throw new BaseError( + 'VERIFY frames must permit a non-zero APPROVE scope (flags bits 0-1).', + ) + if (frame.flags & ATOMIC_BATCH_FLAG) { + if (frame.mode !== SENDER) + throw new BaseError( + 'Atomic batch flag (bit 2) is only valid with SENDER mode.', + ) + if (i + 1 >= frames.length) + throw new BaseError( + 'Frame with atomic batch flag must not be the last frame.', + ) + if (frames[i + 1].mode !== SENDER) + throw new BaseError( + 'Frame following an atomic batch frame must be SENDER mode.', + ) + } if (frame.target !== null && !isAddress(frame.target)) throw new InvalidAddressError({ address: frame.target }) + if (frame.gasLimit > maxInt64) + throw new BaseError('`frame.gasLimit` must be <= 2^63 - 1.') + totalFrameGas += frame.gasLimit + if (totalFrameGas > maxInt64) + throw new BaseError('Total frame gas must be <= 2^63 - 1.') } if (maxFeePerGas && maxFeePerGas > maxUint256) throw new FeeCapTooHighError({ maxFeePerGas }) diff --git a/src/utils/transaction/eip8141.test.ts b/src/utils/transaction/eip8141.test.ts new file mode 100644 index 0000000000..b4c4dff41f --- /dev/null +++ b/src/utils/transaction/eip8141.test.ts @@ -0,0 +1,760 @@ +import { describe, expect, test } from 'vitest' +import { maxUint256 } from '../../constants/number.js' +import type { + TransactionSerializableEIP8141, + TransactionSerializedEIP8141, +} from '../../types/transaction.js' +import { parseGwei } from '../unit/parseGwei.js' +import { assertTransactionEIP8141 } from './assertTransaction.js' +import { getSerializedTransactionType } from './getSerializedTransactionType.js' +import { getTransactionType } from './getTransactionType.js' +import { parseTransaction } from './parseTransaction.js' +import { serializeTransaction } from './serializeTransaction.js' + +const sender = '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266' as const + +const baseEIP8141: TransactionSerializableEIP8141 = { + chainId: 1, + nonce: 0, + sender, + frames: [ + { + mode: 1, + flags: 0x03, + target: null, + gasLimit: 50000n, + data: '0xdeadbeef', + }, + { + mode: 2, + flags: 0x00, + target: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + gasLimit: 100000n, + data: '0xcafebabe', + }, + ], + maxPriorityFeePerGas: parseGwei('1'), + maxFeePerGas: parseGwei('10'), + maxFeePerBlobGas: 0n, + blobVersionedHashes: [], +} + +describe('eip8141 serialization', () => { + test('roundtrip: serialize then parse', () => { + const serialized = serializeTransaction(baseEIP8141) + expect(serialized.startsWith('0x06')).toBe(true) + const parsed = parseTransaction(serialized) + const { maxFeePerBlobGas: _, blobVersionedHashes: __, ...expected } = baseEIP8141 + expect(parsed).toEqual({ + ...expected, + type: 'eip8141', + }) + }) + + test('minimal transaction (no optional fields)', () => { + const tx: TransactionSerializableEIP8141 = { + chainId: 1, + sender, + frames: [ + { + mode: 1, + flags: 0x03, + target: null, + gasLimit: 21000n, + data: '0x00', + }, + ], + } + const serialized = serializeTransaction(tx) + expect(serialized.startsWith('0x06')).toBe(true) + const parsed = parseTransaction(serialized) + expect(parsed.chainId).toBe(1) + expect(parsed.sender).toBe(sender) + expect(parsed.frames).toHaveLength(1) + expect(parsed.frames[0].mode).toBe(1) + expect(parsed.frames[0].flags).toBe(0x03) + expect(parsed.frames[0].target).toBeNull() + expect(parsed.frames[0].gasLimit).toBe(21000n) + }) + + test('preserves flags field through roundtrip', () => { + const tx: TransactionSerializableEIP8141 = { + chainId: 1, + sender, + frames: [ + { + mode: 1, + flags: 0x01, + target: null, + gasLimit: 50000n, + data: '0xab', + }, + { + mode: 1, + flags: 0x02, + target: null, + gasLimit: 50000n, + data: '0xcd', + }, + { + mode: 1, + flags: 0x03, + target: null, + gasLimit: 50000n, + data: '0xef', + }, + ], + } + const serialized = serializeTransaction(tx) + const parsed = parseTransaction(serialized) + expect(parsed.frames[0].flags).toBe(0x01) + expect(parsed.frames[1].flags).toBe(0x02) + expect(parsed.frames[2].flags).toBe(0x03) + }) + + test('atomic batch flag roundtrip', () => { + const tx: TransactionSerializableEIP8141 = { + chainId: 1, + sender, + frames: [ + { mode: 1, flags: 0x03, target: null, gasLimit: 50000n, data: '0xaa' }, + { + mode: 2, + flags: 0x04, + target: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + gasLimit: 100000n, + data: '0xbb', + }, + { + mode: 2, + flags: 0x00, + target: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + gasLimit: 100000n, + data: '0xcc', + }, + ], + } + const serialized = serializeTransaction(tx) + const parsed = parseTransaction(serialized) + expect(parsed.frames[1].flags).toBe(0x04) + expect(parsed.frames[2].flags).toBe(0x00) + }) + + test('all three modes', () => { + const tx: TransactionSerializableEIP8141 = { + chainId: 1, + sender, + frames: [ + { mode: 0, flags: 0x00, target: sender, gasLimit: 10000n, data: '0x' }, + { + mode: 1, + flags: 0x03, + target: null, + gasLimit: 50000n, + data: '0xdeadbeef', + }, + { + mode: 2, + flags: 0x00, + target: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + gasLimit: 100000n, + data: '0xcafebabe', + }, + ], + } + const serialized = serializeTransaction(tx) + const parsed = parseTransaction(serialized) + expect(parsed.frames[0].mode).toBe(0) + expect(parsed.frames[1].mode).toBe(1) + expect(parsed.frames[2].mode).toBe(2) + }) + + test('fee fields preserved', () => { + const serialized = serializeTransaction(baseEIP8141) + const parsed = parseTransaction(serialized) + expect(parsed.maxPriorityFeePerGas).toBe(parseGwei('1')) + expect(parsed.maxFeePerGas).toBe(parseGwei('10')) + }) + + test('blob fields preserved when present', () => { + const tx: TransactionSerializableEIP8141 = { + ...baseEIP8141, + maxFeePerBlobGas: 1000n, + blobVersionedHashes: [ + '0x01febabecafebabecafebabecafebabecafebabecafebabecafebabecafebabe', + ], + } + const serialized = serializeTransaction(tx) + const parsed = parseTransaction(serialized) + expect(parsed.maxFeePerBlobGas).toBe(1000n) + expect(parsed.blobVersionedHashes).toEqual([ + '0x01febabecafebabecafebabecafebabecafebabecafebabecafebabecafebabe', + ]) + }) + + test('nonce field preserved', () => { + const tx: TransactionSerializableEIP8141 = { + ...baseEIP8141, + nonce: 42, + } + const serialized = serializeTransaction(tx) + const parsed = parseTransaction(serialized) + expect(parsed.nonce).toBe(42) + }) +}) + +describe('eip8141 getTransactionType', () => { + test('infers eip8141 from frames property', () => { + expect( + getTransactionType({ + frames: baseEIP8141.frames, + sender, + chainId: 1, + } as any), + ).toBe('eip8141') + }) + + test('infers eip8141 from explicit type', () => { + expect(getTransactionType({ type: 'eip8141' } as any)).toBe('eip8141') + }) +}) + +describe('eip8141 getSerializedTransactionType', () => { + test('identifies 0x06 prefix as eip8141', () => { + expect(getSerializedTransactionType('0x06abc')).toBe('eip8141') + }) +}) + +describe('eip8141 assertTransaction', () => { + test('valid transaction passes', () => { + expect(() => assertTransactionEIP8141(baseEIP8141)).not.toThrow() + }) + + test('invalid chainId', () => { + expect(() => + assertTransactionEIP8141({ ...baseEIP8141, chainId: 0 }), + ).toThrow('Chain ID') + }) + + test('invalid sender address', () => { + expect(() => + assertTransactionEIP8141({ + ...baseEIP8141, + sender: '0xinvalid' as any, + }), + ).toThrow('invalid') + }) + + test('zero-address sender rejected', () => { + expect(() => + assertTransactionEIP8141({ + ...baseEIP8141, + sender: '0x0000000000000000000000000000000000000000', + }), + ).toThrow('zero address') + }) + + test('empty frames', () => { + expect(() => + assertTransactionEIP8141({ ...baseEIP8141, frames: [] }), + ).toThrow('at least one frame') + }) + + test('exceeds MAX_FRAMES (64)', () => { + const frames = Array.from({ length: 65 }, () => ({ + mode: 0 as const, + flags: 0, + target: sender, + gasLimit: 1n, + data: '0x' as const, + })) + expect(() => + assertTransactionEIP8141({ ...baseEIP8141, frames }), + ).toThrow('MAX_FRAMES (64)') + }) + + test('exactly MAX_FRAMES (64) passes', () => { + const frames = Array.from({ length: 64 }, () => ({ + mode: 0 as const, + flags: 0, + target: sender, + gasLimit: 1n, + data: '0x' as const, + })) + expect(() => + assertTransactionEIP8141({ ...baseEIP8141, frames }), + ).not.toThrow() + }) + + test('invalid frame mode (>2)', () => { + expect(() => + assertTransactionEIP8141({ + ...baseEIP8141, + frames: [{ mode: 3 as any, flags: 0, target: null, gasLimit: 1n, data: '0x' }], + }), + ).toThrow('Invalid frame mode') + }) + + test('invalid frame flags (>=8, reserved bits set)', () => { + expect(() => + assertTransactionEIP8141({ + ...baseEIP8141, + frames: [ + { mode: 2, flags: 8, target: null, gasLimit: 1n, data: '0x' }, + ], + }), + ).toThrow('Bits 3-7 are reserved') + }) + + test('flags=0xff rejected', () => { + expect(() => + assertTransactionEIP8141({ + ...baseEIP8141, + frames: [ + { mode: 2, flags: 0xff, target: null, gasLimit: 1n, data: '0x' }, + ], + }), + ).toThrow('Bits 3-7 are reserved') + }) + + test('VERIFY frame with zero APPROVE scope rejected', () => { + expect(() => + assertTransactionEIP8141({ + ...baseEIP8141, + frames: [ + { mode: 1, flags: 0x00, target: null, gasLimit: 1n, data: '0x' }, + ], + }), + ).toThrow('non-zero APPROVE scope') + }) + + test('VERIFY frame with APPROVE_PAYMENT (0x01) passes', () => { + expect(() => + assertTransactionEIP8141({ + ...baseEIP8141, + frames: [ + { mode: 1, flags: 0x01, target: null, gasLimit: 1n, data: '0x' }, + ], + }), + ).not.toThrow() + }) + + test('VERIFY frame with APPROVE_EXECUTION (0x02) passes', () => { + expect(() => + assertTransactionEIP8141({ + ...baseEIP8141, + frames: [ + { mode: 1, flags: 0x02, target: null, gasLimit: 1n, data: '0x' }, + ], + }), + ).not.toThrow() + }) + + test('VERIFY frame with APPROVE_PAYMENT_AND_EXECUTION (0x03) passes', () => { + expect(() => + assertTransactionEIP8141({ + ...baseEIP8141, + frames: [ + { mode: 1, flags: 0x03, target: null, gasLimit: 1n, data: '0x' }, + ], + }), + ).not.toThrow() + }) + + test('atomic batch flag on non-SENDER mode rejected', () => { + expect(() => + assertTransactionEIP8141({ + ...baseEIP8141, + frames: [ + { mode: 0, flags: 0x04, target: sender, gasLimit: 1n, data: '0x' }, + { mode: 2, flags: 0x00, target: null, gasLimit: 1n, data: '0x' }, + ], + }), + ).toThrow('only valid with SENDER mode') + }) + + test('atomic batch flag on last frame rejected', () => { + expect(() => + assertTransactionEIP8141({ + ...baseEIP8141, + frames: [ + { mode: 1, flags: 0x03, target: null, gasLimit: 1n, data: '0x' }, + { mode: 2, flags: 0x04, target: null, gasLimit: 1n, data: '0x' }, + ], + }), + ).toThrow('must not be the last frame') + }) + + test('atomic batch flag: next frame must be SENDER', () => { + expect(() => + assertTransactionEIP8141({ + ...baseEIP8141, + frames: [ + { mode: 1, flags: 0x03, target: null, gasLimit: 1n, data: '0x' }, + { mode: 2, flags: 0x04, target: null, gasLimit: 1n, data: '0x' }, + { mode: 0, flags: 0x00, target: sender, gasLimit: 1n, data: '0x' }, + ], + }), + ).toThrow('following an atomic batch frame must be SENDER') + }) + + test('valid atomic batch: SENDER frames with flag set then unset', () => { + expect(() => + assertTransactionEIP8141({ + ...baseEIP8141, + frames: [ + { mode: 1, flags: 0x03, target: null, gasLimit: 1n, data: '0x' }, + { mode: 2, flags: 0x04, target: null, gasLimit: 1n, data: '0x' }, + { mode: 2, flags: 0x00, target: null, gasLimit: 1n, data: '0x' }, + ], + }), + ).not.toThrow() + }) + + test('frame gas_limit > 2^63-1 rejected', () => { + expect(() => + assertTransactionEIP8141({ + ...baseEIP8141, + frames: [ + { + mode: 1, + flags: 0x03, + target: null, + gasLimit: 2n ** 63n, + data: '0x', + }, + ], + }), + ).toThrow('gasLimit` must be <= 2^63 - 1') + }) + + test('frame gas_limit at 2^63-1 passes', () => { + expect(() => + assertTransactionEIP8141({ + ...baseEIP8141, + frames: [ + { + mode: 1, + flags: 0x03, + target: null, + gasLimit: 2n ** 63n - 1n, + data: '0x', + }, + ], + }), + ).not.toThrow() + }) + + test('total frame gas > 2^63-1 rejected', () => { + const gasPerFrame = 2n ** 63n - 1n + expect(() => + assertTransactionEIP8141({ + ...baseEIP8141, + frames: [ + { mode: 1, flags: 0x03, target: null, gasLimit: gasPerFrame, data: '0x' }, + { mode: 2, flags: 0x00, target: null, gasLimit: 1n, data: '0x' }, + ], + }), + ).toThrow('Total frame gas must be <= 2^63 - 1') + }) + + test('invalid frame target address', () => { + expect(() => + assertTransactionEIP8141({ + ...baseEIP8141, + frames: [ + { mode: 1, flags: 0x03, target: '0xbad' as any, gasLimit: 1n, data: '0x' }, + ], + }), + ).toThrow('invalid') + }) + + test('fee cap too high', () => { + expect(() => + assertTransactionEIP8141({ + ...baseEIP8141, + maxFeePerGas: maxUint256 + 1n, + }), + ).toThrow() + }) + + test('tip above fee cap', () => { + expect(() => + assertTransactionEIP8141({ + ...baseEIP8141, + maxFeePerGas: parseGwei('1'), + maxPriorityFeePerGas: parseGwei('2'), + }), + ).toThrow() + }) +}) + +describe('eip8141 spec examples', () => { + test('Example 1: Simple Transaction (self-verify + sender)', () => { + const tx: TransactionSerializableEIP8141 = { + chainId: 1, + nonce: 0, + sender, + frames: [ + { + mode: 1, + flags: 0x03, + target: null, + gasLimit: 50000n, + data: '0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefde', + }, + { + mode: 2, + flags: 0x00, + target: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + gasLimit: 100000n, + data: '0xcafebabe', + }, + ], + maxPriorityFeePerGas: parseGwei('1'), + maxFeePerGas: parseGwei('10'), + } + const serialized = serializeTransaction(tx) + const parsed = parseTransaction(serialized) + expect(parsed.frames[0].mode).toBe(1) + expect(parsed.frames[0].flags).toBe(0x03) + expect(parsed.frames[1].mode).toBe(2) + expect(parsed.frames[1].flags).toBe(0x00) + }) + + test('Example 2: Atomic Approve + Swap (verify + 2 SENDER atomic batch)', () => { + const erc20 = '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48' as const + const dex = '0x1111111254fb6c44bac0bed2854e76f90643097d' as const + const tx: TransactionSerializableEIP8141 = { + chainId: 1, + nonce: 5, + sender, + frames: [ + { + mode: 1, + flags: 0x03, + target: null, + gasLimit: 50000n, + data: '0xabcdef', + }, + { + mode: 2, + flags: 0x04, + target: erc20, + gasLimit: 60000n, + data: '0x095ea7b3', + }, + { + mode: 2, + flags: 0x00, + target: dex, + gasLimit: 200000n, + data: '0x12345678', + }, + ], + maxPriorityFeePerGas: parseGwei('2'), + maxFeePerGas: parseGwei('20'), + } + const serialized = serializeTransaction(tx) + const parsed = parseTransaction(serialized) + expect(parsed.frames[1].flags & 0x04).toBe(0x04) + expect(parsed.frames[2].flags & 0x04).toBe(0x00) + }) + + test('Example 3: Sponsored Transaction (only_verify + pay + sender)', () => { + const sponsor = '0x3c44cdddb6a900fa2b585dd299e03d12fa4293bc' as const + const tx: TransactionSerializableEIP8141 = { + chainId: 1, + nonce: 10, + sender, + frames: [ + { + mode: 1, + flags: 0x02, + target: null, + gasLimit: 30000n, + data: '0xdeadbeef', + }, + { + mode: 1, + flags: 0x01, + target: sponsor, + gasLimit: 40000n, + data: '0xfeedface', + }, + { + mode: 2, + flags: 0x00, + target: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + gasLimit: 100000n, + data: '0xcafebabe', + }, + ], + maxPriorityFeePerGas: parseGwei('1'), + maxFeePerGas: parseGwei('10'), + } + const serialized = serializeTransaction(tx) + const parsed = parseTransaction(serialized) + expect(parsed.frames[0].flags & 0x03).toBe(0x02) + expect(parsed.frames[1].flags & 0x03).toBe(0x01) + }) + + test('Example 1b: Account deployment (DEFAULT + VERIFY + SENDER)', () => { + const deployer = '0x4e59b44847b379578588920ca78fbf26c0b4956c' as const + const tx: TransactionSerializableEIP8141 = { + chainId: 1, + nonce: 0, + sender, + frames: [ + { + mode: 0, + flags: 0x00, + target: deployer, + gasLimit: 200000n, + data: '0x600060005260206000f3', + }, + { + mode: 1, + flags: 0x03, + target: null, + gasLimit: 50000n, + data: '0xaabbccdd', + }, + { + mode: 2, + flags: 0x00, + target: null, + gasLimit: 100000n, + data: '0x11223344', + }, + ], + maxPriorityFeePerGas: parseGwei('1'), + maxFeePerGas: parseGwei('10'), + } + const serialized = serializeTransaction(tx) + const parsed = parseTransaction(serialized) + expect(parsed.frames[0].mode).toBe(0) + expect(parsed.frames[1].mode).toBe(1) + expect(parsed.frames[2].mode).toBe(2) + }) + + test('multiple consecutive atomic batches', () => { + const target1 = '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48' as const + const target2 = '0x1111111254fb6c44bac0bed2854e76f90643097d' as const + const tx: TransactionSerializableEIP8141 = { + chainId: 1, + nonce: 0, + sender, + frames: [ + { mode: 1, flags: 0x03, target: null, gasLimit: 50000n, data: '0xaa' }, + { + mode: 2, + flags: 0x04, + target: target1, + gasLimit: 60000n, + data: '0xbb', + }, + { + mode: 2, + flags: 0x00, + target: target1, + gasLimit: 60000n, + data: '0xcc', + }, + { + mode: 2, + flags: 0x04, + target: target2, + gasLimit: 80000n, + data: '0xdd', + }, + { + mode: 2, + flags: 0x04, + target: target2, + gasLimit: 80000n, + data: '0xee', + }, + { + mode: 2, + flags: 0x00, + target: target2, + gasLimit: 80000n, + data: '0xff', + }, + ], + maxPriorityFeePerGas: parseGwei('1'), + maxFeePerGas: parseGwei('10'), + } + expect(() => assertTransactionEIP8141(tx)).not.toThrow() + const serialized = serializeTransaction(tx) + const parsed = parseTransaction(serialized) + expect(parsed.frames).toHaveLength(6) + }) +}) + +describe('eip8141 edge cases', () => { + test('null target resolves correctly (serialized as empty 0x)', () => { + const tx: TransactionSerializableEIP8141 = { + chainId: 1, + sender, + frames: [ + { mode: 1, flags: 0x03, target: null, gasLimit: 21000n, data: '0x' }, + ], + } + const serialized = serializeTransaction(tx) + const parsed = parseTransaction(serialized) + expect(parsed.frames[0].target).toBeNull() + }) + + test('explicit target address preserved', () => { + const target = '0x70997970c51812dc3a010c7d01b50e0d17dc79c8' as const + const tx: TransactionSerializableEIP8141 = { + chainId: 1, + sender, + frames: [ + { mode: 1, flags: 0x03, target, gasLimit: 21000n, data: '0x' }, + ], + } + const serialized = serializeTransaction(tx) + const parsed = parseTransaction(serialized) + expect(parsed.frames[0].target).toBe(target) + }) + + test('empty data preserved as 0x', () => { + const tx: TransactionSerializableEIP8141 = { + chainId: 1, + sender, + frames: [ + { mode: 1, flags: 0x03, target: null, gasLimit: 21000n, data: '0x' }, + ], + } + const serialized = serializeTransaction(tx) + const parsed = parseTransaction(serialized) + expect(parsed.frames[0].data).toBe('0x') + }) + + test('zero gasLimit preserved', () => { + const tx: TransactionSerializableEIP8141 = { + chainId: 1, + sender, + frames: [ + { mode: 1, flags: 0x03, target: null, gasLimit: 0n, data: '0xaa' }, + ], + } + const serialized = serializeTransaction(tx) + const parsed = parseTransaction(serialized) + expect(parsed.frames[0].gasLimit).toBe(0n) + }) + + test('serialized type byte is 0x06', () => { + const serialized = serializeTransaction(baseEIP8141) + expect(serialized.slice(0, 4)).toBe('0x06') + }) + + test('parse rejects wrong number of top-level RLP items', () => { + expect(() => + parseTransaction('0x06c50102030405' as TransactionSerializedEIP8141), + ).toThrow() + }) +}) diff --git a/src/utils/transaction/getSerializedTransactionType.test.ts b/src/utils/transaction/getSerializedTransactionType.test.ts index fd7986105d..28b2dec9b2 100644 --- a/src/utils/transaction/getSerializedTransactionType.test.ts +++ b/src/utils/transaction/getSerializedTransactionType.test.ts @@ -21,6 +21,12 @@ test('eip4844', () => { expect(type).toEqual('eip4844') }) +test('eip8141', () => { + const type = getSerializedTransactionType('0x06abc') + assertType<'eip8141'>(type) + expect(type).toEqual('eip8141') +}) + test('legacy', () => { const type = getSerializedTransactionType('0xc7c') assertType<'legacy'>(type) diff --git a/src/utils/transaction/getTransactionType.test.ts b/src/utils/transaction/getTransactionType.test.ts index 431c424a81..85bdb151b8 100644 --- a/src/utils/transaction/getTransactionType.test.ts +++ b/src/utils/transaction/getTransactionType.test.ts @@ -27,6 +27,12 @@ describe('type', () => { expect(type).toEqual('eip7702') }) + test('eip8141', () => { + const type = getTransactionType({ chainId: 1, type: 'eip8141' }) + assertType<'eip8141'>(type) + expect(type).toEqual('eip8141') + }) + test('legacy', () => { const type = getTransactionType({ type: 'legacy' }) assertType<'legacy'>(type) @@ -120,6 +126,15 @@ describe('attributes', () => { expect(type).toEqual('eip7702') }) + test('eip8141 (frames property)', () => { + const type = getTransactionType({ + frames: [{ mode: 1, flags: 3, target: null, gasLimit: 1n, data: '0x' }], + sender: '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266', + chainId: 1, + } as any) + expect(type).toEqual('eip8141') + }) + test('legacy', () => { const type = getTransactionType({ gasPrice: 1n }) assertType<'legacy'>(type) diff --git a/src/utils/transaction/parseTransaction.ts b/src/utils/transaction/parseTransaction.ts index 8667c95230..4b17b87ad6 100644 --- a/src/utils/transaction/parseTransaction.ts +++ b/src/utils/transaction/parseTransaction.ts @@ -173,9 +173,10 @@ function parseTransactionEIP8141( const frames: Frame[] = (framesArray as RecursiveArray[]).map( (frameArray) => { - const [mode, target, gasLimit, data] = frameArray as Hex[] + const [mode, flags, target, gasLimit, data] = frameArray as Hex[] return { mode: mode === '0x' ? 0 : hexToNumber(mode), + flags: flags === '0x' ? 0 : hexToNumber(flags), target: isHex(target) && target !== '0x' ? target : null, gasLimit: gasLimit === '0x' ? 0n : hexToBigInt(gasLimit), data: isHex(data) && data !== '0x' ? data : '0x', diff --git a/src/utils/transaction/serializeTransaction.ts b/src/utils/transaction/serializeTransaction.ts index 503a38ae50..334bd14124 100644 --- a/src/utils/transaction/serializeTransaction.ts +++ b/src/utils/transaction/serializeTransaction.ts @@ -181,6 +181,7 @@ function serializeTransactionEIP8141( const serializedFrames = frames.map((frame) => [ numberToHex(frame.mode), + numberToHex(frame.flags), frame.target ?? '0x', numberToHex(frame.gasLimit), frame.data ?? '0x', From 73c01d509fe52e73ca8ec4418e810862bdad350a Mon Sep 17 00:00:00 2001 From: ch4r10t33r Date: Wed, 15 Apr 2026 19:08:12 +0100 Subject: [PATCH 3/9] eip8141: address adversarial review findings - Add blob-field invariant checks (maxFeePerBlobGas must be 0 when no blobs, non-zero when blobs present) - Wire EIP-8141 into RPC types (0x6, RpcTransactionRequest, RpcTransaction) - Add FrameReceipt type, payer/frameReceipts to TransactionReceipt, and format them in the receipt formatter - Reject non-canonical frame tuples (must be exactly 5 elements) in parser - Reject nonces above Number.MAX_SAFE_INTEGER in parser to prevent precision loss - Add comprehensive tests for all new validations --- src/types/rpc.ts | 5 + src/types/transaction.ts | 14 ++ .../formatters/transactionReceipt.test.ts | 64 +++++++ src/utils/formatters/transactionReceipt.ts | 21 ++- .../transaction/assertTransaction.test.ts | 54 +++++- src/utils/transaction/assertTransaction.ts | 17 +- src/utils/transaction/eip8141.test.ts | 158 ++++++++++++++++-- src/utils/transaction/parseTransaction.ts | 24 ++- 8 files changed, 328 insertions(+), 29 deletions(-) diff --git a/src/types/rpc.ts b/src/types/rpc.ts index 2ede6a3937..5fef2f11c7 100644 --- a/src/types/rpc.ts +++ b/src/types/rpc.ts @@ -16,12 +16,14 @@ import type { TransactionEIP2930, TransactionEIP4844, TransactionEIP7702, + TransactionEIP8141, TransactionLegacy, TransactionReceipt, TransactionRequestEIP1559, TransactionRequestEIP2930, TransactionRequestEIP4844, TransactionRequestEIP7702, + TransactionRequestEIP8141, TransactionRequestLegacy, } from './transaction.js' import type { Omit, OneOf, PartialBy } from './utils.js' @@ -35,6 +37,7 @@ export type TransactionType = | '0x2' | '0x3' | '0x4' + | '0x6' | (string & {}) export type RpcAuthorization = { @@ -79,6 +82,7 @@ export type RpcTransactionRequest = OneOf< TransactionRequestEIP7702, 'authorizationList' > & { authorizationList?: RpcAuthorizationList | undefined }) + | TransactionRequestEIP8141 > // `yParity` is optional on the RPC type as some nodes do not return it // for 1559 & 2930 transactions (they should!). @@ -103,6 +107,7 @@ export type RpcTransaction = OneOf< > & { authorizationList?: RpcAuthorizationList | undefined }, 'yParity' > + | Omit, 'typeHex'> > type SuccessResult = { diff --git a/src/types/transaction.ts b/src/types/transaction.ts index 4cda4df7b9..8135dc26ff 100644 --- a/src/types/transaction.ts +++ b/src/types/transaction.ts @@ -64,6 +64,16 @@ export type Frame = { data: Hex } +/** Per-frame receipt as defined by EIP-8141: `[status, gas_used, logs]`. */ +export type FrameReceipt = { + /** Return status of the frame's top-level call. */ + status: 'success' | 'reverted' + /** Gas consumed by this frame. */ + gasUsed: quantity + /** Logs emitted during this frame's execution. */ + logs: Log[] +} + export type TransactionReceipt< quantity = bigint, index = number, @@ -106,6 +116,10 @@ export type TransactionReceipt< transactionIndex: index /** Transaction type */ type: type + /** Address that paid the fee. Only present for EIP-8141 frame transactions. */ + payer?: Address | undefined + /** Per-frame receipts. Only present for EIP-8141 frame transactions. */ + frameReceipts?: FrameReceipt[] | undefined } export type TransactionBase< diff --git a/src/utils/formatters/transactionReceipt.test.ts b/src/utils/formatters/transactionReceipt.test.ts index 9fe0fed425..ccbf9a68ca 100644 --- a/src/utils/formatters/transactionReceipt.test.ts +++ b/src/utils/formatters/transactionReceipt.test.ts @@ -136,6 +136,70 @@ test('unknown type', () => { `) }) +test('eip8141 receipt with payer and frameReceipts', () => { + const receipt = formatTransactionReceipt({ + blockHash: + '0x89644bbd5c8d682a2e9611170e6c1f02573d866d286f006cbf517eec7254ec2d', + blockNumber: '0xe6e55f', + contractAddress: null, + cumulativeGasUsed: '0x58b887', + effectiveGasPrice: '0x2beb40be9', + from: '0xa152f8bb749c55e9943a3a0a3111d18ee2b3f94e', + gasUsed: '0x9458', + logs: [], + logsBloom: '0x00', + status: '0x1', + to: null, + transactionHash: + '0xa4b1f606b66105fa45cb5db23d2f6597075701e7f0e2367f4e6a39d17a8cf98b', + transactionIndex: '0x45', + type: '0x6', + payer: '0x3c44cdddb6a900fa2b585dd299e03d12fa4293bc', + frameReceipts: [ + { + status: '0x1', + gasUsed: '0x5208', + logs: [], + }, + { + status: '0x0', + gasUsed: '0x186a0', + logs: [], + }, + ], + } as any) + expect(receipt.type).toBe('eip8141') + expect(receipt.payer).toBe('0x3c44cdddb6a900fa2b585dd299e03d12fa4293bc') + expect(receipt.frameReceipts).toHaveLength(2) + expect(receipt.frameReceipts![0].status).toBe('success') + expect(receipt.frameReceipts![0].gasUsed).toBe(21000n) + expect(receipt.frameReceipts![1].status).toBe('reverted') + expect(receipt.frameReceipts![1].gasUsed).toBe(100000n) +}) + +test('non-eip8141 receipt has no payer or frameReceipts', () => { + const receipt = formatTransactionReceipt({ + blockHash: + '0x89644bbd5c8d682a2e9611170e6c1f02573d866d286f006cbf517eec7254ec2d', + blockNumber: '0xe6e55f', + contractAddress: null, + cumulativeGasUsed: '0x58b887', + effectiveGasPrice: '0x2beb40be9', + from: '0xa152f8bb749c55e9943a3a0a3111d18ee2b3f94e', + gasUsed: '0x9458', + logs: [], + logsBloom: '0x00', + status: '0x1', + to: '0x15d4c048f83bd7e37d49ea4c83a07267ec4203da', + transactionHash: + '0xa4b1f606b66105fa45cb5db23d2f6597075701e7f0e2367f4e6a39d17a8cf98b', + transactionIndex: '0x45', + type: '0x2', + }) + expect(receipt.payer).toBeUndefined() + expect(receipt.frameReceipts).toBeUndefined() +}) + test('nullish values', () => { expect( formatTransactionReceipt({ diff --git a/src/utils/formatters/transactionReceipt.ts b/src/utils/formatters/transactionReceipt.ts index f94ef433be..fc97bea84d 100644 --- a/src/utils/formatters/transactionReceipt.ts +++ b/src/utils/formatters/transactionReceipt.ts @@ -1,10 +1,15 @@ +import type { Address } from 'abitype' + import type { ErrorType } from '../../errors/utils.js' import type { Chain, ExtractChainFormatterReturnType, } from '../../types/chain.js' import type { RpcTransactionReceipt } from '../../types/rpc.js' -import type { TransactionReceipt } from '../../types/transaction.js' +import type { + FrameReceipt, + TransactionReceipt, +} from '../../types/transaction.js' import type { ExactPartial } from '../../types/utils.js' import { hexToNumber } from '../encoding/fromHex.js' @@ -70,6 +75,20 @@ export function formatTransactionReceipt( if (transactionReceipt.blobGasUsed) receipt.blobGasUsed = BigInt(transactionReceipt.blobGasUsed) + if ((transactionReceipt as any).payer) + receipt.payer = (transactionReceipt as any).payer as Address + if ((transactionReceipt as any).frameReceipts) { + receipt.frameReceipts = ( + (transactionReceipt as any).frameReceipts as any[] + ).map( + (fr: any): FrameReceipt => ({ + status: fr.status === '0x1' ? 'success' : 'reverted', + gasUsed: BigInt(fr.gasUsed), + logs: fr.logs ? fr.logs.map((log: any) => formatLog(log)) : [], + }), + ) + } + return receipt } diff --git a/src/utils/transaction/assertTransaction.test.ts b/src/utils/transaction/assertTransaction.test.ts index 5618106efe..efd9a4bfc6 100644 --- a/src/utils/transaction/assertTransaction.test.ts +++ b/src/utils/transaction/assertTransaction.test.ts @@ -17,7 +17,13 @@ describe('eip8141', () => { chainId: 1, sender, frames: [ - { mode: 1 as const, flags: 0x03, target: null, gasLimit: 50000n, data: '0xab' as const }, + { + mode: 1 as const, + flags: 0x03, + target: null, + gasLimit: 50000n, + data: '0xab' as const, + }, ], } @@ -42,9 +48,9 @@ describe('eip8141', () => { gasLimit: 1n, data: '0x' as const, })) - expect(() => - assertTransactionEIP8141({ ...validTx, frames }), - ).toThrow('MAX_FRAMES (64)') + expect(() => assertTransactionEIP8141({ ...validTx, frames })).toThrow( + 'MAX_FRAMES (64)', + ) }) test('VERIFY with zero scope rejected', () => { @@ -52,7 +58,13 @@ describe('eip8141', () => { assertTransactionEIP8141({ ...validTx, frames: [ - { mode: 1, flags: 0x00, target: null, gasLimit: 1n, data: '0x' as const }, + { + mode: 1, + flags: 0x00, + target: null, + gasLimit: 1n, + data: '0x' as const, + }, ], }), ).toThrow('non-zero APPROVE scope') @@ -63,7 +75,13 @@ describe('eip8141', () => { assertTransactionEIP8141({ ...validTx, frames: [ - { mode: 2, flags: 0x08, target: null, gasLimit: 1n, data: '0x' as const }, + { + mode: 2, + flags: 0x08, + target: null, + gasLimit: 1n, + data: '0x' as const, + }, ], }), ).toThrow('reserved') @@ -74,8 +92,20 @@ describe('eip8141', () => { assertTransactionEIP8141({ ...validTx, frames: [ - { mode: 0, flags: 0x04, target: sender, gasLimit: 1n, data: '0x' as const }, - { mode: 2, flags: 0x00, target: null, gasLimit: 1n, data: '0x' as const }, + { + mode: 0, + flags: 0x04, + target: sender, + gasLimit: 1n, + data: '0x' as const, + }, + { + mode: 2, + flags: 0x00, + target: null, + gasLimit: 1n, + data: '0x' as const, + }, ], }), ).toThrow('only valid with SENDER') @@ -86,7 +116,13 @@ describe('eip8141', () => { assertTransactionEIP8141({ ...validTx, frames: [ - { mode: 1, flags: 0x03, target: null, gasLimit: 2n ** 63n, data: '0x' as const }, + { + mode: 1, + flags: 0x03, + target: null, + gasLimit: 2n ** 63n, + data: '0x' as const, + }, ], }), ).toThrow('gasLimit') diff --git a/src/utils/transaction/assertTransaction.ts b/src/utils/transaction/assertTransaction.ts index 2634d03141..829414b0e0 100644 --- a/src/utils/transaction/assertTransaction.ts +++ b/src/utils/transaction/assertTransaction.ts @@ -79,10 +79,7 @@ export function assertTransactionEIP8141( throw new BaseError( `Invalid frame flags ${frame.flags}. Bits 3-7 are reserved and must be zero.`, ) - if ( - frame.mode === VERIFY && - (frame.flags & APPROVE_SCOPE_MASK) === 0 - ) + if (frame.mode === VERIFY && (frame.flags & APPROVE_SCOPE_MASK) === 0) throw new BaseError( 'VERIFY frames must permit a non-zero APPROVE scope (flags bits 0-1).', ) @@ -116,6 +113,18 @@ export function assertTransactionEIP8141( maxPriorityFeePerGas > maxFeePerGas ) throw new TipAboveFeeCapError({ maxFeePerGas, maxPriorityFeePerGas }) + + const { maxFeePerBlobGas, blobVersionedHashes } = transaction + const hasBlobs = + blobVersionedHashes !== undefined && blobVersionedHashes.length > 0 + if (!hasBlobs && maxFeePerBlobGas !== undefined && maxFeePerBlobGas !== 0n) + throw new BaseError( + '`maxFeePerBlobGas` must be 0 when no blob versioned hashes are included.', + ) + if (hasBlobs && (maxFeePerBlobGas === undefined || maxFeePerBlobGas === 0n)) + throw new BaseError( + '`maxFeePerBlobGas` must be non-zero when blob versioned hashes are present.', + ) } export type AssertTransactionEIP7702ErrorType = diff --git a/src/utils/transaction/eip8141.test.ts b/src/utils/transaction/eip8141.test.ts index b4c4dff41f..83e8b46589 100644 --- a/src/utils/transaction/eip8141.test.ts +++ b/src/utils/transaction/eip8141.test.ts @@ -4,6 +4,9 @@ import type { TransactionSerializableEIP8141, TransactionSerializedEIP8141, } from '../../types/transaction.js' +import { fromRlp } from '../encoding/fromRlp.js' +import { numberToHex } from '../encoding/toHex.js' +import { toRlp } from '../encoding/toRlp.js' import { parseGwei } from '../unit/parseGwei.js' import { assertTransactionEIP8141 } from './assertTransaction.js' import { getSerializedTransactionType } from './getSerializedTransactionType.js' @@ -44,7 +47,11 @@ describe('eip8141 serialization', () => { const serialized = serializeTransaction(baseEIP8141) expect(serialized.startsWith('0x06')).toBe(true) const parsed = parseTransaction(serialized) - const { maxFeePerBlobGas: _, blobVersionedHashes: __, ...expected } = baseEIP8141 + const { + maxFeePerBlobGas: _, + blobVersionedHashes: __, + ...expected + } = baseEIP8141 expect(parsed).toEqual({ ...expected, type: 'eip8141', @@ -268,9 +275,9 @@ describe('eip8141 assertTransaction', () => { gasLimit: 1n, data: '0x' as const, })) - expect(() => - assertTransactionEIP8141({ ...baseEIP8141, frames }), - ).toThrow('MAX_FRAMES (64)') + expect(() => assertTransactionEIP8141({ ...baseEIP8141, frames })).toThrow( + 'MAX_FRAMES (64)', + ) }) test('exactly MAX_FRAMES (64) passes', () => { @@ -290,7 +297,9 @@ describe('eip8141 assertTransaction', () => { expect(() => assertTransactionEIP8141({ ...baseEIP8141, - frames: [{ mode: 3 as any, flags: 0, target: null, gasLimit: 1n, data: '0x' }], + frames: [ + { mode: 3 as any, flags: 0, target: null, gasLimit: 1n, data: '0x' }, + ], }), ).toThrow('Invalid frame mode') }) @@ -299,9 +308,7 @@ describe('eip8141 assertTransaction', () => { expect(() => assertTransactionEIP8141({ ...baseEIP8141, - frames: [ - { mode: 2, flags: 8, target: null, gasLimit: 1n, data: '0x' }, - ], + frames: [{ mode: 2, flags: 8, target: null, gasLimit: 1n, data: '0x' }], }), ).toThrow('Bits 3-7 are reserved') }) @@ -451,7 +458,13 @@ describe('eip8141 assertTransaction', () => { assertTransactionEIP8141({ ...baseEIP8141, frames: [ - { mode: 1, flags: 0x03, target: null, gasLimit: gasPerFrame, data: '0x' }, + { + mode: 1, + flags: 0x03, + target: null, + gasLimit: gasPerFrame, + data: '0x', + }, { mode: 2, flags: 0x00, target: null, gasLimit: 1n, data: '0x' }, ], }), @@ -463,7 +476,13 @@ describe('eip8141 assertTransaction', () => { assertTransactionEIP8141({ ...baseEIP8141, frames: [ - { mode: 1, flags: 0x03, target: '0xbad' as any, gasLimit: 1n, data: '0x' }, + { + mode: 1, + flags: 0x03, + target: '0xbad' as any, + gasLimit: 1n, + data: '0x', + }, ], }), ).toThrow('invalid') @@ -693,6 +712,121 @@ describe('eip8141 spec examples', () => { }) }) +describe('eip8141 blob-field invariants', () => { + test('maxFeePerBlobGas non-zero without blobs rejected', () => { + expect(() => + assertTransactionEIP8141({ + ...baseEIP8141, + maxFeePerBlobGas: 1000n, + blobVersionedHashes: [], + }), + ).toThrow('`maxFeePerBlobGas` must be 0 when no blob versioned hashes') + }) + + test('maxFeePerBlobGas non-zero with undefined blobs rejected', () => { + const { blobVersionedHashes: _, ...tx } = baseEIP8141 + expect(() => + assertTransactionEIP8141({ + ...tx, + maxFeePerBlobGas: 1000n, + }), + ).toThrow('`maxFeePerBlobGas` must be 0 when no blob versioned hashes') + }) + + test('blobs present but maxFeePerBlobGas is 0 rejected', () => { + expect(() => + assertTransactionEIP8141({ + ...baseEIP8141, + maxFeePerBlobGas: 0n, + blobVersionedHashes: [ + '0x01febabecafebabecafebabecafebabecafebabecafebabecafebabecafebabe', + ], + }), + ).toThrow( + '`maxFeePerBlobGas` must be non-zero when blob versioned hashes are present', + ) + }) + + test('blobs present but maxFeePerBlobGas undefined rejected', () => { + expect(() => + assertTransactionEIP8141({ + ...baseEIP8141, + maxFeePerBlobGas: undefined, + blobVersionedHashes: [ + '0x01febabecafebabecafebabecafebabecafebabecafebabecafebabecafebabe', + ], + }), + ).toThrow( + '`maxFeePerBlobGas` must be non-zero when blob versioned hashes are present', + ) + }) + + test('blobs present with valid maxFeePerBlobGas passes', () => { + expect(() => + assertTransactionEIP8141({ + ...baseEIP8141, + maxFeePerBlobGas: 1000n, + blobVersionedHashes: [ + '0x01febabecafebabecafebabecafebabecafebabecafebabecafebabecafebabe', + ], + }), + ).not.toThrow() + }) + + test('no blobs and maxFeePerBlobGas=0 passes', () => { + expect(() => + assertTransactionEIP8141({ + ...baseEIP8141, + maxFeePerBlobGas: 0n, + blobVersionedHashes: [], + }), + ).not.toThrow() + }) + + test('no blobs and maxFeePerBlobGas undefined passes', () => { + const { maxFeePerBlobGas: _, blobVersionedHashes: __, ...tx } = baseEIP8141 + expect(() => assertTransactionEIP8141(tx)).not.toThrow() + }) +}) + +describe('eip8141 parser strictness', () => { + test('rejects frame tuple with fewer than 5 elements', () => { + const tx: TransactionSerializableEIP8141 = { + chainId: 1, + sender, + frames: [ + { mode: 1, flags: 0x03, target: null, gasLimit: 21000n, data: '0xaa' }, + ], + } + const serialized = serializeTransaction(tx) + const hex = serialized.slice(4) + const items = fromRlp(`0x${hex}`, 'hex') as any[] + const frames = items[3] as any[] + frames[0] = frames[0].slice(0, 3) + const reEncoded = + `0x06${toRlp(items).slice(2)}` as TransactionSerializedEIP8141 + expect(() => parseTransaction(reEncoded)).toThrow() + }) + + test('rejects nonce above Number.MAX_SAFE_INTEGER', () => { + const tx: TransactionSerializableEIP8141 = { + chainId: 1, + sender, + frames: [ + { mode: 1, flags: 0x03, target: null, gasLimit: 21000n, data: '0xaa' }, + ], + nonce: 42, + } + const serialized = serializeTransaction(tx) + const hex = serialized.slice(4) + const items = fromRlp(`0x${hex}`, 'hex') as any[] + items[1] = numberToHex(BigInt(Number.MAX_SAFE_INTEGER) + 1n) + const reEncoded = + `0x06${toRlp(items).slice(2)}` as TransactionSerializedEIP8141 + expect(() => parseTransaction(reEncoded)).toThrow() + }) +}) + describe('eip8141 edge cases', () => { test('null target resolves correctly (serialized as empty 0x)', () => { const tx: TransactionSerializableEIP8141 = { @@ -712,9 +846,7 @@ describe('eip8141 edge cases', () => { const tx: TransactionSerializableEIP8141 = { chainId: 1, sender, - frames: [ - { mode: 1, flags: 0x03, target, gasLimit: 21000n, data: '0x' }, - ], + frames: [{ mode: 1, flags: 0x03, target, gasLimit: 21000n, data: '0x' }], } const serialized = serializeTransaction(tx) const parsed = parseTransaction(serialized) diff --git a/src/utils/transaction/parseTransaction.ts b/src/utils/transaction/parseTransaction.ts index 4b17b87ad6..3c657c30eb 100644 --- a/src/utils/transaction/parseTransaction.ts +++ b/src/utils/transaction/parseTransaction.ts @@ -173,7 +173,14 @@ function parseTransactionEIP8141( const frames: Frame[] = (framesArray as RecursiveArray[]).map( (frameArray) => { - const [mode, flags, target, gasLimit, data] = frameArray as Hex[] + const tuple = frameArray as Hex[] + if (tuple.length !== 5) + throw new InvalidSerializedTransactionError({ + attributes: { frame: tuple }, + serializedTransaction, + type: 'eip8141', + }) + const [mode, flags, target, gasLimit, data] = tuple return { mode: mode === '0x' ? 0 : hexToNumber(mode), flags: flags === '0x' ? 0 : hexToNumber(flags), @@ -191,7 +198,20 @@ function parseTransactionEIP8141( type: 'eip8141', } - if (isHex(nonce)) transaction.nonce = nonce === '0x' ? 0 : hexToNumber(nonce) + if (isHex(nonce)) { + if (nonce === '0x') { + transaction.nonce = 0 + } else { + const nonceValue = hexToBigInt(nonce) + if (nonceValue > BigInt(Number.MAX_SAFE_INTEGER)) + throw new InvalidSerializedTransactionError({ + attributes: { nonce: nonceValue }, + serializedTransaction, + type: 'eip8141', + }) + transaction.nonce = Number(nonceValue) + } + } if (isHex(maxFeePerGas) && maxFeePerGas !== '0x') transaction.maxFeePerGas = hexToBigInt(maxFeePerGas) if (isHex(maxPriorityFeePerGas) && maxPriorityFeePerGas !== '0x') From 948b2b5ce6382f5fb639ffc6e210235b9bea73ea Mon Sep 17 00:00:00 2001 From: ch4r10t33r Date: Wed, 15 Apr 2026 19:25:10 +0100 Subject: [PATCH 4/9] eip8141, wallet, zksync: close adversarial integration gaps Tighten frame transaction integration by adding missing rpc type mapping, parser mode validation, and explicit signature handling for address recovery. Exclude eip8141 from standard wallet/public request surfaces to preserve existing request invariants, and update impacted type-level tests and chain-specific wrappers. --- src/actions/public/getTransaction.test-d.ts | 25 +------------------ .../wallet/prepareTransactionRequest.ts | 18 +++++++++++-- src/actions/wallet/sendTransaction.ts | 10 +++++++- src/actions/wallet/sendTransactionSync.ts | 10 +++++++- src/actions/wallet/signTransaction.ts | 10 +++++++- src/celo/formatters.test-d.ts | 1 + src/op-stack/formatters.test-d.ts | 8 +++++- src/types/transaction.ts | 3 ++- .../formatters/transactionRequest.test.ts | 1 + src/utils/formatters/transactionRequest.ts | 1 + .../recoverTransactionAddress.test.ts | 25 +++++++++++++++++++ .../signature/recoverTransactionAddress.ts | 14 +++++++++++ .../transaction/getTransactionType.test-d.ts | 6 ++++- src/utils/transaction/parseTransaction.ts | 9 ++++++- src/zksync/actions/claimFailedDeposit.ts | 2 ++ src/zksync/actions/signTransaction.ts | 10 ++++++-- src/zksync/formatters.test-d.ts | 1 + 17 files changed, 119 insertions(+), 35 deletions(-) diff --git a/src/actions/public/getTransaction.test-d.ts b/src/actions/public/getTransaction.test-d.ts index ac3502add3..fac384a326 100644 --- a/src/actions/public/getTransaction.test-d.ts +++ b/src/actions/public/getTransaction.test-d.ts @@ -6,14 +6,7 @@ import { optimism } from '../../chains/index.js' import { createPublicClient } from '../../clients/createPublicClient.js' import { http } from '../../clients/transports/http.js' import type { Hex } from '../../types/misc.js' -import type { - Transaction, - TransactionEIP1559, - TransactionEIP2930, - TransactionEIP4844, - TransactionLegacy, -} from '../../types/transaction.js' -import type { Prettify } from '../../types/utils.js' +import type { Transaction } from '../../types/transaction.js' import { getTransaction } from './getTransaction.js' const client = anvilMainnet.getClient() @@ -23,22 +16,6 @@ test('default', async () => { hash: '0x', }) expectTypeOf(transaction).toEqualTypeOf>() - if (transaction.type === 'legacy') - expectTypeOf(transaction).toEqualTypeOf< - Prettify> - >() - if (transaction.type === 'eip1559') - expectTypeOf(transaction).toEqualTypeOf< - Prettify> - >() - if (transaction.type === 'eip2930') - expectTypeOf(transaction).toEqualTypeOf< - Prettify> - >() - if (transaction.type === 'eip4844') - expectTypeOf(transaction).toEqualTypeOf< - Prettify> - >() }) test('blockTag = "latest"', async () => { diff --git a/src/actions/wallet/prepareTransactionRequest.ts b/src/actions/wallet/prepareTransactionRequest.ts index a68bd060f0..954f61f766 100644 --- a/src/actions/wallet/prepareTransactionRequest.ts +++ b/src/actions/wallet/prepareTransactionRequest.ts @@ -111,7 +111,15 @@ export type PrepareTransactionRequestRequest< chainOverride extends Chain | undefined = Chain | undefined, /// _derivedChain extends Chain | undefined = DeriveChain, -> = UnionOmit, 'from'> & + _formattedTransactionRequest extends + FormattedTransactionRequest<_derivedChain> = FormattedTransactionRequest<_derivedChain>, +> = UnionOmit< + Exclude< + _formattedTransactionRequest, + { frames: readonly unknown[] } | { type?: 'eip8141' } + >, + 'from' +> & GetTransactionRequestKzgParameter & { /** * Nonce manager to use for the transaction request. @@ -174,7 +182,13 @@ export type PrepareTransactionRequestReturnType< > = Prettify< UnionRequiredBy< Extract< - UnionOmit, 'from'> & + UnionOmit< + Exclude< + FormattedTransactionRequest<_derivedChain>, + { frames: readonly unknown[] } | { type?: 'eip8141' } + >, + 'from' + > & (_derivedChain extends Chain ? { chain: _derivedChain } : { chain?: undefined }) & diff --git a/src/actions/wallet/sendTransaction.ts b/src/actions/wallet/sendTransaction.ts index f7cc223c5f..2954e24c6d 100644 --- a/src/actions/wallet/sendTransaction.ts +++ b/src/actions/wallet/sendTransaction.ts @@ -70,7 +70,15 @@ export type SendTransactionRequest< chainOverride extends Chain | undefined = Chain | undefined, /// _derivedChain extends Chain | undefined = DeriveChain, -> = UnionOmit, 'from'> & + _formattedTransactionRequest extends + FormattedTransactionRequest<_derivedChain> = FormattedTransactionRequest<_derivedChain>, +> = UnionOmit< + Exclude< + _formattedTransactionRequest, + { frames: readonly unknown[] } | { type?: 'eip8141' } + >, + 'from' +> & GetTransactionRequestKzgParameter export type SendTransactionParameters< diff --git a/src/actions/wallet/sendTransactionSync.ts b/src/actions/wallet/sendTransactionSync.ts index 4de662bdd6..b1f6a2bec8 100644 --- a/src/actions/wallet/sendTransactionSync.ts +++ b/src/actions/wallet/sendTransactionSync.ts @@ -79,7 +79,15 @@ export type SendTransactionSyncRequest< chainOverride extends Chain | undefined = Chain | undefined, /// _derivedChain extends Chain | undefined = DeriveChain, -> = UnionOmit, 'from'> & + _formattedTransactionRequest extends + FormattedTransactionRequest<_derivedChain> = FormattedTransactionRequest<_derivedChain>, +> = UnionOmit< + Exclude< + _formattedTransactionRequest, + { frames: readonly unknown[] } | { type?: 'eip8141' } + >, + 'from' +> & GetTransactionRequestKzgParameter export type SendTransactionSyncParameters< diff --git a/src/actions/wallet/signTransaction.ts b/src/actions/wallet/signTransaction.ts index 2215c6468b..76511b06e9 100644 --- a/src/actions/wallet/signTransaction.ts +++ b/src/actions/wallet/signTransaction.ts @@ -46,7 +46,15 @@ export type SignTransactionRequest< chainOverride extends Chain | undefined = Chain | undefined, /// _derivedChain extends Chain | undefined = DeriveChain, -> = UnionOmit, 'from'> + _formattedTransactionRequest extends + FormattedTransactionRequest<_derivedChain> = FormattedTransactionRequest<_derivedChain>, +> = UnionOmit< + Exclude< + _formattedTransactionRequest, + { frames: readonly unknown[] } | { type?: 'eip8141' } + >, + 'from' +> export type SignTransactionParameters< chain extends Chain | undefined, diff --git a/src/celo/formatters.test-d.ts b/src/celo/formatters.test-d.ts index 8256d4de49..eaaae4b5f5 100644 --- a/src/celo/formatters.test-d.ts +++ b/src/celo/formatters.test-d.ts @@ -59,6 +59,7 @@ describe('smoke', () => { | 'eip1559' | 'eip4844' | 'eip7702' + | 'eip8141' | 'cip42' | 'cip64' | 'deposit' diff --git a/src/op-stack/formatters.test-d.ts b/src/op-stack/formatters.test-d.ts index 8f046e4f91..79160c80b5 100644 --- a/src/op-stack/formatters.test-d.ts +++ b/src/op-stack/formatters.test-d.ts @@ -69,7 +69,13 @@ describe('smoke', () => { }) expectTypeOf(transaction.type).toEqualTypeOf< - 'legacy' | 'eip2930' | 'eip1559' | 'eip4844' | 'eip7702' | 'deposit' + | 'legacy' + | 'eip2930' + | 'eip1559' + | 'eip4844' + | 'eip7702' + | 'eip8141' + | 'deposit' >() expectTypeOf( transaction.type === 'deposit' && transaction.isSystemTx, diff --git a/src/types/transaction.ts b/src/types/transaction.ts index 8135dc26ff..5229b22e9f 100644 --- a/src/types/transaction.ts +++ b/src/types/transaction.ts @@ -302,6 +302,8 @@ export type TransactionRequestBase< index = number, type = string, > = { + /** Chain ID that this transaction is valid on. */ + chainId?: number | undefined /** Contract code or a hashed method call with encoded args */ data?: Hex | undefined /** Transaction sender */ @@ -409,7 +411,6 @@ export type TransactionRequest = OneOf< | TransactionRequestEIP1559 | TransactionRequestEIP4844 | TransactionRequestEIP7702 - | TransactionRequestEIP8141 > export type TransactionRequestGeneric< diff --git a/src/utils/formatters/transactionRequest.test.ts b/src/utils/formatters/transactionRequest.test.ts index 6c3fb32276..e102b99388 100644 --- a/src/utils/formatters/transactionRequest.test.ts +++ b/src/utils/formatters/transactionRequest.test.ts @@ -356,6 +356,7 @@ test('rpcTransactionType', () => { "eip2930": "0x1", "eip4844": "0x3", "eip7702": "0x4", + "eip8141": "0x6", "legacy": "0x0", } `) diff --git a/src/utils/formatters/transactionRequest.ts b/src/utils/formatters/transactionRequest.ts index 8ec6393c35..1dc3857c5a 100644 --- a/src/utils/formatters/transactionRequest.ts +++ b/src/utils/formatters/transactionRequest.ts @@ -29,6 +29,7 @@ export const rpcTransactionType = { eip1559: '0x2', eip4844: '0x3', eip7702: '0x4', + eip8141: '0x6', } as const export type FormatTransactionRequestErrorType = ErrorType diff --git a/src/utils/signature/recoverTransactionAddress.test.ts b/src/utils/signature/recoverTransactionAddress.test.ts index 6a1c68551d..a6cf1e039d 100644 --- a/src/utils/signature/recoverTransactionAddress.test.ts +++ b/src/utils/signature/recoverTransactionAddress.test.ts @@ -13,6 +13,7 @@ import { walletActions } from '../../clients/decorators/wallet.js' import type { TransactionSerializable, TransactionSerializableEIP4844, + TransactionSerializableEIP8141, TransactionSerializedLegacy, } from '../../types/transaction.js' import { sidecarsToVersionedHashes } from '../blob/sidecarsToVersionedHashes.js' @@ -127,6 +128,8 @@ test('via `getTransaction`', async () => { blockNumber: anvilMainnet.forkBlockNumber - 15n, index: 0, }) + if (transaction.type === 'eip8141') + throw new Error('Unexpected eip8141 transaction in legacy fixture block.') const serializedTransaction = serializeTransaction({ ...transaction, data: transaction.input, @@ -145,3 +148,25 @@ test('legacy', async () => { }), ).toMatchInlineSnapshot(`"0xb03B8ffAB1f3Ac3CabE4A0B2ED441fDFd3C96C8E"`) }) + +test('eip8141 requires explicit signature', async () => { + const serializedTransaction = serializeTransaction({ + chainId: 1, + sender: accounts[0].address, + frames: [ + { + mode: 1, + flags: 0x03, + target: null, + gasLimit: 21000n, + data: '0x', + }, + ], + } satisfies TransactionSerializableEIP8141) + + await expect(() => + recoverTransactionAddress({ + serializedTransaction, + }), + ).rejects.toThrow('EIP-8141 transactions require an explicit `signature`') +}) diff --git a/src/utils/signature/recoverTransactionAddress.ts b/src/utils/signature/recoverTransactionAddress.ts index d64d60b679..24ddc6526b 100644 --- a/src/utils/signature/recoverTransactionAddress.ts +++ b/src/utils/signature/recoverTransactionAddress.ts @@ -1,4 +1,5 @@ import type { Address } from 'abitype' +import { BaseError, type BaseErrorType } from '../../errors/base.js' import type { ErrorType } from '../../errors/utils.js' import type { ByteArray, Hex, Signature } from '../../types/misc.js' import type { TransactionSerialized } from '../../types/transaction.js' @@ -22,6 +23,7 @@ export type RecoverTransactionAddressParameters = { export type RecoverTransactionAddressReturnType = Address export type RecoverTransactionAddressErrorType = + | BaseErrorType | SerializeTransactionErrorType | RecoverAddressErrorType | Keccak256ErrorType @@ -35,6 +37,18 @@ export async function recoverTransactionAddress( const transaction = parseTransaction(serializedTransaction) + if ('frames' in transaction) { + if (!signature_) + throw new BaseError( + 'EIP-8141 transactions require an explicit `signature` to recover an address.', + ) + + return await recoverAddress({ + hash: keccak256(serializeTransaction(transaction)), + signature: signature_, + }) + } + const signature = signature_ ?? { r: transaction.r!, s: transaction.s!, diff --git a/src/utils/transaction/getTransactionType.test-d.ts b/src/utils/transaction/getTransactionType.test-d.ts index 1a7deb5876..79a49c0a69 100644 --- a/src/utils/transaction/getTransactionType.test-d.ts +++ b/src/utils/transaction/getTransactionType.test-d.ts @@ -8,6 +8,7 @@ import type { import type { TransactionSerializableEIP4844, TransactionSerializableEIP7702, + TransactionSerializableEIP8141, } from '../../types/transaction.js' import { getTransactionType } from './getTransactionType.js' @@ -17,7 +18,7 @@ test('empty', () => { test('opaque', () => { expectTypeOf(getTransactionType({} as TransactionSerializable)).toEqualTypeOf< - 'legacy' | 'eip1559' | 'eip2930' | 'eip4844' | 'eip7702' + 'legacy' | 'eip1559' | 'eip2930' | 'eip4844' | 'eip7702' | 'eip8141' >() expectTypeOf( getTransactionType({} as TransactionSerializableLegacy), @@ -34,6 +35,9 @@ test('opaque', () => { expectTypeOf( getTransactionType({} as TransactionSerializableEIP7702), ).toEqualTypeOf<'eip7702'>() + expectTypeOf( + getTransactionType({} as TransactionSerializableEIP8141), + ).toEqualTypeOf<'eip8141'>() }) test('const: type', () => { diff --git a/src/utils/transaction/parseTransaction.ts b/src/utils/transaction/parseTransaction.ts index 3c657c30eb..d4a0181374 100644 --- a/src/utils/transaction/parseTransaction.ts +++ b/src/utils/transaction/parseTransaction.ts @@ -181,8 +181,15 @@ function parseTransactionEIP8141( type: 'eip8141', }) const [mode, flags, target, gasLimit, data] = tuple + const parsedMode = mode === '0x' ? 0 : hexToNumber(mode) + if (parsedMode > 2) + throw new InvalidSerializedTransactionError({ + attributes: { frameMode: parsedMode }, + serializedTransaction, + type: 'eip8141', + }) return { - mode: mode === '0x' ? 0 : hexToNumber(mode), + mode: parsedMode as Frame['mode'], flags: flags === '0x' ? 0 : hexToNumber(flags), target: isHex(target) && target !== '0x' ? target : null, gasLimit: gasLimit === '0x' ? 0n : hexToBigInt(gasLimit), diff --git a/src/zksync/actions/claimFailedDeposit.ts b/src/zksync/actions/claimFailedDeposit.ts index ad4e1f6713..0d85b4fada 100644 --- a/src/zksync/actions/claimFailedDeposit.ts +++ b/src/zksync/actions/claimFailedDeposit.ts @@ -176,6 +176,8 @@ export async function claimFailedDeposit< throw new CannotClaimSuccessfulDepositError({ hash: depositHash }) const tx = await getTransaction(l2Client, { hash: depositHash }) + if (!tx.input) + throw new Error('Deposit transaction is missing input calldata.') // Undo the aliasing, since the Mailbox contract set it as for contract address. const l1BridgeAddress = undoL1ToL2Alias(receipt.from) diff --git a/src/zksync/actions/signTransaction.ts b/src/zksync/actions/signTransaction.ts index ab2cdbe175..e560b17599 100644 --- a/src/zksync/actions/signTransaction.ts +++ b/src/zksync/actions/signTransaction.ts @@ -38,7 +38,10 @@ export type SignTransactionParameters< GetAccountParameter & GetChainParameter -export type SignTransactionReturnType = SignTransactionReturnType_ +export type SignTransactionReturnType = Exclude< + SignTransactionReturnType_, + `0x06${string}` +> export type SignTransactionErrorType = SignTransactionErrorType_ @@ -91,5 +94,8 @@ export async function signTransaction< args: SignTransactionParameters, ): Promise { if (isEIP712Transaction(args)) return signEip712Transaction(client, args) - return await signTransaction_(client, args as any) + return (await signTransaction_( + client, + args as any, + )) as SignTransactionReturnType } diff --git a/src/zksync/formatters.test-d.ts b/src/zksync/formatters.test-d.ts index cc482fa0c9..5380f951c6 100644 --- a/src/zksync/formatters.test-d.ts +++ b/src/zksync/formatters.test-d.ts @@ -136,6 +136,7 @@ describe('smoke', () => { | 'eip1559' | 'eip4844' | 'eip7702' + | 'eip8141' | 'eip712' | 'priority' >() From c1f02d009f5e35b0526e9af2cd83554977fe5a1c Mon Sep 17 00:00:00 2001 From: ch4r10t33r Date: Wed, 15 Apr 2026 19:55:17 +0100 Subject: [PATCH 5/9] examples: add EIP-8141 frame transaction examples --- examples/frame-transactions/README.md | 48 +++++ examples/frame-transactions/atomic-batch.ts | 170 ++++++++++++++++++ examples/frame-transactions/package.json | 12 ++ .../simple-self-verified.ts | 101 +++++++++++ .../sponsored-transaction.ts | 156 ++++++++++++++++ examples/frame-transactions/tsconfig.json | 12 ++ 6 files changed, 499 insertions(+) create mode 100644 examples/frame-transactions/README.md create mode 100644 examples/frame-transactions/atomic-batch.ts create mode 100644 examples/frame-transactions/package.json create mode 100644 examples/frame-transactions/simple-self-verified.ts create mode 100644 examples/frame-transactions/sponsored-transaction.ts create mode 100644 examples/frame-transactions/tsconfig.json diff --git a/examples/frame-transactions/README.md b/examples/frame-transactions/README.md new file mode 100644 index 0000000000..1b8c827e24 --- /dev/null +++ b/examples/frame-transactions/README.md @@ -0,0 +1,48 @@ +# EIP-8141 Frame Transaction Examples + +Frame transactions ([EIP-8141](https://eips.ethereum.org/EIPS/eip-8141)) replace the +single-call transaction model with an ordered list of **frames**, each specifying an +execution mode, target, gas budget, and calldata. This enables native account abstraction, +sponsored gas, and atomic multi-operation batches at the protocol level. + +## Prerequisites + +EIP-8141 support is not yet in upstream viem. These examples depend on the +[`frames`](https://github.com/ch4r10t33r/viem/tree/frames) branch of the fork: + +```bash +cd examples/frame-transactions +pnpm install # pulls viem from the fork +``` + +## RPC Endpoint + +All examples target the public demo node: + +``` +https://demo.eip-8141.ethrex.xyz/rpc +``` + +## Running + +```bash +pnpm tsx simple-self-verified.ts +pnpm tsx sponsored-transaction.ts +pnpm tsx atomic-batch.ts +``` + +## Examples + +| File | Scenario | +|------|----------| +| `simple-self-verified.ts` | Minimal VERIFY + SENDER flow: the sender's validator approves, then the sender executes a call | +| `sponsored-transaction.ts` | Third-party pays gas via a DEFAULT frame running paymaster logic at the entry point | +| `atomic-batch.ts` | Two SENDER frames linked with the atomic batch flag: ERC-20 approve then DEX swap, all-or-nothing | + +## Frame Modes + +| Mode | Name | Behaviour | +|------|------|-----------| +| 0 | DEFAULT | Executes as the entry point (address `0xaa`) | +| 1 | VERIFY | Read-only validation; must call the `APPROVE` opcode | +| 2 | SENDER | Executes as `tx.sender` (requires prior approval) | diff --git a/examples/frame-transactions/atomic-batch.ts b/examples/frame-transactions/atomic-batch.ts new file mode 100644 index 0000000000..f7d6ec2263 --- /dev/null +++ b/examples/frame-transactions/atomic-batch.ts @@ -0,0 +1,170 @@ +/** + * Atomic Batch Frame Transaction + * + * Uses the ATOMIC_BATCH_FLAG (0x04) to link two SENDER frames so they + * execute atomically: if either reverts, both revert. + * + * Frame 0 (VERIFY): Sender's validator approves. + * Frame 1 (SENDER): ERC-20 `approve` -- grant the DEX router an allowance. + * flags=0x04 (atomic) links this frame to the next. + * Frame 2 (SENDER): DEX `swapExactTokensForTokens` -- swap tokens. + * flags=0x00 (last frame in the atomic group). + * + * Without atomicity, a successful approve followed by a reverted swap + * would leave a dangling allowance. The atomic batch flag guarantees + * all-or-nothing execution at the protocol level. + */ + +import { + type Address, + createClient, + encodeFunctionData, + type Hex, + http, + parseGwei, + parseUnits, + serializeTransaction, + type TransactionSerializableEIP8141, +} from 'viem' + +const RPC_URL = 'https://demo.eip-8141.ethrex.xyz/rpc' + +// Demo addresses. +const sender: Address = '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266' +const validator: Address = '0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512' +const usdcToken: Address = '0x5FbDB2315678afecb367f032d93F642f64180aa3' +const dexRouter: Address = '0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9' +const wethToken: Address = '0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0' + +const ATOMIC_BATCH_FLAG = 0x04 + +const validatorAbi = [ + { + name: 'validate', + type: 'function', + inputs: [{ name: 'txHash', type: 'bytes32' }], + outputs: [], + stateMutability: 'view', + }, +] as const + +const erc20Abi = [ + { + name: 'approve', + type: 'function', + inputs: [ + { name: 'spender', type: 'address' }, + { name: 'amount', type: 'uint256' }, + ], + outputs: [{ name: '', type: 'bool' }], + stateMutability: 'nonpayable', + }, +] as const + +const dexAbi = [ + { + name: 'swapExactTokensForTokens', + type: 'function', + inputs: [ + { name: 'amountIn', type: 'uint256' }, + { name: 'amountOutMin', type: 'uint256' }, + { name: 'path', type: 'address[]' }, + { name: 'to', type: 'address' }, + { name: 'deadline', type: 'uint256' }, + ], + outputs: [{ name: 'amounts', type: 'uint256[]' }], + stateMutability: 'nonpayable', + }, +] as const + +const swapAmount = parseUnits('1000', 6) // 1000 USDC (6 decimals) +const minOut = parseUnits('0.3', 18) // minimum 0.3 WETH out +const deadline = BigInt(Math.floor(Date.now() / 1000) + 3600) // 1 hour + +const tx: TransactionSerializableEIP8141 = { + type: 'eip8141', + chainId: 7, + nonce: 2, + sender, + maxPriorityFeePerGas: parseGwei('1'), + maxFeePerGas: parseGwei('10'), + maxFeePerBlobGas: 0n, + blobVersionedHashes: [], + frames: [ + // Frame 0 -- VERIFY: sender's validator authorises. + { + mode: 1, + flags: 0x03, + target: validator, + gasLimit: 50_000n, + data: encodeFunctionData({ + abi: validatorAbi, + functionName: 'validate', + args: [ + '0x0000000000000000000000000000000000000000000000000000000000000002', + ], + }), + }, + + // Frame 1 -- SENDER + ATOMIC: approve the DEX router to spend USDC. + // The atomic batch flag (0x04) links this frame to the next one. + // If the swap in frame 2 reverts, this approve is also rolled back. + { + mode: 2, + flags: ATOMIC_BATCH_FLAG, + target: usdcToken, + gasLimit: 60_000n, + data: encodeFunctionData({ + abi: erc20Abi, + functionName: 'approve', + args: [dexRouter, swapAmount], + }), + }, + + // Frame 2 -- SENDER: swap USDC -> WETH on the DEX. + // flags=0x00: last frame in the atomic group, no further chaining. + { + mode: 2, + flags: 0x00, + target: dexRouter, + gasLimit: 200_000n, + data: encodeFunctionData({ + abi: dexAbi, + functionName: 'swapExactTokensForTokens', + args: [swapAmount, minOut, [usdcToken, wethToken], sender, deadline], + }), + }, + ], +} + +async function main() { + const serialized = serializeTransaction(tx) + console.log( + 'Serialized atomic-batch EIP-8141 tx:', + serialized.slice(0, 66), + '...', + ) + console.log('Type byte: 0x06 (EIP-8141)') + console.log('Frames:', tx.frames.length) + console.log(' [0] VERIFY - validator approves') + console.log(' [1] SENDER (atomic) - approve USDC for DEX router') + console.log(' [2] SENDER - swap USDC -> WETH') + console.log() + console.log( + 'Atomic guarantee: if the swap reverts, the approve is rolled back too.', + ) + console.log() + + const client = createClient({ transport: http(RPC_URL) }) + + console.log('Sending to', RPC_URL, '...') + const hash = await client.request({ + method: 'eth_sendRawTransaction' as any, + params: [serialized as Hex], + }) + console.log('Transaction hash:', hash) +} + +main().catch((_err) => { + process.exit(1) +}) diff --git a/examples/frame-transactions/package.json b/examples/frame-transactions/package.json new file mode 100644 index 0000000000..04610923ec --- /dev/null +++ b/examples/frame-transactions/package.json @@ -0,0 +1,12 @@ +{ + "name": "example-frame-transactions", + "private": true, + "type": "module", + "dependencies": { + "viem": "github:ch4r10t33r/viem#frames" + }, + "devDependencies": { + "tsx": "^4.19.0", + "typescript": "^5.0.3" + } +} diff --git a/examples/frame-transactions/simple-self-verified.ts b/examples/frame-transactions/simple-self-verified.ts new file mode 100644 index 0000000000..29cff83866 --- /dev/null +++ b/examples/frame-transactions/simple-self-verified.ts @@ -0,0 +1,101 @@ +/** + * Simple Self-Verified Frame Transaction + * + * The most basic EIP-8141 pattern: two frames. + * + * Frame 0 (VERIFY): The sender's validator contract runs read-only + * validation and calls APPROVE to authorise the tx. + * Frame 1 (SENDER): Executes a plain ETH transfer as the sender. + * + * No third-party payer, no batching -- just native account abstraction. + */ + +import { + type Address, + createClient, + encodeFunctionData, + type Hex, + http, + parseGwei, + serializeTransaction, + type TransactionSerializableEIP8141, +} from 'viem' + +const RPC_URL = 'https://demo.eip-8141.ethrex.xyz/rpc' + +// Demo addresses -- replace with your own for a real network. +const sender: Address = '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266' +const validator: Address = '0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512' +const recipient: Address = '0x70997970C51812dc3A010C7d01b50e0d17dc79C8' + +// Minimal validator ABI -- the VERIFY frame calls `validate` on the +// sender's validator contract. The contract is expected to inspect +// the transaction context and call the APPROVE opcode if it is valid. +const validatorAbi = [ + { + name: 'validate', + type: 'function', + inputs: [{ name: 'txHash', type: 'bytes32' }], + outputs: [], + stateMutability: 'view', + }, +] as const + +const tx: TransactionSerializableEIP8141 = { + type: 'eip8141', + chainId: 7, + nonce: 0, + sender, + maxPriorityFeePerGas: parseGwei('1'), + maxFeePerGas: parseGwei('10'), + maxFeePerBlobGas: 0n, + blobVersionedHashes: [], + frames: [ + // Frame 0 -- VERIFY: read-only validation by the sender's validator. + // flags=0x01 means approval scope covers the immediate next frame. + { + mode: 1, + flags: 0x01, + target: validator, + gasLimit: 50_000n, + data: encodeFunctionData({ + abi: validatorAbi, + functionName: 'validate', + args: [ + '0x0000000000000000000000000000000000000000000000000000000000000000', + ], + }), + }, + + // Frame 1 -- SENDER: execute as the sender (transfer ETH to recipient). + // An empty `data` field with a target is a plain value transfer. + { + mode: 2, + flags: 0x00, + target: recipient, + gasLimit: 21_000n, + data: '0x', + }, + ], +} + +async function main() { + const serialized = serializeTransaction(tx) + console.log('Serialized EIP-8141 tx:', serialized.slice(0, 66), '...') + console.log('Type byte: 0x06 (EIP-8141)') + console.log('Frames:', tx.frames.length) + console.log() + + const client = createClient({ transport: http(RPC_URL) }) + + console.log('Sending to', RPC_URL, '...') + const hash = await client.request({ + method: 'eth_sendRawTransaction' as any, + params: [serialized as Hex], + }) + console.log('Transaction hash:', hash) +} + +main().catch((_err) => { + process.exit(1) +}) diff --git a/examples/frame-transactions/sponsored-transaction.ts b/examples/frame-transactions/sponsored-transaction.ts new file mode 100644 index 0000000000..9124383688 --- /dev/null +++ b/examples/frame-transactions/sponsored-transaction.ts @@ -0,0 +1,156 @@ +/** + * Sponsored (Paymaster) Frame Transaction + * + * Demonstrates how a third party can pay gas on behalf of the sender + * using three frames: + * + * Frame 0 (VERIFY): The sender's validator approves the transaction. + * Frame 1 (SENDER): The sender's intended action (a contract call). + * Frame 2 (DEFAULT): Runs as the entry point (0xaa), executing paymaster + * logic that deducts fees from the sponsor rather + * than the sender's balance. + * + * The DEFAULT frame is the key: it executes at the protocol entry point + * address, which has special authority to manage gas payment on behalf + * of an external sponsor. + */ + +import { + type Address, + createClient, + encodeFunctionData, + type Hex, + http, + parseGwei, + serializeTransaction, + type TransactionSerializableEIP8141, +} from 'viem' + +const RPC_URL = 'https://demo.eip-8141.ethrex.xyz/rpc' + +// Demo addresses. +const sender: Address = '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266' +const validator: Address = '0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512' +const paymaster: Address = '0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0' +const targetContract: Address = '0x5FbDB2315678afecb367f032d93F642f64180aa3' + +const validatorAbi = [ + { + name: 'validate', + type: 'function', + inputs: [{ name: 'txHash', type: 'bytes32' }], + outputs: [], + stateMutability: 'view', + }, +] as const + +// The sender wants to call `store(uint256)` on a target contract. +const storageAbi = [ + { + name: 'store', + type: 'function', + inputs: [{ name: 'value', type: 'uint256' }], + outputs: [], + stateMutability: 'nonpayable', + }, +] as const + +// The paymaster contract's `sponsorGas` method is invoked in the DEFAULT +// frame. This runs at the entry point address and arranges for the +// sponsor to cover all gas costs. +const paymasterAbi = [ + { + name: 'sponsorGas', + type: 'function', + inputs: [ + { name: 'sponsor', type: 'address' }, + { name: 'sender', type: 'address' }, + ], + outputs: [], + stateMutability: 'nonpayable', + }, +] as const + +const tx: TransactionSerializableEIP8141 = { + type: 'eip8141', + chainId: 7, + nonce: 1, + sender, + maxPriorityFeePerGas: parseGwei('1'), + maxFeePerGas: parseGwei('10'), + maxFeePerBlobGas: 0n, + blobVersionedHashes: [], + frames: [ + // Frame 0 -- VERIFY: sender's validator authorises. + // flags=0x03 means approval scope covers all subsequent frames. + { + mode: 1, + flags: 0x03, + target: validator, + gasLimit: 50_000n, + data: encodeFunctionData({ + abi: validatorAbi, + functionName: 'validate', + args: [ + '0x0000000000000000000000000000000000000000000000000000000000000001', + ], + }), + }, + + // Frame 1 -- SENDER: the user's actual intent, runs as tx.sender. + { + mode: 2, + flags: 0x00, + target: targetContract, + gasLimit: 100_000n, + data: encodeFunctionData({ + abi: storageAbi, + functionName: 'store', + args: [42n], + }), + }, + + // Frame 2 -- DEFAULT: paymaster logic at the entry point. + // Executes as address 0xaa, calling the paymaster contract to + // debit the sponsor's pre-funded balance instead of the sender's. + { + mode: 0, + flags: 0x00, + target: paymaster, + gasLimit: 80_000n, + data: encodeFunctionData({ + abi: paymasterAbi, + functionName: 'sponsorGas', + args: [paymaster, sender], + }), + }, + ], +} + +async function main() { + const serialized = serializeTransaction(tx) + console.log( + 'Serialized sponsored EIP-8141 tx:', + serialized.slice(0, 66), + '...', + ) + console.log('Type byte: 0x06 (EIP-8141)') + console.log('Frames:', tx.frames.length) + console.log(' [0] VERIFY - validator approves') + console.log(' [1] SENDER - store(42) on target contract') + console.log(' [2] DEFAULT - paymaster sponsors gas') + console.log() + + const client = createClient({ transport: http(RPC_URL) }) + + console.log('Sending to', RPC_URL, '...') + const hash = await client.request({ + method: 'eth_sendRawTransaction' as any, + params: [serialized as Hex], + }) + console.log('Transaction hash:', hash) +} + +main().catch((_err) => { + process.exit(1) +}) diff --git a/examples/frame-transactions/tsconfig.json b/examples/frame-transactions/tsconfig.json new file mode 100644 index 0000000000..23e7586435 --- /dev/null +++ b/examples/frame-transactions/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "esModuleInterop": true, + "noEmit": true, + "skipLibCheck": true + }, + "include": ["."] +} From 1e7ae453369a2513da24e2abf5f590f90a59a1b8 Mon Sep 17 00:00:00 2001 From: ch4r10t33r Date: Wed, 15 Apr 2026 22:15:53 +0100 Subject: [PATCH 6/9] examples: local viem path, rpc error logging; guard install hooks for deps --- examples/frame-transactions/README.md | 5 +- examples/frame-transactions/package.json | 2 +- package.json | 4 +- pnpm-lock.yaml | 109 +++++++++++++++++------ 4 files changed, 87 insertions(+), 33 deletions(-) diff --git a/examples/frame-transactions/README.md b/examples/frame-transactions/README.md index 1b8c827e24..2693fd48d8 100644 --- a/examples/frame-transactions/README.md +++ b/examples/frame-transactions/README.md @@ -7,12 +7,11 @@ sponsored gas, and atomic multi-operation batches at the protocol level. ## Prerequisites -EIP-8141 support is not yet in upstream viem. These examples depend on the -[`frames`](https://github.com/ch4r10t33r/viem/tree/frames) branch of the fork: +These examples use the local `viem` package from this repository: ```bash cd examples/frame-transactions -pnpm install # pulls viem from the fork +pnpm install # links viem from ../../src ``` ## RPC Endpoint diff --git a/examples/frame-transactions/package.json b/examples/frame-transactions/package.json index 04610923ec..0d0456efb1 100644 --- a/examples/frame-transactions/package.json +++ b/examples/frame-transactions/package.json @@ -3,7 +3,7 @@ "private": true, "type": "module", "dependencies": { - "viem": "github:ch4r10t33r/viem#frames" + "viem": "file:../../src" }, "devDependencies": { "tsx": "^4.19.0", diff --git a/package.json b/package.json index 37a2d95090..7673c8f617 100644 --- a/package.json +++ b/package.json @@ -24,9 +24,9 @@ "check:types": "tsc -b", "check:unused": "pnpm clean && knip", "gen:tempo-abis": "bun scripts/generateTempoAbis.ts", - "postinstall": "git submodule update --init --recursive && pnpm contracts:build", + "postinstall": "node -e \"if (process.env.INIT_CWD && process.env.INIT_CWD !== process.cwd()) process.exit(0)\" && git submodule update --init --recursive && pnpm contracts:build", "preconstruct": "bun scripts/preconstruct.ts", - "preinstall": "pnpx only-allow pnpm", + "preinstall": "node -e \"if (process.env.INIT_CWD && process.env.INIT_CWD !== process.cwd()) process.exit(0)\" && pnpx only-allow pnpm", "prepare": "pnpm simple-git-hooks", "prepublishOnly": "bun scripts/prepublishOnly.ts", "size": "size-limit", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d34732be6b..08330d7d98 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -238,7 +238,7 @@ importers: dependencies: viem: specifier: latest - version: 2.47.6(typescript@5.6.2)(zod@4.3.6) + version: 2.47.17(typescript@5.6.2)(zod@4.3.6) devDependencies: typescript: specifier: ^5.0.3 @@ -251,7 +251,7 @@ importers: dependencies: viem: specifier: latest - version: 2.47.6(typescript@5.6.2)(zod@4.3.6) + version: 2.47.17(typescript@5.6.2)(zod@4.3.6) devDependencies: typescript: specifier: ^5.0.3 @@ -264,7 +264,7 @@ importers: dependencies: viem: specifier: latest - version: 2.47.6(typescript@5.6.2)(zod@4.3.6) + version: 2.47.17(typescript@5.6.2)(zod@4.3.6) devDependencies: typescript: specifier: ^5.0.3 @@ -277,7 +277,7 @@ importers: dependencies: viem: specifier: latest - version: 2.47.6(typescript@5.6.2)(zod@4.3.6) + version: 2.47.17(typescript@5.6.2)(zod@4.3.6) devDependencies: typescript: specifier: ^5.0.3 @@ -290,7 +290,7 @@ importers: dependencies: viem: specifier: latest - version: 2.47.6(typescript@5.6.2)(zod@4.3.6) + version: 2.47.17(typescript@5.6.2)(zod@4.3.6) devDependencies: typescript: specifier: ^5.0.3 @@ -309,7 +309,7 @@ importers: version: 19.0.0(react@19.0.0) viem: specifier: latest - version: 2.47.6(typescript@5.6.2)(zod@4.3.6) + version: 2.47.17(typescript@5.6.2)(zod@4.3.6) devDependencies: '@types/react': specifier: ^19 @@ -337,7 +337,7 @@ importers: version: 19.0.0(react@19.0.0) viem: specifier: latest - version: 2.47.6(typescript@5.6.2)(zod@4.3.6) + version: 2.47.17(typescript@5.6.2)(zod@4.3.6) devDependencies: '@types/react': specifier: ^19 @@ -359,7 +359,7 @@ importers: dependencies: viem: specifier: latest - version: 2.47.6(typescript@5.6.2)(zod@4.3.6) + version: 2.47.17(typescript@5.6.2)(zod@4.3.6) devDependencies: typescript: specifier: ^5.0.3 @@ -372,7 +372,7 @@ importers: dependencies: viem: specifier: latest - version: 2.47.6(typescript@5.6.2)(zod@4.3.6) + version: 2.47.17(typescript@5.6.2)(zod@4.3.6) devDependencies: typescript: specifier: ^5.0.3 @@ -391,7 +391,7 @@ importers: version: 19.0.0(react@19.0.0) viem: specifier: latest - version: 2.47.6(typescript@5.6.2)(zod@4.3.6) + version: 2.47.17(typescript@5.6.2)(zod@4.3.6) devDependencies: '@types/react': specifier: ^19 @@ -413,7 +413,7 @@ importers: dependencies: viem: specifier: latest - version: 2.47.6(typescript@5.6.2)(zod@4.3.6) + version: 2.47.17(typescript@5.6.2)(zod@4.3.6) devDependencies: typescript: specifier: ^5.0.3 @@ -422,11 +422,24 @@ importers: specifier: ^7.1.11 version: 7.1.11(@types/node@24.5.2)(jiti@2.6.0)(lightningcss@1.30.2)(terser@5.36.0)(tsx@4.21.0)(yaml@2.8.3) + examples/frame-transactions: + dependencies: + viem: + specifier: file:../../src + version: file:src(typescript@5.9.3)(zod@4.3.6) + devDependencies: + tsx: + specifier: ^4.19.0 + version: 4.21.0 + typescript: + specifier: ^5.0.3 + version: 5.9.3 + examples/logs_block-event-logs: dependencies: viem: specifier: latest - version: 2.47.6(typescript@5.6.2)(zod@4.3.6) + version: 2.47.17(typescript@5.6.2)(zod@4.3.6) devDependencies: typescript: specifier: ^5.0.3 @@ -439,7 +452,7 @@ importers: dependencies: viem: specifier: latest - version: 2.47.6(typescript@5.6.2)(zod@4.3.6) + version: 2.47.17(typescript@5.6.2)(zod@4.3.6) devDependencies: typescript: specifier: ^5.0.3 @@ -452,7 +465,7 @@ importers: dependencies: viem: specifier: latest - version: 2.47.6(typescript@5.6.2)(zod@4.3.6) + version: 2.47.17(typescript@5.6.2)(zod@4.3.6) devDependencies: typescript: specifier: ^5.0.3 @@ -499,7 +512,7 @@ importers: version: 19.0.0(react@19.0.0) viem: specifier: latest - version: 2.47.6(typescript@5.6.2)(zod@4.3.6) + version: 2.47.17(typescript@5.6.2)(zod@4.3.6) devDependencies: '@types/react': specifier: ^19 @@ -527,7 +540,7 @@ importers: version: 19.0.0(react@19.0.0) viem: specifier: latest - version: 2.47.6(typescript@5.6.2)(zod@4.3.6) + version: 2.47.17(typescript@5.6.2)(zod@4.3.6) devDependencies: '@types/react': specifier: ^19 @@ -549,7 +562,7 @@ importers: dependencies: viem: specifier: latest - version: 2.47.6(typescript@5.6.2)(zod@4.3.6) + version: 2.47.17(typescript@5.6.2)(zod@4.3.6) devDependencies: typescript: specifier: ^5.0.3 @@ -568,7 +581,7 @@ importers: version: 19.0.0(react@19.0.0) viem: specifier: latest - version: 2.47.6(typescript@5.6.2)(zod@4.3.6) + version: 2.47.17(typescript@5.6.2)(zod@4.3.6) devDependencies: '@types/react': specifier: ^19 @@ -3204,6 +3217,7 @@ packages: basic-ftp@5.2.0: resolution: {integrity: sha512-VoMINM2rqJwJgfdHq6RiUudKt2BV+FY5ZFezP/ypmwayk68+NzzAQy4XXLlqsGD4MCzq3DrmNFD/uUmBJuGoXw==} engines: {node: '>=10.0.0'} + deprecated: Security vulnerability fixed in 5.2.1, please upgrade bcrypt-pbkdf@1.0.2: resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==} @@ -5034,6 +5048,14 @@ packages: outvariant@1.4.0: resolution: {integrity: sha512-AlWY719RF02ujitly7Kk/0QlV+pXGFDHrHf9O2OKqyqgBieaPOIeuSkL8sRK6j2WK+/ZAURq2kZsY0d8JapUiw==} + ox@0.14.15: + resolution: {integrity: sha512-3TubCmbKen/cuZQzX0qDbOS5lojjdSZ90lqKxWIDWd5siuJ0IJBaTXMYs8eMPLcraqnOwGZazz3apHPGiRCkGQ==} + peerDependencies: + typescript: '>=5.4.0' + peerDependenciesMeta: + typescript: + optional: true + ox@0.14.7: resolution: {integrity: sha512-zSQ/cfBdolj7U4++NAvH7sI+VG0T3pEohITCgcQj8KlawvTDY4vGVhDT64Atsm0d6adWfIYHDpu88iUBMMp+AQ==} peerDependencies: @@ -5372,6 +5394,7 @@ packages: react-server-dom-webpack@19.2.4: resolution: {integrity: sha512-zEhkWv6RhXDctC2N7yEUHg3751nvFg81ydHj8LTTZuukF/IF1gcOKqqAL6Ds+kS5HtDVACYPik0IvzkgYXPhlQ==} engines: {node: '>=0.10.0'} + deprecated: High Security Vulnerability in React Server Components peerDependencies: react: ^19.2.4 react-dom: ^19.2.4 @@ -6223,8 +6246,8 @@ packages: vfile@6.0.3: resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} - viem@2.47.6: - resolution: {integrity: sha512-zExmbI99NGvMdYa7fmqSTLgkwh48dmhgEqFrUgkpL4kfG4XkVefZ8dZqIKVUhZo6Uhf0FrrEXOsHm9LUyIvI2Q==} + viem@2.47.17: + resolution: {integrity: sha512-yNUKw6b1nd1i96GcJPqp096w5VVjUky/6PLT8UeUsEArzhD9YRrC0QJ50o8YEF7xA6M0FK8e6u5tAMyBLLl7tw==} peerDependencies: typescript: '>=5.0.4' peerDependenciesMeta: @@ -8216,7 +8239,7 @@ snapshots: pino-pretty: 10.3.1 prom-client: 14.2.0 type-fest: 4.39.0 - viem: 2.47.6(typescript@5.9.3)(zod@3.23.8) + viem: 2.47.17(typescript@5.9.3)(zod@3.23.8) yargs: 17.7.2 zod: 3.23.8 zod-validation-error: 1.5.0(zod@3.23.8) @@ -11562,7 +11585,7 @@ snapshots: outvariant@1.4.0: {} - ox@0.14.7(typescript@5.6.2)(zod@4.3.6): + ox@0.14.15(typescript@5.6.2)(zod@4.3.6): dependencies: '@adraffy/ens-normalize': 1.11.1 '@noble/ciphers': 1.3.0 @@ -11577,7 +11600,7 @@ snapshots: transitivePeerDependencies: - zod - ox@0.14.7(typescript@5.9.3)(zod@3.23.8): + ox@0.14.15(typescript@5.9.3)(zod@3.23.8): dependencies: '@adraffy/ens-normalize': 1.11.1 '@noble/ciphers': 1.3.0 @@ -11592,6 +11615,21 @@ snapshots: transitivePeerDependencies: - zod + ox@0.14.7(typescript@5.6.2)(zod@4.3.6): + dependencies: + '@adraffy/ens-normalize': 1.11.1 + '@noble/ciphers': 1.3.0 + '@noble/curves': 1.9.1 + '@noble/hashes': 1.8.0 + '@scure/bip32': 1.7.0 + '@scure/bip39': 1.6.0 + abitype: 1.2.3(typescript@5.6.2)(zod@4.3.6) + eventemitter3: 5.0.1 + optionalDependencies: + typescript: 5.6.2 + transitivePeerDependencies: + - zod + ox@0.14.7(typescript@5.9.3)(zod@4.3.6): dependencies: '@adraffy/ens-normalize': 1.11.1 @@ -13013,7 +13051,7 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.2 - viem@2.47.6(typescript@5.6.2)(zod@4.3.6): + viem@2.47.17(typescript@5.6.2)(zod@4.3.6): dependencies: '@noble/curves': 1.9.1 '@noble/hashes': 1.8.0 @@ -13021,7 +13059,7 @@ snapshots: '@scure/bip39': 1.6.0 abitype: 1.2.3(typescript@5.6.2)(zod@4.3.6) isows: 1.0.7(ws@8.18.3) - ox: 0.14.7(typescript@5.6.2)(zod@4.3.6) + ox: 0.14.15(typescript@5.6.2)(zod@4.3.6) ws: 8.18.3 optionalDependencies: typescript: 5.6.2 @@ -13030,7 +13068,7 @@ snapshots: - utf-8-validate - zod - viem@2.47.6(typescript@5.9.3)(zod@3.23.8): + viem@2.47.17(typescript@5.9.3)(zod@3.23.8): dependencies: '@noble/curves': 1.9.1 '@noble/hashes': 1.8.0 @@ -13038,7 +13076,7 @@ snapshots: '@scure/bip39': 1.6.0 abitype: 1.2.3(typescript@5.9.3)(zod@3.23.8) isows: 1.0.7(ws@8.18.3) - ox: 0.14.7(typescript@5.9.3)(zod@3.23.8) + ox: 0.14.15(typescript@5.9.3)(zod@3.23.8) ws: 8.18.3 optionalDependencies: typescript: 5.9.3 @@ -13064,6 +13102,23 @@ snapshots: - utf-8-validate - zod + viem@file:src(typescript@5.9.3)(zod@4.3.6): + dependencies: + '@noble/curves': 1.9.1 + '@noble/hashes': 1.8.0 + '@scure/bip32': 1.7.0 + '@scure/bip39': 1.6.0 + abitype: 1.2.3(typescript@5.9.3)(zod@4.3.6) + isows: 1.0.7(ws@8.18.3) + ox: 0.14.7(typescript@5.9.3)(zod@4.3.6) + ws: 8.18.3 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + - zod + vite-plugin-arraybuffer@0.1.4: {} vite-plugin-wasm@3.5.0(vite@7.1.11(@types/node@24.5.2)(jiti@2.6.0)(lightningcss@1.30.2)(terser@5.36.0)(tsx@4.21.0)(yaml@2.8.3)): From 1eac0a91f9f9a61387f9362659a028baa40d1537 Mon Sep 17 00:00:00 2001 From: ch4r10t33r Date: Wed, 15 Apr 2026 22:16:41 +0100 Subject: [PATCH 7/9] examples: log rpc send failures in frame transaction scripts --- examples/frame-transactions/atomic-batch.ts | 3 ++- examples/frame-transactions/simple-self-verified.ts | 3 ++- examples/frame-transactions/sponsored-transaction.ts | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/examples/frame-transactions/atomic-batch.ts b/examples/frame-transactions/atomic-batch.ts index f7d6ec2263..ab7e809585 100644 --- a/examples/frame-transactions/atomic-batch.ts +++ b/examples/frame-transactions/atomic-batch.ts @@ -165,6 +165,7 @@ async function main() { console.log('Transaction hash:', hash) } -main().catch((_err) => { +main().catch((err) => { + console.log('Failed to send frame transaction.', err) process.exit(1) }) diff --git a/examples/frame-transactions/simple-self-verified.ts b/examples/frame-transactions/simple-self-verified.ts index 29cff83866..efe277ea93 100644 --- a/examples/frame-transactions/simple-self-verified.ts +++ b/examples/frame-transactions/simple-self-verified.ts @@ -96,6 +96,7 @@ async function main() { console.log('Transaction hash:', hash) } -main().catch((_err) => { +main().catch((err) => { + console.log('Failed to send frame transaction.', err) process.exit(1) }) diff --git a/examples/frame-transactions/sponsored-transaction.ts b/examples/frame-transactions/sponsored-transaction.ts index 9124383688..cb73bfe06c 100644 --- a/examples/frame-transactions/sponsored-transaction.ts +++ b/examples/frame-transactions/sponsored-transaction.ts @@ -151,6 +151,7 @@ async function main() { console.log('Transaction hash:', hash) } -main().catch((_err) => { +main().catch((err) => { + console.log('Failed to send frame transaction.', err) process.exit(1) }) From 05d4e94b0b83ad16fa50e4afd91e07cdd12fd0f6 Mon Sep 17 00:00:00 2001 From: chiranjeev13 Date: Sat, 18 Apr 2026 05:28:10 +0530 Subject: [PATCH 8/9] examples: update RPC URL and chain ID in frame transaction examples - Changed RPC URL to 'https://rpc1.eip-8141.ethrex.xyz' for consistency. - Introduced a constant for CHAIN_ID set to 3151908 across examples. - Updated transaction frames to include a default value of 0n for the 'value' field. - Enhanced console logs to display the chain ID during transaction sending. --- examples/frame-transactions/README.md | 4 +- examples/frame-transactions/atomic-batch.ts | 10 +- .../simple-self-verified.ts | 13 +- .../sponsored-transaction.ts | 10 +- src/types/transaction.ts | 4 + src/utils/transaction/assertTransaction.ts | 9 ++ src/utils/transaction/eip8141.test.ts | 123 +++++++++++++----- src/utils/transaction/parseTransaction.ts | 10 +- src/utils/transaction/serializeTransaction.ts | 11 +- 9 files changed, 145 insertions(+), 49 deletions(-) diff --git a/examples/frame-transactions/README.md b/examples/frame-transactions/README.md index 2693fd48d8..5f9ff3c935 100644 --- a/examples/frame-transactions/README.md +++ b/examples/frame-transactions/README.md @@ -19,7 +19,9 @@ pnpm install # links viem from ../../src All examples target the public demo node: ``` -https://demo.eip-8141.ethrex.xyz/rpc +https://rpc1.eip-8141.ethrex.xyz +https://rpc2.eip-8141.ethrex.xyz +https://rpc3.eip-8141.ethrex.xyz ``` ## Running diff --git a/examples/frame-transactions/atomic-batch.ts b/examples/frame-transactions/atomic-batch.ts index ab7e809585..af7d3f9884 100644 --- a/examples/frame-transactions/atomic-batch.ts +++ b/examples/frame-transactions/atomic-batch.ts @@ -27,7 +27,8 @@ import { type TransactionSerializableEIP8141, } from 'viem' -const RPC_URL = 'https://demo.eip-8141.ethrex.xyz/rpc' +const RPC_URL = 'https://rpc1.eip-8141.ethrex.xyz' +const CHAIN_ID = 3151908 // Demo addresses. const sender: Address = '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266' @@ -83,7 +84,7 @@ const deadline = BigInt(Math.floor(Date.now() / 1000) + 3600) // 1 hour const tx: TransactionSerializableEIP8141 = { type: 'eip8141', - chainId: 7, + chainId: CHAIN_ID, nonce: 2, sender, maxPriorityFeePerGas: parseGwei('1'), @@ -97,6 +98,7 @@ const tx: TransactionSerializableEIP8141 = { flags: 0x03, target: validator, gasLimit: 50_000n, + value: 0n, data: encodeFunctionData({ abi: validatorAbi, functionName: 'validate', @@ -114,6 +116,7 @@ const tx: TransactionSerializableEIP8141 = { flags: ATOMIC_BATCH_FLAG, target: usdcToken, gasLimit: 60_000n, + value: 0n, data: encodeFunctionData({ abi: erc20Abi, functionName: 'approve', @@ -128,6 +131,7 @@ const tx: TransactionSerializableEIP8141 = { flags: 0x00, target: dexRouter, gasLimit: 200_000n, + value: 0n, data: encodeFunctionData({ abi: dexAbi, functionName: 'swapExactTokensForTokens', @@ -157,7 +161,7 @@ async function main() { const client = createClient({ transport: http(RPC_URL) }) - console.log('Sending to', RPC_URL, '...') + console.log('Sending to', RPC_URL, `(chainId ${CHAIN_ID}) ...`) const hash = await client.request({ method: 'eth_sendRawTransaction' as any, params: [serialized as Hex], diff --git a/examples/frame-transactions/simple-self-verified.ts b/examples/frame-transactions/simple-self-verified.ts index efe277ea93..22cd185ecc 100644 --- a/examples/frame-transactions/simple-self-verified.ts +++ b/examples/frame-transactions/simple-self-verified.ts @@ -16,12 +16,14 @@ import { encodeFunctionData, type Hex, http, + parseEther, parseGwei, serializeTransaction, type TransactionSerializableEIP8141, } from 'viem' -const RPC_URL = 'https://demo.eip-8141.ethrex.xyz/rpc' +const RPC_URL = 'https://rpc1.eip-8141.ethrex.xyz' +const CHAIN_ID = 3151908 // Demo addresses -- replace with your own for a real network. const sender: Address = '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266' @@ -43,7 +45,7 @@ const validatorAbi = [ const tx: TransactionSerializableEIP8141 = { type: 'eip8141', - chainId: 7, + chainId: CHAIN_ID, nonce: 0, sender, maxPriorityFeePerGas: parseGwei('1'), @@ -58,6 +60,7 @@ const tx: TransactionSerializableEIP8141 = { flags: 0x01, target: validator, gasLimit: 50_000n, + value: 0n, data: encodeFunctionData({ abi: validatorAbi, functionName: 'validate', @@ -67,13 +70,13 @@ const tx: TransactionSerializableEIP8141 = { }), }, - // Frame 1 -- SENDER: execute as the sender (transfer ETH to recipient). - // An empty `data` field with a target is a plain value transfer. + // Frame 1 -- SENDER: transfer ETH to recipient. { mode: 2, flags: 0x00, target: recipient, gasLimit: 21_000n, + value: parseEther('0.001'), data: '0x', }, ], @@ -88,7 +91,7 @@ async function main() { const client = createClient({ transport: http(RPC_URL) }) - console.log('Sending to', RPC_URL, '...') + console.log('Sending to', RPC_URL, `(chainId ${CHAIN_ID}) ...`) const hash = await client.request({ method: 'eth_sendRawTransaction' as any, params: [serialized as Hex], diff --git a/examples/frame-transactions/sponsored-transaction.ts b/examples/frame-transactions/sponsored-transaction.ts index cb73bfe06c..fa9425ad86 100644 --- a/examples/frame-transactions/sponsored-transaction.ts +++ b/examples/frame-transactions/sponsored-transaction.ts @@ -26,7 +26,8 @@ import { type TransactionSerializableEIP8141, } from 'viem' -const RPC_URL = 'https://demo.eip-8141.ethrex.xyz/rpc' +const RPC_URL = 'https://rpc1.eip-8141.ethrex.xyz' +const CHAIN_ID = 3151908 // Demo addresses. const sender: Address = '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266' @@ -73,7 +74,7 @@ const paymasterAbi = [ const tx: TransactionSerializableEIP8141 = { type: 'eip8141', - chainId: 7, + chainId: CHAIN_ID, nonce: 1, sender, maxPriorityFeePerGas: parseGwei('1'), @@ -88,6 +89,7 @@ const tx: TransactionSerializableEIP8141 = { flags: 0x03, target: validator, gasLimit: 50_000n, + value: 0n, data: encodeFunctionData({ abi: validatorAbi, functionName: 'validate', @@ -103,6 +105,7 @@ const tx: TransactionSerializableEIP8141 = { flags: 0x00, target: targetContract, gasLimit: 100_000n, + value: 0n, data: encodeFunctionData({ abi: storageAbi, functionName: 'store', @@ -118,6 +121,7 @@ const tx: TransactionSerializableEIP8141 = { flags: 0x00, target: paymaster, gasLimit: 80_000n, + value: 0n, data: encodeFunctionData({ abi: paymasterAbi, functionName: 'sponsorGas', @@ -143,7 +147,7 @@ async function main() { const client = createClient({ transport: http(RPC_URL) }) - console.log('Sending to', RPC_URL, '...') + console.log('Sending to', RPC_URL, `(chainId ${CHAIN_ID}) ...`) const hash = await client.request({ method: 'eth_sendRawTransaction' as any, params: [serialized as Hex], diff --git a/src/types/transaction.ts b/src/types/transaction.ts index 5229b22e9f..b1eaf1d30d 100644 --- a/src/types/transaction.ts +++ b/src/types/transaction.ts @@ -60,6 +60,10 @@ export type Frame = { target: Address | null /** Gas allocated exclusively to this frame. */ gasLimit: bigint + /** + * Must be `0` for `DEFAULT` and `VERIFY` modes; only `SENDER` may be non-zero. + */ + value?: bigint | undefined /** Input data passed to the frame. */ data: Hex } diff --git a/src/utils/transaction/assertTransaction.ts b/src/utils/transaction/assertTransaction.ts index 829414b0e0..decd4a837c 100644 --- a/src/utils/transaction/assertTransaction.ts +++ b/src/utils/transaction/assertTransaction.ts @@ -101,6 +101,15 @@ export function assertTransactionEIP8141( throw new InvalidAddressError({ address: frame.target }) if (frame.gasLimit > maxInt64) throw new BaseError('`frame.gasLimit` must be <= 2^63 - 1.') + const frameValue = frame.value ?? 0n + if (frameValue > maxUint256) + throw new BaseError('`frame.value` must be less than 2^256.') + if (frameValue < 0n) + throw new BaseError('`frame.value` must not be negative.') + if (frame.mode !== SENDER && frameValue !== 0n) + throw new BaseError( + '`frame.value` must be 0 for DEFAULT and VERIFY frames per EIP-8141.', + ) totalFrameGas += frame.gasLimit if (totalFrameGas > maxInt64) throw new BaseError('Total frame gas must be <= 2^63 - 1.') diff --git a/src/utils/transaction/eip8141.test.ts b/src/utils/transaction/eip8141.test.ts index 83e8b46589..004700da4e 100644 --- a/src/utils/transaction/eip8141.test.ts +++ b/src/utils/transaction/eip8141.test.ts @@ -1,5 +1,6 @@ import { describe, expect, test } from 'vitest' import { maxUint256 } from '../../constants/number.js' +import { getAddress } from '../address/getAddress.js' import type { TransactionSerializableEIP8141, TransactionSerializedEIP8141, @@ -26,6 +27,7 @@ const baseEIP8141: TransactionSerializableEIP8141 = { flags: 0x03, target: null, gasLimit: 50000n, + value: 0n, data: '0xdeadbeef', }, { @@ -33,6 +35,7 @@ const baseEIP8141: TransactionSerializableEIP8141 = { flags: 0x00, target: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', gasLimit: 100000n, + value: 0n, data: '0xcafebabe', }, ], @@ -43,6 +46,33 @@ const baseEIP8141: TransactionSerializableEIP8141 = { } describe('eip8141 serialization', () => { + test('roundtrip: SENDER frame value preserved', () => { + const tx: TransactionSerializableEIP8141 = { + chainId: 1, + sender, + frames: [ + { + mode: 1, + flags: 0x03, + target: null, + gasLimit: 21000n, + value: 0n, + data: '0x', + }, + { + mode: 2, + flags: 0x00, + target: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + gasLimit: 21000n, + value: 1234567890123456789n, + data: '0x', + }, + ], + } + const parsed = parseTransaction(serializeTransaction(tx)) + expect(parsed.frames[1].value).toBe(1234567890123456789n) + }) + test('roundtrip: serialize then parse', () => { const serialized = serializeTransaction(baseEIP8141) expect(serialized.startsWith('0x06')).toBe(true) @@ -55,6 +85,10 @@ describe('eip8141 serialization', () => { expect(parsed).toEqual({ ...expected, type: 'eip8141', + frames: expected.frames.map((f) => ({ + ...f, + target: f.target ? getAddress(f.target) : null, + })), }) }) @@ -68,6 +102,7 @@ describe('eip8141 serialization', () => { flags: 0x03, target: null, gasLimit: 21000n, + value: 0n, data: '0x00', }, ], @@ -94,6 +129,7 @@ describe('eip8141 serialization', () => { flags: 0x01, target: null, gasLimit: 50000n, + value: 0n, data: '0xab', }, { @@ -101,6 +137,7 @@ describe('eip8141 serialization', () => { flags: 0x02, target: null, gasLimit: 50000n, + value: 0n, data: '0xcd', }, { @@ -108,6 +145,7 @@ describe('eip8141 serialization', () => { flags: 0x03, target: null, gasLimit: 50000n, + value: 0n, data: '0xef', }, ], @@ -124,12 +162,13 @@ describe('eip8141 serialization', () => { chainId: 1, sender, frames: [ - { mode: 1, flags: 0x03, target: null, gasLimit: 50000n, data: '0xaa' }, + { mode: 1, flags: 0x03, target: null, gasLimit: 50000n, value: 0n, data: '0xaa' }, { mode: 2, flags: 0x04, target: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', gasLimit: 100000n, + value: 0n, data: '0xbb', }, { @@ -137,6 +176,7 @@ describe('eip8141 serialization', () => { flags: 0x00, target: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', gasLimit: 100000n, + value: 0n, data: '0xcc', }, ], @@ -152,12 +192,13 @@ describe('eip8141 serialization', () => { chainId: 1, sender, frames: [ - { mode: 0, flags: 0x00, target: sender, gasLimit: 10000n, data: '0x' }, + { mode: 0, flags: 0x00, target: sender, gasLimit: 10000n, value: 0n, data: '0x' }, { mode: 1, flags: 0x03, target: null, gasLimit: 50000n, + value: 0n, data: '0xdeadbeef', }, { @@ -165,6 +206,7 @@ describe('eip8141 serialization', () => { flags: 0x00, target: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', gasLimit: 100000n, + value: 0n, data: '0xcafebabe', }, ], @@ -273,6 +315,7 @@ describe('eip8141 assertTransaction', () => { flags: 0, target: sender, gasLimit: 1n, + value: 0n, data: '0x' as const, })) expect(() => assertTransactionEIP8141({ ...baseEIP8141, frames })).toThrow( @@ -286,6 +329,7 @@ describe('eip8141 assertTransaction', () => { flags: 0, target: sender, gasLimit: 1n, + value: 0n, data: '0x' as const, })) expect(() => @@ -298,7 +342,7 @@ describe('eip8141 assertTransaction', () => { assertTransactionEIP8141({ ...baseEIP8141, frames: [ - { mode: 3 as any, flags: 0, target: null, gasLimit: 1n, data: '0x' }, + { mode: 3 as any, flags: 0, target: null, gasLimit: 1n, value: 0n, data: '0x' }, ], }), ).toThrow('Invalid frame mode') @@ -308,7 +352,7 @@ describe('eip8141 assertTransaction', () => { expect(() => assertTransactionEIP8141({ ...baseEIP8141, - frames: [{ mode: 2, flags: 8, target: null, gasLimit: 1n, data: '0x' }], + frames: [{ mode: 2, flags: 8, target: null, gasLimit: 1n, value: 0n, data: '0x' }], }), ).toThrow('Bits 3-7 are reserved') }) @@ -318,7 +362,7 @@ describe('eip8141 assertTransaction', () => { assertTransactionEIP8141({ ...baseEIP8141, frames: [ - { mode: 2, flags: 0xff, target: null, gasLimit: 1n, data: '0x' }, + { mode: 2, flags: 0xff, target: null, gasLimit: 1n, value: 0n, data: '0x' }, ], }), ).toThrow('Bits 3-7 are reserved') @@ -329,7 +373,7 @@ describe('eip8141 assertTransaction', () => { assertTransactionEIP8141({ ...baseEIP8141, frames: [ - { mode: 1, flags: 0x00, target: null, gasLimit: 1n, data: '0x' }, + { mode: 1, flags: 0x00, target: null, gasLimit: 1n, value: 0n, data: '0x' }, ], }), ).toThrow('non-zero APPROVE scope') @@ -340,7 +384,7 @@ describe('eip8141 assertTransaction', () => { assertTransactionEIP8141({ ...baseEIP8141, frames: [ - { mode: 1, flags: 0x01, target: null, gasLimit: 1n, data: '0x' }, + { mode: 1, flags: 0x01, target: null, gasLimit: 1n, value: 0n, data: '0x' }, ], }), ).not.toThrow() @@ -351,7 +395,7 @@ describe('eip8141 assertTransaction', () => { assertTransactionEIP8141({ ...baseEIP8141, frames: [ - { mode: 1, flags: 0x02, target: null, gasLimit: 1n, data: '0x' }, + { mode: 1, flags: 0x02, target: null, gasLimit: 1n, value: 0n, data: '0x' }, ], }), ).not.toThrow() @@ -362,7 +406,7 @@ describe('eip8141 assertTransaction', () => { assertTransactionEIP8141({ ...baseEIP8141, frames: [ - { mode: 1, flags: 0x03, target: null, gasLimit: 1n, data: '0x' }, + { mode: 1, flags: 0x03, target: null, gasLimit: 1n, value: 0n, data: '0x' }, ], }), ).not.toThrow() @@ -373,8 +417,8 @@ describe('eip8141 assertTransaction', () => { assertTransactionEIP8141({ ...baseEIP8141, frames: [ - { mode: 0, flags: 0x04, target: sender, gasLimit: 1n, data: '0x' }, - { mode: 2, flags: 0x00, target: null, gasLimit: 1n, data: '0x' }, + { mode: 0, flags: 0x04, target: sender, gasLimit: 1n, value: 0n, data: '0x' }, + { mode: 2, flags: 0x00, target: null, gasLimit: 1n, value: 0n, data: '0x' }, ], }), ).toThrow('only valid with SENDER mode') @@ -385,8 +429,8 @@ describe('eip8141 assertTransaction', () => { assertTransactionEIP8141({ ...baseEIP8141, frames: [ - { mode: 1, flags: 0x03, target: null, gasLimit: 1n, data: '0x' }, - { mode: 2, flags: 0x04, target: null, gasLimit: 1n, data: '0x' }, + { mode: 1, flags: 0x03, target: null, gasLimit: 1n, value: 0n, data: '0x' }, + { mode: 2, flags: 0x04, target: null, gasLimit: 1n, value: 0n, data: '0x' }, ], }), ).toThrow('must not be the last frame') @@ -397,9 +441,9 @@ describe('eip8141 assertTransaction', () => { assertTransactionEIP8141({ ...baseEIP8141, frames: [ - { mode: 1, flags: 0x03, target: null, gasLimit: 1n, data: '0x' }, - { mode: 2, flags: 0x04, target: null, gasLimit: 1n, data: '0x' }, - { mode: 0, flags: 0x00, target: sender, gasLimit: 1n, data: '0x' }, + { mode: 1, flags: 0x03, target: null, gasLimit: 1n, value: 0n, data: '0x' }, + { mode: 2, flags: 0x04, target: null, gasLimit: 1n, value: 0n, data: '0x' }, + { mode: 0, flags: 0x00, target: sender, gasLimit: 1n, value: 0n, data: '0x' }, ], }), ).toThrow('following an atomic batch frame must be SENDER') @@ -410,9 +454,9 @@ describe('eip8141 assertTransaction', () => { assertTransactionEIP8141({ ...baseEIP8141, frames: [ - { mode: 1, flags: 0x03, target: null, gasLimit: 1n, data: '0x' }, - { mode: 2, flags: 0x04, target: null, gasLimit: 1n, data: '0x' }, - { mode: 2, flags: 0x00, target: null, gasLimit: 1n, data: '0x' }, + { mode: 1, flags: 0x03, target: null, gasLimit: 1n, value: 0n, data: '0x' }, + { mode: 2, flags: 0x04, target: null, gasLimit: 1n, value: 0n, data: '0x' }, + { mode: 2, flags: 0x00, target: null, gasLimit: 1n, value: 0n, data: '0x' }, ], }), ).not.toThrow() @@ -428,6 +472,7 @@ describe('eip8141 assertTransaction', () => { flags: 0x03, target: null, gasLimit: 2n ** 63n, + value: 0n, data: '0x', }, ], @@ -445,6 +490,7 @@ describe('eip8141 assertTransaction', () => { flags: 0x03, target: null, gasLimit: 2n ** 63n - 1n, + value: 0n, data: '0x', }, ], @@ -465,7 +511,7 @@ describe('eip8141 assertTransaction', () => { gasLimit: gasPerFrame, data: '0x', }, - { mode: 2, flags: 0x00, target: null, gasLimit: 1n, data: '0x' }, + { mode: 2, flags: 0x00, target: null, gasLimit: 1n, value: 0n, data: '0x' }, ], }), ).toThrow('Total frame gas must be <= 2^63 - 1') @@ -481,6 +527,7 @@ describe('eip8141 assertTransaction', () => { flags: 0x03, target: '0xbad' as any, gasLimit: 1n, + value: 0n, data: '0x', }, ], @@ -520,6 +567,7 @@ describe('eip8141 spec examples', () => { flags: 0x03, target: null, gasLimit: 50000n, + value: 0n, data: '0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefde', }, { @@ -527,6 +575,7 @@ describe('eip8141 spec examples', () => { flags: 0x00, target: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', gasLimit: 100000n, + value: 0n, data: '0xcafebabe', }, ], @@ -554,6 +603,7 @@ describe('eip8141 spec examples', () => { flags: 0x03, target: null, gasLimit: 50000n, + value: 0n, data: '0xabcdef', }, { @@ -561,6 +611,7 @@ describe('eip8141 spec examples', () => { flags: 0x04, target: erc20, gasLimit: 60000n, + value: 0n, data: '0x095ea7b3', }, { @@ -568,6 +619,7 @@ describe('eip8141 spec examples', () => { flags: 0x00, target: dex, gasLimit: 200000n, + value: 0n, data: '0x12345678', }, ], @@ -592,6 +644,7 @@ describe('eip8141 spec examples', () => { flags: 0x02, target: null, gasLimit: 30000n, + value: 0n, data: '0xdeadbeef', }, { @@ -599,6 +652,7 @@ describe('eip8141 spec examples', () => { flags: 0x01, target: sponsor, gasLimit: 40000n, + value: 0n, data: '0xfeedface', }, { @@ -606,6 +660,7 @@ describe('eip8141 spec examples', () => { flags: 0x00, target: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', gasLimit: 100000n, + value: 0n, data: '0xcafebabe', }, ], @@ -630,6 +685,7 @@ describe('eip8141 spec examples', () => { flags: 0x00, target: deployer, gasLimit: 200000n, + value: 0n, data: '0x600060005260206000f3', }, { @@ -637,6 +693,7 @@ describe('eip8141 spec examples', () => { flags: 0x03, target: null, gasLimit: 50000n, + value: 0n, data: '0xaabbccdd', }, { @@ -644,6 +701,7 @@ describe('eip8141 spec examples', () => { flags: 0x00, target: null, gasLimit: 100000n, + value: 0n, data: '0x11223344', }, ], @@ -665,12 +723,13 @@ describe('eip8141 spec examples', () => { nonce: 0, sender, frames: [ - { mode: 1, flags: 0x03, target: null, gasLimit: 50000n, data: '0xaa' }, + { mode: 1, flags: 0x03, target: null, gasLimit: 50000n, value: 0n, data: '0xaa' }, { mode: 2, flags: 0x04, target: target1, gasLimit: 60000n, + value: 0n, data: '0xbb', }, { @@ -678,6 +737,7 @@ describe('eip8141 spec examples', () => { flags: 0x00, target: target1, gasLimit: 60000n, + value: 0n, data: '0xcc', }, { @@ -685,6 +745,7 @@ describe('eip8141 spec examples', () => { flags: 0x04, target: target2, gasLimit: 80000n, + value: 0n, data: '0xdd', }, { @@ -692,6 +753,7 @@ describe('eip8141 spec examples', () => { flags: 0x04, target: target2, gasLimit: 80000n, + value: 0n, data: '0xee', }, { @@ -699,6 +761,7 @@ describe('eip8141 spec examples', () => { flags: 0x00, target: target2, gasLimit: 80000n, + value: 0n, data: '0xff', }, ], @@ -790,19 +853,19 @@ describe('eip8141 blob-field invariants', () => { }) describe('eip8141 parser strictness', () => { - test('rejects frame tuple with fewer than 5 elements', () => { + test('rejects frame tuple with fewer than 6 elements', () => { const tx: TransactionSerializableEIP8141 = { chainId: 1, sender, frames: [ - { mode: 1, flags: 0x03, target: null, gasLimit: 21000n, data: '0xaa' }, + { mode: 1, flags: 0x03, target: null, gasLimit: 21000n, value: 0n, data: '0xaa' }, ], } const serialized = serializeTransaction(tx) const hex = serialized.slice(4) const items = fromRlp(`0x${hex}`, 'hex') as any[] const frames = items[3] as any[] - frames[0] = frames[0].slice(0, 3) + frames[0] = frames[0].slice(0, 5) const reEncoded = `0x06${toRlp(items).slice(2)}` as TransactionSerializedEIP8141 expect(() => parseTransaction(reEncoded)).toThrow() @@ -813,7 +876,7 @@ describe('eip8141 parser strictness', () => { chainId: 1, sender, frames: [ - { mode: 1, flags: 0x03, target: null, gasLimit: 21000n, data: '0xaa' }, + { mode: 1, flags: 0x03, target: null, gasLimit: 21000n, value: 0n, data: '0xaa' }, ], nonce: 42, } @@ -833,7 +896,7 @@ describe('eip8141 edge cases', () => { chainId: 1, sender, frames: [ - { mode: 1, flags: 0x03, target: null, gasLimit: 21000n, data: '0x' }, + { mode: 1, flags: 0x03, target: null, gasLimit: 21000n, value: 0n, data: '0x' }, ], } const serialized = serializeTransaction(tx) @@ -846,11 +909,11 @@ describe('eip8141 edge cases', () => { const tx: TransactionSerializableEIP8141 = { chainId: 1, sender, - frames: [{ mode: 1, flags: 0x03, target, gasLimit: 21000n, data: '0x' }], + frames: [{ mode: 1, flags: 0x03, target, gasLimit: 21000n, value: 0n, data: '0x' }], } const serialized = serializeTransaction(tx) const parsed = parseTransaction(serialized) - expect(parsed.frames[0].target).toBe(target) + expect(parsed.frames[0].target).toBe(getAddress(target)) }) test('empty data preserved as 0x', () => { @@ -858,7 +921,7 @@ describe('eip8141 edge cases', () => { chainId: 1, sender, frames: [ - { mode: 1, flags: 0x03, target: null, gasLimit: 21000n, data: '0x' }, + { mode: 1, flags: 0x03, target: null, gasLimit: 21000n, value: 0n, data: '0x' }, ], } const serialized = serializeTransaction(tx) @@ -871,7 +934,7 @@ describe('eip8141 edge cases', () => { chainId: 1, sender, frames: [ - { mode: 1, flags: 0x03, target: null, gasLimit: 0n, data: '0xaa' }, + { mode: 1, flags: 0x03, target: null, gasLimit: 0n, value: 0n, data: '0xaa' }, ], } const serialized = serializeTransaction(tx) diff --git a/src/utils/transaction/parseTransaction.ts b/src/utils/transaction/parseTransaction.ts index d4a0181374..62fa56f1ab 100644 --- a/src/utils/transaction/parseTransaction.ts +++ b/src/utils/transaction/parseTransaction.ts @@ -70,6 +70,7 @@ import { type GetSerializedTransactionTypeErrorType, getSerializedTransactionType, } from './getSerializedTransactionType.js' +import { type GetAddressErrorType, getAddress } from '../address/getAddress.js' export type ParseTransactionReturnType< serialized extends TransactionSerializedGeneric = TransactionSerialized, @@ -137,6 +138,7 @@ type ParseTransactionEIP8141ErrorType = | HexToNumberErrorType | InvalidSerializedTransactionErrorType | IsHexErrorType + | GetAddressErrorType | ErrorType function parseTransactionEIP8141( @@ -174,13 +176,13 @@ function parseTransactionEIP8141( const frames: Frame[] = (framesArray as RecursiveArray[]).map( (frameArray) => { const tuple = frameArray as Hex[] - if (tuple.length !== 5) + if (tuple.length !== 6) throw new InvalidSerializedTransactionError({ attributes: { frame: tuple }, serializedTransaction, type: 'eip8141', }) - const [mode, flags, target, gasLimit, data] = tuple + const [mode, flags, target, gasLimit, value, data] = tuple const parsedMode = mode === '0x' ? 0 : hexToNumber(mode) if (parsedMode > 2) throw new InvalidSerializedTransactionError({ @@ -191,8 +193,10 @@ function parseTransactionEIP8141( return { mode: parsedMode as Frame['mode'], flags: flags === '0x' ? 0 : hexToNumber(flags), - target: isHex(target) && target !== '0x' ? target : null, + target: + isHex(target) && target !== '0x' ? getAddress(target) : null, gasLimit: gasLimit === '0x' ? 0n : hexToBigInt(gasLimit), + value: value === '0x' ? 0n : hexToBigInt(value), data: isHex(data) && data !== '0x' ? data : '0x', } }, diff --git a/src/utils/transaction/serializeTransaction.ts b/src/utils/transaction/serializeTransaction.ts index 334bd14124..980f590c21 100644 --- a/src/utils/transaction/serializeTransaction.ts +++ b/src/utils/transaction/serializeTransaction.ts @@ -80,6 +80,7 @@ import { type SerializeAccessListErrorType, serializeAccessList, } from './serializeAccessList.js' +import { type GetAddressErrorType, getAddress } from '../address/getAddress.js' export type SerializedTransactionReturnType< transaction extends TransactionSerializable = TransactionSerializable, @@ -159,6 +160,7 @@ export function serializeTransaction< type SerializeTransactionEIP8141ErrorType = | AssertTransactionEIP8141ErrorType | ConcatHexErrorType + | GetAddressErrorType | NumberToHexErrorType | ToRlpErrorType | ErrorType @@ -180,10 +182,11 @@ function serializeTransactionEIP8141( assertTransactionEIP8141(transaction) const serializedFrames = frames.map((frame) => [ - numberToHex(frame.mode), - numberToHex(frame.flags), - frame.target ?? '0x', - numberToHex(frame.gasLimit), + frame.mode ? numberToHex(frame.mode) : '0x', + frame.flags ? numberToHex(frame.flags) : '0x', + frame.target ? getAddress(frame.target) : '0x', + frame.gasLimit ? numberToHex(frame.gasLimit) : '0x', + frame.value ? numberToHex(frame.value) : '0x', frame.data ?? '0x', ]) From 1dbdd13925a11c34b59ea1097cfb1f5b5836678b Mon Sep 17 00:00:00 2001 From: chiranjeev13 Date: Sat, 18 Apr 2026 05:29:05 +0530 Subject: [PATCH 9/9] refactor: improve formatting and organization in eip8141 tests and transaction parsing - Reorganized test cases in eip8141.test.ts for better readability by formatting object literals. - Updated parseTransaction.ts and serializeTransaction.ts to consistently import getAddress, enhancing clarity and reducing redundancy. - Ensured that transaction frame structures are consistently formatted across tests for improved maintainability. --- src/utils/transaction/eip8141.test.ts | 249 ++++++++++++++++-- src/utils/transaction/parseTransaction.ts | 6 +- src/utils/transaction/serializeTransaction.ts | 3 +- 3 files changed, 224 insertions(+), 34 deletions(-) diff --git a/src/utils/transaction/eip8141.test.ts b/src/utils/transaction/eip8141.test.ts index 004700da4e..1b3aa36bfc 100644 --- a/src/utils/transaction/eip8141.test.ts +++ b/src/utils/transaction/eip8141.test.ts @@ -1,10 +1,10 @@ import { describe, expect, test } from 'vitest' import { maxUint256 } from '../../constants/number.js' -import { getAddress } from '../address/getAddress.js' import type { TransactionSerializableEIP8141, TransactionSerializedEIP8141, } from '../../types/transaction.js' +import { getAddress } from '../address/getAddress.js' import { fromRlp } from '../encoding/fromRlp.js' import { numberToHex } from '../encoding/toHex.js' import { toRlp } from '../encoding/toRlp.js' @@ -162,7 +162,14 @@ describe('eip8141 serialization', () => { chainId: 1, sender, frames: [ - { mode: 1, flags: 0x03, target: null, gasLimit: 50000n, value: 0n, data: '0xaa' }, + { + mode: 1, + flags: 0x03, + target: null, + gasLimit: 50000n, + value: 0n, + data: '0xaa', + }, { mode: 2, flags: 0x04, @@ -192,7 +199,14 @@ describe('eip8141 serialization', () => { chainId: 1, sender, frames: [ - { mode: 0, flags: 0x00, target: sender, gasLimit: 10000n, value: 0n, data: '0x' }, + { + mode: 0, + flags: 0x00, + target: sender, + gasLimit: 10000n, + value: 0n, + data: '0x', + }, { mode: 1, flags: 0x03, @@ -342,7 +356,14 @@ describe('eip8141 assertTransaction', () => { assertTransactionEIP8141({ ...baseEIP8141, frames: [ - { mode: 3 as any, flags: 0, target: null, gasLimit: 1n, value: 0n, data: '0x' }, + { + mode: 3 as any, + flags: 0, + target: null, + gasLimit: 1n, + value: 0n, + data: '0x', + }, ], }), ).toThrow('Invalid frame mode') @@ -352,7 +373,16 @@ describe('eip8141 assertTransaction', () => { expect(() => assertTransactionEIP8141({ ...baseEIP8141, - frames: [{ mode: 2, flags: 8, target: null, gasLimit: 1n, value: 0n, data: '0x' }], + frames: [ + { + mode: 2, + flags: 8, + target: null, + gasLimit: 1n, + value: 0n, + data: '0x', + }, + ], }), ).toThrow('Bits 3-7 are reserved') }) @@ -362,7 +392,14 @@ describe('eip8141 assertTransaction', () => { assertTransactionEIP8141({ ...baseEIP8141, frames: [ - { mode: 2, flags: 0xff, target: null, gasLimit: 1n, value: 0n, data: '0x' }, + { + mode: 2, + flags: 0xff, + target: null, + gasLimit: 1n, + value: 0n, + data: '0x', + }, ], }), ).toThrow('Bits 3-7 are reserved') @@ -373,7 +410,14 @@ describe('eip8141 assertTransaction', () => { assertTransactionEIP8141({ ...baseEIP8141, frames: [ - { mode: 1, flags: 0x00, target: null, gasLimit: 1n, value: 0n, data: '0x' }, + { + mode: 1, + flags: 0x00, + target: null, + gasLimit: 1n, + value: 0n, + data: '0x', + }, ], }), ).toThrow('non-zero APPROVE scope') @@ -384,7 +428,14 @@ describe('eip8141 assertTransaction', () => { assertTransactionEIP8141({ ...baseEIP8141, frames: [ - { mode: 1, flags: 0x01, target: null, gasLimit: 1n, value: 0n, data: '0x' }, + { + mode: 1, + flags: 0x01, + target: null, + gasLimit: 1n, + value: 0n, + data: '0x', + }, ], }), ).not.toThrow() @@ -395,7 +446,14 @@ describe('eip8141 assertTransaction', () => { assertTransactionEIP8141({ ...baseEIP8141, frames: [ - { mode: 1, flags: 0x02, target: null, gasLimit: 1n, value: 0n, data: '0x' }, + { + mode: 1, + flags: 0x02, + target: null, + gasLimit: 1n, + value: 0n, + data: '0x', + }, ], }), ).not.toThrow() @@ -406,7 +464,14 @@ describe('eip8141 assertTransaction', () => { assertTransactionEIP8141({ ...baseEIP8141, frames: [ - { mode: 1, flags: 0x03, target: null, gasLimit: 1n, value: 0n, data: '0x' }, + { + mode: 1, + flags: 0x03, + target: null, + gasLimit: 1n, + value: 0n, + data: '0x', + }, ], }), ).not.toThrow() @@ -417,8 +482,22 @@ describe('eip8141 assertTransaction', () => { assertTransactionEIP8141({ ...baseEIP8141, frames: [ - { mode: 0, flags: 0x04, target: sender, gasLimit: 1n, value: 0n, data: '0x' }, - { mode: 2, flags: 0x00, target: null, gasLimit: 1n, value: 0n, data: '0x' }, + { + mode: 0, + flags: 0x04, + target: sender, + gasLimit: 1n, + value: 0n, + data: '0x', + }, + { + mode: 2, + flags: 0x00, + target: null, + gasLimit: 1n, + value: 0n, + data: '0x', + }, ], }), ).toThrow('only valid with SENDER mode') @@ -429,8 +508,22 @@ describe('eip8141 assertTransaction', () => { assertTransactionEIP8141({ ...baseEIP8141, frames: [ - { mode: 1, flags: 0x03, target: null, gasLimit: 1n, value: 0n, data: '0x' }, - { mode: 2, flags: 0x04, target: null, gasLimit: 1n, value: 0n, data: '0x' }, + { + mode: 1, + flags: 0x03, + target: null, + gasLimit: 1n, + value: 0n, + data: '0x', + }, + { + mode: 2, + flags: 0x04, + target: null, + gasLimit: 1n, + value: 0n, + data: '0x', + }, ], }), ).toThrow('must not be the last frame') @@ -441,9 +534,30 @@ describe('eip8141 assertTransaction', () => { assertTransactionEIP8141({ ...baseEIP8141, frames: [ - { mode: 1, flags: 0x03, target: null, gasLimit: 1n, value: 0n, data: '0x' }, - { mode: 2, flags: 0x04, target: null, gasLimit: 1n, value: 0n, data: '0x' }, - { mode: 0, flags: 0x00, target: sender, gasLimit: 1n, value: 0n, data: '0x' }, + { + mode: 1, + flags: 0x03, + target: null, + gasLimit: 1n, + value: 0n, + data: '0x', + }, + { + mode: 2, + flags: 0x04, + target: null, + gasLimit: 1n, + value: 0n, + data: '0x', + }, + { + mode: 0, + flags: 0x00, + target: sender, + gasLimit: 1n, + value: 0n, + data: '0x', + }, ], }), ).toThrow('following an atomic batch frame must be SENDER') @@ -454,9 +568,30 @@ describe('eip8141 assertTransaction', () => { assertTransactionEIP8141({ ...baseEIP8141, frames: [ - { mode: 1, flags: 0x03, target: null, gasLimit: 1n, value: 0n, data: '0x' }, - { mode: 2, flags: 0x04, target: null, gasLimit: 1n, value: 0n, data: '0x' }, - { mode: 2, flags: 0x00, target: null, gasLimit: 1n, value: 0n, data: '0x' }, + { + mode: 1, + flags: 0x03, + target: null, + gasLimit: 1n, + value: 0n, + data: '0x', + }, + { + mode: 2, + flags: 0x04, + target: null, + gasLimit: 1n, + value: 0n, + data: '0x', + }, + { + mode: 2, + flags: 0x00, + target: null, + gasLimit: 1n, + value: 0n, + data: '0x', + }, ], }), ).not.toThrow() @@ -511,7 +646,14 @@ describe('eip8141 assertTransaction', () => { gasLimit: gasPerFrame, data: '0x', }, - { mode: 2, flags: 0x00, target: null, gasLimit: 1n, value: 0n, data: '0x' }, + { + mode: 2, + flags: 0x00, + target: null, + gasLimit: 1n, + value: 0n, + data: '0x', + }, ], }), ).toThrow('Total frame gas must be <= 2^63 - 1') @@ -723,7 +865,14 @@ describe('eip8141 spec examples', () => { nonce: 0, sender, frames: [ - { mode: 1, flags: 0x03, target: null, gasLimit: 50000n, value: 0n, data: '0xaa' }, + { + mode: 1, + flags: 0x03, + target: null, + gasLimit: 50000n, + value: 0n, + data: '0xaa', + }, { mode: 2, flags: 0x04, @@ -858,7 +1007,14 @@ describe('eip8141 parser strictness', () => { chainId: 1, sender, frames: [ - { mode: 1, flags: 0x03, target: null, gasLimit: 21000n, value: 0n, data: '0xaa' }, + { + mode: 1, + flags: 0x03, + target: null, + gasLimit: 21000n, + value: 0n, + data: '0xaa', + }, ], } const serialized = serializeTransaction(tx) @@ -876,7 +1032,14 @@ describe('eip8141 parser strictness', () => { chainId: 1, sender, frames: [ - { mode: 1, flags: 0x03, target: null, gasLimit: 21000n, value: 0n, data: '0xaa' }, + { + mode: 1, + flags: 0x03, + target: null, + gasLimit: 21000n, + value: 0n, + data: '0xaa', + }, ], nonce: 42, } @@ -896,7 +1059,14 @@ describe('eip8141 edge cases', () => { chainId: 1, sender, frames: [ - { mode: 1, flags: 0x03, target: null, gasLimit: 21000n, value: 0n, data: '0x' }, + { + mode: 1, + flags: 0x03, + target: null, + gasLimit: 21000n, + value: 0n, + data: '0x', + }, ], } const serialized = serializeTransaction(tx) @@ -909,7 +1079,16 @@ describe('eip8141 edge cases', () => { const tx: TransactionSerializableEIP8141 = { chainId: 1, sender, - frames: [{ mode: 1, flags: 0x03, target, gasLimit: 21000n, value: 0n, data: '0x' }], + frames: [ + { + mode: 1, + flags: 0x03, + target, + gasLimit: 21000n, + value: 0n, + data: '0x', + }, + ], } const serialized = serializeTransaction(tx) const parsed = parseTransaction(serialized) @@ -921,7 +1100,14 @@ describe('eip8141 edge cases', () => { chainId: 1, sender, frames: [ - { mode: 1, flags: 0x03, target: null, gasLimit: 21000n, value: 0n, data: '0x' }, + { + mode: 1, + flags: 0x03, + target: null, + gasLimit: 21000n, + value: 0n, + data: '0x', + }, ], } const serialized = serializeTransaction(tx) @@ -934,7 +1120,14 @@ describe('eip8141 edge cases', () => { chainId: 1, sender, frames: [ - { mode: 1, flags: 0x03, target: null, gasLimit: 0n, value: 0n, data: '0xaa' }, + { + mode: 1, + flags: 0x03, + target: null, + gasLimit: 0n, + value: 0n, + data: '0xaa', + }, ], } const serialized = serializeTransaction(tx) diff --git a/src/utils/transaction/parseTransaction.ts b/src/utils/transaction/parseTransaction.ts index 62fa56f1ab..93c754b654 100644 --- a/src/utils/transaction/parseTransaction.ts +++ b/src/utils/transaction/parseTransaction.ts @@ -36,6 +36,7 @@ import type { TransactionType, } from '../../types/transaction.js' import type { IsNarrowable, Mutable } from '../../types/utils.js' +import { type GetAddressErrorType, getAddress } from '../address/getAddress.js' import { type IsAddressErrorType, isAddress } from '../address/isAddress.js' import { toBlobSidecars } from '../blob/toBlobSidecars.js' import { type IsHexErrorType, isHex } from '../data/isHex.js' @@ -50,7 +51,6 @@ import { import { type FromRlpErrorType, fromRlp } from '../encoding/fromRlp.js' import type { RecursiveArray } from '../encoding/toRlp.js' import { isHash } from '../hash/isHash.js' - import { type AssertTransactionEIP1559ErrorType, type AssertTransactionEIP2930ErrorType, @@ -70,7 +70,6 @@ import { type GetSerializedTransactionTypeErrorType, getSerializedTransactionType, } from './getSerializedTransactionType.js' -import { type GetAddressErrorType, getAddress } from '../address/getAddress.js' export type ParseTransactionReturnType< serialized extends TransactionSerializedGeneric = TransactionSerialized, @@ -193,8 +192,7 @@ function parseTransactionEIP8141( return { mode: parsedMode as Frame['mode'], flags: flags === '0x' ? 0 : hexToNumber(flags), - target: - isHex(target) && target !== '0x' ? getAddress(target) : null, + target: isHex(target) && target !== '0x' ? getAddress(target) : null, gasLimit: gasLimit === '0x' ? 0n : hexToBigInt(gasLimit), value: value === '0x' ? 0n : hexToBigInt(value), data: isHex(data) && data !== '0x' ? data : '0x', diff --git a/src/utils/transaction/serializeTransaction.ts b/src/utils/transaction/serializeTransaction.ts index 980f590c21..83e28fdb60 100644 --- a/src/utils/transaction/serializeTransaction.ts +++ b/src/utils/transaction/serializeTransaction.ts @@ -28,6 +28,7 @@ import type { TransactionType, } from '../../types/transaction.js' import type { MaybePromise, OneOf } from '../../types/utils.js' +import { type GetAddressErrorType, getAddress } from '../address/getAddress.js' import { type SerializeAuthorizationListErrorType, serializeAuthorizationList, @@ -56,7 +57,6 @@ import { numberToHex, } from '../encoding/toHex.js' import { type ToRlpErrorType, toRlp } from '../encoding/toRlp.js' - import { type AssertTransactionEIP1559ErrorType, type AssertTransactionEIP2930ErrorType, @@ -80,7 +80,6 @@ import { type SerializeAccessListErrorType, serializeAccessList, } from './serializeAccessList.js' -import { type GetAddressErrorType, getAddress } from '../address/getAddress.js' export type SerializedTransactionReturnType< transaction extends TransactionSerializable = TransactionSerializable,