ox 1.0.2 → 1.0.3

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 (53) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/tempo/KeyAuthorization.d.ts +22 -16
  3. package/dist/tempo/KeyAuthorization.d.ts.map +1 -1
  4. package/dist/tempo/KeyAuthorization.js +31 -7
  5. package/dist/tempo/KeyAuthorization.js.map +1 -1
  6. package/dist/tempo/MultisigConfig.d.ts +8 -4
  7. package/dist/tempo/MultisigConfig.d.ts.map +1 -1
  8. package/dist/tempo/MultisigConfig.js +16 -4
  9. package/dist/tempo/MultisigConfig.js.map +1 -1
  10. package/dist/tempo/SignatureEnvelope.d.ts +31 -13
  11. package/dist/tempo/SignatureEnvelope.d.ts.map +1 -1
  12. package/dist/tempo/SignatureEnvelope.js +172 -55
  13. package/dist/tempo/SignatureEnvelope.js.map +1 -1
  14. package/dist/zod/tempo/AuthorizationTempo.d.ts +70 -60
  15. package/dist/zod/tempo/AuthorizationTempo.d.ts.map +1 -1
  16. package/dist/zod/tempo/KeyAuthorization.d.ts +0 -306
  17. package/dist/zod/tempo/KeyAuthorization.d.ts.map +1 -1
  18. package/dist/zod/tempo/KeyAuthorization.js +2 -2
  19. package/dist/zod/tempo/KeyAuthorization.js.map +1 -1
  20. package/dist/zod/tempo/MultisigConfig.d.ts.map +1 -1
  21. package/dist/zod/tempo/MultisigConfig.js +5 -2
  22. package/dist/zod/tempo/MultisigConfig.js.map +1 -1
  23. package/dist/zod/tempo/RpcSchemaTempo.d.ts +28 -132
  24. package/dist/zod/tempo/RpcSchemaTempo.d.ts.map +1 -1
  25. package/dist/zod/tempo/SignatureEnvelope.d.ts +79 -18
  26. package/dist/zod/tempo/SignatureEnvelope.d.ts.map +1 -1
  27. package/dist/zod/tempo/SignatureEnvelope.js +39 -11
  28. package/dist/zod/tempo/SignatureEnvelope.js.map +1 -1
  29. package/dist/zod/tempo/Transaction.d.ts +84 -396
  30. package/dist/zod/tempo/Transaction.d.ts.map +1 -1
  31. package/dist/zod/tempo/TransactionRequest.d.ts +42 -234
  32. package/dist/zod/tempo/TransactionRequest.d.ts.map +1 -1
  33. package/dist/zod/tempo/TxEnvelopeTempo.d.ts +0 -108
  34. package/dist/zod/tempo/TxEnvelopeTempo.d.ts.map +1 -1
  35. package/package.json +1 -1
  36. package/src/tempo/KeyAuthorization.test-d.ts +62 -0
  37. package/src/tempo/KeyAuthorization.test.ts +53 -0
  38. package/src/tempo/KeyAuthorization.ts +69 -27
  39. package/src/tempo/MultisigConfig.test.ts +34 -2
  40. package/src/tempo/MultisigConfig.ts +18 -4
  41. package/src/tempo/SignatureEnvelope.test-d.ts +97 -0
  42. package/src/tempo/SignatureEnvelope.test.ts +247 -36
  43. package/src/tempo/SignatureEnvelope.ts +271 -85
  44. package/src/tempo/e2e.test.ts +0 -416
  45. package/src/tempo/multisig.e2e.test.ts +582 -0
  46. package/src/version.ts +1 -1
  47. package/src/zod/tempo/KeyAuthorization.ts +2 -2
  48. package/src/zod/tempo/MultisigConfig.ts +13 -5
  49. package/src/zod/tempo/SignatureEnvelope.ts +81 -18
  50. package/src/zod/tempo/_test/KeyAuthorization.test.ts +25 -0
  51. package/src/zod/tempo/_test/MultisigConfig.test.ts +44 -0
  52. package/src/zod/tempo/_test/SignatureEnvelope.test-d.ts +9 -0
  53. package/src/zod/tempo/_test/SignatureEnvelope.test.ts +142 -6
@@ -0,0 +1,582 @@
1
+ import { AbiFunction, Address, Hex, Secp256k1, Value } from 'ox'
2
+ import { getTransactionCount } from 'viem/actions'
3
+ import { describe, expect, test } from 'vp/test'
4
+ import { chain, client, fundAddress } from '../../test/tempo/config.js'
5
+ import { KeyAuthorization, MultisigConfig, SignatureEnvelope } from './index.js'
6
+ import * as Transaction from './Transaction.js'
7
+ import * as TransactionReceipt from './TransactionReceipt.js'
8
+ import * as TxEnvelopeTempo from './TxEnvelopeTempo.js'
9
+
10
+ const chainId = chain.id
11
+
12
+ describe('behavior: multisig (TIP-1061)', () => {
13
+ function setup(parameters: { count: number; threshold: number }) {
14
+ const { count, threshold } = parameters
15
+ const ownerKeys = Array.from({ length: count }, () => {
16
+ const privateKey = Secp256k1.randomPrivateKey()
17
+ const address = Address.fromPublicKey(
18
+ Secp256k1.getPublicKey({ privateKey }),
19
+ )
20
+ return { address, privateKey } as const
21
+ })
22
+
23
+ const genesisConfig = MultisigConfig.from({
24
+ salt: Hex.random(32),
25
+ threshold,
26
+ owners: ownerKeys.map((key) => ({
27
+ owner: key.address,
28
+ weight: 1,
29
+ })),
30
+ })
31
+ const account = MultisigConfig.getAddress(genesisConfig)
32
+
33
+ return { account, genesisConfig, ownerKeys } as const
34
+ }
35
+
36
+ function approve(parameters: {
37
+ genesisConfig: MultisigConfig.Config
38
+ payload: Hex.Hex
39
+ signers: readonly { privateKey: Hex.Hex }[]
40
+ }) {
41
+ const { genesisConfig, payload, signers } = parameters
42
+ const digest = MultisigConfig.getSignPayload({ payload, genesisConfig })
43
+ const signatures = signers.map((signer) =>
44
+ SignatureEnvelope.from(
45
+ Secp256k1.sign({ payload: digest, privateKey: signer.privateKey }),
46
+ ),
47
+ )
48
+ // The node requires approvals ordered by recovered owner address.
49
+ return SignatureEnvelope.sortMultisigApprovals({
50
+ genesisConfig,
51
+ payload,
52
+ signatures,
53
+ })
54
+ }
55
+
56
+ test('behavior: bootstrap + spend (2-of-3 secp256k1)', async () => {
57
+ const { account, genesisConfig, ownerKeys } = setup({
58
+ count: 3,
59
+ threshold: 2,
60
+ })
61
+
62
+ await fundAddress(client, { address: account })
63
+
64
+ const bootstrap = TxEnvelopeTempo.from({
65
+ calls: [{ to: '0x0000000000000000000000000000000000000000' }],
66
+ chainId,
67
+ feeToken: '0x20c0000000000000000000000000000000000001',
68
+ nonce: 0n,
69
+ gas: 5_000_000n,
70
+ maxFeePerGas: Value.fromGwei('20'),
71
+ maxPriorityFeePerGas: Value.fromGwei('10'),
72
+ })
73
+
74
+ const bootstrap_signed = TxEnvelopeTempo.serialize(bootstrap, {
75
+ signature: SignatureEnvelope.from({
76
+ genesisConfig,
77
+ init: true,
78
+ signatures: approve({
79
+ genesisConfig,
80
+ payload: TxEnvelopeTempo.getSignPayload(bootstrap),
81
+ signers: [ownerKeys[0]!, ownerKeys[1]!],
82
+ }),
83
+ }),
84
+ })
85
+
86
+ const bootstrap_receipt = (await client
87
+ .request({
88
+ method: 'eth_sendRawTransactionSync',
89
+ params: [bootstrap_signed],
90
+ })
91
+ .then((tx) => TransactionReceipt.fromRpc(tx as any)))!
92
+ expect(bootstrap_receipt).toBeDefined()
93
+ expect(bootstrap_receipt.status).toBe('success')
94
+ expect(bootstrap_receipt.from).toBe(account)
95
+
96
+ {
97
+ const response = await client
98
+ .request({
99
+ method: 'eth_getTransactionByHash',
100
+ params: [bootstrap_receipt.transactionHash],
101
+ })
102
+ .then((tx) => Transaction.fromRpc(tx as any))
103
+ if (!response) throw new Error()
104
+ expect(response.from).toBe(account)
105
+ expect(response.signature?.type).toBe('multisig')
106
+ expect(
107
+ (response.signature as SignatureEnvelope.Multisig | undefined)?.init,
108
+ ).toEqual(genesisConfig)
109
+ }
110
+
111
+ const nonce = await getTransactionCount(client, {
112
+ address: account,
113
+ blockTag: 'pending',
114
+ })
115
+
116
+ const spend = TxEnvelopeTempo.from({
117
+ calls: [{ to: '0x0000000000000000000000000000000000000000' }],
118
+ chainId,
119
+ feeToken: '0x20c0000000000000000000000000000000000001',
120
+ nonce: BigInt(nonce),
121
+ gas: 5_000_000n,
122
+ maxFeePerGas: Value.fromGwei('20'),
123
+ maxPriorityFeePerGas: Value.fromGwei('10'),
124
+ })
125
+
126
+ const spend_signed = TxEnvelopeTempo.serialize(spend, {
127
+ signature: SignatureEnvelope.from({
128
+ genesisConfig,
129
+ signatures: approve({
130
+ genesisConfig,
131
+ payload: TxEnvelopeTempo.getSignPayload(spend),
132
+ signers: [ownerKeys[1]!, ownerKeys[2]!],
133
+ }),
134
+ }),
135
+ })
136
+
137
+ const spend_receipt = (await client
138
+ .request({
139
+ method: 'eth_sendRawTransactionSync',
140
+ params: [spend_signed],
141
+ })
142
+ .then((tx) => TransactionReceipt.fromRpc(tx as any)))!
143
+ expect(spend_receipt).toBeDefined()
144
+ expect(spend_receipt.status).toBe('success')
145
+ expect(spend_receipt.from).toBe(account)
146
+ })
147
+
148
+ test('behavior: nested multisig owner', async () => {
149
+ const child = setup({ count: 1, threshold: 1 })
150
+ await fundAddress(client, { address: child.account })
151
+
152
+ const childBootstrap = TxEnvelopeTempo.from({
153
+ calls: [{ to: '0x0000000000000000000000000000000000000000' }],
154
+ chainId,
155
+ feeToken: '0x20c0000000000000000000000000000000000001',
156
+ nonce: 0n,
157
+ gas: 5_000_000n,
158
+ maxFeePerGas: Value.fromGwei('20'),
159
+ maxPriorityFeePerGas: Value.fromGwei('10'),
160
+ })
161
+ const childBootstrapSigned = TxEnvelopeTempo.serialize(childBootstrap, {
162
+ signature: SignatureEnvelope.from({
163
+ genesisConfig: child.genesisConfig,
164
+ init: true,
165
+ signatures: approve({
166
+ genesisConfig: child.genesisConfig,
167
+ payload: TxEnvelopeTempo.getSignPayload(childBootstrap),
168
+ signers: child.ownerKeys,
169
+ }),
170
+ }),
171
+ })
172
+ const childReceipt = (await client
173
+ .request({
174
+ method: 'eth_sendRawTransactionSync',
175
+ params: [childBootstrapSigned],
176
+ })
177
+ .then((tx) => TransactionReceipt.fromRpc(tx as any)))!
178
+ expect(childReceipt.status).toBe('success')
179
+
180
+ const genesisConfig = MultisigConfig.from({
181
+ salt: Hex.random(32),
182
+ threshold: 1,
183
+ owners: [{ owner: child.account, weight: 1 }],
184
+ })
185
+ const account = MultisigConfig.getAddress(genesisConfig)
186
+ await fundAddress(client, { address: account })
187
+
188
+ const bootstrap = TxEnvelopeTempo.from({
189
+ calls: [{ to: '0x0000000000000000000000000000000000000000' }],
190
+ chainId,
191
+ feeToken: '0x20c0000000000000000000000000000000000001',
192
+ nonce: 0n,
193
+ gas: 5_000_000n,
194
+ maxFeePerGas: Value.fromGwei('20'),
195
+ maxPriorityFeePerGas: Value.fromGwei('10'),
196
+ })
197
+ const digest = MultisigConfig.getSignPayload({
198
+ genesisConfig,
199
+ payload: TxEnvelopeTempo.getSignPayload(bootstrap),
200
+ })
201
+ const nested = SignatureEnvelope.from({
202
+ genesisConfig: child.genesisConfig,
203
+ signatures: approve({
204
+ genesisConfig: child.genesisConfig,
205
+ payload: digest,
206
+ signers: child.ownerKeys,
207
+ }),
208
+ })
209
+ const bootstrapSigned = TxEnvelopeTempo.serialize(bootstrap, {
210
+ signature: SignatureEnvelope.from({
211
+ genesisConfig,
212
+ init: true,
213
+ signatures: [nested],
214
+ }),
215
+ })
216
+
217
+ const receipt = (await client
218
+ .request({
219
+ method: 'eth_sendRawTransactionSync',
220
+ params: [bootstrapSigned],
221
+ })
222
+ .then((tx) => TransactionReceipt.fromRpc(tx as any)))!
223
+ expect(receipt.status).toBe('success')
224
+ expect(receipt.from).toBe(account)
225
+ })
226
+
227
+ test('behavior: rejects below-threshold approvals', async () => {
228
+ const { account, genesisConfig, ownerKeys } = setup({
229
+ count: 3,
230
+ threshold: 2,
231
+ })
232
+
233
+ await fundAddress(client, { address: account })
234
+
235
+ const bootstrap = TxEnvelopeTempo.from({
236
+ calls: [{ to: '0x0000000000000000000000000000000000000000' }],
237
+ chainId,
238
+ feeToken: '0x20c0000000000000000000000000000000000001',
239
+ nonce: 0n,
240
+ gas: 5_000_000n,
241
+ maxFeePerGas: Value.fromGwei('20'),
242
+ maxPriorityFeePerGas: Value.fromGwei('10'),
243
+ })
244
+
245
+ const serialized_signed = TxEnvelopeTempo.serialize(bootstrap, {
246
+ signature: SignatureEnvelope.from({
247
+ genesisConfig,
248
+ init: true,
249
+ signatures: approve({
250
+ genesisConfig,
251
+ payload: TxEnvelopeTempo.getSignPayload(bootstrap),
252
+ signers: [ownerKeys[0]!],
253
+ }),
254
+ }),
255
+ })
256
+
257
+ await expect(
258
+ client.request({
259
+ method: 'eth_sendRawTransactionSync',
260
+ params: [serialized_signed],
261
+ }),
262
+ ).rejects.toThrow()
263
+ })
264
+
265
+ test('behavior: keychain access key (authorize, spend, reject config update)', async () => {
266
+ const { account, genesisConfig, ownerKeys } = setup({
267
+ count: 2,
268
+ threshold: 2,
269
+ })
270
+
271
+ await fundAddress(client, { address: account })
272
+
273
+ const access = (() => {
274
+ const privateKey = Secp256k1.randomPrivateKey()
275
+ const address = Address.fromPublicKey(
276
+ Secp256k1.getPublicKey({ privateKey }),
277
+ )
278
+ return { address, privateKey } as const
279
+ })()
280
+
281
+ const bootstrap = TxEnvelopeTempo.from({
282
+ calls: [{ to: '0x0000000000000000000000000000000000000000' }],
283
+ chainId,
284
+ feeToken: '0x20c0000000000000000000000000000000000001',
285
+ nonce: 0n,
286
+ gas: 5_000_000n,
287
+ maxFeePerGas: Value.fromGwei('20'),
288
+ maxPriorityFeePerGas: Value.fromGwei('10'),
289
+ })
290
+
291
+ const bootstrap_signed = TxEnvelopeTempo.serialize(bootstrap, {
292
+ signature: SignatureEnvelope.from({
293
+ genesisConfig,
294
+ init: true,
295
+ signatures: approve({
296
+ genesisConfig,
297
+ payload: TxEnvelopeTempo.getSignPayload(bootstrap),
298
+ signers: ownerKeys,
299
+ }),
300
+ }),
301
+ })
302
+
303
+ const bootstrap_receipt = (await client
304
+ .request({
305
+ method: 'eth_sendRawTransactionSync',
306
+ params: [bootstrap_signed],
307
+ })
308
+ .then((tx) => TransactionReceipt.fromRpc(tx as any)))!
309
+ expect(bootstrap_receipt.status).toBe('success')
310
+
311
+ // Multisig accounts authorize access keys through AccountKeychain.
312
+ const authorizeKey = AbiFunction.from(
313
+ '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)',
314
+ )
315
+
316
+ const authorize = TxEnvelopeTempo.from({
317
+ calls: [
318
+ {
319
+ to: '0xaaaaaaaa00000000000000000000000000000000',
320
+ data: AbiFunction.encodeData(authorizeKey, [
321
+ access.address,
322
+ 0, // secp256k1
323
+ {
324
+ expiry: 18446744073709551615n, // u64 max (never expires)
325
+ enforceLimits: false,
326
+ limits: [],
327
+ allowAnyCalls: true,
328
+ allowedCalls: [],
329
+ },
330
+ ]),
331
+ },
332
+ ],
333
+ chainId,
334
+ feeToken: '0x20c0000000000000000000000000000000000001',
335
+ nonce: 1n,
336
+ gas: 5_000_000n,
337
+ maxFeePerGas: Value.fromGwei('20'),
338
+ maxPriorityFeePerGas: Value.fromGwei('10'),
339
+ })
340
+
341
+ const authorize_signed = TxEnvelopeTempo.serialize(authorize, {
342
+ signature: SignatureEnvelope.from({
343
+ genesisConfig,
344
+ signatures: approve({
345
+ genesisConfig,
346
+ payload: TxEnvelopeTempo.getSignPayload(authorize),
347
+ signers: ownerKeys,
348
+ }),
349
+ }),
350
+ })
351
+
352
+ const authorize_receipt = (await client
353
+ .request({
354
+ method: 'eth_sendRawTransactionSync',
355
+ params: [authorize_signed],
356
+ })
357
+ .then((tx) => TransactionReceipt.fromRpc(tx as any)))!
358
+ expect(authorize_receipt.status).toBe('success')
359
+ expect(authorize_receipt.from).toBe(account)
360
+
361
+ const nonce = await getTransactionCount(client, {
362
+ address: account,
363
+ blockTag: 'pending',
364
+ })
365
+
366
+ const spend = TxEnvelopeTempo.from({
367
+ calls: [{ to: '0x0000000000000000000000000000000000000000' }],
368
+ chainId,
369
+ feeToken: '0x20c0000000000000000000000000000000000001',
370
+ nonce: BigInt(nonce),
371
+ gas: 5_000_000n,
372
+ maxFeePerGas: Value.fromGwei('20'),
373
+ maxPriorityFeePerGas: Value.fromGwei('10'),
374
+ })
375
+
376
+ const spend_signature = Secp256k1.sign({
377
+ payload: TxEnvelopeTempo.getSignPayload(spend, { from: account }),
378
+ privateKey: access.privateKey,
379
+ })
380
+
381
+ const spend_signed = TxEnvelopeTempo.serialize(spend, {
382
+ signature: SignatureEnvelope.from({
383
+ userAddress: account,
384
+ inner: SignatureEnvelope.from(spend_signature),
385
+ type: 'keychain',
386
+ }),
387
+ })
388
+
389
+ const spend_receipt = (await client
390
+ .request({
391
+ method: 'eth_sendRawTransactionSync',
392
+ params: [spend_signed],
393
+ })
394
+ .then((tx) => TransactionReceipt.fromRpc(tx as any)))!
395
+ expect(spend_receipt.status).toBe('success')
396
+ expect(spend_receipt.from).toBe(account)
397
+
398
+ {
399
+ const response = await client
400
+ .request({
401
+ method: 'eth_getTransactionByHash',
402
+ params: [spend_receipt.transactionHash],
403
+ })
404
+ .then((tx) => Transaction.fromRpc(tx as any))
405
+ if (!response) throw new Error()
406
+ expect(response.from).toBe(account)
407
+ expect(response.signature?.type).toBe('keychain')
408
+ expect(
409
+ (response.signature as SignatureEnvelope.Keychain | undefined)
410
+ ?.userAddress,
411
+ ).toBe(account)
412
+ }
413
+
414
+ const updateMultisigConfig = AbiFunction.from(
415
+ 'function updateMultisigConfig(uint8 threshold, (address owner, uint8 weight)[] owners)',
416
+ )
417
+ const updateNonce = await getTransactionCount(client, {
418
+ address: account,
419
+ blockTag: 'pending',
420
+ })
421
+ const update = TxEnvelopeTempo.from({
422
+ calls: [
423
+ {
424
+ to: '0xaacc000000000000000000000000000000000000',
425
+ data: AbiFunction.encodeData(updateMultisigConfig, [
426
+ genesisConfig.threshold,
427
+ genesisConfig.owners,
428
+ ]),
429
+ },
430
+ ],
431
+ chainId,
432
+ feeToken: '0x20c0000000000000000000000000000000000001',
433
+ nonce: BigInt(updateNonce),
434
+ gas: 5_000_000n,
435
+ maxFeePerGas: Value.fromGwei('20'),
436
+ maxPriorityFeePerGas: Value.fromGwei('10'),
437
+ })
438
+ const updateSignature = Secp256k1.sign({
439
+ payload: TxEnvelopeTempo.getSignPayload(update, { from: account }),
440
+ privateKey: access.privateKey,
441
+ })
442
+ const updateSigned = TxEnvelopeTempo.serialize(update, {
443
+ signature: SignatureEnvelope.from({
444
+ userAddress: account,
445
+ inner: SignatureEnvelope.from(updateSignature),
446
+ type: 'keychain',
447
+ }),
448
+ })
449
+
450
+ const updateReceipt = (await client
451
+ .request({
452
+ method: 'eth_sendRawTransactionSync',
453
+ params: [updateSigned],
454
+ })
455
+ .then((tx) => TransactionReceipt.fromRpc(tx as any)))!
456
+ expect(updateReceipt.status).toBe('reverted')
457
+ })
458
+
459
+ test('behavior: rejects owner-signed `keyAuthorization` executed by multisig during bootstrap', async () => {
460
+ const { account, genesisConfig, ownerKeys } = setup({
461
+ count: 1,
462
+ threshold: 1,
463
+ })
464
+
465
+ await fundAddress(client, { address: account })
466
+
467
+ const access = (() => {
468
+ const privateKey = Secp256k1.randomPrivateKey()
469
+ const address = Address.fromPublicKey(
470
+ Secp256k1.getPublicKey({ privateKey }),
471
+ )
472
+ return { address, privateKey } as const
473
+ })()
474
+
475
+ const keyAuth = KeyAuthorization.from({
476
+ address: access.address,
477
+ chainId: BigInt(chainId),
478
+ type: 'secp256k1',
479
+ })
480
+ const keyAuth_signed = KeyAuthorization.from(keyAuth, {
481
+ signature: SignatureEnvelope.from(
482
+ Secp256k1.sign({
483
+ payload: KeyAuthorization.getSignPayload(keyAuth),
484
+ privateKey: ownerKeys[0]!.privateKey,
485
+ }),
486
+ ),
487
+ })
488
+
489
+ const bootstrap = TxEnvelopeTempo.from({
490
+ calls: [{ to: '0x0000000000000000000000000000000000000000' }],
491
+ chainId,
492
+ feeToken: '0x20c0000000000000000000000000000000000001',
493
+ keyAuthorization: keyAuth_signed,
494
+ nonce: 0n,
495
+ gas: 5_000_000n,
496
+ maxFeePerGas: Value.fromGwei('20'),
497
+ maxPriorityFeePerGas: Value.fromGwei('10'),
498
+ })
499
+
500
+ const serialized_signed = TxEnvelopeTempo.serialize(bootstrap, {
501
+ signature: SignatureEnvelope.from({
502
+ genesisConfig,
503
+ init: true,
504
+ signatures: approve({
505
+ genesisConfig,
506
+ payload: TxEnvelopeTempo.getSignPayload(bootstrap),
507
+ signers: ownerKeys,
508
+ }),
509
+ }),
510
+ })
511
+
512
+ await expect(
513
+ client.request({
514
+ method: 'eth_sendRawTransactionSync',
515
+ params: [serialized_signed],
516
+ }),
517
+ ).rejects.toThrow(
518
+ 'native multisig transactions cannot carry key_authorization',
519
+ )
520
+ })
521
+
522
+ test('behavior: rejects owner-signed `keyAuthorization` executed by access key before bootstrap', async () => {
523
+ const { account, ownerKeys } = setup({
524
+ count: 1,
525
+ threshold: 1,
526
+ })
527
+
528
+ await fundAddress(client, { address: account })
529
+
530
+ const access = (() => {
531
+ const privateKey = Secp256k1.randomPrivateKey()
532
+ const address = Address.fromPublicKey(
533
+ Secp256k1.getPublicKey({ privateKey }),
534
+ )
535
+ return { address, privateKey } as const
536
+ })()
537
+
538
+ const keyAuth = KeyAuthorization.from({
539
+ address: access.address,
540
+ chainId: BigInt(chainId),
541
+ type: 'secp256k1',
542
+ })
543
+ const keyAuth_signed = KeyAuthorization.from(keyAuth, {
544
+ signature: SignatureEnvelope.from(
545
+ Secp256k1.sign({
546
+ payload: KeyAuthorization.getSignPayload(keyAuth),
547
+ privateKey: ownerKeys[0]!.privateKey,
548
+ }),
549
+ ),
550
+ })
551
+
552
+ const bootstrap = TxEnvelopeTempo.from({
553
+ calls: [{ to: '0x0000000000000000000000000000000000000000' }],
554
+ chainId,
555
+ feeToken: '0x20c0000000000000000000000000000000000001',
556
+ keyAuthorization: keyAuth_signed,
557
+ nonce: 0n,
558
+ gas: 5_000_000n,
559
+ maxFeePerGas: Value.fromGwei('20'),
560
+ maxPriorityFeePerGas: Value.fromGwei('10'),
561
+ })
562
+
563
+ const signature = Secp256k1.sign({
564
+ payload: TxEnvelopeTempo.getSignPayload(bootstrap, { from: account }),
565
+ privateKey: access.privateKey,
566
+ })
567
+ const serialized_signed = TxEnvelopeTempo.serialize(bootstrap, {
568
+ signature: SignatureEnvelope.from({
569
+ userAddress: account,
570
+ inner: SignatureEnvelope.from(signature),
571
+ type: 'keychain',
572
+ }),
573
+ })
574
+
575
+ await expect(
576
+ client.request({
577
+ method: 'eth_sendRawTransactionSync',
578
+ params: [serialized_signed],
579
+ }),
580
+ ).rejects.toThrow('admin-signed key authorization account mismatch')
581
+ })
582
+ })
package/src/version.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  /** @internal */
2
- export const version = '1.0.2'
2
+ export const version = '1.0.3'
@@ -38,7 +38,7 @@ export const Rpc = z.object({
38
38
  keyId: z_Address.Address,
39
39
  keyType: z_SignatureEnvelope.Type,
40
40
  limits: z.optional(z.nullable(z.readonly(z.array(RpcTokenLimit)))),
41
- signature: z_SignatureEnvelope.Rpc,
41
+ signature: z_SignatureEnvelope.PrimitiveRpc,
42
42
  witness: z.optional(z.nullable(z_Hex.Hex)),
43
43
  })
44
44
 
@@ -69,7 +69,7 @@ const domainShape = {
69
69
  expiry: z.optional(z.number()),
70
70
  limits: z.optional(z.readonly(z.array(TokenLimit))),
71
71
  scopes: z.optional(z.readonly(z.array(Scope))),
72
- signature: z_SignatureEnvelope.Domain,
72
+ signature: z_SignatureEnvelope.Primitive,
73
73
  type: z_SignatureEnvelope.Type,
74
74
  witness: z.optional(z_Hex.Hex),
75
75
  }
@@ -1,4 +1,5 @@
1
1
  /* eslint-disable jsdoc-js/require-jsdoc, jsdoc-js/require-description, jsdoc-js/require-example */
2
+ import * as core_MultisigConfig from '../../tempo/MultisigConfig.js'
2
3
  import * as z_Address from '../Address.js'
3
4
  import * as z_Hex from '../Hex.js'
4
5
  import * as z from 'zod/mini'
@@ -10,8 +11,15 @@ export const Owner = z.object({
10
11
  })
11
12
 
12
13
  /** Native multisig configuration schema. */
13
- export const Config = z.object({
14
- salt: z.optional(z_Hex.Hex),
15
- threshold: z.number(),
16
- owners: z.readonly(z.array(Owner)),
17
- })
14
+ export const Config = z
15
+ .object({
16
+ salt: z.optional(z_Hex.Hex),
17
+ threshold: z.number(),
18
+ owners: z.readonly(z.array(Owner)),
19
+ })
20
+ .check(
21
+ z.refine(
22
+ (value) => core_MultisigConfig.validate(value),
23
+ 'expected valid native multisig configuration',
24
+ ),
25
+ )