ox 1.7.1 → 1.7.2

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