ox 1.8.3 → 1.8.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/core/Transaction.d.ts +5 -2
  3. package/dist/core/Transaction.d.ts.map +1 -1
  4. package/dist/core/Transaction.js +8 -0
  5. package/dist/core/Transaction.js.map +1 -1
  6. package/dist/core/TransactionRequest.d.ts +5 -2
  7. package/dist/core/TransactionRequest.d.ts.map +1 -1
  8. package/dist/core/TransactionRequest.js +8 -0
  9. package/dist/core/TransactionRequest.js.map +1 -1
  10. package/dist/core/TxEnvelope.d.ts +1 -1
  11. package/dist/core/TxEnvelope.d.ts.map +1 -1
  12. package/dist/core/TxEnvelope.js +2 -1
  13. package/dist/core/TxEnvelope.js.map +1 -1
  14. package/dist/core/TxEnvelopeEip8141.d.ts +18 -7
  15. package/dist/core/TxEnvelopeEip8141.d.ts.map +1 -1
  16. package/dist/core/TxEnvelopeEip8141.js +37 -8
  17. package/dist/core/TxEnvelopeEip8141.js.map +1 -1
  18. package/dist/core/internal/frameNonce.d.ts +10 -0
  19. package/dist/core/internal/frameNonce.d.ts.map +1 -0
  20. package/dist/core/internal/frameNonce.js +45 -0
  21. package/dist/core/internal/frameNonce.js.map +1 -0
  22. package/dist/zod/Block.d.ts +3 -0
  23. package/dist/zod/Block.d.ts.map +1 -1
  24. package/dist/zod/RpcSchema.d.ts +23 -0
  25. package/dist/zod/RpcSchema.d.ts.map +1 -1
  26. package/dist/zod/Transaction.d.ts +6 -0
  27. package/dist/zod/Transaction.d.ts.map +1 -1
  28. package/dist/zod/Transaction.js +18 -11
  29. package/dist/zod/Transaction.js.map +1 -1
  30. package/dist/zod/TransactionEnvelope.d.ts +10 -0
  31. package/dist/zod/TransactionEnvelope.d.ts.map +1 -1
  32. package/dist/zod/TransactionRequest.d.ts +2 -0
  33. package/dist/zod/TransactionRequest.d.ts.map +1 -1
  34. package/dist/zod/TransactionRequest.js +9 -3
  35. package/dist/zod/TransactionRequest.js.map +1 -1
  36. package/dist/zod/TxEnvelopeEip8141.d.ts +7 -0
  37. package/dist/zod/TxEnvelopeEip8141.d.ts.map +1 -1
  38. package/dist/zod/TxEnvelopeEip8141.js +2 -0
  39. package/dist/zod/TxEnvelopeEip8141.js.map +1 -1
  40. package/dist/zod/internal/Frame.d.ts +8 -0
  41. package/dist/zod/internal/Frame.d.ts.map +1 -1
  42. package/dist/zod/internal/Frame.js +29 -0
  43. package/dist/zod/internal/Frame.js.map +1 -1
  44. package/dist/zod/internal/rpcSchemas/Eth.d.ts +23 -0
  45. package/dist/zod/internal/rpcSchemas/Eth.d.ts.map +1 -1
  46. package/dist/zod/internal/rpcSchemas/Wallet.d.ts +2 -0
  47. package/dist/zod/internal/rpcSchemas/Wallet.d.ts.map +1 -1
  48. package/dist/zod/tempo/RpcSchemaTempo.d.ts +4 -0
  49. package/dist/zod/tempo/RpcSchemaTempo.d.ts.map +1 -1
  50. package/dist/zod/tempo/Transaction.d.ts +3 -0
  51. package/dist/zod/tempo/Transaction.d.ts.map +1 -1
  52. package/package.json +1 -1
  53. package/src/core/Transaction.ts +17 -0
  54. package/src/core/TransactionRequest.ts +12 -0
  55. package/src/core/TxEnvelope.ts +7 -2
  56. package/src/core/TxEnvelopeEip8141.ts +61 -13
  57. package/src/core/_test/TransactionEnvelope.test-d.ts +27 -0
  58. package/src/core/_test/TransactionEnvelope.test.ts +26 -0
  59. package/src/core/_test/TransactionRequest.test.ts +28 -0
  60. package/src/core/_test/TxEnvelopeEip8141.test-d.ts +27 -0
  61. package/src/core/_test/TxEnvelopeEip8141.test.ts +456 -2
  62. package/src/core/internal/frameNonce.ts +61 -0
  63. package/src/version.ts +1 -1
  64. package/src/zod/Transaction.ts +42 -23
  65. package/src/zod/TransactionRequest.ts +32 -17
  66. package/src/zod/TxEnvelopeEip8141.ts +2 -0
  67. package/src/zod/_test/TxEnvelopeEip8141.nonce.test.ts +174 -0
  68. package/src/zod/_test/TxEnvelopeEip8141.test-d.ts +29 -0
  69. package/src/zod/internal/Frame.ts +45 -0
@@ -6,12 +6,18 @@ import * as Frame from './Frame.js'
6
6
  import * as FrameSignature from './FrameSignature.js'
7
7
  import * as Hash from './Hash.js'
8
8
  import * as Hex from './Hex.js'
9
+ import * as FrameNonce from './internal/frameNonce.js'
9
10
  import * as Quantity from './internal/quantity.js'
10
11
  import type { Assign, Compute, PartialBy } from './internal/types.js'
11
12
  import * as Rlp from './Rlp.js'
12
13
  import type * as TxEnvelopeEip4844 from './TxEnvelopeEip4844.js'
13
14
 
14
- /** An EIP-8141 transaction containing independently budgeted call frames. */
15
+ /**
16
+ * An EIP-8141 transaction containing independently budgeted call frames.
17
+ *
18
+ * Omitted `nonceKeys` preserves the original EIP-8141 encoding. Supplying keys,
19
+ * including `[0n]`, selects EIP-8250 encoding and changes the signing hash.
20
+ */
15
21
  export type TxEnvelopeEip8141 = {
16
22
  /** Versioned blob hashes. @default [] */
17
23
  blobVersionedHashes?: readonly Hex.Hex[] | undefined
@@ -25,8 +31,10 @@ export type TxEnvelopeEip8141 = {
25
31
  maxFeePerGas?: bigint | undefined
26
32
  /** Maximum priority fee per gas, in wei. @default 0n */
27
33
  maxPriorityFeePerGas?: bigint | undefined
28
- /** Sender nonce. @default 0n */
34
+ /** Sender nonce or shared sequence. With `nonceKeys`, the sequence must be less than 2^64 - 1. @default 0n */
29
35
  nonce?: bigint | undefined
36
+ /** EIP-8250 domains: 1–16 strictly increasing uint256 keys. `[0n]` selects the legacy account nonce; zero cannot accompany other keys. */
37
+ nonceKeys?: readonly bigint[] | undefined
30
38
  /** Account authorizing execution. */
31
39
  sender: Address.Address
32
40
  /** PeerDAS sidecars. Excluded from transaction and signing hashes. */
@@ -53,8 +61,10 @@ export type Rpc = {
53
61
  maxFeePerGas: Hex.Hex
54
62
  /** Maximum priority fee per gas. */
55
63
  maxPriorityFeePerGas: Hex.Hex
56
- /** Sender nonce. */
64
+ /** Sender nonce or shared EIP-8250 sequence. */
57
65
  nonce: Hex.Hex
66
+ /** EIP-8250 domains. Omitted for the original EIP-8141 encoding. */
67
+ nonceKeys?: readonly Hex.Hex[] | undefined
58
68
  /** Frame signature entries. */
59
69
  signatures: readonly FrameSignature.Rpc[]
60
70
  /** RPC transaction type. */
@@ -106,6 +116,7 @@ export function assert(envelope: PartialBy<TxEnvelopeEip8141, 'type'>): void {
106
116
  maxFeePerGas = 0n,
107
117
  maxPriorityFeePerGas = 0n,
108
118
  nonce = 0n,
119
+ nonceKeys,
109
120
  sender,
110
121
  signatures = [],
111
122
  } = envelope
@@ -118,6 +129,7 @@ export function assert(envelope: PartialBy<TxEnvelopeEip8141, 'type'>): void {
118
129
  throw new InvalidError(
119
130
  'chainId must be an unsigned 256-bit integer; use bigint for large IDs.',
120
131
  )
132
+ if (nonceKeys !== undefined) FrameNonce.assert(nonceKeys, nonce)
121
133
  Address.assert(sender, { strict: false })
122
134
  for (const [field, value, bits] of [
123
135
  ['maxFeePerBlobGas', maxFeePerBlobGas, 256n],
@@ -157,6 +169,18 @@ export function assert(envelope: PartialBy<TxEnvelopeEip8141, 'type'>): void {
157
169
  for (const byte of Hex.toBytes(data)) tokens += byte === 0 ? 1n : 4n
158
170
  size += BigInt(Hex.size(data))
159
171
  }
172
+ if (nonceKeys !== undefined) {
173
+ count(
174
+ Rlp.fromHex(
175
+ nonceKeys.map((key) =>
176
+ key === 0n ? '0x' : Hex.fromBytes(Bytes.fromNumber(key)),
177
+ ),
178
+ ),
179
+ )
180
+ count(
181
+ Rlp.fromHex(nonce === 0n ? '0x' : Hex.fromBytes(Bytes.fromNumber(nonce))),
182
+ )
183
+ }
160
184
  for (const [index, frame] of (frames as readonly Frame.Frame[]).entries()) {
161
185
  Frame.assert(frame)
162
186
  const flags =
@@ -221,6 +245,7 @@ export function assert(envelope: PartialBy<TxEnvelopeEip8141, 'type'>): void {
221
245
  export declare namespace assert {
222
246
  type ErrorType =
223
247
  | InvalidError
248
+ | FrameNonce.InvalidError
224
249
  | Frame.assert.ErrorType
225
250
  | FrameSignature.toTuple.ErrorType
226
251
  | Address.assert.ErrorType
@@ -258,9 +283,14 @@ export function deserialize(serialized: Serialized): TxEnvelopeEip8141 {
258
283
  throw new InvalidError('Expected a transaction list.')
259
284
  const wrapped = Array.isArray(decoded[0])
260
285
  const body = wrapped ? decoded[0] : decoded
261
- if (!Array.isArray(body) || body.length !== 7)
262
- throw new InvalidError('Expected seven transaction fields.')
263
- const [chainId, nonce, sender, frames, signatures, fees, hashes] = body
286
+ if (!Array.isArray(body) || (body.length !== 7 && body.length !== 8))
287
+ throw new InvalidError('Expected seven or eight transaction fields.')
288
+ const keyed = body.length === 8
289
+ const [chainId, ...fields] = body
290
+ const keys = keyed ? fields.shift() : undefined
291
+ if (keyed && !Array.isArray(keys))
292
+ throw new InvalidError('Expected a nonce key list.')
293
+ const [nonce, sender, frames, signatures, fees, hashes] = fields
264
294
  if (
265
295
  typeof sender !== 'string' ||
266
296
  !Array.isArray(frames) ||
@@ -288,6 +318,7 @@ export function deserialize(serialized: Serialized): TxEnvelopeEip8141 {
288
318
  maxFeePerGas: integer(fees[1]),
289
319
  maxPriorityFeePerGas: integer(fees[0]),
290
320
  nonce: integer(nonce),
321
+ ...(Array.isArray(keys) ? { nonceKeys: keys.map(integer) } : {}),
291
322
  sender: sender as Address.Address,
292
323
  signatures: signatures.map((entry) =>
293
324
  FrameSignature.fromTuple(entry as FrameSignature.Tuple),
@@ -403,6 +434,9 @@ export function fromRpc(envelope: Rpc): TxEnvelopeEip8141 {
403
434
  maxFeePerGas: Hex.toBigInt(envelope.maxFeePerGas),
404
435
  maxPriorityFeePerGas: Hex.toBigInt(envelope.maxPriorityFeePerGas),
405
436
  nonce: Hex.toBigInt(envelope.nonce),
437
+ ...(envelope.nonceKeys !== undefined
438
+ ? { nonceKeys: FrameNonce.fromRpc(envelope.nonceKeys, envelope.nonce) }
439
+ : {}),
406
440
  sender: envelope.from,
407
441
  signatures: envelope.signatures.map(FrameSignature.fromRpc),
408
442
  })
@@ -410,6 +444,7 @@ export function fromRpc(envelope: Rpc): TxEnvelopeEip8141 {
410
444
 
411
445
  export declare namespace fromRpc {
412
446
  type ErrorType =
447
+ | FrameNonce.InvalidError
413
448
  | from.ErrorType
414
449
  | Frame.fromRpc.ErrorType
415
450
  | FrameSignature.fromRpc.ErrorType
@@ -448,6 +483,9 @@ export function toRpc(envelope: toRpc.Input): Rpc {
448
483
  envelope.maxPriorityFeePerGas ?? 0n,
449
484
  ),
450
485
  nonce: Quantity.fromNumberish(envelope.nonce ?? 0n),
486
+ ...(envelope.nonceKeys !== undefined
487
+ ? { nonceKeys: FrameNonce.toRpc(envelope.nonceKeys, envelope.nonce) }
488
+ : {}),
451
489
  signatures: (envelope.signatures ?? []).map(FrameSignature.toRpc),
452
490
  type: '0x6',
453
491
  }
@@ -462,6 +500,7 @@ export declare namespace toRpc {
462
500
  | 'maxFeePerGas'
463
501
  | 'maxPriorityFeePerGas'
464
502
  | 'nonce'
503
+ | 'nonceKeys'
465
504
  | 'type'
466
505
  > & {
467
506
  chainId: Hex.Hex | bigint | number
@@ -470,9 +509,11 @@ export declare namespace toRpc {
470
509
  maxFeePerGas?: Hex.Hex | bigint | number | undefined
471
510
  maxPriorityFeePerGas?: Hex.Hex | bigint | number | undefined
472
511
  nonce?: Hex.Hex | bigint | number | undefined
512
+ nonceKeys?: readonly (Hex.Hex | bigint | number)[] | undefined
473
513
  type?: Type | undefined
474
514
  }
475
515
  type ErrorType =
516
+ | FrameNonce.InvalidError
476
517
  | Frame.toRpc.ErrorType
477
518
  | FrameSignature.toRpc.ErrorType
478
519
  | Hex.fromNumber.ErrorType
@@ -560,10 +601,12 @@ export function hash(
560
601
  ): Hex.Hex {
561
602
  assert(envelope)
562
603
  const body = toTuple(envelope)
563
- if (options.presign)
564
- body[4] = body[4].map((entry) =>
565
- entry[2] === '0x' ? [entry[0], entry[1], entry[2], '0x'] : entry,
566
- )
604
+ if (options.presign) {
605
+ const signatures = body.length === 8 ? body[5] : body[4]
606
+ for (const [index, entry] of signatures.entries())
607
+ if (entry[2] === '0x')
608
+ signatures[index] = [entry[0], entry[1], entry[2], '0x']
609
+ }
567
610
  return Hash.keccak256(Hex.concat(serializedType, Rlp.fromHex(body)))
568
611
  }
569
612
  export declare namespace hash {
@@ -690,8 +733,7 @@ export class InvalidError extends Errors.BaseError {
690
733
  function toTuple(envelope: PartialBy<TxEnvelopeEip8141, 'type'>) {
691
734
  const quantity = (value: bigint | number | undefined): Hex.Hex =>
692
735
  value ? Hex.fromBytes(Bytes.fromNumber(value)) : '0x'
693
- return [
694
- quantity(envelope.chainId),
736
+ const fields = [
695
737
  quantity(envelope.nonce),
696
738
  envelope.sender,
697
739
  envelope.frames.map((frame) => Frame.toTuple(frame)),
@@ -703,7 +745,6 @@ function toTuple(envelope: PartialBy<TxEnvelopeEip8141, 'type'>) {
703
745
  ],
704
746
  envelope.blobVersionedHashes ?? [],
705
747
  ] satisfies [
706
- Hex.Hex,
707
748
  Hex.Hex,
708
749
  Address.Address,
709
750
  Frame.Tuple[],
@@ -711,4 +752,11 @@ function toTuple(envelope: PartialBy<TxEnvelopeEip8141, 'type'>) {
711
752
  Hex.Hex[],
712
753
  readonly Hex.Hex[],
713
754
  ]
755
+ return envelope.nonceKeys === undefined
756
+ ? ([quantity(envelope.chainId), ...fields] as const)
757
+ : ([
758
+ quantity(envelope.chainId),
759
+ envelope.nonceKeys.map(quantity),
760
+ ...fields,
761
+ ] as const)
714
762
  }
@@ -10,6 +10,33 @@ import {
10
10
  import { describe, expectTypeOf, test } from 'vp/test'
11
11
 
12
12
  describe('getType', () => {
13
+ test('infers frame transactions from nonce keys', () => {
14
+ expectTypeOf(
15
+ TransactionEnvelope.getType({ nonceKeys: [1n] }),
16
+ ).toEqualTypeOf<'eip8141'>()
17
+ expectTypeOf(
18
+ TransactionEnvelope.getType({ nonceKeys: [0n] }),
19
+ ).toEqualTypeOf<'eip8141'>()
20
+ expectTypeOf(
21
+ TransactionEnvelope.getType({ nonceKeys: [] }),
22
+ ).toEqualTypeOf<'eip8141'>()
23
+ expectTypeOf(
24
+ TransactionEnvelope.getType({
25
+ nonceKeys: [1n],
26
+ frames: undefined,
27
+ authorizationList: [],
28
+ blobVersionedHashes: [],
29
+ gasPrice: 1n,
30
+ }),
31
+ ).toEqualTypeOf<'eip8141'>()
32
+ expectTypeOf(
33
+ TransactionEnvelope.getType({ nonceKeys: undefined, maxFeePerGas: 1n }),
34
+ ).toEqualTypeOf<'eip1559'>()
35
+ expectTypeOf(
36
+ TransactionEnvelope.getType({ nonceKeys: [1n], type: 'eip1559' }),
37
+ ).toEqualTypeOf<'eip1559'>()
38
+ })
39
+
13
40
  test('behavior: infers explicit transaction types', () => {
14
41
  expectTypeOf(
15
42
  TransactionEnvelope.getType({ type: 'legacy' }),
@@ -99,6 +99,32 @@ test('exports', () => {
99
99
  })
100
100
 
101
101
  describe('getType', () => {
102
+ test('infers frame transactions from nonce keys', () => {
103
+ expect([
104
+ TransactionEnvelope.getType({ nonceKeys: [1n] }),
105
+ TransactionEnvelope.getType({ nonceKeys: [0n] }),
106
+ TransactionEnvelope.getType({ nonceKeys: [] }),
107
+ TransactionEnvelope.getType({
108
+ nonceKeys: [1n],
109
+ frames: undefined,
110
+ authorizationList: [],
111
+ blobVersionedHashes: [],
112
+ gasPrice: 1n,
113
+ }),
114
+ TransactionEnvelope.getType({ nonceKeys: undefined, maxFeePerGas: 1n }),
115
+ TransactionEnvelope.getType({ nonceKeys: [1n], type: 'eip1559' }),
116
+ ]).toMatchInlineSnapshot(`
117
+ [
118
+ "eip8141",
119
+ "eip8141",
120
+ "eip8141",
121
+ "eip8141",
122
+ "eip1559",
123
+ "eip1559",
124
+ ]
125
+ `)
126
+ })
127
+
102
128
  test('frame fields take precedence over blobs and fees', () => {
103
129
  expect(
104
130
  TransactionEnvelope.getType({
@@ -13,6 +13,21 @@ import { kzg } from '../../../test/kzg.js'
13
13
  import { anvilMainnet } from '../../../test/prool.js'
14
14
 
15
15
  describe('toRpc', () => {
16
+ test('preserves nonce keys without type or frames', () => {
17
+ const request = { nonceKeys: [1n, 2n], nonce: 0n }
18
+ const rpc = TransactionRequest.toRpc(request)
19
+ expect(rpc).toMatchInlineSnapshot(`
20
+ {
21
+ "nonce": "0x0",
22
+ "nonceKeys": [
23
+ "0x1",
24
+ "0x2",
25
+ ],
26
+ }
27
+ `)
28
+ expect(TransactionRequest.fromRpc(rpc)).toEqual(request)
29
+ })
30
+
16
31
  test('default', () => {
17
32
  const request = TransactionRequest.toRpc({
18
33
  to: '0x0000000000000000000000000000000000000000',
@@ -439,6 +454,19 @@ test('exports', () => {
439
454
  })
440
455
 
441
456
  describe('frame transactions', () => {
457
+ test('requires frames when nonce keys select a frame envelope', () => {
458
+ expect(() =>
459
+ TransactionRequest.toEnvelope({
460
+ chainId: 1,
461
+ from: '0x1111111111111111111111111111111111111111',
462
+ nonceKeys: [1n],
463
+ nonce: 0n,
464
+ }),
465
+ ).toThrowErrorMatchingInlineSnapshot(
466
+ '[Quantity.MissingFieldError: Missing required field `frames` on `TransactionRequest`.]',
467
+ )
468
+ })
469
+
442
470
  test('derives frame blob sidecars and replaces stale hashes', () => {
443
471
  const blobs = Blobs.from('0xdeadbeef')
444
472
  const request = {
@@ -42,3 +42,30 @@ test('unsigned signature', () => {
42
42
  }>()
43
43
  expectTypeOf(envelope).toMatchTypeOf<TxEnvelopeEip8141.TxEnvelopeEip8141>()
44
44
  })
45
+
46
+ test('keyed nonce literals and numberish RPC input', () => {
47
+ const envelope = TxEnvelopeEip8141.from({
48
+ chainId: 1,
49
+ frames: [{}],
50
+ sender: '0x1111111111111111111111111111111111111111',
51
+ nonceKeys: [1n, 2n],
52
+ nonce: 3n,
53
+ })
54
+ expectTypeOf(envelope.nonceKeys).toEqualTypeOf<readonly [1n, 2n]>()
55
+ expectTypeOf(envelope.nonce).toEqualTypeOf<3n>()
56
+ expectTypeOf(TxEnvelopeEip8141.toRpc(envelope).nonceKeys).toEqualTypeOf<
57
+ readonly `0x${string}`[] | undefined
58
+ >()
59
+ expectTypeOf(
60
+ TxEnvelopeEip8141.toRpc({
61
+ ...envelope,
62
+ nonceKeys: ['0x1', 2],
63
+ nonce: '0x3',
64
+ }),
65
+ ).toEqualTypeOf<TxEnvelopeEip8141.Rpc>()
66
+ // @ts-expect-error decoded nonce keys are bigint
67
+ TxEnvelopeEip8141.from({ ...envelope, nonceKeys: [1] })
68
+ expectTypeOf<keyof TxEnvelopeEip8141.Rpc>()
69
+ .exclude<'nonceSeq'>()
70
+ .toEqualTypeOf<keyof TxEnvelopeEip8141.Rpc>()
71
+ })