ox 1.7.4 → 1.7.5

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 (105) hide show
  1. package/CHANGELOG.md +16 -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/Signature.d.ts +6 -5
  9. package/dist/core/Signature.d.ts.map +1 -1
  10. package/dist/core/Signature.js +17 -7
  11. package/dist/core/Signature.js.map +1 -1
  12. package/dist/core/Siwe.d.ts.map +1 -1
  13. package/dist/core/Siwe.js +21 -1
  14. package/dist/core/Siwe.js.map +1 -1
  15. package/dist/tempo/KeyAuthorization.d.ts +42 -38
  16. package/dist/tempo/KeyAuthorization.d.ts.map +1 -1
  17. package/dist/tempo/KeyAuthorization.js +24 -4
  18. package/dist/tempo/KeyAuthorization.js.map +1 -1
  19. package/dist/tempo/MultisigConfig.d.ts +18 -15
  20. package/dist/tempo/MultisigConfig.d.ts.map +1 -1
  21. package/dist/tempo/MultisigConfig.js +49 -20
  22. package/dist/tempo/MultisigConfig.js.map +1 -1
  23. package/dist/tempo/MultisigOperation.d.ts +3 -2
  24. package/dist/tempo/MultisigOperation.d.ts.map +1 -1
  25. package/dist/tempo/MultisigOperation.js +20 -100
  26. package/dist/tempo/MultisigOperation.js.map +1 -1
  27. package/dist/tempo/MultisigSimulation.d.ts +2 -49
  28. package/dist/tempo/MultisigSimulation.d.ts.map +1 -1
  29. package/dist/tempo/MultisigSimulation.js +18 -46
  30. package/dist/tempo/MultisigSimulation.js.map +1 -1
  31. package/dist/tempo/SignatureEnvelope.d.ts +72 -140
  32. package/dist/tempo/SignatureEnvelope.d.ts.map +1 -1
  33. package/dist/tempo/SignatureEnvelope.js +130 -181
  34. package/dist/tempo/SignatureEnvelope.js.map +1 -1
  35. package/dist/tempo/TransactionRequest.d.ts +4 -2
  36. package/dist/tempo/TransactionRequest.d.ts.map +1 -1
  37. package/dist/tempo/TransactionRequest.js +6 -1
  38. package/dist/tempo/TransactionRequest.js.map +1 -1
  39. package/dist/tempo/index.d.ts +3 -1
  40. package/dist/tempo/index.d.ts.map +1 -1
  41. package/dist/tempo/index.js +3 -1
  42. package/dist/tempo/index.js.map +1 -1
  43. package/dist/zod/tempo/AuthorizationTempo.d.ts +350 -10
  44. package/dist/zod/tempo/AuthorizationTempo.d.ts.map +1 -1
  45. package/dist/zod/tempo/KeyAuthorization.d.ts +242 -422
  46. package/dist/zod/tempo/KeyAuthorization.d.ts.map +1 -1
  47. package/dist/zod/tempo/KeyAuthorization.js +29 -31
  48. package/dist/zod/tempo/KeyAuthorization.js.map +1 -1
  49. package/dist/zod/tempo/MultisigSimulation.d.ts +8 -76
  50. package/dist/zod/tempo/MultisigSimulation.d.ts.map +1 -1
  51. package/dist/zod/tempo/MultisigSimulation.js +3 -37
  52. package/dist/zod/tempo/MultisigSimulation.js.map +1 -1
  53. package/dist/zod/tempo/RpcSchemaTempo.d.ts +284 -228
  54. package/dist/zod/tempo/RpcSchemaTempo.d.ts.map +1 -1
  55. package/dist/zod/tempo/SignatureEnvelope.d.ts +105 -3
  56. package/dist/zod/tempo/SignatureEnvelope.d.ts.map +1 -1
  57. package/dist/zod/tempo/SignatureEnvelope.js +21 -9
  58. package/dist/zod/tempo/SignatureEnvelope.js.map +1 -1
  59. package/dist/zod/tempo/Transaction.d.ts +676 -448
  60. package/dist/zod/tempo/Transaction.d.ts.map +1 -1
  61. package/dist/zod/tempo/TransactionRequest.d.ts +590 -473
  62. package/dist/zod/tempo/TransactionRequest.d.ts.map +1 -1
  63. package/dist/zod/tempo/TransactionRequest.js +8 -0
  64. package/dist/zod/tempo/TransactionRequest.js.map +1 -1
  65. package/dist/zod/tempo/TxEnvelopeTempo.d.ts +327 -213
  66. package/dist/zod/tempo/TxEnvelopeTempo.d.ts.map +1 -1
  67. package/dist/zod/tempo/ZoneRpcAuthentication.d.ts +105 -3
  68. package/dist/zod/tempo/ZoneRpcAuthentication.d.ts.map +1 -1
  69. package/package.json +1 -1
  70. package/src/core/AbiEvent.ts +11 -9
  71. package/src/core/Blobs.ts +6 -2
  72. package/src/core/Signature.ts +41 -11
  73. package/src/core/Siwe.ts +20 -1
  74. package/src/core/_test/AbiEvent.test.ts +45 -0
  75. package/src/core/_test/Blobs.test.ts +22 -0
  76. package/src/core/_test/Signature.test.ts +166 -0
  77. package/src/core/_test/Siwe.test.ts +100 -1
  78. package/src/tempo/AuthorizationTempo.test.ts +8 -4
  79. package/src/tempo/KeyAuthorization.test-d.ts +16 -63
  80. package/src/tempo/KeyAuthorization.test.ts +69 -11
  81. package/src/tempo/KeyAuthorization.ts +107 -63
  82. package/src/tempo/MultisigConfig.test.ts +44 -20
  83. package/src/tempo/MultisigConfig.ts +65 -23
  84. package/src/tempo/MultisigOperation.test.ts +79 -550
  85. package/src/tempo/MultisigOperation.ts +31 -146
  86. package/src/tempo/MultisigSimulation.test-d.ts +2 -2
  87. package/src/tempo/MultisigSimulation.test.ts +21 -110
  88. package/src/tempo/MultisigSimulation.ts +25 -99
  89. package/src/tempo/SignatureEnvelope.test-d.ts +29 -47
  90. package/src/tempo/SignatureEnvelope.test.ts +328 -541
  91. package/src/tempo/SignatureEnvelope.ts +196 -291
  92. package/src/tempo/Transaction.test.ts +4 -4
  93. package/src/tempo/TransactionRequest.test.ts +17 -80
  94. package/src/tempo/TransactionRequest.ts +19 -2
  95. package/src/tempo/index.ts +3 -1
  96. package/src/tempo/multisig.e2e.test.ts +321 -860
  97. package/src/version.ts +1 -1
  98. package/src/zod/tempo/KeyAuthorization.ts +50 -39
  99. package/src/zod/tempo/MultisigSimulation.ts +3 -48
  100. package/src/zod/tempo/SignatureEnvelope.ts +46 -35
  101. package/src/zod/tempo/TransactionRequest.ts +14 -0
  102. package/src/zod/tempo/_test/KeyAuthorization.test.ts +69 -20
  103. package/src/zod/tempo/_test/MultisigSimulation.test.ts +6 -31
  104. package/src/zod/tempo/_test/SignatureEnvelope.test.ts +118 -85
  105. package/src/zod/tempo/_test/TransactionRequest.test.ts +9 -19
@@ -1,916 +1,377 @@
1
1
  import { AbiFunction, Address, Hex, Secp256k1, Value } from 'ox'
2
- import { getTransactionCount } from 'viem/actions'
3
2
  import { describe, expect, test } from 'vp/test'
4
3
  import { chain, client, fundAddress } from '../../test/tempo/config.js'
4
+ import { factory } from '../../test/tempo/multisig.js'
5
5
  import {
6
6
  KeyAuthorization,
7
7
  MultisigConfig,
8
+ MultisigOperation,
8
9
  SignatureEnvelope,
9
- TransactionRequest,
10
10
  } from './index.js'
11
11
  import * as Transaction from './Transaction.js'
12
12
  import * as TransactionReceipt from './TransactionReceipt.js'
13
+ import * as TransactionRequest from './TransactionRequest.js'
13
14
  import * as TxEnvelopeTempo from './TxEnvelopeTempo.js'
14
15
 
15
- const chainId = chain.id
16
+ const precompile = '0xaacc000000000000000000000000000000000000'
16
17
  const updateConfig = AbiFunction.from(
17
18
  'function updateConfig((bytes32 salt, uint64 version, uint8 threshold, (address owner, uint8 weight)[] owners) current, uint8 threshold, (address owner, uint8 weight)[] owners)',
18
19
  )
19
20
 
20
- describe('behavior: multisig (TIP-1061)', () => {
21
- function createKey() {
21
+ async function setup() {
22
+ const keys = Array.from({ length: 3 }, () => {
22
23
  const privateKey = Secp256k1.randomPrivateKey()
23
- const address = Address.fromPublicKey(
24
- Secp256k1.getPublicKey({ privateKey }),
25
- )
26
- return { address, privateKey } as const
27
- }
28
-
29
- function setup(parameters: {
30
- count: number
31
- threshold: number
32
- weights?: readonly number[] | undefined
33
- }) {
34
- const { count, threshold, weights = Array(count).fill(1) } = parameters
35
- const ownerKeys = Array.from({ length: count }, () => {
36
- const privateKey = Secp256k1.randomPrivateKey()
37
- const address = Address.fromPublicKey(
38
- Secp256k1.getPublicKey({ privateKey }),
39
- )
40
- return { address, privateKey } as const
41
- }).sort((a, b) => a.address.localeCompare(b.address))
42
-
43
- const initialConfig = MultisigConfig.from({
44
- salt: Hex.random(32),
45
- threshold,
46
- owners: ownerKeys.map((key, index) => ({
47
- owner: key.address,
48
- weight: weights[index]!,
49
- })),
50
- })
51
- const account = MultisigConfig.getAddress(initialConfig)
52
-
53
- return { account, initialConfig, ownerKeys } as const
54
- }
55
-
56
- function approve(parameters: {
57
- address?: Address.Address | undefined
58
- config: MultisigConfig.Config
59
- payload: Hex.Hex
60
- signers: readonly { privateKey: Hex.Hex }[]
61
- }) {
62
- const {
63
- address = MultisigConfig.getAddress(parameters.config),
64
- config,
65
- payload,
66
- signers,
67
- } = parameters
68
- const digest = MultisigConfig.getSignPayload({
69
- account: address,
70
- config,
71
- payload,
72
- })
73
- const signatures = signers.map((signer) =>
74
- SignatureEnvelope.from(
75
- Secp256k1.sign({ payload: digest, privateKey: signer.privateKey }),
76
- ),
77
- )
78
- // The node requires approvals ordered by recovered owner address.
79
- return SignatureEnvelope.sortMultisigApprovals({
80
- account: address,
81
- config,
82
- payload,
83
- signatures,
84
- })
85
- }
86
-
87
- async function send(serialized: Hex.Hex) {
88
- return (await client
89
- .request({
90
- method: 'eth_sendRawTransactionSync',
91
- params: [serialized],
92
- })
93
- .then((transaction) => TransactionReceipt.fromRpc(transaction as any)))!
94
- }
24
+ return {
25
+ privateKey,
26
+ address: Address.fromPublicKey(Secp256k1.getPublicKey({ privateKey })),
27
+ }
28
+ })
29
+ const config: MultisigConfig.Config = MultisigConfig.from({
30
+ salt: Hex.random(32),
31
+ threshold: 2,
32
+ owners: keys.map((key) => ({ owner: key.address, weight: 1 })),
33
+ })
34
+ const account = MultisigConfig.getAddress(config, { factory })
35
+ await fundAddress(client, { address: account })
36
+ return { account, config, keys }
37
+ }
38
+
39
+ function transaction(
40
+ nonce: bigint,
41
+ calls: TxEnvelopeTempo.TxEnvelopeTempo['calls'] = [
42
+ { to: '0x0000000000000000000000000000000000000000' },
43
+ ],
44
+ ) {
45
+ return TxEnvelopeTempo.from({
46
+ calls,
47
+ chainId: chain.id,
48
+ feeToken: '0x20c0000000000000000000000000000000000001',
49
+ nonce,
50
+ gas: 5_000_000n,
51
+ maxFeePerGas: Value.fromGwei('20'),
52
+ maxPriorityFeePerGas: Value.fromGwei('10'),
53
+ })
54
+ }
95
55
 
96
- async function bootstrap(multisig: ReturnType<typeof setup>) {
97
- await fundAddress(client, { address: multisig.account })
98
- const transaction = TxEnvelopeTempo.from({
99
- calls: [{ to: '0x0000000000000000000000000000000000000000' }],
100
- chainId,
101
- feeToken: '0x20c0000000000000000000000000000000000001',
102
- nonce: 0n,
103
- gas: 5_000_000n,
104
- maxFeePerGas: Value.fromGwei('20'),
105
- maxPriorityFeePerGas: Value.fromGwei('10'),
106
- })
107
- const receipt = await send(
108
- TxEnvelopeTempo.serialize(transaction, {
109
- signature: SignatureEnvelope.from({
110
- account: multisig.account,
111
- config: multisig.initialConfig,
112
- signatures: approve({
113
- config: multisig.initialConfig,
114
- payload: TxEnvelopeTempo.getSignPayload(transaction),
115
- signers: multisig.ownerKeys,
116
- }),
117
- }),
118
- }),
119
- )
120
- expect(receipt.status).toBe('success')
56
+ function sign(account: Awaited<ReturnType<typeof setup>>, payload: Hex.Hex) {
57
+ const value = {
58
+ account: account.account,
59
+ config: account.config,
60
+ payload,
121
61
  }
122
-
123
- function accessKeySignature(parameters: {
124
- accessKey: { privateKey: Hex.Hex }
125
- account: Address.Address
126
- transaction: TxEnvelopeTempo.TxEnvelopeTempo
127
- }) {
128
- const { accessKey, account, transaction } = parameters
129
- return SignatureEnvelope.from({
130
- inner: SignatureEnvelope.from(
131
- Secp256k1.sign({
132
- payload: TxEnvelopeTempo.getSignPayload(transaction, {
133
- from: account,
134
- }),
135
- privateKey: accessKey.privateKey,
136
- }),
137
- ),
138
- type: 'keychain',
139
- userAddress: account,
140
- })
141
- }
142
-
143
- test('behavior: fills a multisig simulation', async () => {
144
- const { account, initialConfig, ownerKeys } = setup({
145
- count: 2,
146
- threshold: 2,
147
- })
148
- await fundAddress(client, { address: account })
149
-
150
- const request = TransactionRequest.toRpc({
151
- calls: [{ to: '0x0000000000000000000000000000000000000000' }],
152
- from: account,
153
- multisigSimulation: {
154
- account,
155
- approvals: ownerKeys.map((owner) => ({
156
- keyType: 'secp256k1',
157
- owner: owner.address,
158
- type: 'primitive',
159
- })),
160
- config: initialConfig,
161
- },
162
- })
163
- expect(request.multisigSimulation?.config).toBeTypeOf('string')
164
-
165
- const response = await client.request({
166
- method: 'eth_fillTransaction',
167
- params: [request as never],
168
- })
169
- const transaction = TransactionRequest.fromRpc(
170
- response.tx as TransactionRequest.Rpc,
171
- )
172
-
173
- expect(Hex.validate(response.raw)).toBe(true)
174
- expect(transaction.calls).toHaveLength(1)
175
- expect(transaction.calls?.[0]?.to).toBe(
176
- '0x0000000000000000000000000000000000000000',
177
- )
178
- expect(transaction.chainId).toBe(chainId)
179
- expect(transaction.feeToken).toBeNull()
180
- expect(transaction.gas).toBeGreaterThan(0n)
181
- expect(transaction.maxFeePerGas).toBeTypeOf('bigint')
182
- expect(transaction.maxPriorityFeePerGas).toBeTypeOf('bigint')
183
- expect(transaction.nonce ?? 0n).toBe(0n)
184
- expect(transaction.nonceKey ?? 0n).toBe(0n)
185
- expect(transaction.type).toBe('tempo')
62
+ const digest = MultisigConfig.getSignPayload(value)
63
+ return SignatureEnvelope.from({
64
+ account: account.account,
65
+ config: account.config,
66
+ signatures: SignatureEnvelope.sortMultisigApprovals({
67
+ ...value,
68
+ signatures: account.keys
69
+ .slice(0, 2)
70
+ .map((key) =>
71
+ SignatureEnvelope.from(
72
+ Secp256k1.sign({ payload: digest, privateKey: key.privateKey }),
73
+ ),
74
+ ),
75
+ }),
186
76
  })
187
-
188
- test('examples: bootstrap, initialized, and configuration rotation', async () => {
189
- const { account, initialConfig, ownerKeys } = setup({
190
- count: 3,
191
- threshold: 2,
192
- })
193
- const replacement = createKey()
194
- const rotatedConfig = MultisigConfig.from({
195
- owners: [{ owner: replacement.address, weight: 1 }],
196
- salt: initialConfig.salt,
197
- threshold: 1,
198
- version: 1,
199
- })
200
-
201
- await fundAddress(client, { address: account })
202
-
203
- const bootstrap = TxEnvelopeTempo.from({
204
- calls: [{ to: '0x0000000000000000000000000000000000000000' }],
205
- chainId,
206
- feeToken: '0x20c0000000000000000000000000000000000001',
207
- nonce: 0n,
208
- gas: 5_000_000n,
209
- maxFeePerGas: Value.fromGwei('20'),
210
- maxPriorityFeePerGas: Value.fromGwei('10'),
211
- })
212
-
213
- const bootstrap_signed = TxEnvelopeTempo.serialize(bootstrap, {
214
- signature: SignatureEnvelope.from({
215
- account,
216
- config: initialConfig,
217
- signatures: approve({
218
- config: initialConfig,
219
- payload: TxEnvelopeTempo.getSignPayload(bootstrap),
220
- signers: [ownerKeys[0]!, ownerKeys[1]!],
221
- }),
222
- }),
223
- })
224
-
225
- const bootstrap_receipt = (await client
226
- .request({
227
- method: 'eth_sendRawTransactionSync',
228
- params: [bootstrap_signed],
77
+ }
78
+
79
+ async function submit(
80
+ tx: TxEnvelopeTempo.TxEnvelopeTempo,
81
+ signature: SignatureEnvelope.SignatureEnvelope,
82
+ ) {
83
+ const result = await client.request({
84
+ method: 'eth_sendRawTransactionSync',
85
+ params: [TxEnvelopeTempo.serialize(tx, { signature })],
86
+ })
87
+ return TransactionReceipt.fromRpc(result as unknown as TransactionReceipt.Rpc)
88
+ }
89
+
90
+ describe('behavior: configurable accounts', () => {
91
+ test('registers, spends, and round-trips the RPC signature', async () => {
92
+ const account = await setup()
93
+ for (const nonce of [0n, 1n]) {
94
+ const tx = transaction(nonce)
95
+ const signature = sign(account, TxEnvelopeTempo.getSignPayload(tx))
96
+ const receipt = await submit(tx, signature)
97
+ expect(receipt.status).toMatchInlineSnapshot('"success"')
98
+ expect(receipt.from).toBe(account.account)
99
+ const rpc = await client.request({
100
+ method: 'eth_getTransactionByHash',
101
+ params: [receipt.transactionHash],
229
102
  })
230
- .then((tx) => TransactionReceipt.fromRpc(tx as any)))!
231
- expect(bootstrap_receipt).toBeDefined()
232
- expect(bootstrap_receipt.status).toBe('success')
233
- expect(bootstrap_receipt.from).toBe(account)
234
-
235
- {
236
- const response = await client
237
- .request({
238
- method: 'eth_getTransactionByHash',
239
- params: [bootstrap_receipt.transactionHash],
240
- })
241
- .then((tx) => Transaction.fromRpc(tx as any))
242
- if (!response) throw new Error()
243
- expect(response.from).toBe(account)
244
- expect(response.signature?.type).toBe('multisig')
245
- expect(
246
- (response.signature as SignatureEnvelope.Multisig | undefined)?.config,
247
- ).toEqual(initialConfig)
103
+ const returned = Transaction.fromRpc(rpc as unknown as Transaction.Rpc)
104
+ expect(returned.signature).toEqual(signature)
248
105
  }
249
-
250
- const updateNonce = await getTransactionCount(client, {
251
- address: account,
252
- blockTag: 'pending',
253
- })
254
- const update = TxEnvelopeTempo.from({
255
- calls: [
256
- {
257
- to: '0xaacc000000000000000000000000000000000000',
258
- data: AbiFunction.encodeData(updateConfig, [
259
- initialConfig,
260
- rotatedConfig.threshold,
261
- rotatedConfig.owners,
262
- ]),
263
- },
264
- ],
265
- chainId,
266
- feeToken: '0x20c0000000000000000000000000000000000001',
267
- nonce: BigInt(updateNonce),
268
- gas: 5_000_000n,
269
- maxFeePerGas: Value.fromGwei('20'),
270
- maxPriorityFeePerGas: Value.fromGwei('10'),
271
- })
272
- const updateSigned = TxEnvelopeTempo.serialize(update, {
273
- signature: SignatureEnvelope.from({
274
- account,
275
- config: initialConfig,
276
- signatures: approve({
277
- config: initialConfig,
278
- payload: TxEnvelopeTempo.getSignPayload(update),
279
- signers: [ownerKeys[0]!, ownerKeys[1]!],
280
- }),
281
- }),
282
- })
283
- const updateReceipt = (await client
284
- .request({
285
- method: 'eth_sendRawTransactionSync',
286
- params: [updateSigned],
287
- })
288
- .then((tx) => TransactionReceipt.fromRpc(tx as any)))!
289
- expect(updateReceipt.status).toBe('success')
290
-
291
- const nonce = await getTransactionCount(client, {
292
- address: account,
293
- blockTag: 'pending',
294
- })
295
-
296
- const spend = TxEnvelopeTempo.from({
297
- calls: [{ to: '0x0000000000000000000000000000000000000000' }],
298
- chainId,
299
- feeToken: '0x20c0000000000000000000000000000000000001',
300
- nonce: BigInt(nonce),
301
- gas: 5_000_000n,
302
- maxFeePerGas: Value.fromGwei('20'),
303
- maxPriorityFeePerGas: Value.fromGwei('10'),
304
- })
305
-
306
- const spend_signed = TxEnvelopeTempo.serialize(spend, {
307
- signature: SignatureEnvelope.from({
308
- account,
309
- config: rotatedConfig,
310
- signatures: approve({
311
- address: account,
312
- config: rotatedConfig,
313
- payload: TxEnvelopeTempo.getSignPayload(spend),
314
- signers: [replacement],
315
- }),
316
- }),
317
- })
318
-
319
- const spend_receipt = (await client
320
- .request({
321
- method: 'eth_sendRawTransactionSync',
322
- params: [spend_signed],
323
- })
324
- .then((tx) => TransactionReceipt.fromRpc(tx as any)))!
325
- expect(spend_receipt).toBeDefined()
326
- expect(spend_receipt.status).toBe('success')
327
- expect(spend_receipt.from).toBe(account)
328
106
  })
329
107
 
330
- test('example: nested ownership', async () => {
331
- const child = setup({ count: 1, threshold: 1 })
332
- await fundAddress(client, { address: child.account })
333
-
334
- const childBootstrap = TxEnvelopeTempo.from({
335
- calls: [{ to: '0x0000000000000000000000000000000000000000' }],
336
- chainId,
337
- feeToken: '0x20c0000000000000000000000000000000000001',
338
- nonce: 0n,
339
- gas: 5_000_000n,
340
- maxFeePerGas: Value.fromGwei('20'),
341
- maxPriorityFeePerGas: Value.fromGwei('10'),
342
- })
343
- const childBootstrapSigned = TxEnvelopeTempo.serialize(childBootstrap, {
344
- signature: SignatureEnvelope.from({
345
- account: child.account,
346
- config: child.initialConfig,
347
- signatures: approve({
348
- config: child.initialConfig,
349
- payload: TxEnvelopeTempo.getSignPayload(childBootstrap),
350
- signers: child.ownerKeys,
351
- }),
352
- }),
353
- })
354
- const childReceipt = (await client
355
- .request({
356
- method: 'eth_sendRawTransactionSync',
357
- params: [childBootstrapSigned],
358
- })
359
- .then((tx) => TransactionReceipt.fromRpc(tx as any)))!
360
- expect(childReceipt.status).toBe('success')
361
-
362
- const initialConfig = MultisigConfig.from({
363
- salt: Hex.random(32),
364
- threshold: 1,
365
- owners: [{ owner: child.account, weight: 1 }],
366
- })
367
- const account = MultisigConfig.getAddress(initialConfig)
368
- await fundAddress(client, { address: account })
369
-
370
- const bootstrap = TxEnvelopeTempo.from({
371
- calls: [{ to: '0x0000000000000000000000000000000000000000' }],
372
- chainId,
373
- feeToken: '0x20c0000000000000000000000000000000000001',
374
- nonce: 0n,
375
- gas: 5_000_000n,
376
- maxFeePerGas: Value.fromGwei('20'),
377
- maxPriorityFeePerGas: Value.fromGwei('10'),
378
- })
379
- const digest = MultisigConfig.getSignPayload({
380
- account,
381
- config: initialConfig,
382
- payload: TxEnvelopeTempo.getSignPayload(bootstrap),
383
- })
384
- const nested = SignatureEnvelope.from({
385
- account: child.account,
386
- config: child.initialConfig,
387
- signatures: approve({
388
- config: child.initialConfig,
389
- payload: digest,
390
- signers: child.ownerKeys,
391
- }),
392
- })
393
- const bootstrapSigned = TxEnvelopeTempo.serialize(bootstrap, {
394
- signature: SignatureEnvelope.from({
395
- account,
396
- config: initialConfig,
397
- signatures: [nested],
398
- }),
399
- })
400
-
401
- const receipt = (await client
402
- .request({
403
- method: 'eth_sendRawTransactionSync',
404
- params: [bootstrapSigned],
405
- })
406
- .then((tx) => TransactionReceipt.fromRpc(tx as any)))!
407
- expect(receipt.status).toBe('success')
408
- expect(receipt.from).toBe(account)
409
-
410
- const transaction = TxEnvelopeTempo.from({
411
- calls: [{ to: '0x0000000000000000000000000000000000000000' }],
412
- chainId,
413
- feeToken: '0x20c0000000000000000000000000000000000001',
414
- nonce: 1n,
415
- gas: 5_000_000n,
416
- maxFeePerGas: Value.fromGwei('20'),
417
- maxPriorityFeePerGas: Value.fromGwei('10'),
418
- })
419
- const parentDigest = MultisigConfig.getSignPayload({
420
- account,
421
- config: initialConfig,
422
- payload: TxEnvelopeTempo.getSignPayload(transaction),
423
- })
424
- const childSignature = SignatureEnvelope.from({
425
- account: child.account,
426
- config: child.initialConfig,
427
- signatures: approve({
428
- config: child.initialConfig,
429
- payload: parentDigest,
430
- signers: child.ownerKeys,
431
- }),
432
- })
433
- const serialized = TxEnvelopeTempo.serialize(transaction, {
434
- signature: SignatureEnvelope.from({
435
- account,
436
- config: initialConfig,
437
- signatures: [childSignature],
438
- }),
439
- })
440
- const nestedReceipt = (await client
441
- .request({
442
- method: 'eth_sendRawTransactionSync',
443
- params: [serialized],
444
- })
445
- .then((tx) => TransactionReceipt.fromRpc(tx as any)))!
446
- expect(nestedReceipt.status).toBe('success')
447
- expect(nestedReceipt.from).toBe(account)
108
+ test('selects operation approvals and submits the serialized transaction', async () => {
109
+ const account = await setup()
110
+ const tx = transaction(0n)
111
+ const serialized = TxEnvelopeTempo.serialize(tx)
112
+ const hash = MultisigOperation.getHash({
113
+ account: account.account,
114
+ config: account.config,
115
+ transaction: serialized,
116
+ type: 'transaction',
117
+ })
118
+ const selected = await MultisigOperation.selectApprovals({
119
+ account: account.account,
120
+ approvals: account.keys.map(({ privateKey }) =>
121
+ SignatureEnvelope.serialize(
122
+ SignatureEnvelope.from(Secp256k1.sign({ payload: hash, privateKey })),
123
+ ),
124
+ ),
125
+ config: account.config,
126
+ hash,
127
+ })
128
+ const operation = MultisigOperation.from({
129
+ account: account.account,
130
+ approvals: selected.approvals,
131
+ config: account.config,
132
+ createdAt: 1,
133
+ hash,
134
+ signatureCount: selected.signatureCount,
135
+ status: 'pending',
136
+ threshold: selected.threshold,
137
+ transaction: serialized,
138
+ type: 'transaction',
139
+ updatedAt: 1,
140
+ weight: selected.weight,
141
+ })
142
+ const signed = MultisigOperation.serializeTransaction(operation, {
143
+ approvals: selected.selectedApprovals,
144
+ })
145
+ const result = await client.request({
146
+ method: 'eth_sendRawTransactionSync',
147
+ params: [signed],
148
+ })
149
+ const receipt = TransactionReceipt.fromRpc(
150
+ result as unknown as TransactionReceipt.Rpc,
151
+ )
152
+ expect(receipt.status).toMatchInlineSnapshot('"success"')
153
+ expect(receipt.from).toBe(account.account)
448
154
  })
449
155
 
450
- test('example: fee sponsorship (owners sign first)', async () => {
451
- const multisig = setup({ count: 2, threshold: 2 })
452
- const feePayer = createKey()
453
- await Promise.all([
454
- bootstrap(multisig),
455
- fundAddress(client, { address: feePayer.address }),
456
- ])
457
-
458
- const transaction = TxEnvelopeTempo.from({
459
- calls: [{ to: '0x0000000000000000000000000000000000000000' }],
460
- chainId,
461
- feePayerSignature: null,
462
- nonce: 1n,
156
+ test('simulates registration with the full config without persisting it', async () => {
157
+ const account = await setup()
158
+ const data = AbiFunction.encodeData(
159
+ AbiFunction.from(
160
+ 'function getConfigCommitment(address account) view returns (bytes32)',
161
+ ),
162
+ [account.account],
163
+ )
164
+ const request = TransactionRequest.toRpc({
165
+ from: account.account,
166
+ to: precompile,
167
+ data,
463
168
  gas: 5_000_000n,
464
- maxFeePerGas: Value.fromGwei('20'),
465
- maxPriorityFeePerGas: Value.fromGwei('10'),
466
- })
467
- const signedByOwners = TxEnvelopeTempo.from(transaction, {
468
- signature: SignatureEnvelope.from({
469
- account: multisig.account,
470
- config: multisig.initialConfig,
471
- signatures: approve({
472
- config: multisig.initialConfig,
473
- payload: TxEnvelopeTempo.getSignPayload(transaction),
474
- signers: multisig.ownerKeys,
475
- }),
476
- }),
477
- })
478
- const sponsored = TxEnvelopeTempo.from({
479
- ...signedByOwners,
480
169
  feeToken: '0x20c0000000000000000000000000000000000001',
170
+ multisigSimulation: {
171
+ config: account.config,
172
+ approvals: account.config.owners
173
+ .slice(0, 2)
174
+ .map(({ owner }) => ({ owner, keyType: 'secp256k1' })),
175
+ },
481
176
  })
482
- const feePayerSignature = Secp256k1.sign({
483
- payload: TxEnvelopeTempo.getFeePayerSignPayload(sponsored, {
484
- sender: multisig.account,
485
- }),
486
- privateKey: feePayer.privateKey,
177
+ const simulated = await client.request({
178
+ method: 'eth_call',
179
+ params: [request as never, 'latest'],
487
180
  })
488
- const receipt = await send(
489
- TxEnvelopeTempo.serialize(sponsored, { feePayerSignature }),
490
- )
491
- expect(receipt.status).toBe('success')
492
- expect(receipt.from).toBe(multisig.account)
493
- expect(receipt.feePayer).toBe(feePayer.address)
494
- })
495
-
496
- test('example: fee sponsorship (fee payer signs first)', async () => {
497
- const multisig = setup({ count: 2, threshold: 2 })
498
- const feePayer = createKey()
499
- await Promise.all([
500
- bootstrap(multisig),
501
- fundAddress(client, { address: feePayer.address }),
502
- ])
503
-
504
- const transaction = TxEnvelopeTempo.from({
505
- calls: [{ to: '0x0000000000000000000000000000000000000000' }],
506
- chainId,
507
- feePayerSignature: null,
508
- feeToken: '0x20c0000000000000000000000000000000000001',
509
- nonce: 1n,
510
- gas: 5_000_000n,
511
- maxFeePerGas: Value.fromGwei('20'),
512
- maxPriorityFeePerGas: Value.fromGwei('10'),
181
+ expect(simulated).not.toBe(MultisigConfig.zeroSalt)
182
+ const estimate = await client.request({
183
+ method: 'eth_estimateGas',
184
+ params: [request as never],
513
185
  })
514
- const feePayerSignature = Secp256k1.sign({
515
- payload: TxEnvelopeTempo.getFeePayerSignPayload(transaction, {
516
- sender: multisig.account,
186
+ expect(Hex.toBigInt(estimate)).toBeGreaterThan(0n)
187
+ const persisted = await client.request({
188
+ method: 'eth_call',
189
+ params: [{ to: precompile, data }, 'latest'],
190
+ })
191
+ expect(persisted).toBe(MultisigConfig.zeroSalt)
192
+ const tx = transaction(0n)
193
+ await submit(tx, sign(account, TxEnvelopeTempo.getSignPayload(tx)))
194
+ expect(
195
+ await client.request({
196
+ method: 'eth_call',
197
+ params: [{ to: precompile, data }, 'latest'],
517
198
  }),
518
- privateKey: feePayer.privateKey,
519
- })
520
- const sponsored = TxEnvelopeTempo.from(transaction, { feePayerSignature })
521
- const receipt = await send(
522
- TxEnvelopeTempo.serialize(sponsored, {
523
- signature: SignatureEnvelope.from({
524
- account: multisig.account,
525
- config: multisig.initialConfig,
526
- signatures: approve({
527
- config: multisig.initialConfig,
528
- payload: TxEnvelopeTempo.getSignPayload(sponsored),
529
- signers: multisig.ownerKeys,
530
- }),
531
- }),
532
- }),
533
- )
534
- expect(receipt.status).toBe('success')
535
- expect(receipt.from).toBe(multisig.account)
536
- expect(receipt.feePayer).toBe(feePayer.address)
199
+ ).toBe(simulated)
537
200
  })
538
201
 
539
- test('example: weighted quorum', async () => {
540
- const { account, initialConfig, ownerKeys } = setup({
541
- count: 3,
542
- threshold: 3,
543
- weights: [2, 1, 1],
544
- })
545
-
546
- await fundAddress(client, { address: account })
547
-
548
- const transaction = (nonce: bigint) =>
549
- TxEnvelopeTempo.from({
550
- calls: [{ to: '0x0000000000000000000000000000000000000000' }],
551
- chainId,
552
- feeToken: '0x20c0000000000000000000000000000000000001',
553
- nonce,
554
- gas: 5_000_000n,
555
- maxFeePerGas: Value.fromGwei('20'),
556
- maxPriorityFeePerGas: Value.fromGwei('10'),
557
- })
558
- const serialize = (
559
- value: ReturnType<typeof transaction>,
560
- signers: readonly { privateKey: Hex.Hex }[],
561
- ) =>
562
- TxEnvelopeTempo.serialize(value, {
563
- signature: SignatureEnvelope.from({
564
- account,
565
- config: initialConfig,
566
- signatures: approve({
567
- config: initialConfig,
568
- payload: TxEnvelopeTempo.getSignPayload(value),
569
- signers,
570
- }),
571
- }),
572
- })
573
-
574
- const bootstrap = transaction(0n)
575
- const bootstrapReceipt = (await client
576
- .request({
577
- method: 'eth_sendRawTransactionSync',
578
- params: [serialize(bootstrap, [ownerKeys[0]!, ownerKeys[1]!])],
579
- })
580
- .then((tx) => TransactionReceipt.fromRpc(tx as any)))!
581
- expect(bootstrapReceipt.status).toBe('success')
582
-
583
- const valid = transaction(1n)
584
- const validReceipt = (await client
585
- .request({
586
- method: 'eth_sendRawTransactionSync',
587
- params: [serialize(valid, [ownerKeys[0]!, ownerKeys[2]!])],
588
- })
589
- .then((tx) => TransactionReceipt.fromRpc(tx as any)))!
590
- expect(validReceipt.status).toBe('success')
591
-
592
- const belowThreshold = transaction(2n)
202
+ test('rotates config and rejects stale witnesses and signatures', async () => {
203
+ const account = await setup()
204
+ const replacement = account
205
+ const tx = transaction(0n, [
206
+ {
207
+ to: precompile,
208
+ data: AbiFunction.encodeData(updateConfig, [
209
+ { ...account.config, salt: account.config.salt!, version: 0n },
210
+ 2,
211
+ replacement.config.owners,
212
+ ]),
213
+ },
214
+ ])
215
+ expect(
216
+ (await submit(tx, sign(account, TxEnvelopeTempo.getSignPayload(tx))))
217
+ .status,
218
+ ).toMatchInlineSnapshot('"success"')
219
+ const spend = transaction(1n)
220
+ const old = sign(account, TxEnvelopeTempo.getSignPayload(spend))
221
+ await expect(submit(spend, old)).rejects.toThrow(/commitment mismatch/)
222
+ const rotated = {
223
+ ...replacement,
224
+ account: account.account,
225
+ config: { ...replacement.config, salt: account.config.salt, version: 1n },
226
+ }
593
227
  await expect(
594
- client.request({
595
- method: 'eth_sendRawTransactionSync',
596
- params: [serialize(belowThreshold, [ownerKeys[1]!, ownerKeys[2]!])],
597
- }),
228
+ submit(spend, { ...old, config: rotated.config }),
598
229
  ).rejects.toThrow()
230
+ expect(
231
+ (
232
+ await submit(
233
+ spend,
234
+ sign(rotated, TxEnvelopeTempo.getSignPayload(spend)),
235
+ )
236
+ ).status,
237
+ ).toMatchInlineSnapshot('"success"')
238
+ })
599
239
 
240
+ test('rejects insufficient approvals without registering the account', async () => {
241
+ const account = await setup()
242
+ const tx = transaction(0n)
243
+ const signature = sign(account, TxEnvelopeTempo.getSignPayload(tx))
600
244
  await expect(
601
- client.request({
602
- method: 'eth_sendRawTransactionSync',
603
- params: [serialize(belowThreshold, ownerKeys)],
245
+ submit(tx, {
246
+ ...signature,
247
+ signatures: signature.signatures.slice(0, 1),
604
248
  }),
605
- ).rejects.toThrow()
249
+ ).rejects.toThrow(/threshold|weight|quorum/)
250
+ expect((await submit(tx, signature)).status).toMatchInlineSnapshot(
251
+ '"success"',
252
+ )
606
253
  })
607
254
 
608
- test('behavior: keychain access key (authorize, spend, reject config update)', async () => {
609
- const { account, initialConfig, ownerKeys } = setup({
610
- count: 2,
611
- threshold: 2,
612
- })
613
-
614
- await fundAddress(client, { address: account })
615
-
616
- const access = (() => {
617
- const privateKey = Secp256k1.randomPrivateKey()
618
- const address = Address.fromPublicKey(
619
- Secp256k1.getPublicKey({ privateKey }),
620
- )
621
- return { address, privateKey } as const
622
- })()
623
-
624
- const bootstrap = TxEnvelopeTempo.from({
625
- calls: [{ to: '0x0000000000000000000000000000000000000000' }],
626
- chainId,
627
- feeToken: '0x20c0000000000000000000000000000000000001',
628
- nonce: 0n,
629
- gas: 5_000_000n,
630
- maxFeePerGas: Value.fromGwei('20'),
631
- maxPriorityFeePerGas: Value.fromGwei('10'),
632
- })
633
-
634
- const bootstrap_signed = TxEnvelopeTempo.serialize(bootstrap, {
635
- signature: SignatureEnvelope.from({
636
- account,
637
- config: initialConfig,
638
- signatures: approve({
639
- config: initialConfig,
640
- payload: TxEnvelopeTempo.getSignPayload(bootstrap),
641
- signers: ownerKeys,
642
- }),
643
- }),
255
+ test('authorizes a multisig delegate and preserves the grant after delegate rotation', async () => {
256
+ const parent = await setup()
257
+ const delegate = await setup()
258
+ const grant = KeyAuthorization.from({
259
+ address: delegate.account,
260
+ account: parent.account,
261
+ chainId: BigInt(chain.id),
262
+ type: 'multisig',
263
+ })
264
+ const signed = KeyAuthorization.from(grant, {
265
+ signature: sign(parent, KeyAuthorization.getSignPayload(grant)),
266
+ })
267
+ const tx = { ...transaction(0n), keyAuthorization: signed }
268
+ const signature = SignatureEnvelope.from({
269
+ userAddress: parent.account,
270
+ version: 'v2',
271
+ inner: sign(
272
+ delegate,
273
+ TxEnvelopeTempo.getSignPayload(tx, { from: parent.account }),
274
+ ),
644
275
  })
645
-
646
- const bootstrap_receipt = (await client
647
- .request({
648
- method: 'eth_sendRawTransactionSync',
649
- params: [bootstrap_signed],
650
- })
651
- .then((tx) => TransactionReceipt.fromRpc(tx as any)))!
652
- expect(bootstrap_receipt.status).toBe('success')
653
-
654
- // Multisig accounts authorize access keys through AccountKeychain.
655
- const authorizeKey = AbiFunction.from(
656
- 'function authorizeKey(address keyId, uint8 signatureType, (uint64 expiry, bool enforceLimits, (address token, uint256 amount, uint64 period)[] limits, bool allowAnyCalls, (address target, (bytes4 selector, address[] recipients)[] selectorRules)[] allowedCalls) config)',
276
+ expect((await submit(tx, signature)).status).toMatchInlineSnapshot(
277
+ '"success"',
657
278
  )
658
279
 
659
- const authorize = TxEnvelopeTempo.from({
660
- calls: [
661
- {
662
- to: '0xaaaaaaaa00000000000000000000000000000000',
663
- data: AbiFunction.encodeData(authorizeKey, [
664
- access.address,
665
- 0, // secp256k1
666
- {
667
- expiry: 18446744073709551615n, // u64 max (never expires)
668
- enforceLimits: false,
669
- limits: [],
670
- allowAnyCalls: true,
671
- allowedCalls: [],
672
- },
673
- ]),
674
- },
675
- ],
676
- chainId,
677
- feeToken: '0x20c0000000000000000000000000000000000001',
678
- nonce: 1n,
679
- gas: 5_000_000n,
680
- maxFeePerGas: Value.fromGwei('20'),
681
- maxPriorityFeePerGas: Value.fromGwei('10'),
682
- })
683
-
684
- const authorize_signed = TxEnvelopeTempo.serialize(authorize, {
685
- signature: SignatureEnvelope.from({
686
- account,
687
- config: initialConfig,
688
- signatures: approve({
689
- config: initialConfig,
690
- payload: TxEnvelopeTempo.getSignPayload(authorize),
691
- signers: ownerKeys,
692
- }),
693
- }),
694
- })
695
-
696
- const authorize_receipt = (await client
697
- .request({
698
- method: 'eth_sendRawTransactionSync',
699
- params: [authorize_signed],
700
- })
701
- .then((tx) => TransactionReceipt.fromRpc(tx as any)))!
702
- expect(authorize_receipt.status).toBe('success')
703
- expect(authorize_receipt.from).toBe(account)
704
-
705
- const nonce = await getTransactionCount(client, {
706
- address: account,
707
- blockTag: 'pending',
708
- })
709
-
710
- const spend = TxEnvelopeTempo.from({
711
- calls: [{ to: '0x0000000000000000000000000000000000000000' }],
712
- chainId,
713
- feeToken: '0x20c0000000000000000000000000000000000001',
714
- nonce: BigInt(nonce),
715
- gas: 5_000_000n,
716
- maxFeePerGas: Value.fromGwei('20'),
717
- maxPriorityFeePerGas: Value.fromGwei('10'),
718
- })
719
-
720
- const spend_signature = Secp256k1.sign({
721
- payload: TxEnvelopeTempo.getSignPayload(spend, { from: account }),
722
- privateKey: access.privateKey,
723
- })
724
-
725
- const spend_signed = TxEnvelopeTempo.serialize(spend, {
726
- signature: SignatureEnvelope.from({
727
- userAddress: account,
728
- inner: SignatureEnvelope.from(spend_signature),
729
- type: 'keychain',
730
- }),
731
- })
732
-
733
- const spend_receipt = (await client
734
- .request({
735
- method: 'eth_sendRawTransactionSync',
736
- params: [spend_signed],
737
- })
738
- .then((tx) => TransactionReceipt.fromRpc(tx as any)))!
739
- expect(spend_receipt.status).toBe('success')
740
- expect(spend_receipt.from).toBe(account)
741
-
742
- {
743
- const response = await client
744
- .request({
745
- method: 'eth_getTransactionByHash',
746
- params: [spend_receipt.transactionHash],
747
- })
748
- .then((tx) => Transaction.fromRpc(tx as any))
749
- if (!response) throw new Error()
750
- expect(response.from).toBe(account)
751
- expect(response.signature?.type).toBe('keychain')
752
- expect(
753
- (response.signature as SignatureEnvelope.Keychain | undefined)
754
- ?.userAddress,
755
- ).toBe(account)
280
+ const replacement = await setup()
281
+ const rotation = transaction(0n, [
282
+ {
283
+ to: precompile,
284
+ data: AbiFunction.encodeData(updateConfig, [
285
+ { ...delegate.config, salt: delegate.config.salt!, version: 0n },
286
+ 2,
287
+ replacement.config.owners,
288
+ ]),
289
+ },
290
+ ])
291
+ expect(
292
+ (
293
+ await submit(
294
+ rotation,
295
+ sign(delegate, TxEnvelopeTempo.getSignPayload(rotation)),
296
+ )
297
+ ).status,
298
+ ).toMatchInlineSnapshot('"success"')
299
+ const spend = transaction(1n)
300
+ const rotated = {
301
+ ...replacement,
302
+ account: delegate.account,
303
+ config: {
304
+ ...replacement.config,
305
+ salt: delegate.config.salt,
306
+ version: 1n,
307
+ },
756
308
  }
757
-
758
- const updateNonce = await getTransactionCount(client, {
759
- address: account,
760
- blockTag: 'pending',
761
- })
762
- const update = TxEnvelopeTempo.from({
763
- calls: [
764
- {
765
- to: '0xaacc000000000000000000000000000000000000',
766
- data: AbiFunction.encodeData(updateConfig, [
767
- initialConfig,
768
- initialConfig.threshold,
769
- initialConfig.owners,
770
- ]),
771
- },
772
- ],
773
- chainId,
774
- feeToken: '0x20c0000000000000000000000000000000000001',
775
- nonce: BigInt(updateNonce),
776
- gas: 5_000_000n,
777
- maxFeePerGas: Value.fromGwei('20'),
778
- maxPriorityFeePerGas: Value.fromGwei('10'),
309
+ const payload = TxEnvelopeTempo.getSignPayload(spend, {
310
+ from: parent.account,
779
311
  })
780
- const updateSignature = Secp256k1.sign({
781
- payload: TxEnvelopeTempo.getSignPayload(update, { from: account }),
782
- privateKey: access.privateKey,
783
- })
784
- const updateSigned = TxEnvelopeTempo.serialize(update, {
785
- signature: SignatureEnvelope.from({
786
- userAddress: account,
787
- inner: SignatureEnvelope.from(updateSignature),
788
- type: 'keychain',
789
- }),
790
- })
791
-
792
- const updateReceipt = (await client
793
- .request({
794
- method: 'eth_sendRawTransactionSync',
795
- params: [updateSigned],
796
- })
797
- .then((tx) => TransactionReceipt.fromRpc(tx as any)))!
798
- expect(updateReceipt.status).toBe('reverted')
799
- })
800
-
801
- test('example: bootstrap and immediate access key use', async () => {
802
- const multisig = setup({ count: 2, threshold: 2 })
803
- const accessKey = createKey()
804
- await fundAddress(client, { address: multisig.account })
805
-
806
- const keyAuthorization = KeyAuthorization.from({
807
- account: multisig.account,
808
- address: accessKey.address,
809
- chainId: BigInt(chainId),
810
- isAdmin: false,
811
- type: 'secp256k1',
812
- })
813
- const keyAuthorizationSigned = KeyAuthorization.from(keyAuthorization, {
814
- signature: SignatureEnvelope.from({
815
- account: multisig.account,
816
- config: multisig.initialConfig,
817
- signatures: approve({
818
- config: multisig.initialConfig,
819
- payload: KeyAuthorization.getSignPayload(keyAuthorization),
820
- signers: multisig.ownerKeys,
312
+ await expect(
313
+ submit(
314
+ spend,
315
+ SignatureEnvelope.from({
316
+ userAddress: parent.account,
317
+ version: 'v2',
318
+ inner: sign(delegate, payload),
821
319
  }),
822
- }),
823
- })
824
- const transaction = TxEnvelopeTempo.from({
825
- calls: [{ to: '0x0000000000000000000000000000000000000000' }],
826
- chainId,
827
- feeToken: '0x20c0000000000000000000000000000000000001',
828
- keyAuthorization: keyAuthorizationSigned,
829
- nonce: 0n,
830
- gas: 5_000_000n,
831
- maxFeePerGas: Value.fromGwei('20'),
832
- maxPriorityFeePerGas: Value.fromGwei('10'),
320
+ ),
321
+ ).rejects.toThrow(/commitment mismatch/)
322
+ expect(
323
+ (
324
+ await submit(
325
+ spend,
326
+ SignatureEnvelope.from({
327
+ userAddress: parent.account,
328
+ version: 'v2',
329
+ inner: sign(rotated, payload),
330
+ }),
331
+ )
332
+ ).status,
333
+ ).toMatchInlineSnapshot('"success"')
334
+ const unauthorized = transaction(2n, [
335
+ {
336
+ to: precompile,
337
+ data: AbiFunction.encodeData(updateConfig, [
338
+ { ...parent.config, salt: parent.config.salt!, version: 0n },
339
+ 1,
340
+ parent.config.owners,
341
+ ]),
342
+ },
343
+ ])
344
+ const unauthorizedSignature = SignatureEnvelope.from({
345
+ userAddress: parent.account,
346
+ version: 'v2',
347
+ inner: sign(
348
+ rotated,
349
+ TxEnvelopeTempo.getSignPayload(unauthorized, { from: parent.account }),
350
+ ),
833
351
  })
834
- const receipt = await send(
835
- TxEnvelopeTempo.serialize(transaction, {
836
- signature: accessKeySignature({
837
- accessKey,
838
- account: multisig.account,
839
- transaction,
840
- }),
841
- }),
842
- )
843
- expect(receipt.status).toBe('success')
844
- expect(receipt.from).toBe(multisig.account)
352
+ expect(
353
+ (await submit(unauthorized, unauthorizedSignature)).status,
354
+ ).toMatchInlineSnapshot('"reverted"')
845
355
  })
846
356
 
847
- test('example: bootstrap and subsequent access key use', async () => {
848
- const multisig = setup({ count: 2, threshold: 2 })
849
- const accessKey = createKey()
850
- await fundAddress(client, { address: multisig.account })
851
-
852
- const keyAuthorization = KeyAuthorization.from({
853
- account: multisig.account,
854
- address: accessKey.address,
855
- chainId: BigInt(chainId),
856
- isAdmin: false,
357
+ test('rejects a grant signed by an individual owner instead of the account quorum', async () => {
358
+ const account = await setup()
359
+ const key = account.keys[0]!
360
+ const grant = KeyAuthorization.from({
361
+ address: key.address,
362
+ account: account.account,
363
+ chainId: BigInt(chain.id),
857
364
  type: 'secp256k1',
858
365
  })
859
- const keyAuthorizationSigned = KeyAuthorization.from(keyAuthorization, {
860
- signature: SignatureEnvelope.from({
861
- account: multisig.account,
862
- config: multisig.initialConfig,
863
- signatures: approve({
864
- config: multisig.initialConfig,
865
- payload: KeyAuthorization.getSignPayload(keyAuthorization),
866
- signers: multisig.ownerKeys,
867
- }),
868
- }),
869
- })
870
- const bootstrapTransaction = TxEnvelopeTempo.from({
871
- calls: [{ to: '0x0000000000000000000000000000000000000000' }],
872
- chainId,
873
- feeToken: '0x20c0000000000000000000000000000000000001',
874
- keyAuthorization: keyAuthorizationSigned,
875
- nonce: 0n,
876
- gas: 5_000_000n,
877
- maxFeePerGas: Value.fromGwei('20'),
878
- maxPriorityFeePerGas: Value.fromGwei('10'),
879
- })
880
- const bootstrapReceipt = await send(
881
- TxEnvelopeTempo.serialize(bootstrapTransaction, {
882
- signature: SignatureEnvelope.from({
883
- account: multisig.account,
884
- config: multisig.initialConfig,
885
- signatures: approve({
886
- config: multisig.initialConfig,
887
- payload: TxEnvelopeTempo.getSignPayload(bootstrapTransaction),
888
- signers: multisig.ownerKeys,
889
- }),
890
- }),
366
+ const signed = KeyAuthorization.from(grant, {
367
+ signature: Secp256k1.sign({
368
+ payload: KeyAuthorization.getSignPayload(grant),
369
+ privateKey: key.privateKey,
891
370
  }),
892
- )
893
- expect(bootstrapReceipt.status).toBe('success')
894
-
895
- const transaction = TxEnvelopeTempo.from({
896
- calls: [{ to: '0x0000000000000000000000000000000000000000' }],
897
- chainId,
898
- feeToken: '0x20c0000000000000000000000000000000000001',
899
- nonce: 1n,
900
- gas: 5_000_000n,
901
- maxFeePerGas: Value.fromGwei('20'),
902
- maxPriorityFeePerGas: Value.fromGwei('10'),
903
371
  })
904
- const receipt = await send(
905
- TxEnvelopeTempo.serialize(transaction, {
906
- signature: accessKeySignature({
907
- accessKey,
908
- account: multisig.account,
909
- transaction,
910
- }),
911
- }),
912
- )
913
- expect(receipt.status).toBe('success')
914
- expect(receipt.from).toBe(multisig.account)
372
+ const tx = { ...transaction(0n), keyAuthorization: signed }
373
+ await expect(
374
+ submit(tx, sign(account, TxEnvelopeTempo.getSignPayload(tx))),
375
+ ).rejects.toThrow()
915
376
  })
916
377
  })