ox 0.14.37 → 0.14.38
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/_cjs/tempo/MultisigOperation.js +217 -1
- package/_cjs/tempo/MultisigOperation.js.map +1 -1
- package/_cjs/version.js +1 -1
- package/_esm/tempo/MultisigOperation.js +307 -0
- package/_esm/tempo/MultisigOperation.js.map +1 -1
- package/_esm/tempo/index.js +2 -2
- package/_esm/version.js +1 -1
- package/_types/tempo/MultisigOperation.d.ts +168 -0
- package/_types/tempo/MultisigOperation.d.ts.map +1 -1
- package/_types/tempo/index.d.ts +2 -2
- package/_types/version.d.ts +1 -1
- package/package.json +1 -1
- package/tempo/MultisigOperation.test-d.ts +19 -0
- package/tempo/MultisigOperation.test.ts +693 -2
- package/tempo/MultisigOperation.ts +525 -0
- package/tempo/index.ts +2 -2
- package/version.ts +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Address, P256 } from 'ox'
|
|
1
|
+
import { Address, Hash, P256 } from 'ox'
|
|
2
2
|
import {
|
|
3
3
|
KeyAuthorization,
|
|
4
4
|
MultisigConfig,
|
|
@@ -10,11 +10,14 @@ import { describe, expect, test } from 'vitest'
|
|
|
10
10
|
|
|
11
11
|
const owners = [1n, 2n, 3n]
|
|
12
12
|
.map((value, index) => {
|
|
13
|
+
const privateKey = `0x${value.toString(16).padStart(64, '0')}` as const
|
|
13
14
|
const publicKey = P256.getPublicKey({
|
|
14
|
-
privateKey
|
|
15
|
+
privateKey,
|
|
15
16
|
})
|
|
16
17
|
return {
|
|
17
18
|
address: Address.fromPublicKey(publicKey),
|
|
19
|
+
privateKey,
|
|
20
|
+
publicKey,
|
|
18
21
|
signature: {
|
|
19
22
|
prehash: false,
|
|
20
23
|
publicKey,
|
|
@@ -101,6 +104,694 @@ const keyAuthorizationPending = {
|
|
|
101
104
|
type: 'keyAuthorization',
|
|
102
105
|
} as const
|
|
103
106
|
|
|
107
|
+
function signApproval(
|
|
108
|
+
owner: (typeof owners)[number],
|
|
109
|
+
hash: `0x${string}`,
|
|
110
|
+
extraEntropy: false | `0x${string}` = false,
|
|
111
|
+
) {
|
|
112
|
+
return SignatureEnvelope.serialize({
|
|
113
|
+
prehash: false,
|
|
114
|
+
publicKey: owner.publicKey,
|
|
115
|
+
signature: P256.sign({
|
|
116
|
+
extraEntropy,
|
|
117
|
+
payload: hash,
|
|
118
|
+
privateKey: owner.privateKey,
|
|
119
|
+
}),
|
|
120
|
+
type: 'p256',
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function approvalAddresses(
|
|
125
|
+
approvals: readonly `0x${string}`[],
|
|
126
|
+
hash: `0x${string}`,
|
|
127
|
+
) {
|
|
128
|
+
return approvals.map((approval) =>
|
|
129
|
+
SignatureEnvelope.extractAddress({
|
|
130
|
+
payload: hash,
|
|
131
|
+
signature: SignatureEnvelope.deserialize(approval),
|
|
132
|
+
}),
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
describe('getHash', () => {
|
|
137
|
+
test('transaction and key authorization', () => {
|
|
138
|
+
expect({
|
|
139
|
+
keyAuthorization: MultisigOperation.getHash({
|
|
140
|
+
account,
|
|
141
|
+
configVersion: 1n,
|
|
142
|
+
keyAuthorization,
|
|
143
|
+
type: 'keyAuthorization',
|
|
144
|
+
}),
|
|
145
|
+
transaction: MultisigOperation.getHash({
|
|
146
|
+
account,
|
|
147
|
+
configVersion: 1n,
|
|
148
|
+
transaction,
|
|
149
|
+
type: 'transaction',
|
|
150
|
+
}),
|
|
151
|
+
}).toMatchInlineSnapshot(`
|
|
152
|
+
{
|
|
153
|
+
"keyAuthorization": "0xf6995eb69c12c03d3d13b357abf7cac22b4effe52fba8ff02e5aef26161f77a0",
|
|
154
|
+
"transaction": "0xcdbc24a8fb192f799c5d166b13a99fb29cbb15e71a5e730988d6f8b3c5959d02",
|
|
155
|
+
}
|
|
156
|
+
`)
|
|
157
|
+
})
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
describe('selectApprovals', () => {
|
|
161
|
+
test('selects a deterministic weighted quorum', async () => {
|
|
162
|
+
const config = MultisigConfig.from({
|
|
163
|
+
owners: [
|
|
164
|
+
{ owner: owners[0]!.address, weight: 2 },
|
|
165
|
+
{ owner: owners[1]!.address, weight: 1 },
|
|
166
|
+
{ owner: owners[2]!.address, weight: 1 },
|
|
167
|
+
],
|
|
168
|
+
threshold: 3,
|
|
169
|
+
})
|
|
170
|
+
const account = MultisigConfig.getAddress(config)
|
|
171
|
+
const hash = MultisigOperation.getHash({
|
|
172
|
+
account,
|
|
173
|
+
configVersion: 1n,
|
|
174
|
+
transaction,
|
|
175
|
+
type: 'transaction',
|
|
176
|
+
})
|
|
177
|
+
const approvals = owners.map((owner) => signApproval(owner, hash))
|
|
178
|
+
const alternate = signApproval(owners[0]!, hash, `0x${'01'.repeat(32)}`)
|
|
179
|
+
const selection = await MultisigOperation.selectApprovals({
|
|
180
|
+
account,
|
|
181
|
+
approvals: [
|
|
182
|
+
approvals[2]!,
|
|
183
|
+
approvals[0]!,
|
|
184
|
+
approvals[1]!,
|
|
185
|
+
alternate,
|
|
186
|
+
approvals[0]!,
|
|
187
|
+
],
|
|
188
|
+
config,
|
|
189
|
+
hash,
|
|
190
|
+
})
|
|
191
|
+
const reversed = await MultisigOperation.selectApprovals({
|
|
192
|
+
account,
|
|
193
|
+
approvals: [
|
|
194
|
+
approvals[0]!,
|
|
195
|
+
alternate,
|
|
196
|
+
approvals[1]!,
|
|
197
|
+
approvals[0]!,
|
|
198
|
+
approvals[2]!,
|
|
199
|
+
],
|
|
200
|
+
config,
|
|
201
|
+
hash,
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
expect(reversed).toStrictEqual(selection)
|
|
205
|
+
expect({
|
|
206
|
+
...selection,
|
|
207
|
+
approvals: approvalAddresses(selection.approvals, hash),
|
|
208
|
+
selectedApprovals: approvalAddresses(selection.selectedApprovals, hash),
|
|
209
|
+
}).toMatchInlineSnapshot(`
|
|
210
|
+
{
|
|
211
|
+
"approvals": [
|
|
212
|
+
"0x07e1ed8ea0e9601e5546b0a03aed683df3601407",
|
|
213
|
+
"0x288f0cd85005f34168f731a468aef268c2f9456f",
|
|
214
|
+
"0xd3a9f047ad43d7e2e4e7e491f1fe2e657a2651b6",
|
|
215
|
+
],
|
|
216
|
+
"selectedApprovals": [
|
|
217
|
+
"0x07e1ed8ea0e9601e5546b0a03aed683df3601407",
|
|
218
|
+
"0x288f0cd85005f34168f731a468aef268c2f9456f",
|
|
219
|
+
],
|
|
220
|
+
"signatureCount": 2,
|
|
221
|
+
"threshold": 3,
|
|
222
|
+
"weight": 3,
|
|
223
|
+
}
|
|
224
|
+
`)
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
test('counts a nested owner only after its quorum', async () => {
|
|
228
|
+
const childConfig = MultisigConfig.from({
|
|
229
|
+
owners: [
|
|
230
|
+
{ owner: owners[1]!.address, weight: 1 },
|
|
231
|
+
{ owner: owners[2]!.address, weight: 1 },
|
|
232
|
+
],
|
|
233
|
+
threshold: 2,
|
|
234
|
+
})
|
|
235
|
+
const child = MultisigConfig.getAddress(childConfig)
|
|
236
|
+
const config = MultisigConfig.from({
|
|
237
|
+
owners: [
|
|
238
|
+
{ owner: owners[0]!.address, weight: 1 },
|
|
239
|
+
{ owner: child, weight: 2 },
|
|
240
|
+
],
|
|
241
|
+
threshold: 2,
|
|
242
|
+
})
|
|
243
|
+
const account = MultisigConfig.getAddress(config)
|
|
244
|
+
const hash = MultisigOperation.getHash({
|
|
245
|
+
account,
|
|
246
|
+
configVersion: 1n,
|
|
247
|
+
transaction,
|
|
248
|
+
type: 'transaction',
|
|
249
|
+
})
|
|
250
|
+
const childHash = MultisigConfig.getSignPayload({
|
|
251
|
+
account: child,
|
|
252
|
+
payload: hash,
|
|
253
|
+
version: 2n,
|
|
254
|
+
})
|
|
255
|
+
const childApprovals = [
|
|
256
|
+
signApproval(owners[1]!, childHash),
|
|
257
|
+
signApproval(owners[2]!, childHash),
|
|
258
|
+
]
|
|
259
|
+
const rootApproval = signApproval(owners[0]!, hash)
|
|
260
|
+
const resolveConfig: MultisigOperation.selectApprovals.ResolveConfig = ({
|
|
261
|
+
account,
|
|
262
|
+
}) => {
|
|
263
|
+
if (!Address.isEqual(account, child)) throw new Error('unknown account')
|
|
264
|
+
return { config: childConfig, version: 2n }
|
|
265
|
+
}
|
|
266
|
+
const partial = await MultisigOperation.selectApprovals({
|
|
267
|
+
account,
|
|
268
|
+
approvals: [
|
|
269
|
+
rootApproval,
|
|
270
|
+
SignatureEnvelope.serialize({
|
|
271
|
+
account: child,
|
|
272
|
+
signatures: [SignatureEnvelope.deserialize(childApprovals[0]!)],
|
|
273
|
+
type: 'multisig',
|
|
274
|
+
}),
|
|
275
|
+
],
|
|
276
|
+
config,
|
|
277
|
+
hash,
|
|
278
|
+
resolveConfig,
|
|
279
|
+
})
|
|
280
|
+
const complete = await MultisigOperation.selectApprovals({
|
|
281
|
+
account,
|
|
282
|
+
approvals: [
|
|
283
|
+
rootApproval,
|
|
284
|
+
SignatureEnvelope.serialize({
|
|
285
|
+
account: child,
|
|
286
|
+
signatures: childApprovals.map((approval) =>
|
|
287
|
+
SignatureEnvelope.deserialize(approval),
|
|
288
|
+
),
|
|
289
|
+
type: 'multisig',
|
|
290
|
+
}),
|
|
291
|
+
],
|
|
292
|
+
config,
|
|
293
|
+
hash,
|
|
294
|
+
resolveConfig,
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
expect({
|
|
298
|
+
complete: {
|
|
299
|
+
...complete,
|
|
300
|
+
approvals: approvalAddresses(complete.approvals, hash),
|
|
301
|
+
selectedApprovals: approvalAddresses(complete.selectedApprovals, hash),
|
|
302
|
+
},
|
|
303
|
+
partial: {
|
|
304
|
+
...partial,
|
|
305
|
+
approvals: approvalAddresses(partial.approvals, hash),
|
|
306
|
+
selectedApprovals: approvalAddresses(partial.selectedApprovals, hash),
|
|
307
|
+
},
|
|
308
|
+
}).toMatchInlineSnapshot(`
|
|
309
|
+
{
|
|
310
|
+
"complete": {
|
|
311
|
+
"approvals": [
|
|
312
|
+
"0x07e1ed8ea0e9601e5546b0a03aed683df3601407",
|
|
313
|
+
"0xe2d2c3c2fc4b17af341cc5c1a459af9606167e8a",
|
|
314
|
+
],
|
|
315
|
+
"selectedApprovals": [
|
|
316
|
+
"0xe2d2c3c2fc4b17af341cc5c1a459af9606167e8a",
|
|
317
|
+
],
|
|
318
|
+
"signatureCount": 1,
|
|
319
|
+
"threshold": 2,
|
|
320
|
+
"weight": 2,
|
|
321
|
+
},
|
|
322
|
+
"partial": {
|
|
323
|
+
"approvals": [
|
|
324
|
+
"0x07e1ed8ea0e9601e5546b0a03aed683df3601407",
|
|
325
|
+
"0xe2d2c3c2fc4b17af341cc5c1a459af9606167e8a",
|
|
326
|
+
],
|
|
327
|
+
"selectedApprovals": [
|
|
328
|
+
"0x07e1ed8ea0e9601e5546b0a03aed683df3601407",
|
|
329
|
+
],
|
|
330
|
+
"signatureCount": 1,
|
|
331
|
+
"threshold": 2,
|
|
332
|
+
"weight": 1,
|
|
333
|
+
},
|
|
334
|
+
}
|
|
335
|
+
`)
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
test('rejects invalid and non-owner approvals', async () => {
|
|
339
|
+
const hash = MultisigOperation.getHash({
|
|
340
|
+
account,
|
|
341
|
+
configVersion: 1n,
|
|
342
|
+
transaction,
|
|
343
|
+
type: 'transaction',
|
|
344
|
+
})
|
|
345
|
+
const invalid = signApproval(
|
|
346
|
+
owners[0]!,
|
|
347
|
+
`0x${'ff'.repeat(32)}` as `0x${string}`,
|
|
348
|
+
)
|
|
349
|
+
const nonOwner = signApproval(owners[2]!, hash)
|
|
350
|
+
|
|
351
|
+
await expect(
|
|
352
|
+
MultisigOperation.selectApprovals({
|
|
353
|
+
account,
|
|
354
|
+
approvals: [invalid],
|
|
355
|
+
config,
|
|
356
|
+
hash,
|
|
357
|
+
}),
|
|
358
|
+
).rejects.toThrowErrorMatchingInlineSnapshot(
|
|
359
|
+
`[MultisigOperation.InvalidApprovalError: Invalid multisig approval: signature from owner 0x07e1ed8ea0e9601e5546b0a03aed683df3601407 is invalid.]`,
|
|
360
|
+
)
|
|
361
|
+
await expect(
|
|
362
|
+
MultisigOperation.selectApprovals({
|
|
363
|
+
account,
|
|
364
|
+
approvals: [nonOwner],
|
|
365
|
+
config,
|
|
366
|
+
hash,
|
|
367
|
+
}),
|
|
368
|
+
).rejects.toThrowErrorMatchingInlineSnapshot(
|
|
369
|
+
`[MultisigOperation.InvalidApprovalError: Invalid multisig approval: signature is from non-owner 0xd3a9f047ad43d7e2e4e7e491f1fe2e657a2651b6.]`,
|
|
370
|
+
)
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
test('requires a resolver for nested owners', async () => {
|
|
374
|
+
const childConfig = MultisigConfig.from({
|
|
375
|
+
owners: [{ owner: owners[1]!.address, weight: 1 }],
|
|
376
|
+
threshold: 1,
|
|
377
|
+
})
|
|
378
|
+
const child = MultisigConfig.getAddress(childConfig)
|
|
379
|
+
const config = MultisigConfig.from({
|
|
380
|
+
owners: [{ owner: child, weight: 1 }],
|
|
381
|
+
threshold: 1,
|
|
382
|
+
})
|
|
383
|
+
const account = MultisigConfig.getAddress(config)
|
|
384
|
+
const hash = MultisigOperation.getHash({
|
|
385
|
+
account,
|
|
386
|
+
configVersion: 1n,
|
|
387
|
+
transaction,
|
|
388
|
+
type: 'transaction',
|
|
389
|
+
})
|
|
390
|
+
const childHash = MultisigConfig.getSignPayload({
|
|
391
|
+
account: child,
|
|
392
|
+
payload: hash,
|
|
393
|
+
version: 1n,
|
|
394
|
+
})
|
|
395
|
+
|
|
396
|
+
await expect(
|
|
397
|
+
MultisigOperation.selectApprovals({
|
|
398
|
+
account,
|
|
399
|
+
approvals: [
|
|
400
|
+
SignatureEnvelope.serialize({
|
|
401
|
+
account: child,
|
|
402
|
+
signatures: [
|
|
403
|
+
SignatureEnvelope.deserialize(
|
|
404
|
+
signApproval(owners[1]!, childHash),
|
|
405
|
+
),
|
|
406
|
+
],
|
|
407
|
+
type: 'multisig',
|
|
408
|
+
}),
|
|
409
|
+
],
|
|
410
|
+
config,
|
|
411
|
+
hash,
|
|
412
|
+
}),
|
|
413
|
+
).rejects.toThrowErrorMatchingInlineSnapshot(
|
|
414
|
+
`[MultisigOperation.InvalidApprovalError: Invalid multisig approval: nested multisig owner 0x7888d60e9cc26c8569d394fce435c248f1e49c3b requires a config resolver.]`,
|
|
415
|
+
)
|
|
416
|
+
})
|
|
417
|
+
|
|
418
|
+
test('rejects keychain and bootstrap nested approvals', async () => {
|
|
419
|
+
const childConfig = MultisigConfig.from({
|
|
420
|
+
owners: [{ owner: owners[1]!.address, weight: 1 }],
|
|
421
|
+
threshold: 1,
|
|
422
|
+
})
|
|
423
|
+
const child = MultisigConfig.getAddress(childConfig)
|
|
424
|
+
const config = MultisigConfig.from({
|
|
425
|
+
owners: [
|
|
426
|
+
{ owner: owners[0]!.address, weight: 1 },
|
|
427
|
+
{ owner: child, weight: 1 },
|
|
428
|
+
],
|
|
429
|
+
threshold: 1,
|
|
430
|
+
})
|
|
431
|
+
const account = MultisigConfig.getAddress(config)
|
|
432
|
+
const hash = MultisigOperation.getHash({
|
|
433
|
+
account,
|
|
434
|
+
configVersion: 1n,
|
|
435
|
+
transaction,
|
|
436
|
+
type: 'transaction',
|
|
437
|
+
})
|
|
438
|
+
const childHash = MultisigConfig.getSignPayload({
|
|
439
|
+
account: child,
|
|
440
|
+
payload: hash,
|
|
441
|
+
version: 0n,
|
|
442
|
+
})
|
|
443
|
+
|
|
444
|
+
await expect(
|
|
445
|
+
MultisigOperation.selectApprovals({
|
|
446
|
+
account,
|
|
447
|
+
approvals: [
|
|
448
|
+
SignatureEnvelope.serialize({
|
|
449
|
+
inner: SignatureEnvelope.deserialize(
|
|
450
|
+
signApproval(owners[0]!, hash),
|
|
451
|
+
),
|
|
452
|
+
type: 'keychain',
|
|
453
|
+
userAddress: owners[0]!.address,
|
|
454
|
+
}),
|
|
455
|
+
],
|
|
456
|
+
config,
|
|
457
|
+
hash,
|
|
458
|
+
}),
|
|
459
|
+
).rejects.toThrowErrorMatchingInlineSnapshot(
|
|
460
|
+
`[MultisigOperation.InvalidApprovalError: Invalid multisig approval: keychain signatures cannot approve a multisig operation.]`,
|
|
461
|
+
)
|
|
462
|
+
await expect(
|
|
463
|
+
MultisigOperation.selectApprovals({
|
|
464
|
+
account,
|
|
465
|
+
approvals: [
|
|
466
|
+
SignatureEnvelope.serialize({
|
|
467
|
+
account: child,
|
|
468
|
+
init: childConfig,
|
|
469
|
+
signatures: [
|
|
470
|
+
SignatureEnvelope.deserialize(
|
|
471
|
+
signApproval(owners[1]!, childHash),
|
|
472
|
+
),
|
|
473
|
+
],
|
|
474
|
+
type: 'multisig',
|
|
475
|
+
}),
|
|
476
|
+
],
|
|
477
|
+
config,
|
|
478
|
+
hash,
|
|
479
|
+
resolveConfig: () => ({ config: childConfig, version: 0n }),
|
|
480
|
+
}),
|
|
481
|
+
).rejects.toThrowErrorMatchingInlineSnapshot(
|
|
482
|
+
`[MultisigOperation.InvalidApprovalError: Invalid multisig approval: nested multisig owner 0x7888d60e9cc26c8569d394fce435c248f1e49c3b cannot carry init.]`,
|
|
483
|
+
)
|
|
484
|
+
})
|
|
485
|
+
|
|
486
|
+
test('rejects nested account cycles', async () => {
|
|
487
|
+
const config = MultisigConfig.from({
|
|
488
|
+
owners: [{ owner: account, weight: 1 }],
|
|
489
|
+
threshold: 1,
|
|
490
|
+
})
|
|
491
|
+
const hash = MultisigOperation.getHash({
|
|
492
|
+
account,
|
|
493
|
+
configVersion: 2n,
|
|
494
|
+
transaction,
|
|
495
|
+
type: 'transaction',
|
|
496
|
+
})
|
|
497
|
+
const nestedHash = MultisigConfig.getSignPayload({
|
|
498
|
+
account,
|
|
499
|
+
payload: hash,
|
|
500
|
+
version: 2n,
|
|
501
|
+
})
|
|
502
|
+
|
|
503
|
+
await expect(
|
|
504
|
+
MultisigOperation.selectApprovals({
|
|
505
|
+
account,
|
|
506
|
+
approvals: [
|
|
507
|
+
SignatureEnvelope.serialize({
|
|
508
|
+
account,
|
|
509
|
+
signatures: [
|
|
510
|
+
SignatureEnvelope.deserialize(
|
|
511
|
+
signApproval(owners[0]!, nestedHash),
|
|
512
|
+
),
|
|
513
|
+
],
|
|
514
|
+
type: 'multisig',
|
|
515
|
+
}),
|
|
516
|
+
],
|
|
517
|
+
config,
|
|
518
|
+
hash,
|
|
519
|
+
resolveConfig: () => ({ config, version: 2n }),
|
|
520
|
+
}),
|
|
521
|
+
).rejects.toThrowErrorMatchingInlineSnapshot(
|
|
522
|
+
`[MultisigOperation.InvalidApprovalError: Invalid multisig approval: nested multisig owner 0xf81b7763d3a6876195d780865bd783dbd97dd36e is invalid.]`,
|
|
523
|
+
)
|
|
524
|
+
})
|
|
525
|
+
|
|
526
|
+
test('rejects excess nesting depth', async () => {
|
|
527
|
+
const grandchildConfig = MultisigConfig.from({
|
|
528
|
+
owners: [{ owner: owners[2]!.address, weight: 1 }],
|
|
529
|
+
threshold: 1,
|
|
530
|
+
})
|
|
531
|
+
const grandchild = MultisigConfig.getAddress(grandchildConfig)
|
|
532
|
+
const childConfig = MultisigConfig.from({
|
|
533
|
+
owners: [{ owner: grandchild, weight: 1 }],
|
|
534
|
+
threshold: 1,
|
|
535
|
+
})
|
|
536
|
+
const child = MultisigConfig.getAddress(childConfig)
|
|
537
|
+
const config = MultisigConfig.from({
|
|
538
|
+
owners: [{ owner: child, weight: 1 }],
|
|
539
|
+
threshold: 1,
|
|
540
|
+
})
|
|
541
|
+
const account = MultisigConfig.getAddress(config)
|
|
542
|
+
const hash = MultisigOperation.getHash({
|
|
543
|
+
account,
|
|
544
|
+
configVersion: 1n,
|
|
545
|
+
transaction,
|
|
546
|
+
type: 'transaction',
|
|
547
|
+
})
|
|
548
|
+
const childHash = MultisigConfig.getSignPayload({
|
|
549
|
+
account: child,
|
|
550
|
+
payload: hash,
|
|
551
|
+
version: 1n,
|
|
552
|
+
})
|
|
553
|
+
const grandchildHash = MultisigConfig.getSignPayload({
|
|
554
|
+
account: grandchild,
|
|
555
|
+
payload: childHash,
|
|
556
|
+
version: 1n,
|
|
557
|
+
})
|
|
558
|
+
|
|
559
|
+
await expect(
|
|
560
|
+
MultisigOperation.selectApprovals({
|
|
561
|
+
account,
|
|
562
|
+
approvals: [
|
|
563
|
+
SignatureEnvelope.serialize({
|
|
564
|
+
account: child,
|
|
565
|
+
signatures: [
|
|
566
|
+
{
|
|
567
|
+
account: grandchild,
|
|
568
|
+
signatures: [
|
|
569
|
+
SignatureEnvelope.deserialize(
|
|
570
|
+
signApproval(owners[2]!, grandchildHash),
|
|
571
|
+
),
|
|
572
|
+
],
|
|
573
|
+
type: 'multisig',
|
|
574
|
+
},
|
|
575
|
+
],
|
|
576
|
+
type: 'multisig',
|
|
577
|
+
}),
|
|
578
|
+
],
|
|
579
|
+
config,
|
|
580
|
+
hash,
|
|
581
|
+
resolveConfig: ({ account }) => {
|
|
582
|
+
if (Address.isEqual(account, child))
|
|
583
|
+
return { config: childConfig, version: 1n }
|
|
584
|
+
return { config: grandchildConfig, version: 1n }
|
|
585
|
+
},
|
|
586
|
+
}),
|
|
587
|
+
).rejects.toThrowErrorMatchingInlineSnapshot(
|
|
588
|
+
`[MultisigOperation.InvalidApprovalError: Invalid multisig approval: nested multisig owner 0xf529c8f6b2b4c72102af4cfe5eeb55b7d45dede2 is invalid.]`,
|
|
589
|
+
)
|
|
590
|
+
})
|
|
591
|
+
})
|
|
592
|
+
|
|
593
|
+
describe('serializeTransaction', () => {
|
|
594
|
+
test('initialized and bootstrap transactions', async () => {
|
|
595
|
+
const results = []
|
|
596
|
+
for (const init of [false, true]) {
|
|
597
|
+
const configVersion = init ? 0n : 1n
|
|
598
|
+
const hash = MultisigOperation.getHash({
|
|
599
|
+
account,
|
|
600
|
+
configVersion,
|
|
601
|
+
transaction,
|
|
602
|
+
type: 'transaction',
|
|
603
|
+
})
|
|
604
|
+
const selection = await MultisigOperation.selectApprovals({
|
|
605
|
+
account,
|
|
606
|
+
approvals: [
|
|
607
|
+
signApproval(owners[1]!, hash),
|
|
608
|
+
signApproval(owners[0]!, hash),
|
|
609
|
+
],
|
|
610
|
+
config,
|
|
611
|
+
hash,
|
|
612
|
+
})
|
|
613
|
+
const operation = MultisigOperation.from({
|
|
614
|
+
account,
|
|
615
|
+
approvals: selection.approvals,
|
|
616
|
+
config,
|
|
617
|
+
configVersion,
|
|
618
|
+
createdAt: 1,
|
|
619
|
+
hash,
|
|
620
|
+
init,
|
|
621
|
+
signatureCount: selection.signatureCount,
|
|
622
|
+
status: 'pending',
|
|
623
|
+
threshold: selection.threshold,
|
|
624
|
+
transaction,
|
|
625
|
+
type: 'transaction',
|
|
626
|
+
updatedAt: 1,
|
|
627
|
+
weight: selection.weight,
|
|
628
|
+
})
|
|
629
|
+
const serialized = MultisigOperation.serializeTransaction(operation, {
|
|
630
|
+
approvals: selection.selectedApprovals,
|
|
631
|
+
})
|
|
632
|
+
const value = TxEnvelopeTempo.deserialize(serialized)
|
|
633
|
+
results.push({
|
|
634
|
+
account: value.signature?.account,
|
|
635
|
+
hash: Hash.keccak256(serialized),
|
|
636
|
+
init: value.signature?.type === 'multisig' && !!value.signature.init,
|
|
637
|
+
signatureCount:
|
|
638
|
+
value.signature?.type === 'multisig'
|
|
639
|
+
? value.signature.signatures.length
|
|
640
|
+
: 0,
|
|
641
|
+
type: serialized.slice(0, 4),
|
|
642
|
+
})
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
expect(results).toMatchInlineSnapshot(`
|
|
646
|
+
[
|
|
647
|
+
{
|
|
648
|
+
"account": "0xf81b7763d3a6876195d780865bd783dbd97dd36e",
|
|
649
|
+
"hash": "0x704768725a4e798e2656f0f355f99507522c97b2947f551a5ee696216a2ba6fe",
|
|
650
|
+
"init": false,
|
|
651
|
+
"signatureCount": 2,
|
|
652
|
+
"type": "0x76",
|
|
653
|
+
},
|
|
654
|
+
{
|
|
655
|
+
"account": "0xf81b7763d3a6876195d780865bd783dbd97dd36e",
|
|
656
|
+
"hash": "0x08131922349c385f9112fefc617fa6f6258a71df1125ec243bb3ee7cff59b700",
|
|
657
|
+
"init": true,
|
|
658
|
+
"signatureCount": 2,
|
|
659
|
+
"type": "0x76",
|
|
660
|
+
},
|
|
661
|
+
]
|
|
662
|
+
`)
|
|
663
|
+
})
|
|
664
|
+
|
|
665
|
+
test('fee-payer transactions', async () => {
|
|
666
|
+
const envelope = TxEnvelopeTempo.from({
|
|
667
|
+
calls: [{ data: '0x1234', to: owner_1 }],
|
|
668
|
+
chainId: 4217,
|
|
669
|
+
})
|
|
670
|
+
const transactions = [
|
|
671
|
+
TxEnvelopeTempo.serialize(envelope, {
|
|
672
|
+
format: 'feePayer',
|
|
673
|
+
sender: account,
|
|
674
|
+
}),
|
|
675
|
+
TxEnvelopeTempo.serialize(
|
|
676
|
+
{
|
|
677
|
+
...envelope,
|
|
678
|
+
feePayerSignature: { r: 5n, s: 6n, yParity: 0 },
|
|
679
|
+
},
|
|
680
|
+
{ format: 'feePayer' },
|
|
681
|
+
),
|
|
682
|
+
] as const
|
|
683
|
+
const results = []
|
|
684
|
+
for (const transaction of transactions) {
|
|
685
|
+
const hash = MultisigOperation.getHash({
|
|
686
|
+
account,
|
|
687
|
+
configVersion: 1n,
|
|
688
|
+
transaction,
|
|
689
|
+
type: 'transaction',
|
|
690
|
+
})
|
|
691
|
+
const selection = await MultisigOperation.selectApprovals({
|
|
692
|
+
account,
|
|
693
|
+
approvals: [
|
|
694
|
+
signApproval(owners[0]!, hash),
|
|
695
|
+
signApproval(owners[1]!, hash),
|
|
696
|
+
],
|
|
697
|
+
config,
|
|
698
|
+
hash,
|
|
699
|
+
})
|
|
700
|
+
const serialized = MultisigOperation.serializeTransaction(
|
|
701
|
+
MultisigOperation.from({
|
|
702
|
+
account,
|
|
703
|
+
approvals: selection.approvals,
|
|
704
|
+
config,
|
|
705
|
+
configVersion: 1n,
|
|
706
|
+
createdAt: 1,
|
|
707
|
+
hash,
|
|
708
|
+
init: false,
|
|
709
|
+
signatureCount: selection.signatureCount,
|
|
710
|
+
status: 'pending',
|
|
711
|
+
threshold: selection.threshold,
|
|
712
|
+
transaction,
|
|
713
|
+
type: 'transaction',
|
|
714
|
+
updatedAt: 1,
|
|
715
|
+
weight: selection.weight,
|
|
716
|
+
}),
|
|
717
|
+
{ approvals: selection.selectedApprovals },
|
|
718
|
+
)
|
|
719
|
+
const value = TxEnvelopeTempo.deserialize(serialized)
|
|
720
|
+
results.push({
|
|
721
|
+
feePayerSignature: value.feePayerSignature,
|
|
722
|
+
from: value.from,
|
|
723
|
+
signatureCount:
|
|
724
|
+
value.signature?.type === 'multisig'
|
|
725
|
+
? value.signature.signatures.length
|
|
726
|
+
: 0,
|
|
727
|
+
type: serialized.slice(0, 4),
|
|
728
|
+
})
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
expect(results).toMatchInlineSnapshot(`
|
|
732
|
+
[
|
|
733
|
+
{
|
|
734
|
+
"feePayerSignature": null,
|
|
735
|
+
"from": "0xf81b7763d3a6876195d780865bd783dbd97dd36e",
|
|
736
|
+
"signatureCount": 2,
|
|
737
|
+
"type": "0x78",
|
|
738
|
+
},
|
|
739
|
+
{
|
|
740
|
+
"feePayerSignature": {
|
|
741
|
+
"r": 5n,
|
|
742
|
+
"s": 6n,
|
|
743
|
+
"yParity": 0,
|
|
744
|
+
},
|
|
745
|
+
"from": "0xf81b7763d3a6876195d780865bd783dbd97dd36e",
|
|
746
|
+
"signatureCount": 2,
|
|
747
|
+
"type": "0x78",
|
|
748
|
+
},
|
|
749
|
+
]
|
|
750
|
+
`)
|
|
751
|
+
})
|
|
752
|
+
|
|
753
|
+
test('rejects an approval not retained by the operation', async () => {
|
|
754
|
+
const hash = MultisigOperation.getHash({
|
|
755
|
+
account,
|
|
756
|
+
configVersion: 1n,
|
|
757
|
+
transaction,
|
|
758
|
+
type: 'transaction',
|
|
759
|
+
})
|
|
760
|
+
const approval_1 = signApproval(owners[0]!, hash)
|
|
761
|
+
const approval_2 = signApproval(owners[1]!, hash)
|
|
762
|
+
const selection = await MultisigOperation.selectApprovals({
|
|
763
|
+
account,
|
|
764
|
+
approvals: [approval_1],
|
|
765
|
+
config,
|
|
766
|
+
hash,
|
|
767
|
+
})
|
|
768
|
+
const operation = MultisigOperation.from({
|
|
769
|
+
account,
|
|
770
|
+
approvals: selection.approvals,
|
|
771
|
+
config,
|
|
772
|
+
configVersion: 1n,
|
|
773
|
+
createdAt: 1,
|
|
774
|
+
hash,
|
|
775
|
+
init: false,
|
|
776
|
+
signatureCount: selection.signatureCount,
|
|
777
|
+
status: 'pending',
|
|
778
|
+
threshold: selection.threshold,
|
|
779
|
+
transaction,
|
|
780
|
+
type: 'transaction',
|
|
781
|
+
updatedAt: 1,
|
|
782
|
+
weight: selection.weight,
|
|
783
|
+
})
|
|
784
|
+
|
|
785
|
+
expect(() =>
|
|
786
|
+
MultisigOperation.serializeTransaction(operation, {
|
|
787
|
+
approvals: [approval_2],
|
|
788
|
+
}),
|
|
789
|
+
).toThrowErrorMatchingInlineSnapshot(
|
|
790
|
+
`[MultisigOperation.InvalidOperationError: Invalid multisig operation: transaction signature is not a retained approval.]`,
|
|
791
|
+
)
|
|
792
|
+
})
|
|
793
|
+
})
|
|
794
|
+
|
|
104
795
|
describe('from', () => {
|
|
105
796
|
test('transaction states', () => {
|
|
106
797
|
const pending = MultisigOperation.from(transactionPending)
|