ox 0.14.35 → 0.14.37

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 +12 -0
  2. package/_cjs/core/Hash.js +1 -1
  3. package/_cjs/core/Hash.js.map +1 -1
  4. package/_cjs/core/P256.js +2 -2
  5. package/_cjs/core/P256.js.map +1 -1
  6. package/_cjs/core/Secp256k1.js +2 -2
  7. package/_cjs/core/Secp256k1.js.map +1 -1
  8. package/_cjs/core/internal/mnemonic.js +18 -0
  9. package/_cjs/core/internal/mnemonic.js.map +1 -0
  10. package/_cjs/tempo/MultisigOperation.js +400 -0
  11. package/_cjs/tempo/MultisigOperation.js.map +1 -0
  12. package/_cjs/tempo/index.js +2 -1
  13. package/_cjs/tempo/index.js.map +1 -1
  14. package/_cjs/version.js +1 -1
  15. package/_esm/core/Hash.js +1 -1
  16. package/_esm/core/Hash.js.map +1 -1
  17. package/_esm/core/P256.js +2 -2
  18. package/_esm/core/P256.js.map +1 -1
  19. package/_esm/core/Secp256k1.js +2 -2
  20. package/_esm/core/Secp256k1.js.map +1 -1
  21. package/_esm/core/internal/mnemonic.js +16 -0
  22. package/_esm/core/internal/mnemonic.js.map +1 -0
  23. package/_esm/tempo/MultisigOperation.js +502 -0
  24. package/_esm/tempo/MultisigOperation.js.map +1 -0
  25. package/_esm/tempo/index.js +11 -1
  26. package/_esm/tempo/index.js.map +1 -1
  27. package/_esm/version.js +1 -1
  28. package/_types/core/Secp256k1.d.ts +1 -1
  29. package/_types/core/Secp256k1.d.ts.map +1 -1
  30. package/_types/core/internal/mnemonic.d.ts +13 -0
  31. package/_types/core/internal/mnemonic.d.ts.map +1 -0
  32. package/_types/tempo/MultisigOperation.d.ts +153 -0
  33. package/_types/tempo/MultisigOperation.d.ts.map +1 -0
  34. package/_types/tempo/RpcSchemaTempo.d.ts +35 -0
  35. package/_types/tempo/RpcSchemaTempo.d.ts.map +1 -1
  36. package/_types/tempo/index.d.ts +11 -1
  37. package/_types/tempo/index.d.ts.map +1 -1
  38. package/_types/version.d.ts +1 -1
  39. package/core/Hash.ts +1 -1
  40. package/core/P256.ts +2 -2
  41. package/core/Secp256k1.ts +3 -2
  42. package/core/internal/mnemonic.ts +31 -0
  43. package/package.json +16 -1
  44. package/tempo/MultisigOperation/package.json +6 -0
  45. package/tempo/MultisigOperation.test-d/package.json +6 -0
  46. package/tempo/MultisigOperation.test-d.ts +52 -0
  47. package/tempo/MultisigOperation.test.ts +1052 -0
  48. package/tempo/MultisigOperation.ts +720 -0
  49. package/tempo/RpcSchemaTempo.test-d/package.json +6 -0
  50. package/tempo/RpcSchemaTempo.test-d.ts +56 -0
  51. package/tempo/RpcSchemaTempo.ts +37 -0
  52. package/tempo/index.ts +11 -1
  53. package/version.ts +1 -1
@@ -0,0 +1,1052 @@
1
+ import { Address, P256 } from 'ox'
2
+ import {
3
+ KeyAuthorization,
4
+ MultisigConfig,
5
+ MultisigOperation,
6
+ SignatureEnvelope,
7
+ TxEnvelopeTempo,
8
+ } from 'ox/tempo'
9
+ import { describe, expect, test } from 'vitest'
10
+
11
+ const owners = [1n, 2n, 3n]
12
+ .map((value, index) => {
13
+ const publicKey = P256.getPublicKey({
14
+ privateKey: `0x${value.toString(16).padStart(64, '0')}`,
15
+ })
16
+ return {
17
+ address: Address.fromPublicKey(publicKey),
18
+ signature: {
19
+ prehash: false,
20
+ publicKey,
21
+ signature: { r: BigInt(index * 2 + 1), s: BigInt(index * 2 + 2) },
22
+ type: 'p256',
23
+ },
24
+ } as const
25
+ })
26
+ .sort((a, b) => a.address.localeCompare(b.address))
27
+ const owner_1 = owners[0]!.address
28
+ const owner_2 = owners[1]!.address
29
+ const config = MultisigConfig.from({
30
+ owners: [
31
+ { owner: owner_1, weight: 1 },
32
+ { owner: owner_2, weight: 1 },
33
+ ],
34
+ threshold: 2,
35
+ })
36
+ const account = MultisigConfig.getAddress(config)
37
+ const ownerSignature_1 = owners[0]!.signature
38
+ const approval_1 = SignatureEnvelope.serialize(ownerSignature_1)
39
+ const approval_2 = SignatureEnvelope.serialize(owners[1]!.signature)
40
+ const approval_3 = SignatureEnvelope.serialize(owners[2]!.signature)
41
+ const transaction = TxEnvelopeTempo.serialize(
42
+ TxEnvelopeTempo.from({
43
+ calls: [{ data: '0x1234', to: owner_1 }],
44
+ chainId: 4217,
45
+ }),
46
+ )
47
+ const transactionHash = `0x${'aa'.repeat(32)}` as const
48
+ const submissionId = `0x${'bb'.repeat(32)}` as const
49
+ const keyAuthorization = KeyAuthorization.serialize(
50
+ KeyAuthorization.from({
51
+ account,
52
+ address: '0x3333333333333333333333333333333333333333',
53
+ chainId: 4217n,
54
+ expiry: 1_800_000_000,
55
+ isAdmin: false,
56
+ type: 'secp256k1',
57
+ }),
58
+ )
59
+
60
+ const transactionHash_ = MultisigConfig.getSignPayload({
61
+ account,
62
+ payload: TxEnvelopeTempo.getSignPayload(
63
+ TxEnvelopeTempo.deserialize(transaction),
64
+ ),
65
+ version: 1n,
66
+ })
67
+ const keyAuthorizationHash = MultisigConfig.getSignPayload({
68
+ account,
69
+ payload: KeyAuthorization.getSignPayload(
70
+ KeyAuthorization.deserialize(keyAuthorization),
71
+ ),
72
+ version: 1n,
73
+ })
74
+
75
+ const base = {
76
+ account,
77
+ approvals: [approval_1],
78
+ config,
79
+ configVersion: 1n,
80
+ createdAt: 1,
81
+ init: false,
82
+ signatureCount: 1,
83
+ threshold: 2,
84
+ updatedAt: 2,
85
+ weight: 1,
86
+ } as const
87
+
88
+ const transactionPending = {
89
+ ...base,
90
+ hash: transactionHash_,
91
+ status: 'pending',
92
+ transaction,
93
+ type: 'transaction',
94
+ } as const
95
+
96
+ const keyAuthorizationPending = {
97
+ ...base,
98
+ hash: keyAuthorizationHash,
99
+ keyAuthorization,
100
+ status: 'pending',
101
+ type: 'keyAuthorization',
102
+ } as const
103
+
104
+ describe('from', () => {
105
+ test('transaction states', () => {
106
+ const pending = MultisigOperation.from(transactionPending)
107
+ const submitting = MultisigOperation.from({
108
+ ...transactionPending,
109
+ approvals: [approval_1, approval_2],
110
+ expiresAt: 10,
111
+ signatureCount: 2,
112
+ status: 'submitting',
113
+ submissionId,
114
+ weight: 2,
115
+ })
116
+ const success = MultisigOperation.from({
117
+ ...transactionPending,
118
+ approvals: [approval_1, approval_2],
119
+ signatureCount: 2,
120
+ status: 'success',
121
+ transactionHash,
122
+ weight: 2,
123
+ })
124
+
125
+ expect({ pending, submitting, success }).toMatchInlineSnapshot(`
126
+ {
127
+ "pending": {
128
+ "account": "0xf81b7763d3a6876195d780865bd783dbd97dd36e",
129
+ "approvals": [
130
+ "0x01000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000065ecbe4d1a6330a44c8f7ef951d4bf165e6c6b721efada985fb41661bc6e7fd6c8734640c4998ff7e374b06ce1a64a2ecd82ab036384fb83d9a79b127a27d503200",
131
+ ],
132
+ "config": {
133
+ "owners": [
134
+ {
135
+ "owner": "0x07e1ed8ea0e9601e5546b0a03aed683df3601407",
136
+ "weight": 1,
137
+ },
138
+ {
139
+ "owner": "0x288f0cd85005f34168f731a468aef268c2f9456f",
140
+ "weight": 1,
141
+ },
142
+ ],
143
+ "salt": "0x0000000000000000000000000000000000000000000000000000000000000000",
144
+ "threshold": 2,
145
+ },
146
+ "configVersion": 1n,
147
+ "createdAt": 1,
148
+ "hash": "0xcdbc24a8fb192f799c5d166b13a99fb29cbb15e71a5e730988d6f8b3c5959d02",
149
+ "init": false,
150
+ "signatureCount": 1,
151
+ "status": "pending",
152
+ "threshold": 2,
153
+ "transaction": "0x76e9821079808080dad99407e1ed8ea0e9601e5546b0a03aed683df360140780821234c0808080808080c0",
154
+ "type": "transaction",
155
+ "updatedAt": 2,
156
+ "weight": 1,
157
+ },
158
+ "submitting": {
159
+ "account": "0xf81b7763d3a6876195d780865bd783dbd97dd36e",
160
+ "approvals": [
161
+ "0x01000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000065ecbe4d1a6330a44c8f7ef951d4bf165e6c6b721efada985fb41661bc6e7fd6c8734640c4998ff7e374b06ce1a64a2ecd82ab036384fb83d9a79b127a27d503200",
162
+ "0x01000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000047cf27b188d034f7e8a52380304b51ac3c08969e277f21b35a60b48fc4766997807775510db8ed040293d9ac69f7430dbba7dade63ce982299e04b79d227873d100",
163
+ ],
164
+ "config": {
165
+ "owners": [
166
+ {
167
+ "owner": "0x07e1ed8ea0e9601e5546b0a03aed683df3601407",
168
+ "weight": 1,
169
+ },
170
+ {
171
+ "owner": "0x288f0cd85005f34168f731a468aef268c2f9456f",
172
+ "weight": 1,
173
+ },
174
+ ],
175
+ "salt": "0x0000000000000000000000000000000000000000000000000000000000000000",
176
+ "threshold": 2,
177
+ },
178
+ "configVersion": 1n,
179
+ "createdAt": 1,
180
+ "expiresAt": 10,
181
+ "hash": "0xcdbc24a8fb192f799c5d166b13a99fb29cbb15e71a5e730988d6f8b3c5959d02",
182
+ "init": false,
183
+ "signatureCount": 2,
184
+ "status": "submitting",
185
+ "submissionId": "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
186
+ "threshold": 2,
187
+ "transaction": "0x76e9821079808080dad99407e1ed8ea0e9601e5546b0a03aed683df360140780821234c0808080808080c0",
188
+ "type": "transaction",
189
+ "updatedAt": 2,
190
+ "weight": 2,
191
+ },
192
+ "success": {
193
+ "account": "0xf81b7763d3a6876195d780865bd783dbd97dd36e",
194
+ "approvals": [
195
+ "0x01000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000065ecbe4d1a6330a44c8f7ef951d4bf165e6c6b721efada985fb41661bc6e7fd6c8734640c4998ff7e374b06ce1a64a2ecd82ab036384fb83d9a79b127a27d503200",
196
+ "0x01000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000047cf27b188d034f7e8a52380304b51ac3c08969e277f21b35a60b48fc4766997807775510db8ed040293d9ac69f7430dbba7dade63ce982299e04b79d227873d100",
197
+ ],
198
+ "config": {
199
+ "owners": [
200
+ {
201
+ "owner": "0x07e1ed8ea0e9601e5546b0a03aed683df3601407",
202
+ "weight": 1,
203
+ },
204
+ {
205
+ "owner": "0x288f0cd85005f34168f731a468aef268c2f9456f",
206
+ "weight": 1,
207
+ },
208
+ ],
209
+ "salt": "0x0000000000000000000000000000000000000000000000000000000000000000",
210
+ "threshold": 2,
211
+ },
212
+ "configVersion": 1n,
213
+ "createdAt": 1,
214
+ "hash": "0xcdbc24a8fb192f799c5d166b13a99fb29cbb15e71a5e730988d6f8b3c5959d02",
215
+ "init": false,
216
+ "signatureCount": 2,
217
+ "status": "success",
218
+ "threshold": 2,
219
+ "transaction": "0x76e9821079808080dad99407e1ed8ea0e9601e5546b0a03aed683df360140780821234c0808080808080c0",
220
+ "transactionHash": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
221
+ "type": "transaction",
222
+ "updatedAt": 2,
223
+ "weight": 2,
224
+ },
225
+ }
226
+ `)
227
+ })
228
+
229
+ test('fee-payer transaction', () => {
230
+ const transaction = TxEnvelopeTempo.serialize(
231
+ TxEnvelopeTempo.from({
232
+ calls: [{ data: '0x1234', to: owner_1 }],
233
+ chainId: 4217,
234
+ }),
235
+ { format: 'feePayer', sender: account },
236
+ )
237
+ const operation = MultisigOperation.from({
238
+ ...transactionPending,
239
+ hash: MultisigConfig.getSignPayload({
240
+ account,
241
+ payload: TxEnvelopeTempo.getSignPayload(
242
+ TxEnvelopeTempo.deserialize(transaction),
243
+ ),
244
+ version: 1n,
245
+ }),
246
+ transaction,
247
+ })
248
+
249
+ expect(operation).toMatchInlineSnapshot(
250
+ {
251
+ hash: expect.any(String),
252
+ transaction: expect.stringMatching(/^0x78/),
253
+ },
254
+ `
255
+ {
256
+ "account": "0xf81b7763d3a6876195d780865bd783dbd97dd36e",
257
+ "approvals": [
258
+ "0x01000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000065ecbe4d1a6330a44c8f7ef951d4bf165e6c6b721efada985fb41661bc6e7fd6c8734640c4998ff7e374b06ce1a64a2ecd82ab036384fb83d9a79b127a27d503200",
259
+ ],
260
+ "config": {
261
+ "owners": [
262
+ {
263
+ "owner": "0x07e1ed8ea0e9601e5546b0a03aed683df3601407",
264
+ "weight": 1,
265
+ },
266
+ {
267
+ "owner": "0x288f0cd85005f34168f731a468aef268c2f9456f",
268
+ "weight": 1,
269
+ },
270
+ ],
271
+ "salt": "0x0000000000000000000000000000000000000000000000000000000000000000",
272
+ "threshold": 2,
273
+ },
274
+ "configVersion": 1n,
275
+ "createdAt": 1,
276
+ "hash": Any<String>,
277
+ "init": false,
278
+ "signatureCount": 1,
279
+ "status": "pending",
280
+ "threshold": 2,
281
+ "transaction": StringMatching /\\^0x78/,
282
+ "type": "transaction",
283
+ "updatedAt": 2,
284
+ "weight": 1,
285
+ }
286
+ `,
287
+ )
288
+ })
289
+
290
+ test('fee-payer-signed transaction', () => {
291
+ const transaction = TxEnvelopeTempo.serialize(
292
+ TxEnvelopeTempo.from({
293
+ calls: [{ data: '0x1234', to: owner_1 }],
294
+ chainId: 4217,
295
+ feePayerSignature: { r: 5n, s: 6n, yParity: 0 },
296
+ }),
297
+ { format: 'feePayer' },
298
+ )
299
+ const operation = MultisigOperation.from({
300
+ ...transactionPending,
301
+ hash: MultisigConfig.getSignPayload({
302
+ account,
303
+ payload: TxEnvelopeTempo.getSignPayload(
304
+ TxEnvelopeTempo.deserialize(transaction),
305
+ ),
306
+ version: 1n,
307
+ }),
308
+ transaction,
309
+ })
310
+
311
+ expect(operation.transaction).toBe(transaction)
312
+ })
313
+
314
+ test('bootstrap transaction', () => {
315
+ const operation = MultisigOperation.from({
316
+ ...transactionPending,
317
+ configVersion: 0n,
318
+ hash: MultisigConfig.getSignPayload({
319
+ account,
320
+ payload: TxEnvelopeTempo.getSignPayload(
321
+ TxEnvelopeTempo.deserialize(transaction),
322
+ ),
323
+ version: 0n,
324
+ }),
325
+ init: true,
326
+ })
327
+
328
+ expect(operation).toMatchInlineSnapshot(
329
+ {
330
+ hash: expect.any(String),
331
+ transaction: expect.any(String),
332
+ },
333
+ `
334
+ {
335
+ "account": "0xf81b7763d3a6876195d780865bd783dbd97dd36e",
336
+ "approvals": [
337
+ "0x01000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000065ecbe4d1a6330a44c8f7ef951d4bf165e6c6b721efada985fb41661bc6e7fd6c8734640c4998ff7e374b06ce1a64a2ecd82ab036384fb83d9a79b127a27d503200",
338
+ ],
339
+ "config": {
340
+ "owners": [
341
+ {
342
+ "owner": "0x07e1ed8ea0e9601e5546b0a03aed683df3601407",
343
+ "weight": 1,
344
+ },
345
+ {
346
+ "owner": "0x288f0cd85005f34168f731a468aef268c2f9456f",
347
+ "weight": 1,
348
+ },
349
+ ],
350
+ "salt": "0x0000000000000000000000000000000000000000000000000000000000000000",
351
+ "threshold": 2,
352
+ },
353
+ "configVersion": 0n,
354
+ "createdAt": 1,
355
+ "hash": Any<String>,
356
+ "init": true,
357
+ "signatureCount": 1,
358
+ "status": "pending",
359
+ "threshold": 2,
360
+ "transaction": Any<String>,
361
+ "type": "transaction",
362
+ "updatedAt": 2,
363
+ "weight": 1,
364
+ }
365
+ `,
366
+ )
367
+ })
368
+
369
+ test('initialized transaction at config version zero', () => {
370
+ const operation = MultisigOperation.from({
371
+ ...transactionPending,
372
+ configVersion: 0n,
373
+ hash: MultisigConfig.getSignPayload({
374
+ account,
375
+ payload: TxEnvelopeTempo.getSignPayload(
376
+ TxEnvelopeTempo.deserialize(transaction),
377
+ ),
378
+ version: 0n,
379
+ }),
380
+ })
381
+
382
+ expect({
383
+ configVersion: operation.configVersion,
384
+ init: operation.init,
385
+ }).toMatchInlineSnapshot(`
386
+ {
387
+ "configVersion": 0n,
388
+ "init": false,
389
+ }
390
+ `)
391
+ })
392
+
393
+ test('key authorization states', () => {
394
+ const pending = MultisigOperation.from(keyAuthorizationPending)
395
+ const authorization = KeyAuthorization.deserialize(keyAuthorization)
396
+ const success = MultisigOperation.from({
397
+ ...keyAuthorizationPending,
398
+ approvals: [approval_1, approval_2],
399
+ keyAuthorization: KeyAuthorization.serialize(
400
+ KeyAuthorization.from(authorization, {
401
+ signature: {
402
+ account,
403
+ signatures: [
404
+ SignatureEnvelope.deserialize(approval_1),
405
+ SignatureEnvelope.deserialize(approval_2),
406
+ ],
407
+ type: 'multisig',
408
+ },
409
+ }),
410
+ ),
411
+ signatureCount: 2,
412
+ status: 'success',
413
+ weight: 2,
414
+ })
415
+
416
+ expect({ pending, success }).toMatchInlineSnapshot(`
417
+ {
418
+ "pending": {
419
+ "account": "0xf81b7763d3a6876195d780865bd783dbd97dd36e",
420
+ "approvals": [
421
+ "0x01000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000065ecbe4d1a6330a44c8f7ef951d4bf165e6c6b721efada985fb41661bc6e7fd6c8734640c4998ff7e374b06ce1a64a2ecd82ab036384fb83d9a79b127a27d503200",
422
+ ],
423
+ "config": {
424
+ "owners": [
425
+ {
426
+ "owner": "0x07e1ed8ea0e9601e5546b0a03aed683df3601407",
427
+ "weight": 1,
428
+ },
429
+ {
430
+ "owner": "0x288f0cd85005f34168f731a468aef268c2f9456f",
431
+ "weight": 1,
432
+ },
433
+ ],
434
+ "salt": "0x0000000000000000000000000000000000000000000000000000000000000000",
435
+ "threshold": 2,
436
+ },
437
+ "configVersion": 1n,
438
+ "createdAt": 1,
439
+ "hash": "0xf6995eb69c12c03d3d13b357abf7cac22b4effe52fba8ff02e5aef26161f77a0",
440
+ "init": false,
441
+ "keyAuthorization": "0xf838f782107980943333333333333333333333333333333333333333846b49d2008080808094f81b7763d3a6876195d780865bd783dbd97dd36e",
442
+ "signatureCount": 1,
443
+ "status": "pending",
444
+ "threshold": 2,
445
+ "type": "keyAuthorization",
446
+ "updatedAt": 2,
447
+ "weight": 1,
448
+ },
449
+ "success": {
450
+ "account": "0xf81b7763d3a6876195d780865bd783dbd97dd36e",
451
+ "approvals": [
452
+ "0x01000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000065ecbe4d1a6330a44c8f7ef951d4bf165e6c6b721efada985fb41661bc6e7fd6c8734640c4998ff7e374b06ce1a64a2ecd82ab036384fb83d9a79b127a27d503200",
453
+ "0x01000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000047cf27b188d034f7e8a52380304b51ac3c08969e277f21b35a60b48fc4766997807775510db8ed040293d9ac69f7430dbba7dade63ce982299e04b79d227873d100",
454
+ ],
455
+ "config": {
456
+ "owners": [
457
+ {
458
+ "owner": "0x07e1ed8ea0e9601e5546b0a03aed683df3601407",
459
+ "weight": 1,
460
+ },
461
+ {
462
+ "owner": "0x288f0cd85005f34168f731a468aef268c2f9456f",
463
+ "weight": 1,
464
+ },
465
+ ],
466
+ "salt": "0x0000000000000000000000000000000000000000000000000000000000000000",
467
+ "threshold": 2,
468
+ },
469
+ "configVersion": 1n,
470
+ "createdAt": 1,
471
+ "hash": "0xf6995eb69c12c03d3d13b357abf7cac22b4effe52fba8ff02e5aef26161f77a0",
472
+ "init": false,
473
+ "keyAuthorization": "0xf9015ff782107980943333333333333333333333333333333333333333846b49d2008080808094f81b7763d3a6876195d780865bd783dbd97dd36eb9012405f9012094f81b7763d3a6876195d780865bd783dbd97dd36ef90108b88201000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000065ecbe4d1a6330a44c8f7ef951d4bf165e6c6b721efada985fb41661bc6e7fd6c8734640c4998ff7e374b06ce1a64a2ecd82ab036384fb83d9a79b127a27d503200b88201000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000047cf27b188d034f7e8a52380304b51ac3c08969e277f21b35a60b48fc4766997807775510db8ed040293d9ac69f7430dbba7dade63ce982299e04b79d227873d100",
474
+ "signatureCount": 2,
475
+ "status": "success",
476
+ "threshold": 2,
477
+ "type": "keyAuthorization",
478
+ "updatedAt": 2,
479
+ "weight": 2,
480
+ },
481
+ }
482
+ `)
483
+ })
484
+
485
+ test('bootstrap key authorization', () => {
486
+ const authorization = KeyAuthorization.deserialize(keyAuthorization)
487
+ const keyAuthorization_ = KeyAuthorization.serialize(
488
+ KeyAuthorization.from(authorization, {
489
+ signature: {
490
+ account,
491
+ init: config,
492
+ signatures: [
493
+ SignatureEnvelope.deserialize(approval_1),
494
+ SignatureEnvelope.deserialize(approval_2),
495
+ ],
496
+ type: 'multisig',
497
+ },
498
+ }),
499
+ )
500
+ const operation = MultisigOperation.from({
501
+ ...keyAuthorizationPending,
502
+ approvals: [approval_1, approval_2],
503
+ configVersion: 0n,
504
+ hash: MultisigConfig.getSignPayload({
505
+ account,
506
+ payload: KeyAuthorization.getSignPayload(authorization),
507
+ version: 0n,
508
+ }),
509
+ init: true,
510
+ keyAuthorization: keyAuthorization_,
511
+ signatureCount: 2,
512
+ status: 'success',
513
+ weight: 2,
514
+ })
515
+
516
+ expect(operation).toMatchInlineSnapshot(
517
+ {
518
+ hash: expect.any(String),
519
+ keyAuthorization: expect.any(String),
520
+ },
521
+ `
522
+ {
523
+ "account": "0xf81b7763d3a6876195d780865bd783dbd97dd36e",
524
+ "approvals": [
525
+ "0x01000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000065ecbe4d1a6330a44c8f7ef951d4bf165e6c6b721efada985fb41661bc6e7fd6c8734640c4998ff7e374b06ce1a64a2ecd82ab036384fb83d9a79b127a27d503200",
526
+ "0x01000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000047cf27b188d034f7e8a52380304b51ac3c08969e277f21b35a60b48fc4766997807775510db8ed040293d9ac69f7430dbba7dade63ce982299e04b79d227873d100",
527
+ ],
528
+ "config": {
529
+ "owners": [
530
+ {
531
+ "owner": "0x07e1ed8ea0e9601e5546b0a03aed683df3601407",
532
+ "weight": 1,
533
+ },
534
+ {
535
+ "owner": "0x288f0cd85005f34168f731a468aef268c2f9456f",
536
+ "weight": 1,
537
+ },
538
+ ],
539
+ "salt": "0x0000000000000000000000000000000000000000000000000000000000000000",
540
+ "threshold": 2,
541
+ },
542
+ "configVersion": 0n,
543
+ "createdAt": 1,
544
+ "hash": Any<String>,
545
+ "init": true,
546
+ "keyAuthorization": Any<String>,
547
+ "signatureCount": 2,
548
+ "status": "success",
549
+ "threshold": 2,
550
+ "type": "keyAuthorization",
551
+ "updatedAt": 2,
552
+ "weight": 2,
553
+ }
554
+ `,
555
+ )
556
+ })
557
+ })
558
+
559
+ describe('RPC conversion', () => {
560
+ test('round-trips transaction and key authorization operations', () => {
561
+ const transactionRpc = MultisigOperation.toRpc(transactionPending)
562
+ const keyAuthorizationRpc = MultisigOperation.toRpc(keyAuthorizationPending)
563
+
564
+ expect({ keyAuthorizationRpc, transactionRpc }).toMatchInlineSnapshot(`
565
+ {
566
+ "keyAuthorizationRpc": {
567
+ "account": "0xf81b7763d3a6876195d780865bd783dbd97dd36e",
568
+ "approvals": [
569
+ "0x01000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000065ecbe4d1a6330a44c8f7ef951d4bf165e6c6b721efada985fb41661bc6e7fd6c8734640c4998ff7e374b06ce1a64a2ecd82ab036384fb83d9a79b127a27d503200",
570
+ ],
571
+ "config": {
572
+ "owners": [
573
+ {
574
+ "owner": "0x07e1ed8ea0e9601e5546b0a03aed683df3601407",
575
+ "weight": 1,
576
+ },
577
+ {
578
+ "owner": "0x288f0cd85005f34168f731a468aef268c2f9456f",
579
+ "weight": 1,
580
+ },
581
+ ],
582
+ "salt": "0x0000000000000000000000000000000000000000000000000000000000000000",
583
+ "threshold": 2,
584
+ },
585
+ "configVersion": "0x1",
586
+ "createdAt": 1,
587
+ "hash": "0xf6995eb69c12c03d3d13b357abf7cac22b4effe52fba8ff02e5aef26161f77a0",
588
+ "init": false,
589
+ "keyAuthorization": "0xf838f782107980943333333333333333333333333333333333333333846b49d2008080808094f81b7763d3a6876195d780865bd783dbd97dd36e",
590
+ "signatureCount": 1,
591
+ "status": "pending",
592
+ "threshold": 2,
593
+ "type": "keyAuthorization",
594
+ "updatedAt": 2,
595
+ "weight": 1,
596
+ },
597
+ "transactionRpc": {
598
+ "account": "0xf81b7763d3a6876195d780865bd783dbd97dd36e",
599
+ "approvals": [
600
+ "0x01000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000065ecbe4d1a6330a44c8f7ef951d4bf165e6c6b721efada985fb41661bc6e7fd6c8734640c4998ff7e374b06ce1a64a2ecd82ab036384fb83d9a79b127a27d503200",
601
+ ],
602
+ "config": {
603
+ "owners": [
604
+ {
605
+ "owner": "0x07e1ed8ea0e9601e5546b0a03aed683df3601407",
606
+ "weight": 1,
607
+ },
608
+ {
609
+ "owner": "0x288f0cd85005f34168f731a468aef268c2f9456f",
610
+ "weight": 1,
611
+ },
612
+ ],
613
+ "salt": "0x0000000000000000000000000000000000000000000000000000000000000000",
614
+ "threshold": 2,
615
+ },
616
+ "configVersion": "0x1",
617
+ "createdAt": 1,
618
+ "hash": "0xcdbc24a8fb192f799c5d166b13a99fb29cbb15e71a5e730988d6f8b3c5959d02",
619
+ "init": false,
620
+ "signatureCount": 1,
621
+ "status": "pending",
622
+ "threshold": 2,
623
+ "transaction": "0x76e9821079808080dad99407e1ed8ea0e9601e5546b0a03aed683df360140780821234c0808080808080c0",
624
+ "type": "transaction",
625
+ "updatedAt": 2,
626
+ "weight": 1,
627
+ },
628
+ }
629
+ `)
630
+ expect(MultisigOperation.fromRpc(transactionRpc)).toStrictEqual(
631
+ MultisigOperation.from(transactionPending),
632
+ )
633
+ expect(MultisigOperation.fromRpc(keyAuthorizationRpc)).toStrictEqual(
634
+ MultisigOperation.from(keyAuthorizationPending),
635
+ )
636
+ })
637
+ })
638
+
639
+ describe('validation', () => {
640
+ test.each([
641
+ {
642
+ name: 'mismatched hash',
643
+ operation: { ...transactionPending, hash: transactionHash },
644
+ },
645
+ {
646
+ name: 'outer transaction signature',
647
+ operation: {
648
+ ...transactionPending,
649
+ transaction: TxEnvelopeTempo.serialize(
650
+ TxEnvelopeTempo.deserialize(transaction),
651
+ { signature: SignatureEnvelope.deserialize(approval_1) },
652
+ ),
653
+ },
654
+ },
655
+ {
656
+ name: 'pending transaction submission fields',
657
+ operation: { ...transactionPending, submissionId },
658
+ },
659
+ {
660
+ name: 'submitting transaction without lease',
661
+ operation: { ...transactionPending, status: 'submitting', weight: 2 },
662
+ },
663
+ {
664
+ name: 'successful transaction without hash',
665
+ operation: { ...transactionPending, status: 'success', weight: 2 },
666
+ },
667
+ {
668
+ name: 'signed pending key authorization',
669
+ operation: {
670
+ ...keyAuthorizationPending,
671
+ keyAuthorization: KeyAuthorization.serialize(
672
+ KeyAuthorization.from(
673
+ KeyAuthorization.deserialize(keyAuthorization),
674
+ {
675
+ signature: ownerSignature_1,
676
+ },
677
+ ),
678
+ ),
679
+ },
680
+ },
681
+ {
682
+ name: 'successful key authorization without quorum',
683
+ operation: { ...keyAuthorizationPending, status: 'success' },
684
+ },
685
+ {
686
+ name: 'pending key authorization with quorum',
687
+ operation: {
688
+ ...keyAuthorizationPending,
689
+ approvals: [approval_1, approval_2],
690
+ signatureCount: 2,
691
+ weight: 2,
692
+ },
693
+ },
694
+ {
695
+ name: 'key authorization with transaction fields',
696
+ operation: { ...keyAuthorizationPending, transaction },
697
+ },
698
+ {
699
+ name: 'transaction with key authorization fields',
700
+ operation: { ...transactionPending, keyAuthorization },
701
+ },
702
+ {
703
+ name: 'selected signature without weight',
704
+ operation: { ...transactionPending, weight: 0 },
705
+ },
706
+ {
707
+ name: 'string config threshold',
708
+ operation: {
709
+ ...transactionPending,
710
+ config: { ...config, threshold: '2' },
711
+ },
712
+ },
713
+ {
714
+ name: 'bigint config owner weight',
715
+ operation: {
716
+ ...transactionPending,
717
+ config: {
718
+ ...config,
719
+ owners: config.owners.map((owner) => ({ ...owner, weight: 1n })),
720
+ },
721
+ },
722
+ },
723
+ {
724
+ name: 'weight unreachable by the selected signature count',
725
+ operation: { ...transactionPending, weight: 2 },
726
+ },
727
+ {
728
+ name: 'weight unreachable by the retained owner approvals',
729
+ operation: {
730
+ ...transactionPending,
731
+ config: MultisigConfig.from({
732
+ owners: [
733
+ { owner: owner_1, weight: 1 },
734
+ { owner: owner_2, weight: 2 },
735
+ ],
736
+ threshold: 2,
737
+ }),
738
+ weight: 2,
739
+ },
740
+ },
741
+ {
742
+ name: 'non-owner approval',
743
+ operation: { ...transactionPending, approvals: [approval_3] },
744
+ },
745
+ {
746
+ name: 'zero account',
747
+ operation: {
748
+ ...transactionPending,
749
+ account: '0x0000000000000000000000000000000000000000',
750
+ hash: MultisigConfig.getSignPayload({
751
+ account: '0x0000000000000000000000000000000000000000',
752
+ payload: TxEnvelopeTempo.getSignPayload(
753
+ TxEnvelopeTempo.deserialize(transaction),
754
+ ),
755
+ version: 1n,
756
+ }),
757
+ },
758
+ },
759
+ {
760
+ name: 'non-hex transaction hash',
761
+ operation: {
762
+ ...transactionPending,
763
+ approvals: [approval_1, approval_2],
764
+ signatureCount: 2,
765
+ status: 'success',
766
+ transactionHash: `0x${'gg'.repeat(32)}`,
767
+ weight: 2,
768
+ },
769
+ },
770
+ {
771
+ name: 'non-hex submission ID',
772
+ operation: {
773
+ ...transactionPending,
774
+ approvals: [approval_1, approval_2],
775
+ expiresAt: 10,
776
+ signatureCount: 2,
777
+ status: 'submitting',
778
+ submissionId: `0x${'gg'.repeat(32)}`,
779
+ weight: 2,
780
+ },
781
+ },
782
+ {
783
+ name: 'odd-length transaction hash',
784
+ operation: {
785
+ ...transactionPending,
786
+ approvals: [approval_1, approval_2],
787
+ signatureCount: 2,
788
+ status: 'success',
789
+ transactionHash: `0x${'a'.repeat(63)}`,
790
+ weight: 2,
791
+ },
792
+ },
793
+ {
794
+ name: 'odd-length submission ID',
795
+ operation: {
796
+ ...transactionPending,
797
+ approvals: [approval_1, approval_2],
798
+ expiresAt: 10,
799
+ signatureCount: 2,
800
+ status: 'submitting',
801
+ submissionId: `0x${'b'.repeat(63)}`,
802
+ weight: 2,
803
+ },
804
+ },
805
+ {
806
+ name: 'keychain owner approval',
807
+ operation: {
808
+ ...transactionPending,
809
+ approvals: [
810
+ SignatureEnvelope.serialize({
811
+ inner: ownerSignature_1,
812
+ type: 'keychain',
813
+ userAddress: owner_1,
814
+ }),
815
+ ],
816
+ },
817
+ },
818
+ {
819
+ name: 'nested bootstrap owner approval',
820
+ operation: {
821
+ ...transactionPending,
822
+ approvals: [
823
+ SignatureEnvelope.serialize({
824
+ account,
825
+ init: config,
826
+ signatures: [ownerSignature_1],
827
+ type: 'multisig',
828
+ }),
829
+ ],
830
+ },
831
+ },
832
+ {
833
+ name: 'bootstrap with initialized version',
834
+ operation: { ...transactionPending, init: true },
835
+ },
836
+ ])('rejects $name', ({ operation }) => {
837
+ expect(() =>
838
+ MultisigOperation.from(operation as MultisigOperation.Operation),
839
+ ).toThrowError(MultisigOperation.InvalidOperationError)
840
+ })
841
+
842
+ test('rejects noncanonical RPC quantities', () => {
843
+ expect(() =>
844
+ MultisigOperation.fromRpc({
845
+ ...MultisigOperation.toRpc(transactionPending),
846
+ configVersion: '0x01',
847
+ }),
848
+ ).toThrowError(
849
+ 'Invalid multisig operation: configVersion must use canonical quantity encoding.',
850
+ )
851
+ })
852
+
853
+ test('rejects key authorization signatures absent from retained approvals', () => {
854
+ const authorization = KeyAuthorization.deserialize(keyAuthorization)
855
+ const operation = {
856
+ ...keyAuthorizationPending,
857
+ approvals: [approval_1, approval_2],
858
+ keyAuthorization: KeyAuthorization.serialize(
859
+ KeyAuthorization.from(authorization, {
860
+ signature: {
861
+ account,
862
+ signatures: [
863
+ SignatureEnvelope.deserialize(approval_1),
864
+ SignatureEnvelope.deserialize(approval_3),
865
+ ],
866
+ type: 'multisig',
867
+ },
868
+ }),
869
+ ),
870
+ signatureCount: 2,
871
+ status: 'success',
872
+ weight: 2,
873
+ } as const
874
+
875
+ expect(() => MultisigOperation.from(operation)).toThrowError(
876
+ 'Invalid multisig operation: key authorization signature is not a retained approval.',
877
+ )
878
+ })
879
+
880
+ test('rejects unordered key authorization approvals', () => {
881
+ const authorization = KeyAuthorization.deserialize(keyAuthorization)
882
+ const signatures = [
883
+ ...SignatureEnvelope.sortMultisigApprovals({
884
+ account,
885
+ payload: KeyAuthorization.getSignPayload(authorization),
886
+ signatures: [
887
+ SignatureEnvelope.deserialize(approval_1),
888
+ SignatureEnvelope.deserialize(approval_2),
889
+ ],
890
+ version: 1n,
891
+ }),
892
+ ].reverse()
893
+ const operation = {
894
+ ...keyAuthorizationPending,
895
+ approvals: [approval_1, approval_2],
896
+ keyAuthorization: KeyAuthorization.serialize(
897
+ KeyAuthorization.from(authorization, {
898
+ signature: { account, signatures, type: 'multisig' },
899
+ }),
900
+ ),
901
+ signatureCount: 2,
902
+ status: 'success',
903
+ weight: 2,
904
+ } as const
905
+
906
+ expect(() => MultisigOperation.from(operation)).toThrowError(
907
+ 'Invalid multisig operation: key authorization approvals are not canonically ordered.',
908
+ )
909
+ })
910
+
911
+ test('rejects duplicate key authorization approvals', () => {
912
+ const authorization = KeyAuthorization.deserialize(keyAuthorization)
913
+ const operation = {
914
+ ...keyAuthorizationPending,
915
+ approvals: [approval_1, approval_1],
916
+ keyAuthorization: KeyAuthorization.serialize(
917
+ KeyAuthorization.from(authorization, {
918
+ signature: {
919
+ account,
920
+ signatures: [
921
+ SignatureEnvelope.deserialize(approval_1),
922
+ SignatureEnvelope.deserialize(approval_1),
923
+ ],
924
+ type: 'multisig',
925
+ },
926
+ }),
927
+ ),
928
+ signatureCount: 2,
929
+ status: 'success',
930
+ weight: 2,
931
+ } as const
932
+
933
+ expect(() => MultisigOperation.from(operation)).toThrowError(
934
+ 'Invalid multisig operation: key authorization contains duplicate owner approvals.',
935
+ )
936
+ })
937
+
938
+ test('rejects reordered nested key authorization approvals', () => {
939
+ const authorization = KeyAuthorization.deserialize(keyAuthorization)
940
+ const childSignatures = SignatureEnvelope.sortMultisigApprovals({
941
+ account: owner_1,
942
+ payload: keyAuthorizationHash,
943
+ signatures: [
944
+ SignatureEnvelope.deserialize(approval_1),
945
+ SignatureEnvelope.deserialize(approval_2),
946
+ ],
947
+ version: 1n,
948
+ })
949
+ const retainedNested = SignatureEnvelope.from({
950
+ account: owner_1,
951
+ signatures: childSignatures,
952
+ type: 'multisig',
953
+ })
954
+ const selectedNested = SignatureEnvelope.from({
955
+ account: owner_1,
956
+ signatures: [...childSignatures].reverse(),
957
+ type: 'multisig',
958
+ })
959
+ const selected = SignatureEnvelope.sortMultisigApprovals({
960
+ account,
961
+ payload: KeyAuthorization.getSignPayload(authorization),
962
+ signatures: [selectedNested, SignatureEnvelope.deserialize(approval_2)],
963
+ version: 1n,
964
+ })
965
+ const operation = {
966
+ ...keyAuthorizationPending,
967
+ approvals: [SignatureEnvelope.serialize(retainedNested), approval_2],
968
+ keyAuthorization: KeyAuthorization.serialize(
969
+ KeyAuthorization.from(authorization, {
970
+ signature: { account, signatures: selected, type: 'multisig' },
971
+ }),
972
+ ),
973
+ signatureCount: 2,
974
+ status: 'success',
975
+ weight: 2,
976
+ } as const
977
+
978
+ expect(() => MultisigOperation.from(operation)).toThrowError(
979
+ 'Invalid multisig operation: key authorization signature is not a retained approval.',
980
+ )
981
+ })
982
+
983
+ test('accepts case-insensitive bootstrap configs', () => {
984
+ const config = MultisigConfig.from({
985
+ owners: [
986
+ {
987
+ owner: Address.checksum(owner_1),
988
+ weight: 1,
989
+ },
990
+ {
991
+ owner: Address.checksum(owner_2),
992
+ weight: 1,
993
+ },
994
+ ],
995
+ salt: `0x${'AB'.repeat(32)}`,
996
+ threshold: 2,
997
+ })
998
+ const account = MultisigConfig.getAddress(config)
999
+ const authorization = KeyAuthorization.from({
1000
+ account,
1001
+ address: '0x3333333333333333333333333333333333333333',
1002
+ chainId: 4217n,
1003
+ expiry: 1_800_000_000,
1004
+ isAdmin: false,
1005
+ type: 'secp256k1',
1006
+ })
1007
+ const signatures = SignatureEnvelope.sortMultisigApprovals({
1008
+ account,
1009
+ payload: KeyAuthorization.getSignPayload(authorization),
1010
+ signatures: [
1011
+ SignatureEnvelope.deserialize(approval_1),
1012
+ SignatureEnvelope.deserialize(approval_2),
1013
+ ],
1014
+ version: 0n,
1015
+ })
1016
+ const approvals = signatures.map((signature) =>
1017
+ SignatureEnvelope.serialize(signature),
1018
+ )
1019
+
1020
+ const operation = MultisigOperation.from({
1021
+ account,
1022
+ approvals,
1023
+ config,
1024
+ configVersion: 0n,
1025
+ createdAt: 1,
1026
+ hash: MultisigConfig.getSignPayload({
1027
+ account,
1028
+ payload: KeyAuthorization.getSignPayload(authorization),
1029
+ version: 0n,
1030
+ }),
1031
+ init: true,
1032
+ keyAuthorization: KeyAuthorization.serialize(
1033
+ KeyAuthorization.from(authorization, {
1034
+ signature: {
1035
+ account,
1036
+ init: config,
1037
+ signatures,
1038
+ type: 'multisig',
1039
+ },
1040
+ }),
1041
+ ),
1042
+ signatureCount: 2,
1043
+ status: 'success',
1044
+ threshold: 2,
1045
+ type: 'keyAuthorization',
1046
+ updatedAt: 2,
1047
+ weight: 2,
1048
+ })
1049
+
1050
+ expect(operation.config).toStrictEqual(config)
1051
+ })
1052
+ })