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
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
15 changes: 8 additions & 7 deletions src/core/TransactionRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ 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 * as FrameRequest from './FrameRequest.js'
import * as FrameSignature from './FrameSignature.js'
import * as Hex from './Hex.js'
import type { Compute } from './internal/types.js'
Expand All @@ -13,7 +13,7 @@ export type TransactionRequest<
bigintType = bigint,
numberType = number,
type extends string = string,
frame = Frame.Frame<bigintType>,
frame = FrameRequest.FrameRequest<bigintType>,
signature = FrameSignature.FrameSignature,
> = Compute<{
/** EIP-2930 Access List. */
Expand Down Expand Up @@ -63,7 +63,7 @@ export type Rpc = TransactionRequest<
Hex.Hex,
Hex.Hex,
string,
Frame.Rpc,
FrameRequest.Rpc,
FrameSignature.Rpc
>

Expand Down Expand Up @@ -92,7 +92,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')
Expand Down Expand Up @@ -120,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
Expand Down Expand Up @@ -189,7 +189,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')
Expand Down Expand Up @@ -224,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
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 'vitest'

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()
})
43 changes: 43 additions & 0 deletions src/core/_test/FrameRequest.test.ts
Original file line number Diff line number Diff line change
@@ -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",
}
`)
})
})
108 changes: 108 additions & 0 deletions src/core/_test/FrameRpc.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
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', () => {
expect(FrameSignature.fromRpc({ scheme: '0x1', signer: '0x' })).toEqual({
scheme: 'secp256k1',
payload: '0x',
})
})
})
2 changes: 1 addition & 1 deletion src/core/_test/FrameSignature.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -460,7 +460,7 @@ describe('fromRpc', () => {
msg: '0x',
scheme: '0x0',
signature: '0xaabb',
signer: null,
signer: '0x',
}),
).toEqual(FrameSignature.from('0xaabb'))
})
Expand Down
45 changes: 45 additions & 0 deletions src/core/_test/RpcSchema.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,3 +116,48 @@ test('roundtrip: FromViem -> ToViem preserves schema', () => {
expectTypeOf<Roundtrip[0]['Method']>().toEqualTypeOf<'eth_blockNumber'>()
expectTypeOf<Roundtrip[0]['ReturnType']>().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<Filled>()
})

test('frame simulation results expose payer and frame results', () => {
type Result = Extract<
RpcSchema.Eth,
{ Request: { method: 'eth_simulateV1' } }
>['ReturnType'][number]
type Call = NonNullable<Result['calls']>[number]
expectTypeOf<Call['payer']>().toEqualTypeOf<`0x${string}` | undefined>()
type Frame = NonNullable<Call['frameResults']>[number]
expectTypeOf<Frame['status']>().toEqualTypeOf<'0x0' | '0x1' | '0x2'>()
expectTypeOf<Frame['executionGasUsed']>().toEqualTypeOf<`0x${string}`>()
expectTypeOf<Frame['stateGasUsed']>().toEqualTypeOf<`0x${string}`>()
expectTypeOf<Frame['returnData']>().toEqualTypeOf<`0x${string}`>()
})
2 changes: 2 additions & 0 deletions src/core/_test/TxEnvelopeEip8141.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}),
Expand Down
Loading
Loading