ox 1.7.1 → 1.7.4

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