From 7b325355ccb46e3d14109e3cf8f5b35b102fe1a2 Mon Sep 17 00:00:00 2001 From: jxom <7336481+jxom@users.noreply.github.com> Date: Sun, 27 Sep 2026 16:34:12 +1000 Subject: [PATCH 1/4] fix: align frame rpc requests and results --- .changeset/frame-rpc-alignment.md | 5 + src/core/FrameSignature.ts | 6 +- src/core/TransactionRequest.ts | 10 +- src/core/_test/FrameRpc.test.ts | 115 +++++++++++++++++++++++ src/core/_test/FrameSignature.test.ts | 2 +- src/core/_test/RpcSchema.test-d.ts | 45 +++++++++ src/core/_test/TxEnvelopeEip8141.test.ts | 2 + src/core/internal/frameRequest.ts | 34 +++++++ src/core/internal/rpcSchemas/eth.ts | 39 ++++++++ 9 files changed, 252 insertions(+), 6 deletions(-) create mode 100644 .changeset/frame-rpc-alignment.md create mode 100644 src/core/_test/FrameRpc.test.ts create mode 100644 src/core/internal/frameRequest.ts diff --git a/.changeset/frame-rpc-alignment.md b/.changeset/frame-rpc-alignment.md new file mode 100644 index 000000000..ad6321cf0 --- /dev/null +++ b/.changeset/frame-rpc-alignment.md @@ -0,0 +1,5 @@ +--- +"ox": patch +--- + +Aligned frame request gas estimation, signature signers, fill responses, and simulation results with the execution API definitions. diff --git a/src/core/FrameSignature.ts b/src/core/FrameSignature.ts index 51f3c210a..a613481b7 100644 --- a/src/core/FrameSignature.ts +++ b/src/core/FrameSignature.ts @@ -73,7 +73,7 @@ export type Rpc = { /** Encoded signature bytes. */ signature?: Hex.Hex | undefined /** Signer address; absent for the transaction sender. */ - signer?: Address.Address | null | undefined + signer?: Address.Address | '0x' | undefined } /** RLP-ready signature entry. The payload occupies the specification's `msg` field. */ @@ -360,6 +360,10 @@ export declare namespace from { * @returns The converted value. */ export function fromRpc(entry: Rpc): FrameSignature { + if (entry.signer === null) + throw new InvalidError( + 'signer must be omitted, empty bytes, or an address.', + ) return fromTuple([ entry.scheme === '0x0' ? '0x' diff --git a/src/core/TransactionRequest.ts b/src/core/TransactionRequest.ts index b6ca95c9b..278f6f1ce 100644 --- a/src/core/TransactionRequest.ts +++ b/src/core/TransactionRequest.ts @@ -2,9 +2,10 @@ import type * as AccessList from './AccessList.js' import type * as Address from './Address.js' import * as Authorization from './Authorization.js' import type * as Errors from './Errors.js' -import * as Frame from './Frame.js' +import type * as Frame from './Frame.js' import * as FrameSignature from './FrameSignature.js' import * as Hex from './Hex.js' +import * as FrameRequest from './internal/frameRequest.js' import type { Compute } from './internal/types.js' import * as Transaction from './Transaction.js' @@ -63,7 +64,7 @@ export type Rpc = TransactionRequest< Hex.Hex, Hex.Hex, string, - Frame.Rpc, + FrameRequest.Rpc, FrameSignature.Rpc > @@ -92,7 +93,7 @@ export function fromRpc(request: Rpc): TransactionRequest { ) if (typeof request.chainId !== 'undefined') request_.chainId = Hex.toNumber(request.chainId) - if (request.frames) request_.frames = request.frames.map(Frame.fromRpc) + if (request.frames) request_.frames = request.frames.map(FrameRequest.fromRpc) if (request.signatures) request_.signatures = request.signatures.map(FrameSignature.fromRpc) if (typeof request.gas !== 'undefined') @@ -189,7 +190,8 @@ export function toRpc(request: TransactionRequest): Rpc { request_rpc.input = request.input } if (typeof request.from !== 'undefined') request_rpc.from = request.from - if (request.frames) request_rpc.frames = request.frames.map(Frame.toRpc) + if (request.frames) + request_rpc.frames = request.frames.map(FrameRequest.toRpc) if (request.signatures) request_rpc.signatures = request.signatures.map(FrameSignature.toRpc) if (typeof request.gas !== 'undefined') diff --git a/src/core/_test/FrameRpc.test.ts b/src/core/_test/FrameRpc.test.ts new file mode 100644 index 000000000..8670a4642 --- /dev/null +++ b/src/core/_test/FrameRpc.test.ts @@ -0,0 +1,115 @@ +import { + Frame, + FrameSignature, + TransactionRequest, + TxEnvelopeEip8141, +} from 'ox' +import { describe, expect, test } from 'vitest' + +describe('TransactionRequest.toRpc', () => { + test('preserves omitted frame gas and explicit zero limits', () => { + expect( + TransactionRequest.toRpc({ + type: 'eip8141', + frames: [ + { mode: 'sender' }, + { mode: 'sender', executionGas: 0n }, + { mode: 'sender', stateGas: 0n }, + { mode: 'sender', executionGas: 50_000n, stateGas: 1_000n }, + ], + }), + ).toEqual({ + type: '0x6', + frames: [ + { mode: '0x2', flags: '0x0', data: '0x', value: '0x0' }, + { + mode: '0x2', + flags: '0x0', + data: '0x', + value: '0x0', + executionGas: '0x0', + }, + { + mode: '0x2', + flags: '0x0', + data: '0x', + value: '0x0', + stateGas: '0x0', + }, + { + mode: '0x2', + flags: '0x0', + data: '0x', + value: '0x0', + executionGas: '0xc350', + stateGas: '0x3e8', + }, + ], + }) + }) +}) + +describe('TransactionRequest.fromRpc', () => { + test('decodes minimal requests without assigning gas limits', () => { + expect( + TransactionRequest.fromRpc({ + type: '0x6', + frames: [ + { mode: '0x2' }, + { mode: '0x2', executionGas: '0x0', stateGas: '0x0', target: null }, + ], + }), + ).toEqual({ + type: 'eip8141', + frames: [ + { mode: 2, flags: 0, data: '0x', value: 0n }, + { + mode: 2, + flags: 0, + data: '0x', + value: 0n, + executionGas: 0n, + stateGas: 0n, + }, + ], + }) + }) +}) + +describe('TxEnvelopeEip8141.toRpc', () => { + test('retains complete frame gas fields', () => { + const envelope = TxEnvelopeEip8141.toRpc( + TxEnvelopeEip8141.from({ + chainId: 1, + frames: [Frame.from({ mode: 'sender' })], + sender: '0x1111111111111111111111111111111111111111', + }), + ) + expect(envelope.frames).toEqual([ + { + mode: '0x2', + flags: '0x0', + data: '0x', + value: '0x0', + executionGas: '0x0', + stateGas: '0x0', + }, + ]) + }) +}) + +describe('FrameSignature.fromRpc', () => { + test('accepts empty signers and rejects null signers', () => { + expect(FrameSignature.fromRpc({ scheme: '0x1', signer: '0x' })).toEqual({ + scheme: 'secp256k1', + payload: '0x', + }) + expect(() => + FrameSignature.fromRpc({ + scheme: '0x1', + // @ts-expect-error Null is not an RPC signer. + signer: null, + }), + ).toThrow('signer must be omitted, empty bytes, or an address.') + }) +}) diff --git a/src/core/_test/FrameSignature.test.ts b/src/core/_test/FrameSignature.test.ts index cdeab395c..845549f8c 100644 --- a/src/core/_test/FrameSignature.test.ts +++ b/src/core/_test/FrameSignature.test.ts @@ -460,7 +460,7 @@ describe('fromRpc', () => { msg: '0x', scheme: '0x0', signature: '0xaabb', - signer: null, + signer: '0x', }), ).toEqual(FrameSignature.from('0xaabb')) }) diff --git a/src/core/_test/RpcSchema.test-d.ts b/src/core/_test/RpcSchema.test-d.ts index 05bb3244d..29b1694ac 100644 --- a/src/core/_test/RpcSchema.test-d.ts +++ b/src/core/_test/RpcSchema.test-d.ts @@ -116,3 +116,48 @@ test('roundtrip: FromViem -> ToViem preserves schema', () => { expectTypeOf().toEqualTypeOf<'eth_blockNumber'>() expectTypeOf().toEqualTypeOf<`0x${string}`>() }) + +test('frame fill responses do not require raw bytes or lookup metadata', () => { + type Filled = Extract< + RpcSchema.Eth, + { Request: { method: 'eth_fillTransaction' } } + >['ReturnType'] + const filled = { + tx: { + type: '0x6', + chainId: '0x1', + nonce: '0x0', + from: '0x1111111111111111111111111111111111111111', + frames: [ + { + mode: '0x2', + flags: '0x0', + executionGas: '0x10000', + stateGas: '0x0', + value: '0x0', + data: '0x', + }, + ], + signatures: [{ scheme: '0x1' }], + maxFeePerGas: '0x2', + maxPriorityFeePerGas: '0x1', + maxFeePerBlobGas: '0x0', + blobVersionedHashes: [], + }, + } as const satisfies Filled + expectTypeOf(filled).toExtend() +}) + +test('frame simulation results expose payer and frame results', () => { + type Result = Extract< + RpcSchema.Eth, + { Request: { method: 'eth_simulateV1' } } + >['ReturnType'][number] + type Call = NonNullable[number] + expectTypeOf().toEqualTypeOf<`0x${string}` | undefined>() + type Frame = NonNullable[number] + expectTypeOf().toEqualTypeOf<'0x0' | '0x1' | '0x2'>() + expectTypeOf().toEqualTypeOf<`0x${string}`>() + expectTypeOf().toEqualTypeOf<`0x${string}`>() + expectTypeOf().toEqualTypeOf<`0x${string}`>() +}) diff --git a/src/core/_test/TxEnvelopeEip8141.test.ts b/src/core/_test/TxEnvelopeEip8141.test.ts index 06b07390f..29c518b9f 100644 --- a/src/core/_test/TxEnvelopeEip8141.test.ts +++ b/src/core/_test/TxEnvelopeEip8141.test.ts @@ -631,10 +631,12 @@ describe('serialize', () => { executionGas: 50_000n, flags: 'approveExecutionAndPayment', mode: 'verify', + stateGas: 0n, }), Frame.from({ executionGas: 50_000n, mode: 'sender', + stateGas: 0n, to: target, value: 1n, }), diff --git a/src/core/internal/frameRequest.ts b/src/core/internal/frameRequest.ts new file mode 100644 index 000000000..7d52159e0 --- /dev/null +++ b/src/core/internal/frameRequest.ts @@ -0,0 +1,34 @@ +import * as Frame from '../Frame.js' +import type { Compute, PartialBy } from './types.js' + +/** @internal */ +export type Rpc = Compute< + PartialBy> +> + +/** @internal */ +export function fromRpc(frame: Rpc): Frame.Frame { + const { executionGas, stateGas, ...rest } = Frame.fromRpc({ + ...frame, + data: frame.data ?? '0x', + executionGas: frame.executionGas ?? '0x0', + flags: frame.flags ?? '0x0', + stateGas: frame.stateGas ?? '0x0', + value: frame.value ?? '0x0', + }) + return { + ...rest, + ...(frame.executionGas === undefined ? {} : { executionGas }), + ...(frame.stateGas === undefined ? {} : { stateGas }), + } +} + +/** @internal */ +export function toRpc(frame: Frame.toRpc.Input): Rpc { + const { executionGas, stateGas, ...rest } = Frame.toRpc(frame) + return { + ...rest, + ...(frame.executionGas === undefined ? {} : { executionGas }), + ...(frame.stateGas === undefined ? {} : { stateGas }), + } +} diff --git a/src/core/internal/rpcSchemas/eth.ts b/src/core/internal/rpcSchemas/eth.ts index 6df691ba0..66b6eff1d 100644 --- a/src/core/internal/rpcSchemas/eth.ts +++ b/src/core/internal/rpcSchemas/eth.ts @@ -4,6 +4,7 @@ import type * as Block from '../../Block.js' import type * as BlockOverrides from '../../BlockOverrides.js' import type * as Fee from '../../Fee.js' import type * as Filter from '../../Filter.js' +import type * as FrameReceipt from '../../FrameReceipt.js' import type * as Hex from '../../Hex.js' import type * as Log from '../../Log.js' import type * as RpcSchema from '../../RpcSchema.js' @@ -11,6 +12,12 @@ import type * as StateOverrides from '../../StateOverrides.js' import type * as Transaction from '../../Transaction.js' import type * as TransactionReceipt from '../../TransactionReceipt.js' import type * as TransactionRequest from '../../TransactionRequest.js' +import type * as TxEnvelopeEip1559 from '../../TxEnvelopeEip1559.js' +import type * as TxEnvelopeEip2930 from '../../TxEnvelopeEip2930.js' +import type * as TxEnvelopeEip4844 from '../../TxEnvelopeEip4844.js' +import type * as TxEnvelopeEip7702 from '../../TxEnvelopeEip7702.js' +import type * as TxEnvelopeEip8141 from '../../TxEnvelopeEip8141.js' +import type * as TxEnvelopeLegacy from '../../TxEnvelopeLegacy.js' /** * Union of all JSON-RPC Methods for the `eth_` namespace. @@ -217,6 +224,31 @@ export type Eth = RpcSchema.From< } ReturnType: Fee.FeeHistoryRpc } + /** + * Fills transaction defaults and returns the unsigned transaction. + * + * @example + * ``` + * request({ method: 'eth_fillTransaction', params: [{ from: '0x...', to: '0x...', value: '0x...' }] }) + * // { tx: { ... } } + * ``` + */ + | { + Request: { + method: 'eth_fillTransaction' + params: [transaction: TransactionRequest.Rpc] + } + ReturnType: { + tx: ( + | TxEnvelopeLegacy.Rpc + | TxEnvelopeEip2930.Rpc + | TxEnvelopeEip1559.Rpc + | TxEnvelopeEip4844.Rpc + | TxEnvelopeEip7702.Rpc + | TxEnvelopeEip8141.Rpc + ) & { from: Address.Address } + } + } /** * Returns the current price of gas expressed in wei * @@ -772,6 +804,13 @@ export type Eth = RpcSchema.From< message: string } | undefined + frameResults?: + | readonly (FrameReceipt.Rpc & { + error?: { code: number; message: string } | undefined + returnData: Hex.Hex + })[] + | undefined + payer?: Address.Address | undefined logs?: readonly Log.Rpc[] | undefined gasUsed: Hex.Hex returnData: Hex.Hex From 9ae5d333d19caa1f513343d3de3d47468ef38251 Mon Sep 17 00:00:00 2001 From: jxom <7336481+jxom@users.noreply.github.com> Date: Sun, 27 Sep 2026 16:43:34 +1000 Subject: [PATCH 2/4] fix: remove null signer guard --- src/core/FrameSignature.ts | 4 ---- src/core/_test/FrameRpc.test.ts | 9 +-------- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/src/core/FrameSignature.ts b/src/core/FrameSignature.ts index a613481b7..83107113a 100644 --- a/src/core/FrameSignature.ts +++ b/src/core/FrameSignature.ts @@ -360,10 +360,6 @@ export declare namespace from { * @returns The converted value. */ export function fromRpc(entry: Rpc): FrameSignature { - if (entry.signer === null) - throw new InvalidError( - 'signer must be omitted, empty bytes, or an address.', - ) return fromTuple([ entry.scheme === '0x0' ? '0x' diff --git a/src/core/_test/FrameRpc.test.ts b/src/core/_test/FrameRpc.test.ts index 8670a4642..920c84ccd 100644 --- a/src/core/_test/FrameRpc.test.ts +++ b/src/core/_test/FrameRpc.test.ts @@ -99,17 +99,10 @@ describe('TxEnvelopeEip8141.toRpc', () => { }) describe('FrameSignature.fromRpc', () => { - test('accepts empty signers and rejects null signers', () => { + test('accepts empty signers', () => { expect(FrameSignature.fromRpc({ scheme: '0x1', signer: '0x' })).toEqual({ scheme: 'secp256k1', payload: '0x', }) - expect(() => - FrameSignature.fromRpc({ - scheme: '0x1', - // @ts-expect-error Null is not an RPC signer. - signer: null, - }), - ).toThrow('signer must be omitted, empty bytes, or an address.') }) }) From b4fb549939749e2bef34022099bd4a2b3204e7f1 Mon Sep 17 00:00:00 2001 From: jxom <7336481+jxom@users.noreply.github.com> Date: Sun, 27 Sep 2026 16:44:25 +1000 Subject: [PATCH 3/4] feat: expose frame request module --- .changeset/frame-rpc-alignment.md | 8 ++- src/core/FrameRequest.ts | 72 +++++++++++++++++++++++++++ src/core/TransactionRequest.ts | 9 ++-- src/core/_test/FrameRequest.test-d.ts | 16 ++++++ src/core/_test/FrameRequest.test.ts | 43 ++++++++++++++++ src/core/internal/frameRequest.ts | 34 ------------- src/index.ts | 15 ++++++ 7 files changed, 157 insertions(+), 40 deletions(-) create mode 100644 src/core/FrameRequest.ts create mode 100644 src/core/_test/FrameRequest.test-d.ts create mode 100644 src/core/_test/FrameRequest.test.ts delete mode 100644 src/core/internal/frameRequest.ts diff --git a/.changeset/frame-rpc-alignment.md b/.changeset/frame-rpc-alignment.md index ad6321cf0..6bcca8cba 100644 --- a/.changeset/frame-rpc-alignment.md +++ b/.changeset/frame-rpc-alignment.md @@ -2,4 +2,10 @@ "ox": patch --- -Aligned frame request gas estimation, signature signers, fill responses, and simulation results with the execution API definitions. +Exposed `FrameRequest` and aligned frame request gas estimation, signature signers, fill responses, and simulation result types with the execution API definitions. + +```ts +import { FrameRequest } from 'ox' + +FrameRequest.toRpc({ mode: 'sender', stateGas: 0n }) +``` diff --git a/src/core/FrameRequest.ts b/src/core/FrameRequest.ts new file mode 100644 index 000000000..26ce09971 --- /dev/null +++ b/src/core/FrameRequest.ts @@ -0,0 +1,72 @@ +import * as Frame from './Frame.js' +import type { Compute, PartialBy } from './internal/types.js' + +/** A frame request whose omitted gas limits are estimated by the node. */ +export type FrameRequest = Frame.Frame + +/** JSON-RPC frame request. Only the execution mode is required. */ +export type Rpc = Compute< + PartialBy> +> + +/** + * Converts an RPC frame request, preserving omitted gas limits for node estimation. + * + * @example + * ```ts twoslash + * import { FrameRequest } from 'ox' + * + * const frame = FrameRequest.fromRpc({ mode: '0x2', stateGas: '0x0' }) + * ``` + * + * @param frame - The RPC frame request. + * @returns The decoded request with omitted gas limits preserved. + */ +export function fromRpc(frame: Rpc): FrameRequest { + const { executionGas, stateGas, ...rest } = Frame.fromRpc({ + ...frame, + data: frame.data ?? '0x', + executionGas: frame.executionGas ?? '0x0', + flags: frame.flags ?? '0x0', + stateGas: frame.stateGas ?? '0x0', + value: frame.value ?? '0x0', + }) + return { + ...rest, + ...(frame.executionGas === undefined ? {} : { executionGas }), + ...(frame.stateGas === undefined ? {} : { stateGas }), + } +} + +export declare namespace fromRpc { + type ErrorType = Frame.fromRpc.ErrorType +} + +/** + * Converts a frame request to RPC fields without filling omitted gas limits. + * + * Explicit zero limits remain zero. Omitted mode, flags, value, and data use frame defaults. + * + * @example + * ```ts twoslash + * import { FrameRequest } from 'ox' + * + * const frame = FrameRequest.toRpc({ mode: 'sender', stateGas: 0n }) + * ``` + * + * @param frame - The frame request. Gas and value accept hex, bigint, or number values. + * @returns The RPC request with omitted gas limits preserved. + */ +export function toRpc(frame: toRpc.Input): Rpc { + const { executionGas, stateGas, ...rest } = Frame.toRpc(frame) + return { + ...rest, + ...(frame.executionGas === undefined ? {} : { executionGas }), + ...(frame.stateGas === undefined ? {} : { stateGas }), + } +} + +export declare namespace toRpc { + type Input = Frame.toRpc.Input + type ErrorType = Frame.toRpc.ErrorType +} diff --git a/src/core/TransactionRequest.ts b/src/core/TransactionRequest.ts index 278f6f1ce..0f699c5a7 100644 --- a/src/core/TransactionRequest.ts +++ b/src/core/TransactionRequest.ts @@ -2,10 +2,9 @@ import type * as AccessList from './AccessList.js' import type * as Address from './Address.js' import * as Authorization from './Authorization.js' import type * as Errors from './Errors.js' -import type * as Frame from './Frame.js' +import * as FrameRequest from './FrameRequest.js' import * as FrameSignature from './FrameSignature.js' import * as Hex from './Hex.js' -import * as FrameRequest from './internal/frameRequest.js' import type { Compute } from './internal/types.js' import * as Transaction from './Transaction.js' @@ -14,7 +13,7 @@ export type TransactionRequest< bigintType = bigint, numberType = number, type extends string = string, - frame = Frame.Frame, + frame = FrameRequest.FrameRequest, signature = FrameSignature.FrameSignature, > = Compute<{ /** EIP-2930 Access List. */ @@ -121,7 +120,7 @@ export function fromRpc(request: Rpc): TransactionRequest { export declare namespace fromRpc { export type ErrorType = - | Frame.fromRpc.ErrorType + | FrameRequest.fromRpc.ErrorType | FrameSignature.fromRpc.ErrorType | Authorization.fromRpcList.ErrorType | Hex.toNumber.ErrorType @@ -226,7 +225,7 @@ export function toRpc(request: TransactionRequest): Rpc { export declare namespace toRpc { export type ErrorType = - | Frame.toRpc.ErrorType + | FrameRequest.toRpc.ErrorType | FrameSignature.toRpc.ErrorType | Authorization.toRpcList.ErrorType | Hex.fromNumber.ErrorType diff --git a/src/core/_test/FrameRequest.test-d.ts b/src/core/_test/FrameRequest.test-d.ts new file mode 100644 index 000000000..ce07b2162 --- /dev/null +++ b/src/core/_test/FrameRequest.test-d.ts @@ -0,0 +1,16 @@ +import { FrameRequest } from 'ox' +import { expectTypeOf, test } from 'vitest' + +test('public frame request types', () => { + const rpc = { mode: '0x2' } as const satisfies FrameRequest.Rpc + const request = FrameRequest.fromRpc(rpc) + expectTypeOf(request).toEqualTypeOf() + expectTypeOf(FrameRequest.toRpc(request)).toEqualTypeOf() + const input = { + executionGas: '0x1', + stateGas: 0, + } as const satisfies FrameRequest.toRpc.Input + expectTypeOf(FrameRequest.toRpc(input)).toEqualTypeOf() + expectTypeOf().not.toBeNever() + expectTypeOf().not.toBeNever() +}) diff --git a/src/core/_test/FrameRequest.test.ts b/src/core/_test/FrameRequest.test.ts new file mode 100644 index 000000000..e06035da2 --- /dev/null +++ b/src/core/_test/FrameRequest.test.ts @@ -0,0 +1,43 @@ +import { FrameRequest } from 'ox' +import { describe, expect, test } from 'vitest' + +test('exports', () => { + expect(Object.keys(FrameRequest).sort()).toMatchInlineSnapshot(` + [ + "fromRpc", + "toRpc", + ] + `) +}) + +describe('fromRpc', () => { + test('preserves omitted execution gas and explicit zero state gas', () => { + expect( + FrameRequest.fromRpc({ mode: '0x2', stateGas: '0x0' }), + ).toMatchInlineSnapshot(` + { + "data": "0x", + "flags": 0, + "mode": 2, + "stateGas": 0n, + "value": 0n, + } + `) + }) +}) + +describe('toRpc', () => { + test('preserves omitted gas and encodes numberish values', () => { + expect( + FrameRequest.toRpc({ mode: 'sender', stateGas: 0, value: '0x1' }), + ).toMatchInlineSnapshot(` + { + "data": "0x", + "flags": "0x0", + "mode": "0x2", + "stateGas": "0x0", + "value": "0x1", + } + `) + }) +}) diff --git a/src/core/internal/frameRequest.ts b/src/core/internal/frameRequest.ts deleted file mode 100644 index 7d52159e0..000000000 --- a/src/core/internal/frameRequest.ts +++ /dev/null @@ -1,34 +0,0 @@ -import * as Frame from '../Frame.js' -import type { Compute, PartialBy } from './types.js' - -/** @internal */ -export type Rpc = Compute< - PartialBy> -> - -/** @internal */ -export function fromRpc(frame: Rpc): Frame.Frame { - const { executionGas, stateGas, ...rest } = Frame.fromRpc({ - ...frame, - data: frame.data ?? '0x', - executionGas: frame.executionGas ?? '0x0', - flags: frame.flags ?? '0x0', - stateGas: frame.stateGas ?? '0x0', - value: frame.value ?? '0x0', - }) - return { - ...rest, - ...(frame.executionGas === undefined ? {} : { executionGas }), - ...(frame.stateGas === undefined ? {} : { stateGas }), - } -} - -/** @internal */ -export function toRpc(frame: Frame.toRpc.Input): Rpc { - const { executionGas, stateGas, ...rest } = Frame.toRpc(frame) - return { - ...rest, - ...(frame.executionGas === undefined ? {} : { executionGas }), - ...(frame.stateGas === undefined ? {} : { stateGas }), - } -} diff --git a/src/index.ts b/src/index.ts index dc3ea38f7..9de9f561a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1579,6 +1579,21 @@ export * as Frame from './core/Frame.js' * @category Frames (EIP-8141) */ export * as FrameReceipt from './core/FrameReceipt.js' + +/** + * Convert EIP-8141 frame requests while preserving omitted gas limits for node estimation. + * + * @example + * ```ts twoslash + * import { FrameRequest } from 'ox' + * + * const frame = FrameRequest.toRpc({ mode: 'sender', stateGas: 0n }) + * ``` + * + * @category Frames (EIP-8141) + */ +export * as FrameRequest from './core/FrameRequest.js' + /** * Utilities for constructing, validating, and encoding [EIP-8141](https://eips.ethereum.org/EIPS/eip-8141) signature entries. * From 657a032a4e0a5a91f9cd17f5e7fba2e8d2b145e1 Mon Sep 17 00:00:00 2001 From: jxom <7336481+jxom@users.noreply.github.com> Date: Sun, 27 Sep 2026 16:46:11 +1000 Subject: [PATCH 4/4] Delete .changeset/frame-rpc-alignment.md --- .changeset/frame-rpc-alignment.md | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 .changeset/frame-rpc-alignment.md diff --git a/.changeset/frame-rpc-alignment.md b/.changeset/frame-rpc-alignment.md deleted file mode 100644 index 6bcca8cba..000000000 --- a/.changeset/frame-rpc-alignment.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -"ox": patch ---- - -Exposed `FrameRequest` and aligned frame request gas estimation, signature signers, fill responses, and simulation result types with the execution API definitions. - -```ts -import { FrameRequest } from 'ox' - -FrameRequest.toRpc({ mode: 'sender', stateGas: 0n }) -```