ox 0.14.36 → 0.14.38

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.
@@ -0,0 +1,1743 @@
1
+ import { Address, Hash, 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 privateKey = `0x${value.toString(16).padStart(64, '0')}` as const
14
+ const publicKey = P256.getPublicKey({
15
+ privateKey,
16
+ })
17
+ return {
18
+ address: Address.fromPublicKey(publicKey),
19
+ privateKey,
20
+ publicKey,
21
+ signature: {
22
+ prehash: false,
23
+ publicKey,
24
+ signature: { r: BigInt(index * 2 + 1), s: BigInt(index * 2 + 2) },
25
+ type: 'p256',
26
+ },
27
+ } as const
28
+ })
29
+ .sort((a, b) => a.address.localeCompare(b.address))
30
+ const owner_1 = owners[0]!.address
31
+ const owner_2 = owners[1]!.address
32
+ const config = MultisigConfig.from({
33
+ owners: [
34
+ { owner: owner_1, weight: 1 },
35
+ { owner: owner_2, weight: 1 },
36
+ ],
37
+ threshold: 2,
38
+ })
39
+ const account = MultisigConfig.getAddress(config)
40
+ const ownerSignature_1 = owners[0]!.signature
41
+ const approval_1 = SignatureEnvelope.serialize(ownerSignature_1)
42
+ const approval_2 = SignatureEnvelope.serialize(owners[1]!.signature)
43
+ const approval_3 = SignatureEnvelope.serialize(owners[2]!.signature)
44
+ const transaction = TxEnvelopeTempo.serialize(
45
+ TxEnvelopeTempo.from({
46
+ calls: [{ data: '0x1234', to: owner_1 }],
47
+ chainId: 4217,
48
+ }),
49
+ )
50
+ const transactionHash = `0x${'aa'.repeat(32)}` as const
51
+ const submissionId = `0x${'bb'.repeat(32)}` as const
52
+ const keyAuthorization = KeyAuthorization.serialize(
53
+ KeyAuthorization.from({
54
+ account,
55
+ address: '0x3333333333333333333333333333333333333333',
56
+ chainId: 4217n,
57
+ expiry: 1_800_000_000,
58
+ isAdmin: false,
59
+ type: 'secp256k1',
60
+ }),
61
+ )
62
+
63
+ const transactionHash_ = MultisigConfig.getSignPayload({
64
+ account,
65
+ payload: TxEnvelopeTempo.getSignPayload(
66
+ TxEnvelopeTempo.deserialize(transaction),
67
+ ),
68
+ version: 1n,
69
+ })
70
+ const keyAuthorizationHash = MultisigConfig.getSignPayload({
71
+ account,
72
+ payload: KeyAuthorization.getSignPayload(
73
+ KeyAuthorization.deserialize(keyAuthorization),
74
+ ),
75
+ version: 1n,
76
+ })
77
+
78
+ const base = {
79
+ account,
80
+ approvals: [approval_1],
81
+ config,
82
+ configVersion: 1n,
83
+ createdAt: 1,
84
+ init: false,
85
+ signatureCount: 1,
86
+ threshold: 2,
87
+ updatedAt: 2,
88
+ weight: 1,
89
+ } as const
90
+
91
+ const transactionPending = {
92
+ ...base,
93
+ hash: transactionHash_,
94
+ status: 'pending',
95
+ transaction,
96
+ type: 'transaction',
97
+ } as const
98
+
99
+ const keyAuthorizationPending = {
100
+ ...base,
101
+ hash: keyAuthorizationHash,
102
+ keyAuthorization,
103
+ status: 'pending',
104
+ type: 'keyAuthorization',
105
+ } as const
106
+
107
+ function signApproval(
108
+ owner: (typeof owners)[number],
109
+ hash: `0x${string}`,
110
+ extraEntropy: false | `0x${string}` = false,
111
+ ) {
112
+ return SignatureEnvelope.serialize({
113
+ prehash: false,
114
+ publicKey: owner.publicKey,
115
+ signature: P256.sign({
116
+ extraEntropy,
117
+ payload: hash,
118
+ privateKey: owner.privateKey,
119
+ }),
120
+ type: 'p256',
121
+ })
122
+ }
123
+
124
+ function approvalAddresses(
125
+ approvals: readonly `0x${string}`[],
126
+ hash: `0x${string}`,
127
+ ) {
128
+ return approvals.map((approval) =>
129
+ SignatureEnvelope.extractAddress({
130
+ payload: hash,
131
+ signature: SignatureEnvelope.deserialize(approval),
132
+ }),
133
+ )
134
+ }
135
+
136
+ describe('getHash', () => {
137
+ test('transaction and key authorization', () => {
138
+ expect({
139
+ keyAuthorization: MultisigOperation.getHash({
140
+ account,
141
+ configVersion: 1n,
142
+ keyAuthorization,
143
+ type: 'keyAuthorization',
144
+ }),
145
+ transaction: MultisigOperation.getHash({
146
+ account,
147
+ configVersion: 1n,
148
+ transaction,
149
+ type: 'transaction',
150
+ }),
151
+ }).toMatchInlineSnapshot(`
152
+ {
153
+ "keyAuthorization": "0xf6995eb69c12c03d3d13b357abf7cac22b4effe52fba8ff02e5aef26161f77a0",
154
+ "transaction": "0xcdbc24a8fb192f799c5d166b13a99fb29cbb15e71a5e730988d6f8b3c5959d02",
155
+ }
156
+ `)
157
+ })
158
+ })
159
+
160
+ describe('selectApprovals', () => {
161
+ test('selects a deterministic weighted quorum', async () => {
162
+ const config = MultisigConfig.from({
163
+ owners: [
164
+ { owner: owners[0]!.address, weight: 2 },
165
+ { owner: owners[1]!.address, weight: 1 },
166
+ { owner: owners[2]!.address, weight: 1 },
167
+ ],
168
+ threshold: 3,
169
+ })
170
+ const account = MultisigConfig.getAddress(config)
171
+ const hash = MultisigOperation.getHash({
172
+ account,
173
+ configVersion: 1n,
174
+ transaction,
175
+ type: 'transaction',
176
+ })
177
+ const approvals = owners.map((owner) => signApproval(owner, hash))
178
+ const alternate = signApproval(owners[0]!, hash, `0x${'01'.repeat(32)}`)
179
+ const selection = await MultisigOperation.selectApprovals({
180
+ account,
181
+ approvals: [
182
+ approvals[2]!,
183
+ approvals[0]!,
184
+ approvals[1]!,
185
+ alternate,
186
+ approvals[0]!,
187
+ ],
188
+ config,
189
+ hash,
190
+ })
191
+ const reversed = await MultisigOperation.selectApprovals({
192
+ account,
193
+ approvals: [
194
+ approvals[0]!,
195
+ alternate,
196
+ approvals[1]!,
197
+ approvals[0]!,
198
+ approvals[2]!,
199
+ ],
200
+ config,
201
+ hash,
202
+ })
203
+
204
+ expect(reversed).toStrictEqual(selection)
205
+ expect({
206
+ ...selection,
207
+ approvals: approvalAddresses(selection.approvals, hash),
208
+ selectedApprovals: approvalAddresses(selection.selectedApprovals, hash),
209
+ }).toMatchInlineSnapshot(`
210
+ {
211
+ "approvals": [
212
+ "0x07e1ed8ea0e9601e5546b0a03aed683df3601407",
213
+ "0x288f0cd85005f34168f731a468aef268c2f9456f",
214
+ "0xd3a9f047ad43d7e2e4e7e491f1fe2e657a2651b6",
215
+ ],
216
+ "selectedApprovals": [
217
+ "0x07e1ed8ea0e9601e5546b0a03aed683df3601407",
218
+ "0x288f0cd85005f34168f731a468aef268c2f9456f",
219
+ ],
220
+ "signatureCount": 2,
221
+ "threshold": 3,
222
+ "weight": 3,
223
+ }
224
+ `)
225
+ })
226
+
227
+ test('counts a nested owner only after its quorum', async () => {
228
+ const childConfig = MultisigConfig.from({
229
+ owners: [
230
+ { owner: owners[1]!.address, weight: 1 },
231
+ { owner: owners[2]!.address, weight: 1 },
232
+ ],
233
+ threshold: 2,
234
+ })
235
+ const child = MultisigConfig.getAddress(childConfig)
236
+ const config = MultisigConfig.from({
237
+ owners: [
238
+ { owner: owners[0]!.address, weight: 1 },
239
+ { owner: child, weight: 2 },
240
+ ],
241
+ threshold: 2,
242
+ })
243
+ const account = MultisigConfig.getAddress(config)
244
+ const hash = MultisigOperation.getHash({
245
+ account,
246
+ configVersion: 1n,
247
+ transaction,
248
+ type: 'transaction',
249
+ })
250
+ const childHash = MultisigConfig.getSignPayload({
251
+ account: child,
252
+ payload: hash,
253
+ version: 2n,
254
+ })
255
+ const childApprovals = [
256
+ signApproval(owners[1]!, childHash),
257
+ signApproval(owners[2]!, childHash),
258
+ ]
259
+ const rootApproval = signApproval(owners[0]!, hash)
260
+ const resolveConfig: MultisigOperation.selectApprovals.ResolveConfig = ({
261
+ account,
262
+ }) => {
263
+ if (!Address.isEqual(account, child)) throw new Error('unknown account')
264
+ return { config: childConfig, version: 2n }
265
+ }
266
+ const partial = await MultisigOperation.selectApprovals({
267
+ account,
268
+ approvals: [
269
+ rootApproval,
270
+ SignatureEnvelope.serialize({
271
+ account: child,
272
+ signatures: [SignatureEnvelope.deserialize(childApprovals[0]!)],
273
+ type: 'multisig',
274
+ }),
275
+ ],
276
+ config,
277
+ hash,
278
+ resolveConfig,
279
+ })
280
+ const complete = await MultisigOperation.selectApprovals({
281
+ account,
282
+ approvals: [
283
+ rootApproval,
284
+ SignatureEnvelope.serialize({
285
+ account: child,
286
+ signatures: childApprovals.map((approval) =>
287
+ SignatureEnvelope.deserialize(approval),
288
+ ),
289
+ type: 'multisig',
290
+ }),
291
+ ],
292
+ config,
293
+ hash,
294
+ resolveConfig,
295
+ })
296
+
297
+ expect({
298
+ complete: {
299
+ ...complete,
300
+ approvals: approvalAddresses(complete.approvals, hash),
301
+ selectedApprovals: approvalAddresses(complete.selectedApprovals, hash),
302
+ },
303
+ partial: {
304
+ ...partial,
305
+ approvals: approvalAddresses(partial.approvals, hash),
306
+ selectedApprovals: approvalAddresses(partial.selectedApprovals, hash),
307
+ },
308
+ }).toMatchInlineSnapshot(`
309
+ {
310
+ "complete": {
311
+ "approvals": [
312
+ "0x07e1ed8ea0e9601e5546b0a03aed683df3601407",
313
+ "0xe2d2c3c2fc4b17af341cc5c1a459af9606167e8a",
314
+ ],
315
+ "selectedApprovals": [
316
+ "0xe2d2c3c2fc4b17af341cc5c1a459af9606167e8a",
317
+ ],
318
+ "signatureCount": 1,
319
+ "threshold": 2,
320
+ "weight": 2,
321
+ },
322
+ "partial": {
323
+ "approvals": [
324
+ "0x07e1ed8ea0e9601e5546b0a03aed683df3601407",
325
+ "0xe2d2c3c2fc4b17af341cc5c1a459af9606167e8a",
326
+ ],
327
+ "selectedApprovals": [
328
+ "0x07e1ed8ea0e9601e5546b0a03aed683df3601407",
329
+ ],
330
+ "signatureCount": 1,
331
+ "threshold": 2,
332
+ "weight": 1,
333
+ },
334
+ }
335
+ `)
336
+ })
337
+
338
+ test('rejects invalid and non-owner approvals', async () => {
339
+ const hash = MultisigOperation.getHash({
340
+ account,
341
+ configVersion: 1n,
342
+ transaction,
343
+ type: 'transaction',
344
+ })
345
+ const invalid = signApproval(
346
+ owners[0]!,
347
+ `0x${'ff'.repeat(32)}` as `0x${string}`,
348
+ )
349
+ const nonOwner = signApproval(owners[2]!, hash)
350
+
351
+ await expect(
352
+ MultisigOperation.selectApprovals({
353
+ account,
354
+ approvals: [invalid],
355
+ config,
356
+ hash,
357
+ }),
358
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
359
+ `[MultisigOperation.InvalidApprovalError: Invalid multisig approval: signature from owner 0x07e1ed8ea0e9601e5546b0a03aed683df3601407 is invalid.]`,
360
+ )
361
+ await expect(
362
+ MultisigOperation.selectApprovals({
363
+ account,
364
+ approvals: [nonOwner],
365
+ config,
366
+ hash,
367
+ }),
368
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
369
+ `[MultisigOperation.InvalidApprovalError: Invalid multisig approval: signature is from non-owner 0xd3a9f047ad43d7e2e4e7e491f1fe2e657a2651b6.]`,
370
+ )
371
+ })
372
+
373
+ test('requires a resolver for nested owners', async () => {
374
+ const childConfig = MultisigConfig.from({
375
+ owners: [{ owner: owners[1]!.address, weight: 1 }],
376
+ threshold: 1,
377
+ })
378
+ const child = MultisigConfig.getAddress(childConfig)
379
+ const config = MultisigConfig.from({
380
+ owners: [{ owner: child, weight: 1 }],
381
+ threshold: 1,
382
+ })
383
+ const account = MultisigConfig.getAddress(config)
384
+ const hash = MultisigOperation.getHash({
385
+ account,
386
+ configVersion: 1n,
387
+ transaction,
388
+ type: 'transaction',
389
+ })
390
+ const childHash = MultisigConfig.getSignPayload({
391
+ account: child,
392
+ payload: hash,
393
+ version: 1n,
394
+ })
395
+
396
+ await expect(
397
+ MultisigOperation.selectApprovals({
398
+ account,
399
+ approvals: [
400
+ SignatureEnvelope.serialize({
401
+ account: child,
402
+ signatures: [
403
+ SignatureEnvelope.deserialize(
404
+ signApproval(owners[1]!, childHash),
405
+ ),
406
+ ],
407
+ type: 'multisig',
408
+ }),
409
+ ],
410
+ config,
411
+ hash,
412
+ }),
413
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
414
+ `[MultisigOperation.InvalidApprovalError: Invalid multisig approval: nested multisig owner 0x7888d60e9cc26c8569d394fce435c248f1e49c3b requires a config resolver.]`,
415
+ )
416
+ })
417
+
418
+ test('rejects keychain and bootstrap nested approvals', async () => {
419
+ const childConfig = MultisigConfig.from({
420
+ owners: [{ owner: owners[1]!.address, weight: 1 }],
421
+ threshold: 1,
422
+ })
423
+ const child = MultisigConfig.getAddress(childConfig)
424
+ const config = MultisigConfig.from({
425
+ owners: [
426
+ { owner: owners[0]!.address, weight: 1 },
427
+ { owner: child, weight: 1 },
428
+ ],
429
+ threshold: 1,
430
+ })
431
+ const account = MultisigConfig.getAddress(config)
432
+ const hash = MultisigOperation.getHash({
433
+ account,
434
+ configVersion: 1n,
435
+ transaction,
436
+ type: 'transaction',
437
+ })
438
+ const childHash = MultisigConfig.getSignPayload({
439
+ account: child,
440
+ payload: hash,
441
+ version: 0n,
442
+ })
443
+
444
+ await expect(
445
+ MultisigOperation.selectApprovals({
446
+ account,
447
+ approvals: [
448
+ SignatureEnvelope.serialize({
449
+ inner: SignatureEnvelope.deserialize(
450
+ signApproval(owners[0]!, hash),
451
+ ),
452
+ type: 'keychain',
453
+ userAddress: owners[0]!.address,
454
+ }),
455
+ ],
456
+ config,
457
+ hash,
458
+ }),
459
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
460
+ `[MultisigOperation.InvalidApprovalError: Invalid multisig approval: keychain signatures cannot approve a multisig operation.]`,
461
+ )
462
+ await expect(
463
+ MultisigOperation.selectApprovals({
464
+ account,
465
+ approvals: [
466
+ SignatureEnvelope.serialize({
467
+ account: child,
468
+ init: childConfig,
469
+ signatures: [
470
+ SignatureEnvelope.deserialize(
471
+ signApproval(owners[1]!, childHash),
472
+ ),
473
+ ],
474
+ type: 'multisig',
475
+ }),
476
+ ],
477
+ config,
478
+ hash,
479
+ resolveConfig: () => ({ config: childConfig, version: 0n }),
480
+ }),
481
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
482
+ `[MultisigOperation.InvalidApprovalError: Invalid multisig approval: nested multisig owner 0x7888d60e9cc26c8569d394fce435c248f1e49c3b cannot carry init.]`,
483
+ )
484
+ })
485
+
486
+ test('rejects nested account cycles', async () => {
487
+ const config = MultisigConfig.from({
488
+ owners: [{ owner: account, weight: 1 }],
489
+ threshold: 1,
490
+ })
491
+ const hash = MultisigOperation.getHash({
492
+ account,
493
+ configVersion: 2n,
494
+ transaction,
495
+ type: 'transaction',
496
+ })
497
+ const nestedHash = MultisigConfig.getSignPayload({
498
+ account,
499
+ payload: hash,
500
+ version: 2n,
501
+ })
502
+
503
+ await expect(
504
+ MultisigOperation.selectApprovals({
505
+ account,
506
+ approvals: [
507
+ SignatureEnvelope.serialize({
508
+ account,
509
+ signatures: [
510
+ SignatureEnvelope.deserialize(
511
+ signApproval(owners[0]!, nestedHash),
512
+ ),
513
+ ],
514
+ type: 'multisig',
515
+ }),
516
+ ],
517
+ config,
518
+ hash,
519
+ resolveConfig: () => ({ config, version: 2n }),
520
+ }),
521
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
522
+ `[MultisigOperation.InvalidApprovalError: Invalid multisig approval: nested multisig owner 0xf81b7763d3a6876195d780865bd783dbd97dd36e is invalid.]`,
523
+ )
524
+ })
525
+
526
+ test('rejects excess nesting depth', async () => {
527
+ const grandchildConfig = MultisigConfig.from({
528
+ owners: [{ owner: owners[2]!.address, weight: 1 }],
529
+ threshold: 1,
530
+ })
531
+ const grandchild = MultisigConfig.getAddress(grandchildConfig)
532
+ const childConfig = MultisigConfig.from({
533
+ owners: [{ owner: grandchild, weight: 1 }],
534
+ threshold: 1,
535
+ })
536
+ const child = MultisigConfig.getAddress(childConfig)
537
+ const config = MultisigConfig.from({
538
+ owners: [{ owner: child, weight: 1 }],
539
+ threshold: 1,
540
+ })
541
+ const account = MultisigConfig.getAddress(config)
542
+ const hash = MultisigOperation.getHash({
543
+ account,
544
+ configVersion: 1n,
545
+ transaction,
546
+ type: 'transaction',
547
+ })
548
+ const childHash = MultisigConfig.getSignPayload({
549
+ account: child,
550
+ payload: hash,
551
+ version: 1n,
552
+ })
553
+ const grandchildHash = MultisigConfig.getSignPayload({
554
+ account: grandchild,
555
+ payload: childHash,
556
+ version: 1n,
557
+ })
558
+
559
+ await expect(
560
+ MultisigOperation.selectApprovals({
561
+ account,
562
+ approvals: [
563
+ SignatureEnvelope.serialize({
564
+ account: child,
565
+ signatures: [
566
+ {
567
+ account: grandchild,
568
+ signatures: [
569
+ SignatureEnvelope.deserialize(
570
+ signApproval(owners[2]!, grandchildHash),
571
+ ),
572
+ ],
573
+ type: 'multisig',
574
+ },
575
+ ],
576
+ type: 'multisig',
577
+ }),
578
+ ],
579
+ config,
580
+ hash,
581
+ resolveConfig: ({ account }) => {
582
+ if (Address.isEqual(account, child))
583
+ return { config: childConfig, version: 1n }
584
+ return { config: grandchildConfig, version: 1n }
585
+ },
586
+ }),
587
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
588
+ `[MultisigOperation.InvalidApprovalError: Invalid multisig approval: nested multisig owner 0xf529c8f6b2b4c72102af4cfe5eeb55b7d45dede2 is invalid.]`,
589
+ )
590
+ })
591
+ })
592
+
593
+ describe('serializeTransaction', () => {
594
+ test('initialized and bootstrap transactions', async () => {
595
+ const results = []
596
+ for (const init of [false, true]) {
597
+ const configVersion = init ? 0n : 1n
598
+ const hash = MultisigOperation.getHash({
599
+ account,
600
+ configVersion,
601
+ transaction,
602
+ type: 'transaction',
603
+ })
604
+ const selection = await MultisigOperation.selectApprovals({
605
+ account,
606
+ approvals: [
607
+ signApproval(owners[1]!, hash),
608
+ signApproval(owners[0]!, hash),
609
+ ],
610
+ config,
611
+ hash,
612
+ })
613
+ const operation = MultisigOperation.from({
614
+ account,
615
+ approvals: selection.approvals,
616
+ config,
617
+ configVersion,
618
+ createdAt: 1,
619
+ hash,
620
+ init,
621
+ signatureCount: selection.signatureCount,
622
+ status: 'pending',
623
+ threshold: selection.threshold,
624
+ transaction,
625
+ type: 'transaction',
626
+ updatedAt: 1,
627
+ weight: selection.weight,
628
+ })
629
+ const serialized = MultisigOperation.serializeTransaction(operation, {
630
+ approvals: selection.selectedApprovals,
631
+ })
632
+ const value = TxEnvelopeTempo.deserialize(serialized)
633
+ results.push({
634
+ account: value.signature?.account,
635
+ hash: Hash.keccak256(serialized),
636
+ init: value.signature?.type === 'multisig' && !!value.signature.init,
637
+ signatureCount:
638
+ value.signature?.type === 'multisig'
639
+ ? value.signature.signatures.length
640
+ : 0,
641
+ type: serialized.slice(0, 4),
642
+ })
643
+ }
644
+
645
+ expect(results).toMatchInlineSnapshot(`
646
+ [
647
+ {
648
+ "account": "0xf81b7763d3a6876195d780865bd783dbd97dd36e",
649
+ "hash": "0x704768725a4e798e2656f0f355f99507522c97b2947f551a5ee696216a2ba6fe",
650
+ "init": false,
651
+ "signatureCount": 2,
652
+ "type": "0x76",
653
+ },
654
+ {
655
+ "account": "0xf81b7763d3a6876195d780865bd783dbd97dd36e",
656
+ "hash": "0x08131922349c385f9112fefc617fa6f6258a71df1125ec243bb3ee7cff59b700",
657
+ "init": true,
658
+ "signatureCount": 2,
659
+ "type": "0x76",
660
+ },
661
+ ]
662
+ `)
663
+ })
664
+
665
+ test('fee-payer transactions', async () => {
666
+ const envelope = TxEnvelopeTempo.from({
667
+ calls: [{ data: '0x1234', to: owner_1 }],
668
+ chainId: 4217,
669
+ })
670
+ const transactions = [
671
+ TxEnvelopeTempo.serialize(envelope, {
672
+ format: 'feePayer',
673
+ sender: account,
674
+ }),
675
+ TxEnvelopeTempo.serialize(
676
+ {
677
+ ...envelope,
678
+ feePayerSignature: { r: 5n, s: 6n, yParity: 0 },
679
+ },
680
+ { format: 'feePayer' },
681
+ ),
682
+ ] as const
683
+ const results = []
684
+ for (const transaction of transactions) {
685
+ const hash = MultisigOperation.getHash({
686
+ account,
687
+ configVersion: 1n,
688
+ transaction,
689
+ type: 'transaction',
690
+ })
691
+ const selection = await MultisigOperation.selectApprovals({
692
+ account,
693
+ approvals: [
694
+ signApproval(owners[0]!, hash),
695
+ signApproval(owners[1]!, hash),
696
+ ],
697
+ config,
698
+ hash,
699
+ })
700
+ const serialized = MultisigOperation.serializeTransaction(
701
+ MultisigOperation.from({
702
+ account,
703
+ approvals: selection.approvals,
704
+ config,
705
+ configVersion: 1n,
706
+ createdAt: 1,
707
+ hash,
708
+ init: false,
709
+ signatureCount: selection.signatureCount,
710
+ status: 'pending',
711
+ threshold: selection.threshold,
712
+ transaction,
713
+ type: 'transaction',
714
+ updatedAt: 1,
715
+ weight: selection.weight,
716
+ }),
717
+ { approvals: selection.selectedApprovals },
718
+ )
719
+ const value = TxEnvelopeTempo.deserialize(serialized)
720
+ results.push({
721
+ feePayerSignature: value.feePayerSignature,
722
+ from: value.from,
723
+ signatureCount:
724
+ value.signature?.type === 'multisig'
725
+ ? value.signature.signatures.length
726
+ : 0,
727
+ type: serialized.slice(0, 4),
728
+ })
729
+ }
730
+
731
+ expect(results).toMatchInlineSnapshot(`
732
+ [
733
+ {
734
+ "feePayerSignature": null,
735
+ "from": "0xf81b7763d3a6876195d780865bd783dbd97dd36e",
736
+ "signatureCount": 2,
737
+ "type": "0x78",
738
+ },
739
+ {
740
+ "feePayerSignature": {
741
+ "r": 5n,
742
+ "s": 6n,
743
+ "yParity": 0,
744
+ },
745
+ "from": "0xf81b7763d3a6876195d780865bd783dbd97dd36e",
746
+ "signatureCount": 2,
747
+ "type": "0x78",
748
+ },
749
+ ]
750
+ `)
751
+ })
752
+
753
+ test('rejects an approval not retained by the operation', async () => {
754
+ const hash = MultisigOperation.getHash({
755
+ account,
756
+ configVersion: 1n,
757
+ transaction,
758
+ type: 'transaction',
759
+ })
760
+ const approval_1 = signApproval(owners[0]!, hash)
761
+ const approval_2 = signApproval(owners[1]!, hash)
762
+ const selection = await MultisigOperation.selectApprovals({
763
+ account,
764
+ approvals: [approval_1],
765
+ config,
766
+ hash,
767
+ })
768
+ const operation = MultisigOperation.from({
769
+ account,
770
+ approvals: selection.approvals,
771
+ config,
772
+ configVersion: 1n,
773
+ createdAt: 1,
774
+ hash,
775
+ init: false,
776
+ signatureCount: selection.signatureCount,
777
+ status: 'pending',
778
+ threshold: selection.threshold,
779
+ transaction,
780
+ type: 'transaction',
781
+ updatedAt: 1,
782
+ weight: selection.weight,
783
+ })
784
+
785
+ expect(() =>
786
+ MultisigOperation.serializeTransaction(operation, {
787
+ approvals: [approval_2],
788
+ }),
789
+ ).toThrowErrorMatchingInlineSnapshot(
790
+ `[MultisigOperation.InvalidOperationError: Invalid multisig operation: transaction signature is not a retained approval.]`,
791
+ )
792
+ })
793
+ })
794
+
795
+ describe('from', () => {
796
+ test('transaction states', () => {
797
+ const pending = MultisigOperation.from(transactionPending)
798
+ const submitting = MultisigOperation.from({
799
+ ...transactionPending,
800
+ approvals: [approval_1, approval_2],
801
+ expiresAt: 10,
802
+ signatureCount: 2,
803
+ status: 'submitting',
804
+ submissionId,
805
+ weight: 2,
806
+ })
807
+ const success = MultisigOperation.from({
808
+ ...transactionPending,
809
+ approvals: [approval_1, approval_2],
810
+ signatureCount: 2,
811
+ status: 'success',
812
+ transactionHash,
813
+ weight: 2,
814
+ })
815
+
816
+ expect({ pending, submitting, success }).toMatchInlineSnapshot(`
817
+ {
818
+ "pending": {
819
+ "account": "0xf81b7763d3a6876195d780865bd783dbd97dd36e",
820
+ "approvals": [
821
+ "0x01000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000065ecbe4d1a6330a44c8f7ef951d4bf165e6c6b721efada985fb41661bc6e7fd6c8734640c4998ff7e374b06ce1a64a2ecd82ab036384fb83d9a79b127a27d503200",
822
+ ],
823
+ "config": {
824
+ "owners": [
825
+ {
826
+ "owner": "0x07e1ed8ea0e9601e5546b0a03aed683df3601407",
827
+ "weight": 1,
828
+ },
829
+ {
830
+ "owner": "0x288f0cd85005f34168f731a468aef268c2f9456f",
831
+ "weight": 1,
832
+ },
833
+ ],
834
+ "salt": "0x0000000000000000000000000000000000000000000000000000000000000000",
835
+ "threshold": 2,
836
+ },
837
+ "configVersion": 1n,
838
+ "createdAt": 1,
839
+ "hash": "0xcdbc24a8fb192f799c5d166b13a99fb29cbb15e71a5e730988d6f8b3c5959d02",
840
+ "init": false,
841
+ "signatureCount": 1,
842
+ "status": "pending",
843
+ "threshold": 2,
844
+ "transaction": "0x76e9821079808080dad99407e1ed8ea0e9601e5546b0a03aed683df360140780821234c0808080808080c0",
845
+ "type": "transaction",
846
+ "updatedAt": 2,
847
+ "weight": 1,
848
+ },
849
+ "submitting": {
850
+ "account": "0xf81b7763d3a6876195d780865bd783dbd97dd36e",
851
+ "approvals": [
852
+ "0x01000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000065ecbe4d1a6330a44c8f7ef951d4bf165e6c6b721efada985fb41661bc6e7fd6c8734640c4998ff7e374b06ce1a64a2ecd82ab036384fb83d9a79b127a27d503200",
853
+ "0x01000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000047cf27b188d034f7e8a52380304b51ac3c08969e277f21b35a60b48fc4766997807775510db8ed040293d9ac69f7430dbba7dade63ce982299e04b79d227873d100",
854
+ ],
855
+ "config": {
856
+ "owners": [
857
+ {
858
+ "owner": "0x07e1ed8ea0e9601e5546b0a03aed683df3601407",
859
+ "weight": 1,
860
+ },
861
+ {
862
+ "owner": "0x288f0cd85005f34168f731a468aef268c2f9456f",
863
+ "weight": 1,
864
+ },
865
+ ],
866
+ "salt": "0x0000000000000000000000000000000000000000000000000000000000000000",
867
+ "threshold": 2,
868
+ },
869
+ "configVersion": 1n,
870
+ "createdAt": 1,
871
+ "expiresAt": 10,
872
+ "hash": "0xcdbc24a8fb192f799c5d166b13a99fb29cbb15e71a5e730988d6f8b3c5959d02",
873
+ "init": false,
874
+ "signatureCount": 2,
875
+ "status": "submitting",
876
+ "submissionId": "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
877
+ "threshold": 2,
878
+ "transaction": "0x76e9821079808080dad99407e1ed8ea0e9601e5546b0a03aed683df360140780821234c0808080808080c0",
879
+ "type": "transaction",
880
+ "updatedAt": 2,
881
+ "weight": 2,
882
+ },
883
+ "success": {
884
+ "account": "0xf81b7763d3a6876195d780865bd783dbd97dd36e",
885
+ "approvals": [
886
+ "0x01000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000065ecbe4d1a6330a44c8f7ef951d4bf165e6c6b721efada985fb41661bc6e7fd6c8734640c4998ff7e374b06ce1a64a2ecd82ab036384fb83d9a79b127a27d503200",
887
+ "0x01000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000047cf27b188d034f7e8a52380304b51ac3c08969e277f21b35a60b48fc4766997807775510db8ed040293d9ac69f7430dbba7dade63ce982299e04b79d227873d100",
888
+ ],
889
+ "config": {
890
+ "owners": [
891
+ {
892
+ "owner": "0x07e1ed8ea0e9601e5546b0a03aed683df3601407",
893
+ "weight": 1,
894
+ },
895
+ {
896
+ "owner": "0x288f0cd85005f34168f731a468aef268c2f9456f",
897
+ "weight": 1,
898
+ },
899
+ ],
900
+ "salt": "0x0000000000000000000000000000000000000000000000000000000000000000",
901
+ "threshold": 2,
902
+ },
903
+ "configVersion": 1n,
904
+ "createdAt": 1,
905
+ "hash": "0xcdbc24a8fb192f799c5d166b13a99fb29cbb15e71a5e730988d6f8b3c5959d02",
906
+ "init": false,
907
+ "signatureCount": 2,
908
+ "status": "success",
909
+ "threshold": 2,
910
+ "transaction": "0x76e9821079808080dad99407e1ed8ea0e9601e5546b0a03aed683df360140780821234c0808080808080c0",
911
+ "transactionHash": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
912
+ "type": "transaction",
913
+ "updatedAt": 2,
914
+ "weight": 2,
915
+ },
916
+ }
917
+ `)
918
+ })
919
+
920
+ test('fee-payer transaction', () => {
921
+ const transaction = TxEnvelopeTempo.serialize(
922
+ TxEnvelopeTempo.from({
923
+ calls: [{ data: '0x1234', to: owner_1 }],
924
+ chainId: 4217,
925
+ }),
926
+ { format: 'feePayer', sender: account },
927
+ )
928
+ const operation = MultisigOperation.from({
929
+ ...transactionPending,
930
+ hash: MultisigConfig.getSignPayload({
931
+ account,
932
+ payload: TxEnvelopeTempo.getSignPayload(
933
+ TxEnvelopeTempo.deserialize(transaction),
934
+ ),
935
+ version: 1n,
936
+ }),
937
+ transaction,
938
+ })
939
+
940
+ expect(operation).toMatchInlineSnapshot(
941
+ {
942
+ hash: expect.any(String),
943
+ transaction: expect.stringMatching(/^0x78/),
944
+ },
945
+ `
946
+ {
947
+ "account": "0xf81b7763d3a6876195d780865bd783dbd97dd36e",
948
+ "approvals": [
949
+ "0x01000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000065ecbe4d1a6330a44c8f7ef951d4bf165e6c6b721efada985fb41661bc6e7fd6c8734640c4998ff7e374b06ce1a64a2ecd82ab036384fb83d9a79b127a27d503200",
950
+ ],
951
+ "config": {
952
+ "owners": [
953
+ {
954
+ "owner": "0x07e1ed8ea0e9601e5546b0a03aed683df3601407",
955
+ "weight": 1,
956
+ },
957
+ {
958
+ "owner": "0x288f0cd85005f34168f731a468aef268c2f9456f",
959
+ "weight": 1,
960
+ },
961
+ ],
962
+ "salt": "0x0000000000000000000000000000000000000000000000000000000000000000",
963
+ "threshold": 2,
964
+ },
965
+ "configVersion": 1n,
966
+ "createdAt": 1,
967
+ "hash": Any<String>,
968
+ "init": false,
969
+ "signatureCount": 1,
970
+ "status": "pending",
971
+ "threshold": 2,
972
+ "transaction": StringMatching /\\^0x78/,
973
+ "type": "transaction",
974
+ "updatedAt": 2,
975
+ "weight": 1,
976
+ }
977
+ `,
978
+ )
979
+ })
980
+
981
+ test('fee-payer-signed transaction', () => {
982
+ const transaction = TxEnvelopeTempo.serialize(
983
+ TxEnvelopeTempo.from({
984
+ calls: [{ data: '0x1234', to: owner_1 }],
985
+ chainId: 4217,
986
+ feePayerSignature: { r: 5n, s: 6n, yParity: 0 },
987
+ }),
988
+ { format: 'feePayer' },
989
+ )
990
+ const operation = MultisigOperation.from({
991
+ ...transactionPending,
992
+ hash: MultisigConfig.getSignPayload({
993
+ account,
994
+ payload: TxEnvelopeTempo.getSignPayload(
995
+ TxEnvelopeTempo.deserialize(transaction),
996
+ ),
997
+ version: 1n,
998
+ }),
999
+ transaction,
1000
+ })
1001
+
1002
+ expect(operation.transaction).toBe(transaction)
1003
+ })
1004
+
1005
+ test('bootstrap transaction', () => {
1006
+ const operation = MultisigOperation.from({
1007
+ ...transactionPending,
1008
+ configVersion: 0n,
1009
+ hash: MultisigConfig.getSignPayload({
1010
+ account,
1011
+ payload: TxEnvelopeTempo.getSignPayload(
1012
+ TxEnvelopeTempo.deserialize(transaction),
1013
+ ),
1014
+ version: 0n,
1015
+ }),
1016
+ init: true,
1017
+ })
1018
+
1019
+ expect(operation).toMatchInlineSnapshot(
1020
+ {
1021
+ hash: expect.any(String),
1022
+ transaction: expect.any(String),
1023
+ },
1024
+ `
1025
+ {
1026
+ "account": "0xf81b7763d3a6876195d780865bd783dbd97dd36e",
1027
+ "approvals": [
1028
+ "0x01000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000065ecbe4d1a6330a44c8f7ef951d4bf165e6c6b721efada985fb41661bc6e7fd6c8734640c4998ff7e374b06ce1a64a2ecd82ab036384fb83d9a79b127a27d503200",
1029
+ ],
1030
+ "config": {
1031
+ "owners": [
1032
+ {
1033
+ "owner": "0x07e1ed8ea0e9601e5546b0a03aed683df3601407",
1034
+ "weight": 1,
1035
+ },
1036
+ {
1037
+ "owner": "0x288f0cd85005f34168f731a468aef268c2f9456f",
1038
+ "weight": 1,
1039
+ },
1040
+ ],
1041
+ "salt": "0x0000000000000000000000000000000000000000000000000000000000000000",
1042
+ "threshold": 2,
1043
+ },
1044
+ "configVersion": 0n,
1045
+ "createdAt": 1,
1046
+ "hash": Any<String>,
1047
+ "init": true,
1048
+ "signatureCount": 1,
1049
+ "status": "pending",
1050
+ "threshold": 2,
1051
+ "transaction": Any<String>,
1052
+ "type": "transaction",
1053
+ "updatedAt": 2,
1054
+ "weight": 1,
1055
+ }
1056
+ `,
1057
+ )
1058
+ })
1059
+
1060
+ test('initialized transaction at config version zero', () => {
1061
+ const operation = MultisigOperation.from({
1062
+ ...transactionPending,
1063
+ configVersion: 0n,
1064
+ hash: MultisigConfig.getSignPayload({
1065
+ account,
1066
+ payload: TxEnvelopeTempo.getSignPayload(
1067
+ TxEnvelopeTempo.deserialize(transaction),
1068
+ ),
1069
+ version: 0n,
1070
+ }),
1071
+ })
1072
+
1073
+ expect({
1074
+ configVersion: operation.configVersion,
1075
+ init: operation.init,
1076
+ }).toMatchInlineSnapshot(`
1077
+ {
1078
+ "configVersion": 0n,
1079
+ "init": false,
1080
+ }
1081
+ `)
1082
+ })
1083
+
1084
+ test('key authorization states', () => {
1085
+ const pending = MultisigOperation.from(keyAuthorizationPending)
1086
+ const authorization = KeyAuthorization.deserialize(keyAuthorization)
1087
+ const success = MultisigOperation.from({
1088
+ ...keyAuthorizationPending,
1089
+ approvals: [approval_1, approval_2],
1090
+ keyAuthorization: KeyAuthorization.serialize(
1091
+ KeyAuthorization.from(authorization, {
1092
+ signature: {
1093
+ account,
1094
+ signatures: [
1095
+ SignatureEnvelope.deserialize(approval_1),
1096
+ SignatureEnvelope.deserialize(approval_2),
1097
+ ],
1098
+ type: 'multisig',
1099
+ },
1100
+ }),
1101
+ ),
1102
+ signatureCount: 2,
1103
+ status: 'success',
1104
+ weight: 2,
1105
+ })
1106
+
1107
+ expect({ pending, success }).toMatchInlineSnapshot(`
1108
+ {
1109
+ "pending": {
1110
+ "account": "0xf81b7763d3a6876195d780865bd783dbd97dd36e",
1111
+ "approvals": [
1112
+ "0x01000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000065ecbe4d1a6330a44c8f7ef951d4bf165e6c6b721efada985fb41661bc6e7fd6c8734640c4998ff7e374b06ce1a64a2ecd82ab036384fb83d9a79b127a27d503200",
1113
+ ],
1114
+ "config": {
1115
+ "owners": [
1116
+ {
1117
+ "owner": "0x07e1ed8ea0e9601e5546b0a03aed683df3601407",
1118
+ "weight": 1,
1119
+ },
1120
+ {
1121
+ "owner": "0x288f0cd85005f34168f731a468aef268c2f9456f",
1122
+ "weight": 1,
1123
+ },
1124
+ ],
1125
+ "salt": "0x0000000000000000000000000000000000000000000000000000000000000000",
1126
+ "threshold": 2,
1127
+ },
1128
+ "configVersion": 1n,
1129
+ "createdAt": 1,
1130
+ "hash": "0xf6995eb69c12c03d3d13b357abf7cac22b4effe52fba8ff02e5aef26161f77a0",
1131
+ "init": false,
1132
+ "keyAuthorization": "0xf838f782107980943333333333333333333333333333333333333333846b49d2008080808094f81b7763d3a6876195d780865bd783dbd97dd36e",
1133
+ "signatureCount": 1,
1134
+ "status": "pending",
1135
+ "threshold": 2,
1136
+ "type": "keyAuthorization",
1137
+ "updatedAt": 2,
1138
+ "weight": 1,
1139
+ },
1140
+ "success": {
1141
+ "account": "0xf81b7763d3a6876195d780865bd783dbd97dd36e",
1142
+ "approvals": [
1143
+ "0x01000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000065ecbe4d1a6330a44c8f7ef951d4bf165e6c6b721efada985fb41661bc6e7fd6c8734640c4998ff7e374b06ce1a64a2ecd82ab036384fb83d9a79b127a27d503200",
1144
+ "0x01000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000047cf27b188d034f7e8a52380304b51ac3c08969e277f21b35a60b48fc4766997807775510db8ed040293d9ac69f7430dbba7dade63ce982299e04b79d227873d100",
1145
+ ],
1146
+ "config": {
1147
+ "owners": [
1148
+ {
1149
+ "owner": "0x07e1ed8ea0e9601e5546b0a03aed683df3601407",
1150
+ "weight": 1,
1151
+ },
1152
+ {
1153
+ "owner": "0x288f0cd85005f34168f731a468aef268c2f9456f",
1154
+ "weight": 1,
1155
+ },
1156
+ ],
1157
+ "salt": "0x0000000000000000000000000000000000000000000000000000000000000000",
1158
+ "threshold": 2,
1159
+ },
1160
+ "configVersion": 1n,
1161
+ "createdAt": 1,
1162
+ "hash": "0xf6995eb69c12c03d3d13b357abf7cac22b4effe52fba8ff02e5aef26161f77a0",
1163
+ "init": false,
1164
+ "keyAuthorization": "0xf9015ff782107980943333333333333333333333333333333333333333846b49d2008080808094f81b7763d3a6876195d780865bd783dbd97dd36eb9012405f9012094f81b7763d3a6876195d780865bd783dbd97dd36ef90108b88201000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000065ecbe4d1a6330a44c8f7ef951d4bf165e6c6b721efada985fb41661bc6e7fd6c8734640c4998ff7e374b06ce1a64a2ecd82ab036384fb83d9a79b127a27d503200b88201000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000047cf27b188d034f7e8a52380304b51ac3c08969e277f21b35a60b48fc4766997807775510db8ed040293d9ac69f7430dbba7dade63ce982299e04b79d227873d100",
1165
+ "signatureCount": 2,
1166
+ "status": "success",
1167
+ "threshold": 2,
1168
+ "type": "keyAuthorization",
1169
+ "updatedAt": 2,
1170
+ "weight": 2,
1171
+ },
1172
+ }
1173
+ `)
1174
+ })
1175
+
1176
+ test('bootstrap key authorization', () => {
1177
+ const authorization = KeyAuthorization.deserialize(keyAuthorization)
1178
+ const keyAuthorization_ = KeyAuthorization.serialize(
1179
+ KeyAuthorization.from(authorization, {
1180
+ signature: {
1181
+ account,
1182
+ init: config,
1183
+ signatures: [
1184
+ SignatureEnvelope.deserialize(approval_1),
1185
+ SignatureEnvelope.deserialize(approval_2),
1186
+ ],
1187
+ type: 'multisig',
1188
+ },
1189
+ }),
1190
+ )
1191
+ const operation = MultisigOperation.from({
1192
+ ...keyAuthorizationPending,
1193
+ approvals: [approval_1, approval_2],
1194
+ configVersion: 0n,
1195
+ hash: MultisigConfig.getSignPayload({
1196
+ account,
1197
+ payload: KeyAuthorization.getSignPayload(authorization),
1198
+ version: 0n,
1199
+ }),
1200
+ init: true,
1201
+ keyAuthorization: keyAuthorization_,
1202
+ signatureCount: 2,
1203
+ status: 'success',
1204
+ weight: 2,
1205
+ })
1206
+
1207
+ expect(operation).toMatchInlineSnapshot(
1208
+ {
1209
+ hash: expect.any(String),
1210
+ keyAuthorization: expect.any(String),
1211
+ },
1212
+ `
1213
+ {
1214
+ "account": "0xf81b7763d3a6876195d780865bd783dbd97dd36e",
1215
+ "approvals": [
1216
+ "0x01000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000065ecbe4d1a6330a44c8f7ef951d4bf165e6c6b721efada985fb41661bc6e7fd6c8734640c4998ff7e374b06ce1a64a2ecd82ab036384fb83d9a79b127a27d503200",
1217
+ "0x01000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000047cf27b188d034f7e8a52380304b51ac3c08969e277f21b35a60b48fc4766997807775510db8ed040293d9ac69f7430dbba7dade63ce982299e04b79d227873d100",
1218
+ ],
1219
+ "config": {
1220
+ "owners": [
1221
+ {
1222
+ "owner": "0x07e1ed8ea0e9601e5546b0a03aed683df3601407",
1223
+ "weight": 1,
1224
+ },
1225
+ {
1226
+ "owner": "0x288f0cd85005f34168f731a468aef268c2f9456f",
1227
+ "weight": 1,
1228
+ },
1229
+ ],
1230
+ "salt": "0x0000000000000000000000000000000000000000000000000000000000000000",
1231
+ "threshold": 2,
1232
+ },
1233
+ "configVersion": 0n,
1234
+ "createdAt": 1,
1235
+ "hash": Any<String>,
1236
+ "init": true,
1237
+ "keyAuthorization": Any<String>,
1238
+ "signatureCount": 2,
1239
+ "status": "success",
1240
+ "threshold": 2,
1241
+ "type": "keyAuthorization",
1242
+ "updatedAt": 2,
1243
+ "weight": 2,
1244
+ }
1245
+ `,
1246
+ )
1247
+ })
1248
+ })
1249
+
1250
+ describe('RPC conversion', () => {
1251
+ test('round-trips transaction and key authorization operations', () => {
1252
+ const transactionRpc = MultisigOperation.toRpc(transactionPending)
1253
+ const keyAuthorizationRpc = MultisigOperation.toRpc(keyAuthorizationPending)
1254
+
1255
+ expect({ keyAuthorizationRpc, transactionRpc }).toMatchInlineSnapshot(`
1256
+ {
1257
+ "keyAuthorizationRpc": {
1258
+ "account": "0xf81b7763d3a6876195d780865bd783dbd97dd36e",
1259
+ "approvals": [
1260
+ "0x01000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000065ecbe4d1a6330a44c8f7ef951d4bf165e6c6b721efada985fb41661bc6e7fd6c8734640c4998ff7e374b06ce1a64a2ecd82ab036384fb83d9a79b127a27d503200",
1261
+ ],
1262
+ "config": {
1263
+ "owners": [
1264
+ {
1265
+ "owner": "0x07e1ed8ea0e9601e5546b0a03aed683df3601407",
1266
+ "weight": 1,
1267
+ },
1268
+ {
1269
+ "owner": "0x288f0cd85005f34168f731a468aef268c2f9456f",
1270
+ "weight": 1,
1271
+ },
1272
+ ],
1273
+ "salt": "0x0000000000000000000000000000000000000000000000000000000000000000",
1274
+ "threshold": 2,
1275
+ },
1276
+ "configVersion": "0x1",
1277
+ "createdAt": 1,
1278
+ "hash": "0xf6995eb69c12c03d3d13b357abf7cac22b4effe52fba8ff02e5aef26161f77a0",
1279
+ "init": false,
1280
+ "keyAuthorization": "0xf838f782107980943333333333333333333333333333333333333333846b49d2008080808094f81b7763d3a6876195d780865bd783dbd97dd36e",
1281
+ "signatureCount": 1,
1282
+ "status": "pending",
1283
+ "threshold": 2,
1284
+ "type": "keyAuthorization",
1285
+ "updatedAt": 2,
1286
+ "weight": 1,
1287
+ },
1288
+ "transactionRpc": {
1289
+ "account": "0xf81b7763d3a6876195d780865bd783dbd97dd36e",
1290
+ "approvals": [
1291
+ "0x01000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000065ecbe4d1a6330a44c8f7ef951d4bf165e6c6b721efada985fb41661bc6e7fd6c8734640c4998ff7e374b06ce1a64a2ecd82ab036384fb83d9a79b127a27d503200",
1292
+ ],
1293
+ "config": {
1294
+ "owners": [
1295
+ {
1296
+ "owner": "0x07e1ed8ea0e9601e5546b0a03aed683df3601407",
1297
+ "weight": 1,
1298
+ },
1299
+ {
1300
+ "owner": "0x288f0cd85005f34168f731a468aef268c2f9456f",
1301
+ "weight": 1,
1302
+ },
1303
+ ],
1304
+ "salt": "0x0000000000000000000000000000000000000000000000000000000000000000",
1305
+ "threshold": 2,
1306
+ },
1307
+ "configVersion": "0x1",
1308
+ "createdAt": 1,
1309
+ "hash": "0xcdbc24a8fb192f799c5d166b13a99fb29cbb15e71a5e730988d6f8b3c5959d02",
1310
+ "init": false,
1311
+ "signatureCount": 1,
1312
+ "status": "pending",
1313
+ "threshold": 2,
1314
+ "transaction": "0x76e9821079808080dad99407e1ed8ea0e9601e5546b0a03aed683df360140780821234c0808080808080c0",
1315
+ "type": "transaction",
1316
+ "updatedAt": 2,
1317
+ "weight": 1,
1318
+ },
1319
+ }
1320
+ `)
1321
+ expect(MultisigOperation.fromRpc(transactionRpc)).toStrictEqual(
1322
+ MultisigOperation.from(transactionPending),
1323
+ )
1324
+ expect(MultisigOperation.fromRpc(keyAuthorizationRpc)).toStrictEqual(
1325
+ MultisigOperation.from(keyAuthorizationPending),
1326
+ )
1327
+ })
1328
+ })
1329
+
1330
+ describe('validation', () => {
1331
+ test.each([
1332
+ {
1333
+ name: 'mismatched hash',
1334
+ operation: { ...transactionPending, hash: transactionHash },
1335
+ },
1336
+ {
1337
+ name: 'outer transaction signature',
1338
+ operation: {
1339
+ ...transactionPending,
1340
+ transaction: TxEnvelopeTempo.serialize(
1341
+ TxEnvelopeTempo.deserialize(transaction),
1342
+ { signature: SignatureEnvelope.deserialize(approval_1) },
1343
+ ),
1344
+ },
1345
+ },
1346
+ {
1347
+ name: 'pending transaction submission fields',
1348
+ operation: { ...transactionPending, submissionId },
1349
+ },
1350
+ {
1351
+ name: 'submitting transaction without lease',
1352
+ operation: { ...transactionPending, status: 'submitting', weight: 2 },
1353
+ },
1354
+ {
1355
+ name: 'successful transaction without hash',
1356
+ operation: { ...transactionPending, status: 'success', weight: 2 },
1357
+ },
1358
+ {
1359
+ name: 'signed pending key authorization',
1360
+ operation: {
1361
+ ...keyAuthorizationPending,
1362
+ keyAuthorization: KeyAuthorization.serialize(
1363
+ KeyAuthorization.from(
1364
+ KeyAuthorization.deserialize(keyAuthorization),
1365
+ {
1366
+ signature: ownerSignature_1,
1367
+ },
1368
+ ),
1369
+ ),
1370
+ },
1371
+ },
1372
+ {
1373
+ name: 'successful key authorization without quorum',
1374
+ operation: { ...keyAuthorizationPending, status: 'success' },
1375
+ },
1376
+ {
1377
+ name: 'pending key authorization with quorum',
1378
+ operation: {
1379
+ ...keyAuthorizationPending,
1380
+ approvals: [approval_1, approval_2],
1381
+ signatureCount: 2,
1382
+ weight: 2,
1383
+ },
1384
+ },
1385
+ {
1386
+ name: 'key authorization with transaction fields',
1387
+ operation: { ...keyAuthorizationPending, transaction },
1388
+ },
1389
+ {
1390
+ name: 'transaction with key authorization fields',
1391
+ operation: { ...transactionPending, keyAuthorization },
1392
+ },
1393
+ {
1394
+ name: 'selected signature without weight',
1395
+ operation: { ...transactionPending, weight: 0 },
1396
+ },
1397
+ {
1398
+ name: 'string config threshold',
1399
+ operation: {
1400
+ ...transactionPending,
1401
+ config: { ...config, threshold: '2' },
1402
+ },
1403
+ },
1404
+ {
1405
+ name: 'bigint config owner weight',
1406
+ operation: {
1407
+ ...transactionPending,
1408
+ config: {
1409
+ ...config,
1410
+ owners: config.owners.map((owner) => ({ ...owner, weight: 1n })),
1411
+ },
1412
+ },
1413
+ },
1414
+ {
1415
+ name: 'weight unreachable by the selected signature count',
1416
+ operation: { ...transactionPending, weight: 2 },
1417
+ },
1418
+ {
1419
+ name: 'weight unreachable by the retained owner approvals',
1420
+ operation: {
1421
+ ...transactionPending,
1422
+ config: MultisigConfig.from({
1423
+ owners: [
1424
+ { owner: owner_1, weight: 1 },
1425
+ { owner: owner_2, weight: 2 },
1426
+ ],
1427
+ threshold: 2,
1428
+ }),
1429
+ weight: 2,
1430
+ },
1431
+ },
1432
+ {
1433
+ name: 'non-owner approval',
1434
+ operation: { ...transactionPending, approvals: [approval_3] },
1435
+ },
1436
+ {
1437
+ name: 'zero account',
1438
+ operation: {
1439
+ ...transactionPending,
1440
+ account: '0x0000000000000000000000000000000000000000',
1441
+ hash: MultisigConfig.getSignPayload({
1442
+ account: '0x0000000000000000000000000000000000000000',
1443
+ payload: TxEnvelopeTempo.getSignPayload(
1444
+ TxEnvelopeTempo.deserialize(transaction),
1445
+ ),
1446
+ version: 1n,
1447
+ }),
1448
+ },
1449
+ },
1450
+ {
1451
+ name: 'non-hex transaction hash',
1452
+ operation: {
1453
+ ...transactionPending,
1454
+ approvals: [approval_1, approval_2],
1455
+ signatureCount: 2,
1456
+ status: 'success',
1457
+ transactionHash: `0x${'gg'.repeat(32)}`,
1458
+ weight: 2,
1459
+ },
1460
+ },
1461
+ {
1462
+ name: 'non-hex submission ID',
1463
+ operation: {
1464
+ ...transactionPending,
1465
+ approvals: [approval_1, approval_2],
1466
+ expiresAt: 10,
1467
+ signatureCount: 2,
1468
+ status: 'submitting',
1469
+ submissionId: `0x${'gg'.repeat(32)}`,
1470
+ weight: 2,
1471
+ },
1472
+ },
1473
+ {
1474
+ name: 'odd-length transaction hash',
1475
+ operation: {
1476
+ ...transactionPending,
1477
+ approvals: [approval_1, approval_2],
1478
+ signatureCount: 2,
1479
+ status: 'success',
1480
+ transactionHash: `0x${'a'.repeat(63)}`,
1481
+ weight: 2,
1482
+ },
1483
+ },
1484
+ {
1485
+ name: 'odd-length submission ID',
1486
+ operation: {
1487
+ ...transactionPending,
1488
+ approvals: [approval_1, approval_2],
1489
+ expiresAt: 10,
1490
+ signatureCount: 2,
1491
+ status: 'submitting',
1492
+ submissionId: `0x${'b'.repeat(63)}`,
1493
+ weight: 2,
1494
+ },
1495
+ },
1496
+ {
1497
+ name: 'keychain owner approval',
1498
+ operation: {
1499
+ ...transactionPending,
1500
+ approvals: [
1501
+ SignatureEnvelope.serialize({
1502
+ inner: ownerSignature_1,
1503
+ type: 'keychain',
1504
+ userAddress: owner_1,
1505
+ }),
1506
+ ],
1507
+ },
1508
+ },
1509
+ {
1510
+ name: 'nested bootstrap owner approval',
1511
+ operation: {
1512
+ ...transactionPending,
1513
+ approvals: [
1514
+ SignatureEnvelope.serialize({
1515
+ account,
1516
+ init: config,
1517
+ signatures: [ownerSignature_1],
1518
+ type: 'multisig',
1519
+ }),
1520
+ ],
1521
+ },
1522
+ },
1523
+ {
1524
+ name: 'bootstrap with initialized version',
1525
+ operation: { ...transactionPending, init: true },
1526
+ },
1527
+ ])('rejects $name', ({ operation }) => {
1528
+ expect(() =>
1529
+ MultisigOperation.from(operation as MultisigOperation.Operation),
1530
+ ).toThrowError(MultisigOperation.InvalidOperationError)
1531
+ })
1532
+
1533
+ test('rejects noncanonical RPC quantities', () => {
1534
+ expect(() =>
1535
+ MultisigOperation.fromRpc({
1536
+ ...MultisigOperation.toRpc(transactionPending),
1537
+ configVersion: '0x01',
1538
+ }),
1539
+ ).toThrowError(
1540
+ 'Invalid multisig operation: configVersion must use canonical quantity encoding.',
1541
+ )
1542
+ })
1543
+
1544
+ test('rejects key authorization signatures absent from retained approvals', () => {
1545
+ const authorization = KeyAuthorization.deserialize(keyAuthorization)
1546
+ const operation = {
1547
+ ...keyAuthorizationPending,
1548
+ approvals: [approval_1, approval_2],
1549
+ keyAuthorization: KeyAuthorization.serialize(
1550
+ KeyAuthorization.from(authorization, {
1551
+ signature: {
1552
+ account,
1553
+ signatures: [
1554
+ SignatureEnvelope.deserialize(approval_1),
1555
+ SignatureEnvelope.deserialize(approval_3),
1556
+ ],
1557
+ type: 'multisig',
1558
+ },
1559
+ }),
1560
+ ),
1561
+ signatureCount: 2,
1562
+ status: 'success',
1563
+ weight: 2,
1564
+ } as const
1565
+
1566
+ expect(() => MultisigOperation.from(operation)).toThrowError(
1567
+ 'Invalid multisig operation: key authorization signature is not a retained approval.',
1568
+ )
1569
+ })
1570
+
1571
+ test('rejects unordered key authorization approvals', () => {
1572
+ const authorization = KeyAuthorization.deserialize(keyAuthorization)
1573
+ const signatures = [
1574
+ ...SignatureEnvelope.sortMultisigApprovals({
1575
+ account,
1576
+ payload: KeyAuthorization.getSignPayload(authorization),
1577
+ signatures: [
1578
+ SignatureEnvelope.deserialize(approval_1),
1579
+ SignatureEnvelope.deserialize(approval_2),
1580
+ ],
1581
+ version: 1n,
1582
+ }),
1583
+ ].reverse()
1584
+ const operation = {
1585
+ ...keyAuthorizationPending,
1586
+ approvals: [approval_1, approval_2],
1587
+ keyAuthorization: KeyAuthorization.serialize(
1588
+ KeyAuthorization.from(authorization, {
1589
+ signature: { account, signatures, type: 'multisig' },
1590
+ }),
1591
+ ),
1592
+ signatureCount: 2,
1593
+ status: 'success',
1594
+ weight: 2,
1595
+ } as const
1596
+
1597
+ expect(() => MultisigOperation.from(operation)).toThrowError(
1598
+ 'Invalid multisig operation: key authorization approvals are not canonically ordered.',
1599
+ )
1600
+ })
1601
+
1602
+ test('rejects duplicate key authorization approvals', () => {
1603
+ const authorization = KeyAuthorization.deserialize(keyAuthorization)
1604
+ const operation = {
1605
+ ...keyAuthorizationPending,
1606
+ approvals: [approval_1, approval_1],
1607
+ keyAuthorization: KeyAuthorization.serialize(
1608
+ KeyAuthorization.from(authorization, {
1609
+ signature: {
1610
+ account,
1611
+ signatures: [
1612
+ SignatureEnvelope.deserialize(approval_1),
1613
+ SignatureEnvelope.deserialize(approval_1),
1614
+ ],
1615
+ type: 'multisig',
1616
+ },
1617
+ }),
1618
+ ),
1619
+ signatureCount: 2,
1620
+ status: 'success',
1621
+ weight: 2,
1622
+ } as const
1623
+
1624
+ expect(() => MultisigOperation.from(operation)).toThrowError(
1625
+ 'Invalid multisig operation: key authorization contains duplicate owner approvals.',
1626
+ )
1627
+ })
1628
+
1629
+ test('rejects reordered nested key authorization approvals', () => {
1630
+ const authorization = KeyAuthorization.deserialize(keyAuthorization)
1631
+ const childSignatures = SignatureEnvelope.sortMultisigApprovals({
1632
+ account: owner_1,
1633
+ payload: keyAuthorizationHash,
1634
+ signatures: [
1635
+ SignatureEnvelope.deserialize(approval_1),
1636
+ SignatureEnvelope.deserialize(approval_2),
1637
+ ],
1638
+ version: 1n,
1639
+ })
1640
+ const retainedNested = SignatureEnvelope.from({
1641
+ account: owner_1,
1642
+ signatures: childSignatures,
1643
+ type: 'multisig',
1644
+ })
1645
+ const selectedNested = SignatureEnvelope.from({
1646
+ account: owner_1,
1647
+ signatures: [...childSignatures].reverse(),
1648
+ type: 'multisig',
1649
+ })
1650
+ const selected = SignatureEnvelope.sortMultisigApprovals({
1651
+ account,
1652
+ payload: KeyAuthorization.getSignPayload(authorization),
1653
+ signatures: [selectedNested, SignatureEnvelope.deserialize(approval_2)],
1654
+ version: 1n,
1655
+ })
1656
+ const operation = {
1657
+ ...keyAuthorizationPending,
1658
+ approvals: [SignatureEnvelope.serialize(retainedNested), approval_2],
1659
+ keyAuthorization: KeyAuthorization.serialize(
1660
+ KeyAuthorization.from(authorization, {
1661
+ signature: { account, signatures: selected, type: 'multisig' },
1662
+ }),
1663
+ ),
1664
+ signatureCount: 2,
1665
+ status: 'success',
1666
+ weight: 2,
1667
+ } as const
1668
+
1669
+ expect(() => MultisigOperation.from(operation)).toThrowError(
1670
+ 'Invalid multisig operation: key authorization signature is not a retained approval.',
1671
+ )
1672
+ })
1673
+
1674
+ test('accepts case-insensitive bootstrap configs', () => {
1675
+ const config = MultisigConfig.from({
1676
+ owners: [
1677
+ {
1678
+ owner: Address.checksum(owner_1),
1679
+ weight: 1,
1680
+ },
1681
+ {
1682
+ owner: Address.checksum(owner_2),
1683
+ weight: 1,
1684
+ },
1685
+ ],
1686
+ salt: `0x${'AB'.repeat(32)}`,
1687
+ threshold: 2,
1688
+ })
1689
+ const account = MultisigConfig.getAddress(config)
1690
+ const authorization = KeyAuthorization.from({
1691
+ account,
1692
+ address: '0x3333333333333333333333333333333333333333',
1693
+ chainId: 4217n,
1694
+ expiry: 1_800_000_000,
1695
+ isAdmin: false,
1696
+ type: 'secp256k1',
1697
+ })
1698
+ const signatures = SignatureEnvelope.sortMultisigApprovals({
1699
+ account,
1700
+ payload: KeyAuthorization.getSignPayload(authorization),
1701
+ signatures: [
1702
+ SignatureEnvelope.deserialize(approval_1),
1703
+ SignatureEnvelope.deserialize(approval_2),
1704
+ ],
1705
+ version: 0n,
1706
+ })
1707
+ const approvals = signatures.map((signature) =>
1708
+ SignatureEnvelope.serialize(signature),
1709
+ )
1710
+
1711
+ const operation = MultisigOperation.from({
1712
+ account,
1713
+ approvals,
1714
+ config,
1715
+ configVersion: 0n,
1716
+ createdAt: 1,
1717
+ hash: MultisigConfig.getSignPayload({
1718
+ account,
1719
+ payload: KeyAuthorization.getSignPayload(authorization),
1720
+ version: 0n,
1721
+ }),
1722
+ init: true,
1723
+ keyAuthorization: KeyAuthorization.serialize(
1724
+ KeyAuthorization.from(authorization, {
1725
+ signature: {
1726
+ account,
1727
+ init: config,
1728
+ signatures,
1729
+ type: 'multisig',
1730
+ },
1731
+ }),
1732
+ ),
1733
+ signatureCount: 2,
1734
+ status: 'success',
1735
+ threshold: 2,
1736
+ type: 'keyAuthorization',
1737
+ updatedAt: 2,
1738
+ weight: 2,
1739
+ })
1740
+
1741
+ expect(operation.config).toStrictEqual(config)
1742
+ })
1743
+ })