Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .changeset/frame-rpc-fields.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
"ox": patch
---

Aligned frame RPC quantities, signature placeholders, and receipt gas totals with the execution API definitions.
Aligned to frame tranasction spec.
10 changes: 10 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,11 @@
"types": "./dist/core/FrameReceipt.d.ts",
"default": "./dist/core/FrameReceipt.js"
},
"./FrameRequest": {
"src": "./src/core/FrameRequest.ts",
"types": "./dist/core/FrameRequest.d.ts",
"default": "./dist/core/FrameRequest.js"
},
"./FrameSignature": {
"src": "./src/core/FrameSignature.ts",
"types": "./dist/core/FrameSignature.d.ts",
Expand Down Expand Up @@ -1096,6 +1101,11 @@
"types": "./dist/zod/FrameReceipt.d.ts",
"default": "./dist/zod/FrameReceipt.js"
},
"./zod/FrameRequest": {
"src": "./src/zod/FrameRequest.ts",
"types": "./dist/zod/FrameRequest.d.ts",
"default": "./dist/zod/FrameRequest.js"
},
"./zod/FrameSignature": {
"src": "./src/zod/FrameSignature.ts",
"types": "./dist/zod/FrameSignature.d.ts",
Expand Down
72 changes: 72 additions & 0 deletions src/core/FrameRequest.ts
Original file line number Diff line number Diff line change
@@ -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<bigintType = bigint> = Frame.Frame<bigintType>

/** JSON-RPC frame request. Only the execution mode is required. */
export type Rpc = Compute<
PartialBy<Frame.Rpc, Exclude<keyof Frame.Rpc, 'mode'>>
>

/**
* 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
}
2 changes: 1 addition & 1 deletion src/core/FrameSignature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down
16 changes: 9 additions & 7 deletions src/core/TransactionRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type * as Address from './Address.js'
import * as Authorization from './Authorization.js'
import * as Blobs from './Blobs.js'
import * as Errors from './Errors.js'
import * 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 Quantity from './internal/quantity.js'
Expand All @@ -19,7 +19,9 @@ export type TransactionRequest<
bigintType = bigint,
numberType = number,
type extends string = string,
frame = [bigintType] extends [Hex.Hex] ? Frame.Rpc : Frame.Frame<bigintType>,
frame = [bigintType] extends [Hex.Hex]
? FrameRequest.Rpc
: FrameRequest.FrameRequest<bigintType>,
signature = [bigintType] extends [Hex.Hex]
? FrameSignature.Rpc
: FrameSignature.FrameSignature,
Expand Down Expand Up @@ -79,7 +81,7 @@ export type Rpc = TransactionRequest<
Hex.Hex,
Hex.Hex,
string,
Frame.Rpc,
FrameRequest.Rpc,
FrameSignature.Rpc
>

Expand Down Expand Up @@ -109,7 +111,7 @@ export function fromRpc(request: Rpc): TransactionRequest {
if (typeof request.chainId !== 'undefined')
request_.chainId = Hex.toNumber(request.chainId)
if (request.frames !== undefined)
request_.frames = request.frames.map(Frame.fromRpc)
request_.frames = request.frames.map(FrameRequest.fromRpc)
if (request.signatures !== undefined)
request_.signatures = request.signatures.map(FrameSignature.fromRpc)
if (typeof request.gas !== 'undefined')
Expand Down Expand Up @@ -141,7 +143,7 @@ export function fromRpc(request: Rpc): TransactionRequest {
export declare namespace fromRpc {
export type ErrorType =
| Authorization.fromRpcList.ErrorType
| Frame.fromRpc.ErrorType
| FrameRequest.fromRpc.ErrorType
| FrameSignature.fromRpc.ErrorType
| Hex.toNumber.ErrorType
| Hex.toBigInt.ErrorType
Expand Down Expand Up @@ -210,7 +212,7 @@ export function toRpc(request: toRpc.Input): Rpc {
request_rpc.input = request.input
}
if (request.frames !== undefined)
request_rpc.frames = request.frames.map(Frame.toRpc)
request_rpc.frames = request.frames.map(FrameRequest.toRpc)
if (request.signatures !== undefined)
request_rpc.signatures = request.signatures.map(FrameSignature.toRpc)
if (typeof request.from !== 'undefined') request_rpc.from = request.from
Expand Down Expand Up @@ -258,7 +260,7 @@ export declare namespace toRpc {

export type ErrorType =
| Authorization.toRpcList.ErrorType
| Frame.toRpc.ErrorType
| FrameRequest.toRpc.ErrorType
| FrameSignature.toRpc.ErrorType
| Hex.fromNumber.ErrorType
| Errors.GlobalErrorType
Expand Down
6 changes: 6 additions & 0 deletions src/core/TxEnvelope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -781,6 +781,12 @@ export function toTransactionRequest(
...rest,
...(sidecars ? { blobs: sidecars.blobs } : {}),
chainId: Hex.toNumber(Hex.fromNumber(rest.chainId)),
// Envelopes commit to zero defaults; request omission would trigger estimation.
frames: rest.frames.map((frame) => ({
...frame,
executionGas: frame.executionGas ?? 0n,
stateGas: frame.stateGas ?? 0n,
})),
from: sender,
}
}
Expand Down
16 changes: 16 additions & 0 deletions src/core/_test/FrameRequest.test-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { FrameRequest } from 'ox'
import { expectTypeOf, test } from 'vp/test'

test('public frame request types', () => {
const rpc = { mode: '0x2' } as const satisfies FrameRequest.Rpc
const request = FrameRequest.fromRpc(rpc)
expectTypeOf(request).toEqualTypeOf<FrameRequest.FrameRequest>()
expectTypeOf(FrameRequest.toRpc(request)).toEqualTypeOf<FrameRequest.Rpc>()
const input = {
executionGas: '0x1',
stateGas: 0,
} as const satisfies FrameRequest.toRpc.Input
expectTypeOf(FrameRequest.toRpc(input)).toEqualTypeOf<FrameRequest.Rpc>()
expectTypeOf<FrameRequest.fromRpc.ErrorType>().not.toBeNever()
expectTypeOf<FrameRequest.toRpc.ErrorType>().not.toBeNever()
})
41 changes: 41 additions & 0 deletions src/core/_test/FrameRequest.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { FrameRequest } from 'ox'
import { describe, expect, test } from 'vp/test'

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",
}
`)
})
})
131 changes: 131 additions & 0 deletions src/core/_test/FrameRpc.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import {
Frame,
FrameSignature,
TransactionRequest,
TransactionEnvelope,
TxEnvelopeEip8141,
} from 'ox'
import { describe, expect, test } from 'vp/test'

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', () => {
expect(FrameSignature.fromRpc({ scheme: '0x1', signer: '0x' })).toEqual({
scheme: 'secp256k1',
payload: '0x',
})
})
})

describe('TransactionEnvelope.toTransactionRequest', () => {
test('retains committed zero defaults when converting envelopes to requests', () => {
const envelope = TxEnvelopeEip8141.from({
chainId: 1,
frames: [Frame.from({ mode: 'sender', executionGas: 50_000n })],
sender: '0x1111111111111111111111111111111111111111',
signatures: [FrameSignature.from('0xaabb')],
})
const request = TransactionEnvelope.toTransactionRequest(envelope)
expect(request.frames).toEqual([
{ mode: 'sender', executionGas: 50_000n, stateGas: 0n },
])
expect(
TxEnvelopeEip8141.getSignPayload(
TransactionRequest.toEnvelope(
request,
) as TxEnvelopeEip8141.TxEnvelopeEip8141,
),
).toBe(TxEnvelopeEip8141.getSignPayload(envelope))
})
})
2 changes: 1 addition & 1 deletion src/core/_test/FrameSignature.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -457,7 +457,7 @@ describe('fromRpc', () => {
msg: '0x',
scheme: '0x0',
signature: '0xaabb',
signer: null,
signer: '0x',
}),
).toEqual(FrameSignature.from('0xaabb'))
})
Expand Down
Loading
Loading