ox 1.7.5 → 1.8.0

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 (139) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/core/Frame.d.ts +290 -0
  3. package/dist/core/Frame.d.ts.map +1 -0
  4. package/dist/core/Frame.js +306 -0
  5. package/dist/core/Frame.js.map +1 -0
  6. package/dist/core/FrameReceipt.d.ts +92 -0
  7. package/dist/core/FrameReceipt.d.ts.map +1 -0
  8. package/dist/core/FrameReceipt.js +72 -0
  9. package/dist/core/FrameReceipt.js.map +1 -0
  10. package/dist/core/FrameSignature.d.ts +357 -0
  11. package/dist/core/FrameSignature.d.ts.map +1 -0
  12. package/dist/core/FrameSignature.js +411 -0
  13. package/dist/core/FrameSignature.js.map +1 -0
  14. package/dist/core/Transaction.d.ts +29 -4
  15. package/dist/core/Transaction.d.ts.map +1 -1
  16. package/dist/core/Transaction.js +25 -4
  17. package/dist/core/Transaction.js.map +1 -1
  18. package/dist/core/TransactionReceipt.d.ts +13 -4
  19. package/dist/core/TransactionReceipt.d.ts.map +1 -1
  20. package/dist/core/TransactionReceipt.js +14 -0
  21. package/dist/core/TransactionReceipt.js.map +1 -1
  22. package/dist/core/TransactionRequest.d.ts +15 -7
  23. package/dist/core/TransactionRequest.d.ts.map +1 -1
  24. package/dist/core/TransactionRequest.js +47 -1
  25. package/dist/core/TransactionRequest.js.map +1 -1
  26. package/dist/core/TxEnvelope.d.ts +35 -21
  27. package/dist/core/TxEnvelope.d.ts.map +1 -1
  28. package/dist/core/TxEnvelope.js +46 -6
  29. package/dist/core/TxEnvelope.js.map +1 -1
  30. package/dist/core/TxEnvelopeEip8141.d.ts +341 -0
  31. package/dist/core/TxEnvelopeEip8141.d.ts.map +1 -0
  32. package/dist/core/TxEnvelopeEip8141.js +485 -0
  33. package/dist/core/TxEnvelopeEip8141.js.map +1 -0
  34. package/dist/core/internal/curves.d.ts +2 -0
  35. package/dist/core/internal/curves.d.ts.map +1 -1
  36. package/dist/core/internal/curves.js +2 -0
  37. package/dist/core/internal/curves.js.map +1 -1
  38. package/dist/index.d.ts +73 -0
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +73 -0
  41. package/dist/index.js.map +1 -1
  42. package/dist/tempo/Transaction.d.ts +2 -0
  43. package/dist/tempo/Transaction.d.ts.map +1 -1
  44. package/dist/tempo/TransactionReceipt.d.ts +2 -0
  45. package/dist/tempo/TransactionReceipt.d.ts.map +1 -1
  46. package/dist/zod/Block.d.ts +123 -0
  47. package/dist/zod/Block.d.ts.map +1 -1
  48. package/dist/zod/Frame.d.ts +73 -0
  49. package/dist/zod/Frame.d.ts.map +1 -0
  50. package/dist/zod/Frame.js +74 -0
  51. package/dist/zod/Frame.js.map +1 -0
  52. package/dist/zod/FrameReceipt.d.ts +50 -0
  53. package/dist/zod/FrameReceipt.d.ts.map +1 -0
  54. package/dist/zod/FrameReceipt.js +37 -0
  55. package/dist/zod/FrameReceipt.js.map +1 -0
  56. package/dist/zod/FrameSignature.d.ts +63 -0
  57. package/dist/zod/FrameSignature.d.ts.map +1 -0
  58. package/dist/zod/FrameSignature.js +66 -0
  59. package/dist/zod/FrameSignature.js.map +1 -0
  60. package/dist/zod/RpcSchema.d.ts +806 -11
  61. package/dist/zod/RpcSchema.d.ts.map +1 -1
  62. package/dist/zod/Transaction.d.ts +252 -0
  63. package/dist/zod/Transaction.d.ts.map +1 -1
  64. package/dist/zod/Transaction.js +54 -0
  65. package/dist/zod/Transaction.js.map +1 -1
  66. package/dist/zod/TransactionEnvelope.d.ts +233 -4
  67. package/dist/zod/TransactionEnvelope.d.ts.map +1 -1
  68. package/dist/zod/TransactionEnvelope.js +9 -0
  69. package/dist/zod/TransactionEnvelope.js.map +1 -1
  70. package/dist/zod/TransactionReceipt.d.ts +48 -0
  71. package/dist/zod/TransactionReceipt.d.ts.map +1 -1
  72. package/dist/zod/TransactionReceipt.js +8 -3
  73. package/dist/zod/TransactionReceipt.js.map +1 -1
  74. package/dist/zod/TransactionRequest.d.ts +50 -2
  75. package/dist/zod/TransactionRequest.d.ts.map +1 -1
  76. package/dist/zod/TransactionRequest.js +10 -4
  77. package/dist/zod/TransactionRequest.js.map +1 -1
  78. package/dist/zod/TxEnvelopeEip8141.d.ts +166 -0
  79. package/dist/zod/TxEnvelopeEip8141.d.ts.map +1 -0
  80. package/dist/zod/TxEnvelopeEip8141.js +76 -0
  81. package/dist/zod/TxEnvelopeEip8141.js.map +1 -0
  82. package/dist/zod/internal/Frame.d.ts +4 -0
  83. package/dist/zod/internal/Frame.d.ts.map +1 -0
  84. package/dist/zod/internal/Frame.js +17 -0
  85. package/dist/zod/internal/Frame.js.map +1 -0
  86. package/dist/zod/internal/rpcSchemas/Eth.d.ts +842 -11
  87. package/dist/zod/internal/rpcSchemas/Eth.d.ts.map +1 -1
  88. package/dist/zod/internal/rpcSchemas/Wallet.d.ts +56 -2
  89. package/dist/zod/internal/rpcSchemas/Wallet.d.ts.map +1 -1
  90. package/dist/zod/tempo/RpcSchemaTempo.d.ts +140 -0
  91. package/dist/zod/tempo/RpcSchemaTempo.d.ts.map +1 -1
  92. package/dist/zod/tempo/Transaction.d.ts +123 -0
  93. package/dist/zod/tempo/Transaction.d.ts.map +1 -1
  94. package/dist/zod/z.d.ts +4 -0
  95. package/dist/zod/z.d.ts.map +1 -1
  96. package/dist/zod/z.js +4 -0
  97. package/dist/zod/z.js.map +1 -1
  98. package/package.json +41 -1
  99. package/src/core/Frame.ts +443 -0
  100. package/src/core/FrameReceipt.ts +117 -0
  101. package/src/core/FrameSignature.ts +593 -0
  102. package/src/core/Transaction.ts +82 -5
  103. package/src/core/TransactionReceipt.ts +41 -3
  104. package/src/core/TransactionRequest.ts +71 -4
  105. package/src/core/TxEnvelope.ts +159 -90
  106. package/src/core/TxEnvelopeEip8141.ts +715 -0
  107. package/src/core/_test/Frame.test-d.ts +96 -0
  108. package/src/core/_test/Frame.test.ts +267 -0
  109. package/src/core/_test/FrameReceipt.test.ts +86 -0
  110. package/src/core/_test/FrameSignature.test-d.ts +93 -0
  111. package/src/core/_test/FrameSignature.test.ts +554 -0
  112. package/src/core/_test/Transaction.test.ts +33 -0
  113. package/src/core/_test/TransactionEnvelope.test-d.ts +74 -0
  114. package/src/core/_test/TransactionEnvelope.test.ts +107 -0
  115. package/src/core/_test/TransactionReceipt.test.ts +59 -0
  116. package/src/core/_test/TransactionRequest.test-d.ts +50 -0
  117. package/src/core/_test/TransactionRequest.test.ts +88 -1
  118. package/src/core/_test/TxEnvelopeEip8141.test-d.ts +44 -0
  119. package/src/core/_test/TxEnvelopeEip8141.test.ts +1775 -0
  120. package/src/core/_test/curves.test.ts +6 -1
  121. package/src/core/_test/index.test.ts +4 -0
  122. package/src/core/internal/curves.ts +4 -0
  123. package/src/index.ts +77 -0
  124. package/src/version.ts +1 -1
  125. package/src/zod/Frame.ts +82 -0
  126. package/src/zod/FrameReceipt.ts +48 -0
  127. package/src/zod/FrameSignature.ts +78 -0
  128. package/src/zod/Transaction.ts +63 -0
  129. package/src/zod/TransactionEnvelope.ts +33 -0
  130. package/src/zod/TransactionReceipt.ts +14 -3
  131. package/src/zod/TransactionRequest.ts +18 -3
  132. package/src/zod/TxEnvelopeEip8141.ts +102 -0
  133. package/src/zod/_test/Frame.test-d.ts +41 -0
  134. package/src/zod/_test/Frame.test.ts +54 -0
  135. package/src/zod/_test/FrameReceipt.test.ts +75 -0
  136. package/src/zod/_test/FrameSignature.test.ts +94 -0
  137. package/src/zod/_test/TxEnvelopeEip8141.test.ts +116 -0
  138. package/src/zod/internal/Frame.ts +26 -0
  139. package/src/zod/z.ts +4 -0
@@ -0,0 +1,554 @@
1
+ import { p256 } from '@noble/curves/nist.js'
2
+ import { secp256k1 } from '@noble/curves/secp256k1.js'
3
+ import { FrameSignature, Hex, P256, Rlp, Secp256k1 } from 'ox'
4
+ import { describe, expect, test } from 'vite-plus/test'
5
+
6
+ const arbitrary = {
7
+ payload: '0x',
8
+ scheme: 'arbitrary',
9
+ signature: '0xaabb',
10
+ } as const
11
+ const privateKey = Hex.fromNumber(1, { size: 32 })
12
+ const payload = Hex.fromNumber(1, { size: 32 })
13
+ const publicKey = {
14
+ prefix: 4,
15
+ x: '0x6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296',
16
+ y: '0x4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5',
17
+ } as const
18
+ const secpBytes =
19
+ '0x0100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002'
20
+ const p256Bytes =
21
+ '0x000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000026b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c2964fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5'
22
+
23
+ describe('from', () => {
24
+ test.each(['0x', '0xaabb'] as const)(
25
+ 'constructs an arbitrary signature from %s',
26
+ (signature) => {
27
+ expect(FrameSignature.from(signature)).toEqual({
28
+ payload: '0x',
29
+ scheme: 'arbitrary',
30
+ signature,
31
+ })
32
+ },
33
+ )
34
+
35
+ test.each(['0xa', '0xgg', 'aabb'])(
36
+ 'rejects malformed signature bytes %s',
37
+ (signature) => {
38
+ expect(() => FrameSignature.from(signature as Hex.Hex)).toThrow()
39
+ },
40
+ )
41
+
42
+ test('defaults to arbitrary and the canonical transaction payload', () => {
43
+ expect(FrameSignature.from({ signature: '0xaabb' })).toMatchInlineSnapshot(`
44
+ {
45
+ "payload": "0x",
46
+ "scheme": "arbitrary",
47
+ "signature": "0xaabb",
48
+ }
49
+ `)
50
+ expect(
51
+ FrameSignature.from({
52
+ payload: undefined,
53
+ scheme: undefined,
54
+ signature: '0xaabb',
55
+ }),
56
+ ).toEqual(arbitrary)
57
+ })
58
+
59
+ test('copies metadata without resolving the signer', () => {
60
+ expect(FrameSignature.from(arbitrary)).toEqual(arbitrary)
61
+ expect(FrameSignature.from(arbitrary)).not.toBe(arbitrary)
62
+ expect(FrameSignature.from({ scheme: 1 })).toEqual({
63
+ payload: '0x',
64
+ scheme: 1,
65
+ })
66
+ })
67
+
68
+ test('preserves structured signatures and public keys', () => {
69
+ const signature = P256.sign({ extraEntropy: false, payload, privateKey })
70
+ const entry = FrameSignature.from({
71
+ payload,
72
+ publicKey,
73
+ scheme: 'p256',
74
+ signature,
75
+ })
76
+ expect(entry).toEqual({ payload, publicKey, scheme: 'p256', signature })
77
+ })
78
+
79
+ describe('p256', () => {
80
+ test('constructs a structured entry with default payload', () => {
81
+ const signature = { r: '0x01', s: '0x02' } as const
82
+ const entry = FrameSignature.from({
83
+ publicKey,
84
+ scheme: 'p256',
85
+ signature,
86
+ })
87
+ expect(entry).toEqual({
88
+ payload: '0x',
89
+ publicKey,
90
+ scheme: 'p256',
91
+ signature,
92
+ })
93
+ expect(FrameSignature.toTuple(entry)[3]).toBe(p256Bytes)
94
+ })
95
+
96
+ test('preserves metadata and high-s until encoding', () => {
97
+ const signature = Object.freeze({
98
+ r: '0x01',
99
+ s: Hex.fromNumber(p256.Point.Fn.ORDER - 2n),
100
+ })
101
+ const signer = '0x0000000000000000000000000000000000000000'
102
+ const entry = FrameSignature.from({
103
+ payload,
104
+ publicKey,
105
+ scheme: 'p256',
106
+ signature,
107
+ signer,
108
+ })
109
+ expect(entry).toEqual({
110
+ payload,
111
+ publicKey,
112
+ scheme: 'p256',
113
+ signature,
114
+ signer,
115
+ })
116
+ expect(FrameSignature.toTuple(entry)[3]).toBe(p256Bytes)
117
+ expect(entry.signature.s).toBe(signature.s)
118
+ })
119
+
120
+ test('rejects a missing public key', () => {
121
+ expect(() =>
122
+ FrameSignature.from({
123
+ scheme: 'p256',
124
+ signature: { r: '0x01', s: '0x02' },
125
+ } as never),
126
+ ).toThrow()
127
+ })
128
+ })
129
+
130
+ describe('secp256k1', () => {
131
+ test('constructs a structured entry with default payload', () => {
132
+ const signature = { r: '0x01', s: '0x02', yParity: 1 } as const
133
+ const entry = FrameSignature.from({ scheme: 'secp256k1', signature })
134
+ expect(entry).toEqual({ payload: '0x', scheme: 'secp256k1', signature })
135
+ expect(FrameSignature.toTuple(entry)[3]).toBe(secpBytes)
136
+ })
137
+
138
+ test('preserves explicit payload and signer', () => {
139
+ const signature = Secp256k1.sign({ payload, privateKey })
140
+ const signer = '0x0000000000000000000000000000000000000000'
141
+ expect(
142
+ FrameSignature.from({
143
+ payload,
144
+ scheme: 'secp256k1',
145
+ signature,
146
+ signer,
147
+ }),
148
+ ).toEqual({ payload, scheme: 'secp256k1', signature, signer })
149
+ })
150
+
151
+ test('rejects invalid parity', () => {
152
+ expect(() =>
153
+ FrameSignature.from({
154
+ scheme: 'secp256k1',
155
+ signature: { r: '0x01', s: '0x02', yParity: 27 },
156
+ }),
157
+ ).toThrow()
158
+ })
159
+ })
160
+ })
161
+
162
+ describe('toTuple', () => {
163
+ test('omitted payload selects the canonical signing hash', () => {
164
+ expect(
165
+ FrameSignature.toTuple({ scheme: 'arbitrary', signature: '0xaabb' }),
166
+ ).toEqual(['0x', '0x', '0x', '0xaabb'])
167
+ expect(FrameSignature.toTuple({ scheme: 'secp256k1' })).toEqual([
168
+ '0x01',
169
+ '0x',
170
+ '0x',
171
+ '0x',
172
+ ])
173
+ expect(
174
+ FrameSignature.toTuple({ payload: undefined, scheme: 'p256' }),
175
+ ).toEqual(['0x02', '0x', '0x', '0x'])
176
+ })
177
+
178
+ test('encodes arbitrary witness bytes in specification order', () => {
179
+ expect(FrameSignature.toTuple(arbitrary)).toEqual([
180
+ '0x',
181
+ '0x',
182
+ '0x',
183
+ '0xaabb',
184
+ ])
185
+ expect(
186
+ Rlp.fromHex(FrameSignature.toTuple(arbitrary)),
187
+ ).toMatchInlineSnapshot('"0xc680808082aabb"')
188
+ })
189
+
190
+ test.each([0, 'arbitrary'] as const)(
191
+ 'encodes arbitrary scheme %s',
192
+ (scheme) => {
193
+ expect(FrameSignature.toTuple({ ...arbitrary, scheme })).toEqual([
194
+ '0x',
195
+ '0x',
196
+ '0x',
197
+ '0xaabb',
198
+ ])
199
+ },
200
+ )
201
+
202
+ test.each([1, 'secp256k1'] as const)(
203
+ 'encodes secp256k1 scheme %s',
204
+ (scheme) => {
205
+ const entry = FrameSignature.from({
206
+ scheme,
207
+ signature: { r: '0x01', s: '0x02', yParity: 1 },
208
+ })
209
+ expect(FrameSignature.toTuple(entry)).toEqual([
210
+ '0x01',
211
+ '0x',
212
+ '0x',
213
+ secpBytes,
214
+ ])
215
+ },
216
+ )
217
+
218
+ test.each([2, 'p256'] as const)('encodes P-256 scheme %s', (scheme) => {
219
+ const entry = FrameSignature.from({
220
+ publicKey,
221
+ scheme,
222
+ signature: { r: '0x01', s: '0x02' },
223
+ })
224
+ expect(FrameSignature.toTuple(entry)).toEqual([
225
+ '0x02',
226
+ '0x',
227
+ '0x',
228
+ p256Bytes,
229
+ ])
230
+ })
231
+
232
+ test('retains explicit payload and signer', () => {
233
+ const entry = FrameSignature.from({
234
+ payload,
235
+ scheme: 'secp256k1',
236
+ signer: '0x0000000000000000000000000000000000000000',
237
+ })
238
+ expect(FrameSignature.toTuple(entry)).toEqual([
239
+ '0x01',
240
+ entry.signer,
241
+ payload,
242
+ '0x',
243
+ ])
244
+ expect(FrameSignature.fromTuple(FrameSignature.toTuple(entry))).toEqual(
245
+ entry,
246
+ )
247
+ })
248
+
249
+ test('normalizes P-256 high-s without mutating input', () => {
250
+ const signature = Object.freeze({
251
+ r: '0x01',
252
+ s: Hex.fromNumber(p256.Point.Fn.ORDER - 2n),
253
+ })
254
+ const entry = FrameSignature.from({ publicKey, scheme: 'p256', signature })
255
+ expect(FrameSignature.toTuple(entry)[3]).toBe(p256Bytes)
256
+ expect(entry.signature.s).toBe(Hex.fromNumber(p256.Point.Fn.ORDER - 2n))
257
+ })
258
+ })
259
+
260
+ describe('fromTuple', () => {
261
+ test('decodes fixed arbitrary tuple', () => {
262
+ expect(FrameSignature.fromTuple(['0x', '0x', '0x', '0xaabb'])).toEqual(
263
+ arbitrary,
264
+ )
265
+ })
266
+
267
+ test('decodes fixed secp256k1 bytes', () => {
268
+ expect(FrameSignature.fromTuple(['0x01', '0x', '0x', secpBytes])).toEqual({
269
+ payload: '0x',
270
+ scheme: 'secp256k1',
271
+ signature: {
272
+ r: Hex.fromNumber(1, { size: 32 }),
273
+ s: Hex.fromNumber(2, { size: 32 }),
274
+ yParity: 1,
275
+ },
276
+ })
277
+ })
278
+
279
+ test('decodes fixed P-256 bytes', () => {
280
+ expect(FrameSignature.fromTuple(['0x02', '0x', '0x', p256Bytes])).toEqual({
281
+ payload: '0x',
282
+ publicKey,
283
+ scheme: 'p256',
284
+ signature: {
285
+ r: Hex.fromNumber(1, { size: 32 }),
286
+ s: Hex.fromNumber(2, { size: 32 }),
287
+ },
288
+ })
289
+ })
290
+
291
+ test.each([
292
+ ['0x01', 'secp256k1'],
293
+ ['0x02', 'p256'],
294
+ ] as const)('decodes unsigned %s entries', (scheme, name) => {
295
+ expect(FrameSignature.fromTuple([scheme, '0x', '0x', '0x'])).toEqual({
296
+ payload: '0x',
297
+ scheme: name,
298
+ })
299
+ })
300
+
301
+ test('round-trips a real secp256k1 signature', () => {
302
+ const signature = Secp256k1.sign({
303
+ extraEntropy: false,
304
+ payload,
305
+ privateKey,
306
+ })
307
+ const entry = FrameSignature.fromTuple(
308
+ FrameSignature.toTuple(
309
+ FrameSignature.from({ payload, scheme: 'secp256k1', signature }),
310
+ ),
311
+ ) as FrameSignature.Secp256k1
312
+ expect(entry.signature).toEqual(signature)
313
+ expect(
314
+ Secp256k1.recoverAddress({ payload, signature: entry.signature! }),
315
+ ).toBe('0x7e5f4552091a69125d5dfcb7b8c2659029395bdf')
316
+ })
317
+
318
+ test('round-trips a real P-256 signature', () => {
319
+ const signature = P256.sign({ extraEntropy: false, payload, privateKey })
320
+ const entry = FrameSignature.fromTuple(
321
+ FrameSignature.toTuple(
322
+ FrameSignature.from({ publicKey, scheme: 'p256', signature }),
323
+ ),
324
+ ) as FrameSignature.P256
325
+ expect(
326
+ P256.verify({
327
+ payload,
328
+ publicKey: entry.publicKey!,
329
+ signature: entry.signature!,
330
+ }),
331
+ ).toBe(true)
332
+ })
333
+
334
+ test.each(['0x00', '0x0001', '0x1', '0x03'])(
335
+ 'rejects scheme %s',
336
+ (scheme) => {
337
+ expect(() =>
338
+ FrameSignature.fromTuple([scheme, '0x', '0x', '0x'] as never),
339
+ ).toThrow()
340
+ },
341
+ )
342
+
343
+ test.each([
344
+ [],
345
+ ['0x', '0x', '0x'],
346
+ ['0x', '0x', '0x', '0x', '0x'],
347
+ ['0x01', undefined, '0x', '0x'],
348
+ ['0x', '0x0000000000000000000000000000000000000000', '0x', '0x'],
349
+ ['0x01', '0x', '0x', '0x01'],
350
+ ['0x02', '0x', '0x', '0x01'],
351
+ ['0x01', '0x', '0x', `0x02${secpBytes.slice(4)}`],
352
+ [
353
+ '0x02',
354
+ '0x',
355
+ '0x',
356
+ Hex.concat(
357
+ Hex.slice(p256Bytes, 0, 32),
358
+ Hex.fromNumber(p256.Point.Fn.ORDER - 2n, { size: 32 }),
359
+ Hex.slice(p256Bytes, 64),
360
+ ),
361
+ ],
362
+ ])('rejects malformed tuple %#', (...tuple) => {
363
+ expect(() => FrameSignature.fromTuple(tuple as never)).toThrow()
364
+ })
365
+ })
366
+
367
+ describe('assert', () => {
368
+ test.each([
369
+ FrameSignature.from({ scheme: 1 }),
370
+ FrameSignature.from({ scheme: 2 }),
371
+ FrameSignature.from({ scheme: 'secp256k1' }),
372
+ FrameSignature.from({ scheme: 'p256' }),
373
+ ])('requires signature only when signed for $scheme', (entry) => {
374
+ expect(() => FrameSignature.assert(entry)).not.toThrow()
375
+ expect(() => FrameSignature.assert(entry, { signed: true })).toThrow(
376
+ FrameSignature.InvalidError,
377
+ )
378
+ })
379
+
380
+ test('allows empty arbitrary witness even when signed', () => {
381
+ expect(() =>
382
+ FrameSignature.assert(FrameSignature.from({ signature: '0x' }), {
383
+ signed: true,
384
+ }),
385
+ ).not.toThrow()
386
+ })
387
+
388
+ test.each([
389
+ { scheme: 3 },
390
+ { scheme: -1 },
391
+ { scheme: 1.5 },
392
+ { scheme: 'invalid' },
393
+ { scheme: 'toString' },
394
+ { signer: '0x0000000000000000000000000000000000000000' },
395
+ { payload: '0x00' },
396
+ { payload: `0x${'00'.repeat(32)}` },
397
+ { payload: '0x0' },
398
+ { signature: '0xa' },
399
+ { signature: '0xgg' },
400
+ { scheme: 1, signature: '0x01' },
401
+ { scheme: 2, signature: '0x01' },
402
+ ])('rejects invalid entry %#', (fields) => {
403
+ expect(() =>
404
+ FrameSignature.assert({ ...arbitrary, ...fields } as never),
405
+ ).toThrow()
406
+ })
407
+
408
+ test.each([0n, secp256k1.Point.Fn.ORDER, secp256k1.Point.Fn.ORDER - 1n])(
409
+ 'rejects invalid secp256k1 s %s',
410
+ (s) => {
411
+ expect(() =>
412
+ FrameSignature.from({
413
+ scheme: 1,
414
+ signature: { r: '0x01', s: Hex.fromNumber(s), yParity: 0 },
415
+ }),
416
+ ).toThrow()
417
+ },
418
+ )
419
+
420
+ test.each([0n, p256.Point.Fn.ORDER, p256.Point.Fn.ORDER + 1n])(
421
+ 'rejects invalid P-256 s %s',
422
+ (s) => {
423
+ expect(() =>
424
+ FrameSignature.from({
425
+ publicKey,
426
+ scheme: 2,
427
+ signature: { r: '0x01', s: Hex.fromNumber(s) },
428
+ }),
429
+ ).toThrow()
430
+ },
431
+ )
432
+
433
+ test('rejects missing public key for signed P-256', () => {
434
+ expect(() =>
435
+ FrameSignature.assert({
436
+ payload: '0x',
437
+ scheme: 2,
438
+ signature: { r: '0x01', s: '0x02' },
439
+ } as never),
440
+ ).toThrow()
441
+ })
442
+ })
443
+
444
+ describe('validate', () => {
445
+ test('reports structural validity without signature verification', () => {
446
+ expect(FrameSignature.validate(arbitrary)).toBe(true)
447
+ expect(FrameSignature.validate({ ...arbitrary, payload: '0x01' })).toBe(
448
+ false,
449
+ )
450
+ })
451
+ })
452
+
453
+ describe('fromRpc', () => {
454
+ test('arbitrary signature', () => {
455
+ expect(
456
+ FrameSignature.fromRpc({
457
+ msg: '0x',
458
+ scheme: 0,
459
+ signature: '0xaabb',
460
+ signer: null,
461
+ }),
462
+ ).toEqual(FrameSignature.from('0xaabb'))
463
+ })
464
+ test('secp256k1 signature', () => {
465
+ expect(
466
+ FrameSignature.fromRpc({ msg: '0x', scheme: 1, signature: secpBytes }),
467
+ ).toEqual(
468
+ FrameSignature.from({
469
+ scheme: 'secp256k1',
470
+ signature: {
471
+ r: `0x${'00'.repeat(31)}01`,
472
+ s: `0x${'00'.repeat(31)}02`,
473
+ yParity: 1,
474
+ },
475
+ }),
476
+ )
477
+ })
478
+ test('P-256 signature', () => {
479
+ const entry = FrameSignature.fromRpc({
480
+ msg: '0x',
481
+ scheme: 2,
482
+ signature: p256Bytes,
483
+ })
484
+ expect(entry).toMatchObject({ payload: '0x', publicKey, scheme: 'p256' })
485
+ expect(FrameSignature.toRpc(entry)).toEqual({
486
+ msg: '0x',
487
+ scheme: 2,
488
+ signature: p256Bytes,
489
+ })
490
+ })
491
+ test('unsigned placeholder', () => {
492
+ expect(
493
+ FrameSignature.fromRpc({ msg: '0x', scheme: 1, signature: '0x' }),
494
+ ).toEqual(FrameSignature.from({ scheme: 'secp256k1' }))
495
+ })
496
+ test('malformed protocol signature', () => {
497
+ expect(() =>
498
+ FrameSignature.fromRpc({ msg: '0x', scheme: 1, signature: '0x01' }),
499
+ ).toThrowErrorMatchingInlineSnapshot(
500
+ `[FrameSignature.InvalidError: Invalid frame signature.
501
+
502
+ Details: Invalid protocol signature length.]`,
503
+ )
504
+ })
505
+ })
506
+
507
+ describe('toRpc', () => {
508
+ test('preserves explicit payload and signer', () => {
509
+ const entry = FrameSignature.from({
510
+ payload,
511
+ scheme: 'secp256k1',
512
+ signature: { r: '0x01', s: '0x02', yParity: 1 },
513
+ signer: '0x1111111111111111111111111111111111111111',
514
+ })
515
+ expect(FrameSignature.toRpc(entry)).toEqual({
516
+ msg: payload,
517
+ scheme: 1,
518
+ signature: secpBytes,
519
+ signer: '0x1111111111111111111111111111111111111111',
520
+ })
521
+ expect(entry.signature.r).toBe('0x01')
522
+ })
523
+ })
524
+
525
+ describe('structured validation', () => {
526
+ test.each(['invalid', '1', '0x', `0x${'11'.repeat(33)}`])(
527
+ 'rejects coordinate %s',
528
+ (coordinate) => {
529
+ for (const key of ['x', 'y'] as const) {
530
+ const entry = {
531
+ payload: '0x',
532
+ publicKey: { ...publicKey, [key]: coordinate },
533
+ scheme: 'p256',
534
+ } as FrameSignature.P256
535
+ expect(FrameSignature.validate(entry)).toBe(false)
536
+ expect(() => FrameSignature.from(entry)).toThrow()
537
+ }
538
+ },
539
+ )
540
+ test.each([
541
+ { r: '1', s: '0x02', yParity: 1 },
542
+ { r: '0x01', s: '2', yParity: 1 },
543
+ { r: '0x01', s: '0x02', yParity: '1' },
544
+ { r: '0x01', s: '0x02', yParity: undefined },
545
+ ])('rejects malformed scalars or parity %j', (signature) => {
546
+ const entry = {
547
+ payload: '0x',
548
+ scheme: 'secp256k1',
549
+ signature,
550
+ } as unknown as FrameSignature.Secp256k1
551
+ expect(FrameSignature.validate(entry)).toBe(false)
552
+ expect(() => FrameSignature.from(entry)).toThrow()
553
+ })
554
+ })
@@ -3,6 +3,9 @@ import {
3
3
  Secp256k1,
4
4
  Transaction,
5
5
  TxEnvelopeEip1559,
6
+ Frame,
7
+ FrameSignature,
8
+ TxEnvelopeEip8141,
6
9
  TxEnvelopeLegacy,
7
10
  } from 'ox'
8
11
  import { describe, expect, test } from 'vp/test'
@@ -2046,3 +2049,33 @@ test('exports', () => {
2046
2049
  ]
2047
2050
  `)
2048
2051
  })
2052
+
2053
+ describe('frame transaction RPC', () => {
2054
+ test('preserves pending metadata and large chain IDs without outer signatures', () => {
2055
+ const envelope = TxEnvelopeEip8141.from({
2056
+ chainId: 9007199254740993n,
2057
+ frames: [Frame.from({})],
2058
+ sender: '0x1111111111111111111111111111111111111111',
2059
+ signatures: [FrameSignature.from({ scheme: 'secp256k1' })],
2060
+ })
2061
+ const rpc = {
2062
+ ...TxEnvelopeEip8141.toRpc(envelope),
2063
+ blockHash: null,
2064
+ blockNumber: null,
2065
+ blockTimestamp: null,
2066
+ gas: '0x0',
2067
+ hash: '0x00',
2068
+ input: '0x',
2069
+ to: null,
2070
+ transactionIndex: null,
2071
+ value: '0x0',
2072
+ } satisfies Transaction.Eip8141Rpc<true>
2073
+ const transaction = Transaction.fromRpc(rpc, { pending: true })!
2074
+ expect(transaction.chainId).toBe(9007199254740993n)
2075
+ expect(transaction.blockNumber).toBeNull()
2076
+ expect(transaction.transactionIndex).toBeNull()
2077
+ expect(transaction).not.toHaveProperty('r')
2078
+ expect(transaction).not.toHaveProperty('s')
2079
+ expect(Transaction.toRpc(transaction, { pending: true })).toEqual(rpc)
2080
+ })
2081
+ })
@@ -4,6 +4,7 @@ import {
4
4
  type TxEnvelopeEip2930,
5
5
  type TxEnvelopeEip4844,
6
6
  type TxEnvelopeEip7702,
7
+ type TxEnvelopeEip8141,
7
8
  type TxEnvelopeLegacy,
8
9
  } from 'ox'
9
10
  import { describe, expectTypeOf, test } from 'vp/test'
@@ -180,3 +181,76 @@ describe('toRpc', () => {
180
181
  expectTypeOf(rpc).toEqualTypeOf<TxEnvelopeEip1559.Rpc>()
181
182
  })
182
183
  })
184
+
185
+ describe('eip8141', () => {
186
+ test('preserves constructor literals', () => {
187
+ const envelope = TransactionEnvelope.from({
188
+ chainId: 1n,
189
+ frames: [],
190
+ sender: '0x1111111111111111111111111111111111111111',
191
+ })
192
+ expectTypeOf(envelope).toEqualTypeOf<{
193
+ readonly chainId: 1n
194
+ readonly frames: readonly []
195
+ readonly sender: '0x1111111111111111111111111111111111111111'
196
+ readonly type: 'eip8141'
197
+ }>()
198
+ expectTypeOf(
199
+ TransactionEnvelope.serialize(envelope),
200
+ ).toEqualTypeOf<TxEnvelopeEip8141.Serialized>()
201
+ expectTypeOf(
202
+ TransactionEnvelope.deserialize('0x06c0'),
203
+ ).toEqualTypeOf<TxEnvelopeEip8141.TxEnvelopeEip8141>()
204
+ expectTypeOf(
205
+ TransactionEnvelope.from('0x06c0'),
206
+ ).toEqualTypeOf<TxEnvelopeEip8141.TxEnvelopeEip8141>()
207
+ expectTypeOf(
208
+ TransactionEnvelope.getSerializedType('0x06c0'),
209
+ ).toEqualTypeOf<'eip8141'>()
210
+ expectTypeOf(envelope).toMatchTypeOf<TransactionEnvelope.TxEnvelope<true>>()
211
+ expectTypeOf(
212
+ TransactionEnvelope.toRpc(envelope),
213
+ ).toEqualTypeOf<TxEnvelopeEip8141.Rpc>()
214
+ })
215
+
216
+ test('frames take precedence over blob and fee fields', () => {
217
+ expectTypeOf(
218
+ TransactionEnvelope.getType({
219
+ blobVersionedHashes: [],
220
+ frames: [],
221
+ maxFeePerGas: 1n,
222
+ }),
223
+ ).toEqualTypeOf<'eip8141'>()
224
+ expectTypeOf(
225
+ TransactionEnvelope.getType({ frames: undefined, maxFeePerGas: 1n }),
226
+ ).toEqualTypeOf<'eip1559'>()
227
+ expectTypeOf(
228
+ TransactionEnvelope.getType({ frames: [], type: 'eip1559' }),
229
+ ).toEqualTypeOf<'eip1559'>()
230
+ })
231
+ })
232
+
233
+ test('toRpc accepts implicit frame envelopes', () => {
234
+ const envelope = {
235
+ chainId: 1,
236
+ frames: [{}],
237
+ sender: '0x1111111111111111111111111111111111111111',
238
+ } as const
239
+ expectTypeOf(
240
+ TransactionEnvelope.toRpc(envelope),
241
+ ).toEqualTypeOf<TxEnvelopeEip8141.Rpc>()
242
+ })
243
+
244
+ test('request conversions accept frames and undefined', () => {
245
+ TransactionEnvelope.toRpc({ chainId: 1, frames: undefined, maxFeePerGas: 1n })
246
+ TransactionEnvelope.toTransactionRequest({
247
+ chainId: 1,
248
+ frames: undefined,
249
+ maxFeePerGas: 1n,
250
+ })
251
+ TransactionEnvelope.toTransactionRequest({
252
+ chainId: 1,
253
+ frames: [{}],
254
+ sender: '0x1111111111111111111111111111111111111111',
255
+ })
256
+ })