ox 1.7.4 → 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 (237) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/core/AbiEvent.d.ts.map +1 -1
  3. package/dist/core/AbiEvent.js +10 -9
  4. package/dist/core/AbiEvent.js.map +1 -1
  5. package/dist/core/Blobs.d.ts.map +1 -1
  6. package/dist/core/Blobs.js +6 -2
  7. package/dist/core/Blobs.js.map +1 -1
  8. package/dist/core/Frame.d.ts +290 -0
  9. package/dist/core/Frame.d.ts.map +1 -0
  10. package/dist/core/Frame.js +306 -0
  11. package/dist/core/Frame.js.map +1 -0
  12. package/dist/core/FrameReceipt.d.ts +92 -0
  13. package/dist/core/FrameReceipt.d.ts.map +1 -0
  14. package/dist/core/FrameReceipt.js +72 -0
  15. package/dist/core/FrameReceipt.js.map +1 -0
  16. package/dist/core/FrameSignature.d.ts +357 -0
  17. package/dist/core/FrameSignature.d.ts.map +1 -0
  18. package/dist/core/FrameSignature.js +411 -0
  19. package/dist/core/FrameSignature.js.map +1 -0
  20. package/dist/core/Signature.d.ts +6 -5
  21. package/dist/core/Signature.d.ts.map +1 -1
  22. package/dist/core/Signature.js +17 -7
  23. package/dist/core/Signature.js.map +1 -1
  24. package/dist/core/Siwe.d.ts.map +1 -1
  25. package/dist/core/Siwe.js +21 -1
  26. package/dist/core/Siwe.js.map +1 -1
  27. package/dist/core/Transaction.d.ts +29 -4
  28. package/dist/core/Transaction.d.ts.map +1 -1
  29. package/dist/core/Transaction.js +25 -4
  30. package/dist/core/Transaction.js.map +1 -1
  31. package/dist/core/TransactionReceipt.d.ts +13 -4
  32. package/dist/core/TransactionReceipt.d.ts.map +1 -1
  33. package/dist/core/TransactionReceipt.js +14 -0
  34. package/dist/core/TransactionReceipt.js.map +1 -1
  35. package/dist/core/TransactionRequest.d.ts +15 -7
  36. package/dist/core/TransactionRequest.d.ts.map +1 -1
  37. package/dist/core/TransactionRequest.js +47 -1
  38. package/dist/core/TransactionRequest.js.map +1 -1
  39. package/dist/core/TxEnvelope.d.ts +35 -21
  40. package/dist/core/TxEnvelope.d.ts.map +1 -1
  41. package/dist/core/TxEnvelope.js +46 -6
  42. package/dist/core/TxEnvelope.js.map +1 -1
  43. package/dist/core/TxEnvelopeEip8141.d.ts +341 -0
  44. package/dist/core/TxEnvelopeEip8141.d.ts.map +1 -0
  45. package/dist/core/TxEnvelopeEip8141.js +485 -0
  46. package/dist/core/TxEnvelopeEip8141.js.map +1 -0
  47. package/dist/core/internal/curves.d.ts +2 -0
  48. package/dist/core/internal/curves.d.ts.map +1 -1
  49. package/dist/core/internal/curves.js +2 -0
  50. package/dist/core/internal/curves.js.map +1 -1
  51. package/dist/index.d.ts +73 -0
  52. package/dist/index.d.ts.map +1 -1
  53. package/dist/index.js +73 -0
  54. package/dist/index.js.map +1 -1
  55. package/dist/tempo/KeyAuthorization.d.ts +42 -38
  56. package/dist/tempo/KeyAuthorization.d.ts.map +1 -1
  57. package/dist/tempo/KeyAuthorization.js +24 -4
  58. package/dist/tempo/KeyAuthorization.js.map +1 -1
  59. package/dist/tempo/MultisigConfig.d.ts +18 -15
  60. package/dist/tempo/MultisigConfig.d.ts.map +1 -1
  61. package/dist/tempo/MultisigConfig.js +49 -20
  62. package/dist/tempo/MultisigConfig.js.map +1 -1
  63. package/dist/tempo/MultisigOperation.d.ts +3 -2
  64. package/dist/tempo/MultisigOperation.d.ts.map +1 -1
  65. package/dist/tempo/MultisigOperation.js +20 -100
  66. package/dist/tempo/MultisigOperation.js.map +1 -1
  67. package/dist/tempo/MultisigSimulation.d.ts +2 -49
  68. package/dist/tempo/MultisigSimulation.d.ts.map +1 -1
  69. package/dist/tempo/MultisigSimulation.js +18 -46
  70. package/dist/tempo/MultisigSimulation.js.map +1 -1
  71. package/dist/tempo/SignatureEnvelope.d.ts +72 -140
  72. package/dist/tempo/SignatureEnvelope.d.ts.map +1 -1
  73. package/dist/tempo/SignatureEnvelope.js +130 -181
  74. package/dist/tempo/SignatureEnvelope.js.map +1 -1
  75. package/dist/tempo/Transaction.d.ts +2 -0
  76. package/dist/tempo/Transaction.d.ts.map +1 -1
  77. package/dist/tempo/TransactionReceipt.d.ts +2 -0
  78. package/dist/tempo/TransactionReceipt.d.ts.map +1 -1
  79. package/dist/tempo/TransactionRequest.d.ts +4 -2
  80. package/dist/tempo/TransactionRequest.d.ts.map +1 -1
  81. package/dist/tempo/TransactionRequest.js +6 -1
  82. package/dist/tempo/TransactionRequest.js.map +1 -1
  83. package/dist/tempo/index.d.ts +3 -1
  84. package/dist/tempo/index.d.ts.map +1 -1
  85. package/dist/tempo/index.js +3 -1
  86. package/dist/tempo/index.js.map +1 -1
  87. package/dist/zod/Block.d.ts +123 -0
  88. package/dist/zod/Block.d.ts.map +1 -1
  89. package/dist/zod/Frame.d.ts +73 -0
  90. package/dist/zod/Frame.d.ts.map +1 -0
  91. package/dist/zod/Frame.js +74 -0
  92. package/dist/zod/Frame.js.map +1 -0
  93. package/dist/zod/FrameReceipt.d.ts +50 -0
  94. package/dist/zod/FrameReceipt.d.ts.map +1 -0
  95. package/dist/zod/FrameReceipt.js +37 -0
  96. package/dist/zod/FrameReceipt.js.map +1 -0
  97. package/dist/zod/FrameSignature.d.ts +63 -0
  98. package/dist/zod/FrameSignature.d.ts.map +1 -0
  99. package/dist/zod/FrameSignature.js +66 -0
  100. package/dist/zod/FrameSignature.js.map +1 -0
  101. package/dist/zod/RpcSchema.d.ts +806 -11
  102. package/dist/zod/RpcSchema.d.ts.map +1 -1
  103. package/dist/zod/Transaction.d.ts +252 -0
  104. package/dist/zod/Transaction.d.ts.map +1 -1
  105. package/dist/zod/Transaction.js +54 -0
  106. package/dist/zod/Transaction.js.map +1 -1
  107. package/dist/zod/TransactionEnvelope.d.ts +233 -4
  108. package/dist/zod/TransactionEnvelope.d.ts.map +1 -1
  109. package/dist/zod/TransactionEnvelope.js +9 -0
  110. package/dist/zod/TransactionEnvelope.js.map +1 -1
  111. package/dist/zod/TransactionReceipt.d.ts +48 -0
  112. package/dist/zod/TransactionReceipt.d.ts.map +1 -1
  113. package/dist/zod/TransactionReceipt.js +8 -3
  114. package/dist/zod/TransactionReceipt.js.map +1 -1
  115. package/dist/zod/TransactionRequest.d.ts +50 -2
  116. package/dist/zod/TransactionRequest.d.ts.map +1 -1
  117. package/dist/zod/TransactionRequest.js +10 -4
  118. package/dist/zod/TransactionRequest.js.map +1 -1
  119. package/dist/zod/TxEnvelopeEip8141.d.ts +166 -0
  120. package/dist/zod/TxEnvelopeEip8141.d.ts.map +1 -0
  121. package/dist/zod/TxEnvelopeEip8141.js +76 -0
  122. package/dist/zod/TxEnvelopeEip8141.js.map +1 -0
  123. package/dist/zod/internal/Frame.d.ts +4 -0
  124. package/dist/zod/internal/Frame.d.ts.map +1 -0
  125. package/dist/zod/internal/Frame.js +17 -0
  126. package/dist/zod/internal/Frame.js.map +1 -0
  127. package/dist/zod/internal/rpcSchemas/Eth.d.ts +842 -11
  128. package/dist/zod/internal/rpcSchemas/Eth.d.ts.map +1 -1
  129. package/dist/zod/internal/rpcSchemas/Wallet.d.ts +56 -2
  130. package/dist/zod/internal/rpcSchemas/Wallet.d.ts.map +1 -1
  131. package/dist/zod/tempo/AuthorizationTempo.d.ts +350 -10
  132. package/dist/zod/tempo/AuthorizationTempo.d.ts.map +1 -1
  133. package/dist/zod/tempo/KeyAuthorization.d.ts +242 -422
  134. package/dist/zod/tempo/KeyAuthorization.d.ts.map +1 -1
  135. package/dist/zod/tempo/KeyAuthorization.js +29 -31
  136. package/dist/zod/tempo/KeyAuthorization.js.map +1 -1
  137. package/dist/zod/tempo/MultisigSimulation.d.ts +8 -76
  138. package/dist/zod/tempo/MultisigSimulation.d.ts.map +1 -1
  139. package/dist/zod/tempo/MultisigSimulation.js +3 -37
  140. package/dist/zod/tempo/MultisigSimulation.js.map +1 -1
  141. package/dist/zod/tempo/RpcSchemaTempo.d.ts +424 -228
  142. package/dist/zod/tempo/RpcSchemaTempo.d.ts.map +1 -1
  143. package/dist/zod/tempo/SignatureEnvelope.d.ts +105 -3
  144. package/dist/zod/tempo/SignatureEnvelope.d.ts.map +1 -1
  145. package/dist/zod/tempo/SignatureEnvelope.js +21 -9
  146. package/dist/zod/tempo/SignatureEnvelope.js.map +1 -1
  147. package/dist/zod/tempo/Transaction.d.ts +783 -432
  148. package/dist/zod/tempo/Transaction.d.ts.map +1 -1
  149. package/dist/zod/tempo/TransactionRequest.d.ts +590 -473
  150. package/dist/zod/tempo/TransactionRequest.d.ts.map +1 -1
  151. package/dist/zod/tempo/TransactionRequest.js +8 -0
  152. package/dist/zod/tempo/TransactionRequest.js.map +1 -1
  153. package/dist/zod/tempo/TxEnvelopeTempo.d.ts +327 -213
  154. package/dist/zod/tempo/TxEnvelopeTempo.d.ts.map +1 -1
  155. package/dist/zod/tempo/ZoneRpcAuthentication.d.ts +105 -3
  156. package/dist/zod/tempo/ZoneRpcAuthentication.d.ts.map +1 -1
  157. package/dist/zod/z.d.ts +4 -0
  158. package/dist/zod/z.d.ts.map +1 -1
  159. package/dist/zod/z.js +4 -0
  160. package/dist/zod/z.js.map +1 -1
  161. package/package.json +41 -1
  162. package/src/core/AbiEvent.ts +11 -9
  163. package/src/core/Blobs.ts +6 -2
  164. package/src/core/Frame.ts +443 -0
  165. package/src/core/FrameReceipt.ts +117 -0
  166. package/src/core/FrameSignature.ts +593 -0
  167. package/src/core/Signature.ts +41 -11
  168. package/src/core/Siwe.ts +20 -1
  169. package/src/core/Transaction.ts +82 -5
  170. package/src/core/TransactionReceipt.ts +41 -3
  171. package/src/core/TransactionRequest.ts +71 -4
  172. package/src/core/TxEnvelope.ts +159 -90
  173. package/src/core/TxEnvelopeEip8141.ts +715 -0
  174. package/src/core/_test/AbiEvent.test.ts +45 -0
  175. package/src/core/_test/Blobs.test.ts +22 -0
  176. package/src/core/_test/Frame.test-d.ts +96 -0
  177. package/src/core/_test/Frame.test.ts +267 -0
  178. package/src/core/_test/FrameReceipt.test.ts +86 -0
  179. package/src/core/_test/FrameSignature.test-d.ts +93 -0
  180. package/src/core/_test/FrameSignature.test.ts +554 -0
  181. package/src/core/_test/Signature.test.ts +166 -0
  182. package/src/core/_test/Siwe.test.ts +100 -1
  183. package/src/core/_test/Transaction.test.ts +33 -0
  184. package/src/core/_test/TransactionEnvelope.test-d.ts +74 -0
  185. package/src/core/_test/TransactionEnvelope.test.ts +107 -0
  186. package/src/core/_test/TransactionReceipt.test.ts +59 -0
  187. package/src/core/_test/TransactionRequest.test-d.ts +50 -0
  188. package/src/core/_test/TransactionRequest.test.ts +88 -1
  189. package/src/core/_test/TxEnvelopeEip8141.test-d.ts +44 -0
  190. package/src/core/_test/TxEnvelopeEip8141.test.ts +1775 -0
  191. package/src/core/_test/curves.test.ts +6 -1
  192. package/src/core/_test/index.test.ts +4 -0
  193. package/src/core/internal/curves.ts +4 -0
  194. package/src/index.ts +77 -0
  195. package/src/tempo/AuthorizationTempo.test.ts +8 -4
  196. package/src/tempo/KeyAuthorization.test-d.ts +16 -63
  197. package/src/tempo/KeyAuthorization.test.ts +69 -11
  198. package/src/tempo/KeyAuthorization.ts +107 -63
  199. package/src/tempo/MultisigConfig.test.ts +44 -20
  200. package/src/tempo/MultisigConfig.ts +65 -23
  201. package/src/tempo/MultisigOperation.test.ts +79 -550
  202. package/src/tempo/MultisigOperation.ts +31 -146
  203. package/src/tempo/MultisigSimulation.test-d.ts +2 -2
  204. package/src/tempo/MultisigSimulation.test.ts +21 -110
  205. package/src/tempo/MultisigSimulation.ts +25 -99
  206. package/src/tempo/SignatureEnvelope.test-d.ts +29 -47
  207. package/src/tempo/SignatureEnvelope.test.ts +328 -541
  208. package/src/tempo/SignatureEnvelope.ts +196 -291
  209. package/src/tempo/Transaction.test.ts +4 -4
  210. package/src/tempo/TransactionRequest.test.ts +17 -80
  211. package/src/tempo/TransactionRequest.ts +19 -2
  212. package/src/tempo/index.ts +3 -1
  213. package/src/tempo/multisig.e2e.test.ts +321 -860
  214. package/src/version.ts +1 -1
  215. package/src/zod/Frame.ts +82 -0
  216. package/src/zod/FrameReceipt.ts +48 -0
  217. package/src/zod/FrameSignature.ts +78 -0
  218. package/src/zod/Transaction.ts +63 -0
  219. package/src/zod/TransactionEnvelope.ts +33 -0
  220. package/src/zod/TransactionReceipt.ts +14 -3
  221. package/src/zod/TransactionRequest.ts +18 -3
  222. package/src/zod/TxEnvelopeEip8141.ts +102 -0
  223. package/src/zod/_test/Frame.test-d.ts +41 -0
  224. package/src/zod/_test/Frame.test.ts +54 -0
  225. package/src/zod/_test/FrameReceipt.test.ts +75 -0
  226. package/src/zod/_test/FrameSignature.test.ts +94 -0
  227. package/src/zod/_test/TxEnvelopeEip8141.test.ts +116 -0
  228. package/src/zod/internal/Frame.ts +26 -0
  229. package/src/zod/tempo/KeyAuthorization.ts +50 -39
  230. package/src/zod/tempo/MultisigSimulation.ts +3 -48
  231. package/src/zod/tempo/SignatureEnvelope.ts +46 -35
  232. package/src/zod/tempo/TransactionRequest.ts +14 -0
  233. package/src/zod/tempo/_test/KeyAuthorization.test.ts +69 -20
  234. package/src/zod/tempo/_test/MultisigSimulation.test.ts +6 -31
  235. package/src/zod/tempo/_test/SignatureEnvelope.test.ts +118 -85
  236. package/src/zod/tempo/_test/TransactionRequest.test.ts +9 -19
  237. package/src/zod/z.ts +4 -0
@@ -0,0 +1,1775 @@
1
+ import {
2
+ Address,
3
+ Blobs,
4
+ Frame,
5
+ FrameSignature,
6
+ Hex,
7
+ P256,
8
+ Rlp,
9
+ RpcTransport,
10
+ Secp256k1,
11
+ Transaction,
12
+ TransactionEnvelope,
13
+ TransactionReceipt,
14
+ TransactionRequest,
15
+ TxEnvelopeEip8141,
16
+ } from 'ox'
17
+ import { z } from 'ox/zod'
18
+ import { describe, expect, test } from 'vp/test'
19
+ import { accounts } from '../../../test/constants/accounts.js'
20
+ import { rpcUrl } from '../../../test/frames/prool.js'
21
+
22
+ const rpc = RpcTransport.fromHttp(rpcUrl)
23
+
24
+ test('exports', () => {
25
+ expect(Object.keys(TxEnvelopeEip8141).sort()).toMatchInlineSnapshot(`
26
+ [
27
+ "InvalidError",
28
+ "assert",
29
+ "deserialize",
30
+ "from",
31
+ "fromRpc",
32
+ "getSignPayload",
33
+ "hash",
34
+ "serialize",
35
+ "serializedType",
36
+ "toRpc",
37
+ "type",
38
+ "validate",
39
+ ]
40
+ `)
41
+ })
42
+
43
+ describe('assert', () => {
44
+ test('negative chainId', () => {
45
+ expect(() =>
46
+ TxEnvelopeEip8141.assert({
47
+ chainId: -1,
48
+ frames: [{}],
49
+ sender: accounts[0].address,
50
+ }),
51
+ ).toThrowErrorMatchingInlineSnapshot(
52
+ `[TxEnvelopeEip8141.InvalidError: chainId must be an unsigned 256-bit integer; use bigint for large IDs.]`,
53
+ )
54
+ })
55
+
56
+ test('fractional chainId', () => {
57
+ expect(() =>
58
+ TxEnvelopeEip8141.assert({
59
+ chainId: 1.5,
60
+ frames: [{}],
61
+ sender: accounts[0].address,
62
+ }),
63
+ ).toThrowErrorMatchingInlineSnapshot(
64
+ `[TxEnvelopeEip8141.InvalidError: chainId must be an unsigned 256-bit integer; use bigint for large IDs.]`,
65
+ )
66
+ })
67
+
68
+ test('unsafe numeric chainId', () => {
69
+ expect(() =>
70
+ TxEnvelopeEip8141.assert({
71
+ chainId: Number.MAX_SAFE_INTEGER + 1,
72
+ frames: [{}],
73
+ sender: accounts[0].address,
74
+ }),
75
+ ).toThrowErrorMatchingInlineSnapshot(
76
+ `[TxEnvelopeEip8141.InvalidError: chainId must be an unsigned 256-bit integer; use bigint for large IDs.]`,
77
+ )
78
+ })
79
+
80
+ test('chainId exceeds 256 bits', () => {
81
+ expect(() =>
82
+ TxEnvelopeEip8141.assert({
83
+ chainId: 2n ** 256n,
84
+ frames: [{}],
85
+ sender: accounts[0].address,
86
+ }),
87
+ ).toThrowErrorMatchingInlineSnapshot(
88
+ `[TxEnvelopeEip8141.InvalidError: chainId must be an unsigned 256-bit integer; use bigint for large IDs.]`,
89
+ )
90
+ })
91
+
92
+ test('negative nonce', () => {
93
+ expect(() =>
94
+ TxEnvelopeEip8141.assert({
95
+ chainId: 1,
96
+ frames: [{}],
97
+ nonce: -1n,
98
+ sender: accounts[0].address,
99
+ }),
100
+ ).toThrowErrorMatchingInlineSnapshot(
101
+ `[TxEnvelopeEip8141.InvalidError: nonce must be an unsigned 64-bit integer.]`,
102
+ )
103
+ })
104
+
105
+ test('nonce exceeds 64 bits', () => {
106
+ expect(() =>
107
+ TxEnvelopeEip8141.assert({
108
+ chainId: 1,
109
+ frames: [{}],
110
+ nonce: 2n ** 64n,
111
+ sender: accounts[0].address,
112
+ }),
113
+ ).toThrowErrorMatchingInlineSnapshot(
114
+ `[TxEnvelopeEip8141.InvalidError: nonce must be an unsigned 64-bit integer.]`,
115
+ )
116
+ })
117
+
118
+ test('null nonce', () => {
119
+ expect(() =>
120
+ TxEnvelopeEip8141.assert({
121
+ chainId: 1,
122
+ frames: [{}],
123
+ nonce: null,
124
+ sender: accounts[0].address,
125
+ } as unknown as TxEnvelopeEip8141.TxEnvelopeEip8141),
126
+ ).toThrowErrorMatchingInlineSnapshot(
127
+ `[TxEnvelopeEip8141.InvalidError: nonce must be an unsigned 64-bit integer.]`,
128
+ )
129
+ })
130
+
131
+ test('negative fee cap', () => {
132
+ expect(() =>
133
+ TxEnvelopeEip8141.assert({
134
+ chainId: 1,
135
+ frames: [{}],
136
+ maxFeePerGas: -1n,
137
+ sender: accounts[0].address,
138
+ }),
139
+ ).toThrowErrorMatchingInlineSnapshot(
140
+ `[TxEnvelopeEip8141.InvalidError: maxFeePerGas must be an unsigned 256-bit integer.]`,
141
+ )
142
+ })
143
+
144
+ test('fee cap exceeds 256 bits', () => {
145
+ expect(() =>
146
+ TxEnvelopeEip8141.assert({
147
+ chainId: 1,
148
+ frames: [{}],
149
+ maxFeePerGas: 2n ** 256n,
150
+ sender: accounts[0].address,
151
+ }),
152
+ ).toThrowErrorMatchingInlineSnapshot(
153
+ `[TxEnvelopeEip8141.InvalidError: maxFeePerGas must be an unsigned 256-bit integer.]`,
154
+ )
155
+ })
156
+
157
+ test('tip above fee cap', () => {
158
+ expect(() =>
159
+ TxEnvelopeEip8141.assert({
160
+ chainId: 1,
161
+ frames: [{}],
162
+ maxPriorityFeePerGas: 1n,
163
+ sender: accounts[0].address,
164
+ }),
165
+ ).toThrowErrorMatchingInlineSnapshot(
166
+ `[TxEnvelopeEip8141.InvalidError: maxPriorityFeePerGas exceeds maxFeePerGas.]`,
167
+ )
168
+ })
169
+
170
+ test('blob fee without blobs', () => {
171
+ expect(() =>
172
+ TxEnvelopeEip8141.assert({
173
+ chainId: 1,
174
+ frames: [{}],
175
+ maxFeePerBlobGas: 1n,
176
+ sender: accounts[0].address,
177
+ }),
178
+ ).toThrowErrorMatchingInlineSnapshot(
179
+ `[TxEnvelopeEip8141.InvalidError: maxFeePerBlobGas must be zero without blobs.]`,
180
+ )
181
+ })
182
+
183
+ test('rejects a half-byte versioned hash', () => {
184
+ expect(() =>
185
+ TxEnvelopeEip8141.from({
186
+ blobVersionedHashes: [`0x01${'0'.repeat(61)}`],
187
+ chainId: 1,
188
+ frames: [{}],
189
+ sender: accounts[0].address,
190
+ }),
191
+ ).toThrow('Blob versioned hashes must contain 32 bytes and version 0x01.')
192
+ })
193
+
194
+ test('invalid blob hash size', () => {
195
+ expect(() =>
196
+ TxEnvelopeEip8141.assert({
197
+ blobVersionedHashes: ['0x01'],
198
+ chainId: 1,
199
+ frames: [{}],
200
+ sender: accounts[0].address,
201
+ }),
202
+ ).toThrowErrorMatchingInlineSnapshot(
203
+ `[TxEnvelopeEip8141.InvalidError: Blob versioned hashes must contain 32 bytes and version 0x01.]`,
204
+ )
205
+ })
206
+
207
+ test('invalid blob hash version', () => {
208
+ expect(() =>
209
+ TxEnvelopeEip8141.assert({
210
+ blobVersionedHashes: [`0x02${'00'.repeat(31)}`],
211
+ chainId: 1,
212
+ frames: [{}],
213
+ sender: accounts[0].address,
214
+ }),
215
+ ).toThrowErrorMatchingInlineSnapshot(
216
+ `[TxEnvelopeEip8141.InvalidError: Blob versioned hashes must contain 32 bytes and version 0x01.]`,
217
+ )
218
+ })
219
+
220
+ test('empty frames', () => {
221
+ expect(() =>
222
+ TxEnvelopeEip8141.assert({
223
+ chainId: 1,
224
+ frames: [],
225
+ sender: accounts[0].address,
226
+ }),
227
+ ).toThrowErrorMatchingInlineSnapshot(
228
+ `[TxEnvelopeEip8141.InvalidError: Expected between 1 and 64 frames.]`,
229
+ )
230
+ })
231
+
232
+ test('too many frames', () => {
233
+ expect(() =>
234
+ TxEnvelopeEip8141.assert({
235
+ chainId: 1,
236
+ frames: Array.from({ length: 65 }, () => ({})),
237
+ sender: accounts[0].address,
238
+ }),
239
+ ).toThrowErrorMatchingInlineSnapshot(
240
+ `[TxEnvelopeEip8141.InvalidError: Expected between 1 and 64 frames.]`,
241
+ )
242
+ })
243
+
244
+ test('combined gas exceeds 64 bits', () => {
245
+ expect(() =>
246
+ TxEnvelopeEip8141.assert({
247
+ chainId: 1,
248
+ frames: [{ gas: 2n ** 64n - 1n }, { stateGas: 1n }],
249
+ sender: accounts[0].address,
250
+ }),
251
+ ).toThrowErrorMatchingInlineSnapshot(
252
+ `[TxEnvelopeEip8141.InvalidError: Combined frame gas must be less than 2^64.]`,
253
+ )
254
+ })
255
+
256
+ test('execution gas exceeds transaction cap', () => {
257
+ expect(() =>
258
+ TxEnvelopeEip8141.assert({
259
+ chainId: 1,
260
+ frames: [{ gas: 16_777_216n }],
261
+ sender: accounts[0].address,
262
+ }),
263
+ ).toThrowErrorMatchingInlineSnapshot(
264
+ `[TxEnvelopeEip8141.InvalidError: Transaction execution gas exceeds 16777216.]`,
265
+ )
266
+ })
267
+
268
+ test('execution approval targets another account', () => {
269
+ expect(() =>
270
+ TxEnvelopeEip8141.assert({
271
+ chainId: 1,
272
+ frames: [{ flags: 'approveExecution', target: accounts[1].address }],
273
+ sender: accounts[0].address,
274
+ }),
275
+ ).toThrowErrorMatchingInlineSnapshot(
276
+ `[TxEnvelopeEip8141.InvalidError: Execution approval must target the sender.]`,
277
+ )
278
+ })
279
+
280
+ test('atomic batch without a following frame', () => {
281
+ expect(() =>
282
+ TxEnvelopeEip8141.assert({
283
+ chainId: 1,
284
+ frames: [{ flags: 'atomicBatch' }],
285
+ sender: accounts[0].address,
286
+ }),
287
+ ).toThrowErrorMatchingInlineSnapshot(
288
+ `[TxEnvelopeEip8141.InvalidError: An atomic batch requires a following non-verify frame.]`,
289
+ )
290
+ })
291
+
292
+ test('atomic batch followed by verification', () => {
293
+ expect(() =>
294
+ TxEnvelopeEip8141.assert({
295
+ chainId: 1,
296
+ frames: [{ flags: 'atomicBatch' }, { mode: 'verify' }],
297
+ sender: accounts[0].address,
298
+ }),
299
+ ).toThrowErrorMatchingInlineSnapshot(
300
+ `[TxEnvelopeEip8141.InvalidError: An atomic batch requires a following non-verify frame.]`,
301
+ )
302
+ })
303
+
304
+ test('approval inside an atomic batch', () => {
305
+ expect(() =>
306
+ TxEnvelopeEip8141.assert({
307
+ chainId: 1,
308
+ frames: [{ flags: 'atomicBatch' }, { flags: 'approvePayment' }],
309
+ sender: accounts[0].address,
310
+ }),
311
+ ).toThrowErrorMatchingInlineSnapshot(
312
+ `[TxEnvelopeEip8141.InvalidError: Atomic batches cannot contain approval frames.]`,
313
+ )
314
+ })
315
+
316
+ test('invalid expiry calldata', () => {
317
+ expect(() =>
318
+ TxEnvelopeEip8141.assert({
319
+ chainId: 1,
320
+ frames: [
321
+ {
322
+ mode: 'verify',
323
+ target: '0x0000000000000000000000000000000000008141',
324
+ },
325
+ ],
326
+ sender: accounts[0].address,
327
+ }),
328
+ ).toThrowErrorMatchingInlineSnapshot(
329
+ `[TxEnvelopeEip8141.InvalidError: Invalid or duplicate expiry verifier frame.]`,
330
+ )
331
+ })
332
+
333
+ test('invalid sender', () => {
334
+ expect(() =>
335
+ TxEnvelopeEip8141.assert({ chainId: 1, frames: [{}], sender: '0x' }),
336
+ ).toThrowErrorMatchingInlineSnapshot(`
337
+ [Address.InvalidAddressError: Address "0x" is invalid.
338
+
339
+ Details: Address is not a 20 byte (40 hexadecimal character) value.]
340
+ `)
341
+ })
342
+ })
343
+
344
+ describe('deserialize', () => {
345
+ test('default', () => {
346
+ expect(
347
+ TxEnvelopeEip8141.deserialize(
348
+ '0x06e70180941111111111111111111111111111111111111111c9c8808080c280808080c0c3808080c0',
349
+ ),
350
+ ).toMatchInlineSnapshot(`
351
+ {
352
+ "blobVersionedHashes": [],
353
+ "chainId": 1,
354
+ "frames": [
355
+ {
356
+ "data": "0x",
357
+ "flags": 0,
358
+ "gas": 0n,
359
+ "mode": 0,
360
+ "stateGas": 0n,
361
+ "value": 0n,
362
+ },
363
+ ],
364
+ "maxFeePerBlobGas": 0n,
365
+ "maxFeePerGas": 0n,
366
+ "maxPriorityFeePerGas": 0n,
367
+ "nonce": 0n,
368
+ "sender": "0x1111111111111111111111111111111111111111",
369
+ "signatures": [],
370
+ "type": "eip8141",
371
+ }
372
+ `)
373
+ })
374
+
375
+ test('unsigned signatures and large chainId', () => {
376
+ const envelope = { chainId: 1, frames: [{}], sender: accounts[0].address }
377
+
378
+ const input = {
379
+ ...envelope,
380
+ chainId: 2n ** 256n - 1n,
381
+ signatures: [
382
+ FrameSignature.from({ scheme: 'p256' }),
383
+ FrameSignature.from({ scheme: 'secp256k1' }),
384
+ ],
385
+ }
386
+ const decoded = TxEnvelopeEip8141.deserialize(
387
+ TxEnvelopeEip8141.serialize(input),
388
+ )
389
+ expect(decoded.chainId).toBe(input.chainId)
390
+ expect(decoded.signatures).toEqual(input.signatures)
391
+ })
392
+
393
+ test('missing body', () => {
394
+ expect(() =>
395
+ TxEnvelopeEip8141.deserialize('0x06' as TxEnvelopeEip8141.Serialized),
396
+ ).toThrowErrorMatchingInlineSnapshot(
397
+ `[Hex.SliceOffsetOutOfBoundsError: Slice starting at offset \`1\` is out-of-bounds (size: \`1\`).]`,
398
+ )
399
+ })
400
+
401
+ test('empty list', () => {
402
+ expect(() =>
403
+ TxEnvelopeEip8141.deserialize('0x06c0' as TxEnvelopeEip8141.Serialized),
404
+ ).toThrowErrorMatchingInlineSnapshot(
405
+ `[TxEnvelopeEip8141.InvalidError: Expected seven transaction fields.]`,
406
+ )
407
+ })
408
+
409
+ test('truncated scalar', () => {
410
+ expect(() =>
411
+ TxEnvelopeEip8141.deserialize('0x06a0' as TxEnvelopeEip8141.Serialized),
412
+ ).toThrowErrorMatchingInlineSnapshot(
413
+ `[Cursor.PositionOutOfBoundsError: Position \`32\` is out of bounds (\`0 < position < 1\`).]`,
414
+ )
415
+ })
416
+
417
+ test('incorrect type', () => {
418
+ expect(() =>
419
+ TxEnvelopeEip8141.deserialize('0x05c0' as TxEnvelopeEip8141.Serialized),
420
+ ).toThrowErrorMatchingInlineSnapshot(
421
+ `[TxEnvelopeEip8141.InvalidError: Expected transaction type 0x06.]`,
422
+ )
423
+ })
424
+
425
+ test('missing fields', () => {
426
+ expect(() =>
427
+ TxEnvelopeEip8141.deserialize('0x06c180' as TxEnvelopeEip8141.Serialized),
428
+ ).toThrowErrorMatchingInlineSnapshot(
429
+ `[TxEnvelopeEip8141.InvalidError: Expected seven transaction fields.]`,
430
+ )
431
+ })
432
+
433
+ test('nonminimal integers', () => {
434
+ const serialized =
435
+ '0x06e70180941111111111111111111111111111111111111111c9c8808080c280808080c0c3808080c0'
436
+ const fields = Rlp.toHex(Hex.slice(serialized, 1)) as Hex.Hex[]
437
+ fields[0] = '0x0001'
438
+ expect(() =>
439
+ TxEnvelopeEip8141.deserialize(
440
+ Hex.concat('0x06', Rlp.fromHex(fields)) as TxEnvelopeEip8141.Serialized,
441
+ ),
442
+ ).toThrowErrorMatchingInlineSnapshot(
443
+ `[TxEnvelopeEip8141.InvalidError: Expected a minimally encoded integer.]`,
444
+ )
445
+ })
446
+ })
447
+
448
+ describe('from', () => {
449
+ test('default', () => {
450
+ expect(
451
+ TxEnvelopeEip8141.from({
452
+ chainId: 1,
453
+ frames: [{ gas: 50000n }],
454
+ sender: accounts[0].address,
455
+ }),
456
+ ).toMatchInlineSnapshot(`
457
+ {
458
+ "chainId": 1,
459
+ "frames": [
460
+ {
461
+ "gas": 50000n,
462
+ },
463
+ ],
464
+ "sender": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
465
+ "type": "eip8141",
466
+ }
467
+ `)
468
+ })
469
+
470
+ test('serialized', () => {
471
+ expect(
472
+ TxEnvelopeEip8141.from(
473
+ '0x06e70180941111111111111111111111111111111111111111c9c8808080c280808080c0c3808080c0',
474
+ ),
475
+ ).toMatchInlineSnapshot(`
476
+ {
477
+ "blobVersionedHashes": [],
478
+ "chainId": 1,
479
+ "frames": [
480
+ {
481
+ "data": "0x",
482
+ "flags": 0,
483
+ "gas": 0n,
484
+ "mode": 0,
485
+ "stateGas": 0n,
486
+ "value": 0n,
487
+ },
488
+ ],
489
+ "maxFeePerBlobGas": 0n,
490
+ "maxFeePerGas": 0n,
491
+ "maxPriorityFeePerGas": 0n,
492
+ "nonce": 0n,
493
+ "sender": "0x1111111111111111111111111111111111111111",
494
+ "signatures": [],
495
+ "type": "eip8141",
496
+ }
497
+ `)
498
+ })
499
+ })
500
+
501
+ describe('getSignPayload', () => {
502
+ test('omitted nested defaults preserve the signing hash and encoding', () => {
503
+ const envelope = TxEnvelopeEip8141.from({
504
+ chainId: 1,
505
+ frames: [{ gas: 50_000n, mode: 'sender' }],
506
+ sender: accounts[0].address,
507
+ signatures: [{ scheme: 'secp256k1' }],
508
+ })
509
+ const explicit = TxEnvelopeEip8141.from({
510
+ ...envelope,
511
+ frames: [Frame.from({ gas: 50_000n, mode: 'sender' })],
512
+ signatures: [FrameSignature.from({ scheme: 'secp256k1' })],
513
+ })
514
+ expect(TxEnvelopeEip8141.getSignPayload(envelope)).toBe(
515
+ TxEnvelopeEip8141.getSignPayload(explicit),
516
+ )
517
+ expect(TxEnvelopeEip8141.serialize(envelope)).toBe(
518
+ TxEnvelopeEip8141.serialize(explicit),
519
+ )
520
+ })
521
+
522
+ test('default', () => {
523
+ const envelope = {
524
+ chainId: 1,
525
+ frames: [{}],
526
+ sender: '0x1111111111111111111111111111111111111111' as const,
527
+ }
528
+ expect(TxEnvelopeEip8141.getSignPayload(envelope)).toMatchInlineSnapshot(
529
+ '"0xa795b6de44696d46c096aaa3d6401d6eaa21b9b639408c82be7e9e60358e4387"',
530
+ )
531
+ })
532
+
533
+ test('elides only canonical-payload signature bytes without mutation', () => {
534
+ const envelope = {
535
+ chainId: 1,
536
+ frames: [{}],
537
+ sender: '0x1111111111111111111111111111111111111111' as const,
538
+ }
539
+
540
+ const canonical = FrameSignature.from('0xaabb')
541
+ const explicit = FrameSignature.from({
542
+ payload: `0x${'ab'.repeat(32)}`,
543
+ signature: '0xccdd',
544
+ })
545
+ const input = { ...envelope, signatures: [canonical, explicit] }
546
+ expect(TxEnvelopeEip8141.hash(input)).toBe(
547
+ '0xee871bbb2466152f79e357e1cfcbdd71b1587c5b3a9060d9cd91545c1a3a5220',
548
+ )
549
+ const original = structuredClone(input)
550
+ const expected =
551
+ '0x85ec4544ddb227c5425add4b606f71ddfc0852100d14066d0061a1b092bad6d3'
552
+ expect(TxEnvelopeEip8141.getSignPayload(input)).toBe(expected)
553
+ expect(TxEnvelopeEip8141.hash(input, { presign: true })).toBe(expected)
554
+ expect(TxEnvelopeEip8141.hash(input)).not.toBe(expected)
555
+ expect(input).toEqual(original)
556
+ expect(
557
+ TxEnvelopeEip8141.getSignPayload({
558
+ ...input,
559
+ signatures: [
560
+ canonical,
561
+ FrameSignature.from({ ...explicit, signature: '0xee' }),
562
+ ],
563
+ }),
564
+ ).not.toBe(expected)
565
+ })
566
+
567
+ test('signing digest remains stable after attaching a protocol signature', () => {
568
+ const envelope = { chainId: 1, frames: [{}], sender: accounts[0].address }
569
+
570
+ const input = {
571
+ ...envelope,
572
+ signatures: [FrameSignature.from({ scheme: 'secp256k1' })],
573
+ }
574
+ const payload = TxEnvelopeEip8141.getSignPayload(input)
575
+ const signature = Secp256k1.sign({
576
+ payload,
577
+ privateKey: accounts[0].privateKey,
578
+ })
579
+ const signed = {
580
+ ...input,
581
+ signatures: [FrameSignature.from({ scheme: 'secp256k1', signature })],
582
+ }
583
+ expect(TxEnvelopeEip8141.getSignPayload(signed)).toBe(payload)
584
+ expect(
585
+ TxEnvelopeEip8141.deserialize(TxEnvelopeEip8141.serialize(signed))
586
+ .signatures,
587
+ ).toEqual(signed.signatures)
588
+ })
589
+ })
590
+
591
+ describe('hash', () => {
592
+ test('default', () => {
593
+ const envelope = {
594
+ chainId: 1,
595
+ frames: [{}],
596
+ sender: '0x1111111111111111111111111111111111111111' as const,
597
+ }
598
+ expect(TxEnvelopeEip8141.hash(envelope)).toMatchInlineSnapshot(
599
+ '"0xa795b6de44696d46c096aaa3d6401d6eaa21b9b639408c82be7e9e60358e4387"',
600
+ )
601
+ })
602
+
603
+ test('presign', () => {
604
+ const envelope = {
605
+ chainId: 1,
606
+ frames: [{}],
607
+ sender: '0x1111111111111111111111111111111111111111' as const,
608
+ }
609
+ expect(
610
+ TxEnvelopeEip8141.hash(envelope, { presign: true }),
611
+ ).toMatchInlineSnapshot(
612
+ '"0xa795b6de44696d46c096aaa3d6401d6eaa21b9b639408c82be7e9e60358e4387"',
613
+ )
614
+ })
615
+ })
616
+
617
+ describe('serialize', () => {
618
+ describe('e2e', () => {
619
+ test('fills, signs, and mines a frame transaction', async () => {
620
+ const sender = accounts[0].address
621
+ const target = accounts[1].address
622
+ const balance = await rpc.request({
623
+ method: 'eth_getBalance',
624
+ params: [target, 'latest'],
625
+ })
626
+ const nonce = await rpc.request({
627
+ method: 'eth_getTransactionCount',
628
+ params: [sender, 'pending'],
629
+ })
630
+ const request = TransactionRequest.toRpc({
631
+ frames: [
632
+ Frame.from({
633
+ flags: 'approveExecutionAndPayment',
634
+ gas: 50_000n,
635
+ mode: 'verify',
636
+ }),
637
+ Frame.from({ gas: 50_000n, mode: 'sender', target, value: 1n }),
638
+ ],
639
+ from: sender,
640
+ signatures: [{ scheme: 'secp256k1' }],
641
+ // Nethermind's simulation mapping requires an outer recipient before processing frames.
642
+ to: sender,
643
+ type: 'eip8141',
644
+ })
645
+ // TODO: remove once migrated to reth or anvil.
646
+ // Nethermind validates signatures when filling gas, before the transaction can be signed.
647
+ await expect(
648
+ rpc.request({ method: 'eth_fillTransaction', params: [request] }),
649
+ ).rejects.toThrow('frame transaction signature has the wrong length')
650
+ const { tx } = await rpc.request({
651
+ method: 'eth_fillTransaction',
652
+ params: [{ ...request, gas: Hex.fromNumber(100_000n) }],
653
+ })
654
+ expect(tx.type).toBe('0x6')
655
+ if (tx.type !== '0x6' || !tx.frames)
656
+ throw new Error('Expected a frame transaction')
657
+ expect(tx.chainId).toBe('0x1fcd')
658
+ expect(tx.nonce).toBe(nonce)
659
+ expect(tx.frames).toEqual(request.frames)
660
+ expect(tx.signatures).toEqual(request.signatures)
661
+ expect(Hex.toBigInt(tx.gas)).toBe(100_000n)
662
+ expect(Hex.toBigInt(tx.maxFeePerGas)).toBeGreaterThan(0n)
663
+ expect(Hex.toBigInt(tx.maxPriorityFeePerGas)).toBeGreaterThanOrEqual(0n)
664
+ expect(Hex.toBigInt(tx.maxFeePerGas)).toBeGreaterThanOrEqual(
665
+ Hex.toBigInt(tx.maxPriorityFeePerGas),
666
+ )
667
+ expect(
668
+ await rpc.request({
669
+ method: 'eth_getTransactionCount',
670
+ params: [sender, 'pending'],
671
+ }),
672
+ ).toBe(nonce)
673
+ expect(
674
+ await rpc.request({
675
+ method: 'eth_getBalance',
676
+ params: [target, 'latest'],
677
+ }),
678
+ ).toBe(balance)
679
+
680
+ const envelope = TxEnvelopeEip8141.fromRpc(tx)
681
+ const signature = Secp256k1.sign({
682
+ payload: TxEnvelopeEip8141.getSignPayload(envelope),
683
+ privateKey: accounts[0].privateKey,
684
+ })
685
+ const signed = TxEnvelopeEip8141.from({
686
+ ...envelope,
687
+ signatures: [{ scheme: 'secp256k1', signature }],
688
+ })
689
+ const hash = await rpc.request({
690
+ method: 'eth_sendRawTransaction',
691
+ params: [TxEnvelopeEip8141.serialize(signed)],
692
+ })
693
+ expect(hash).toBe(TxEnvelopeEip8141.hash(signed))
694
+ await expect
695
+ .poll(
696
+ () =>
697
+ rpc.request({
698
+ method: 'eth_getTransactionReceipt',
699
+ params: [hash],
700
+ }),
701
+ { timeout: 30_000 },
702
+ )
703
+ .toMatchObject({ status: '0x1' })
704
+ expect(
705
+ Hex.toBigInt(
706
+ await rpc.request({
707
+ method: 'eth_getBalance',
708
+ params: [target, 'latest'],
709
+ }),
710
+ ),
711
+ ).toBe(Hex.toBigInt(balance) + 1n)
712
+ })
713
+
714
+ test.each(['eth_call', 'eth_estimateGas'] as const)(
715
+ '%s handles frame transactions without changing state',
716
+ async (method) => {
717
+ const sender = accounts[0].address
718
+ const target = accounts[1].address
719
+ const balance = await rpc.request({
720
+ method: 'eth_getBalance',
721
+ params: [target, 'latest'],
722
+ })
723
+ const nonce = await rpc.request({
724
+ method: 'eth_getTransactionCount',
725
+ params: [sender, 'latest'],
726
+ })
727
+ const envelope = TxEnvelopeEip8141.from({
728
+ chainId: 8141,
729
+ frames: [
730
+ Frame.from({
731
+ flags: 'approveExecutionAndPayment',
732
+ gas: 50_000n,
733
+ mode: 'verify',
734
+ }),
735
+ Frame.from({ gas: 50_000n, mode: 'sender', target, value: 1n }),
736
+ Frame.from({
737
+ data: '0xdeadbeef',
738
+ gas: 50_000n,
739
+ mode: 'sender',
740
+ target: '0x0000000000000000000000000000000000000004',
741
+ }),
742
+ ],
743
+ maxFeePerGas: 10_000_000_000n,
744
+ maxPriorityFeePerGas: 1_000_000_000n,
745
+ nonce: Hex.toBigInt(nonce),
746
+ sender,
747
+ signatures: [FrameSignature.from({ scheme: 'secp256k1' })],
748
+ })
749
+ const signature = Secp256k1.sign({
750
+ payload: TxEnvelopeEip8141.getSignPayload(envelope),
751
+ privateKey: accounts[0].privateKey,
752
+ })
753
+ const signed = TxEnvelopeEip8141.from({
754
+ ...envelope,
755
+ signatures: [FrameSignature.from({ scheme: 'secp256k1', signature })],
756
+ })
757
+ const request = TransactionRequest.toRpc({
758
+ ...TransactionEnvelope.toTransactionRequest(signed),
759
+ // Nethermind's simulation mapping requires an outer recipient before processing frames.
760
+ to: sender,
761
+ })
762
+ const result = await rpc.request({
763
+ method,
764
+ params: [request, 'latest'],
765
+ })
766
+ // Frame return data is not exposed by Nethermind's transaction-level call result.
767
+ if (method === 'eth_call') expect(result).toBe('0x')
768
+ else {
769
+ // Estimation includes all signed frame budgets plus intrinsic gas.
770
+ expect(Hex.toBigInt(result)).toBeGreaterThan(150_000n)
771
+ expect(Hex.toBigInt(result)).toBeLessThan(200_000n)
772
+ }
773
+ await expect(
774
+ rpc.request({
775
+ method,
776
+ params: [
777
+ { ...request, frames: [{ ...request.frames![0]!, mode: 255 }] },
778
+ 'latest',
779
+ ],
780
+ }),
781
+ ).rejects.toThrow('frame mode')
782
+ expect(
783
+ await rpc.request({
784
+ method: 'eth_getBalance',
785
+ params: [target, 'latest'],
786
+ }),
787
+ ).toBe(balance)
788
+ expect(
789
+ await rpc.request({
790
+ method: 'eth_getTransactionCount',
791
+ params: [sender, 'latest'],
792
+ }),
793
+ ).toBe(nonce)
794
+ },
795
+ )
796
+
797
+ test('signs, submits, and mines through the generic envelope API', async () => {
798
+ const target = accounts[1].address
799
+ const before = BigInt(
800
+ await rpc.request({
801
+ method: 'eth_getBalance',
802
+ params: [target, 'latest'],
803
+ }),
804
+ )
805
+ const envelope = TransactionEnvelope.from({
806
+ chainId: 8141,
807
+ frames: [
808
+ Frame.from({
809
+ flags: 'approveExecutionAndPayment',
810
+ gas: 50_000n,
811
+ mode: 'verify',
812
+ }),
813
+ Frame.from({ gas: 50_000n, mode: 'sender', target, value: 1n }),
814
+ ],
815
+ maxFeePerGas: 10_000_000_000n,
816
+ maxPriorityFeePerGas: 1_000_000_000n,
817
+ nonce: BigInt(
818
+ await rpc.request({
819
+ method: 'eth_getTransactionCount',
820
+ params: [accounts[0].address, 'latest'],
821
+ }),
822
+ ),
823
+ sender: accounts[0].address,
824
+ signatures: [FrameSignature.from({ scheme: 'secp256k1' })],
825
+ })
826
+ const payload = TransactionEnvelope.getSignPayload(envelope)
827
+ const signature = Secp256k1.sign({
828
+ payload,
829
+ privateKey: accounts[0].privateKey,
830
+ })
831
+
832
+ const signed = TransactionEnvelope.from({
833
+ ...envelope,
834
+ signatures: [FrameSignature.from({ scheme: 'secp256k1', signature })],
835
+ })
836
+ const serialized = TransactionEnvelope.serialize(signed)
837
+ const hash = await rpc.request({
838
+ method: 'eth_sendRawTransaction',
839
+ params: [serialized],
840
+ })
841
+ expect(hash).toBe(TransactionEnvelope.hash(signed))
842
+ expect(
843
+ TransactionRequest.toEnvelope(
844
+ TransactionEnvelope.toTransactionRequest(signed),
845
+ ),
846
+ ).toEqual(signed)
847
+ expect(TxEnvelopeEip8141.toRpc(signed)).toMatchObject(
848
+ TransactionRequest.toRpc(
849
+ TransactionEnvelope.toTransactionRequest(signed),
850
+ ),
851
+ )
852
+ await expect
853
+ .poll(
854
+ () =>
855
+ rpc.request({
856
+ method: 'eth_getTransactionReceipt',
857
+ params: [hash],
858
+ }),
859
+ { timeout: 30_000 },
860
+ )
861
+ .not.toBeNull()
862
+ const receiptRpc = await rpc.request({
863
+ method: 'eth_getTransactionReceipt',
864
+ params: [hash],
865
+ })
866
+ const receipt = TransactionReceipt.fromRpc(receiptRpc)!
867
+ expect(receipt.payer!.toLowerCase()).toBe(
868
+ accounts[0].address.toLowerCase(),
869
+ )
870
+ const transactionRpc = await rpc.request({
871
+ method: 'eth_getTransactionByHash',
872
+ params: [hash],
873
+ })
874
+ expect(transactionRpc).not.toBeNull()
875
+ const transaction = Transaction.fromRpc(transactionRpc)!
876
+ expect(transaction.type).toBe('eip8141')
877
+ const schemaTransaction = z.decode(
878
+ z.Transaction.Transaction,
879
+ transactionRpc!,
880
+ )
881
+ if (schemaTransaction.type !== 'eip8141')
882
+ throw new Error('Expected a frame transaction')
883
+ expect(schemaTransaction.frames).toEqual(transaction.frames)
884
+ expect(schemaTransaction.signatures).toEqual(transaction.signatures)
885
+ expect(
886
+ z.decode(z.TransactionReceipt.TransactionReceipt, receiptRpc!)
887
+ .frameReceipts,
888
+ ).toEqual(receipt.frameReceipts)
889
+ expect(
890
+ z.encode(z.TransactionReceipt.TransactionReceipt, receipt)
891
+ .frameReceipts,
892
+ ).toEqual(receiptRpc!.frameReceipts)
893
+
894
+ if (transaction.type !== 'eip8141')
895
+ throw new Error('Expected a frame transaction')
896
+ const decoded = TxEnvelopeEip8141.fromRpc(
897
+ transactionRpc as Transaction.Eip8141Rpc,
898
+ )
899
+ expect(TxEnvelopeEip8141.hash(decoded)).toBe(hash)
900
+ expect(transaction.frames).toEqual(decoded.frames)
901
+ expect(transaction.signatures).toEqual(decoded.signatures)
902
+ expect(Transaction.toRpc(transaction)).toMatchObject(
903
+ TxEnvelopeEip8141.toRpc(decoded),
904
+ )
905
+ expect(receipt.type).toBe('eip8141')
906
+ expect(receipt.frameReceipts![0]!.gasUsed).toBeGreaterThan(0n)
907
+ expect(receipt.frameReceipts![0]!.stateGasUsed).toBe(0n)
908
+ expect(TransactionReceipt.toRpc(receipt).frameReceipts).toEqual(
909
+ receiptRpc!.frameReceipts,
910
+ )
911
+ expect(receipt.frameReceipts!.map((frame) => frame.status)).toEqual([
912
+ 'success',
913
+ 'success',
914
+ ])
915
+ expect(
916
+ BigInt(
917
+ await rpc.request({
918
+ method: 'eth_getBalance',
919
+ params: [target, 'latest'],
920
+ }),
921
+ ),
922
+ ).toBe(before + 1n)
923
+ })
924
+
925
+ test('charges a separate sponsor', async () => {
926
+ const senderBefore = BigInt(
927
+ await rpc.request({
928
+ method: 'eth_getBalance',
929
+ params: [accounts[0].address, 'latest'],
930
+ }),
931
+ )
932
+ const payerBefore = BigInt(
933
+ await rpc.request({
934
+ method: 'eth_getBalance',
935
+ params: [accounts[1].address, 'latest'],
936
+ }),
937
+ )
938
+ const envelope = TxEnvelopeEip8141.from({
939
+ chainId: 8141,
940
+ frames: [
941
+ Frame.from({
942
+ flags: 'approveExecution',
943
+ gas: 50_000n,
944
+ mode: 'verify',
945
+ }),
946
+ Frame.from({
947
+ flags: 'approvePayment',
948
+ gas: 50_000n,
949
+ mode: 'verify',
950
+ target: accounts[1].address,
951
+ }),
952
+ Frame.from({
953
+ gas: 50_000n,
954
+ mode: 'sender',
955
+ target: accounts[1].address,
956
+ value: 1n,
957
+ }),
958
+ ],
959
+ maxFeePerGas: 10_000_000_000n,
960
+ maxPriorityFeePerGas: 1_000_000_000n,
961
+ nonce: BigInt(
962
+ await rpc.request({
963
+ method: 'eth_getTransactionCount',
964
+ params: [accounts[0].address, 'latest'],
965
+ }),
966
+ ),
967
+ sender: accounts[0].address,
968
+ signatures: [
969
+ FrameSignature.from({ scheme: 'secp256k1' }),
970
+ FrameSignature.from({
971
+ scheme: 'secp256k1',
972
+ signer: accounts[1].address,
973
+ }),
974
+ ],
975
+ })
976
+ const payload = TxEnvelopeEip8141.getSignPayload(envelope)
977
+ const signature = Secp256k1.sign({
978
+ payload,
979
+ privateKey: accounts[0].privateKey,
980
+ })
981
+ const payerSignature = Secp256k1.sign({
982
+ payload,
983
+ privateKey: accounts[1].privateKey,
984
+ })
985
+ const signed = TxEnvelopeEip8141.from({
986
+ ...envelope,
987
+ signatures: [
988
+ FrameSignature.from({ scheme: 'secp256k1', signature }),
989
+ FrameSignature.from({
990
+ scheme: 'secp256k1',
991
+ signature: payerSignature,
992
+ signer: accounts[1].address,
993
+ }),
994
+ ],
995
+ })
996
+ const serialized = TxEnvelopeEip8141.serialize(signed)
997
+ const hash = await rpc.request({
998
+ method: 'eth_sendRawTransaction',
999
+ params: [serialized],
1000
+ })
1001
+ expect(hash).toBe(TxEnvelopeEip8141.hash(signed))
1002
+ await expect
1003
+ .poll(
1004
+ () =>
1005
+ rpc.request({
1006
+ method: 'eth_getTransactionReceipt',
1007
+ params: [hash],
1008
+ }),
1009
+ { timeout: 30_000 },
1010
+ )
1011
+ .not.toBeNull()
1012
+ const receipt = TransactionReceipt.fromRpc(
1013
+ await rpc.request({
1014
+ method: 'eth_getTransactionReceipt',
1015
+ params: [hash],
1016
+ }),
1017
+ )!
1018
+ expect(receipt.payer!.toLowerCase()).toBe(
1019
+ accounts[1].address.toLowerCase(),
1020
+ )
1021
+ expect(
1022
+ BigInt(
1023
+ await rpc.request({
1024
+ method: 'eth_getBalance',
1025
+ params: [accounts[0].address, 'latest'],
1026
+ }),
1027
+ ),
1028
+ ).toBe(senderBefore - 1n)
1029
+ expect(
1030
+ BigInt(
1031
+ await rpc.request({
1032
+ method: 'eth_getBalance',
1033
+ params: [accounts[1].address, 'latest'],
1034
+ }),
1035
+ ),
1036
+ ).toBeLessThan(payerBefore)
1037
+ })
1038
+
1039
+ test('rolls back an atomic batch and skips its remaining frames', async () => {
1040
+ const before = await rpc.request({
1041
+ method: 'eth_getBalance',
1042
+ params: [accounts[1].address, 'latest'],
1043
+ })
1044
+ const envelope = TxEnvelopeEip8141.from({
1045
+ chainId: 8141,
1046
+ frames: [
1047
+ Frame.from({
1048
+ flags: 'approveExecutionAndPayment',
1049
+ gas: 50_000n,
1050
+ mode: 'verify',
1051
+ }),
1052
+ Frame.from({
1053
+ flags: 'atomicBatch',
1054
+ gas: 50_000n,
1055
+ mode: 'sender',
1056
+ target: accounts[1].address,
1057
+ value: 1n,
1058
+ }),
1059
+ Frame.from({
1060
+ flags: 'atomicBatch',
1061
+ gas: 50_000n,
1062
+ mode: 'sender',
1063
+ target: '0x0000000000000000000000000000000000001000',
1064
+ }),
1065
+ Frame.from({
1066
+ gas: 50_000n,
1067
+ mode: 'sender',
1068
+ target: accounts[1].address,
1069
+ value: 2n,
1070
+ }),
1071
+ ],
1072
+ maxFeePerGas: 10_000_000_000n,
1073
+ maxPriorityFeePerGas: 1_000_000_000n,
1074
+ nonce: BigInt(
1075
+ await rpc.request({
1076
+ method: 'eth_getTransactionCount',
1077
+ params: [accounts[0].address, 'latest'],
1078
+ }),
1079
+ ),
1080
+ sender: accounts[0].address,
1081
+ signatures: [FrameSignature.from({ scheme: 'secp256k1' })],
1082
+ })
1083
+ const payload = TxEnvelopeEip8141.getSignPayload(envelope)
1084
+ const signature = Secp256k1.sign({
1085
+ payload,
1086
+ privateKey: accounts[0].privateKey,
1087
+ })
1088
+
1089
+ const signed = TxEnvelopeEip8141.from({
1090
+ ...envelope,
1091
+ signatures: [FrameSignature.from({ scheme: 'secp256k1', signature })],
1092
+ })
1093
+ const serialized = TxEnvelopeEip8141.serialize(signed)
1094
+ const hash = await rpc.request({
1095
+ method: 'eth_sendRawTransaction',
1096
+ params: [serialized],
1097
+ })
1098
+ expect(hash).toBe(TxEnvelopeEip8141.hash(signed))
1099
+ await expect
1100
+ .poll(
1101
+ () =>
1102
+ rpc.request({
1103
+ method: 'eth_getTransactionReceipt',
1104
+ params: [hash],
1105
+ }),
1106
+ { timeout: 30_000 },
1107
+ )
1108
+ .not.toBeNull()
1109
+ const receipt = TransactionReceipt.fromRpc(
1110
+ await rpc.request({
1111
+ method: 'eth_getTransactionReceipt',
1112
+ params: [hash],
1113
+ }),
1114
+ )!
1115
+ expect(receipt.frameReceipts!.map((frame) => frame.status)).toEqual([
1116
+ 'success',
1117
+ 'success',
1118
+ 'reverted',
1119
+ 'skipped',
1120
+ ])
1121
+ expect(
1122
+ await rpc.request({
1123
+ method: 'eth_getBalance',
1124
+ params: [accounts[1].address, 'latest'],
1125
+ }),
1126
+ ).toBe(before)
1127
+ })
1128
+
1129
+ test('rejects a signature from the wrong sender', async () => {
1130
+ const envelope = TxEnvelopeEip8141.from({
1131
+ chainId: 8141,
1132
+ frames: [
1133
+ Frame.from({
1134
+ flags: 'approveExecutionAndPayment',
1135
+ gas: 50_000n,
1136
+ mode: 'verify',
1137
+ }),
1138
+ ],
1139
+ maxFeePerGas: 10_000_000_000n,
1140
+ maxPriorityFeePerGas: 1_000_000_000n,
1141
+ nonce: BigInt(
1142
+ await rpc.request({
1143
+ method: 'eth_getTransactionCount',
1144
+ params: [accounts[0].address, 'latest'],
1145
+ }),
1146
+ ),
1147
+ sender: accounts[0].address,
1148
+ signatures: [FrameSignature.from({ scheme: 'secp256k1' })],
1149
+ })
1150
+ const signature = Secp256k1.sign({
1151
+ payload: TxEnvelopeEip8141.getSignPayload(envelope),
1152
+ privateKey: accounts[1].privateKey,
1153
+ })
1154
+ await expect(
1155
+ rpc.request({
1156
+ method: 'eth_sendRawTransaction',
1157
+ params: [
1158
+ TxEnvelopeEip8141.serialize(
1159
+ TxEnvelopeEip8141.from({
1160
+ ...envelope,
1161
+ signatures: [
1162
+ FrameSignature.from({ scheme: 'secp256k1', signature }),
1163
+ ],
1164
+ }),
1165
+ ),
1166
+ ],
1167
+ }),
1168
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
1169
+ `[RpcResponse.InvalidInputError: transaction invalid, frame transaction SECP256K1 signer does not match the recovered address]`,
1170
+ )
1171
+ })
1172
+
1173
+ test('rejects an expired transaction', async () => {
1174
+ const envelope = TxEnvelopeEip8141.from({
1175
+ chainId: 8141,
1176
+ frames: [
1177
+ Frame.from({
1178
+ data: '0x0000000000000001',
1179
+ gas: 50_000n,
1180
+ mode: 'verify',
1181
+ target: '0x0000000000000000000000000000000000008141',
1182
+ }),
1183
+ Frame.from({
1184
+ flags: 'approveExecutionAndPayment',
1185
+ gas: 50_000n,
1186
+ mode: 'verify',
1187
+ }),
1188
+ ],
1189
+ maxFeePerGas: 10_000_000_000n,
1190
+ maxPriorityFeePerGas: 1_000_000_000n,
1191
+ nonce: BigInt(
1192
+ await rpc.request({
1193
+ method: 'eth_getTransactionCount',
1194
+ params: [accounts[0].address, 'latest'],
1195
+ }),
1196
+ ),
1197
+ sender: accounts[0].address,
1198
+ signatures: [FrameSignature.from({ scheme: 'secp256k1' })],
1199
+ })
1200
+ const payload = TxEnvelopeEip8141.getSignPayload(envelope)
1201
+ const signature = Secp256k1.sign({
1202
+ payload,
1203
+ privateKey: accounts[0].privateKey,
1204
+ })
1205
+
1206
+ const signed = TxEnvelopeEip8141.from({
1207
+ ...envelope,
1208
+ signatures: [FrameSignature.from({ scheme: 'secp256k1', signature })],
1209
+ })
1210
+ await expect(
1211
+ rpc.request({
1212
+ method: 'eth_sendRawTransaction',
1213
+ params: [TxEnvelopeEip8141.serialize(signed)],
1214
+ }),
1215
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
1216
+ `[RpcResponse.InvalidInputError: frame transaction expired]`,
1217
+ )
1218
+ })
1219
+
1220
+ test('authorizes P-256 through signature introspection', async () => {
1221
+ const privateKey = accounts[0].privateKey
1222
+ const publicKey = P256.getPublicKey({ privateKey })
1223
+ const sender = Address.fromPublicKey(publicKey)
1224
+ // Genesis verifier checks SIGPARAM signer, scheme, and payload before APPROVE(3).
1225
+ const envelope = TxEnvelopeEip8141.from({
1226
+ chainId: 8141,
1227
+ frames: [
1228
+ Frame.from({
1229
+ flags: 'approveExecutionAndPayment',
1230
+ gas: 50_000n,
1231
+ mode: 'verify',
1232
+ }),
1233
+ Frame.from({
1234
+ gas: 50_000n,
1235
+ mode: 'sender',
1236
+ target: accounts[1].address,
1237
+ value: 1n,
1238
+ }),
1239
+ ],
1240
+ maxFeePerGas: 10_000_000_000n,
1241
+ maxPriorityFeePerGas: 1_000_000_000n,
1242
+ nonce: BigInt(
1243
+ await rpc.request({
1244
+ method: 'eth_getTransactionCount',
1245
+ params: [sender, 'latest'],
1246
+ }),
1247
+ ),
1248
+ sender,
1249
+ signatures: [FrameSignature.from({ scheme: 'p256' })],
1250
+ })
1251
+ const signature = P256.sign({
1252
+ payload: TxEnvelopeEip8141.getSignPayload(envelope),
1253
+ privateKey,
1254
+ })
1255
+ const before = BigInt(
1256
+ await rpc.request({
1257
+ method: 'eth_getBalance',
1258
+ params: [accounts[1].address, 'latest'],
1259
+ }),
1260
+ )
1261
+ const signed = TxEnvelopeEip8141.from({
1262
+ ...envelope,
1263
+ signatures: [
1264
+ FrameSignature.from({ publicKey, scheme: 'p256', signature }),
1265
+ ],
1266
+ })
1267
+ const serialized = TxEnvelopeEip8141.serialize(signed)
1268
+ const hash = await rpc.request({
1269
+ method: 'eth_sendRawTransaction',
1270
+ params: [serialized],
1271
+ })
1272
+ expect(hash).toBe(TxEnvelopeEip8141.hash(signed))
1273
+ await expect
1274
+ .poll(
1275
+ () =>
1276
+ rpc.request({
1277
+ method: 'eth_getTransactionReceipt',
1278
+ params: [hash],
1279
+ }),
1280
+ { timeout: 30_000 },
1281
+ )
1282
+ .not.toBeNull()
1283
+ const receipt = TransactionReceipt.fromRpc(
1284
+ await rpc.request({
1285
+ method: 'eth_getTransactionReceipt',
1286
+ params: [hash],
1287
+ }),
1288
+ )!
1289
+ expect(receipt.frameReceipts!.map((frame) => frame.status)).toEqual([
1290
+ 'success',
1291
+ 'success',
1292
+ ])
1293
+ expect(
1294
+ BigInt(
1295
+ await rpc.request({
1296
+ method: 'eth_getBalance',
1297
+ params: [accounts[1].address, 'latest'],
1298
+ }),
1299
+ ),
1300
+ ).toBe(before + 1n)
1301
+ })
1302
+ })
1303
+
1304
+ test('default', () => {
1305
+ const envelope = {
1306
+ chainId: 1,
1307
+ frames: [{}],
1308
+ sender: '0x1111111111111111111111111111111111111111' as const,
1309
+ }
1310
+ expect(TxEnvelopeEip8141.serialize(envelope)).toMatchInlineSnapshot(
1311
+ '"0x06e70180941111111111111111111111111111111111111111c9c8808080c280808080c0c3808080c0"',
1312
+ )
1313
+ })
1314
+ describe('sidecars', () => {
1315
+ const envelope = { chainId: 1, frames: [{}], sender: accounts[0].address }
1316
+ const commitment = `0x${'00'.repeat(48)}` as const
1317
+ const sidecars = {
1318
+ blobs: [`0x${'00'.repeat(131072)}` as const],
1319
+ cellProofs: Array.from({ length: 128 }, () => commitment),
1320
+ commitments: [commitment],
1321
+ }
1322
+ const input = {
1323
+ ...envelope,
1324
+ blobVersionedHashes: [Blobs.commitmentToVersionedHash(commitment)],
1325
+ sidecars,
1326
+ }
1327
+ test.each(['blobs', 'cellProofs', 'commitments'] as const)(
1328
+ 'rejects half-byte %s',
1329
+ (field) => {
1330
+ const values = [...sidecars[field]]
1331
+ values[0] = values[0]!.slice(0, -1) as Hex.Hex
1332
+ expect(() =>
1333
+ TxEnvelopeEip8141.serialize({
1334
+ ...input,
1335
+ sidecars: { ...sidecars, [field]: values },
1336
+ }),
1337
+ ).toThrow(TxEnvelopeEip8141.InvalidError)
1338
+ },
1339
+ )
1340
+
1341
+ test('round-trips the version 1 network wrapper', () => {
1342
+ const encoded = TxEnvelopeEip8141.serialize(input)
1343
+ const tuple = Rlp.toHex(Hex.slice(encoded, 1))
1344
+ expect(tuple[1]).toBe('0x01')
1345
+ expect(tuple.length).toBe(5)
1346
+ expect(
1347
+ z.decode(z.TxEnvelopeEip8141.serialized, encoded).sidecars,
1348
+ ).toEqual(sidecars)
1349
+ expect(TxEnvelopeEip8141.deserialize(encoded).sidecars).toEqual(sidecars)
1350
+ expect(
1351
+ TransactionEnvelope.serialize(
1352
+ TxEnvelopeEip8141.from({ ...input, sidecars: undefined }),
1353
+ { sidecars },
1354
+ ),
1355
+ ).toBe(encoded)
1356
+ expect(TxEnvelopeEip8141.hash(input)).toBe(
1357
+ TxEnvelopeEip8141.hash({ ...input, sidecars: undefined }),
1358
+ )
1359
+ expect(TxEnvelopeEip8141.getSignPayload(input)).toBe(
1360
+ TxEnvelopeEip8141.getSignPayload({ ...input, sidecars: undefined }),
1361
+ )
1362
+ })
1363
+ test('missing blobs', () => {
1364
+ expect(() =>
1365
+ TxEnvelopeEip8141.serialize({
1366
+ ...input,
1367
+ sidecars: { ...sidecars, blobs: [] },
1368
+ }),
1369
+ ).toThrowErrorMatchingInlineSnapshot(
1370
+ `[TxEnvelopeEip8141.InvalidError: PeerDAS sidecar counts do not match blob hashes.]`,
1371
+ )
1372
+ })
1373
+
1374
+ test('missing commitments', () => {
1375
+ expect(() =>
1376
+ TxEnvelopeEip8141.serialize({
1377
+ ...input,
1378
+ sidecars: { ...sidecars, commitments: [] },
1379
+ }),
1380
+ ).toThrowErrorMatchingInlineSnapshot(
1381
+ `[TxEnvelopeEip8141.InvalidError: PeerDAS sidecar counts do not match blob hashes.]`,
1382
+ )
1383
+ })
1384
+
1385
+ test('missing cell proofs', () => {
1386
+ expect(() =>
1387
+ TxEnvelopeEip8141.serialize({
1388
+ ...input,
1389
+ sidecars: { ...sidecars, cellProofs: [] },
1390
+ }),
1391
+ ).toThrowErrorMatchingInlineSnapshot(
1392
+ `[TxEnvelopeEip8141.InvalidError: PeerDAS sidecar counts do not match blob hashes.]`,
1393
+ )
1394
+ })
1395
+
1396
+ test('invalid blob size', () => {
1397
+ expect(() =>
1398
+ TxEnvelopeEip8141.serialize({
1399
+ ...input,
1400
+ sidecars: { ...sidecars, blobs: ['0x'] },
1401
+ }),
1402
+ ).toThrowErrorMatchingInlineSnapshot(
1403
+ `[TxEnvelopeEip8141.InvalidError: Invalid blob size or commitment.]`,
1404
+ )
1405
+ })
1406
+
1407
+ test('invalid commitment size', () => {
1408
+ expect(() =>
1409
+ TxEnvelopeEip8141.serialize({
1410
+ ...input,
1411
+ sidecars: { ...sidecars, commitments: ['0x'] },
1412
+ }),
1413
+ ).toThrowErrorMatchingInlineSnapshot(
1414
+ `[TxEnvelopeEip8141.InvalidError: Invalid blob size or commitment.]`,
1415
+ )
1416
+ })
1417
+
1418
+ test('invalid proof size', () => {
1419
+ expect(() =>
1420
+ TxEnvelopeEip8141.serialize({
1421
+ ...input,
1422
+ sidecars: {
1423
+ ...sidecars,
1424
+ cellProofs: Array.from({ length: 128 }, () => '0x' as const),
1425
+ },
1426
+ }),
1427
+ ).toThrowErrorMatchingInlineSnapshot(
1428
+ `[TxEnvelopeEip8141.InvalidError: Cell proofs must contain 48 bytes.]`,
1429
+ )
1430
+ })
1431
+
1432
+ test('commitment does not match versioned hash', () => {
1433
+ expect(() =>
1434
+ TxEnvelopeEip8141.serialize({
1435
+ ...input,
1436
+ sidecars: { ...sidecars, commitments: [`0x${'11'.repeat(48)}`] },
1437
+ }),
1438
+ ).toThrowErrorMatchingInlineSnapshot(
1439
+ `[TxEnvelopeEip8141.InvalidError: Invalid blob size or commitment.]`,
1440
+ )
1441
+ })
1442
+ test('rejects a wrapper without blob hashes', () => {
1443
+ expect(() =>
1444
+ TxEnvelopeEip8141.serialize({ ...envelope, sidecars }),
1445
+ ).toThrowErrorMatchingInlineSnapshot(
1446
+ `[TxEnvelopeEip8141.InvalidError: PeerDAS sidecar counts do not match blob hashes.]`,
1447
+ )
1448
+ })
1449
+ })
1450
+ })
1451
+
1452
+ describe('validate', () => {
1453
+ test('negative chainId', () => {
1454
+ expect(
1455
+ TxEnvelopeEip8141.validate({
1456
+ chainId: -1,
1457
+ frames: [{}],
1458
+ sender: accounts[0].address,
1459
+ }),
1460
+ ).toBe(false)
1461
+ })
1462
+
1463
+ test('fractional chainId', () => {
1464
+ expect(
1465
+ TxEnvelopeEip8141.validate({
1466
+ chainId: 1.5,
1467
+ frames: [{}],
1468
+ sender: accounts[0].address,
1469
+ }),
1470
+ ).toBe(false)
1471
+ })
1472
+
1473
+ test('unsafe numeric chainId', () => {
1474
+ expect(
1475
+ TxEnvelopeEip8141.validate({
1476
+ chainId: Number.MAX_SAFE_INTEGER + 1,
1477
+ frames: [{}],
1478
+ sender: accounts[0].address,
1479
+ }),
1480
+ ).toBe(false)
1481
+ })
1482
+
1483
+ test('chainId exceeds 256 bits', () => {
1484
+ expect(
1485
+ TxEnvelopeEip8141.validate({
1486
+ chainId: 2n ** 256n,
1487
+ frames: [{}],
1488
+ sender: accounts[0].address,
1489
+ }),
1490
+ ).toBe(false)
1491
+ })
1492
+
1493
+ test('negative nonce', () => {
1494
+ expect(
1495
+ TxEnvelopeEip8141.validate({
1496
+ chainId: 1,
1497
+ frames: [{}],
1498
+ nonce: -1n,
1499
+ sender: accounts[0].address,
1500
+ }),
1501
+ ).toBe(false)
1502
+ })
1503
+
1504
+ test('nonce exceeds 64 bits', () => {
1505
+ expect(
1506
+ TxEnvelopeEip8141.validate({
1507
+ chainId: 1,
1508
+ frames: [{}],
1509
+ nonce: 2n ** 64n,
1510
+ sender: accounts[0].address,
1511
+ }),
1512
+ ).toBe(false)
1513
+ })
1514
+
1515
+ test('null nonce', () => {
1516
+ expect(
1517
+ TxEnvelopeEip8141.validate({
1518
+ chainId: 1,
1519
+ frames: [{}],
1520
+ nonce: null,
1521
+ sender: accounts[0].address,
1522
+ } as unknown as TxEnvelopeEip8141.TxEnvelopeEip8141),
1523
+ ).toBe(false)
1524
+ })
1525
+
1526
+ test('negative fee cap', () => {
1527
+ expect(
1528
+ TxEnvelopeEip8141.validate({
1529
+ chainId: 1,
1530
+ frames: [{}],
1531
+ maxFeePerGas: -1n,
1532
+ sender: accounts[0].address,
1533
+ }),
1534
+ ).toBe(false)
1535
+ })
1536
+
1537
+ test('fee cap exceeds 256 bits', () => {
1538
+ expect(
1539
+ TxEnvelopeEip8141.validate({
1540
+ chainId: 1,
1541
+ frames: [{}],
1542
+ maxFeePerGas: 2n ** 256n,
1543
+ sender: accounts[0].address,
1544
+ }),
1545
+ ).toBe(false)
1546
+ })
1547
+
1548
+ test('tip above fee cap', () => {
1549
+ expect(
1550
+ TxEnvelopeEip8141.validate({
1551
+ chainId: 1,
1552
+ frames: [{}],
1553
+ maxPriorityFeePerGas: 1n,
1554
+ sender: accounts[0].address,
1555
+ }),
1556
+ ).toBe(false)
1557
+ })
1558
+
1559
+ test('blob fee without blobs', () => {
1560
+ expect(
1561
+ TxEnvelopeEip8141.validate({
1562
+ chainId: 1,
1563
+ frames: [{}],
1564
+ maxFeePerBlobGas: 1n,
1565
+ sender: accounts[0].address,
1566
+ }),
1567
+ ).toBe(false)
1568
+ })
1569
+
1570
+ test('invalid blob hash size', () => {
1571
+ expect(
1572
+ TxEnvelopeEip8141.validate({
1573
+ blobVersionedHashes: ['0x01'],
1574
+ chainId: 1,
1575
+ frames: [{}],
1576
+ sender: accounts[0].address,
1577
+ }),
1578
+ ).toBe(false)
1579
+ })
1580
+
1581
+ test('invalid blob hash version', () => {
1582
+ expect(
1583
+ TxEnvelopeEip8141.validate({
1584
+ blobVersionedHashes: [`0x02${'00'.repeat(31)}`],
1585
+ chainId: 1,
1586
+ frames: [{}],
1587
+ sender: accounts[0].address,
1588
+ }),
1589
+ ).toBe(false)
1590
+ })
1591
+
1592
+ test('empty frames', () => {
1593
+ expect(
1594
+ TxEnvelopeEip8141.validate({
1595
+ chainId: 1,
1596
+ frames: [],
1597
+ sender: accounts[0].address,
1598
+ }),
1599
+ ).toBe(false)
1600
+ })
1601
+
1602
+ test('too many frames', () => {
1603
+ expect(
1604
+ TxEnvelopeEip8141.validate({
1605
+ chainId: 1,
1606
+ frames: Array.from({ length: 65 }, () => ({})),
1607
+ sender: accounts[0].address,
1608
+ }),
1609
+ ).toBe(false)
1610
+ })
1611
+
1612
+ test('combined gas exceeds 64 bits', () => {
1613
+ expect(
1614
+ TxEnvelopeEip8141.validate({
1615
+ chainId: 1,
1616
+ frames: [{ gas: 2n ** 64n - 1n }, { stateGas: 1n }],
1617
+ sender: accounts[0].address,
1618
+ }),
1619
+ ).toBe(false)
1620
+ })
1621
+
1622
+ test('execution gas exceeds transaction cap', () => {
1623
+ expect(
1624
+ TxEnvelopeEip8141.validate({
1625
+ chainId: 1,
1626
+ frames: [{ gas: 16_777_216n }],
1627
+ sender: accounts[0].address,
1628
+ }),
1629
+ ).toBe(false)
1630
+ })
1631
+
1632
+ test('execution approval targets another account', () => {
1633
+ expect(
1634
+ TxEnvelopeEip8141.validate({
1635
+ chainId: 1,
1636
+ frames: [{ flags: 'approveExecution', target: accounts[1].address }],
1637
+ sender: accounts[0].address,
1638
+ }),
1639
+ ).toBe(false)
1640
+ })
1641
+
1642
+ test('atomic batch without a following frame', () => {
1643
+ expect(
1644
+ TxEnvelopeEip8141.validate({
1645
+ chainId: 1,
1646
+ frames: [{ flags: 'atomicBatch' }],
1647
+ sender: accounts[0].address,
1648
+ }),
1649
+ ).toBe(false)
1650
+ })
1651
+
1652
+ test('atomic batch followed by verification', () => {
1653
+ expect(
1654
+ TxEnvelopeEip8141.validate({
1655
+ chainId: 1,
1656
+ frames: [{ flags: 'atomicBatch' }, { mode: 'verify' }],
1657
+ sender: accounts[0].address,
1658
+ }),
1659
+ ).toBe(false)
1660
+ })
1661
+
1662
+ test('approval inside an atomic batch', () => {
1663
+ expect(
1664
+ TxEnvelopeEip8141.validate({
1665
+ chainId: 1,
1666
+ frames: [{ flags: 'atomicBatch' }, { flags: 'approvePayment' }],
1667
+ sender: accounts[0].address,
1668
+ }),
1669
+ ).toBe(false)
1670
+ })
1671
+
1672
+ test('invalid expiry calldata', () => {
1673
+ expect(
1674
+ TxEnvelopeEip8141.validate({
1675
+ chainId: 1,
1676
+ frames: [
1677
+ {
1678
+ mode: 'verify',
1679
+ target: '0x0000000000000000000000000000000000008141',
1680
+ },
1681
+ ],
1682
+ sender: accounts[0].address,
1683
+ }),
1684
+ ).toBe(false)
1685
+ })
1686
+
1687
+ test('invalid sender', () => {
1688
+ expect(
1689
+ TxEnvelopeEip8141.validate({ chainId: 1, frames: [{}], sender: '0x' }),
1690
+ ).toBe(false)
1691
+ })
1692
+
1693
+ test('accepts atomic batches and implicit sender approval', () => {
1694
+ const envelope = { chainId: 1, frames: [{}], sender: accounts[0].address }
1695
+
1696
+ expect(
1697
+ TxEnvelopeEip8141.validate({
1698
+ ...envelope,
1699
+ frames: [{ flags: 'approveExecution' }, { flags: 'atomicBatch' }, {}],
1700
+ }),
1701
+ ).toBe(true)
1702
+ })
1703
+
1704
+ test('validates expiry shape and uniqueness', () => {
1705
+ const envelope = { chainId: 1, frames: [{}], sender: accounts[0].address }
1706
+
1707
+ const frame = {
1708
+ data: '0x0000000000000001',
1709
+ gas: 5000n,
1710
+ mode: 'verify',
1711
+ target: '0x0000000000000000000000000000000000008141',
1712
+ } as const
1713
+ expect(TxEnvelopeEip8141.validate({ ...envelope, frames: [frame] })).toBe(
1714
+ true,
1715
+ )
1716
+ expect(
1717
+ TxEnvelopeEip8141.validate({ ...envelope, frames: [frame, frame] }),
1718
+ ).toBe(false)
1719
+ expect(
1720
+ TxEnvelopeEip8141.validate({
1721
+ ...envelope,
1722
+ frames: [{ ...frame, stateGas: 1n }],
1723
+ }),
1724
+ ).toBe(false)
1725
+ })
1726
+ })
1727
+
1728
+ describe('toRpc', () => {
1729
+ test('maps sender and defaults without an outer signature', () => {
1730
+ const envelope = TxEnvelopeEip8141.from({
1731
+ chainId: 1,
1732
+ frames: [Frame.from({})],
1733
+ sender: accounts[0].address,
1734
+ })
1735
+ expect(TxEnvelopeEip8141.toRpc(envelope)).toEqual({
1736
+ blobVersionedHashes: [],
1737
+ chainId: '0x1',
1738
+ frames: [
1739
+ {
1740
+ data: '0x',
1741
+ executionGasLimit: '0x0',
1742
+ flags: 0,
1743
+ mode: 0,
1744
+ stateGasLimit: '0x0',
1745
+ value: '0x0',
1746
+ },
1747
+ ],
1748
+ from: accounts[0].address,
1749
+ maxFeePerBlobGas: '0x0',
1750
+ maxFeePerGas: '0x0',
1751
+ maxPriorityFeePerGas: '0x0',
1752
+ nonce: '0x0',
1753
+ signatures: [],
1754
+ type: '0x6',
1755
+ })
1756
+ })
1757
+ })
1758
+
1759
+ describe('fromRpc', () => {
1760
+ test('retains large chain IDs and canonical bytes', () => {
1761
+ const envelope = TxEnvelopeEip8141.from({
1762
+ chainId: 9007199254740993n,
1763
+ frames: [Frame.from({})],
1764
+ sender: accounts[0].address,
1765
+ signatures: [FrameSignature.from({ scheme: 'secp256k1' })],
1766
+ })
1767
+ const rpc = TxEnvelopeEip8141.toRpc(envelope)
1768
+ expect(rpc.chainId).toBe('0x20000000000001')
1769
+ const result = TxEnvelopeEip8141.fromRpc(rpc)
1770
+ expect(result.chainId).toBe(9007199254740993n)
1771
+ expect(TxEnvelopeEip8141.serialize(result)).toBe(
1772
+ TxEnvelopeEip8141.serialize(envelope),
1773
+ )
1774
+ })
1775
+ })