ox 0.14.37 → 0.14.39

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 (43) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/_cjs/tempo/MultisigConfig.js +71 -15
  3. package/_cjs/tempo/MultisigConfig.js.map +1 -1
  4. package/_cjs/tempo/MultisigOperation.js +241 -19
  5. package/_cjs/tempo/MultisigOperation.js.map +1 -1
  6. package/_cjs/tempo/SignatureEnvelope.js +71 -108
  7. package/_cjs/tempo/SignatureEnvelope.js.map +1 -1
  8. package/_cjs/version.js +1 -1
  9. package/_esm/tempo/MultisigConfig.js +140 -71
  10. package/_esm/tempo/MultisigConfig.js.map +1 -1
  11. package/_esm/tempo/MultisigOperation.js +331 -19
  12. package/_esm/tempo/MultisigOperation.js.map +1 -1
  13. package/_esm/tempo/SignatureEnvelope.js +84 -143
  14. package/_esm/tempo/SignatureEnvelope.js.map +1 -1
  15. package/_esm/tempo/index.js +2 -2
  16. package/_esm/version.js +1 -1
  17. package/_types/tempo/KeyAuthorization.d.ts +1 -1
  18. package/_types/tempo/KeyAuthorization.d.ts.map +1 -1
  19. package/_types/tempo/MultisigConfig.d.ts +137 -73
  20. package/_types/tempo/MultisigConfig.d.ts.map +1 -1
  21. package/_types/tempo/MultisigOperation.d.ts +169 -1
  22. package/_types/tempo/MultisigOperation.d.ts.map +1 -1
  23. package/_types/tempo/SignatureEnvelope.d.ts +81 -75
  24. package/_types/tempo/SignatureEnvelope.d.ts.map +1 -1
  25. package/_types/tempo/index.d.ts +2 -2
  26. package/_types/version.d.ts +1 -1
  27. package/package.json +6 -1
  28. package/tempo/KeyAuthorization.test-d.ts +12 -0
  29. package/tempo/KeyAuthorization.test.ts +54 -0
  30. package/tempo/KeyAuthorization.ts +1 -1
  31. package/tempo/MultisigConfig.test-d/package.json +6 -0
  32. package/tempo/MultisigConfig.test-d.ts +52 -0
  33. package/tempo/MultisigConfig.test.ts +291 -263
  34. package/tempo/MultisigConfig.ts +250 -95
  35. package/tempo/MultisigOperation.test-d.ts +25 -0
  36. package/tempo/MultisigOperation.test.ts +754 -22
  37. package/tempo/MultisigOperation.ts +548 -17
  38. package/tempo/SignatureEnvelope.test-d.ts +57 -40
  39. package/tempo/SignatureEnvelope.test.ts +523 -480
  40. package/tempo/SignatureEnvelope.ts +250 -257
  41. package/tempo/index.ts +2 -2
  42. package/tempo/multisig.e2e.test.ts +113 -86
  43. package/version.ts +1 -1
@@ -1,4 +1,4 @@
1
- import { Address, P256 } from 'ox'
1
+ import { Address, Hash, P256 } from 'ox'
2
2
  import {
3
3
  KeyAuthorization,
4
4
  MultisigConfig,
@@ -10,11 +10,14 @@ import { describe, expect, test } from 'vitest'
10
10
 
11
11
  const owners = [1n, 2n, 3n]
12
12
  .map((value, index) => {
13
+ const privateKey = `0x${value.toString(16).padStart(64, '0')}` as const
13
14
  const publicKey = P256.getPublicKey({
14
- privateKey: `0x${value.toString(16).padStart(64, '0')}`,
15
+ privateKey,
15
16
  })
16
17
  return {
17
18
  address: Address.fromPublicKey(publicKey),
19
+ privateKey,
20
+ publicKey,
18
21
  signature: {
19
22
  prehash: false,
20
23
  publicKey,
@@ -33,6 +36,7 @@ const config = MultisigConfig.from({
33
36
  ],
34
37
  threshold: 2,
35
38
  })
39
+ const initializedConfig = MultisigConfig.from({ ...config, version: 1n })
36
40
  const account = MultisigConfig.getAddress(config)
37
41
  const ownerSignature_1 = owners[0]!.signature
38
42
  const approval_1 = SignatureEnvelope.serialize(ownerSignature_1)
@@ -59,23 +63,23 @@ const keyAuthorization = KeyAuthorization.serialize(
59
63
 
60
64
  const transactionHash_ = MultisigConfig.getSignPayload({
61
65
  account,
66
+ config: initializedConfig,
62
67
  payload: TxEnvelopeTempo.getSignPayload(
63
68
  TxEnvelopeTempo.deserialize(transaction),
64
69
  ),
65
- version: 1n,
66
70
  })
67
71
  const keyAuthorizationHash = MultisigConfig.getSignPayload({
68
72
  account,
73
+ config: initializedConfig,
69
74
  payload: KeyAuthorization.getSignPayload(
70
75
  KeyAuthorization.deserialize(keyAuthorization),
71
76
  ),
72
- version: 1n,
73
77
  })
74
78
 
75
79
  const base = {
76
80
  account,
77
81
  approvals: [approval_1],
78
- config,
82
+ config: initializedConfig,
79
83
  configVersion: 1n,
80
84
  createdAt: 1,
81
85
  init: false,
@@ -101,6 +105,701 @@ const keyAuthorizationPending = {
101
105
  type: 'keyAuthorization',
102
106
  } as const
103
107
 
108
+ function signApproval(
109
+ owner: (typeof owners)[number],
110
+ hash: `0x${string}`,
111
+ extraEntropy: false | `0x${string}` = false,
112
+ ) {
113
+ return SignatureEnvelope.serialize({
114
+ prehash: false,
115
+ publicKey: owner.publicKey,
116
+ signature: P256.sign({
117
+ extraEntropy,
118
+ payload: hash,
119
+ privateKey: owner.privateKey,
120
+ }),
121
+ type: 'p256',
122
+ })
123
+ }
124
+
125
+ function approvalAddresses(
126
+ approvals: readonly `0x${string}`[],
127
+ hash: `0x${string}`,
128
+ ) {
129
+ return approvals.map((approval) =>
130
+ SignatureEnvelope.extractAddress({
131
+ payload: hash,
132
+ signature: SignatureEnvelope.deserialize(approval),
133
+ }),
134
+ )
135
+ }
136
+
137
+ describe('getHash', () => {
138
+ test('transaction and key authorization', () => {
139
+ expect({
140
+ keyAuthorization: MultisigOperation.getHash({
141
+ account,
142
+ configVersion: 1n,
143
+ keyAuthorization,
144
+ type: 'keyAuthorization',
145
+ }),
146
+ transaction: MultisigOperation.getHash({
147
+ account,
148
+ configVersion: 1n,
149
+ transaction,
150
+ type: 'transaction',
151
+ }),
152
+ }).toMatchInlineSnapshot(`
153
+ {
154
+ "keyAuthorization": "0xf6995eb69c12c03d3d13b357abf7cac22b4effe52fba8ff02e5aef26161f77a0",
155
+ "transaction": "0xcdbc24a8fb192f799c5d166b13a99fb29cbb15e71a5e730988d6f8b3c5959d02",
156
+ }
157
+ `)
158
+ })
159
+ })
160
+
161
+ describe('selectApprovals', () => {
162
+ test('selects a deterministic weighted quorum', async () => {
163
+ const config = MultisigConfig.from({
164
+ owners: [
165
+ { owner: owners[0]!.address, weight: 2 },
166
+ { owner: owners[1]!.address, weight: 1 },
167
+ { owner: owners[2]!.address, weight: 1 },
168
+ ],
169
+ threshold: 3,
170
+ })
171
+ const account = MultisigConfig.getAddress(config)
172
+ const hash = MultisigOperation.getHash({
173
+ account,
174
+ configVersion: 1n,
175
+ transaction,
176
+ type: 'transaction',
177
+ })
178
+ const approvals = owners.map((owner) => signApproval(owner, hash))
179
+ const alternate = signApproval(owners[0]!, hash, `0x${'01'.repeat(32)}`)
180
+ const selection = await MultisigOperation.selectApprovals({
181
+ account,
182
+ approvals: [
183
+ approvals[2]!,
184
+ approvals[0]!,
185
+ approvals[1]!,
186
+ alternate,
187
+ approvals[0]!,
188
+ ],
189
+ config,
190
+ hash,
191
+ })
192
+ const reversed = await MultisigOperation.selectApprovals({
193
+ account,
194
+ approvals: [
195
+ approvals[0]!,
196
+ alternate,
197
+ approvals[1]!,
198
+ approvals[0]!,
199
+ approvals[2]!,
200
+ ],
201
+ config,
202
+ hash,
203
+ })
204
+
205
+ expect(reversed).toStrictEqual(selection)
206
+ expect({
207
+ ...selection,
208
+ approvals: approvalAddresses(selection.approvals, hash),
209
+ selectedApprovals: approvalAddresses(selection.selectedApprovals, hash),
210
+ }).toMatchInlineSnapshot(`
211
+ {
212
+ "approvals": [
213
+ "0x07e1ed8ea0e9601e5546b0a03aed683df3601407",
214
+ "0x288f0cd85005f34168f731a468aef268c2f9456f",
215
+ "0xd3a9f047ad43d7e2e4e7e491f1fe2e657a2651b6",
216
+ ],
217
+ "selectedApprovals": [
218
+ "0x07e1ed8ea0e9601e5546b0a03aed683df3601407",
219
+ "0x288f0cd85005f34168f731a468aef268c2f9456f",
220
+ ],
221
+ "signatureCount": 2,
222
+ "threshold": 3,
223
+ "weight": 3,
224
+ }
225
+ `)
226
+ })
227
+
228
+ test('counts a nested owner only after its quorum', async () => {
229
+ const childConfig = MultisigConfig.from({
230
+ owners: [
231
+ { owner: owners[1]!.address, weight: 1 },
232
+ { owner: owners[2]!.address, weight: 1 },
233
+ ],
234
+ threshold: 2,
235
+ })
236
+ const child = MultisigConfig.getAddress(childConfig)
237
+ const config = MultisigConfig.from({
238
+ owners: [
239
+ { owner: owners[0]!.address, weight: 1 },
240
+ { owner: child, weight: 2 },
241
+ ],
242
+ threshold: 2,
243
+ })
244
+ const account = MultisigConfig.getAddress(config)
245
+ const hash = MultisigOperation.getHash({
246
+ account,
247
+ configVersion: 1n,
248
+ transaction,
249
+ type: 'transaction',
250
+ })
251
+ const childHash = MultisigConfig.getSignPayload({
252
+ account: child,
253
+ config: { version: 2n },
254
+ payload: hash,
255
+ })
256
+ const childApprovals = [
257
+ signApproval(owners[1]!, childHash),
258
+ signApproval(owners[2]!, childHash),
259
+ ]
260
+ const rootApproval = signApproval(owners[0]!, hash)
261
+ const resolveConfig: MultisigOperation.selectApprovals.ResolveConfig = ({
262
+ account,
263
+ }) => {
264
+ if (!Address.isEqual(account, child)) throw new Error('unknown account')
265
+ return { config: childConfig, version: 2n }
266
+ }
267
+ const partial = await MultisigOperation.selectApprovals({
268
+ account,
269
+ approvals: [
270
+ rootApproval,
271
+ SignatureEnvelope.serialize({
272
+ account: child,
273
+ config: MultisigConfig.from({ ...childConfig, version: 2n }),
274
+ signatures: [SignatureEnvelope.deserialize(childApprovals[0]!)],
275
+ type: 'multisig',
276
+ }),
277
+ ],
278
+ config,
279
+ hash,
280
+ resolveConfig,
281
+ })
282
+ const complete = await MultisigOperation.selectApprovals({
283
+ account,
284
+ approvals: [
285
+ rootApproval,
286
+ SignatureEnvelope.serialize({
287
+ account: child,
288
+ config: MultisigConfig.from({ ...childConfig, version: 2n }),
289
+ signatures: childApprovals.map((approval) =>
290
+ SignatureEnvelope.deserialize(approval),
291
+ ),
292
+ type: 'multisig',
293
+ }),
294
+ ],
295
+ config,
296
+ hash,
297
+ resolveConfig,
298
+ })
299
+
300
+ expect({
301
+ complete: {
302
+ ...complete,
303
+ approvals: approvalAddresses(complete.approvals, hash),
304
+ selectedApprovals: approvalAddresses(complete.selectedApprovals, hash),
305
+ },
306
+ partial: {
307
+ ...partial,
308
+ approvals: approvalAddresses(partial.approvals, hash),
309
+ selectedApprovals: approvalAddresses(partial.selectedApprovals, hash),
310
+ },
311
+ }).toMatchInlineSnapshot(`
312
+ {
313
+ "complete": {
314
+ "approvals": [
315
+ "0x07e1ed8ea0e9601e5546b0a03aed683df3601407",
316
+ "0xe2d2c3c2fc4b17af341cc5c1a459af9606167e8a",
317
+ ],
318
+ "selectedApprovals": [
319
+ "0xe2d2c3c2fc4b17af341cc5c1a459af9606167e8a",
320
+ ],
321
+ "signatureCount": 1,
322
+ "threshold": 2,
323
+ "weight": 2,
324
+ },
325
+ "partial": {
326
+ "approvals": [
327
+ "0x07e1ed8ea0e9601e5546b0a03aed683df3601407",
328
+ "0xe2d2c3c2fc4b17af341cc5c1a459af9606167e8a",
329
+ ],
330
+ "selectedApprovals": [
331
+ "0x07e1ed8ea0e9601e5546b0a03aed683df3601407",
332
+ ],
333
+ "signatureCount": 1,
334
+ "threshold": 2,
335
+ "weight": 1,
336
+ },
337
+ }
338
+ `)
339
+ })
340
+
341
+ test('rejects invalid and non-owner approvals', async () => {
342
+ const hash = MultisigOperation.getHash({
343
+ account,
344
+ configVersion: 1n,
345
+ transaction,
346
+ type: 'transaction',
347
+ })
348
+ const invalid = signApproval(
349
+ owners[0]!,
350
+ `0x${'ff'.repeat(32)}` as `0x${string}`,
351
+ )
352
+ const nonOwner = signApproval(owners[2]!, hash)
353
+
354
+ await expect(
355
+ MultisigOperation.selectApprovals({
356
+ account,
357
+ approvals: [invalid],
358
+ config,
359
+ hash,
360
+ }),
361
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
362
+ `[MultisigOperation.InvalidApprovalError: Invalid multisig approval: signature from owner 0x07e1ed8ea0e9601e5546b0a03aed683df3601407 is invalid.]`,
363
+ )
364
+ await expect(
365
+ MultisigOperation.selectApprovals({
366
+ account,
367
+ approvals: [nonOwner],
368
+ config,
369
+ hash,
370
+ }),
371
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
372
+ `[MultisigOperation.InvalidApprovalError: Invalid multisig approval: signature is from non-owner 0xd3a9f047ad43d7e2e4e7e491f1fe2e657a2651b6.]`,
373
+ )
374
+ })
375
+
376
+ test('requires a resolver for nested owners', async () => {
377
+ const childConfig = MultisigConfig.from({
378
+ owners: [{ owner: owners[1]!.address, weight: 1 }],
379
+ threshold: 1,
380
+ })
381
+ const child = MultisigConfig.getAddress(childConfig)
382
+ const config = MultisigConfig.from({
383
+ owners: [{ owner: child, weight: 1 }],
384
+ threshold: 1,
385
+ })
386
+ const account = MultisigConfig.getAddress(config)
387
+ const hash = MultisigOperation.getHash({
388
+ account,
389
+ configVersion: 1n,
390
+ transaction,
391
+ type: 'transaction',
392
+ })
393
+ const childHash = MultisigConfig.getSignPayload({
394
+ account: child,
395
+ config: { version: 1n },
396
+ payload: hash,
397
+ })
398
+
399
+ await expect(
400
+ MultisigOperation.selectApprovals({
401
+ account,
402
+ approvals: [
403
+ SignatureEnvelope.serialize({
404
+ account: child,
405
+ config: MultisigConfig.from({ ...childConfig, version: 1n }),
406
+ signatures: [
407
+ SignatureEnvelope.deserialize(
408
+ signApproval(owners[1]!, childHash),
409
+ ),
410
+ ],
411
+ type: 'multisig',
412
+ }),
413
+ ],
414
+ config,
415
+ hash,
416
+ }),
417
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
418
+ `[MultisigOperation.InvalidApprovalError: Invalid multisig approval: nested multisig owner 0x7888d60e9cc26c8569d394fce435c248f1e49c3b requires a config resolver.]`,
419
+ )
420
+ })
421
+
422
+ test('rejects keychain and accepts initial nested approvals', async () => {
423
+ const childConfig = MultisigConfig.from({
424
+ owners: [{ owner: owners[1]!.address, weight: 1 }],
425
+ threshold: 1,
426
+ })
427
+ const child = MultisigConfig.getAddress(childConfig)
428
+ const config = MultisigConfig.from({
429
+ owners: [
430
+ { owner: owners[0]!.address, weight: 1 },
431
+ { owner: child, weight: 1 },
432
+ ],
433
+ threshold: 1,
434
+ })
435
+ const account = MultisigConfig.getAddress(config)
436
+ const hash = MultisigOperation.getHash({
437
+ account,
438
+ configVersion: 1n,
439
+ transaction,
440
+ type: 'transaction',
441
+ })
442
+ const childHash = MultisigConfig.getSignPayload({
443
+ account: child,
444
+ config: childConfig,
445
+ payload: hash,
446
+ })
447
+
448
+ await expect(
449
+ MultisigOperation.selectApprovals({
450
+ account,
451
+ approvals: [
452
+ SignatureEnvelope.serialize({
453
+ inner: SignatureEnvelope.deserialize(
454
+ signApproval(owners[0]!, hash),
455
+ ),
456
+ type: 'keychain',
457
+ userAddress: owners[0]!.address,
458
+ }),
459
+ ],
460
+ config,
461
+ hash,
462
+ }),
463
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
464
+ `[MultisigOperation.InvalidApprovalError: Invalid multisig approval: keychain signatures cannot approve a multisig operation.]`,
465
+ )
466
+ const selection = await MultisigOperation.selectApprovals({
467
+ account,
468
+ approvals: [
469
+ SignatureEnvelope.serialize({
470
+ account: child,
471
+ config: childConfig,
472
+ signatures: [
473
+ SignatureEnvelope.deserialize(signApproval(owners[1]!, childHash)),
474
+ ],
475
+ type: 'multisig',
476
+ }),
477
+ ],
478
+ config,
479
+ hash,
480
+ resolveConfig: () => ({ config: childConfig, version: 0n }),
481
+ })
482
+ expect({
483
+ signatureCount: selection.signatureCount,
484
+ threshold: selection.threshold,
485
+ weight: selection.weight,
486
+ }).toMatchInlineSnapshot(`
487
+ {
488
+ "signatureCount": 1,
489
+ "threshold": 1,
490
+ "weight": 1,
491
+ }
492
+ `)
493
+ })
494
+
495
+ test('rejects nested account cycles while serializing', () => {
496
+ const config = MultisigConfig.from({
497
+ owners: [{ owner: account, weight: 1 }],
498
+ threshold: 1,
499
+ })
500
+ const hash = MultisigOperation.getHash({
501
+ account,
502
+ configVersion: 2n,
503
+ transaction,
504
+ type: 'transaction',
505
+ })
506
+ const nestedHash = MultisigConfig.getSignPayload({
507
+ account,
508
+ config: { version: 2n },
509
+ payload: hash,
510
+ })
511
+
512
+ expect(() =>
513
+ SignatureEnvelope.serialize({
514
+ account,
515
+ config: MultisigConfig.from({ ...config, version: 2n }),
516
+ signatures: [
517
+ SignatureEnvelope.deserialize(signApproval(owners[0]!, nestedHash)),
518
+ ],
519
+ type: 'multisig',
520
+ }),
521
+ ).toThrowErrorMatchingInlineSnapshot(
522
+ `[SignatureEnvelope.InvalidMultisigApprovalError: Invalid native multisig owner approval: multisig account cannot be an owner.]`,
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
+ config: { version: 1n },
551
+ payload: hash,
552
+ })
553
+ const grandchildHash = MultisigConfig.getSignPayload({
554
+ account: grandchild,
555
+ config: { version: 1n },
556
+ payload: childHash,
557
+ })
558
+
559
+ await expect(
560
+ MultisigOperation.selectApprovals({
561
+ account,
562
+ approvals: [
563
+ SignatureEnvelope.serialize({
564
+ account: child,
565
+ config: MultisigConfig.from({ ...childConfig, version: 1n }),
566
+ signatures: [
567
+ {
568
+ account: grandchild,
569
+ config: MultisigConfig.from({
570
+ ...grandchildConfig,
571
+ version: 1n,
572
+ }),
573
+ signatures: [
574
+ SignatureEnvelope.deserialize(
575
+ signApproval(owners[2]!, grandchildHash),
576
+ ),
577
+ ],
578
+ type: 'multisig',
579
+ },
580
+ ],
581
+ type: 'multisig',
582
+ }),
583
+ ],
584
+ config,
585
+ hash,
586
+ resolveConfig: ({ account }) => {
587
+ if (Address.isEqual(account, child))
588
+ return { config: childConfig, version: 1n }
589
+ return { config: grandchildConfig, version: 1n }
590
+ },
591
+ }),
592
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
593
+ `[MultisigOperation.InvalidApprovalError: Invalid multisig approval: nested multisig owner 0xf529c8f6b2b4c72102af4cfe5eeb55b7d45dede2 is invalid.]`,
594
+ )
595
+ })
596
+ })
597
+
598
+ describe('serializeTransaction', () => {
599
+ test('initialized and bootstrap transactions', async () => {
600
+ const results = []
601
+ for (const init of [false, true]) {
602
+ const configVersion = init ? 0n : 1n
603
+ const hash = MultisigOperation.getHash({
604
+ account,
605
+ configVersion,
606
+ transaction,
607
+ type: 'transaction',
608
+ })
609
+ const selection = await MultisigOperation.selectApprovals({
610
+ account,
611
+ approvals: [
612
+ signApproval(owners[1]!, hash),
613
+ signApproval(owners[0]!, hash),
614
+ ],
615
+ config,
616
+ hash,
617
+ })
618
+ const operation = MultisigOperation.from({
619
+ account,
620
+ approvals: selection.approvals,
621
+ config,
622
+ configVersion,
623
+ createdAt: 1,
624
+ hash,
625
+ init,
626
+ signatureCount: selection.signatureCount,
627
+ status: 'pending',
628
+ threshold: selection.threshold,
629
+ transaction,
630
+ type: 'transaction',
631
+ updatedAt: 1,
632
+ weight: selection.weight,
633
+ })
634
+ const serialized = MultisigOperation.serializeTransaction(operation, {
635
+ approvals: selection.selectedApprovals,
636
+ })
637
+ const value = TxEnvelopeTempo.deserialize(serialized)
638
+ results.push({
639
+ account: value.signature?.account,
640
+ configVersion:
641
+ value.signature?.type === 'multisig'
642
+ ? value.signature.config.version
643
+ : undefined,
644
+ hash: Hash.keccak256(serialized),
645
+ signatureCount:
646
+ value.signature?.type === 'multisig'
647
+ ? value.signature.signatures.length
648
+ : 0,
649
+ type: serialized.slice(0, 4),
650
+ })
651
+ }
652
+
653
+ expect(results).toMatchInlineSnapshot(`
654
+ [
655
+ {
656
+ "account": "0xf81b7763d3a6876195d780865bd783dbd97dd36e",
657
+ "configVersion": 1n,
658
+ "hash": "0x8309740edaf304284186f7bcfe4527745cd4eb48e7441795ebdd970798091f8f",
659
+ "signatureCount": 2,
660
+ "type": "0x76",
661
+ },
662
+ {
663
+ "account": "0xf81b7763d3a6876195d780865bd783dbd97dd36e",
664
+ "configVersion": 0n,
665
+ "hash": "0x636af84d8445d87cbcd33039d5a3a2274a5911036b1b7ceb740ac41e2f638611",
666
+ "signatureCount": 2,
667
+ "type": "0x76",
668
+ },
669
+ ]
670
+ `)
671
+ })
672
+
673
+ test('fee-payer transactions', async () => {
674
+ const envelope = TxEnvelopeTempo.from({
675
+ calls: [{ data: '0x1234', to: owner_1 }],
676
+ chainId: 4217,
677
+ })
678
+ const transactions = [
679
+ TxEnvelopeTempo.serialize(envelope, {
680
+ format: 'feePayer',
681
+ sender: account,
682
+ }),
683
+ TxEnvelopeTempo.serialize(
684
+ {
685
+ ...envelope,
686
+ feePayerSignature: { r: 5n, s: 6n, yParity: 0 },
687
+ },
688
+ { format: 'feePayer' },
689
+ ),
690
+ ] as const
691
+ const results = []
692
+ for (const transaction of transactions) {
693
+ const hash = MultisigOperation.getHash({
694
+ account,
695
+ configVersion: 1n,
696
+ transaction,
697
+ type: 'transaction',
698
+ })
699
+ const selection = await MultisigOperation.selectApprovals({
700
+ account,
701
+ approvals: [
702
+ signApproval(owners[0]!, hash),
703
+ signApproval(owners[1]!, hash),
704
+ ],
705
+ config,
706
+ hash,
707
+ })
708
+ const serialized = MultisigOperation.serializeTransaction(
709
+ MultisigOperation.from({
710
+ account,
711
+ approvals: selection.approvals,
712
+ config,
713
+ configVersion: 1n,
714
+ createdAt: 1,
715
+ hash,
716
+ init: false,
717
+ signatureCount: selection.signatureCount,
718
+ status: 'pending',
719
+ threshold: selection.threshold,
720
+ transaction,
721
+ type: 'transaction',
722
+ updatedAt: 1,
723
+ weight: selection.weight,
724
+ }),
725
+ { approvals: selection.selectedApprovals },
726
+ )
727
+ const value = TxEnvelopeTempo.deserialize(serialized)
728
+ results.push({
729
+ feePayerSignature: value.feePayerSignature,
730
+ from: value.from,
731
+ signatureCount:
732
+ value.signature?.type === 'multisig'
733
+ ? value.signature.signatures.length
734
+ : 0,
735
+ type: serialized.slice(0, 4),
736
+ })
737
+ }
738
+
739
+ expect(results).toMatchInlineSnapshot(`
740
+ [
741
+ {
742
+ "feePayerSignature": null,
743
+ "from": "0xf81b7763d3a6876195d780865bd783dbd97dd36e",
744
+ "signatureCount": 2,
745
+ "type": "0x78",
746
+ },
747
+ {
748
+ "feePayerSignature": {
749
+ "r": 5n,
750
+ "s": 6n,
751
+ "yParity": 0,
752
+ },
753
+ "from": "0xf81b7763d3a6876195d780865bd783dbd97dd36e",
754
+ "signatureCount": 2,
755
+ "type": "0x78",
756
+ },
757
+ ]
758
+ `)
759
+ })
760
+
761
+ test('rejects an approval not retained by the operation', async () => {
762
+ const hash = MultisigOperation.getHash({
763
+ account,
764
+ configVersion: 1n,
765
+ transaction,
766
+ type: 'transaction',
767
+ })
768
+ const approval_1 = signApproval(owners[0]!, hash)
769
+ const approval_2 = signApproval(owners[1]!, hash)
770
+ const selection = await MultisigOperation.selectApprovals({
771
+ account,
772
+ approvals: [approval_1],
773
+ config,
774
+ hash,
775
+ })
776
+ const operation = MultisigOperation.from({
777
+ account,
778
+ approvals: selection.approvals,
779
+ config,
780
+ configVersion: 1n,
781
+ createdAt: 1,
782
+ hash,
783
+ init: false,
784
+ signatureCount: selection.signatureCount,
785
+ status: 'pending',
786
+ threshold: selection.threshold,
787
+ transaction,
788
+ type: 'transaction',
789
+ updatedAt: 1,
790
+ weight: selection.weight,
791
+ })
792
+
793
+ expect(() =>
794
+ MultisigOperation.serializeTransaction(operation, {
795
+ approvals: [approval_2],
796
+ }),
797
+ ).toThrowErrorMatchingInlineSnapshot(
798
+ `[MultisigOperation.InvalidOperationError: Invalid multisig operation: transaction signature is not a retained approval.]`,
799
+ )
800
+ })
801
+ })
802
+
104
803
  describe('from', () => {
105
804
  test('transaction states', () => {
106
805
  const pending = MultisigOperation.from(transactionPending)
@@ -142,6 +841,7 @@ describe('from', () => {
142
841
  ],
143
842
  "salt": "0x0000000000000000000000000000000000000000000000000000000000000000",
144
843
  "threshold": 2,
844
+ "version": 1n,
145
845
  },
146
846
  "configVersion": 1n,
147
847
  "createdAt": 1,
@@ -174,6 +874,7 @@ describe('from', () => {
174
874
  ],
175
875
  "salt": "0x0000000000000000000000000000000000000000000000000000000000000000",
176
876
  "threshold": 2,
877
+ "version": 1n,
177
878
  },
178
879
  "configVersion": 1n,
179
880
  "createdAt": 1,
@@ -208,6 +909,7 @@ describe('from', () => {
208
909
  ],
209
910
  "salt": "0x0000000000000000000000000000000000000000000000000000000000000000",
210
911
  "threshold": 2,
912
+ "version": 1n,
211
913
  },
212
914
  "configVersion": 1n,
213
915
  "createdAt": 1,
@@ -238,10 +940,10 @@ describe('from', () => {
238
940
  ...transactionPending,
239
941
  hash: MultisigConfig.getSignPayload({
240
942
  account,
943
+ config: initializedConfig,
241
944
  payload: TxEnvelopeTempo.getSignPayload(
242
945
  TxEnvelopeTempo.deserialize(transaction),
243
946
  ),
244
- version: 1n,
245
947
  }),
246
948
  transaction,
247
949
  })
@@ -270,6 +972,7 @@ describe('from', () => {
270
972
  ],
271
973
  "salt": "0x0000000000000000000000000000000000000000000000000000000000000000",
272
974
  "threshold": 2,
975
+ "version": 1n,
273
976
  },
274
977
  "configVersion": 1n,
275
978
  "createdAt": 1,
@@ -300,10 +1003,10 @@ describe('from', () => {
300
1003
  ...transactionPending,
301
1004
  hash: MultisigConfig.getSignPayload({
302
1005
  account,
1006
+ config: initializedConfig,
303
1007
  payload: TxEnvelopeTempo.getSignPayload(
304
1008
  TxEnvelopeTempo.deserialize(transaction),
305
1009
  ),
306
- version: 1n,
307
1010
  }),
308
1011
  transaction,
309
1012
  })
@@ -317,10 +1020,10 @@ describe('from', () => {
317
1020
  configVersion: 0n,
318
1021
  hash: MultisigConfig.getSignPayload({
319
1022
  account,
1023
+ config,
320
1024
  payload: TxEnvelopeTempo.getSignPayload(
321
1025
  TxEnvelopeTempo.deserialize(transaction),
322
1026
  ),
323
- version: 0n,
324
1027
  }),
325
1028
  init: true,
326
1029
  })
@@ -349,6 +1052,7 @@ describe('from', () => {
349
1052
  ],
350
1053
  "salt": "0x0000000000000000000000000000000000000000000000000000000000000000",
351
1054
  "threshold": 2,
1055
+ "version": 0n,
352
1056
  },
353
1057
  "configVersion": 0n,
354
1058
  "createdAt": 1,
@@ -372,10 +1076,10 @@ describe('from', () => {
372
1076
  configVersion: 0n,
373
1077
  hash: MultisigConfig.getSignPayload({
374
1078
  account,
1079
+ config,
375
1080
  payload: TxEnvelopeTempo.getSignPayload(
376
1081
  TxEnvelopeTempo.deserialize(transaction),
377
1082
  ),
378
- version: 0n,
379
1083
  }),
380
1084
  })
381
1085
 
@@ -400,6 +1104,7 @@ describe('from', () => {
400
1104
  KeyAuthorization.from(authorization, {
401
1105
  signature: {
402
1106
  account,
1107
+ config: initializedConfig,
403
1108
  signatures: [
404
1109
  SignatureEnvelope.deserialize(approval_1),
405
1110
  SignatureEnvelope.deserialize(approval_2),
@@ -433,6 +1138,7 @@ describe('from', () => {
433
1138
  ],
434
1139
  "salt": "0x0000000000000000000000000000000000000000000000000000000000000000",
435
1140
  "threshold": 2,
1141
+ "version": 1n,
436
1142
  },
437
1143
  "configVersion": 1n,
438
1144
  "createdAt": 1,
@@ -465,12 +1171,13 @@ describe('from', () => {
465
1171
  ],
466
1172
  "salt": "0x0000000000000000000000000000000000000000000000000000000000000000",
467
1173
  "threshold": 2,
1174
+ "version": 1n,
468
1175
  },
469
1176
  "configVersion": 1n,
470
1177
  "createdAt": 1,
471
1178
  "hash": "0xf6995eb69c12c03d3d13b357abf7cac22b4effe52fba8ff02e5aef26161f77a0",
472
1179
  "init": false,
473
- "keyAuthorization": "0xf9015ff782107980943333333333333333333333333333333333333333846b49d2008080808094f81b7763d3a6876195d780865bd783dbd97dd36eb9012405f9012094f81b7763d3a6876195d780865bd783dbd97dd36ef90108b88201000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000065ecbe4d1a6330a44c8f7ef951d4bf165e6c6b721efada985fb41661bc6e7fd6c8734640c4998ff7e374b06ce1a64a2ecd82ab036384fb83d9a79b127a27d503200b88201000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000047cf27b188d034f7e8a52380304b51ac3c08969e277f21b35a60b48fc4766997807775510db8ed040293d9ac69f7430dbba7dade63ce982299e04b79d227873d100",
1180
+ "keyAuthorization": "0xf901b3f782107980943333333333333333333333333333333333333333846b49d2008080808094f81b7763d3a6876195d780865bd783dbd97dd36eb9017805f9017494f81b7763d3a6876195d780865bd783dbd97dd36ef852a000000000000000000000000000000000000000000000000000000000000000000102eed69407e1ed8ea0e9601e5546b0a03aed683df360140701d694288f0cd85005f34168f731a468aef268c2f9456f01f90108b88201000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000065ecbe4d1a6330a44c8f7ef951d4bf165e6c6b721efada985fb41661bc6e7fd6c8734640c4998ff7e374b06ce1a64a2ecd82ab036384fb83d9a79b127a27d503200b88201000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000047cf27b188d034f7e8a52380304b51ac3c08969e277f21b35a60b48fc4766997807775510db8ed040293d9ac69f7430dbba7dade63ce982299e04b79d227873d100",
474
1181
  "signatureCount": 2,
475
1182
  "status": "success",
476
1183
  "threshold": 2,
@@ -488,7 +1195,7 @@ describe('from', () => {
488
1195
  KeyAuthorization.from(authorization, {
489
1196
  signature: {
490
1197
  account,
491
- init: config,
1198
+ config,
492
1199
  signatures: [
493
1200
  SignatureEnvelope.deserialize(approval_1),
494
1201
  SignatureEnvelope.deserialize(approval_2),
@@ -503,8 +1210,8 @@ describe('from', () => {
503
1210
  configVersion: 0n,
504
1211
  hash: MultisigConfig.getSignPayload({
505
1212
  account,
1213
+ config,
506
1214
  payload: KeyAuthorization.getSignPayload(authorization),
507
- version: 0n,
508
1215
  }),
509
1216
  init: true,
510
1217
  keyAuthorization: keyAuthorization_,
@@ -538,6 +1245,7 @@ describe('from', () => {
538
1245
  ],
539
1246
  "salt": "0x0000000000000000000000000000000000000000000000000000000000000000",
540
1247
  "threshold": 2,
1248
+ "version": 0n,
541
1249
  },
542
1250
  "configVersion": 0n,
543
1251
  "createdAt": 1,
@@ -561,6 +1269,9 @@ describe('RPC conversion', () => {
561
1269
  const transactionRpc = MultisigOperation.toRpc(transactionPending)
562
1270
  const keyAuthorizationRpc = MultisigOperation.toRpc(keyAuthorizationPending)
563
1271
 
1272
+ expect(() =>
1273
+ JSON.stringify({ keyAuthorizationRpc, transactionRpc }),
1274
+ ).not.toThrow()
564
1275
  expect({ keyAuthorizationRpc, transactionRpc }).toMatchInlineSnapshot(`
565
1276
  {
566
1277
  "keyAuthorizationRpc": {
@@ -581,6 +1292,7 @@ describe('RPC conversion', () => {
581
1292
  ],
582
1293
  "salt": "0x0000000000000000000000000000000000000000000000000000000000000000",
583
1294
  "threshold": 2,
1295
+ "version": "0x1",
584
1296
  },
585
1297
  "configVersion": "0x1",
586
1298
  "createdAt": 1,
@@ -612,6 +1324,7 @@ describe('RPC conversion', () => {
612
1324
  ],
613
1325
  "salt": "0x0000000000000000000000000000000000000000000000000000000000000000",
614
1326
  "threshold": 2,
1327
+ "version": "0x1",
615
1328
  },
616
1329
  "configVersion": "0x1",
617
1330
  "createdAt": 1,
@@ -749,10 +1462,10 @@ describe('validation', () => {
749
1462
  account: '0x0000000000000000000000000000000000000000',
750
1463
  hash: MultisigConfig.getSignPayload({
751
1464
  account: '0x0000000000000000000000000000000000000000',
1465
+ config: initializedConfig,
752
1466
  payload: TxEnvelopeTempo.getSignPayload(
753
1467
  TxEnvelopeTempo.deserialize(transaction),
754
1468
  ),
755
- version: 1n,
756
1469
  }),
757
1470
  },
758
1471
  },
@@ -822,7 +1535,7 @@ describe('validation', () => {
822
1535
  approvals: [
823
1536
  SignatureEnvelope.serialize({
824
1537
  account,
825
- init: config,
1538
+ config,
826
1539
  signatures: [ownerSignature_1],
827
1540
  type: 'multisig',
828
1541
  }),
@@ -859,6 +1572,7 @@ describe('validation', () => {
859
1572
  KeyAuthorization.from(authorization, {
860
1573
  signature: {
861
1574
  account,
1575
+ config: initializedConfig,
862
1576
  signatures: [
863
1577
  SignatureEnvelope.deserialize(approval_1),
864
1578
  SignatureEnvelope.deserialize(approval_3),
@@ -882,12 +1596,12 @@ describe('validation', () => {
882
1596
  const signatures = [
883
1597
  ...SignatureEnvelope.sortMultisigApprovals({
884
1598
  account,
1599
+ config: initializedConfig,
885
1600
  payload: KeyAuthorization.getSignPayload(authorization),
886
1601
  signatures: [
887
1602
  SignatureEnvelope.deserialize(approval_1),
888
1603
  SignatureEnvelope.deserialize(approval_2),
889
1604
  ],
890
- version: 1n,
891
1605
  }),
892
1606
  ].reverse()
893
1607
  const operation = {
@@ -895,7 +1609,12 @@ describe('validation', () => {
895
1609
  approvals: [approval_1, approval_2],
896
1610
  keyAuthorization: KeyAuthorization.serialize(
897
1611
  KeyAuthorization.from(authorization, {
898
- signature: { account, signatures, type: 'multisig' },
1612
+ signature: {
1613
+ account,
1614
+ config: initializedConfig,
1615
+ signatures,
1616
+ type: 'multisig',
1617
+ },
899
1618
  }),
900
1619
  ),
901
1620
  signatureCount: 2,
@@ -917,6 +1636,7 @@ describe('validation', () => {
917
1636
  KeyAuthorization.from(authorization, {
918
1637
  signature: {
919
1638
  account,
1639
+ config: initializedConfig,
920
1640
  signatures: [
921
1641
  SignatureEnvelope.deserialize(approval_1),
922
1642
  SignatureEnvelope.deserialize(approval_1),
@@ -937,37 +1657,49 @@ describe('validation', () => {
937
1657
 
938
1658
  test('rejects reordered nested key authorization approvals', () => {
939
1659
  const authorization = KeyAuthorization.deserialize(keyAuthorization)
1660
+ const nestedConfig = MultisigConfig.from({
1661
+ owners: [{ owner: owner_2, weight: 1 }],
1662
+ threshold: 1,
1663
+ version: 1n,
1664
+ })
940
1665
  const childSignatures = SignatureEnvelope.sortMultisigApprovals({
941
1666
  account: owner_1,
1667
+ config: nestedConfig,
942
1668
  payload: keyAuthorizationHash,
943
1669
  signatures: [
944
1670
  SignatureEnvelope.deserialize(approval_1),
945
1671
  SignatureEnvelope.deserialize(approval_2),
946
1672
  ],
947
- version: 1n,
948
1673
  })
949
1674
  const retainedNested = SignatureEnvelope.from({
950
1675
  account: owner_1,
1676
+ config: nestedConfig,
951
1677
  signatures: childSignatures,
952
1678
  type: 'multisig',
953
1679
  })
954
1680
  const selectedNested = SignatureEnvelope.from({
955
1681
  account: owner_1,
1682
+ config: nestedConfig,
956
1683
  signatures: [...childSignatures].reverse(),
957
1684
  type: 'multisig',
958
1685
  })
959
1686
  const selected = SignatureEnvelope.sortMultisigApprovals({
960
1687
  account,
1688
+ config: initializedConfig,
961
1689
  payload: KeyAuthorization.getSignPayload(authorization),
962
1690
  signatures: [selectedNested, SignatureEnvelope.deserialize(approval_2)],
963
- version: 1n,
964
1691
  })
965
1692
  const operation = {
966
1693
  ...keyAuthorizationPending,
967
1694
  approvals: [SignatureEnvelope.serialize(retainedNested), approval_2],
968
1695
  keyAuthorization: KeyAuthorization.serialize(
969
1696
  KeyAuthorization.from(authorization, {
970
- signature: { account, signatures: selected, type: 'multisig' },
1697
+ signature: {
1698
+ account,
1699
+ config: initializedConfig,
1700
+ signatures: selected,
1701
+ type: 'multisig',
1702
+ },
971
1703
  }),
972
1704
  ),
973
1705
  signatureCount: 2,
@@ -1006,12 +1738,12 @@ describe('validation', () => {
1006
1738
  })
1007
1739
  const signatures = SignatureEnvelope.sortMultisigApprovals({
1008
1740
  account,
1741
+ config,
1009
1742
  payload: KeyAuthorization.getSignPayload(authorization),
1010
1743
  signatures: [
1011
1744
  SignatureEnvelope.deserialize(approval_1),
1012
1745
  SignatureEnvelope.deserialize(approval_2),
1013
1746
  ],
1014
- version: 0n,
1015
1747
  })
1016
1748
  const approvals = signatures.map((signature) =>
1017
1749
  SignatureEnvelope.serialize(signature),
@@ -1025,15 +1757,15 @@ describe('validation', () => {
1025
1757
  createdAt: 1,
1026
1758
  hash: MultisigConfig.getSignPayload({
1027
1759
  account,
1760
+ config,
1028
1761
  payload: KeyAuthorization.getSignPayload(authorization),
1029
- version: 0n,
1030
1762
  }),
1031
1763
  init: true,
1032
1764
  keyAuthorization: KeyAuthorization.serialize(
1033
1765
  KeyAuthorization.from(authorization, {
1034
1766
  signature: {
1035
1767
  account,
1036
- init: config,
1768
+ config,
1037
1769
  signatures,
1038
1770
  type: 'multisig',
1039
1771
  },