@openzeppelin/miden-multisig-client 0.17.0-rc.2 → 0.17.0
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/README.md +172 -47
- package/dist/account/builder.d.ts +2 -2
- package/dist/account/builder.d.ts.map +1 -1
- package/dist/account/builder.js +49 -24
- package/dist/account/builder.js.map +1 -1
- package/dist/connectivity.d.ts +2 -0
- package/dist/connectivity.d.ts.map +1 -1
- package/dist/connectivity.js +7 -4
- package/dist/connectivity.js.map +1 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/dist/multisig/authArgErrors.d.ts +59 -0
- package/dist/multisig/authArgErrors.d.ts.map +1 -0
- package/dist/multisig/authArgErrors.js +115 -0
- package/dist/multisig/authArgErrors.js.map +1 -0
- package/dist/multisig/helpers.d.ts +5 -0
- package/dist/multisig/helpers.d.ts.map +1 -1
- package/dist/multisig/helpers.js +7 -2
- package/dist/multisig/helpers.js.map +1 -1
- package/dist/multisig.d.ts +140 -0
- package/dist/multisig.d.ts.map +1 -1
- package/dist/multisig.js +419 -12
- package/dist/multisig.js.map +1 -1
- package/dist/procedures.d.ts +1 -1
- package/dist/procedures.js +1 -1
- package/dist/recovery/proposalNoteImport.d.ts +173 -0
- package/dist/recovery/proposalNoteImport.d.ts.map +1 -0
- package/dist/recovery/proposalNoteImport.js +411 -0
- package/dist/recovery/proposalNoteImport.js.map +1 -0
- package/dist/recovery/publicNoteBackfill.d.ts +168 -0
- package/dist/recovery/publicNoteBackfill.d.ts.map +1 -0
- package/dist/recovery/publicNoteBackfill.js +368 -0
- package/dist/recovery/publicNoteBackfill.js.map +1 -0
- package/dist/recovery/recoverNotes.d.ts +147 -0
- package/dist/recovery/recoverNotes.d.ts.map +1 -0
- package/dist/recovery/recoverNotes.js +163 -0
- package/dist/recovery/recoverNotes.js.map +1 -0
- package/dist/recovery/transportDrain.d.ts +124 -0
- package/dist/recovery/transportDrain.d.ts.map +1 -0
- package/dist/recovery/transportDrain.js +262 -0
- package/dist/recovery/transportDrain.js.map +1 -0
- package/dist/transaction/consumeNotes.d.ts.map +1 -1
- package/dist/transaction/consumeNotes.js +4 -1
- package/dist/transaction/consumeNotes.js.map +1 -1
- package/dist/transaction/p2id.d.ts.map +1 -1
- package/dist/transaction/p2id.js +4 -1
- package/dist/transaction/p2id.js.map +1 -1
- package/dist/transaction/summary.d.ts +10 -4
- package/dist/transaction/summary.d.ts.map +1 -1
- package/dist/transaction/summary.js +13 -7
- package/dist/transaction/summary.js.map +1 -1
- package/dist/transaction/updateGuardian.js +4 -1
- package/dist/transaction/updateGuardian.js.map +1 -1
- package/dist/transaction/updateProcedureThreshold.js +4 -1
- package/dist/transaction/updateProcedureThreshold.js.map +1 -1
- package/dist/transaction/updateSigners.js +4 -1
- package/dist/transaction/updateSigners.js.map +1 -1
- package/dist/transaction.d.ts +1 -1
- package/dist/transaction.d.ts.map +1 -1
- package/dist/transaction.js +1 -1
- package/dist/transaction.js.map +1 -1
- package/package.json +14 -10
- package/src/account/builder.ts +87 -35
- package/src/connectivity.ts +9 -4
- package/src/index.ts +34 -1
- package/src/multisig/authArgErrors.ts +138 -0
- package/src/multisig/helpers.ts +7 -2
- package/src/multisig.ts +513 -20
- package/src/procedures.ts +1 -1
- package/src/recovery/proposalNoteImport.ts +548 -0
- package/src/recovery/publicNoteBackfill.ts +502 -0
- package/src/recovery/recoverNotes.ts +288 -0
- package/src/recovery/transportDrain.ts +330 -0
- package/src/transaction/consumeNotes.ts +4 -1
- package/src/transaction/p2id.ts +4 -1
- package/src/transaction/summary.ts +13 -7
- package/src/transaction/updateGuardian.ts +4 -1
- package/src/transaction/updateProcedureThreshold.ts +4 -1
- package/src/transaction/updateSigners.ts +4 -1
- package/src/transaction.ts +1 -1
- package/dist/account/builder.test.d.ts +0 -2
- package/dist/account/builder.test.d.ts.map +0 -1
- package/dist/account/builder.test.js +0 -166
- package/dist/account/builder.test.js.map +0 -1
- package/dist/account/masm/account-components/auth.d.ts +0 -2
- package/dist/account/masm/account-components/auth.d.ts.map +0 -1
- package/dist/account/masm/account-components/auth.js +0 -46
- package/dist/account/masm/account-components/auth.js.map +0 -1
- package/dist/account/masm/index.d.ts +0 -2
- package/dist/account/masm/index.d.ts.map +0 -1
- package/dist/account/masm/index.js +0 -4
- package/dist/account/masm/index.js.map +0 -1
- package/dist/account/masm.d.ts +0 -2
- package/dist/account/masm.d.ts.map +0 -1
- package/dist/account/masm.js +0 -4
- package/dist/account/masm.js.map +0 -1
- package/dist/account/storage.test.d.ts +0 -2
- package/dist/account/storage.test.d.ts.map +0 -1
- package/dist/account/storage.test.js +0 -73
- package/dist/account/storage.test.js.map +0 -1
- package/dist/client.test.d.ts +0 -2
- package/dist/client.test.d.ts.map +0 -1
- package/dist/client.test.js +0 -380
- package/dist/client.test.js.map +0 -1
- package/dist/connectivity.test.d.ts +0 -2
- package/dist/connectivity.test.d.ts.map +0 -1
- package/dist/connectivity.test.js +0 -61
- package/dist/connectivity.test.js.map +0 -1
- package/dist/inspector.test.d.ts +0 -2
- package/dist/inspector.test.d.ts.map +0 -1
- package/dist/inspector.test.js +0 -425
- package/dist/inspector.test.js.map +0 -1
- package/dist/lookupAuth.test.d.ts +0 -2
- package/dist/lookupAuth.test.d.ts.map +0 -1
- package/dist/lookupAuth.test.js +0 -138
- package/dist/lookupAuth.test.js.map +0 -1
- package/dist/multisig/consumeNotesErrors.test.d.ts +0 -2
- package/dist/multisig/consumeNotesErrors.test.d.ts.map +0 -1
- package/dist/multisig/consumeNotesErrors.test.js +0 -28
- package/dist/multisig/consumeNotesErrors.test.js.map +0 -1
- package/dist/multisig/helpers.test.d.ts +0 -2
- package/dist/multisig/helpers.test.d.ts.map +0 -1
- package/dist/multisig/helpers.test.js +0 -94
- package/dist/multisig/helpers.test.js.map +0 -1
- package/dist/multisig.test.d.ts +0 -2
- package/dist/multisig.test.d.ts.map +0 -1
- package/dist/multisig.test.js +0 -4112
- package/dist/multisig.test.js.map +0 -1
- package/dist/proposal/factory.test.d.ts +0 -2
- package/dist/proposal/factory.test.d.ts.map +0 -1
- package/dist/proposal/factory.test.js +0 -32
- package/dist/proposal/factory.test.js.map +0 -1
- package/dist/proposal/metadata.test.d.ts +0 -2
- package/dist/proposal/metadata.test.d.ts.map +0 -1
- package/dist/proposal/metadata.test.js +0 -193
- package/dist/proposal/metadata.test.js.map +0 -1
- package/dist/prover/config.test.d.ts +0 -2
- package/dist/prover/config.test.d.ts.map +0 -1
- package/dist/prover/config.test.js +0 -54
- package/dist/prover/config.test.js.map +0 -1
- package/dist/prover/errors.test.d.ts +0 -2
- package/dist/prover/errors.test.d.ts.map +0 -1
- package/dist/prover/errors.test.js +0 -29
- package/dist/prover/errors.test.js.map +0 -1
- package/dist/prover/retry.test.d.ts +0 -2
- package/dist/prover/retry.test.d.ts.map +0 -1
- package/dist/prover/retry.test.js +0 -20
- package/dist/prover/retry.test.js.map +0 -1
- package/dist/prover/workflow.test.d.ts +0 -2
- package/dist/prover/workflow.test.d.ts.map +0 -1
- package/dist/prover/workflow.test.js +0 -105
- package/dist/prover/workflow.test.js.map +0 -1
- package/dist/raw-client.test.d.ts +0 -2
- package/dist/raw-client.test.d.ts.map +0 -1
- package/dist/raw-client.test.js +0 -111
- package/dist/raw-client.test.js.map +0 -1
- package/dist/rpc/config.test.d.ts +0 -2
- package/dist/rpc/config.test.d.ts.map +0 -1
- package/dist/rpc/config.test.js +0 -24
- package/dist/rpc/config.test.js.map +0 -1
- package/dist/rpc/errors.test.d.ts +0 -2
- package/dist/rpc/errors.test.d.ts.map +0 -1
- package/dist/rpc/errors.test.js +0 -34
- package/dist/rpc/errors.test.js.map +0 -1
- package/dist/rpc/retry.test.d.ts +0 -2
- package/dist/rpc/retry.test.d.ts.map +0 -1
- package/dist/rpc/retry.test.js +0 -98
- package/dist/rpc/retry.test.js.map +0 -1
- package/dist/signers/ecdsa.test.d.ts +0 -2
- package/dist/signers/ecdsa.test.d.ts.map +0 -1
- package/dist/signers/ecdsa.test.js +0 -88
- package/dist/signers/ecdsa.test.js.map +0 -1
- package/dist/signers/falcon.test.d.ts +0 -2
- package/dist/signers/falcon.test.d.ts.map +0 -1
- package/dist/signers/falcon.test.js +0 -161
- package/dist/signers/falcon.test.js.map +0 -1
- package/dist/signers/miden-wallet.ecdsa-recovery.test.d.ts +0 -2
- package/dist/signers/miden-wallet.ecdsa-recovery.test.d.ts.map +0 -1
- package/dist/signers/miden-wallet.ecdsa-recovery.test.js +0 -148
- package/dist/signers/miden-wallet.ecdsa-recovery.test.js.map +0 -1
- package/dist/signers/miden-wallet.test.d.ts +0 -2
- package/dist/signers/miden-wallet.test.d.ts.map +0 -1
- package/dist/signers/miden-wallet.test.js +0 -164
- package/dist/signers/miden-wallet.test.js.map +0 -1
- package/dist/signers/para.test.d.ts +0 -2
- package/dist/signers/para.test.d.ts.map +0 -1
- package/dist/signers/para.test.js +0 -146
- package/dist/signers/para.test.js.map +0 -1
- package/dist/transaction/index.d.ts +0 -8
- package/dist/transaction/index.d.ts.map +0 -1
- package/dist/transaction/index.js +0 -7
- package/dist/transaction/index.js.map +0 -1
- package/dist/transaction/p2id.test.d.ts +0 -2
- package/dist/transaction/p2id.test.d.ts.map +0 -1
- package/dist/transaction/p2id.test.js +0 -246
- package/dist/transaction/p2id.test.js.map +0 -1
- package/dist/transaction/rpoRandomCoin.test.d.ts +0 -2
- package/dist/transaction/rpoRandomCoin.test.d.ts.map +0 -1
- package/dist/transaction/rpoRandomCoin.test.js +0 -52
- package/dist/transaction/rpoRandomCoin.test.js.map +0 -1
- package/dist/transaction/summary.test.d.ts +0 -2
- package/dist/transaction/summary.test.d.ts.map +0 -1
- package/dist/transaction/summary.test.js +0 -26
- package/dist/transaction/summary.test.js.map +0 -1
- package/dist/transaction.test.d.ts +0 -2
- package/dist/transaction.test.d.ts.map +0 -1
- package/dist/transaction.test.js +0 -122
- package/dist/transaction.test.js.map +0 -1
- package/dist/types/proposal.test.d.ts +0 -2
- package/dist/types/proposal.test.d.ts.map +0 -1
- package/dist/types/proposal.test.js +0 -56
- package/dist/types/proposal.test.js.map +0 -1
- package/dist/utils/digest.test.d.ts +0 -2
- package/dist/utils/digest.test.d.ts.map +0 -1
- package/dist/utils/digest.test.js +0 -48
- package/dist/utils/digest.test.js.map +0 -1
- package/dist/utils/ecdsa.test.d.ts +0 -2
- package/dist/utils/ecdsa.test.d.ts.map +0 -1
- package/dist/utils/ecdsa.test.js +0 -131
- package/dist/utils/ecdsa.test.js.map +0 -1
- package/dist/utils/encoding.test.d.ts +0 -2
- package/dist/utils/encoding.test.d.ts.map +0 -1
- package/dist/utils/encoding.test.js +0 -185
- package/dist/utils/encoding.test.js.map +0 -1
- package/dist/utils/key.test.d.ts +0 -2
- package/dist/utils/key.test.d.ts.map +0 -1
- package/dist/utils/key.test.js +0 -82
- package/dist/utils/key.test.js.map +0 -1
- package/dist/utils/signature.test.d.ts +0 -2
- package/dist/utils/signature.test.d.ts.map +0 -1
- package/dist/utils/signature.test.js +0 -164
- package/dist/utils/signature.test.js.map +0 -1
- package/dist/utils/word.test.d.ts +0 -2
- package/dist/utils/word.test.d.ts.map +0 -1
- package/dist/utils/word.test.js +0 -54
- package/dist/utils/word.test.js.map +0 -1
- package/masm/account_components/auth/guarded_multisig.masm +0 -42
- package/src/account/builder.test.ts +0 -238
- package/src/account/masm/account-components/auth.ts +0 -46
- package/src/account/masm/index.ts +0 -4
- package/src/account/masm.ts +0 -4
- package/src/account/storage.test.ts +0 -90
- package/src/client.test.ts +0 -473
- package/src/connectivity.test.ts +0 -67
- package/src/inspector.test.ts +0 -542
- package/src/lookupAuth.test.ts +0 -185
- package/src/multisig/consumeNotesErrors.test.ts +0 -43
- package/src/multisig/helpers.test.ts +0 -114
- package/src/multisig.test.ts +0 -5008
- package/src/proposal/factory.test.ts +0 -40
- package/src/proposal/metadata.test.ts +0 -235
- package/src/prover/config.test.ts +0 -90
- package/src/prover/errors.test.ts +0 -61
- package/src/prover/retry.test.ts +0 -38
- package/src/prover/workflow.test.ts +0 -145
- package/src/raw-client.test.ts +0 -171
- package/src/rpc/config.test.ts +0 -45
- package/src/rpc/errors.test.ts +0 -69
- package/src/rpc/retry.test.ts +0 -144
- package/src/signers/ecdsa.test.ts +0 -111
- package/src/signers/falcon.test.ts +0 -197
- package/src/signers/miden-wallet.ecdsa-recovery.test.ts +0 -201
- package/src/signers/miden-wallet.test.ts +0 -206
- package/src/signers/para.test.ts +0 -186
- package/src/transaction/index.ts +0 -13
- package/src/transaction/p2id.test.ts +0 -371
- package/src/transaction/rpoRandomCoin.test.ts +0 -64
- package/src/transaction/summary.test.ts +0 -32
- package/src/transaction.test.ts +0 -142
- package/src/types/proposal.test.ts +0 -68
- package/src/utils/digest.test.ts +0 -55
- package/src/utils/ecdsa.test.ts +0 -164
- package/src/utils/encoding.test.ts +0 -233
- package/src/utils/key.test.ts +0 -91
- package/src/utils/signature.test.ts +0 -210
- package/src/utils/word.test.ts +0 -64
package/dist/multisig.test.js
DELETED
|
@@ -1,4112 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
-
import { Multisig } from './multisig.js';
|
|
3
|
-
import { GuardianHttpClient } from '@openzeppelin/guardian-client';
|
|
4
|
-
import { buildUpdateProcedureThresholdTransactionRequest, buildUpdateGuardianTransactionRequest, buildUpdateSignersTransactionRequest, chainAnchorFromBase64, executeForSummary, executeForSummaryAt, } from './transaction.js';
|
|
5
|
-
const { mockRpcGetAccountDetails, mockAccountDeserialize, mockDetectConfig, mockNoteFileDeserialize, mockGetSignerCommitments, mockGetGuardianCommitment, } = vi.hoisted(() => ({
|
|
6
|
-
mockRpcGetAccountDetails: vi.fn(),
|
|
7
|
-
mockAccountDeserialize: vi.fn(),
|
|
8
|
-
mockDetectConfig: vi.fn(),
|
|
9
|
-
mockNoteFileDeserialize: vi.fn(),
|
|
10
|
-
mockGetSignerCommitments: vi.fn(),
|
|
11
|
-
mockGetGuardianCommitment: vi.fn(),
|
|
12
|
-
}));
|
|
13
|
-
const { MOCK_CHAIN_ANCHOR_B64, createMockChainAnchor } = vi.hoisted(() => {
|
|
14
|
-
const MOCK_CHAIN_ANCHOR_B64 = 'bW9jay1jaGFpbi1hbmNob3I=';
|
|
15
|
-
const createMockChainAnchor = () => ({
|
|
16
|
-
commitment: () => ({ toHex: () => '0x' + 'b'.repeat(64) }),
|
|
17
|
-
free: () => { },
|
|
18
|
-
serialize: () => new Uint8Array([9, 9, 9]),
|
|
19
|
-
});
|
|
20
|
-
return { MOCK_CHAIN_ANCHOR_B64, createMockChainAnchor };
|
|
21
|
-
});
|
|
22
|
-
// Mock the Miden SDK
|
|
23
|
-
vi.mock('@miden-sdk/miden-sdk', () => ({
|
|
24
|
-
Account: {
|
|
25
|
-
deserialize: mockAccountDeserialize,
|
|
26
|
-
},
|
|
27
|
-
AccountId: {
|
|
28
|
-
fromHex: vi.fn((hex) => ({ toString: () => hex })),
|
|
29
|
-
},
|
|
30
|
-
NoteType: {
|
|
31
|
-
Private: 0,
|
|
32
|
-
Public: 1,
|
|
33
|
-
},
|
|
34
|
-
NoteExportFormat: {
|
|
35
|
-
Id: 0,
|
|
36
|
-
Full: 1,
|
|
37
|
-
Details: 2,
|
|
38
|
-
},
|
|
39
|
-
NoteFile: {
|
|
40
|
-
deserialize: mockNoteFileDeserialize,
|
|
41
|
-
},
|
|
42
|
-
TransactionSummary: {
|
|
43
|
-
deserialize: vi.fn().mockReturnValue({
|
|
44
|
-
toCommitment: () => ({
|
|
45
|
-
toHex: () => '0x' + 'c'.repeat(64),
|
|
46
|
-
}),
|
|
47
|
-
blockCommitment: () => ({
|
|
48
|
-
toHex: () => '0x' + 'b'.repeat(64),
|
|
49
|
-
}),
|
|
50
|
-
userParams: () => [0, 0, 0, 1, 2, 3, 4],
|
|
51
|
-
serialize: () => new Uint8Array([1, 2, 3]),
|
|
52
|
-
}),
|
|
53
|
-
},
|
|
54
|
-
Word: {
|
|
55
|
-
fromHex: vi.fn((hex) => ({
|
|
56
|
-
toHex: () => hex,
|
|
57
|
-
toFelts: () => [1, 2, 3, 4],
|
|
58
|
-
})),
|
|
59
|
-
},
|
|
60
|
-
Signature: {
|
|
61
|
-
deserialize: vi.fn().mockReturnValue({
|
|
62
|
-
toPreparedSignature: () => [1, 2, 3],
|
|
63
|
-
}),
|
|
64
|
-
},
|
|
65
|
-
TransactionRequest: {
|
|
66
|
-
deserialize: vi.fn().mockReturnValue({}),
|
|
67
|
-
},
|
|
68
|
-
AdviceMap: vi.fn().mockImplementation(() => ({
|
|
69
|
-
insert: vi.fn(),
|
|
70
|
-
})),
|
|
71
|
-
FeltArray: vi.fn().mockImplementation((arr) => arr),
|
|
72
|
-
Poseidon2: {
|
|
73
|
-
hashElements: vi.fn().mockReturnValue({
|
|
74
|
-
toHex: () => '0x' + 'e'.repeat(64),
|
|
75
|
-
}),
|
|
76
|
-
},
|
|
77
|
-
Endpoint: vi.fn().mockImplementation((url) => ({ url })),
|
|
78
|
-
RpcClient: vi.fn().mockImplementation(() => ({
|
|
79
|
-
getAccountDetails: mockRpcGetAccountDetails,
|
|
80
|
-
})),
|
|
81
|
-
}));
|
|
82
|
-
// Mock transaction module
|
|
83
|
-
vi.mock('./transaction.js', () => ({
|
|
84
|
-
executeForSummary: vi.fn(),
|
|
85
|
-
executeForSummaryAt: vi.fn(),
|
|
86
|
-
chainAnchorToBase64: vi.fn(() => MOCK_CHAIN_ANCHOR_B64),
|
|
87
|
-
chainAnchorFromBase64: vi.fn(() => createMockChainAnchor()),
|
|
88
|
-
summarySalt: vi.fn(() => ({
|
|
89
|
-
toHex: () => '0x' + 'd'.repeat(64),
|
|
90
|
-
})),
|
|
91
|
-
buildUpdateSignersTransactionRequest: vi.fn().mockResolvedValue({
|
|
92
|
-
request: {},
|
|
93
|
-
salt: { toHex: () => '0x' + 'd'.repeat(64) },
|
|
94
|
-
configHash: { toHex: () => '0x' + 'e'.repeat(64) },
|
|
95
|
-
}),
|
|
96
|
-
buildUpdateProcedureThresholdTransactionRequest: vi.fn().mockResolvedValue({
|
|
97
|
-
request: {},
|
|
98
|
-
salt: { toHex: () => '0x' + 'd'.repeat(64) },
|
|
99
|
-
configHash: { toHex: () => '0x' + 'e'.repeat(64) },
|
|
100
|
-
}),
|
|
101
|
-
buildUpdateGuardianTransactionRequest: vi.fn().mockResolvedValue({
|
|
102
|
-
request: {},
|
|
103
|
-
salt: { toHex: () => '0x' + 'd'.repeat(64) },
|
|
104
|
-
}),
|
|
105
|
-
buildConsumeNotesTransactionRequest: vi.fn().mockReturnValue({
|
|
106
|
-
request: {},
|
|
107
|
-
salt: { toHex: () => '0x' + 'd'.repeat(64) },
|
|
108
|
-
}),
|
|
109
|
-
buildP2idTransactionRequest: vi.fn().mockReturnValue({
|
|
110
|
-
request: {},
|
|
111
|
-
salt: { toHex: () => '0x' + 'd'.repeat(64) },
|
|
112
|
-
}),
|
|
113
|
-
buildP2idNoteFromMetadata: vi.fn().mockReturnValue({
|
|
114
|
-
id: () => ({ toString: () => '0x' + 'ab'.repeat(32) }),
|
|
115
|
-
}),
|
|
116
|
-
// Mirrors the real implementations against the mocked NoteType values
|
|
117
|
-
// (Private = 0, Public = 1).
|
|
118
|
-
parseP2idNoteType: vi.fn((value) => {
|
|
119
|
-
if (value === undefined || value === 'public')
|
|
120
|
-
return 1;
|
|
121
|
-
if (value === 'private')
|
|
122
|
-
return 0;
|
|
123
|
-
throw new Error(`unsupported metadata.noteType '${value}': expected 'public' or 'private'`);
|
|
124
|
-
}),
|
|
125
|
-
p2idNoteTypeToMetadata: vi.fn((noteType) => (noteType === 0 ? 'private' : undefined)),
|
|
126
|
-
}));
|
|
127
|
-
vi.mock('./utils/signature.js', async () => {
|
|
128
|
-
const actual = await vi.importActual('./utils/signature.js');
|
|
129
|
-
return {
|
|
130
|
-
...actual,
|
|
131
|
-
buildSignatureAdviceEntry: vi.fn().mockImplementation((signerCommitment) => ({
|
|
132
|
-
key: { toHex: () => signerCommitment.toHex ? signerCommitment.toHex() : '0x' + 'f'.repeat(64) },
|
|
133
|
-
values: [1, 2, 3],
|
|
134
|
-
})),
|
|
135
|
-
signatureHexToBytes: vi.fn((hex) => new Uint8Array([0, 1, 2, 3])),
|
|
136
|
-
// These tests use synthetic signature bytes to exercise advice routing;
|
|
137
|
-
// recoverability is covered by tests/ecdsa-advice-encoding.test.ts.
|
|
138
|
-
assertEcdsaSignatureRecoverable: vi.fn(),
|
|
139
|
-
};
|
|
140
|
-
});
|
|
141
|
-
vi.mock('./utils/encoding.js', async () => {
|
|
142
|
-
const actual = await vi.importActual('./utils/encoding.js');
|
|
143
|
-
return {
|
|
144
|
-
...actual,
|
|
145
|
-
normalizeHexWord: vi.fn((hex) => '0x' + hex.replace(/^0x/i, '').toLowerCase().padStart(64, '0')),
|
|
146
|
-
};
|
|
147
|
-
});
|
|
148
|
-
// Keep the real assertCompleteDetectedConfig so refreshConfigFromAccount's
|
|
149
|
-
// fail-closed validation is exercised.
|
|
150
|
-
vi.mock('./inspector.js', async (importOriginal) => {
|
|
151
|
-
const actual = await importOriginal();
|
|
152
|
-
return {
|
|
153
|
-
...actual,
|
|
154
|
-
AccountInspector: {
|
|
155
|
-
fromAccount: mockDetectConfig,
|
|
156
|
-
getSignerPublicKeyCommitments: mockGetSignerCommitments,
|
|
157
|
-
getGuardianPublicKeyCommitment: mockGetGuardianCommitment,
|
|
158
|
-
},
|
|
159
|
-
};
|
|
160
|
-
});
|
|
161
|
-
// Mock fetch for GUARDIAN client
|
|
162
|
-
const mockFetch = vi.fn();
|
|
163
|
-
vi.stubGlobal('fetch', mockFetch);
|
|
164
|
-
const MIDEN_RPC_ENDPOINT = 'https://rpc.devnet.miden.io';
|
|
165
|
-
function mockedAccount(commitmentHex, nonce = 0) {
|
|
166
|
-
return {
|
|
167
|
-
commitment: () => ({
|
|
168
|
-
toHex: () => commitmentHex,
|
|
169
|
-
}),
|
|
170
|
-
to_commitment: () => ({
|
|
171
|
-
toHex: () => commitmentHex,
|
|
172
|
-
}),
|
|
173
|
-
nonce: () => ({
|
|
174
|
-
asInt: () => BigInt(nonce),
|
|
175
|
-
}),
|
|
176
|
-
};
|
|
177
|
-
}
|
|
178
|
-
describe('Multisig', () => {
|
|
179
|
-
let guardian;
|
|
180
|
-
let mockSigner;
|
|
181
|
-
let mockAccount;
|
|
182
|
-
let mockWebClient;
|
|
183
|
-
function createTestMultisig(config, signer = mockSigner, accountId, proverConfig) {
|
|
184
|
-
return new Multisig(mockAccount, config, guardian, signer, mockWebClient, accountId, MIDEN_RPC_ENDPOINT, proverConfig);
|
|
185
|
-
}
|
|
186
|
-
beforeEach(() => {
|
|
187
|
-
mockFetch.mockReset();
|
|
188
|
-
vi.mocked(executeForSummary).mockResolvedValue({
|
|
189
|
-
summary: {
|
|
190
|
-
toCommitment: () => ({
|
|
191
|
-
toHex: () => '0x' + 'c'.repeat(64),
|
|
192
|
-
}),
|
|
193
|
-
serialize: () => new Uint8Array([1, 2, 3]),
|
|
194
|
-
},
|
|
195
|
-
anchor: createMockChainAnchor(),
|
|
196
|
-
});
|
|
197
|
-
vi.mocked(executeForSummaryAt).mockResolvedValue({
|
|
198
|
-
toCommitment: () => ({
|
|
199
|
-
toHex: () => '0x' + 'c'.repeat(64),
|
|
200
|
-
}),
|
|
201
|
-
serialize: () => new Uint8Array([1, 2, 3]),
|
|
202
|
-
});
|
|
203
|
-
mockRpcGetAccountDetails.mockReset();
|
|
204
|
-
mockAccountDeserialize.mockReset();
|
|
205
|
-
mockRpcGetAccountDetails.mockResolvedValue({
|
|
206
|
-
commitment: () => ({
|
|
207
|
-
toHex: () => '0x' + 'b'.repeat(64),
|
|
208
|
-
}),
|
|
209
|
-
});
|
|
210
|
-
mockAccountDeserialize.mockReturnValue(mockedAccount('0x' + 'b'.repeat(64), 1));
|
|
211
|
-
mockDetectConfig.mockReset();
|
|
212
|
-
mockDetectConfig.mockReturnValue({
|
|
213
|
-
threshold: 1,
|
|
214
|
-
numSigners: 1,
|
|
215
|
-
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
216
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
217
|
-
vaultBalances: [],
|
|
218
|
-
procedureThresholds: new Map(),
|
|
219
|
-
});
|
|
220
|
-
guardian = new GuardianHttpClient('http://localhost:3000');
|
|
221
|
-
mockSigner = {
|
|
222
|
-
commitment: '0x' + '1'.repeat(64),
|
|
223
|
-
publicKey: '0x' + '2'.repeat(64),
|
|
224
|
-
scheme: 'falcon',
|
|
225
|
-
signAccountIdWithTimestamp: vi.fn().mockResolvedValue('0x' + 'a'.repeat(128)),
|
|
226
|
-
signRequest: vi.fn().mockReturnValue('0x' + 'a'.repeat(128)),
|
|
227
|
-
signCommitment: vi.fn().mockReturnValue('0x' + 'b'.repeat(128)),
|
|
228
|
-
};
|
|
229
|
-
guardian.setSigner(mockSigner);
|
|
230
|
-
mockAccount = {
|
|
231
|
-
id: () => ({
|
|
232
|
-
toString: () => '0x' + 'a'.repeat(30),
|
|
233
|
-
prefix: () => ({ asInt: () => BigInt(1) }),
|
|
234
|
-
suffix: () => ({ asInt: () => BigInt(2) }),
|
|
235
|
-
}),
|
|
236
|
-
serialize: () => new Uint8Array([1, 2, 3]),
|
|
237
|
-
};
|
|
238
|
-
mockWebClient = {
|
|
239
|
-
executeTransaction: vi.fn(),
|
|
240
|
-
proveTransaction: vi.fn(),
|
|
241
|
-
submitProvenTransaction: vi.fn(),
|
|
242
|
-
applyTransaction: vi.fn(),
|
|
243
|
-
submitNewTransaction: vi.fn(),
|
|
244
|
-
submitNewTransactionWithProver: vi.fn(),
|
|
245
|
-
transactions: {
|
|
246
|
-
executeRequest: vi.fn(),
|
|
247
|
-
},
|
|
248
|
-
getConsumableNotes: vi.fn().mockResolvedValue([]),
|
|
249
|
-
syncState: vi.fn(),
|
|
250
|
-
getAccount: vi.fn().mockResolvedValue(null),
|
|
251
|
-
newAccount: vi.fn(),
|
|
252
|
-
};
|
|
253
|
-
mockWebClient.transactions.executeRequest.mockImplementation(async (accountId, request) => {
|
|
254
|
-
const result = await mockWebClient.executeTransaction(accountId, request);
|
|
255
|
-
return {
|
|
256
|
-
result,
|
|
257
|
-
prove: async (options) => {
|
|
258
|
-
const proof = options?.prover === undefined
|
|
259
|
-
? await mockWebClient.proveTransaction(result)
|
|
260
|
-
: await mockWebClient.proveTransaction(result, options.prover);
|
|
261
|
-
return {
|
|
262
|
-
proof,
|
|
263
|
-
result,
|
|
264
|
-
submit: async () => {
|
|
265
|
-
const blockNumber = await mockWebClient.submitProvenTransaction(proof, result);
|
|
266
|
-
return {
|
|
267
|
-
blockNumber,
|
|
268
|
-
result,
|
|
269
|
-
apply: () => mockWebClient.applyTransaction(result, blockNumber),
|
|
270
|
-
};
|
|
271
|
-
},
|
|
272
|
-
};
|
|
273
|
-
},
|
|
274
|
-
};
|
|
275
|
-
});
|
|
276
|
-
});
|
|
277
|
-
describe('constructor', () => {
|
|
278
|
-
it('should create Multisig with account', () => {
|
|
279
|
-
const config = {
|
|
280
|
-
threshold: 2,
|
|
281
|
-
signerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
|
|
282
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
283
|
-
};
|
|
284
|
-
const multisig = createTestMultisig(config);
|
|
285
|
-
expect(multisig.threshold).toBe(2);
|
|
286
|
-
expect(multisig.signerCommitments).toEqual(config.signerCommitments);
|
|
287
|
-
expect(multisig.guardianCommitment).toBe(config.guardianCommitment);
|
|
288
|
-
expect(multisig.account).toBe(mockAccount);
|
|
289
|
-
});
|
|
290
|
-
it('should create Multisig with explicit accountId override', () => {
|
|
291
|
-
const config = {
|
|
292
|
-
threshold: 2,
|
|
293
|
-
signerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
|
|
294
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
295
|
-
};
|
|
296
|
-
const accountId = '0x' + 'd'.repeat(30);
|
|
297
|
-
const multisig = createTestMultisig(config, mockSigner, accountId);
|
|
298
|
-
expect(multisig.account).toBe(mockAccount);
|
|
299
|
-
expect(multisig.accountId).toBe(accountId);
|
|
300
|
-
});
|
|
301
|
-
it('should reject a missing Miden RPC endpoint immediately', () => {
|
|
302
|
-
const config = {
|
|
303
|
-
threshold: 2,
|
|
304
|
-
signerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
|
|
305
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
306
|
-
};
|
|
307
|
-
expect(() => new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient, undefined, undefined)).toThrow('missing required configuration: midenRpcEndpoint');
|
|
308
|
-
});
|
|
309
|
-
});
|
|
310
|
-
describe('accountId', () => {
|
|
311
|
-
it('should return account ID from account', () => {
|
|
312
|
-
const config = {
|
|
313
|
-
threshold: 1,
|
|
314
|
-
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
315
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
316
|
-
};
|
|
317
|
-
const multisig = createTestMultisig(config);
|
|
318
|
-
expect(multisig.accountId).toBe('0x' + 'a'.repeat(30));
|
|
319
|
-
});
|
|
320
|
-
it('should return provided account ID when constructor override is set', () => {
|
|
321
|
-
const config = {
|
|
322
|
-
threshold: 1,
|
|
323
|
-
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
324
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
325
|
-
};
|
|
326
|
-
const accountId = '0x' + 'e'.repeat(30);
|
|
327
|
-
const multisig = createTestMultisig(config, mockSigner, accountId);
|
|
328
|
-
expect(multisig.accountId).toBe(accountId);
|
|
329
|
-
});
|
|
330
|
-
});
|
|
331
|
-
describe('history (issue #413)', () => {
|
|
332
|
-
it('delegates to the guardian client with the account id and options', async () => {
|
|
333
|
-
const config = {
|
|
334
|
-
threshold: 1,
|
|
335
|
-
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
336
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
337
|
-
};
|
|
338
|
-
const multisig = createTestMultisig(config);
|
|
339
|
-
const page = {
|
|
340
|
-
entries: [
|
|
341
|
-
{
|
|
342
|
-
nonce: 3,
|
|
343
|
-
status: 'canonical',
|
|
344
|
-
timestamp: '2026-08-01T12:00:03Z',
|
|
345
|
-
newCommitment: '0x' + 'b'.repeat(64),
|
|
346
|
-
inputNotes: [],
|
|
347
|
-
outputNotes: [],
|
|
348
|
-
decodeWarnings: [],
|
|
349
|
-
},
|
|
350
|
-
],
|
|
351
|
-
nextCursor: 'cursor-token',
|
|
352
|
-
};
|
|
353
|
-
const spy = vi.spyOn(guardian, 'getDeltaHistory').mockResolvedValue(page);
|
|
354
|
-
const result = await multisig.deltaHistory({ limit: 5, cursor: 'prev' });
|
|
355
|
-
expect(result).toBe(page);
|
|
356
|
-
expect(spy).toHaveBeenCalledWith('0x' + 'a'.repeat(30), { limit: 5, cursor: 'prev' });
|
|
357
|
-
});
|
|
358
|
-
});
|
|
359
|
-
describe('signerCommitment', () => {
|
|
360
|
-
it('should return signer commitment', () => {
|
|
361
|
-
const config = {
|
|
362
|
-
threshold: 1,
|
|
363
|
-
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
364
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
365
|
-
};
|
|
366
|
-
const multisig = createTestMultisig(config);
|
|
367
|
-
expect(multisig.signerCommitment).toBe(mockSigner.commitment);
|
|
368
|
-
});
|
|
369
|
-
});
|
|
370
|
-
describe('getSignerPublicKeyCommitments (issue #306)', () => {
|
|
371
|
-
const config = {
|
|
372
|
-
threshold: 1,
|
|
373
|
-
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
374
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
375
|
-
};
|
|
376
|
-
it('reads commitments from the store-backed account', async () => {
|
|
377
|
-
const storeAccount = mockedAccount('0x' + 'b'.repeat(64), 1);
|
|
378
|
-
mockWebClient.getAccount.mockResolvedValueOnce(storeAccount);
|
|
379
|
-
const expected = ['0x' + '1'.repeat(64), '0x' + '2'.repeat(64)];
|
|
380
|
-
mockGetSignerCommitments.mockReturnValueOnce(expected);
|
|
381
|
-
const multisig = createTestMultisig(config);
|
|
382
|
-
const commitments = await multisig.getSignerPublicKeyCommitments();
|
|
383
|
-
expect(commitments).toEqual(expected);
|
|
384
|
-
expect(mockGetSignerCommitments).toHaveBeenCalledWith(storeAccount);
|
|
385
|
-
});
|
|
386
|
-
it('falls back to the account snapshot when the store has no record', async () => {
|
|
387
|
-
mockWebClient.getAccount.mockResolvedValueOnce(null);
|
|
388
|
-
mockGetSignerCommitments.mockReturnValueOnce(['0x' + '3'.repeat(64)]);
|
|
389
|
-
const multisig = createTestMultisig(config);
|
|
390
|
-
await multisig.getSignerPublicKeyCommitments();
|
|
391
|
-
expect(mockGetSignerCommitments).toHaveBeenCalledWith(mockAccount);
|
|
392
|
-
});
|
|
393
|
-
});
|
|
394
|
-
describe('getGuardianPublicKeyCommitment (issue #306)', () => {
|
|
395
|
-
it('reads the guardian commitment from the store-backed account', async () => {
|
|
396
|
-
const storeAccount = mockedAccount('0x' + 'b'.repeat(64), 1);
|
|
397
|
-
mockWebClient.getAccount.mockResolvedValueOnce(storeAccount);
|
|
398
|
-
mockGetGuardianCommitment.mockReturnValueOnce('0x' + '4'.repeat(64));
|
|
399
|
-
const multisig = createTestMultisig({
|
|
400
|
-
threshold: 1,
|
|
401
|
-
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
402
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
403
|
-
});
|
|
404
|
-
const commitment = await multisig.getGuardianPublicKeyCommitment();
|
|
405
|
-
expect(commitment).toBe('0x' + '4'.repeat(64));
|
|
406
|
-
expect(mockGetGuardianCommitment).toHaveBeenCalledWith(storeAccount);
|
|
407
|
-
});
|
|
408
|
-
});
|
|
409
|
-
describe('fetchState', () => {
|
|
410
|
-
it('should fetch account state from GUARDIAN', async () => {
|
|
411
|
-
const config = {
|
|
412
|
-
threshold: 1,
|
|
413
|
-
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
414
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
415
|
-
};
|
|
416
|
-
const multisig = createTestMultisig(config);
|
|
417
|
-
mockFetch.mockResolvedValueOnce({
|
|
418
|
-
ok: true,
|
|
419
|
-
json: async () => ({
|
|
420
|
-
account_id: '0x' + 'a'.repeat(30),
|
|
421
|
-
commitment: '0x' + 'b'.repeat(64),
|
|
422
|
-
state_json: { data: 'base64state' },
|
|
423
|
-
created_at: '2024-01-01T00:00:00Z',
|
|
424
|
-
updated_at: '2024-01-02T00:00:00Z',
|
|
425
|
-
}),
|
|
426
|
-
});
|
|
427
|
-
const state = await multisig.fetchState();
|
|
428
|
-
expect(state.accountId).toBe('0x' + 'a'.repeat(30));
|
|
429
|
-
expect(state.commitment).toBe('0x' + 'b'.repeat(64));
|
|
430
|
-
expect(state.stateDataBase64).toBe('base64state');
|
|
431
|
-
});
|
|
432
|
-
});
|
|
433
|
-
describe('syncState', () => {
|
|
434
|
-
it('should overwrite local state when account is missing locally', async () => {
|
|
435
|
-
const config = {
|
|
436
|
-
threshold: 1,
|
|
437
|
-
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
438
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
439
|
-
};
|
|
440
|
-
const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient, undefined, 'https://rpc.devnet.miden.io');
|
|
441
|
-
mockWebClient.getAccount.mockResolvedValueOnce(null);
|
|
442
|
-
mockFetch.mockResolvedValueOnce({
|
|
443
|
-
ok: true,
|
|
444
|
-
json: async () => ({
|
|
445
|
-
account_id: multisig.accountId,
|
|
446
|
-
commitment: '0x' + 'b'.repeat(64),
|
|
447
|
-
state_json: { data: 'AQID' },
|
|
448
|
-
created_at: '2024-01-01T00:00:00Z',
|
|
449
|
-
updated_at: '2024-01-02T00:00:00Z',
|
|
450
|
-
}),
|
|
451
|
-
});
|
|
452
|
-
await multisig.syncState();
|
|
453
|
-
expect(mockWebClient.newAccount).toHaveBeenCalledTimes(1);
|
|
454
|
-
expect(mockRpcGetAccountDetails).toHaveBeenCalledTimes(1);
|
|
455
|
-
});
|
|
456
|
-
it('should overwrite local state when incoming commitment matches on-chain commitment', async () => {
|
|
457
|
-
const config = {
|
|
458
|
-
threshold: 1,
|
|
459
|
-
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
460
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
461
|
-
};
|
|
462
|
-
const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient, undefined, 'https://rpc.devnet.miden.io');
|
|
463
|
-
mockWebClient.getAccount.mockResolvedValueOnce(mockedAccount('0x' + 'a'.repeat(64), 0));
|
|
464
|
-
mockRpcGetAccountDetails.mockResolvedValueOnce({
|
|
465
|
-
commitment: () => ({
|
|
466
|
-
toHex: () => '0x' + 'b'.repeat(64),
|
|
467
|
-
}),
|
|
468
|
-
});
|
|
469
|
-
mockFetch.mockResolvedValueOnce({
|
|
470
|
-
ok: true,
|
|
471
|
-
json: async () => ({
|
|
472
|
-
account_id: multisig.accountId,
|
|
473
|
-
commitment: '0x' + 'b'.repeat(64),
|
|
474
|
-
state_json: { data: 'AQID' },
|
|
475
|
-
created_at: '2024-01-01T00:00:00Z',
|
|
476
|
-
updated_at: '2024-01-02T00:00:00Z',
|
|
477
|
-
}),
|
|
478
|
-
});
|
|
479
|
-
await multisig.syncState();
|
|
480
|
-
expect(mockWebClient.newAccount).toHaveBeenCalledTimes(1);
|
|
481
|
-
});
|
|
482
|
-
it('refreshes multisig config from synced account state', async () => {
|
|
483
|
-
const config = {
|
|
484
|
-
threshold: 1,
|
|
485
|
-
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
486
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
487
|
-
};
|
|
488
|
-
const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient, undefined, 'https://rpc.devnet.miden.io');
|
|
489
|
-
mockWebClient.getAccount.mockResolvedValueOnce(mockedAccount('0x' + 'b'.repeat(64), 0));
|
|
490
|
-
mockFetch.mockResolvedValueOnce({
|
|
491
|
-
ok: true,
|
|
492
|
-
json: async () => ({
|
|
493
|
-
account_id: multisig.accountId,
|
|
494
|
-
commitment: '0x' + 'b'.repeat(64),
|
|
495
|
-
state_json: { data: 'AQID' },
|
|
496
|
-
created_at: '2024-01-01T00:00:00Z',
|
|
497
|
-
updated_at: '2024-01-02T00:00:00Z',
|
|
498
|
-
}),
|
|
499
|
-
});
|
|
500
|
-
mockDetectConfig.mockReturnValueOnce({
|
|
501
|
-
threshold: 2,
|
|
502
|
-
numSigners: 2,
|
|
503
|
-
signerCommitments: ['0x' + '1'.repeat(64), '0x' + '2'.repeat(64)],
|
|
504
|
-
guardianCommitment: '0x' + 'd'.repeat(64),
|
|
505
|
-
vaultBalances: [],
|
|
506
|
-
procedureThresholds: new Map(),
|
|
507
|
-
});
|
|
508
|
-
await multisig.syncState();
|
|
509
|
-
expect(multisig.threshold).toBe(2);
|
|
510
|
-
expect(multisig.signerCommitments).toEqual([
|
|
511
|
-
'0x' + '1'.repeat(64),
|
|
512
|
-
'0x' + '2'.repeat(64),
|
|
513
|
-
]);
|
|
514
|
-
expect(multisig.guardianCommitment).toBe('0x' + 'd'.repeat(64));
|
|
515
|
-
expect(mockWebClient.newAccount).not.toHaveBeenCalled();
|
|
516
|
-
});
|
|
517
|
-
it('keeps the previous config when a refresh reads an incomplete signer set (issue #306 review)', async () => {
|
|
518
|
-
const config = {
|
|
519
|
-
threshold: 1,
|
|
520
|
-
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
521
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
522
|
-
};
|
|
523
|
-
const multisig = createTestMultisig(config, mockSigner, '0x' + 'a'.repeat(30));
|
|
524
|
-
mockWebClient.getAccount.mockResolvedValueOnce(mockedAccount('0x' + 'b'.repeat(64), 0));
|
|
525
|
-
mockFetch.mockResolvedValueOnce({
|
|
526
|
-
ok: true,
|
|
527
|
-
json: async () => ({
|
|
528
|
-
account_id: multisig.accountId,
|
|
529
|
-
commitment: '0x' + 'b'.repeat(64),
|
|
530
|
-
state_json: { data: 'AQID' },
|
|
531
|
-
created_at: '2024-01-01T00:00:00Z',
|
|
532
|
-
updated_at: '2024-01-02T00:00:00Z',
|
|
533
|
-
}),
|
|
534
|
-
});
|
|
535
|
-
// Storage reports 3 signers but only 1 entry was readable: adopting
|
|
536
|
-
// this would let membership proposals rewrite the on-chain set without
|
|
537
|
-
// the omitted keys. The refresh must keep the previous config instead.
|
|
538
|
-
mockDetectConfig.mockReturnValueOnce({
|
|
539
|
-
threshold: 2,
|
|
540
|
-
numSigners: 3,
|
|
541
|
-
signerCommitments: ['0x' + '1'.repeat(64)],
|
|
542
|
-
guardianCommitment: '0x' + 'd'.repeat(64),
|
|
543
|
-
vaultBalances: [],
|
|
544
|
-
procedureThresholds: new Map(),
|
|
545
|
-
});
|
|
546
|
-
await multisig.syncState();
|
|
547
|
-
expect(multisig.threshold).toBe(1);
|
|
548
|
-
expect(multisig.signerCommitments).toEqual(['0x' + 'a'.repeat(64)]);
|
|
549
|
-
expect(multisig.guardianCommitment).toBe('0x' + 'c'.repeat(64));
|
|
550
|
-
});
|
|
551
|
-
it('should overwrite local state when account is not found on-chain', async () => {
|
|
552
|
-
const config = {
|
|
553
|
-
threshold: 1,
|
|
554
|
-
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
555
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
556
|
-
};
|
|
557
|
-
const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient, undefined, 'https://rpc.devnet.miden.io');
|
|
558
|
-
mockWebClient.getAccount.mockResolvedValueOnce(mockedAccount('0x' + 'a'.repeat(64), 0));
|
|
559
|
-
mockRpcGetAccountDetails.mockRejectedValueOnce(new Error('No account header record found for given ID'));
|
|
560
|
-
mockFetch.mockResolvedValueOnce({
|
|
561
|
-
ok: true,
|
|
562
|
-
json: async () => ({
|
|
563
|
-
account_id: multisig.accountId,
|
|
564
|
-
commitment: '0x' + 'b'.repeat(64),
|
|
565
|
-
state_json: { data: 'AQID' },
|
|
566
|
-
created_at: '2024-01-01T00:00:00Z',
|
|
567
|
-
updated_at: '2024-01-02T00:00:00Z',
|
|
568
|
-
}),
|
|
569
|
-
});
|
|
570
|
-
await multisig.syncState();
|
|
571
|
-
expect(mockWebClient.newAccount).toHaveBeenCalledTimes(1);
|
|
572
|
-
});
|
|
573
|
-
it('should throw when incoming commitment does not match on-chain commitment', async () => {
|
|
574
|
-
const config = {
|
|
575
|
-
threshold: 1,
|
|
576
|
-
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
577
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
578
|
-
};
|
|
579
|
-
const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient, undefined, 'https://rpc.devnet.miden.io');
|
|
580
|
-
mockWebClient.getAccount.mockResolvedValueOnce(mockedAccount('0x' + 'a'.repeat(64), 0));
|
|
581
|
-
mockAccountDeserialize.mockReturnValueOnce(mockedAccount('0x' + 'b'.repeat(64), 1));
|
|
582
|
-
mockRpcGetAccountDetails.mockResolvedValueOnce({
|
|
583
|
-
commitment: () => ({
|
|
584
|
-
toHex: () => '0x' + 'c'.repeat(64),
|
|
585
|
-
}),
|
|
586
|
-
});
|
|
587
|
-
mockFetch.mockResolvedValueOnce({
|
|
588
|
-
ok: true,
|
|
589
|
-
json: async () => ({
|
|
590
|
-
account_id: multisig.accountId,
|
|
591
|
-
commitment: '0x' + 'b'.repeat(64),
|
|
592
|
-
state_json: { data: 'AQID' },
|
|
593
|
-
created_at: '2024-01-01T00:00:00Z',
|
|
594
|
-
updated_at: '2024-01-02T00:00:00Z',
|
|
595
|
-
}),
|
|
596
|
-
});
|
|
597
|
-
await expect(multisig.syncState()).rejects.toThrow('Refusing to overwrite local state');
|
|
598
|
-
expect(mockWebClient.newAccount).not.toHaveBeenCalled();
|
|
599
|
-
});
|
|
600
|
-
it('keeps local state and refreshes config from it when GUARDIAN nonce is behind local', async () => {
|
|
601
|
-
const config = {
|
|
602
|
-
threshold: 1,
|
|
603
|
-
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
604
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
605
|
-
};
|
|
606
|
-
const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient, undefined, 'https://rpc.devnet.miden.io');
|
|
607
|
-
const localAccount = mockedAccount('0x' + 'a'.repeat(64), 3);
|
|
608
|
-
mockWebClient.getAccount.mockResolvedValueOnce(localAccount);
|
|
609
|
-
mockAccountDeserialize.mockReturnValueOnce(mockedAccount('0x' + 'b'.repeat(64), 2));
|
|
610
|
-
mockFetch.mockResolvedValueOnce({
|
|
611
|
-
ok: true,
|
|
612
|
-
json: async () => ({
|
|
613
|
-
account_id: multisig.accountId,
|
|
614
|
-
commitment: '0x' + 'b'.repeat(64),
|
|
615
|
-
state_json: { data: 'AQID' },
|
|
616
|
-
created_at: '2024-01-01T00:00:00Z',
|
|
617
|
-
updated_at: '2024-01-02T00:00:00Z',
|
|
618
|
-
}),
|
|
619
|
-
});
|
|
620
|
-
mockDetectConfig.mockReturnValueOnce({
|
|
621
|
-
threshold: 2,
|
|
622
|
-
numSigners: 2,
|
|
623
|
-
signerCommitments: ['0x' + '1'.repeat(64), '0x' + '2'.repeat(64)],
|
|
624
|
-
guardianEnabled: true,
|
|
625
|
-
guardianCommitment: '0x' + 'd'.repeat(64),
|
|
626
|
-
vaultBalances: [],
|
|
627
|
-
procedureThresholds: new Map(),
|
|
628
|
-
});
|
|
629
|
-
// GUARDIAN behind local (nonce 2 < 3): no throw, local kept, no overwrite,
|
|
630
|
-
// and the decision needs no on-chain round-trip.
|
|
631
|
-
await expect(multisig.syncState()).resolves.toBeDefined();
|
|
632
|
-
expect(mockWebClient.newAccount).not.toHaveBeenCalled();
|
|
633
|
-
expect(mockRpcGetAccountDetails).not.toHaveBeenCalled();
|
|
634
|
-
// Config refreshed from the authoritative local account (UI unfreezes).
|
|
635
|
-
expect(multisig.account).toBe(localAccount);
|
|
636
|
-
expect(multisig.threshold).toBe(2);
|
|
637
|
-
});
|
|
638
|
-
it('should throw when incoming state nonce equals local nonce but commitment differs', async () => {
|
|
639
|
-
const config = {
|
|
640
|
-
threshold: 1,
|
|
641
|
-
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
642
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
643
|
-
};
|
|
644
|
-
const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient, undefined, 'https://rpc.devnet.miden.io');
|
|
645
|
-
mockWebClient.getAccount.mockResolvedValueOnce(mockedAccount('0x' + 'a'.repeat(64), 2));
|
|
646
|
-
mockAccountDeserialize.mockReturnValueOnce(mockedAccount('0x' + 'b'.repeat(64), 2));
|
|
647
|
-
mockFetch.mockResolvedValueOnce({
|
|
648
|
-
ok: true,
|
|
649
|
-
json: async () => ({
|
|
650
|
-
account_id: multisig.accountId,
|
|
651
|
-
commitment: '0x' + 'b'.repeat(64),
|
|
652
|
-
state_json: { data: 'AQID' },
|
|
653
|
-
created_at: '2024-01-01T00:00:00Z',
|
|
654
|
-
updated_at: '2024-01-02T00:00:00Z',
|
|
655
|
-
}),
|
|
656
|
-
});
|
|
657
|
-
await expect(multisig.syncState()).rejects.toThrow('incoming nonce 2 equals local nonce 2 but commitments differ');
|
|
658
|
-
expect(mockWebClient.newAccount).not.toHaveBeenCalled();
|
|
659
|
-
});
|
|
660
|
-
it('unfreezes Multisig.account after execute when GUARDIAN still lags by one nonce (regression, #343)', async () => {
|
|
661
|
-
const config = {
|
|
662
|
-
threshold: 1,
|
|
663
|
-
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
664
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
665
|
-
};
|
|
666
|
-
const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient, undefined, 'https://rpc.devnet.miden.io');
|
|
667
|
-
// Post-execute: local advanced to nonce 1, GUARDIAN still reports nonce 0
|
|
668
|
-
// (candidate not canonicalized yet). Before the fix this threw and left
|
|
669
|
-
// Multisig.account frozen at the pre-execute snapshot.
|
|
670
|
-
const localAccount = mockedAccount('0x' + 'a'.repeat(64), 1);
|
|
671
|
-
mockWebClient.getAccount.mockResolvedValueOnce(localAccount);
|
|
672
|
-
mockAccountDeserialize.mockReturnValueOnce(mockedAccount('0x' + 'b'.repeat(64), 0));
|
|
673
|
-
mockFetch.mockResolvedValueOnce({
|
|
674
|
-
ok: true,
|
|
675
|
-
json: async () => ({
|
|
676
|
-
account_id: multisig.accountId,
|
|
677
|
-
commitment: '0x' + 'b'.repeat(64),
|
|
678
|
-
state_json: { data: 'AQID' },
|
|
679
|
-
created_at: '2024-01-01T00:00:00Z',
|
|
680
|
-
updated_at: '2024-01-02T00:00:00Z',
|
|
681
|
-
}),
|
|
682
|
-
});
|
|
683
|
-
await expect(multisig.syncState()).resolves.toBeDefined();
|
|
684
|
-
expect(mockWebClient.newAccount).not.toHaveBeenCalled();
|
|
685
|
-
expect(multisig.account).toBe(localAccount);
|
|
686
|
-
});
|
|
687
|
-
});
|
|
688
|
-
describe('verifyStateCommitment', () => {
|
|
689
|
-
it('should pass when local and on-chain commitments match', async () => {
|
|
690
|
-
const config = {
|
|
691
|
-
threshold: 1,
|
|
692
|
-
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
693
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
694
|
-
};
|
|
695
|
-
mockWebClient.getAccount.mockResolvedValueOnce(mockedAccount('0x' + 'b'.repeat(64), 0));
|
|
696
|
-
const multisigWithRpc = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient, undefined, 'https://rpc.devnet.miden.io');
|
|
697
|
-
await expect(multisigWithRpc.verifyStateCommitment()).resolves.toMatchObject({
|
|
698
|
-
accountId: multisigWithRpc.accountId,
|
|
699
|
-
});
|
|
700
|
-
});
|
|
701
|
-
it('should throw when local account state is missing', async () => {
|
|
702
|
-
const config = {
|
|
703
|
-
threshold: 1,
|
|
704
|
-
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
705
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
706
|
-
};
|
|
707
|
-
mockWebClient.getAccount.mockResolvedValueOnce(null);
|
|
708
|
-
const multisigWithRpc = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient, undefined, 'https://rpc.devnet.miden.io');
|
|
709
|
-
await expect(multisigWithRpc.verifyStateCommitment()).rejects.toThrow('Local account state not found');
|
|
710
|
-
});
|
|
711
|
-
it('should throw when local and on-chain commitments differ', async () => {
|
|
712
|
-
const config = {
|
|
713
|
-
threshold: 1,
|
|
714
|
-
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
715
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
716
|
-
};
|
|
717
|
-
mockWebClient.getAccount.mockResolvedValueOnce(mockedAccount('0x' + 'f'.repeat(64), 0));
|
|
718
|
-
mockRpcGetAccountDetails.mockResolvedValueOnce({
|
|
719
|
-
commitment: () => ({
|
|
720
|
-
toHex: () => '0x' + 'b'.repeat(64),
|
|
721
|
-
}),
|
|
722
|
-
});
|
|
723
|
-
const multisigWithRpc = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient, undefined, 'https://rpc.devnet.miden.io');
|
|
724
|
-
await expect(multisigWithRpc.verifyStateCommitment()).rejects.toThrow('Local account commitment does not match on-chain commitment');
|
|
725
|
-
});
|
|
726
|
-
});
|
|
727
|
-
describe('registerOnGuardian', () => {
|
|
728
|
-
it('should register account on GUARDIAN', async () => {
|
|
729
|
-
const config = {
|
|
730
|
-
threshold: 1,
|
|
731
|
-
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
732
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
733
|
-
};
|
|
734
|
-
const multisig = createTestMultisig(config);
|
|
735
|
-
mockFetch.mockResolvedValueOnce({
|
|
736
|
-
ok: true,
|
|
737
|
-
json: async () => ({
|
|
738
|
-
success: true,
|
|
739
|
-
message: 'Account configured',
|
|
740
|
-
ack_pubkey: '0x' + 'd'.repeat(64),
|
|
741
|
-
}),
|
|
742
|
-
});
|
|
743
|
-
await expect(multisig.registerOnGuardian()).resolves.toBeUndefined();
|
|
744
|
-
});
|
|
745
|
-
it('should register ECDSA accounts with MidenEcdsa auth', async () => {
|
|
746
|
-
const config = {
|
|
747
|
-
threshold: 1,
|
|
748
|
-
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
749
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
750
|
-
};
|
|
751
|
-
const ecdsaSigner = {
|
|
752
|
-
...mockSigner,
|
|
753
|
-
publicKey: '0x' + '2'.repeat(66),
|
|
754
|
-
scheme: 'ecdsa',
|
|
755
|
-
};
|
|
756
|
-
guardian.setSigner(ecdsaSigner);
|
|
757
|
-
const multisig = createTestMultisig(config, ecdsaSigner);
|
|
758
|
-
mockFetch.mockResolvedValueOnce({
|
|
759
|
-
ok: true,
|
|
760
|
-
json: async () => ({
|
|
761
|
-
success: true,
|
|
762
|
-
message: 'Account configured',
|
|
763
|
-
ack_pubkey: '0x' + 'd'.repeat(66),
|
|
764
|
-
}),
|
|
765
|
-
});
|
|
766
|
-
await expect(multisig.registerOnGuardian()).resolves.toBeUndefined();
|
|
767
|
-
const [, requestInit] = mockFetch.mock.calls[0];
|
|
768
|
-
const body = JSON.parse(String(requestInit.body));
|
|
769
|
-
expect(body.auth).toEqual({
|
|
770
|
-
MidenEcdsa: {
|
|
771
|
-
cosigner_commitments: config.signerCommitments,
|
|
772
|
-
},
|
|
773
|
-
});
|
|
774
|
-
});
|
|
775
|
-
it('should accept explicit initial state base64', async () => {
|
|
776
|
-
const config = {
|
|
777
|
-
threshold: 1,
|
|
778
|
-
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
779
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
780
|
-
};
|
|
781
|
-
const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient, '0x' + 'e'.repeat(30), MIDEN_RPC_ENDPOINT);
|
|
782
|
-
mockFetch.mockResolvedValueOnce({
|
|
783
|
-
ok: true,
|
|
784
|
-
json: async () => ({
|
|
785
|
-
success: true,
|
|
786
|
-
message: 'Account configured',
|
|
787
|
-
}),
|
|
788
|
-
});
|
|
789
|
-
await expect(multisig.registerOnGuardian('base64initialstate')).resolves.toBeUndefined();
|
|
790
|
-
});
|
|
791
|
-
it('should throw on GUARDIAN registration failure', async () => {
|
|
792
|
-
const config = {
|
|
793
|
-
threshold: 1,
|
|
794
|
-
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
795
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
796
|
-
};
|
|
797
|
-
const multisig = createTestMultisig(config);
|
|
798
|
-
mockFetch.mockResolvedValueOnce({
|
|
799
|
-
ok: true,
|
|
800
|
-
json: async () => ({
|
|
801
|
-
success: false,
|
|
802
|
-
message: 'Account already exists',
|
|
803
|
-
}),
|
|
804
|
-
});
|
|
805
|
-
await expect(multisig.registerOnGuardian()).rejects.toThrow('Failed to register on GUARDIAN');
|
|
806
|
-
});
|
|
807
|
-
});
|
|
808
|
-
describe('syncProposals', () => {
|
|
809
|
-
it('should sync proposals from GUARDIAN', async () => {
|
|
810
|
-
const config = {
|
|
811
|
-
threshold: 2,
|
|
812
|
-
signerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
|
|
813
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
814
|
-
};
|
|
815
|
-
const multisig = createTestMultisig(config);
|
|
816
|
-
const mockProposals = [
|
|
817
|
-
{
|
|
818
|
-
account_id: '0x' + 'a'.repeat(30),
|
|
819
|
-
nonce: 1,
|
|
820
|
-
prev_commitment: '0x' + 'b'.repeat(64),
|
|
821
|
-
delta_payload: {
|
|
822
|
-
tx_summary: { data: 'AQID' },
|
|
823
|
-
signatures: [],
|
|
824
|
-
metadata: {
|
|
825
|
-
proposal_type: 'add_signer',
|
|
826
|
-
chain_anchor: MOCK_CHAIN_ANCHOR_B64,
|
|
827
|
-
target_threshold: 1,
|
|
828
|
-
signer_commitments: ['0x' + 'a'.repeat(64)],
|
|
829
|
-
description: '',
|
|
830
|
-
},
|
|
831
|
-
},
|
|
832
|
-
status: {
|
|
833
|
-
status: 'pending',
|
|
834
|
-
timestamp: '2024-01-01T00:00:00Z',
|
|
835
|
-
proposer_id: '0x' + 'c'.repeat(64),
|
|
836
|
-
cosigner_sigs: [
|
|
837
|
-
{
|
|
838
|
-
signer_id: '0x' + 'a'.repeat(64),
|
|
839
|
-
signature: { scheme: 'falcon', signature: '0x' + 'e'.repeat(128) },
|
|
840
|
-
timestamp: '2024-01-01T00:00:00Z',
|
|
841
|
-
},
|
|
842
|
-
],
|
|
843
|
-
},
|
|
844
|
-
},
|
|
845
|
-
];
|
|
846
|
-
mockFetch.mockResolvedValueOnce({
|
|
847
|
-
ok: true,
|
|
848
|
-
json: async () => ({ proposals: mockProposals }),
|
|
849
|
-
});
|
|
850
|
-
const proposals = await multisig.syncProposals();
|
|
851
|
-
expect(proposals.length).toBe(1);
|
|
852
|
-
expect(proposals[0].nonce).toBe(1);
|
|
853
|
-
expect(proposals[0].status).toBe('pending');
|
|
854
|
-
});
|
|
855
|
-
it('should return ready status when enough signatures', async () => {
|
|
856
|
-
const config = {
|
|
857
|
-
threshold: 1, // Only 1 signature needed
|
|
858
|
-
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
859
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
860
|
-
};
|
|
861
|
-
const multisig = createTestMultisig(config);
|
|
862
|
-
const mockProposals = [
|
|
863
|
-
{
|
|
864
|
-
account_id: '0x' + 'a'.repeat(30),
|
|
865
|
-
nonce: 1,
|
|
866
|
-
prev_commitment: '0x' + 'b'.repeat(64),
|
|
867
|
-
delta_payload: {
|
|
868
|
-
tx_summary: { data: 'AQID' },
|
|
869
|
-
signatures: [],
|
|
870
|
-
metadata: {
|
|
871
|
-
proposal_type: 'add_signer',
|
|
872
|
-
chain_anchor: MOCK_CHAIN_ANCHOR_B64,
|
|
873
|
-
target_threshold: 1,
|
|
874
|
-
signer_commitments: ['0x' + 'a'.repeat(64)],
|
|
875
|
-
description: '',
|
|
876
|
-
},
|
|
877
|
-
},
|
|
878
|
-
status: {
|
|
879
|
-
status: 'pending',
|
|
880
|
-
timestamp: '2024-01-01T00:00:00Z',
|
|
881
|
-
proposer_id: '0x' + 'c'.repeat(64),
|
|
882
|
-
cosigner_sigs: [
|
|
883
|
-
{
|
|
884
|
-
signer_id: '0x' + 'a'.repeat(64),
|
|
885
|
-
signature: { scheme: 'falcon', signature: '0x' + 'e'.repeat(128) },
|
|
886
|
-
timestamp: '2024-01-01T00:00:00Z',
|
|
887
|
-
},
|
|
888
|
-
],
|
|
889
|
-
},
|
|
890
|
-
},
|
|
891
|
-
];
|
|
892
|
-
mockFetch.mockResolvedValueOnce({
|
|
893
|
-
ok: true,
|
|
894
|
-
json: async () => ({ proposals: mockProposals }),
|
|
895
|
-
});
|
|
896
|
-
const proposals = await multisig.syncProposals();
|
|
897
|
-
expect(proposals[0].status).toBe('ready');
|
|
898
|
-
});
|
|
899
|
-
it('should reject proposals whose metadata does not match tx_summary', async () => {
|
|
900
|
-
const config = {
|
|
901
|
-
threshold: 1,
|
|
902
|
-
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
903
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
904
|
-
};
|
|
905
|
-
const multisig = createTestMultisig(config);
|
|
906
|
-
mockFetch.mockResolvedValueOnce({
|
|
907
|
-
ok: true,
|
|
908
|
-
json: async () => ({
|
|
909
|
-
proposals: [
|
|
910
|
-
{
|
|
911
|
-
account_id: '0x' + 'a'.repeat(30),
|
|
912
|
-
nonce: 1,
|
|
913
|
-
prev_commitment: '0x' + 'b'.repeat(64),
|
|
914
|
-
delta_payload: {
|
|
915
|
-
tx_summary: { data: 'AQID' },
|
|
916
|
-
signatures: [],
|
|
917
|
-
metadata: {
|
|
918
|
-
proposal_type: 'add_signer',
|
|
919
|
-
chain_anchor: MOCK_CHAIN_ANCHOR_B64,
|
|
920
|
-
target_threshold: 1,
|
|
921
|
-
signer_commitments: ['0x' + 'a'.repeat(64)],
|
|
922
|
-
description: '',
|
|
923
|
-
},
|
|
924
|
-
},
|
|
925
|
-
status: {
|
|
926
|
-
status: 'pending',
|
|
927
|
-
timestamp: '2024-01-01T00:00:00Z',
|
|
928
|
-
proposer_id: '0x' + 'c'.repeat(64),
|
|
929
|
-
cosigner_sigs: [],
|
|
930
|
-
},
|
|
931
|
-
},
|
|
932
|
-
],
|
|
933
|
-
}),
|
|
934
|
-
});
|
|
935
|
-
vi.mocked(executeForSummaryAt).mockResolvedValueOnce({
|
|
936
|
-
toCommitment: () => ({
|
|
937
|
-
toHex: () => '0x' + 'f'.repeat(64),
|
|
938
|
-
}),
|
|
939
|
-
});
|
|
940
|
-
await expect(multisig.syncProposals()).rejects.toThrow('Invalid proposal: metadata does not match tx_summary');
|
|
941
|
-
});
|
|
942
|
-
/// A structurally valid anchor pinned to the wrong block must be rejected
|
|
943
|
-
/// before anything executes against it: its commitment disagrees with the
|
|
944
|
-
/// block commitment signed into the tx_summary.
|
|
945
|
-
it('should reject a proposal whose chain anchor does not match the summary block commitment', async () => {
|
|
946
|
-
const config = {
|
|
947
|
-
threshold: 1,
|
|
948
|
-
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
949
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
950
|
-
};
|
|
951
|
-
const multisig = createTestMultisig(config);
|
|
952
|
-
mockFetch.mockResolvedValueOnce({
|
|
953
|
-
ok: true,
|
|
954
|
-
json: async () => ({
|
|
955
|
-
proposals: [
|
|
956
|
-
{
|
|
957
|
-
account_id: '0x' + 'a'.repeat(30),
|
|
958
|
-
nonce: 1,
|
|
959
|
-
prev_commitment: '0x' + 'b'.repeat(64),
|
|
960
|
-
delta_payload: {
|
|
961
|
-
tx_summary: { data: 'AQID' },
|
|
962
|
-
signatures: [],
|
|
963
|
-
metadata: {
|
|
964
|
-
proposal_type: 'add_signer',
|
|
965
|
-
chain_anchor: MOCK_CHAIN_ANCHOR_B64,
|
|
966
|
-
target_threshold: 1,
|
|
967
|
-
signer_commitments: ['0x' + 'a'.repeat(64)],
|
|
968
|
-
description: '',
|
|
969
|
-
},
|
|
970
|
-
},
|
|
971
|
-
status: {
|
|
972
|
-
status: 'pending',
|
|
973
|
-
timestamp: '2024-01-01T00:00:00Z',
|
|
974
|
-
proposer_id: '0x' + 'c'.repeat(64),
|
|
975
|
-
cosigner_sigs: [],
|
|
976
|
-
},
|
|
977
|
-
},
|
|
978
|
-
],
|
|
979
|
-
}),
|
|
980
|
-
});
|
|
981
|
-
// Deserializes fine, but pins a different block than the one bound into
|
|
982
|
-
// the summary (mock summary blockCommitment is 'b' * 64).
|
|
983
|
-
const freed = vi.fn();
|
|
984
|
-
vi.mocked(chainAnchorFromBase64).mockReturnValueOnce({
|
|
985
|
-
commitment: () => ({ toHex: () => '0x' + 'e'.repeat(64) }),
|
|
986
|
-
free: freed,
|
|
987
|
-
serialize: () => new Uint8Array([9, 9, 9]),
|
|
988
|
-
});
|
|
989
|
-
const reExecutionsBefore = vi.mocked(executeForSummaryAt).mock.calls.length;
|
|
990
|
-
await expect(multisig.syncProposals()).rejects.toThrow('chain anchor does not match the block commitment bound into the tx_summary');
|
|
991
|
-
expect(vi.mocked(executeForSummaryAt).mock.calls.length).toBe(reExecutionsBefore);
|
|
992
|
-
expect(freed).toHaveBeenCalledTimes(1);
|
|
993
|
-
});
|
|
994
|
-
it('should reject non-32-byte signer IDs from GUARDIAN proposals', async () => {
|
|
995
|
-
const config = {
|
|
996
|
-
threshold: 1,
|
|
997
|
-
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
998
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
999
|
-
};
|
|
1000
|
-
const multisig = createTestMultisig(config);
|
|
1001
|
-
const mockProposals = [
|
|
1002
|
-
{
|
|
1003
|
-
account_id: '0x' + 'a'.repeat(30),
|
|
1004
|
-
nonce: 1,
|
|
1005
|
-
prev_commitment: '0x' + 'b'.repeat(64),
|
|
1006
|
-
delta_payload: {
|
|
1007
|
-
tx_summary: { data: 'AQID' },
|
|
1008
|
-
signatures: [],
|
|
1009
|
-
metadata: {
|
|
1010
|
-
proposal_type: 'add_signer',
|
|
1011
|
-
chain_anchor: MOCK_CHAIN_ANCHOR_B64,
|
|
1012
|
-
target_threshold: 1,
|
|
1013
|
-
signer_commitments: ['0x' + 'a'.repeat(64)],
|
|
1014
|
-
description: '',
|
|
1015
|
-
},
|
|
1016
|
-
},
|
|
1017
|
-
status: {
|
|
1018
|
-
status: 'pending',
|
|
1019
|
-
timestamp: '2024-01-01T00:00:00Z',
|
|
1020
|
-
proposer_id: '0x' + 'c'.repeat(64),
|
|
1021
|
-
cosigner_sigs: [
|
|
1022
|
-
{
|
|
1023
|
-
signer_id: '0x1',
|
|
1024
|
-
signature: { scheme: 'falcon', signature: '0x' + 'e'.repeat(128) },
|
|
1025
|
-
timestamp: '2024-01-01T00:00:00Z',
|
|
1026
|
-
},
|
|
1027
|
-
],
|
|
1028
|
-
},
|
|
1029
|
-
},
|
|
1030
|
-
];
|
|
1031
|
-
mockFetch.mockResolvedValueOnce({
|
|
1032
|
-
ok: true,
|
|
1033
|
-
json: async () => ({ proposals: mockProposals }),
|
|
1034
|
-
});
|
|
1035
|
-
await expect(multisig.syncProposals()).rejects.toThrow('expected signerId as 32-byte hex');
|
|
1036
|
-
});
|
|
1037
|
-
it('should reject duplicate normalized signer IDs from GUARDIAN proposals', async () => {
|
|
1038
|
-
const config = {
|
|
1039
|
-
threshold: 2,
|
|
1040
|
-
signerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
|
|
1041
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
1042
|
-
};
|
|
1043
|
-
const multisig = createTestMultisig(config);
|
|
1044
|
-
const mockProposals = [
|
|
1045
|
-
{
|
|
1046
|
-
account_id: '0x' + 'a'.repeat(30),
|
|
1047
|
-
nonce: 1,
|
|
1048
|
-
prev_commitment: '0x' + 'b'.repeat(64),
|
|
1049
|
-
delta_payload: {
|
|
1050
|
-
tx_summary: { data: 'AQID' },
|
|
1051
|
-
signatures: [],
|
|
1052
|
-
metadata: {
|
|
1053
|
-
proposal_type: 'add_signer',
|
|
1054
|
-
chain_anchor: MOCK_CHAIN_ANCHOR_B64,
|
|
1055
|
-
target_threshold: 2,
|
|
1056
|
-
signer_commitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
|
|
1057
|
-
description: '',
|
|
1058
|
-
},
|
|
1059
|
-
},
|
|
1060
|
-
status: {
|
|
1061
|
-
status: 'pending',
|
|
1062
|
-
timestamp: '2024-01-01T00:00:00Z',
|
|
1063
|
-
proposer_id: '0x' + 'c'.repeat(64),
|
|
1064
|
-
cosigner_sigs: [
|
|
1065
|
-
{
|
|
1066
|
-
signer_id: '0x' + 'A'.repeat(64),
|
|
1067
|
-
signature: { scheme: 'falcon', signature: '0x' + 'e'.repeat(128) },
|
|
1068
|
-
timestamp: '2024-01-01T00:00:00Z',
|
|
1069
|
-
},
|
|
1070
|
-
{
|
|
1071
|
-
signer_id: '0x' + 'a'.repeat(64),
|
|
1072
|
-
signature: { scheme: 'falcon', signature: '0x' + 'f'.repeat(128) },
|
|
1073
|
-
timestamp: '2024-01-01T00:00:01Z',
|
|
1074
|
-
},
|
|
1075
|
-
],
|
|
1076
|
-
},
|
|
1077
|
-
},
|
|
1078
|
-
];
|
|
1079
|
-
mockFetch.mockResolvedValueOnce({
|
|
1080
|
-
ok: true,
|
|
1081
|
-
json: async () => ({ proposals: mockProposals }),
|
|
1082
|
-
});
|
|
1083
|
-
await expect(multisig.syncProposals()).rejects.toThrow('duplicate signatures for signer');
|
|
1084
|
-
});
|
|
1085
|
-
});
|
|
1086
|
-
describe('listProposals', () => {
|
|
1087
|
-
it('should return empty list initially', () => {
|
|
1088
|
-
const config = {
|
|
1089
|
-
threshold: 1,
|
|
1090
|
-
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
1091
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
1092
|
-
};
|
|
1093
|
-
const multisig = createTestMultisig(config);
|
|
1094
|
-
expect(multisig.listProposals()).toEqual([]);
|
|
1095
|
-
});
|
|
1096
|
-
});
|
|
1097
|
-
describe('createProposal', () => {
|
|
1098
|
-
it('should create a new proposal', async () => {
|
|
1099
|
-
const config = {
|
|
1100
|
-
threshold: 1,
|
|
1101
|
-
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
1102
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
1103
|
-
};
|
|
1104
|
-
const multisig = createTestMultisig(config);
|
|
1105
|
-
const mockDelta = {
|
|
1106
|
-
account_id: '0x' + 'a'.repeat(30),
|
|
1107
|
-
nonce: 1,
|
|
1108
|
-
prev_commitment: '0x' + 'b'.repeat(64),
|
|
1109
|
-
delta_payload: {
|
|
1110
|
-
tx_summary: { data: 'AQID' },
|
|
1111
|
-
signatures: [],
|
|
1112
|
-
},
|
|
1113
|
-
status: {
|
|
1114
|
-
status: 'pending',
|
|
1115
|
-
timestamp: '2024-01-01T00:00:00Z',
|
|
1116
|
-
proposer_id: '0x' + 'c'.repeat(64),
|
|
1117
|
-
cosigner_sigs: [],
|
|
1118
|
-
},
|
|
1119
|
-
};
|
|
1120
|
-
mockFetch.mockResolvedValueOnce({
|
|
1121
|
-
ok: true,
|
|
1122
|
-
json: async () => ({
|
|
1123
|
-
delta: mockDelta,
|
|
1124
|
-
commitment: '0x' + 'c'.repeat(64),
|
|
1125
|
-
}),
|
|
1126
|
-
});
|
|
1127
|
-
const proposal = await multisig.createProposal(1, 'AQID', {
|
|
1128
|
-
proposalType: 'add_signer',
|
|
1129
|
-
chainAnchor: MOCK_CHAIN_ANCHOR_B64,
|
|
1130
|
-
targetThreshold: 1,
|
|
1131
|
-
targetSignerCommitments: ['0x' + 'a'.repeat(64)],
|
|
1132
|
-
description: '',
|
|
1133
|
-
});
|
|
1134
|
-
expect(proposal.nonce).toBe(1);
|
|
1135
|
-
expect(proposal.id).toBe('0x' + 'c'.repeat(64));
|
|
1136
|
-
});
|
|
1137
|
-
it('should reject a mismatched returned commitment', async () => {
|
|
1138
|
-
const config = {
|
|
1139
|
-
threshold: 1,
|
|
1140
|
-
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
1141
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
1142
|
-
};
|
|
1143
|
-
const multisig = createTestMultisig(config);
|
|
1144
|
-
const mockDelta = {
|
|
1145
|
-
account_id: '0x' + 'a'.repeat(30),
|
|
1146
|
-
nonce: 1,
|
|
1147
|
-
prev_commitment: '0x' + 'b'.repeat(64),
|
|
1148
|
-
delta_payload: {
|
|
1149
|
-
tx_summary: { data: 'AQID' },
|
|
1150
|
-
signatures: [],
|
|
1151
|
-
},
|
|
1152
|
-
status: {
|
|
1153
|
-
status: 'pending',
|
|
1154
|
-
timestamp: '2024-01-01T00:00:00Z',
|
|
1155
|
-
proposer_id: '0x' + 'c'.repeat(64),
|
|
1156
|
-
cosigner_sigs: [],
|
|
1157
|
-
},
|
|
1158
|
-
};
|
|
1159
|
-
mockFetch.mockResolvedValueOnce({
|
|
1160
|
-
ok: true,
|
|
1161
|
-
json: async () => ({
|
|
1162
|
-
delta: mockDelta,
|
|
1163
|
-
commitment: '0x' + 'd'.repeat(64),
|
|
1164
|
-
}),
|
|
1165
|
-
});
|
|
1166
|
-
await expect(multisig.createProposal(1, 'AQID', {
|
|
1167
|
-
proposalType: 'add_signer',
|
|
1168
|
-
chainAnchor: MOCK_CHAIN_ANCHOR_B64,
|
|
1169
|
-
targetThreshold: 1,
|
|
1170
|
-
targetSignerCommitments: ['0x' + 'a'.repeat(64)],
|
|
1171
|
-
description: '',
|
|
1172
|
-
})).rejects.toThrow('Invalid proposal: commitment 0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd does not match tx_summary 0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc');
|
|
1173
|
-
});
|
|
1174
|
-
it('should reject a response whose tx_summary does not match the provided metadata', async () => {
|
|
1175
|
-
const config = {
|
|
1176
|
-
threshold: 1,
|
|
1177
|
-
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
1178
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
1179
|
-
};
|
|
1180
|
-
const multisig = createTestMultisig(config);
|
|
1181
|
-
const mockDelta = {
|
|
1182
|
-
account_id: '0x' + 'a'.repeat(30),
|
|
1183
|
-
nonce: 1,
|
|
1184
|
-
prev_commitment: '0x' + 'b'.repeat(64),
|
|
1185
|
-
delta_payload: {
|
|
1186
|
-
tx_summary: { data: 'AQID' },
|
|
1187
|
-
signatures: [],
|
|
1188
|
-
},
|
|
1189
|
-
status: {
|
|
1190
|
-
status: 'pending',
|
|
1191
|
-
timestamp: '2024-01-01T00:00:00Z',
|
|
1192
|
-
proposer_id: '0x' + 'c'.repeat(64),
|
|
1193
|
-
cosigner_sigs: [],
|
|
1194
|
-
},
|
|
1195
|
-
};
|
|
1196
|
-
mockFetch.mockResolvedValueOnce({
|
|
1197
|
-
ok: true,
|
|
1198
|
-
json: async () => ({
|
|
1199
|
-
delta: mockDelta,
|
|
1200
|
-
commitment: '0x' + 'c'.repeat(64),
|
|
1201
|
-
}),
|
|
1202
|
-
});
|
|
1203
|
-
vi.mocked(executeForSummaryAt).mockResolvedValueOnce({
|
|
1204
|
-
toCommitment: () => ({
|
|
1205
|
-
toHex: () => '0x' + 'f'.repeat(64),
|
|
1206
|
-
}),
|
|
1207
|
-
});
|
|
1208
|
-
await expect(multisig.createProposal(1, 'AQID', {
|
|
1209
|
-
proposalType: 'add_signer',
|
|
1210
|
-
chainAnchor: MOCK_CHAIN_ANCHOR_B64,
|
|
1211
|
-
targetThreshold: 1,
|
|
1212
|
-
targetSignerCommitments: ['0x' + 'a'.repeat(64)],
|
|
1213
|
-
description: '',
|
|
1214
|
-
})).rejects.toThrow('Invalid proposal: metadata does not match tx_summary');
|
|
1215
|
-
});
|
|
1216
|
-
});
|
|
1217
|
-
describe('createP2idProposal', () => {
|
|
1218
|
-
it('should include the faucet asset in the proposal description', async () => {
|
|
1219
|
-
const { executeForSummary } = await import('./transaction.js');
|
|
1220
|
-
vi.mocked(executeForSummary).mockResolvedValue({
|
|
1221
|
-
summary: {
|
|
1222
|
-
toCommitment: () => ({
|
|
1223
|
-
toHex: () => '0x' + 'c'.repeat(64),
|
|
1224
|
-
}),
|
|
1225
|
-
serialize: () => new Uint8Array([1, 2, 3]),
|
|
1226
|
-
},
|
|
1227
|
-
anchor: createMockChainAnchor(),
|
|
1228
|
-
});
|
|
1229
|
-
const config = {
|
|
1230
|
-
threshold: 1,
|
|
1231
|
-
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
1232
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
1233
|
-
};
|
|
1234
|
-
const multisig = createTestMultisig(config);
|
|
1235
|
-
const mockDelta = {
|
|
1236
|
-
account_id: '0x' + 'a'.repeat(30),
|
|
1237
|
-
nonce: 1,
|
|
1238
|
-
prev_commitment: '0x' + 'b'.repeat(64),
|
|
1239
|
-
delta_payload: {
|
|
1240
|
-
tx_summary: { data: 'AQID' },
|
|
1241
|
-
signatures: [],
|
|
1242
|
-
metadata: {
|
|
1243
|
-
proposal_type: 'p2id',
|
|
1244
|
-
chain_anchor: MOCK_CHAIN_ANCHOR_B64,
|
|
1245
|
-
recipient_id: '0xrecipient',
|
|
1246
|
-
faucet_id: '0xfaucet',
|
|
1247
|
-
amount: '100',
|
|
1248
|
-
description: '',
|
|
1249
|
-
},
|
|
1250
|
-
},
|
|
1251
|
-
status: {
|
|
1252
|
-
status: 'pending',
|
|
1253
|
-
timestamp: '2024-01-01T00:00:00Z',
|
|
1254
|
-
proposer_id: '0x' + 'c'.repeat(64),
|
|
1255
|
-
cosigner_sigs: [],
|
|
1256
|
-
},
|
|
1257
|
-
};
|
|
1258
|
-
mockFetch.mockResolvedValueOnce({
|
|
1259
|
-
ok: true,
|
|
1260
|
-
json: async () => ({
|
|
1261
|
-
delta: mockDelta,
|
|
1262
|
-
commitment: '0x' + 'c'.repeat(64),
|
|
1263
|
-
}),
|
|
1264
|
-
});
|
|
1265
|
-
const proposal = await multisig.createP2idProposal('0xrecipient', '0xfaucet', 100n, { nonce: 1 });
|
|
1266
|
-
expect(proposal.metadata.description).toBe('Send 100 of asset 0xfaucet... to 0xrecipien...');
|
|
1267
|
-
});
|
|
1268
|
-
it('threads a private noteType into the request and wire metadata (issue #322)', async () => {
|
|
1269
|
-
const { executeForSummary, buildP2idTransactionRequest } = await import('./transaction.js');
|
|
1270
|
-
const { NoteType } = await import('@miden-sdk/miden-sdk');
|
|
1271
|
-
vi.mocked(executeForSummary).mockResolvedValue({
|
|
1272
|
-
summary: {
|
|
1273
|
-
toCommitment: () => ({
|
|
1274
|
-
toHex: () => '0x' + 'c'.repeat(64),
|
|
1275
|
-
}),
|
|
1276
|
-
serialize: () => new Uint8Array([1, 2, 3]),
|
|
1277
|
-
},
|
|
1278
|
-
anchor: createMockChainAnchor(),
|
|
1279
|
-
});
|
|
1280
|
-
const config = {
|
|
1281
|
-
threshold: 1,
|
|
1282
|
-
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
1283
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
1284
|
-
};
|
|
1285
|
-
const multisig = createTestMultisig(config);
|
|
1286
|
-
const mockDelta = {
|
|
1287
|
-
account_id: '0x' + 'a'.repeat(30),
|
|
1288
|
-
nonce: 1,
|
|
1289
|
-
prev_commitment: '0x' + 'b'.repeat(64),
|
|
1290
|
-
delta_payload: {
|
|
1291
|
-
tx_summary: { data: 'AQID' },
|
|
1292
|
-
signatures: [],
|
|
1293
|
-
metadata: {
|
|
1294
|
-
proposal_type: 'p2id',
|
|
1295
|
-
chain_anchor: MOCK_CHAIN_ANCHOR_B64,
|
|
1296
|
-
recipient_id: '0xrecipient',
|
|
1297
|
-
faucet_id: '0xfaucet',
|
|
1298
|
-
amount: '100',
|
|
1299
|
-
note_type: 'private',
|
|
1300
|
-
description: '',
|
|
1301
|
-
},
|
|
1302
|
-
},
|
|
1303
|
-
status: {
|
|
1304
|
-
status: 'pending',
|
|
1305
|
-
timestamp: '2024-01-01T00:00:00Z',
|
|
1306
|
-
proposer_id: '0x' + 'c'.repeat(64),
|
|
1307
|
-
cosigner_sigs: [],
|
|
1308
|
-
},
|
|
1309
|
-
};
|
|
1310
|
-
mockFetch.mockResolvedValueOnce({
|
|
1311
|
-
ok: true,
|
|
1312
|
-
json: async () => ({
|
|
1313
|
-
delta: mockDelta,
|
|
1314
|
-
commitment: '0x' + 'c'.repeat(64),
|
|
1315
|
-
}),
|
|
1316
|
-
});
|
|
1317
|
-
const proposal = await multisig.createP2idProposal('0xrecipient', '0xfaucet', 100n, {
|
|
1318
|
-
nonce: 1,
|
|
1319
|
-
noteType: NoteType.Private,
|
|
1320
|
-
});
|
|
1321
|
-
// Propose path builds the private note...
|
|
1322
|
-
expect(vi.mocked(buildP2idTransactionRequest)).toHaveBeenCalledWith(expect.any(String), '0xrecipient', '0xfaucet', 100n, { noteType: NoteType.Private });
|
|
1323
|
-
// ...and the rebuild-from-metadata path parses note_type back to Private.
|
|
1324
|
-
const lastCall = vi.mocked(buildP2idTransactionRequest).mock.calls.at(-1);
|
|
1325
|
-
expect(lastCall[4]).toMatchObject({ noteType: NoteType.Private });
|
|
1326
|
-
// The pushed wire metadata carries note_type so cosigners rebuild the
|
|
1327
|
-
// same private note at verification/execution.
|
|
1328
|
-
const pushBody = JSON.parse(mockFetch.mock.calls.at(-1)[1].body);
|
|
1329
|
-
expect(pushBody.delta_payload.metadata.note_type).toBe('private');
|
|
1330
|
-
expect(proposal.metadata.proposalType).toBe('p2id');
|
|
1331
|
-
expect(proposal.metadata.noteType).toBe('private');
|
|
1332
|
-
});
|
|
1333
|
-
it('threads P2IDE reclaim/timelock heights into the request and wire metadata (issue #366)', async () => {
|
|
1334
|
-
const { executeForSummary, buildP2idTransactionRequest } = await import('./transaction.js');
|
|
1335
|
-
vi.mocked(executeForSummary).mockResolvedValue({
|
|
1336
|
-
summary: {
|
|
1337
|
-
toCommitment: () => ({
|
|
1338
|
-
toHex: () => '0x' + 'c'.repeat(64),
|
|
1339
|
-
}),
|
|
1340
|
-
serialize: () => new Uint8Array([1, 2, 3]),
|
|
1341
|
-
},
|
|
1342
|
-
anchor: createMockChainAnchor(),
|
|
1343
|
-
});
|
|
1344
|
-
const config = {
|
|
1345
|
-
threshold: 1,
|
|
1346
|
-
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
1347
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
1348
|
-
};
|
|
1349
|
-
const multisig = createTestMultisig(config);
|
|
1350
|
-
const mockDelta = {
|
|
1351
|
-
account_id: '0x' + 'a'.repeat(30),
|
|
1352
|
-
nonce: 1,
|
|
1353
|
-
prev_commitment: '0x' + 'b'.repeat(64),
|
|
1354
|
-
delta_payload: {
|
|
1355
|
-
tx_summary: { data: 'AQID' },
|
|
1356
|
-
signatures: [],
|
|
1357
|
-
metadata: {
|
|
1358
|
-
proposal_type: 'p2id',
|
|
1359
|
-
recipient_id: '0xrecipient',
|
|
1360
|
-
faucet_id: '0xfaucet',
|
|
1361
|
-
amount: '100',
|
|
1362
|
-
reclaim_height: 12345,
|
|
1363
|
-
timelock_height: 700,
|
|
1364
|
-
description: '',
|
|
1365
|
-
},
|
|
1366
|
-
},
|
|
1367
|
-
status: {
|
|
1368
|
-
status: 'pending',
|
|
1369
|
-
timestamp: '2024-01-01T00:00:00Z',
|
|
1370
|
-
proposer_id: '0x' + 'c'.repeat(64),
|
|
1371
|
-
cosigner_sigs: [],
|
|
1372
|
-
},
|
|
1373
|
-
};
|
|
1374
|
-
mockFetch.mockResolvedValueOnce({
|
|
1375
|
-
ok: true,
|
|
1376
|
-
json: async () => ({
|
|
1377
|
-
delta: mockDelta,
|
|
1378
|
-
commitment: '0x' + 'c'.repeat(64),
|
|
1379
|
-
}),
|
|
1380
|
-
});
|
|
1381
|
-
const proposal = await multisig.createP2idProposal('0xrecipient', '0xfaucet', 100n, {
|
|
1382
|
-
nonce: 1,
|
|
1383
|
-
reclaimHeight: 12345,
|
|
1384
|
-
timelockHeight: 700,
|
|
1385
|
-
});
|
|
1386
|
-
// Propose path builds the P2IDE note from the heights...
|
|
1387
|
-
const lastCall = vi.mocked(buildP2idTransactionRequest).mock.calls.at(-1);
|
|
1388
|
-
expect(lastCall[4]).toMatchObject({ reclaimHeight: 12345, timelockHeight: 700 });
|
|
1389
|
-
// ...and the pushed wire metadata carries the heights so cosigners
|
|
1390
|
-
// rebuild the same P2IDE note at verification/execution.
|
|
1391
|
-
const pushBody = JSON.parse(mockFetch.mock.calls.at(-1)[1].body);
|
|
1392
|
-
expect(pushBody.delta_payload.metadata.reclaim_height).toBe(12345);
|
|
1393
|
-
expect(pushBody.delta_payload.metadata.timelock_height).toBe(700);
|
|
1394
|
-
expect(proposal.metadata.proposalType).toBe('p2id');
|
|
1395
|
-
expect(proposal.metadata).toMatchObject({ reclaimHeight: 12345, timelockHeight: 700 });
|
|
1396
|
-
});
|
|
1397
|
-
it('omits the heights from wire metadata for a plain P2ID send (pre-#366 shape)', async () => {
|
|
1398
|
-
const { executeForSummary } = await import('./transaction.js');
|
|
1399
|
-
vi.mocked(executeForSummary).mockResolvedValue({
|
|
1400
|
-
summary: {
|
|
1401
|
-
toCommitment: () => ({
|
|
1402
|
-
toHex: () => '0x' + 'c'.repeat(64),
|
|
1403
|
-
}),
|
|
1404
|
-
serialize: () => new Uint8Array([1, 2, 3]),
|
|
1405
|
-
},
|
|
1406
|
-
anchor: createMockChainAnchor(),
|
|
1407
|
-
});
|
|
1408
|
-
const config = {
|
|
1409
|
-
threshold: 1,
|
|
1410
|
-
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
1411
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
1412
|
-
};
|
|
1413
|
-
const multisig = createTestMultisig(config);
|
|
1414
|
-
const mockDelta = {
|
|
1415
|
-
account_id: '0x' + 'a'.repeat(30),
|
|
1416
|
-
nonce: 1,
|
|
1417
|
-
prev_commitment: '0x' + 'b'.repeat(64),
|
|
1418
|
-
delta_payload: {
|
|
1419
|
-
tx_summary: { data: 'AQID' },
|
|
1420
|
-
signatures: [],
|
|
1421
|
-
metadata: {
|
|
1422
|
-
proposal_type: 'p2id',
|
|
1423
|
-
recipient_id: '0xrecipient',
|
|
1424
|
-
faucet_id: '0xfaucet',
|
|
1425
|
-
amount: '100',
|
|
1426
|
-
description: '',
|
|
1427
|
-
},
|
|
1428
|
-
},
|
|
1429
|
-
status: {
|
|
1430
|
-
status: 'pending',
|
|
1431
|
-
timestamp: '2024-01-01T00:00:00Z',
|
|
1432
|
-
proposer_id: '0x' + 'c'.repeat(64),
|
|
1433
|
-
cosigner_sigs: [],
|
|
1434
|
-
},
|
|
1435
|
-
};
|
|
1436
|
-
mockFetch.mockResolvedValueOnce({
|
|
1437
|
-
ok: true,
|
|
1438
|
-
json: async () => ({
|
|
1439
|
-
delta: mockDelta,
|
|
1440
|
-
commitment: '0x' + 'c'.repeat(64),
|
|
1441
|
-
}),
|
|
1442
|
-
});
|
|
1443
|
-
await multisig.createP2idProposal('0xrecipient', '0xfaucet', 100n, { nonce: 1 });
|
|
1444
|
-
const pushBody = JSON.parse(mockFetch.mock.calls.at(-1)[1].body);
|
|
1445
|
-
expect('reclaim_height' in pushBody.delta_payload.metadata).toBe(false);
|
|
1446
|
-
expect('timelock_height' in pushBody.delta_payload.metadata).toBe(false);
|
|
1447
|
-
});
|
|
1448
|
-
});
|
|
1449
|
-
describe('exportNoteToBytes / importNoteFromBytes (issue #356)', () => {
|
|
1450
|
-
const config = {
|
|
1451
|
-
threshold: 1,
|
|
1452
|
-
signerCommitments: ['0x' + '1'.repeat(64)],
|
|
1453
|
-
guardianCommitment: '0x' + '3'.repeat(64),
|
|
1454
|
-
};
|
|
1455
|
-
it('exports the full note with proof when the inclusion proof is known', async () => {
|
|
1456
|
-
const noteFile = { serialize: () => new Uint8Array([9, 9, 9]) };
|
|
1457
|
-
mockWebClient.getOutputNote = vi.fn().mockResolvedValue({
|
|
1458
|
-
inclusionProof: () => ({}),
|
|
1459
|
-
});
|
|
1460
|
-
mockWebClient.exportNoteFile = vi.fn().mockResolvedValue(noteFile);
|
|
1461
|
-
const multisig = createTestMultisig(config);
|
|
1462
|
-
const bytes = await multisig.exportNoteToBytes('0x' + 'ab'.repeat(32));
|
|
1463
|
-
expect(bytes).toEqual(new Uint8Array([9, 9, 9]));
|
|
1464
|
-
// NoteExportFormat.Full = 1 in the SDK mock
|
|
1465
|
-
expect(mockWebClient.exportNoteFile).toHaveBeenCalledWith('0x' + 'ab'.repeat(32), 1);
|
|
1466
|
-
});
|
|
1467
|
-
it('falls back to a details-only export before the note commits on chain', async () => {
|
|
1468
|
-
const noteFile = { serialize: () => new Uint8Array([7]) };
|
|
1469
|
-
mockWebClient.getOutputNote = vi.fn().mockResolvedValue({
|
|
1470
|
-
inclusionProof: () => undefined,
|
|
1471
|
-
});
|
|
1472
|
-
mockWebClient.exportNoteFile = vi.fn().mockResolvedValue(noteFile);
|
|
1473
|
-
const multisig = createTestMultisig(config);
|
|
1474
|
-
await multisig.exportNoteToBytes(' 0x' + 'ab'.repeat(32) + ' ');
|
|
1475
|
-
// NoteExportFormat.Details = 2 in the SDK mock; the id is trimmed
|
|
1476
|
-
expect(mockWebClient.exportNoteFile).toHaveBeenCalledWith('0x' + 'ab'.repeat(32), 2);
|
|
1477
|
-
});
|
|
1478
|
-
it('rejects exporting a note the local store does not know', async () => {
|
|
1479
|
-
mockWebClient.getOutputNote = vi.fn().mockRejectedValue(new Error('no such note'));
|
|
1480
|
-
mockWebClient.exportNoteFile = vi.fn();
|
|
1481
|
-
const multisig = createTestMultisig(config);
|
|
1482
|
-
await expect(multisig.exportNoteToBytes('0x' + 'ab'.repeat(32))).rejects.toThrow(/not found in the local store/);
|
|
1483
|
-
expect(mockWebClient.exportNoteFile).not.toHaveBeenCalled();
|
|
1484
|
-
});
|
|
1485
|
-
it('rejects exporting when the store resolves no record', async () => {
|
|
1486
|
-
mockWebClient.getOutputNote = vi.fn().mockResolvedValue(undefined);
|
|
1487
|
-
mockWebClient.exportNoteFile = vi.fn();
|
|
1488
|
-
const multisig = createTestMultisig(config);
|
|
1489
|
-
await expect(multisig.exportNoteToBytes('0x' + 'ab'.repeat(32))).rejects.toThrow(/not found in the local store/);
|
|
1490
|
-
expect(mockWebClient.exportNoteFile).not.toHaveBeenCalled();
|
|
1491
|
-
});
|
|
1492
|
-
it('imports note file bytes and returns the resolved identifier', async () => {
|
|
1493
|
-
const decoded = { marker: 'note-file' };
|
|
1494
|
-
mockNoteFileDeserialize.mockReturnValue(decoded);
|
|
1495
|
-
mockWebClient.importNoteFile = vi.fn().mockResolvedValue('0x' + 'cd'.repeat(32));
|
|
1496
|
-
const multisig = createTestMultisig(config);
|
|
1497
|
-
const noteId = await multisig.importNoteFromBytes(new Uint8Array([1, 2, 3]));
|
|
1498
|
-
expect(mockNoteFileDeserialize).toHaveBeenCalledWith(new Uint8Array([1, 2, 3]));
|
|
1499
|
-
expect(mockWebClient.importNoteFile).toHaveBeenCalledWith(decoded);
|
|
1500
|
-
expect(noteId).toBe('0x' + 'cd'.repeat(32));
|
|
1501
|
-
});
|
|
1502
|
-
it('rejects bytes that do not decode as a note file', async () => {
|
|
1503
|
-
mockNoteFileDeserialize.mockImplementation(() => {
|
|
1504
|
-
throw new Error('bad bytes');
|
|
1505
|
-
});
|
|
1506
|
-
mockWebClient.importNoteFile = vi.fn();
|
|
1507
|
-
const multisig = createTestMultisig(config);
|
|
1508
|
-
await expect(multisig.importNoteFromBytes(new Uint8Array([0]))).rejects.toThrow(/failed to decode note file: bad bytes/);
|
|
1509
|
-
expect(mockWebClient.importNoteFile).not.toHaveBeenCalled();
|
|
1510
|
-
});
|
|
1511
|
-
});
|
|
1512
|
-
describe('exportNoteToFile / importNoteFromFile (issue #356)', () => {
|
|
1513
|
-
const config = {
|
|
1514
|
-
threshold: 1,
|
|
1515
|
-
signerCommitments: ['0x' + '1'.repeat(64)],
|
|
1516
|
-
guardianCommitment: '0x' + '3'.repeat(64),
|
|
1517
|
-
};
|
|
1518
|
-
it('rejects exportNoteToFile outside a browser environment', async () => {
|
|
1519
|
-
const multisig = createTestMultisig(config);
|
|
1520
|
-
await expect(multisig.exportNoteToFile('0x' + 'ab'.repeat(32))).rejects.toThrow(/requires a browser environment/);
|
|
1521
|
-
});
|
|
1522
|
-
it('imports from a File/Blob by delegating to importNoteFromBytes', async () => {
|
|
1523
|
-
const decoded = { marker: 'note-file' };
|
|
1524
|
-
mockNoteFileDeserialize.mockReturnValue(decoded);
|
|
1525
|
-
mockWebClient.importNoteFile = vi.fn().mockResolvedValue('0x' + 'cd'.repeat(32));
|
|
1526
|
-
const multisig = createTestMultisig(config);
|
|
1527
|
-
const noteId = await multisig.importNoteFromFile(new Blob([new Uint8Array([1, 2, 3])]));
|
|
1528
|
-
expect(mockNoteFileDeserialize).toHaveBeenCalledWith(new Uint8Array([1, 2, 3]));
|
|
1529
|
-
expect(noteId).toBe('0x' + 'cd'.repeat(32));
|
|
1530
|
-
});
|
|
1531
|
-
});
|
|
1532
|
-
describe('getP2idNoteId (issue #356)', () => {
|
|
1533
|
-
const config = {
|
|
1534
|
-
threshold: 1,
|
|
1535
|
-
signerCommitments: ['0x' + '1'.repeat(64)],
|
|
1536
|
-
guardianCommitment: '0x' + '3'.repeat(64),
|
|
1537
|
-
};
|
|
1538
|
-
it('computes the deterministic note ID from p2id proposal metadata', async () => {
|
|
1539
|
-
const multisig = createTestMultisig(config);
|
|
1540
|
-
const proposal = {
|
|
1541
|
-
metadata: {
|
|
1542
|
-
proposalType: 'p2id',
|
|
1543
|
-
chainAnchor: MOCK_CHAIN_ANCHOR_B64,
|
|
1544
|
-
recipientId: '0x' + 'b'.repeat(30),
|
|
1545
|
-
faucetId: '0x' + 'c'.repeat(30),
|
|
1546
|
-
amount: '100',
|
|
1547
|
-
saltHex: '0x' + 'd'.repeat(64),
|
|
1548
|
-
noteType: 'private',
|
|
1549
|
-
},
|
|
1550
|
-
};
|
|
1551
|
-
const noteId = await multisig.getP2idNoteId(proposal);
|
|
1552
|
-
expect(noteId).toBe('0x' + 'ab'.repeat(32));
|
|
1553
|
-
});
|
|
1554
|
-
it('rejects non-p2id proposals', async () => {
|
|
1555
|
-
const multisig = createTestMultisig(config);
|
|
1556
|
-
const proposal = {
|
|
1557
|
-
metadata: { proposalType: 'consume_notes' },
|
|
1558
|
-
};
|
|
1559
|
-
await expect(multisig.getP2idNoteId(proposal)).rejects.toThrow(/requires a P2ID proposal/);
|
|
1560
|
-
});
|
|
1561
|
-
});
|
|
1562
|
-
describe('createChangeThresholdProposal', () => {
|
|
1563
|
-
it('passes the signer scheme to update-signers requests', async () => {
|
|
1564
|
-
vi.mocked(executeForSummary).mockResolvedValue({
|
|
1565
|
-
summary: {
|
|
1566
|
-
toCommitment: () => ({
|
|
1567
|
-
toHex: () => '0x' + 'c'.repeat(64),
|
|
1568
|
-
}),
|
|
1569
|
-
serialize: () => new Uint8Array([1, 2, 3]),
|
|
1570
|
-
},
|
|
1571
|
-
anchor: createMockChainAnchor(),
|
|
1572
|
-
});
|
|
1573
|
-
const ecdsaSigner = {
|
|
1574
|
-
...mockSigner,
|
|
1575
|
-
publicKey: '0x' + '2'.repeat(66),
|
|
1576
|
-
scheme: 'ecdsa',
|
|
1577
|
-
};
|
|
1578
|
-
guardian.setSigner(ecdsaSigner);
|
|
1579
|
-
const config = {
|
|
1580
|
-
threshold: 1,
|
|
1581
|
-
signerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
|
|
1582
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
1583
|
-
};
|
|
1584
|
-
const mockDelta = {
|
|
1585
|
-
account_id: '0x' + 'a'.repeat(30),
|
|
1586
|
-
nonce: 1,
|
|
1587
|
-
prev_commitment: '0x' + 'b'.repeat(64),
|
|
1588
|
-
delta_payload: {
|
|
1589
|
-
tx_summary: { data: 'AQID' },
|
|
1590
|
-
signatures: [],
|
|
1591
|
-
metadata: {
|
|
1592
|
-
proposal_type: 'change_threshold',
|
|
1593
|
-
chain_anchor: MOCK_CHAIN_ANCHOR_B64,
|
|
1594
|
-
target_threshold: 2,
|
|
1595
|
-
description: '',
|
|
1596
|
-
},
|
|
1597
|
-
},
|
|
1598
|
-
status: {
|
|
1599
|
-
status: 'pending',
|
|
1600
|
-
timestamp: '2024-01-01T00:00:00Z',
|
|
1601
|
-
proposer_id: '0x' + 'c'.repeat(64),
|
|
1602
|
-
cosigner_sigs: [],
|
|
1603
|
-
},
|
|
1604
|
-
};
|
|
1605
|
-
mockFetch.mockResolvedValueOnce({
|
|
1606
|
-
ok: true,
|
|
1607
|
-
json: async () => ({
|
|
1608
|
-
delta: mockDelta,
|
|
1609
|
-
commitment: '0x' + 'c'.repeat(64),
|
|
1610
|
-
}),
|
|
1611
|
-
});
|
|
1612
|
-
const multisig = createTestMultisig(config, ecdsaSigner);
|
|
1613
|
-
await multisig.createChangeThresholdProposal(2, { nonce: 1 });
|
|
1614
|
-
expect(buildUpdateSignersTransactionRequest).toHaveBeenCalledWith(mockWebClient, 2, config.signerCommitments, { signatureScheme: 'ecdsa' });
|
|
1615
|
-
});
|
|
1616
|
-
});
|
|
1617
|
-
describe('createAddSignerProposal / createRemoveSignerProposal', () => {
|
|
1618
|
-
const config = {
|
|
1619
|
-
threshold: 2,
|
|
1620
|
-
signerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64), '0x' + 'd'.repeat(64)],
|
|
1621
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
1622
|
-
};
|
|
1623
|
-
const mockPushResponse = (proposalType) => {
|
|
1624
|
-
mockFetch.mockResolvedValueOnce({
|
|
1625
|
-
ok: true,
|
|
1626
|
-
json: async () => ({
|
|
1627
|
-
delta: {
|
|
1628
|
-
account_id: '0x' + 'a'.repeat(30),
|
|
1629
|
-
nonce: 1,
|
|
1630
|
-
prev_commitment: '0x' + 'b'.repeat(64),
|
|
1631
|
-
delta_payload: {
|
|
1632
|
-
tx_summary: { data: 'AQID' },
|
|
1633
|
-
signatures: [],
|
|
1634
|
-
metadata: { proposal_type: proposalType, description: '' },
|
|
1635
|
-
},
|
|
1636
|
-
status: {
|
|
1637
|
-
status: 'pending',
|
|
1638
|
-
timestamp: '2024-01-01T00:00:00Z',
|
|
1639
|
-
proposer_id: '0x' + 'c'.repeat(64),
|
|
1640
|
-
cosigner_sigs: [],
|
|
1641
|
-
},
|
|
1642
|
-
},
|
|
1643
|
-
commitment: '0x' + 'c'.repeat(64),
|
|
1644
|
-
}),
|
|
1645
|
-
});
|
|
1646
|
-
};
|
|
1647
|
-
beforeEach(() => {
|
|
1648
|
-
vi.mocked(executeForSummary).mockResolvedValue({
|
|
1649
|
-
summary: {
|
|
1650
|
-
toCommitment: () => ({ toHex: () => '0x' + 'c'.repeat(64) }),
|
|
1651
|
-
serialize: () => new Uint8Array([1, 2, 3]),
|
|
1652
|
-
},
|
|
1653
|
-
anchor: createMockChainAnchor(),
|
|
1654
|
-
});
|
|
1655
|
-
});
|
|
1656
|
-
it('add: passes newThreshold from the options bag and appends the commitment', async () => {
|
|
1657
|
-
mockPushResponse('add_signer');
|
|
1658
|
-
const newCommitment = '0x' + 'e'.repeat(64);
|
|
1659
|
-
const multisig = createTestMultisig(config);
|
|
1660
|
-
await multisig.createAddSignerProposal(newCommitment, { nonce: 1, newThreshold: 3 });
|
|
1661
|
-
expect(buildUpdateSignersTransactionRequest).toHaveBeenCalledWith(mockWebClient, 3, [...config.signerCommitments, newCommitment], { signatureScheme: mockSigner.scheme });
|
|
1662
|
-
});
|
|
1663
|
-
it('add: defaults to the current threshold when newThreshold is omitted', async () => {
|
|
1664
|
-
mockPushResponse('add_signer');
|
|
1665
|
-
const multisig = createTestMultisig(config);
|
|
1666
|
-
await multisig.createAddSignerProposal('0x' + 'e'.repeat(64), { nonce: 1 });
|
|
1667
|
-
expect(buildUpdateSignersTransactionRequest).toHaveBeenCalledWith(mockWebClient, config.threshold, expect.any(Array), { signatureScheme: mockSigner.scheme });
|
|
1668
|
-
});
|
|
1669
|
-
it('remove: passes newThreshold from the options bag and drops the commitment', async () => {
|
|
1670
|
-
mockPushResponse('remove_signer');
|
|
1671
|
-
const multisig = createTestMultisig(config);
|
|
1672
|
-
await multisig.createRemoveSignerProposal('0x' + 'd'.repeat(64), { nonce: 1, newThreshold: 1 });
|
|
1673
|
-
expect(buildUpdateSignersTransactionRequest).toHaveBeenCalledWith(mockWebClient, 1, [config.signerCommitments[0], config.signerCommitments[1]], { signatureScheme: mockSigner.scheme });
|
|
1674
|
-
});
|
|
1675
|
-
it('remove: defaults to min(current threshold, remaining signers) when newThreshold is omitted', async () => {
|
|
1676
|
-
mockPushResponse('remove_signer');
|
|
1677
|
-
const multisig = createTestMultisig(config);
|
|
1678
|
-
await multisig.createRemoveSignerProposal('0x' + 'd'.repeat(64), { nonce: 1 });
|
|
1679
|
-
expect(buildUpdateSignersTransactionRequest).toHaveBeenCalledWith(mockWebClient, 2, expect.any(Array), { signatureScheme: mockSigner.scheme });
|
|
1680
|
-
});
|
|
1681
|
-
});
|
|
1682
|
-
describe('legacy positional-caller guard (issue #387)', () => {
|
|
1683
|
-
const config = {
|
|
1684
|
-
threshold: 1,
|
|
1685
|
-
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
1686
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
1687
|
-
};
|
|
1688
|
-
it('rejects a legacy positional nonce where the options bag is expected', async () => {
|
|
1689
|
-
const multisig = createTestMultisig(config);
|
|
1690
|
-
await expect(multisig.createP2idProposal('0xrecipient', '0xfaucet', 100n, 1)).rejects.toThrow(/issue #387/);
|
|
1691
|
-
await expect(multisig.createConsumeNotesProposal(['0x1'], 1)).rejects.toThrow(/issue #387/);
|
|
1692
|
-
await expect(multisig.createAddSignerProposal('0x' + 'e'.repeat(64), 1)).rejects.toThrow(/issue #387/);
|
|
1693
|
-
await expect(multisig.createCustomProposal(new Uint8Array([1]), 'label', 1)).rejects.toThrow(/issue #387/);
|
|
1694
|
-
});
|
|
1695
|
-
it('rejects a legacy trailing argument after the options slot', async () => {
|
|
1696
|
-
const multisig = createTestMultisig(config);
|
|
1697
|
-
// Pre-#387 pattern: createP2idProposal(r, f, amount, nonceHole, { noteType })
|
|
1698
|
-
await expect(multisig.createP2idProposal('0xrecipient', '0xfaucet', 100n, undefined, {
|
|
1699
|
-
noteType: 'private',
|
|
1700
|
-
})).rejects.toThrow(/issue #387/);
|
|
1701
|
-
// Pre-#387 pattern: createAddSignerProposal(commitment, nonceHole, newThreshold)
|
|
1702
|
-
await expect(multisig.createAddSignerProposal('0x' + 'e'.repeat(64), undefined, 3)).rejects.toThrow(/issue #387/);
|
|
1703
|
-
});
|
|
1704
|
-
});
|
|
1705
|
-
describe('createSwitchGuardianProposal', () => {
|
|
1706
|
-
it('should verify new endpoint commitment before creating proposal', async () => {
|
|
1707
|
-
vi.mocked(executeForSummary).mockResolvedValue({
|
|
1708
|
-
summary: {
|
|
1709
|
-
serialize: () => new Uint8Array([1, 2, 3]),
|
|
1710
|
-
},
|
|
1711
|
-
anchor: createMockChainAnchor(),
|
|
1712
|
-
});
|
|
1713
|
-
const config = {
|
|
1714
|
-
threshold: 1,
|
|
1715
|
-
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
1716
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
1717
|
-
};
|
|
1718
|
-
const multisig = createTestMultisig(config);
|
|
1719
|
-
const newGuardianPubkey = '0x' + '1'.repeat(64);
|
|
1720
|
-
mockFetch.mockResolvedValueOnce({
|
|
1721
|
-
ok: true,
|
|
1722
|
-
json: async () => ({ commitment: newGuardianPubkey }),
|
|
1723
|
-
});
|
|
1724
|
-
mockFetch.mockResolvedValueOnce({
|
|
1725
|
-
ok: true,
|
|
1726
|
-
json: async () => ({
|
|
1727
|
-
delta: {
|
|
1728
|
-
account_id: multisig.accountId,
|
|
1729
|
-
nonce: 1,
|
|
1730
|
-
prev_commitment: '0x' + 'b'.repeat(64),
|
|
1731
|
-
delta_payload: { tx_summary: { data: 'AQID' }, signatures: [] },
|
|
1732
|
-
status: {
|
|
1733
|
-
status: 'pending',
|
|
1734
|
-
timestamp: '2024-01-01T00:00:00Z',
|
|
1735
|
-
proposer_id: '0x' + 'c'.repeat(64),
|
|
1736
|
-
cosigner_sigs: [],
|
|
1737
|
-
},
|
|
1738
|
-
},
|
|
1739
|
-
commitment: '0x' + 'c'.repeat(64),
|
|
1740
|
-
}),
|
|
1741
|
-
});
|
|
1742
|
-
const proposal = await multisig.createSwitchGuardianProposal('http://new-guardian.com', newGuardianPubkey);
|
|
1743
|
-
expect(proposal.metadata?.proposalType).toBe('switch_guardian');
|
|
1744
|
-
if (proposal.metadata?.proposalType === 'switch_guardian') {
|
|
1745
|
-
expect(proposal.metadata.newGuardianEndpoint).toBe('http://new-guardian.com');
|
|
1746
|
-
}
|
|
1747
|
-
expect(mockFetch).toHaveBeenCalledWith('http://new-guardian.com/pubkey?scheme=falcon', expect.objectContaining({ method: 'GET' }));
|
|
1748
|
-
});
|
|
1749
|
-
it('should reject switch proposal when endpoint commitment does not match', async () => {
|
|
1750
|
-
vi.mocked(executeForSummary).mockResolvedValue({
|
|
1751
|
-
summary: {
|
|
1752
|
-
serialize: () => new Uint8Array([1, 2, 3]),
|
|
1753
|
-
},
|
|
1754
|
-
anchor: createMockChainAnchor(),
|
|
1755
|
-
});
|
|
1756
|
-
const config = {
|
|
1757
|
-
threshold: 1,
|
|
1758
|
-
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
1759
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
1760
|
-
};
|
|
1761
|
-
const multisig = createTestMultisig(config);
|
|
1762
|
-
mockFetch.mockResolvedValueOnce({
|
|
1763
|
-
ok: true,
|
|
1764
|
-
json: async () => ({ commitment: '0x' + '2'.repeat(64) }),
|
|
1765
|
-
});
|
|
1766
|
-
await expect(multisig.createSwitchGuardianProposal('http://new-guardian.com', '0x' + '1'.repeat(64))).rejects.toThrow('Refusing to use GUARDIAN endpoint');
|
|
1767
|
-
});
|
|
1768
|
-
it('should use the signer scheme when resolving new GUARDIAN commitments', async () => {
|
|
1769
|
-
vi.mocked(executeForSummary).mockResolvedValue({
|
|
1770
|
-
summary: {
|
|
1771
|
-
serialize: () => new Uint8Array([1, 2, 3]),
|
|
1772
|
-
},
|
|
1773
|
-
anchor: createMockChainAnchor(),
|
|
1774
|
-
});
|
|
1775
|
-
const ecdsaSigner = {
|
|
1776
|
-
...mockSigner,
|
|
1777
|
-
publicKey: '0x' + '2'.repeat(66),
|
|
1778
|
-
scheme: 'ecdsa',
|
|
1779
|
-
};
|
|
1780
|
-
guardian.setSigner(ecdsaSigner);
|
|
1781
|
-
const config = {
|
|
1782
|
-
threshold: 1,
|
|
1783
|
-
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
1784
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
1785
|
-
};
|
|
1786
|
-
const multisig = createTestMultisig(config, ecdsaSigner);
|
|
1787
|
-
const newGuardianCommitment = '0x' + '1'.repeat(64);
|
|
1788
|
-
mockFetch.mockResolvedValueOnce({
|
|
1789
|
-
ok: true,
|
|
1790
|
-
json: async () => ({ commitment: newGuardianCommitment }),
|
|
1791
|
-
});
|
|
1792
|
-
mockFetch.mockResolvedValueOnce({
|
|
1793
|
-
ok: true,
|
|
1794
|
-
json: async () => ({
|
|
1795
|
-
delta: {
|
|
1796
|
-
account_id: multisig.accountId,
|
|
1797
|
-
nonce: 1,
|
|
1798
|
-
prev_commitment: '0x' + 'b'.repeat(64),
|
|
1799
|
-
delta_payload: { tx_summary: { data: 'AQID' }, signatures: [] },
|
|
1800
|
-
status: {
|
|
1801
|
-
status: 'pending',
|
|
1802
|
-
timestamp: '2024-01-01T00:00:00Z',
|
|
1803
|
-
proposer_id: '0x' + 'c'.repeat(64),
|
|
1804
|
-
cosigner_sigs: [],
|
|
1805
|
-
},
|
|
1806
|
-
},
|
|
1807
|
-
commitment: '0x' + 'c'.repeat(64),
|
|
1808
|
-
}),
|
|
1809
|
-
});
|
|
1810
|
-
await multisig.createSwitchGuardianProposal('http://new-guardian.com', newGuardianCommitment);
|
|
1811
|
-
expect(mockFetch).toHaveBeenCalledWith('http://new-guardian.com/pubkey?scheme=ecdsa', expect.objectContaining({ method: 'GET' }));
|
|
1812
|
-
expect(buildUpdateGuardianTransactionRequest).toHaveBeenCalledWith(mockWebClient, newGuardianCommitment, { signatureScheme: 'ecdsa' });
|
|
1813
|
-
});
|
|
1814
|
-
});
|
|
1815
|
-
describe('createUpdateProcedureThresholdProposal', () => {
|
|
1816
|
-
it('should create procedure-threshold update proposals', async () => {
|
|
1817
|
-
vi.mocked(executeForSummary).mockResolvedValue({
|
|
1818
|
-
summary: {
|
|
1819
|
-
toCommitment: () => ({
|
|
1820
|
-
toHex: () => '0x' + 'c'.repeat(64),
|
|
1821
|
-
}),
|
|
1822
|
-
serialize: () => new Uint8Array([1, 2, 3]),
|
|
1823
|
-
},
|
|
1824
|
-
anchor: createMockChainAnchor(),
|
|
1825
|
-
});
|
|
1826
|
-
const config = {
|
|
1827
|
-
threshold: 2,
|
|
1828
|
-
signerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
|
|
1829
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
1830
|
-
};
|
|
1831
|
-
const multisig = createTestMultisig(config);
|
|
1832
|
-
const mockDelta = {
|
|
1833
|
-
account_id: '0x' + 'a'.repeat(30),
|
|
1834
|
-
nonce: 1,
|
|
1835
|
-
prev_commitment: '0x' + 'b'.repeat(64),
|
|
1836
|
-
delta_payload: {
|
|
1837
|
-
tx_summary: { data: 'AQID' },
|
|
1838
|
-
signatures: [],
|
|
1839
|
-
metadata: {
|
|
1840
|
-
proposal_type: 'update_procedure_threshold',
|
|
1841
|
-
chain_anchor: MOCK_CHAIN_ANCHOR_B64,
|
|
1842
|
-
target_threshold: 1,
|
|
1843
|
-
target_procedure: 'send_asset',
|
|
1844
|
-
description: '',
|
|
1845
|
-
},
|
|
1846
|
-
},
|
|
1847
|
-
status: {
|
|
1848
|
-
status: 'pending',
|
|
1849
|
-
timestamp: '2024-01-01T00:00:00Z',
|
|
1850
|
-
proposer_id: '0x' + 'c'.repeat(64),
|
|
1851
|
-
cosigner_sigs: [],
|
|
1852
|
-
},
|
|
1853
|
-
};
|
|
1854
|
-
mockFetch.mockResolvedValueOnce({
|
|
1855
|
-
ok: true,
|
|
1856
|
-
json: async () => ({
|
|
1857
|
-
delta: mockDelta,
|
|
1858
|
-
commitment: '0x' + 'c'.repeat(64),
|
|
1859
|
-
}),
|
|
1860
|
-
});
|
|
1861
|
-
const proposal = await multisig.createUpdateProcedureThresholdProposal('send_asset', 1, { nonce: 1 });
|
|
1862
|
-
expect(buildUpdateProcedureThresholdTransactionRequest).toHaveBeenCalledWith(mockWebClient, 'send_asset', 1, { signatureScheme: 'falcon' });
|
|
1863
|
-
expect(proposal.metadata.proposalType).toBe('update_procedure_threshold');
|
|
1864
|
-
if (proposal.metadata.proposalType === 'update_procedure_threshold') {
|
|
1865
|
-
expect(proposal.metadata.targetProcedure).toBe('send_asset');
|
|
1866
|
-
expect(proposal.metadata.targetThreshold).toBe(1);
|
|
1867
|
-
}
|
|
1868
|
-
});
|
|
1869
|
-
it('passes the signer scheme to ECDSA procedure-threshold updates', async () => {
|
|
1870
|
-
vi.mocked(executeForSummary).mockResolvedValue({
|
|
1871
|
-
summary: {
|
|
1872
|
-
toCommitment: () => ({
|
|
1873
|
-
toHex: () => '0x' + 'c'.repeat(64),
|
|
1874
|
-
}),
|
|
1875
|
-
serialize: () => new Uint8Array([1, 2, 3]),
|
|
1876
|
-
},
|
|
1877
|
-
anchor: createMockChainAnchor(),
|
|
1878
|
-
});
|
|
1879
|
-
const ecdsaSigner = {
|
|
1880
|
-
...mockSigner,
|
|
1881
|
-
publicKey: '0x' + '2'.repeat(66),
|
|
1882
|
-
scheme: 'ecdsa',
|
|
1883
|
-
};
|
|
1884
|
-
guardian.setSigner(ecdsaSigner);
|
|
1885
|
-
const config = {
|
|
1886
|
-
threshold: 2,
|
|
1887
|
-
signerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
|
|
1888
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
1889
|
-
};
|
|
1890
|
-
const multisig = createTestMultisig(config, ecdsaSigner);
|
|
1891
|
-
const mockDelta = {
|
|
1892
|
-
account_id: '0x' + 'a'.repeat(30),
|
|
1893
|
-
nonce: 1,
|
|
1894
|
-
prev_commitment: '0x' + 'b'.repeat(64),
|
|
1895
|
-
delta_payload: {
|
|
1896
|
-
tx_summary: { data: 'AQID' },
|
|
1897
|
-
signatures: [],
|
|
1898
|
-
metadata: {
|
|
1899
|
-
proposal_type: 'update_procedure_threshold',
|
|
1900
|
-
chain_anchor: MOCK_CHAIN_ANCHOR_B64,
|
|
1901
|
-
target_threshold: 1,
|
|
1902
|
-
target_procedure: 'send_asset',
|
|
1903
|
-
description: '',
|
|
1904
|
-
},
|
|
1905
|
-
},
|
|
1906
|
-
status: {
|
|
1907
|
-
status: 'pending',
|
|
1908
|
-
timestamp: '2024-01-01T00:00:00Z',
|
|
1909
|
-
proposer_id: '0x' + 'c'.repeat(64),
|
|
1910
|
-
cosigner_sigs: [],
|
|
1911
|
-
},
|
|
1912
|
-
};
|
|
1913
|
-
mockFetch.mockResolvedValueOnce({
|
|
1914
|
-
ok: true,
|
|
1915
|
-
json: async () => ({
|
|
1916
|
-
delta: mockDelta,
|
|
1917
|
-
commitment: '0x' + 'c'.repeat(64),
|
|
1918
|
-
}),
|
|
1919
|
-
});
|
|
1920
|
-
await multisig.createUpdateProcedureThresholdProposal('send_asset', 1, { nonce: 1 });
|
|
1921
|
-
expect(buildUpdateProcedureThresholdTransactionRequest).toHaveBeenCalledWith(mockWebClient, 'send_asset', 1, { signatureScheme: 'ecdsa' });
|
|
1922
|
-
});
|
|
1923
|
-
});
|
|
1924
|
-
describe('signProposal', () => {
|
|
1925
|
-
it('should sign a proposal', async () => {
|
|
1926
|
-
const config = {
|
|
1927
|
-
threshold: 1,
|
|
1928
|
-
signerCommitments: [mockSigner.commitment],
|
|
1929
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
1930
|
-
};
|
|
1931
|
-
const multisig = createTestMultisig(config);
|
|
1932
|
-
// First create a proposal
|
|
1933
|
-
const mockDelta = {
|
|
1934
|
-
account_id: '0x' + 'a'.repeat(30),
|
|
1935
|
-
nonce: 1,
|
|
1936
|
-
prev_commitment: '0x' + 'b'.repeat(64),
|
|
1937
|
-
delta_payload: {
|
|
1938
|
-
tx_summary: { data: 'AQID' },
|
|
1939
|
-
signatures: [],
|
|
1940
|
-
},
|
|
1941
|
-
status: {
|
|
1942
|
-
status: 'pending',
|
|
1943
|
-
timestamp: '2024-01-01T00:00:00Z',
|
|
1944
|
-
proposer_id: '0x' + 'c'.repeat(64),
|
|
1945
|
-
cosigner_sigs: [],
|
|
1946
|
-
},
|
|
1947
|
-
};
|
|
1948
|
-
mockFetch.mockResolvedValueOnce({
|
|
1949
|
-
ok: true,
|
|
1950
|
-
json: async () => ({
|
|
1951
|
-
delta: mockDelta,
|
|
1952
|
-
commitment: '0x' + 'c'.repeat(64),
|
|
1953
|
-
}),
|
|
1954
|
-
});
|
|
1955
|
-
await multisig.createProposal(1, 'AQID', {
|
|
1956
|
-
proposalType: 'add_signer',
|
|
1957
|
-
chainAnchor: MOCK_CHAIN_ANCHOR_B64,
|
|
1958
|
-
targetThreshold: 1,
|
|
1959
|
-
targetSignerCommitments: ['0x' + 'a'.repeat(64)],
|
|
1960
|
-
description: '',
|
|
1961
|
-
});
|
|
1962
|
-
const signedDelta = {
|
|
1963
|
-
...mockDelta,
|
|
1964
|
-
status: {
|
|
1965
|
-
status: 'pending',
|
|
1966
|
-
timestamp: '2024-01-01T00:00:00Z',
|
|
1967
|
-
proposer_id: '0x' + 'c'.repeat(64),
|
|
1968
|
-
cosigner_sigs: [
|
|
1969
|
-
{
|
|
1970
|
-
signer_id: mockSigner.commitment,
|
|
1971
|
-
signature: { scheme: 'falcon', signature: '0x' + 'b'.repeat(128) },
|
|
1972
|
-
timestamp: '2024-01-01T01:00:00Z',
|
|
1973
|
-
},
|
|
1974
|
-
],
|
|
1975
|
-
},
|
|
1976
|
-
delta_payload: {
|
|
1977
|
-
...mockDelta.delta_payload,
|
|
1978
|
-
metadata: {
|
|
1979
|
-
proposal_type: 'add_signer',
|
|
1980
|
-
chain_anchor: MOCK_CHAIN_ANCHOR_B64,
|
|
1981
|
-
description: '',
|
|
1982
|
-
target_threshold: 1,
|
|
1983
|
-
signer_commitments: ['0x' + 'a'.repeat(64)],
|
|
1984
|
-
},
|
|
1985
|
-
},
|
|
1986
|
-
};
|
|
1987
|
-
mockFetch.mockResolvedValueOnce({
|
|
1988
|
-
ok: true,
|
|
1989
|
-
json: async () => signedDelta,
|
|
1990
|
-
});
|
|
1991
|
-
const proposalId = '0x' + 'c'.repeat(64);
|
|
1992
|
-
const signedProposal = await multisig.signProposal(proposalId);
|
|
1993
|
-
expect(mockSigner.signCommitment).toHaveBeenCalledWith(proposalId);
|
|
1994
|
-
expect(signedProposal.signatures.length).toBe(1);
|
|
1995
|
-
});
|
|
1996
|
-
it('should reject signing when metadata does not match tx_summary', async () => {
|
|
1997
|
-
const config = {
|
|
1998
|
-
threshold: 1,
|
|
1999
|
-
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
2000
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
2001
|
-
};
|
|
2002
|
-
const multisig = createTestMultisig(config);
|
|
2003
|
-
const mockDelta = {
|
|
2004
|
-
account_id: '0x' + 'a'.repeat(30),
|
|
2005
|
-
nonce: 1,
|
|
2006
|
-
prev_commitment: '0x' + 'b'.repeat(64),
|
|
2007
|
-
delta_payload: {
|
|
2008
|
-
tx_summary: { data: 'AQID' },
|
|
2009
|
-
signatures: [],
|
|
2010
|
-
},
|
|
2011
|
-
status: {
|
|
2012
|
-
status: 'pending',
|
|
2013
|
-
timestamp: '2024-01-01T00:00:00Z',
|
|
2014
|
-
proposer_id: '0x' + 'c'.repeat(64),
|
|
2015
|
-
cosigner_sigs: [],
|
|
2016
|
-
},
|
|
2017
|
-
};
|
|
2018
|
-
mockFetch.mockResolvedValueOnce({
|
|
2019
|
-
ok: true,
|
|
2020
|
-
json: async () => ({
|
|
2021
|
-
delta: mockDelta,
|
|
2022
|
-
commitment: '0x' + 'c'.repeat(64),
|
|
2023
|
-
}),
|
|
2024
|
-
});
|
|
2025
|
-
await multisig.createProposal(1, 'AQID', {
|
|
2026
|
-
proposalType: 'add_signer',
|
|
2027
|
-
chainAnchor: MOCK_CHAIN_ANCHOR_B64,
|
|
2028
|
-
targetThreshold: 1,
|
|
2029
|
-
targetSignerCommitments: ['0x' + 'a'.repeat(64)],
|
|
2030
|
-
description: '',
|
|
2031
|
-
});
|
|
2032
|
-
vi.mocked(executeForSummaryAt).mockResolvedValueOnce({
|
|
2033
|
-
toCommitment: () => ({
|
|
2034
|
-
toHex: () => '0x' + 'f'.repeat(64),
|
|
2035
|
-
}),
|
|
2036
|
-
});
|
|
2037
|
-
await expect(multisig.signProposal('0x' + 'c'.repeat(64))).rejects.toThrow('Invalid proposal: metadata does not match tx_summary');
|
|
2038
|
-
});
|
|
2039
|
-
it('should reject proposals for a different account before signing', async () => {
|
|
2040
|
-
const config = {
|
|
2041
|
-
threshold: 1,
|
|
2042
|
-
signerCommitments: [mockSigner.commitment],
|
|
2043
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
2044
|
-
};
|
|
2045
|
-
const multisig = createTestMultisig(config);
|
|
2046
|
-
const proposalId = '0x' + 'd'.repeat(64);
|
|
2047
|
-
mockFetch.mockResolvedValueOnce({
|
|
2048
|
-
ok: true,
|
|
2049
|
-
json: async () => ({
|
|
2050
|
-
proposals: [
|
|
2051
|
-
{
|
|
2052
|
-
account_id: '0x' + 'f'.repeat(30),
|
|
2053
|
-
nonce: 1,
|
|
2054
|
-
prev_commitment: '0x' + 'b'.repeat(64),
|
|
2055
|
-
delta_payload: {
|
|
2056
|
-
tx_summary: { data: 'AQID' },
|
|
2057
|
-
signatures: [],
|
|
2058
|
-
metadata: {
|
|
2059
|
-
proposal_type: 'add_signer',
|
|
2060
|
-
chain_anchor: MOCK_CHAIN_ANCHOR_B64,
|
|
2061
|
-
description: '',
|
|
2062
|
-
target_threshold: 1,
|
|
2063
|
-
signer_commitments: [mockSigner.commitment],
|
|
2064
|
-
},
|
|
2065
|
-
},
|
|
2066
|
-
status: {
|
|
2067
|
-
status: 'pending',
|
|
2068
|
-
timestamp: '2024-01-01T00:00:00Z',
|
|
2069
|
-
proposer_id: '0x' + 'c'.repeat(64),
|
|
2070
|
-
cosigner_sigs: [],
|
|
2071
|
-
},
|
|
2072
|
-
},
|
|
2073
|
-
],
|
|
2074
|
-
}),
|
|
2075
|
-
});
|
|
2076
|
-
await expect(multisig.signProposal(proposalId)).rejects.toThrow('Proposal is for a different account: 0x' + 'f'.repeat(30));
|
|
2077
|
-
expect(mockSigner.signCommitment).not.toHaveBeenCalled();
|
|
2078
|
-
});
|
|
2079
|
-
});
|
|
2080
|
-
describe('importProposal', () => {
|
|
2081
|
-
it('should reject imported proposals whose metadata does not match tx_summary', async () => {
|
|
2082
|
-
const config = {
|
|
2083
|
-
threshold: 1,
|
|
2084
|
-
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
2085
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
2086
|
-
};
|
|
2087
|
-
const multisig = createTestMultisig(config);
|
|
2088
|
-
vi.mocked(executeForSummaryAt).mockResolvedValueOnce({
|
|
2089
|
-
toCommitment: () => ({
|
|
2090
|
-
toHex: () => '0x' + 'f'.repeat(64),
|
|
2091
|
-
}),
|
|
2092
|
-
});
|
|
2093
|
-
await expect(multisig.importProposal(JSON.stringify({
|
|
2094
|
-
accountId: '0x' + 'a'.repeat(30),
|
|
2095
|
-
nonce: 1,
|
|
2096
|
-
commitment: '0x' + 'c'.repeat(64),
|
|
2097
|
-
txSummaryBase64: 'AQID',
|
|
2098
|
-
signatures: [],
|
|
2099
|
-
metadata: {
|
|
2100
|
-
proposalType: 'add_signer',
|
|
2101
|
-
chainAnchor: MOCK_CHAIN_ANCHOR_B64,
|
|
2102
|
-
targetThreshold: 1,
|
|
2103
|
-
targetSignerCommitments: ['0x' + 'a'.repeat(64)],
|
|
2104
|
-
description: '',
|
|
2105
|
-
},
|
|
2106
|
-
}))).rejects.toThrow('Invalid proposal: metadata does not match tx_summary');
|
|
2107
|
-
});
|
|
2108
|
-
});
|
|
2109
|
-
describe('signProposalOffline', () => {
|
|
2110
|
-
it('should reject signing imported proposals whose metadata does not match tx_summary', async () => {
|
|
2111
|
-
const config = {
|
|
2112
|
-
threshold: 1,
|
|
2113
|
-
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
2114
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
2115
|
-
};
|
|
2116
|
-
const multisig = createTestMultisig(config);
|
|
2117
|
-
vi.mocked(executeForSummaryAt).mockResolvedValueOnce({
|
|
2118
|
-
toCommitment: () => ({
|
|
2119
|
-
toHex: () => '0x' + 'c'.repeat(64),
|
|
2120
|
-
}),
|
|
2121
|
-
});
|
|
2122
|
-
const proposal = await multisig.importProposal(JSON.stringify({
|
|
2123
|
-
accountId: '0x' + 'a'.repeat(30),
|
|
2124
|
-
nonce: 1,
|
|
2125
|
-
commitment: '0x' + 'c'.repeat(64),
|
|
2126
|
-
txSummaryBase64: 'AQID',
|
|
2127
|
-
signatures: [],
|
|
2128
|
-
metadata: {
|
|
2129
|
-
proposalType: 'add_signer',
|
|
2130
|
-
chainAnchor: MOCK_CHAIN_ANCHOR_B64,
|
|
2131
|
-
targetThreshold: 1,
|
|
2132
|
-
targetSignerCommitments: ['0x' + 'a'.repeat(64)],
|
|
2133
|
-
description: '',
|
|
2134
|
-
},
|
|
2135
|
-
}));
|
|
2136
|
-
proposal.metadata = {
|
|
2137
|
-
proposalType: 'add_signer',
|
|
2138
|
-
chainAnchor: MOCK_CHAIN_ANCHOR_B64,
|
|
2139
|
-
targetThreshold: 2,
|
|
2140
|
-
targetSignerCommitments: ['0x' + 'a'.repeat(64)],
|
|
2141
|
-
description: '',
|
|
2142
|
-
};
|
|
2143
|
-
vi.mocked(executeForSummaryAt).mockResolvedValueOnce({
|
|
2144
|
-
toCommitment: () => ({
|
|
2145
|
-
toHex: () => '0x' + 'f'.repeat(64),
|
|
2146
|
-
}),
|
|
2147
|
-
});
|
|
2148
|
-
await expect(multisig.signProposalOffline(proposal.id)).rejects.toThrow('Invalid proposal: metadata does not match tx_summary');
|
|
2149
|
-
});
|
|
2150
|
-
});
|
|
2151
|
-
describe('exportProposal', () => {
|
|
2152
|
-
it('should export proposal for offline signing', async () => {
|
|
2153
|
-
const config = {
|
|
2154
|
-
threshold: 2,
|
|
2155
|
-
signerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
|
|
2156
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
2157
|
-
};
|
|
2158
|
-
const multisig = createTestMultisig(config);
|
|
2159
|
-
const mockProposals = [
|
|
2160
|
-
{
|
|
2161
|
-
account_id: '0x' + 'a'.repeat(30),
|
|
2162
|
-
nonce: 1,
|
|
2163
|
-
prev_commitment: '0x' + 'b'.repeat(64),
|
|
2164
|
-
delta_payload: {
|
|
2165
|
-
tx_summary: { data: 'AQID' },
|
|
2166
|
-
signatures: [],
|
|
2167
|
-
metadata: {
|
|
2168
|
-
proposal_type: 'add_signer',
|
|
2169
|
-
chain_anchor: MOCK_CHAIN_ANCHOR_B64,
|
|
2170
|
-
description: '',
|
|
2171
|
-
target_threshold: 1,
|
|
2172
|
-
signer_commitments: ['0x' + 'a'.repeat(64)],
|
|
2173
|
-
},
|
|
2174
|
-
},
|
|
2175
|
-
status: {
|
|
2176
|
-
status: 'pending',
|
|
2177
|
-
timestamp: '2024-01-01T00:00:00Z',
|
|
2178
|
-
proposer_id: '0x' + 'c'.repeat(64),
|
|
2179
|
-
cosigner_sigs: [
|
|
2180
|
-
{
|
|
2181
|
-
signer_id: '0x' + 'a'.repeat(64),
|
|
2182
|
-
signature: { scheme: 'falcon', signature: '0x' + 'e'.repeat(128) },
|
|
2183
|
-
timestamp: '2024-01-01T00:00:00Z',
|
|
2184
|
-
},
|
|
2185
|
-
],
|
|
2186
|
-
},
|
|
2187
|
-
},
|
|
2188
|
-
];
|
|
2189
|
-
mockFetch.mockResolvedValueOnce({
|
|
2190
|
-
ok: true,
|
|
2191
|
-
json: async () => mockProposals[0],
|
|
2192
|
-
});
|
|
2193
|
-
// The proposal ID is computed from tx_summary, which is mocked to return 'c'.repeat(64)
|
|
2194
|
-
const exported = await multisig.exportProposal('0x' + 'c'.repeat(64));
|
|
2195
|
-
expect(exported.accountId).toBe('0x' + 'a'.repeat(30));
|
|
2196
|
-
expect(exported.nonce).toBe(1);
|
|
2197
|
-
expect(exported.txSummaryBase64).toBe('AQID');
|
|
2198
|
-
expect(exported.signatures.length).toBe(1);
|
|
2199
|
-
});
|
|
2200
|
-
it('should preserve ECDSA signature metadata in exported proposals', async () => {
|
|
2201
|
-
const config = {
|
|
2202
|
-
threshold: 2,
|
|
2203
|
-
signerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
|
|
2204
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
2205
|
-
};
|
|
2206
|
-
const multisig = createTestMultisig(config);
|
|
2207
|
-
const publicKey = '0x' + 'd'.repeat(66);
|
|
2208
|
-
mockFetch.mockResolvedValueOnce({
|
|
2209
|
-
ok: true,
|
|
2210
|
-
json: async () => ({
|
|
2211
|
-
account_id: '0x' + 'a'.repeat(30),
|
|
2212
|
-
nonce: 1,
|
|
2213
|
-
prev_commitment: '0x' + 'b'.repeat(64),
|
|
2214
|
-
delta_payload: {
|
|
2215
|
-
tx_summary: { data: 'AQID' },
|
|
2216
|
-
signatures: [],
|
|
2217
|
-
metadata: {
|
|
2218
|
-
proposal_type: 'change_threshold',
|
|
2219
|
-
chain_anchor: MOCK_CHAIN_ANCHOR_B64,
|
|
2220
|
-
description: '',
|
|
2221
|
-
target_threshold: 2,
|
|
2222
|
-
signer_commitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
|
|
2223
|
-
},
|
|
2224
|
-
},
|
|
2225
|
-
status: {
|
|
2226
|
-
status: 'pending',
|
|
2227
|
-
timestamp: '2024-01-01T00:00:00Z',
|
|
2228
|
-
proposer_id: '0x' + 'c'.repeat(64),
|
|
2229
|
-
cosigner_sigs: [
|
|
2230
|
-
{
|
|
2231
|
-
signer_id: '0x' + 'a'.repeat(64),
|
|
2232
|
-
signature: {
|
|
2233
|
-
scheme: 'ecdsa',
|
|
2234
|
-
signature: '0x' + 'e'.repeat(130),
|
|
2235
|
-
public_key: publicKey,
|
|
2236
|
-
},
|
|
2237
|
-
timestamp: '2024-01-01T00:00:00Z',
|
|
2238
|
-
},
|
|
2239
|
-
],
|
|
2240
|
-
},
|
|
2241
|
-
}),
|
|
2242
|
-
});
|
|
2243
|
-
const exported = await multisig.exportProposal('0x' + 'c'.repeat(64));
|
|
2244
|
-
expect(exported.signatures).toEqual([
|
|
2245
|
-
{
|
|
2246
|
-
commitment: '0x' + 'a'.repeat(64),
|
|
2247
|
-
signatureHex: '0x' + 'e'.repeat(130),
|
|
2248
|
-
scheme: 'ecdsa',
|
|
2249
|
-
publicKey,
|
|
2250
|
-
timestamp: '2024-01-01T00:00:00Z',
|
|
2251
|
-
},
|
|
2252
|
-
]);
|
|
2253
|
-
});
|
|
2254
|
-
it('should throw if proposal not found', async () => {
|
|
2255
|
-
const config = {
|
|
2256
|
-
threshold: 1,
|
|
2257
|
-
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
2258
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
2259
|
-
};
|
|
2260
|
-
const multisig = createTestMultisig(config);
|
|
2261
|
-
mockFetch.mockResolvedValueOnce({
|
|
2262
|
-
ok: false,
|
|
2263
|
-
status: 404,
|
|
2264
|
-
statusText: 'Not Found',
|
|
2265
|
-
headers: new Headers(),
|
|
2266
|
-
// Feature 009: only a conforming { code, message, meta } envelope is
|
|
2267
|
-
// folded into the error message; raw text bodies are dropped.
|
|
2268
|
-
text: async () => JSON.stringify({
|
|
2269
|
-
code: 'proposal_not_found',
|
|
2270
|
-
message: 'Proposal not found',
|
|
2271
|
-
meta: { retryable: false },
|
|
2272
|
-
}),
|
|
2273
|
-
});
|
|
2274
|
-
await expect(multisig.exportProposal('0x' + 'nonexistent'.repeat(5))).rejects.toThrow('Proposal not found');
|
|
2275
|
-
});
|
|
2276
|
-
});
|
|
2277
|
-
describe('importProposal', () => {
|
|
2278
|
-
it('should reject imported signatures with non-32-byte signer IDs', async () => {
|
|
2279
|
-
const config = {
|
|
2280
|
-
threshold: 1,
|
|
2281
|
-
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
2282
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
2283
|
-
};
|
|
2284
|
-
const multisig = createTestMultisig(config);
|
|
2285
|
-
const exported = {
|
|
2286
|
-
accountId: multisig.accountId,
|
|
2287
|
-
nonce: 1,
|
|
2288
|
-
commitment: '0x' + 'c'.repeat(64),
|
|
2289
|
-
txSummaryBase64: 'AQID',
|
|
2290
|
-
signatures: [
|
|
2291
|
-
{
|
|
2292
|
-
commitment: '0x1',
|
|
2293
|
-
signatureHex: '0x' + 'b'.repeat(128),
|
|
2294
|
-
},
|
|
2295
|
-
],
|
|
2296
|
-
metadata: {
|
|
2297
|
-
proposalType: 'add_signer',
|
|
2298
|
-
chainAnchor: MOCK_CHAIN_ANCHOR_B64,
|
|
2299
|
-
targetThreshold: 1,
|
|
2300
|
-
targetSignerCommitments: ['0x' + 'a'.repeat(64)],
|
|
2301
|
-
description: '',
|
|
2302
|
-
},
|
|
2303
|
-
};
|
|
2304
|
-
await expect(multisig.importProposal(JSON.stringify(exported))).rejects.toThrow('expected signerId as 32-byte hex');
|
|
2305
|
-
});
|
|
2306
|
-
it('should preserve ECDSA imported signature metadata', async () => {
|
|
2307
|
-
const config = {
|
|
2308
|
-
threshold: 1,
|
|
2309
|
-
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
2310
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
2311
|
-
};
|
|
2312
|
-
const multisig = createTestMultisig(config);
|
|
2313
|
-
const publicKey = '0x' + 'd'.repeat(66);
|
|
2314
|
-
const proposal = await multisig.importProposal(JSON.stringify({
|
|
2315
|
-
accountId: multisig.accountId,
|
|
2316
|
-
nonce: 1,
|
|
2317
|
-
commitment: '0x' + 'c'.repeat(64),
|
|
2318
|
-
txSummaryBase64: 'AQID',
|
|
2319
|
-
signatures: [
|
|
2320
|
-
{
|
|
2321
|
-
commitment: '0x' + 'a'.repeat(64),
|
|
2322
|
-
signatureHex: '0x' + 'b'.repeat(130),
|
|
2323
|
-
scheme: 'ecdsa',
|
|
2324
|
-
publicKey,
|
|
2325
|
-
timestamp: '2024-01-01T00:00:00Z',
|
|
2326
|
-
},
|
|
2327
|
-
],
|
|
2328
|
-
metadata: {
|
|
2329
|
-
proposalType: 'change_threshold',
|
|
2330
|
-
chainAnchor: MOCK_CHAIN_ANCHOR_B64,
|
|
2331
|
-
targetThreshold: 1,
|
|
2332
|
-
targetSignerCommitments: ['0x' + 'a'.repeat(64)],
|
|
2333
|
-
description: '',
|
|
2334
|
-
},
|
|
2335
|
-
}));
|
|
2336
|
-
expect(proposal.signatures).toEqual([
|
|
2337
|
-
{
|
|
2338
|
-
signerId: '0x' + 'a'.repeat(64),
|
|
2339
|
-
signature: {
|
|
2340
|
-
scheme: 'ecdsa',
|
|
2341
|
-
signature: '0x' + 'b'.repeat(130),
|
|
2342
|
-
publicKey,
|
|
2343
|
-
},
|
|
2344
|
-
timestamp: '2024-01-01T00:00:00Z',
|
|
2345
|
-
},
|
|
2346
|
-
]);
|
|
2347
|
-
});
|
|
2348
|
-
it('should reject imported ECDSA signatures without a public key', async () => {
|
|
2349
|
-
const config = {
|
|
2350
|
-
threshold: 1,
|
|
2351
|
-
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
2352
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
2353
|
-
};
|
|
2354
|
-
const multisig = createTestMultisig(config);
|
|
2355
|
-
await expect(multisig.importProposal(JSON.stringify({
|
|
2356
|
-
accountId: multisig.accountId,
|
|
2357
|
-
nonce: 1,
|
|
2358
|
-
commitment: '0x' + 'c'.repeat(64),
|
|
2359
|
-
txSummaryBase64: 'AQID',
|
|
2360
|
-
signatures: [
|
|
2361
|
-
{
|
|
2362
|
-
commitment: '0x' + 'a'.repeat(64),
|
|
2363
|
-
signatureHex: '0x' + 'b'.repeat(130),
|
|
2364
|
-
scheme: 'ecdsa',
|
|
2365
|
-
},
|
|
2366
|
-
],
|
|
2367
|
-
metadata: {
|
|
2368
|
-
proposalType: 'change_threshold',
|
|
2369
|
-
chainAnchor: MOCK_CHAIN_ANCHOR_B64,
|
|
2370
|
-
targetThreshold: 1,
|
|
2371
|
-
targetSignerCommitments: ['0x' + 'a'.repeat(64)],
|
|
2372
|
-
description: '',
|
|
2373
|
-
},
|
|
2374
|
-
}))).rejects.toThrow('ECDSA signature for 0x' + 'a'.repeat(64) + ' is missing publicKey');
|
|
2375
|
-
});
|
|
2376
|
-
it('should reject offline signing if an imported proposal account is changed', async () => {
|
|
2377
|
-
const config = {
|
|
2378
|
-
threshold: 2,
|
|
2379
|
-
signerCommitments: ['0x' + 'a'.repeat(64), mockSigner.commitment],
|
|
2380
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
2381
|
-
};
|
|
2382
|
-
const multisig = createTestMultisig(config);
|
|
2383
|
-
const exported = {
|
|
2384
|
-
accountId: multisig.accountId,
|
|
2385
|
-
nonce: 1,
|
|
2386
|
-
commitment: '0x' + 'c'.repeat(64),
|
|
2387
|
-
txSummaryBase64: 'AQID',
|
|
2388
|
-
signatures: [],
|
|
2389
|
-
metadata: {
|
|
2390
|
-
proposalType: 'add_signer',
|
|
2391
|
-
chainAnchor: MOCK_CHAIN_ANCHOR_B64,
|
|
2392
|
-
targetThreshold: 2,
|
|
2393
|
-
targetSignerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
|
|
2394
|
-
description: '',
|
|
2395
|
-
},
|
|
2396
|
-
};
|
|
2397
|
-
const proposal = await multisig.importProposal(JSON.stringify(exported));
|
|
2398
|
-
proposal.accountId = '0x' + 'f'.repeat(30);
|
|
2399
|
-
await expect(multisig.signProposalOffline(proposal.id)).rejects.toThrow('Proposal is for a different account: 0x' + 'f'.repeat(30));
|
|
2400
|
-
expect(mockSigner.signCommitment).not.toHaveBeenCalled();
|
|
2401
|
-
});
|
|
2402
|
-
});
|
|
2403
|
-
describe('createTransactionProposalRequest', () => {
|
|
2404
|
-
it('should return a ready non-switch_guardian request without executing it', async () => {
|
|
2405
|
-
const { buildSignatureAdviceEntry, signatureHexToBytes } = await import('./utils/signature.js');
|
|
2406
|
-
vi.mocked(signatureHexToBytes).mockClear();
|
|
2407
|
-
vi.mocked(buildSignatureAdviceEntry).mockClear();
|
|
2408
|
-
const config = {
|
|
2409
|
-
threshold: 1,
|
|
2410
|
-
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
2411
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
2412
|
-
guardianPublicKey: '0x' + '1'.repeat(66),
|
|
2413
|
-
};
|
|
2414
|
-
const ecdsaSigner = {
|
|
2415
|
-
...mockSigner,
|
|
2416
|
-
scheme: 'ecdsa',
|
|
2417
|
-
publicKey: '0x' + '2'.repeat(66),
|
|
2418
|
-
};
|
|
2419
|
-
const multisig = createTestMultisig(config, ecdsaSigner);
|
|
2420
|
-
const cachedProposalId = '0x' + 'c'.repeat(64);
|
|
2421
|
-
const requestedProposalId = '0x' + 'C'.repeat(64);
|
|
2422
|
-
const cosignerPubkey = '0x' + '3'.repeat(66);
|
|
2423
|
-
const ackPubkey = '0x' + '4'.repeat(66);
|
|
2424
|
-
const cosignerSignature = '0x' + '5'.repeat(130);
|
|
2425
|
-
const ackSignature = '0x' + '6'.repeat(130);
|
|
2426
|
-
const finalRequest = { kind: 'final-change-threshold-request' };
|
|
2427
|
-
vi.mocked(buildUpdateSignersTransactionRequest)
|
|
2428
|
-
.mockResolvedValueOnce({
|
|
2429
|
-
request: { kind: 'verify-change-threshold-request' },
|
|
2430
|
-
salt: { toHex: () => '0x' + 'd'.repeat(64) },
|
|
2431
|
-
configHash: { toHex: () => '0x' + 'e'.repeat(64) },
|
|
2432
|
-
})
|
|
2433
|
-
.mockResolvedValueOnce({
|
|
2434
|
-
request: finalRequest,
|
|
2435
|
-
salt: { toHex: () => '0x' + 'd'.repeat(64) },
|
|
2436
|
-
configHash: { toHex: () => '0x' + 'e'.repeat(64) },
|
|
2437
|
-
});
|
|
2438
|
-
multisig.proposals.set(cachedProposalId, {
|
|
2439
|
-
id: cachedProposalId,
|
|
2440
|
-
accountId: multisig.accountId,
|
|
2441
|
-
nonce: 1,
|
|
2442
|
-
status: 'ready',
|
|
2443
|
-
txSummary: 'AQID',
|
|
2444
|
-
signatures: [
|
|
2445
|
-
{
|
|
2446
|
-
signerId: '0x' + 'a'.repeat(64),
|
|
2447
|
-
signature: {
|
|
2448
|
-
scheme: 'ecdsa',
|
|
2449
|
-
signature: cosignerSignature,
|
|
2450
|
-
publicKey: cosignerPubkey,
|
|
2451
|
-
},
|
|
2452
|
-
timestamp: '2024-01-01T00:00:00Z',
|
|
2453
|
-
},
|
|
2454
|
-
],
|
|
2455
|
-
metadata: {
|
|
2456
|
-
proposalType: 'change_threshold',
|
|
2457
|
-
chainAnchor: MOCK_CHAIN_ANCHOR_B64,
|
|
2458
|
-
targetThreshold: 1,
|
|
2459
|
-
targetSignerCommitments: ['0x' + 'a'.repeat(64)],
|
|
2460
|
-
description: '',
|
|
2461
|
-
},
|
|
2462
|
-
});
|
|
2463
|
-
mockFetch.mockResolvedValueOnce({
|
|
2464
|
-
ok: true,
|
|
2465
|
-
json: async () => ({
|
|
2466
|
-
account_id: multisig.accountId,
|
|
2467
|
-
nonce: 1,
|
|
2468
|
-
prev_commitment: '0x' + 'b'.repeat(64),
|
|
2469
|
-
delta_payload: {
|
|
2470
|
-
tx_summary: { data: 'AQID' },
|
|
2471
|
-
signatures: [],
|
|
2472
|
-
metadata: {
|
|
2473
|
-
proposal_type: 'change_threshold',
|
|
2474
|
-
chain_anchor: MOCK_CHAIN_ANCHOR_B64,
|
|
2475
|
-
target_threshold: 1,
|
|
2476
|
-
signer_commitments: ['0x' + 'a'.repeat(64)],
|
|
2477
|
-
},
|
|
2478
|
-
},
|
|
2479
|
-
status: {
|
|
2480
|
-
status: 'pending',
|
|
2481
|
-
timestamp: '2024-01-01T00:00:00Z',
|
|
2482
|
-
proposer_id: '0x' + 'a'.repeat(64),
|
|
2483
|
-
cosigner_sigs: [],
|
|
2484
|
-
},
|
|
2485
|
-
}),
|
|
2486
|
-
});
|
|
2487
|
-
mockFetch.mockResolvedValueOnce({
|
|
2488
|
-
ok: true,
|
|
2489
|
-
json: async () => ({
|
|
2490
|
-
account_id: multisig.accountId,
|
|
2491
|
-
nonce: 1,
|
|
2492
|
-
ack_sig: ackSignature,
|
|
2493
|
-
ack_pubkey: ackPubkey,
|
|
2494
|
-
ack_scheme: 'ecdsa',
|
|
2495
|
-
}),
|
|
2496
|
-
});
|
|
2497
|
-
await expect(multisig.createTransactionProposalRequest(requestedProposalId)).resolves.toBe(finalRequest);
|
|
2498
|
-
expect(vi.mocked(signatureHexToBytes)).toHaveBeenNthCalledWith(1, cosignerSignature, 'ecdsa');
|
|
2499
|
-
expect(vi.mocked(signatureHexToBytes)).toHaveBeenNthCalledWith(2, ackSignature, 'ecdsa');
|
|
2500
|
-
expect(vi.mocked(buildSignatureAdviceEntry)).toHaveBeenNthCalledWith(1, expect.anything(), expect.anything(), expect.anything());
|
|
2501
|
-
expect(vi.mocked(buildSignatureAdviceEntry)).toHaveBeenNthCalledWith(2, expect.anything(), expect.anything(), expect.anything());
|
|
2502
|
-
// The advice payload now comes from the SDK, so the routing assertion is
|
|
2503
|
-
// the commitment each entry is keyed on: cosigner first, GUARDIAN ack
|
|
2504
|
-
// second. Swapping the two entries must not pass.
|
|
2505
|
-
const adviceCalls = vi.mocked(buildSignatureAdviceEntry).mock.calls;
|
|
2506
|
-
expect(adviceCalls[0][0].toHex()).toBe(config.signerCommitments[0]);
|
|
2507
|
-
expect(adviceCalls[1][0].toHex()).toBe(config.guardianCommitment);
|
|
2508
|
-
expect(mockWebClient.executeTransaction).not.toHaveBeenCalled();
|
|
2509
|
-
expect(mockWebClient.proveTransaction).not.toHaveBeenCalled();
|
|
2510
|
-
expect(mockWebClient.submitProvenTransaction).not.toHaveBeenCalled();
|
|
2511
|
-
expect(mockWebClient.applyTransaction).not.toHaveBeenCalled();
|
|
2512
|
-
});
|
|
2513
|
-
it('should return a ready switch_guardian request without executing it', async () => {
|
|
2514
|
-
const config = {
|
|
2515
|
-
threshold: 1,
|
|
2516
|
-
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
2517
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
2518
|
-
};
|
|
2519
|
-
const multisig = createTestMultisig(config);
|
|
2520
|
-
const proposalId = '0x' + 'c'.repeat(64);
|
|
2521
|
-
const newGuardianPubkey = '0x' + '1'.repeat(64);
|
|
2522
|
-
const finalRequest = { kind: 'final-switch-guardian-request' };
|
|
2523
|
-
// switch_guardian is exempt from binding re-execution, so only the single
|
|
2524
|
-
// final-request build happens here.
|
|
2525
|
-
vi.mocked(buildUpdateGuardianTransactionRequest)
|
|
2526
|
-
.mockResolvedValueOnce({
|
|
2527
|
-
request: finalRequest,
|
|
2528
|
-
salt: { toHex: () => '0x' + 'd'.repeat(64) },
|
|
2529
|
-
});
|
|
2530
|
-
multisig.proposals.set(proposalId, {
|
|
2531
|
-
id: proposalId,
|
|
2532
|
-
accountId: multisig.accountId,
|
|
2533
|
-
nonce: 1,
|
|
2534
|
-
status: 'ready',
|
|
2535
|
-
txSummary: 'AQID',
|
|
2536
|
-
signatures: [
|
|
2537
|
-
{
|
|
2538
|
-
signerId: '0x' + 'a'.repeat(64),
|
|
2539
|
-
signature: { scheme: 'falcon', signature: '0x' + 'b'.repeat(128) },
|
|
2540
|
-
timestamp: '2024-01-01T00:00:00Z',
|
|
2541
|
-
},
|
|
2542
|
-
],
|
|
2543
|
-
metadata: {
|
|
2544
|
-
proposalType: 'switch_guardian',
|
|
2545
|
-
chainAnchor: MOCK_CHAIN_ANCHOR_B64,
|
|
2546
|
-
newGuardianPubkey,
|
|
2547
|
-
newGuardianEndpoint: 'http://new-guardian.com',
|
|
2548
|
-
description: '',
|
|
2549
|
-
},
|
|
2550
|
-
});
|
|
2551
|
-
mockFetch.mockResolvedValueOnce({
|
|
2552
|
-
ok: true,
|
|
2553
|
-
json: async () => ({ commitment: newGuardianPubkey }),
|
|
2554
|
-
});
|
|
2555
|
-
await expect(multisig.createTransactionProposalRequest(proposalId)).resolves.toBe(finalRequest);
|
|
2556
|
-
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
2557
|
-
expect(mockWebClient.executeTransaction).not.toHaveBeenCalled();
|
|
2558
|
-
expect(mockWebClient.proveTransaction).not.toHaveBeenCalled();
|
|
2559
|
-
expect(mockWebClient.submitProvenTransaction).not.toHaveBeenCalled();
|
|
2560
|
-
expect(mockWebClient.applyTransaction).not.toHaveBeenCalled();
|
|
2561
|
-
});
|
|
2562
|
-
it('should throw if proposal not found locally', async () => {
|
|
2563
|
-
const config = {
|
|
2564
|
-
threshold: 1,
|
|
2565
|
-
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
2566
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
2567
|
-
};
|
|
2568
|
-
const multisig = createTestMultisig(config);
|
|
2569
|
-
await expect(multisig.createTransactionProposalRequest('0x' + 'nonexistent'.repeat(5))).rejects.toThrow('Proposal not found');
|
|
2570
|
-
});
|
|
2571
|
-
it('should throw if proposal is still pending', async () => {
|
|
2572
|
-
const config = {
|
|
2573
|
-
threshold: 2,
|
|
2574
|
-
signerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
|
|
2575
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
2576
|
-
};
|
|
2577
|
-
const multisig = createTestMultisig(config);
|
|
2578
|
-
const mockProposals = [
|
|
2579
|
-
{
|
|
2580
|
-
account_id: '0x' + 'a'.repeat(30),
|
|
2581
|
-
nonce: 1,
|
|
2582
|
-
prev_commitment: '0x' + 'b'.repeat(64),
|
|
2583
|
-
delta_payload: {
|
|
2584
|
-
tx_summary: { data: 'AQID' },
|
|
2585
|
-
signatures: [],
|
|
2586
|
-
metadata: {
|
|
2587
|
-
proposal_type: 'add_signer',
|
|
2588
|
-
chain_anchor: MOCK_CHAIN_ANCHOR_B64,
|
|
2589
|
-
description: '',
|
|
2590
|
-
target_threshold: 2,
|
|
2591
|
-
signer_commitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
|
|
2592
|
-
},
|
|
2593
|
-
},
|
|
2594
|
-
status: {
|
|
2595
|
-
status: 'pending',
|
|
2596
|
-
timestamp: '2024-01-01T00:00:00Z',
|
|
2597
|
-
proposer_id: '0x' + 'c'.repeat(64),
|
|
2598
|
-
cosigner_sigs: [
|
|
2599
|
-
{
|
|
2600
|
-
signer_id: '0x' + 'a'.repeat(64),
|
|
2601
|
-
signature: { scheme: 'falcon', signature: '0x' + 'e'.repeat(128) },
|
|
2602
|
-
timestamp: '2024-01-01T00:00:00Z',
|
|
2603
|
-
},
|
|
2604
|
-
],
|
|
2605
|
-
},
|
|
2606
|
-
},
|
|
2607
|
-
];
|
|
2608
|
-
mockFetch.mockResolvedValueOnce({
|
|
2609
|
-
ok: true,
|
|
2610
|
-
json: async () => ({ proposals: mockProposals }),
|
|
2611
|
-
});
|
|
2612
|
-
await multisig.syncProposals();
|
|
2613
|
-
await expect(multisig.createTransactionProposalRequest('0x' + 'c'.repeat(64))).rejects.toThrow('not ready for execution');
|
|
2614
|
-
});
|
|
2615
|
-
it('should throw when proposal metadata does not match tx_summary', async () => {
|
|
2616
|
-
const config = {
|
|
2617
|
-
threshold: 1,
|
|
2618
|
-
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
2619
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
2620
|
-
};
|
|
2621
|
-
const multisig = createTestMultisig(config);
|
|
2622
|
-
const proposalId = '0x' + 'c'.repeat(64);
|
|
2623
|
-
vi.mocked(executeForSummaryAt).mockResolvedValueOnce({
|
|
2624
|
-
toCommitment: () => ({
|
|
2625
|
-
toHex: () => '0x' + 'd'.repeat(64),
|
|
2626
|
-
}),
|
|
2627
|
-
});
|
|
2628
|
-
multisig.proposals.set(proposalId, {
|
|
2629
|
-
id: proposalId,
|
|
2630
|
-
accountId: multisig.accountId,
|
|
2631
|
-
nonce: 1,
|
|
2632
|
-
status: 'ready',
|
|
2633
|
-
txSummary: 'AQID',
|
|
2634
|
-
signatures: [
|
|
2635
|
-
{
|
|
2636
|
-
signerId: '0x' + 'a'.repeat(64),
|
|
2637
|
-
signature: { scheme: 'falcon', signature: '0x' + 'b'.repeat(128) },
|
|
2638
|
-
timestamp: '2024-01-01T00:00:00Z',
|
|
2639
|
-
},
|
|
2640
|
-
],
|
|
2641
|
-
metadata: {
|
|
2642
|
-
proposalType: 'change_threshold',
|
|
2643
|
-
chainAnchor: MOCK_CHAIN_ANCHOR_B64,
|
|
2644
|
-
targetThreshold: 1,
|
|
2645
|
-
targetSignerCommitments: ['0x' + 'a'.repeat(64)],
|
|
2646
|
-
description: '',
|
|
2647
|
-
},
|
|
2648
|
-
});
|
|
2649
|
-
await expect(multisig.createTransactionProposalRequest(proposalId)).rejects.toThrow(`Invalid proposal: metadata does not match tx_summary for ${proposalId}`);
|
|
2650
|
-
expect(mockWebClient.executeTransaction).not.toHaveBeenCalled();
|
|
2651
|
-
});
|
|
2652
|
-
it('should reject switch_guardian requests when endpoint commitment mismatches', async () => {
|
|
2653
|
-
const config = {
|
|
2654
|
-
threshold: 1,
|
|
2655
|
-
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
2656
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
2657
|
-
};
|
|
2658
|
-
const multisig = createTestMultisig(config);
|
|
2659
|
-
const proposalId = '0x' + 'c'.repeat(64);
|
|
2660
|
-
multisig.proposals.set(proposalId, {
|
|
2661
|
-
id: proposalId,
|
|
2662
|
-
accountId: multisig.accountId,
|
|
2663
|
-
nonce: 1,
|
|
2664
|
-
status: 'ready',
|
|
2665
|
-
txSummary: 'AQID',
|
|
2666
|
-
signatures: [
|
|
2667
|
-
{
|
|
2668
|
-
signerId: '0x' + 'a'.repeat(64),
|
|
2669
|
-
signature: { scheme: 'falcon', signature: '0x' + 'b'.repeat(128) },
|
|
2670
|
-
timestamp: '2024-01-01T00:00:00Z',
|
|
2671
|
-
},
|
|
2672
|
-
],
|
|
2673
|
-
metadata: {
|
|
2674
|
-
proposalType: 'switch_guardian',
|
|
2675
|
-
chainAnchor: MOCK_CHAIN_ANCHOR_B64,
|
|
2676
|
-
newGuardianPubkey: '0x' + '1'.repeat(64),
|
|
2677
|
-
newGuardianEndpoint: 'http://new-guardian.com',
|
|
2678
|
-
description: '',
|
|
2679
|
-
},
|
|
2680
|
-
});
|
|
2681
|
-
mockFetch.mockResolvedValueOnce({
|
|
2682
|
-
ok: true,
|
|
2683
|
-
json: async () => ({ commitment: '0x' + '2'.repeat(64) }),
|
|
2684
|
-
});
|
|
2685
|
-
await expect(multisig.createTransactionProposalRequest(proposalId)).rejects.toThrow('Refusing to use GUARDIAN endpoint');
|
|
2686
|
-
expect(mockWebClient.executeTransaction).not.toHaveBeenCalled();
|
|
2687
|
-
});
|
|
2688
|
-
it('should reject duplicate normalized signer IDs during request creation', async () => {
|
|
2689
|
-
const config = {
|
|
2690
|
-
threshold: 2,
|
|
2691
|
-
signerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
|
|
2692
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
2693
|
-
};
|
|
2694
|
-
const multisig = createTestMultisig(config);
|
|
2695
|
-
const proposalId = '0x' + 'c'.repeat(64);
|
|
2696
|
-
multisig.proposals.set(proposalId, {
|
|
2697
|
-
id: proposalId,
|
|
2698
|
-
accountId: multisig.accountId,
|
|
2699
|
-
nonce: 1,
|
|
2700
|
-
status: 'ready',
|
|
2701
|
-
txSummary: 'AQID',
|
|
2702
|
-
signatures: [
|
|
2703
|
-
{
|
|
2704
|
-
signerId: '0x' + 'a'.repeat(64),
|
|
2705
|
-
signature: { scheme: 'falcon', signature: '0x' + 'b'.repeat(128) },
|
|
2706
|
-
timestamp: '2024-01-01T00:00:00Z',
|
|
2707
|
-
},
|
|
2708
|
-
{
|
|
2709
|
-
signerId: '0x' + 'A'.repeat(64),
|
|
2710
|
-
signature: { scheme: 'falcon', signature: '0x' + 'c'.repeat(128) },
|
|
2711
|
-
timestamp: '2024-01-01T00:00:01Z',
|
|
2712
|
-
},
|
|
2713
|
-
],
|
|
2714
|
-
metadata: {
|
|
2715
|
-
proposalType: 'switch_guardian',
|
|
2716
|
-
chainAnchor: MOCK_CHAIN_ANCHOR_B64,
|
|
2717
|
-
newGuardianPubkey: '0x' + '1'.repeat(64),
|
|
2718
|
-
newGuardianEndpoint: 'http://new-guardian.com',
|
|
2719
|
-
description: '',
|
|
2720
|
-
},
|
|
2721
|
-
});
|
|
2722
|
-
await expect(multisig.createTransactionProposalRequest(proposalId)).rejects.toThrow('duplicate signatures for signer');
|
|
2723
|
-
});
|
|
2724
|
-
it('should build a fresh tx commitment word for each advice entry during request creation', async () => {
|
|
2725
|
-
const { buildSignatureAdviceEntry } = await import('./utils/signature.js');
|
|
2726
|
-
const { Word } = await import('@miden-sdk/miden-sdk');
|
|
2727
|
-
const originalAdviceImpl = vi.mocked(buildSignatureAdviceEntry).getMockImplementation();
|
|
2728
|
-
const originalWordFromHexImpl = vi.mocked(Word.fromHex).getMockImplementation();
|
|
2729
|
-
try {
|
|
2730
|
-
vi.mocked(Word.fromHex).mockImplementation((hex) => {
|
|
2731
|
-
let consumed = false;
|
|
2732
|
-
return {
|
|
2733
|
-
toHex: () => hex,
|
|
2734
|
-
toFelts: () => {
|
|
2735
|
-
if (consumed) {
|
|
2736
|
-
throw new Error('Word already consumed');
|
|
2737
|
-
}
|
|
2738
|
-
consumed = true;
|
|
2739
|
-
return [1, 2, 3, 4];
|
|
2740
|
-
},
|
|
2741
|
-
};
|
|
2742
|
-
});
|
|
2743
|
-
vi.mocked(buildSignatureAdviceEntry).mockImplementation((signerCommitment, message) => {
|
|
2744
|
-
message.toFelts();
|
|
2745
|
-
return {
|
|
2746
|
-
key: { toHex: () => signerCommitment.toHex() },
|
|
2747
|
-
values: [1, 2, 3],
|
|
2748
|
-
};
|
|
2749
|
-
});
|
|
2750
|
-
const config = {
|
|
2751
|
-
threshold: 1,
|
|
2752
|
-
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
2753
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
2754
|
-
};
|
|
2755
|
-
const multisig = createTestMultisig(config);
|
|
2756
|
-
const proposalId = '0x' + 'c'.repeat(64);
|
|
2757
|
-
const finalRequest = { kind: 'fresh-message-word-request' };
|
|
2758
|
-
vi.mocked(buildUpdateSignersTransactionRequest)
|
|
2759
|
-
.mockResolvedValueOnce({
|
|
2760
|
-
request: { kind: 'verify-change-threshold-request' },
|
|
2761
|
-
salt: { toHex: () => '0x' + 'd'.repeat(64) },
|
|
2762
|
-
configHash: { toHex: () => '0x' + 'e'.repeat(64) },
|
|
2763
|
-
})
|
|
2764
|
-
.mockResolvedValueOnce({
|
|
2765
|
-
request: finalRequest,
|
|
2766
|
-
salt: { toHex: () => '0x' + 'd'.repeat(64) },
|
|
2767
|
-
configHash: { toHex: () => '0x' + 'e'.repeat(64) },
|
|
2768
|
-
});
|
|
2769
|
-
multisig.proposals.set(proposalId, {
|
|
2770
|
-
id: proposalId,
|
|
2771
|
-
accountId: multisig.accountId,
|
|
2772
|
-
nonce: 1,
|
|
2773
|
-
status: 'ready',
|
|
2774
|
-
txSummary: 'AQID',
|
|
2775
|
-
signatures: [
|
|
2776
|
-
{
|
|
2777
|
-
signerId: '0x' + 'a'.repeat(64),
|
|
2778
|
-
signature: { scheme: 'falcon', signature: '0x' + 'b'.repeat(128) },
|
|
2779
|
-
timestamp: '2024-01-01T00:00:00Z',
|
|
2780
|
-
},
|
|
2781
|
-
],
|
|
2782
|
-
metadata: {
|
|
2783
|
-
proposalType: 'change_threshold',
|
|
2784
|
-
chainAnchor: MOCK_CHAIN_ANCHOR_B64,
|
|
2785
|
-
targetThreshold: 1,
|
|
2786
|
-
targetSignerCommitments: ['0x' + 'a'.repeat(64)],
|
|
2787
|
-
description: '',
|
|
2788
|
-
},
|
|
2789
|
-
});
|
|
2790
|
-
mockFetch.mockResolvedValueOnce({
|
|
2791
|
-
ok: true,
|
|
2792
|
-
json: async () => ({
|
|
2793
|
-
account_id: multisig.accountId,
|
|
2794
|
-
nonce: 1,
|
|
2795
|
-
prev_commitment: '0x' + 'b'.repeat(64),
|
|
2796
|
-
delta_payload: {
|
|
2797
|
-
tx_summary: { data: 'AQID' },
|
|
2798
|
-
signatures: [],
|
|
2799
|
-
metadata: {
|
|
2800
|
-
proposal_type: 'change_threshold',
|
|
2801
|
-
chain_anchor: MOCK_CHAIN_ANCHOR_B64,
|
|
2802
|
-
target_threshold: 1,
|
|
2803
|
-
signer_commitments: ['0x' + 'a'.repeat(64)],
|
|
2804
|
-
},
|
|
2805
|
-
},
|
|
2806
|
-
status: {
|
|
2807
|
-
status: 'pending',
|
|
2808
|
-
timestamp: '2024-01-01T00:00:00Z',
|
|
2809
|
-
proposer_id: '0x' + 'a'.repeat(64),
|
|
2810
|
-
cosigner_sigs: [],
|
|
2811
|
-
},
|
|
2812
|
-
}),
|
|
2813
|
-
});
|
|
2814
|
-
mockFetch.mockResolvedValueOnce({
|
|
2815
|
-
ok: true,
|
|
2816
|
-
json: async () => ({
|
|
2817
|
-
account_id: multisig.accountId,
|
|
2818
|
-
nonce: 1,
|
|
2819
|
-
ack_sig: '0x' + 'f'.repeat(128),
|
|
2820
|
-
ack_scheme: 'falcon',
|
|
2821
|
-
}),
|
|
2822
|
-
});
|
|
2823
|
-
await expect(multisig.createTransactionProposalRequest(proposalId)).resolves.toBe(finalRequest);
|
|
2824
|
-
}
|
|
2825
|
-
finally {
|
|
2826
|
-
if (originalAdviceImpl) {
|
|
2827
|
-
vi.mocked(buildSignatureAdviceEntry).mockImplementation(originalAdviceImpl);
|
|
2828
|
-
}
|
|
2829
|
-
if (originalWordFromHexImpl) {
|
|
2830
|
-
vi.mocked(Word.fromHex).mockImplementation(originalWordFromHexImpl);
|
|
2831
|
-
}
|
|
2832
|
-
}
|
|
2833
|
-
});
|
|
2834
|
-
it('should reject advice-map key collisions during request creation', async () => {
|
|
2835
|
-
const { buildSignatureAdviceEntry } = await import('./utils/signature.js');
|
|
2836
|
-
vi.mocked(buildSignatureAdviceEntry)
|
|
2837
|
-
.mockImplementationOnce(() => ({
|
|
2838
|
-
key: { toHex: () => '0x' + 'f'.repeat(64) },
|
|
2839
|
-
values: [1, 2, 3],
|
|
2840
|
-
}))
|
|
2841
|
-
.mockImplementationOnce(() => ({
|
|
2842
|
-
key: { toHex: () => '0x' + 'f'.repeat(64) },
|
|
2843
|
-
values: [1, 2, 3],
|
|
2844
|
-
}));
|
|
2845
|
-
const config = {
|
|
2846
|
-
threshold: 2,
|
|
2847
|
-
signerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
|
|
2848
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
2849
|
-
};
|
|
2850
|
-
const multisig = createTestMultisig(config);
|
|
2851
|
-
const proposalId = '0x' + 'c'.repeat(64);
|
|
2852
|
-
multisig.proposals.set(proposalId, {
|
|
2853
|
-
id: proposalId,
|
|
2854
|
-
accountId: multisig.accountId,
|
|
2855
|
-
nonce: 1,
|
|
2856
|
-
status: 'ready',
|
|
2857
|
-
txSummary: 'AQID',
|
|
2858
|
-
signatures: [
|
|
2859
|
-
{
|
|
2860
|
-
signerId: '0x' + 'a'.repeat(64),
|
|
2861
|
-
signature: { scheme: 'falcon', signature: '0x' + 'b'.repeat(128) },
|
|
2862
|
-
timestamp: '2024-01-01T00:00:00Z',
|
|
2863
|
-
},
|
|
2864
|
-
{
|
|
2865
|
-
signerId: '0x' + 'b'.repeat(64),
|
|
2866
|
-
signature: { scheme: 'falcon', signature: '0x' + 'c'.repeat(128) },
|
|
2867
|
-
timestamp: '2024-01-01T00:00:01Z',
|
|
2868
|
-
},
|
|
2869
|
-
],
|
|
2870
|
-
metadata: {
|
|
2871
|
-
proposalType: 'switch_guardian',
|
|
2872
|
-
chainAnchor: MOCK_CHAIN_ANCHOR_B64,
|
|
2873
|
-
newGuardianPubkey: '0x' + '1'.repeat(64),
|
|
2874
|
-
newGuardianEndpoint: 'http://new-guardian.com',
|
|
2875
|
-
description: '',
|
|
2876
|
-
},
|
|
2877
|
-
});
|
|
2878
|
-
await expect(multisig.createTransactionProposalRequest(proposalId)).rejects.toThrow('Duplicate advice-map key detected');
|
|
2879
|
-
});
|
|
2880
|
-
});
|
|
2881
|
-
describe('executeProposal', () => {
|
|
2882
|
-
it('should throw if proposal not found locally', async () => {
|
|
2883
|
-
const config = {
|
|
2884
|
-
threshold: 1,
|
|
2885
|
-
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
2886
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
2887
|
-
};
|
|
2888
|
-
const multisig = createTestMultisig(config);
|
|
2889
|
-
await expect(multisig.executeProposal('0x' + 'nonexistent'.repeat(5))).rejects.toThrow('Proposal not found');
|
|
2890
|
-
});
|
|
2891
|
-
it('should throw if proposal is still pending', async () => {
|
|
2892
|
-
const config = {
|
|
2893
|
-
threshold: 2, // Need 2 signatures
|
|
2894
|
-
signerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
|
|
2895
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
2896
|
-
};
|
|
2897
|
-
const multisig = createTestMultisig(config);
|
|
2898
|
-
// Sync with pending proposal (only 1 signature)
|
|
2899
|
-
const mockProposals = [
|
|
2900
|
-
{
|
|
2901
|
-
account_id: '0x' + 'a'.repeat(30),
|
|
2902
|
-
nonce: 1,
|
|
2903
|
-
prev_commitment: '0x' + 'b'.repeat(64),
|
|
2904
|
-
delta_payload: {
|
|
2905
|
-
tx_summary: { data: 'AQID' },
|
|
2906
|
-
signatures: [],
|
|
2907
|
-
metadata: {
|
|
2908
|
-
proposal_type: 'add_signer',
|
|
2909
|
-
chain_anchor: MOCK_CHAIN_ANCHOR_B64,
|
|
2910
|
-
description: '',
|
|
2911
|
-
target_threshold: 2,
|
|
2912
|
-
signer_commitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
|
|
2913
|
-
},
|
|
2914
|
-
},
|
|
2915
|
-
status: {
|
|
2916
|
-
status: 'pending',
|
|
2917
|
-
timestamp: '2024-01-01T00:00:00Z',
|
|
2918
|
-
proposer_id: '0x' + 'c'.repeat(64),
|
|
2919
|
-
cosigner_sigs: [
|
|
2920
|
-
{
|
|
2921
|
-
signer_id: '0x' + 'a'.repeat(64),
|
|
2922
|
-
signature: { scheme: 'falcon', signature: '0x' + 'e'.repeat(128) },
|
|
2923
|
-
timestamp: '2024-01-01T00:00:00Z',
|
|
2924
|
-
},
|
|
2925
|
-
],
|
|
2926
|
-
},
|
|
2927
|
-
},
|
|
2928
|
-
];
|
|
2929
|
-
mockFetch.mockResolvedValueOnce({
|
|
2930
|
-
ok: true,
|
|
2931
|
-
json: async () => ({ proposals: mockProposals }),
|
|
2932
|
-
});
|
|
2933
|
-
await multisig.syncProposals();
|
|
2934
|
-
// Proposal ID is mocked to return 'c'.repeat(64)
|
|
2935
|
-
await expect(multisig.executeProposal('0x' + 'c'.repeat(64))).rejects.toThrow('not ready for execution');
|
|
2936
|
-
});
|
|
2937
|
-
it('should fail when GUARDIAN ack signature is missing', async () => {
|
|
2938
|
-
const config = {
|
|
2939
|
-
threshold: 1,
|
|
2940
|
-
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
2941
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
2942
|
-
};
|
|
2943
|
-
const multisig = createTestMultisig(config);
|
|
2944
|
-
const readyDelta = {
|
|
2945
|
-
account_id: '0x' + 'a'.repeat(30),
|
|
2946
|
-
nonce: 1,
|
|
2947
|
-
prev_commitment: '0x' + 'b'.repeat(64),
|
|
2948
|
-
delta_payload: {
|
|
2949
|
-
tx_summary: { data: 'AQID' },
|
|
2950
|
-
signatures: [],
|
|
2951
|
-
metadata: {
|
|
2952
|
-
proposal_type: 'add_signer',
|
|
2953
|
-
chain_anchor: MOCK_CHAIN_ANCHOR_B64,
|
|
2954
|
-
description: '',
|
|
2955
|
-
target_threshold: 1,
|
|
2956
|
-
signer_commitments: ['0x' + 'a'.repeat(64)],
|
|
2957
|
-
},
|
|
2958
|
-
},
|
|
2959
|
-
status: {
|
|
2960
|
-
status: 'pending',
|
|
2961
|
-
timestamp: '2024-01-01T00:00:00Z',
|
|
2962
|
-
proposer_id: '0x' + 'c'.repeat(64),
|
|
2963
|
-
cosigner_sigs: [
|
|
2964
|
-
{
|
|
2965
|
-
signer_id: '0x' + 'a'.repeat(64),
|
|
2966
|
-
signature: { scheme: 'falcon', signature: '0x' + 'e'.repeat(128) },
|
|
2967
|
-
timestamp: '2024-01-01T00:00:00Z',
|
|
2968
|
-
},
|
|
2969
|
-
],
|
|
2970
|
-
},
|
|
2971
|
-
};
|
|
2972
|
-
const proposalId = '0x' + 'c'.repeat(64);
|
|
2973
|
-
// Prime local cache via syncProposals
|
|
2974
|
-
mockFetch.mockResolvedValueOnce({
|
|
2975
|
-
ok: true,
|
|
2976
|
-
json: async () => ({ proposals: [readyDelta] }),
|
|
2977
|
-
});
|
|
2978
|
-
await multisig.syncProposals();
|
|
2979
|
-
// executeProposal: getDeltaProposal
|
|
2980
|
-
mockFetch.mockResolvedValueOnce({
|
|
2981
|
-
ok: true,
|
|
2982
|
-
json: async () => readyDelta,
|
|
2983
|
-
});
|
|
2984
|
-
// executeProposal: pushDelta without ack_sig
|
|
2985
|
-
mockFetch.mockResolvedValueOnce({
|
|
2986
|
-
ok: true,
|
|
2987
|
-
json: async () => ({ ...readyDelta, ack_sig: null }),
|
|
2988
|
-
});
|
|
2989
|
-
await expect(multisig.executeProposal(proposalId)).rejects.toThrow('GUARDIAN did not return acknowledgment signature');
|
|
2990
|
-
});
|
|
2991
|
-
it('should encode ECDSA proposal and ack signatures with scheme-aware advice', async () => {
|
|
2992
|
-
const { buildSignatureAdviceEntry, signatureHexToBytes } = await import('./utils/signature.js');
|
|
2993
|
-
vi.mocked(signatureHexToBytes).mockClear();
|
|
2994
|
-
vi.mocked(buildSignatureAdviceEntry).mockClear();
|
|
2995
|
-
const config = {
|
|
2996
|
-
threshold: 1,
|
|
2997
|
-
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
2998
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
2999
|
-
guardianPublicKey: '0x' + '1'.repeat(66),
|
|
3000
|
-
};
|
|
3001
|
-
const ecdsaSigner = {
|
|
3002
|
-
...mockSigner,
|
|
3003
|
-
scheme: 'ecdsa',
|
|
3004
|
-
publicKey: '0x' + '2'.repeat(66),
|
|
3005
|
-
};
|
|
3006
|
-
const multisig = createTestMultisig(config, ecdsaSigner, undefined, {
|
|
3007
|
-
kind: 'remote',
|
|
3008
|
-
maxAttempts: 2,
|
|
3009
|
-
createProver: () => ({}),
|
|
3010
|
-
});
|
|
3011
|
-
const proposalId = '0x' + 'c'.repeat(64);
|
|
3012
|
-
const cosignerPubkey = '0x' + '3'.repeat(66);
|
|
3013
|
-
const ackPubkey = '0x' + '4'.repeat(66);
|
|
3014
|
-
const cosignerSignature = '0x' + '5'.repeat(130);
|
|
3015
|
-
const ackSignature = '0x' + '6'.repeat(130);
|
|
3016
|
-
multisig.proposals.set(proposalId, {
|
|
3017
|
-
id: proposalId,
|
|
3018
|
-
accountId: multisig.accountId,
|
|
3019
|
-
nonce: 1,
|
|
3020
|
-
status: 'ready',
|
|
3021
|
-
txSummary: 'AQID',
|
|
3022
|
-
signatures: [
|
|
3023
|
-
{
|
|
3024
|
-
signerId: '0x' + 'a'.repeat(64),
|
|
3025
|
-
signature: {
|
|
3026
|
-
scheme: 'ecdsa',
|
|
3027
|
-
signature: cosignerSignature,
|
|
3028
|
-
publicKey: cosignerPubkey,
|
|
3029
|
-
},
|
|
3030
|
-
timestamp: '2024-01-01T00:00:00Z',
|
|
3031
|
-
},
|
|
3032
|
-
],
|
|
3033
|
-
metadata: {
|
|
3034
|
-
proposalType: 'change_threshold',
|
|
3035
|
-
chainAnchor: MOCK_CHAIN_ANCHOR_B64,
|
|
3036
|
-
targetThreshold: 1,
|
|
3037
|
-
targetSignerCommitments: ['0x' + 'a'.repeat(64)],
|
|
3038
|
-
description: '',
|
|
3039
|
-
},
|
|
3040
|
-
});
|
|
3041
|
-
mockFetch.mockResolvedValueOnce({
|
|
3042
|
-
ok: true,
|
|
3043
|
-
json: async () => ({
|
|
3044
|
-
account_id: multisig.accountId,
|
|
3045
|
-
nonce: 1,
|
|
3046
|
-
prev_commitment: '0x' + 'b'.repeat(64),
|
|
3047
|
-
delta_payload: {
|
|
3048
|
-
tx_summary: { data: 'AQID' },
|
|
3049
|
-
signatures: [],
|
|
3050
|
-
metadata: {
|
|
3051
|
-
proposal_type: 'change_threshold',
|
|
3052
|
-
chain_anchor: MOCK_CHAIN_ANCHOR_B64,
|
|
3053
|
-
target_threshold: 1,
|
|
3054
|
-
signer_commitments: ['0x' + 'a'.repeat(64)],
|
|
3055
|
-
},
|
|
3056
|
-
},
|
|
3057
|
-
status: {
|
|
3058
|
-
status: 'pending',
|
|
3059
|
-
timestamp: '2024-01-01T00:00:00Z',
|
|
3060
|
-
proposer_id: '0x' + 'a'.repeat(64),
|
|
3061
|
-
cosigner_sigs: [],
|
|
3062
|
-
},
|
|
3063
|
-
}),
|
|
3064
|
-
});
|
|
3065
|
-
mockFetch.mockResolvedValueOnce({
|
|
3066
|
-
ok: true,
|
|
3067
|
-
json: async () => ({
|
|
3068
|
-
account_id: multisig.accountId,
|
|
3069
|
-
nonce: 1,
|
|
3070
|
-
ack_sig: ackSignature,
|
|
3071
|
-
ack_pubkey: ackPubkey,
|
|
3072
|
-
ack_scheme: 'ecdsa',
|
|
3073
|
-
}),
|
|
3074
|
-
});
|
|
3075
|
-
mockWebClient.proveTransaction
|
|
3076
|
-
.mockRejectedValueOnce(Object.assign(new Error('unavailable'), { code: 'Unavailable' }))
|
|
3077
|
-
.mockResolvedValueOnce({});
|
|
3078
|
-
vi.useFakeTimers();
|
|
3079
|
-
try {
|
|
3080
|
-
const execution = multisig.executeProposal(proposalId);
|
|
3081
|
-
await vi.runAllTimersAsync();
|
|
3082
|
-
await expect(execution).resolves.toBeUndefined();
|
|
3083
|
-
}
|
|
3084
|
-
finally {
|
|
3085
|
-
vi.useRealTimers();
|
|
3086
|
-
}
|
|
3087
|
-
expect(mockWebClient.executeTransaction).toHaveBeenCalledTimes(1);
|
|
3088
|
-
expect(mockWebClient.proveTransaction).toHaveBeenCalledTimes(2);
|
|
3089
|
-
expect(mockWebClient.submitProvenTransaction).toHaveBeenCalledTimes(1);
|
|
3090
|
-
expect(mockWebClient.applyTransaction).toHaveBeenCalledTimes(1);
|
|
3091
|
-
expect(vi.mocked(signatureHexToBytes)).toHaveBeenNthCalledWith(1, cosignerSignature, 'ecdsa');
|
|
3092
|
-
expect(vi.mocked(signatureHexToBytes)).toHaveBeenNthCalledWith(2, ackSignature, 'ecdsa');
|
|
3093
|
-
expect(vi.mocked(buildSignatureAdviceEntry)).toHaveBeenNthCalledWith(1, expect.anything(), expect.anything(), expect.anything());
|
|
3094
|
-
expect(vi.mocked(buildSignatureAdviceEntry)).toHaveBeenNthCalledWith(2, expect.anything(), expect.anything(), expect.anything());
|
|
3095
|
-
// The advice payload now comes from the SDK, so the routing assertion is
|
|
3096
|
-
// the commitment each entry is keyed on: cosigner first, GUARDIAN ack
|
|
3097
|
-
// second. Swapping the two entries must not pass.
|
|
3098
|
-
const adviceCalls = vi.mocked(buildSignatureAdviceEntry).mock.calls;
|
|
3099
|
-
expect(adviceCalls[0][0].toHex()).toBe(config.signerCommitments[0]);
|
|
3100
|
-
expect(adviceCalls[1][0].toHex()).toBe(config.guardianCommitment);
|
|
3101
|
-
});
|
|
3102
|
-
it('should execute imported ECDSA proposals with scheme-aware advice', async () => {
|
|
3103
|
-
const { buildSignatureAdviceEntry, signatureHexToBytes } = await import('./utils/signature.js');
|
|
3104
|
-
vi.mocked(signatureHexToBytes).mockClear();
|
|
3105
|
-
vi.mocked(buildSignatureAdviceEntry).mockClear();
|
|
3106
|
-
const config = {
|
|
3107
|
-
threshold: 1,
|
|
3108
|
-
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
3109
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
3110
|
-
guardianPublicKey: '0x' + '1'.repeat(66),
|
|
3111
|
-
};
|
|
3112
|
-
const ecdsaSigner = {
|
|
3113
|
-
...mockSigner,
|
|
3114
|
-
scheme: 'ecdsa',
|
|
3115
|
-
publicKey: '0x' + '2'.repeat(66),
|
|
3116
|
-
};
|
|
3117
|
-
const multisig = createTestMultisig(config, ecdsaSigner);
|
|
3118
|
-
const proposalId = '0x' + 'c'.repeat(64);
|
|
3119
|
-
const cosignerPubkey = '0x' + '3'.repeat(66);
|
|
3120
|
-
const ackPubkey = '0x' + '4'.repeat(66);
|
|
3121
|
-
const cosignerSignature = '0x' + '5'.repeat(130);
|
|
3122
|
-
const ackSignature = '0x' + '6'.repeat(130);
|
|
3123
|
-
await multisig.importProposal(JSON.stringify({
|
|
3124
|
-
accountId: multisig.accountId,
|
|
3125
|
-
nonce: 1,
|
|
3126
|
-
commitment: proposalId,
|
|
3127
|
-
txSummaryBase64: 'AQID',
|
|
3128
|
-
signatures: [
|
|
3129
|
-
{
|
|
3130
|
-
commitment: '0x' + 'a'.repeat(64),
|
|
3131
|
-
signatureHex: cosignerSignature,
|
|
3132
|
-
scheme: 'ecdsa',
|
|
3133
|
-
publicKey: cosignerPubkey,
|
|
3134
|
-
timestamp: '2024-01-01T00:00:00Z',
|
|
3135
|
-
},
|
|
3136
|
-
],
|
|
3137
|
-
metadata: {
|
|
3138
|
-
proposalType: 'change_threshold',
|
|
3139
|
-
chainAnchor: MOCK_CHAIN_ANCHOR_B64,
|
|
3140
|
-
targetThreshold: 1,
|
|
3141
|
-
targetSignerCommitments: ['0x' + 'a'.repeat(64)],
|
|
3142
|
-
description: '',
|
|
3143
|
-
},
|
|
3144
|
-
}));
|
|
3145
|
-
mockFetch.mockResolvedValueOnce({
|
|
3146
|
-
ok: true,
|
|
3147
|
-
json: async () => ({
|
|
3148
|
-
account_id: multisig.accountId,
|
|
3149
|
-
nonce: 1,
|
|
3150
|
-
prev_commitment: '0x' + 'b'.repeat(64),
|
|
3151
|
-
delta_payload: {
|
|
3152
|
-
tx_summary: { data: 'AQID' },
|
|
3153
|
-
signatures: [],
|
|
3154
|
-
metadata: {
|
|
3155
|
-
proposal_type: 'change_threshold',
|
|
3156
|
-
chain_anchor: MOCK_CHAIN_ANCHOR_B64,
|
|
3157
|
-
target_threshold: 1,
|
|
3158
|
-
signer_commitments: ['0x' + 'a'.repeat(64)],
|
|
3159
|
-
},
|
|
3160
|
-
},
|
|
3161
|
-
status: {
|
|
3162
|
-
status: 'pending',
|
|
3163
|
-
timestamp: '2024-01-01T00:00:00Z',
|
|
3164
|
-
proposer_id: '0x' + 'a'.repeat(64),
|
|
3165
|
-
cosigner_sigs: [],
|
|
3166
|
-
},
|
|
3167
|
-
}),
|
|
3168
|
-
});
|
|
3169
|
-
mockFetch.mockResolvedValueOnce({
|
|
3170
|
-
ok: true,
|
|
3171
|
-
json: async () => ({
|
|
3172
|
-
account_id: multisig.accountId,
|
|
3173
|
-
nonce: 1,
|
|
3174
|
-
ack_sig: ackSignature,
|
|
3175
|
-
ack_pubkey: ackPubkey,
|
|
3176
|
-
ack_scheme: 'ecdsa',
|
|
3177
|
-
}),
|
|
3178
|
-
});
|
|
3179
|
-
await expect(multisig.executeProposal(proposalId)).resolves.toBeUndefined();
|
|
3180
|
-
expect(mockWebClient.executeTransaction).toHaveBeenCalledTimes(1);
|
|
3181
|
-
expect(mockWebClient.proveTransaction).toHaveBeenCalledTimes(1);
|
|
3182
|
-
expect(mockWebClient.submitProvenTransaction).toHaveBeenCalledTimes(1);
|
|
3183
|
-
expect(mockWebClient.applyTransaction).toHaveBeenCalledTimes(1);
|
|
3184
|
-
expect(vi.mocked(signatureHexToBytes)).toHaveBeenNthCalledWith(1, cosignerSignature, 'ecdsa');
|
|
3185
|
-
expect(vi.mocked(signatureHexToBytes)).toHaveBeenNthCalledWith(2, ackSignature, 'ecdsa');
|
|
3186
|
-
expect(vi.mocked(buildSignatureAdviceEntry)).toHaveBeenNthCalledWith(1, expect.anything(), expect.anything(), expect.anything());
|
|
3187
|
-
expect(vi.mocked(buildSignatureAdviceEntry)).toHaveBeenNthCalledWith(2, expect.anything(), expect.anything(), expect.anything());
|
|
3188
|
-
// The advice payload now comes from the SDK, so the routing assertion is
|
|
3189
|
-
// the commitment each entry is keyed on: cosigner first, GUARDIAN ack
|
|
3190
|
-
// second. Swapping the two entries must not pass.
|
|
3191
|
-
const adviceCalls = vi.mocked(buildSignatureAdviceEntry).mock.calls;
|
|
3192
|
-
expect(adviceCalls[0][0].toHex()).toBe(config.signerCommitments[0]);
|
|
3193
|
-
expect(adviceCalls[1][0].toHex()).toBe(config.guardianCommitment);
|
|
3194
|
-
});
|
|
3195
|
-
it('should verify switch_guardian endpoint commitment before execution', async () => {
|
|
3196
|
-
const config = {
|
|
3197
|
-
threshold: 1,
|
|
3198
|
-
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
3199
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
3200
|
-
};
|
|
3201
|
-
const multisig = createTestMultisig(config);
|
|
3202
|
-
const proposalId = '0x' + 'c'.repeat(64);
|
|
3203
|
-
const newGuardianPubkey = '0x' + '1'.repeat(64);
|
|
3204
|
-
multisig.proposals.set(proposalId, {
|
|
3205
|
-
id: proposalId,
|
|
3206
|
-
accountId: multisig.accountId,
|
|
3207
|
-
nonce: 1,
|
|
3208
|
-
status: 'ready',
|
|
3209
|
-
txSummary: 'AQID',
|
|
3210
|
-
signatures: [
|
|
3211
|
-
{
|
|
3212
|
-
signerId: '0x' + 'a'.repeat(64),
|
|
3213
|
-
signature: { scheme: 'falcon', signature: '0x' + 'b'.repeat(128) },
|
|
3214
|
-
timestamp: '2024-01-01T00:00:00Z',
|
|
3215
|
-
},
|
|
3216
|
-
],
|
|
3217
|
-
metadata: {
|
|
3218
|
-
proposalType: 'switch_guardian',
|
|
3219
|
-
chainAnchor: MOCK_CHAIN_ANCHOR_B64,
|
|
3220
|
-
newGuardianPubkey,
|
|
3221
|
-
newGuardianEndpoint: 'http://new-guardian.com',
|
|
3222
|
-
description: '',
|
|
3223
|
-
},
|
|
3224
|
-
});
|
|
3225
|
-
mockFetch.mockResolvedValueOnce({
|
|
3226
|
-
ok: true,
|
|
3227
|
-
json: async () => ({ commitment: newGuardianPubkey }),
|
|
3228
|
-
});
|
|
3229
|
-
// Pre-switch canonicalization push: getDeltaProposal then pushDelta.
|
|
3230
|
-
mockFetch.mockResolvedValueOnce({
|
|
3231
|
-
ok: true,
|
|
3232
|
-
json: async () => ({
|
|
3233
|
-
account_id: multisig.accountId,
|
|
3234
|
-
nonce: 1,
|
|
3235
|
-
prev_commitment: '0x' + 'b'.repeat(64),
|
|
3236
|
-
delta_payload: {
|
|
3237
|
-
tx_summary: { data: 'AQID' },
|
|
3238
|
-
signatures: [],
|
|
3239
|
-
metadata: {
|
|
3240
|
-
proposal_type: 'switch_guardian',
|
|
3241
|
-
chain_anchor: MOCK_CHAIN_ANCHOR_B64,
|
|
3242
|
-
new_guardian_pubkey: newGuardianPubkey,
|
|
3243
|
-
new_guardian_endpoint: 'http://new-guardian.com',
|
|
3244
|
-
},
|
|
3245
|
-
},
|
|
3246
|
-
status: {
|
|
3247
|
-
status: 'pending',
|
|
3248
|
-
timestamp: '2024-01-01T00:00:00Z',
|
|
3249
|
-
proposer_id: '0x' + 'a'.repeat(64),
|
|
3250
|
-
cosigner_sigs: [],
|
|
3251
|
-
},
|
|
3252
|
-
}),
|
|
3253
|
-
});
|
|
3254
|
-
mockFetch.mockResolvedValueOnce({
|
|
3255
|
-
ok: true,
|
|
3256
|
-
json: async () => ({
|
|
3257
|
-
account_id: multisig.accountId,
|
|
3258
|
-
nonce: 1,
|
|
3259
|
-
ack_sig: '0x' + '6'.repeat(130),
|
|
3260
|
-
ack_pubkey: '0x' + 'f'.repeat(64),
|
|
3261
|
-
ack_scheme: 'falcon',
|
|
3262
|
-
}),
|
|
3263
|
-
});
|
|
3264
|
-
mockWebClient.getAccount.mockResolvedValueOnce({
|
|
3265
|
-
serialize: () => new Uint8Array([1, 2, 3]),
|
|
3266
|
-
});
|
|
3267
|
-
mockFetch.mockResolvedValueOnce({
|
|
3268
|
-
ok: true,
|
|
3269
|
-
json: async () => ({ success: true, message: 'ok', ack_pubkey: '0x' + 'f'.repeat(64) }),
|
|
3270
|
-
});
|
|
3271
|
-
await expect(multisig.executeProposal(proposalId)).resolves.toBeUndefined();
|
|
3272
|
-
expect(mockWebClient.executeTransaction).toHaveBeenCalledTimes(1);
|
|
3273
|
-
expect(mockWebClient.proveTransaction).toHaveBeenCalledTimes(1);
|
|
3274
|
-
expect(mockWebClient.submitProvenTransaction).toHaveBeenCalledTimes(1);
|
|
3275
|
-
expect(mockWebClient.applyTransaction).toHaveBeenCalledTimes(1);
|
|
3276
|
-
});
|
|
3277
|
-
it('should still switch GUARDIAN when the pre-switch canonicalization push fails', async () => {
|
|
3278
|
-
const config = {
|
|
3279
|
-
threshold: 1,
|
|
3280
|
-
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
3281
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
3282
|
-
};
|
|
3283
|
-
const multisig = createTestMultisig(config);
|
|
3284
|
-
const proposalId = '0x' + 'c'.repeat(64);
|
|
3285
|
-
const newGuardianPubkey = '0x' + '1'.repeat(64);
|
|
3286
|
-
multisig.proposals.set(proposalId, {
|
|
3287
|
-
id: proposalId,
|
|
3288
|
-
accountId: multisig.accountId,
|
|
3289
|
-
nonce: 1,
|
|
3290
|
-
status: 'ready',
|
|
3291
|
-
txSummary: 'AQID',
|
|
3292
|
-
signatures: [
|
|
3293
|
-
{
|
|
3294
|
-
signerId: '0x' + 'a'.repeat(64),
|
|
3295
|
-
signature: { scheme: 'falcon', signature: '0x' + 'b'.repeat(128) },
|
|
3296
|
-
timestamp: '2024-01-01T00:00:00Z',
|
|
3297
|
-
},
|
|
3298
|
-
],
|
|
3299
|
-
metadata: {
|
|
3300
|
-
proposalType: 'switch_guardian',
|
|
3301
|
-
chainAnchor: MOCK_CHAIN_ANCHOR_B64,
|
|
3302
|
-
newGuardianPubkey,
|
|
3303
|
-
newGuardianEndpoint: 'http://new-guardian.com',
|
|
3304
|
-
description: '',
|
|
3305
|
-
},
|
|
3306
|
-
});
|
|
3307
|
-
mockFetch.mockResolvedValueOnce({
|
|
3308
|
-
ok: true,
|
|
3309
|
-
json: async () => ({ commitment: newGuardianPubkey }),
|
|
3310
|
-
});
|
|
3311
|
-
// getDeltaProposal against the old GUARDIAN fails — must be swallowed.
|
|
3312
|
-
mockFetch.mockRejectedValueOnce(new Error('pre-switch GUARDIAN unreachable'));
|
|
3313
|
-
mockWebClient.getAccount.mockResolvedValueOnce({
|
|
3314
|
-
serialize: () => new Uint8Array([1, 2, 3]),
|
|
3315
|
-
});
|
|
3316
|
-
mockFetch.mockResolvedValueOnce({
|
|
3317
|
-
ok: true,
|
|
3318
|
-
json: async () => ({ success: true, message: 'ok', ack_pubkey: '0x' + 'f'.repeat(64) }),
|
|
3319
|
-
});
|
|
3320
|
-
await expect(multisig.executeProposal(proposalId)).resolves.toBeUndefined();
|
|
3321
|
-
expect(mockWebClient.executeTransaction).toHaveBeenCalledTimes(1);
|
|
3322
|
-
expect(mockWebClient.proveTransaction).toHaveBeenCalledTimes(1);
|
|
3323
|
-
expect(mockWebClient.submitProvenTransaction).toHaveBeenCalledTimes(1);
|
|
3324
|
-
expect(mockWebClient.applyTransaction).toHaveBeenCalledTimes(1);
|
|
3325
|
-
});
|
|
3326
|
-
it('should reject switch_guardian execution when endpoint commitment mismatches', async () => {
|
|
3327
|
-
const config = {
|
|
3328
|
-
threshold: 1,
|
|
3329
|
-
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
3330
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
3331
|
-
};
|
|
3332
|
-
const multisig = createTestMultisig(config);
|
|
3333
|
-
const proposalId = '0x' + 'c'.repeat(64);
|
|
3334
|
-
multisig.proposals.set(proposalId, {
|
|
3335
|
-
id: proposalId,
|
|
3336
|
-
accountId: multisig.accountId,
|
|
3337
|
-
nonce: 1,
|
|
3338
|
-
status: 'ready',
|
|
3339
|
-
txSummary: 'AQID',
|
|
3340
|
-
signatures: [
|
|
3341
|
-
{
|
|
3342
|
-
signerId: '0x' + 'a'.repeat(64),
|
|
3343
|
-
signature: { scheme: 'falcon', signature: '0x' + 'b'.repeat(128) },
|
|
3344
|
-
timestamp: '2024-01-01T00:00:00Z',
|
|
3345
|
-
},
|
|
3346
|
-
],
|
|
3347
|
-
metadata: {
|
|
3348
|
-
proposalType: 'switch_guardian',
|
|
3349
|
-
chainAnchor: MOCK_CHAIN_ANCHOR_B64,
|
|
3350
|
-
newGuardianPubkey: '0x' + '1'.repeat(64),
|
|
3351
|
-
newGuardianEndpoint: 'http://new-guardian.com',
|
|
3352
|
-
description: '',
|
|
3353
|
-
},
|
|
3354
|
-
});
|
|
3355
|
-
mockFetch.mockResolvedValueOnce({
|
|
3356
|
-
ok: true,
|
|
3357
|
-
json: async () => ({ commitment: '0x' + '2'.repeat(64) }),
|
|
3358
|
-
});
|
|
3359
|
-
await expect(multisig.executeProposal(proposalId)).rejects.toThrow('Refusing to use GUARDIAN endpoint');
|
|
3360
|
-
expect(mockWebClient.executeTransaction).not.toHaveBeenCalled();
|
|
3361
|
-
});
|
|
3362
|
-
it('should reject duplicate normalized signer IDs during execution', async () => {
|
|
3363
|
-
const config = {
|
|
3364
|
-
threshold: 2,
|
|
3365
|
-
signerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
|
|
3366
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
3367
|
-
};
|
|
3368
|
-
const multisig = createTestMultisig(config);
|
|
3369
|
-
const proposalId = '0x' + 'c'.repeat(64);
|
|
3370
|
-
multisig.proposals.set(proposalId, {
|
|
3371
|
-
id: proposalId,
|
|
3372
|
-
accountId: multisig.accountId,
|
|
3373
|
-
nonce: 1,
|
|
3374
|
-
status: 'ready',
|
|
3375
|
-
txSummary: 'AQID',
|
|
3376
|
-
signatures: [
|
|
3377
|
-
{
|
|
3378
|
-
signerId: '0x' + 'a'.repeat(64),
|
|
3379
|
-
signature: { scheme: 'falcon', signature: '0x' + 'b'.repeat(128) },
|
|
3380
|
-
timestamp: '2024-01-01T00:00:00Z',
|
|
3381
|
-
},
|
|
3382
|
-
{
|
|
3383
|
-
signerId: '0x' + 'A'.repeat(64),
|
|
3384
|
-
signature: { scheme: 'falcon', signature: '0x' + 'c'.repeat(128) },
|
|
3385
|
-
timestamp: '2024-01-01T00:00:01Z',
|
|
3386
|
-
},
|
|
3387
|
-
],
|
|
3388
|
-
metadata: {
|
|
3389
|
-
proposalType: 'switch_guardian',
|
|
3390
|
-
chainAnchor: MOCK_CHAIN_ANCHOR_B64,
|
|
3391
|
-
newGuardianPubkey: '0x' + '1'.repeat(64),
|
|
3392
|
-
newGuardianEndpoint: 'http://new-guardian.com',
|
|
3393
|
-
description: '',
|
|
3394
|
-
},
|
|
3395
|
-
});
|
|
3396
|
-
await expect(multisig.executeProposal(proposalId)).rejects.toThrow('duplicate signatures for signer');
|
|
3397
|
-
});
|
|
3398
|
-
it('should reject advice-map key collisions during execution', async () => {
|
|
3399
|
-
const { buildSignatureAdviceEntry } = await import('./utils/signature.js');
|
|
3400
|
-
vi.mocked(buildSignatureAdviceEntry)
|
|
3401
|
-
.mockImplementationOnce(() => ({
|
|
3402
|
-
key: { toHex: () => '0x' + 'f'.repeat(64) },
|
|
3403
|
-
values: [1, 2, 3],
|
|
3404
|
-
}))
|
|
3405
|
-
.mockImplementationOnce(() => ({
|
|
3406
|
-
key: { toHex: () => '0x' + 'f'.repeat(64) },
|
|
3407
|
-
values: [1, 2, 3],
|
|
3408
|
-
}));
|
|
3409
|
-
const config = {
|
|
3410
|
-
threshold: 2,
|
|
3411
|
-
signerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
|
|
3412
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
3413
|
-
};
|
|
3414
|
-
const multisig = createTestMultisig(config);
|
|
3415
|
-
const proposalId = '0x' + 'c'.repeat(64);
|
|
3416
|
-
multisig.proposals.set(proposalId, {
|
|
3417
|
-
id: proposalId,
|
|
3418
|
-
accountId: multisig.accountId,
|
|
3419
|
-
nonce: 1,
|
|
3420
|
-
status: 'ready',
|
|
3421
|
-
txSummary: 'AQID',
|
|
3422
|
-
signatures: [
|
|
3423
|
-
{
|
|
3424
|
-
signerId: '0x' + 'a'.repeat(64),
|
|
3425
|
-
signature: { scheme: 'falcon', signature: '0x' + 'b'.repeat(128) },
|
|
3426
|
-
timestamp: '2024-01-01T00:00:00Z',
|
|
3427
|
-
},
|
|
3428
|
-
{
|
|
3429
|
-
signerId: '0x' + 'b'.repeat(64),
|
|
3430
|
-
signature: { scheme: 'falcon', signature: '0x' + 'c'.repeat(128) },
|
|
3431
|
-
timestamp: '2024-01-01T00:00:01Z',
|
|
3432
|
-
},
|
|
3433
|
-
],
|
|
3434
|
-
metadata: {
|
|
3435
|
-
proposalType: 'switch_guardian',
|
|
3436
|
-
chainAnchor: MOCK_CHAIN_ANCHOR_B64,
|
|
3437
|
-
newGuardianPubkey: '0x' + '1'.repeat(64),
|
|
3438
|
-
newGuardianEndpoint: 'http://new-guardian.com',
|
|
3439
|
-
description: '',
|
|
3440
|
-
},
|
|
3441
|
-
});
|
|
3442
|
-
await expect(multisig.executeProposal(proposalId)).rejects.toThrow('Duplicate advice-map key detected');
|
|
3443
|
-
});
|
|
3444
|
-
});
|
|
3445
|
-
describe('submitTransaction', () => {
|
|
3446
|
-
it('uses the configured total proof-attempt budget without repeating other stages', async () => {
|
|
3447
|
-
const multisig = createTestMultisig({
|
|
3448
|
-
threshold: 1,
|
|
3449
|
-
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
3450
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
3451
|
-
}, mockSigner, undefined, {
|
|
3452
|
-
kind: 'remote',
|
|
3453
|
-
maxAttempts: 4,
|
|
3454
|
-
createProver: () => ({}),
|
|
3455
|
-
});
|
|
3456
|
-
const transient = Object.assign(new Error('unavailable'), { code: 'Unavailable' });
|
|
3457
|
-
mockWebClient.proveTransaction
|
|
3458
|
-
.mockRejectedValueOnce(transient)
|
|
3459
|
-
.mockRejectedValueOnce(transient)
|
|
3460
|
-
.mockRejectedValueOnce(transient)
|
|
3461
|
-
.mockResolvedValueOnce({});
|
|
3462
|
-
mockFetch.mockResolvedValueOnce({
|
|
3463
|
-
ok: true,
|
|
3464
|
-
json: async () => ({
|
|
3465
|
-
account_id: '0x' + 'a'.repeat(30),
|
|
3466
|
-
nonce: 1,
|
|
3467
|
-
prev_commitment: '0x' + 'b'.repeat(64),
|
|
3468
|
-
delta_payload: {
|
|
3469
|
-
tx_summary: { data: 'AQID' },
|
|
3470
|
-
signatures: [],
|
|
3471
|
-
metadata: {
|
|
3472
|
-
proposal_type: 'b2agg',
|
|
3473
|
-
chain_anchor: MOCK_CHAIN_ANCHOR_B64,
|
|
3474
|
-
description: '',
|
|
3475
|
-
},
|
|
3476
|
-
},
|
|
3477
|
-
status: {
|
|
3478
|
-
status: 'pending',
|
|
3479
|
-
timestamp: '2024-01-01T00:00:00Z',
|
|
3480
|
-
proposer_id: '0x' + 'c'.repeat(64),
|
|
3481
|
-
cosigner_sigs: [],
|
|
3482
|
-
},
|
|
3483
|
-
}),
|
|
3484
|
-
});
|
|
3485
|
-
vi.useFakeTimers();
|
|
3486
|
-
try {
|
|
3487
|
-
const submission = multisig.submitTransaction('0x' + 'c'.repeat(64), {});
|
|
3488
|
-
await vi.runAllTimersAsync();
|
|
3489
|
-
await submission;
|
|
3490
|
-
}
|
|
3491
|
-
finally {
|
|
3492
|
-
vi.useRealTimers();
|
|
3493
|
-
}
|
|
3494
|
-
expect(mockWebClient.executeTransaction).toHaveBeenCalledTimes(1);
|
|
3495
|
-
expect(mockWebClient.proveTransaction).toHaveBeenCalledTimes(4);
|
|
3496
|
-
expect(mockWebClient.submitProvenTransaction).toHaveBeenCalledTimes(1);
|
|
3497
|
-
expect(mockWebClient.applyTransaction).toHaveBeenCalledTimes(1);
|
|
3498
|
-
});
|
|
3499
|
-
});
|
|
3500
|
-
describe('prepareCustomExecution', () => {
|
|
3501
|
-
const requestBytes = new Uint8Array([9, 8, 7]);
|
|
3502
|
-
function customDelta(proposalType, cosignerSigs) {
|
|
3503
|
-
return {
|
|
3504
|
-
account_id: '0x' + 'a'.repeat(30),
|
|
3505
|
-
nonce: 1,
|
|
3506
|
-
prev_commitment: '0x' + 'b'.repeat(64),
|
|
3507
|
-
delta_payload: {
|
|
3508
|
-
tx_summary: { data: 'AQID' },
|
|
3509
|
-
signatures: [],
|
|
3510
|
-
metadata: {
|
|
3511
|
-
proposal_type: proposalType,
|
|
3512
|
-
chain_anchor: MOCK_CHAIN_ANCHOR_B64,
|
|
3513
|
-
description: '',
|
|
3514
|
-
},
|
|
3515
|
-
},
|
|
3516
|
-
status: {
|
|
3517
|
-
status: 'pending',
|
|
3518
|
-
timestamp: '2024-01-01T00:00:00Z',
|
|
3519
|
-
proposer_id: '0x' + 'c'.repeat(64),
|
|
3520
|
-
cosigner_sigs: cosignerSigs,
|
|
3521
|
-
},
|
|
3522
|
-
};
|
|
3523
|
-
}
|
|
3524
|
-
function falconSig(signerId) {
|
|
3525
|
-
return {
|
|
3526
|
-
signer_id: signerId,
|
|
3527
|
-
signature: { scheme: 'falcon', signature: '0x' + 'e'.repeat(128) },
|
|
3528
|
-
timestamp: '2024-01-01T00:00:00Z',
|
|
3529
|
-
};
|
|
3530
|
-
}
|
|
3531
|
-
it('rejects a built-in proposal type', async () => {
|
|
3532
|
-
const config = {
|
|
3533
|
-
threshold: 1,
|
|
3534
|
-
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
3535
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
3536
|
-
};
|
|
3537
|
-
const multisig = createTestMultisig(config);
|
|
3538
|
-
const builtinDelta = {
|
|
3539
|
-
...customDelta('change_threshold', [falconSig('0x' + 'a'.repeat(64))]),
|
|
3540
|
-
};
|
|
3541
|
-
builtinDelta.delta_payload.metadata = {
|
|
3542
|
-
proposal_type: 'change_threshold',
|
|
3543
|
-
chain_anchor: MOCK_CHAIN_ANCHOR_B64,
|
|
3544
|
-
description: '',
|
|
3545
|
-
target_threshold: 1,
|
|
3546
|
-
signer_commitments: ['0x' + 'a'.repeat(64)],
|
|
3547
|
-
};
|
|
3548
|
-
mockFetch.mockResolvedValueOnce({
|
|
3549
|
-
ok: true,
|
|
3550
|
-
json: async () => builtinDelta,
|
|
3551
|
-
});
|
|
3552
|
-
await expect(multisig.prepareCustomExecution('0x' + 'c'.repeat(64), requestBytes)).rejects.toThrow('prepareCustomExecution is only for custom proposals');
|
|
3553
|
-
});
|
|
3554
|
-
it('rejects a proposal that is below its signature threshold', async () => {
|
|
3555
|
-
const config = {
|
|
3556
|
-
threshold: 2,
|
|
3557
|
-
signerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
|
|
3558
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
3559
|
-
};
|
|
3560
|
-
const multisig = createTestMultisig(config);
|
|
3561
|
-
mockFetch.mockResolvedValueOnce({
|
|
3562
|
-
ok: true,
|
|
3563
|
-
json: async () => customDelta('b2agg', [falconSig('0x' + 'a'.repeat(64))]),
|
|
3564
|
-
});
|
|
3565
|
-
await expect(multisig.prepareCustomExecution('0x' + 'c'.repeat(64), requestBytes)).rejects.toThrow('have 1 of 2 required signatures');
|
|
3566
|
-
});
|
|
3567
|
-
it('rejects when the rebuilt request does not reproduce the signed commitment', async () => {
|
|
3568
|
-
const config = {
|
|
3569
|
-
threshold: 1,
|
|
3570
|
-
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
3571
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
3572
|
-
};
|
|
3573
|
-
const multisig = createTestMultisig(config);
|
|
3574
|
-
// Signed commitment comes from TransactionSummary.deserialize -> 'c' * 64.
|
|
3575
|
-
// Make the binding request derive a different commitment so the check fails.
|
|
3576
|
-
vi.mocked(executeForSummaryAt).mockResolvedValueOnce({
|
|
3577
|
-
toCommitment: () => ({
|
|
3578
|
-
toHex: () => '0x' + '9'.repeat(64),
|
|
3579
|
-
}),
|
|
3580
|
-
serialize: () => new Uint8Array([1, 2, 3]),
|
|
3581
|
-
});
|
|
3582
|
-
mockFetch.mockResolvedValueOnce({
|
|
3583
|
-
ok: true,
|
|
3584
|
-
json: async () => customDelta('b2agg', [falconSig('0x' + 'a'.repeat(64))]),
|
|
3585
|
-
});
|
|
3586
|
-
await expect(multisig.prepareCustomExecution('0x' + 'c'.repeat(64), requestBytes)).rejects.toThrow('Custom proposal binding mismatch');
|
|
3587
|
-
});
|
|
3588
|
-
it('fails when GUARDIAN does not return an acknowledgment signature', async () => {
|
|
3589
|
-
const config = {
|
|
3590
|
-
threshold: 1,
|
|
3591
|
-
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
3592
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
3593
|
-
};
|
|
3594
|
-
const multisig = createTestMultisig(config);
|
|
3595
|
-
const ready = customDelta('b2agg', [falconSig('0x' + 'a'.repeat(64))]);
|
|
3596
|
-
// getDeltaProposal
|
|
3597
|
-
mockFetch.mockResolvedValueOnce({
|
|
3598
|
-
ok: true,
|
|
3599
|
-
json: async () => ready,
|
|
3600
|
-
});
|
|
3601
|
-
// pushDelta returns no ack_sig
|
|
3602
|
-
mockFetch.mockResolvedValueOnce({
|
|
3603
|
-
ok: true,
|
|
3604
|
-
json: async () => ({ ...ready, ack_sig: null }),
|
|
3605
|
-
});
|
|
3606
|
-
await expect(multisig.prepareCustomExecution('0x' + 'c'.repeat(64), requestBytes)).rejects.toThrow('GUARDIAN did not return acknowledgment signature');
|
|
3607
|
-
});
|
|
3608
|
-
});
|
|
3609
|
-
describe('proposal metadata preservation', () => {
|
|
3610
|
-
it('should preserve local metadata when syncing proposals', async () => {
|
|
3611
|
-
const config = {
|
|
3612
|
-
threshold: 1,
|
|
3613
|
-
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
3614
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
3615
|
-
};
|
|
3616
|
-
const multisig = createTestMultisig(config);
|
|
3617
|
-
// Create a proposal with metadata
|
|
3618
|
-
const mockDelta = {
|
|
3619
|
-
account_id: '0x' + 'a'.repeat(30),
|
|
3620
|
-
nonce: 1,
|
|
3621
|
-
prev_commitment: '0x' + 'b'.repeat(64),
|
|
3622
|
-
delta_payload: {
|
|
3623
|
-
tx_summary: { data: 'AQID' },
|
|
3624
|
-
signatures: [],
|
|
3625
|
-
metadata: {
|
|
3626
|
-
proposal_type: 'add_signer',
|
|
3627
|
-
chain_anchor: MOCK_CHAIN_ANCHOR_B64,
|
|
3628
|
-
target_threshold: 2,
|
|
3629
|
-
signer_commitments: ['0x1', '0x2'],
|
|
3630
|
-
},
|
|
3631
|
-
},
|
|
3632
|
-
status: {
|
|
3633
|
-
status: 'pending',
|
|
3634
|
-
timestamp: '2024-01-01T00:00:00Z',
|
|
3635
|
-
proposer_id: '0x' + 'c'.repeat(64),
|
|
3636
|
-
cosigner_sigs: [],
|
|
3637
|
-
},
|
|
3638
|
-
};
|
|
3639
|
-
mockFetch.mockResolvedValueOnce({
|
|
3640
|
-
ok: true,
|
|
3641
|
-
json: async () => ({
|
|
3642
|
-
delta: mockDelta,
|
|
3643
|
-
commitment: '0x' + 'c'.repeat(64),
|
|
3644
|
-
}),
|
|
3645
|
-
});
|
|
3646
|
-
const proposal = await multisig.createProposal(1, 'AQID', {
|
|
3647
|
-
proposalType: 'add_signer',
|
|
3648
|
-
chainAnchor: MOCK_CHAIN_ANCHOR_B64,
|
|
3649
|
-
targetThreshold: 2,
|
|
3650
|
-
targetSignerCommitments: ['0x1', '0x2'],
|
|
3651
|
-
description: '',
|
|
3652
|
-
});
|
|
3653
|
-
expect(proposal.metadata?.proposalType).toBe('add_signer');
|
|
3654
|
-
// Now sync - should preserve local metadata
|
|
3655
|
-
mockFetch.mockResolvedValueOnce({
|
|
3656
|
-
ok: true,
|
|
3657
|
-
json: async () => ({
|
|
3658
|
-
proposals: [mockDelta],
|
|
3659
|
-
}),
|
|
3660
|
-
});
|
|
3661
|
-
const syncedProposals = await multisig.syncProposals();
|
|
3662
|
-
const syncedProposal = syncedProposals.find(p => p.nonce === 1);
|
|
3663
|
-
expect(syncedProposal?.metadata?.proposalType).toBe('add_signer');
|
|
3664
|
-
});
|
|
3665
|
-
it('should use GUARDIAN metadata for new proposals from other signers', async () => {
|
|
3666
|
-
const config = {
|
|
3667
|
-
threshold: 2,
|
|
3668
|
-
signerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
|
|
3669
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
3670
|
-
};
|
|
3671
|
-
const multisig = createTestMultisig(config);
|
|
3672
|
-
// Sync proposals - no local proposals exist
|
|
3673
|
-
const mockProposals = [
|
|
3674
|
-
{
|
|
3675
|
-
account_id: '0x' + 'a'.repeat(30),
|
|
3676
|
-
nonce: 1,
|
|
3677
|
-
prev_commitment: '0x' + 'b'.repeat(64),
|
|
3678
|
-
delta_payload: {
|
|
3679
|
-
tx_summary: { data: 'AQID' },
|
|
3680
|
-
signatures: [],
|
|
3681
|
-
metadata: {
|
|
3682
|
-
proposal_type: 'p2id',
|
|
3683
|
-
chain_anchor: MOCK_CHAIN_ANCHOR_B64,
|
|
3684
|
-
recipient_id: '0xrecipient',
|
|
3685
|
-
faucet_id: '0xfaucet',
|
|
3686
|
-
amount: '100',
|
|
3687
|
-
},
|
|
3688
|
-
},
|
|
3689
|
-
status: {
|
|
3690
|
-
status: 'pending',
|
|
3691
|
-
timestamp: '2024-01-01T00:00:00Z',
|
|
3692
|
-
proposer_id: '0x' + 'other'.repeat(12),
|
|
3693
|
-
cosigner_sigs: [],
|
|
3694
|
-
},
|
|
3695
|
-
},
|
|
3696
|
-
];
|
|
3697
|
-
mockFetch.mockResolvedValueOnce({
|
|
3698
|
-
ok: true,
|
|
3699
|
-
json: async () => ({ proposals: mockProposals }),
|
|
3700
|
-
});
|
|
3701
|
-
const proposals = await multisig.syncProposals();
|
|
3702
|
-
expect(proposals.length).toBe(1);
|
|
3703
|
-
expect(proposals[0].metadata?.proposalType).toBe('p2id');
|
|
3704
|
-
});
|
|
3705
|
-
});
|
|
3706
|
-
describe('createProposal with different metadata types', () => {
|
|
3707
|
-
it('should create consume_notes proposal', async () => {
|
|
3708
|
-
const config = {
|
|
3709
|
-
threshold: 1,
|
|
3710
|
-
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
3711
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
3712
|
-
};
|
|
3713
|
-
const multisig = createTestMultisig(config);
|
|
3714
|
-
const mockDelta = {
|
|
3715
|
-
account_id: '0x' + 'a'.repeat(30),
|
|
3716
|
-
nonce: 1,
|
|
3717
|
-
prev_commitment: '0x' + 'b'.repeat(64),
|
|
3718
|
-
delta_payload: {
|
|
3719
|
-
tx_summary: { data: 'AQID' },
|
|
3720
|
-
signatures: [],
|
|
3721
|
-
metadata: {
|
|
3722
|
-
proposal_type: 'add_signer',
|
|
3723
|
-
chain_anchor: MOCK_CHAIN_ANCHOR_B64,
|
|
3724
|
-
target_threshold: 2,
|
|
3725
|
-
signer_commitments: ['0x1', '0x2'],
|
|
3726
|
-
description: '',
|
|
3727
|
-
},
|
|
3728
|
-
},
|
|
3729
|
-
status: {
|
|
3730
|
-
status: 'pending',
|
|
3731
|
-
timestamp: '2024-01-01T00:00:00Z',
|
|
3732
|
-
proposer_id: '0x' + 'c'.repeat(64),
|
|
3733
|
-
cosigner_sigs: [],
|
|
3734
|
-
},
|
|
3735
|
-
};
|
|
3736
|
-
mockFetch.mockResolvedValueOnce({
|
|
3737
|
-
ok: true,
|
|
3738
|
-
json: async () => ({
|
|
3739
|
-
delta: mockDelta,
|
|
3740
|
-
commitment: '0x' + 'c'.repeat(64),
|
|
3741
|
-
}),
|
|
3742
|
-
});
|
|
3743
|
-
const proposal = await multisig.createProposal(1, 'AQID', {
|
|
3744
|
-
proposalType: 'consume_notes',
|
|
3745
|
-
chainAnchor: MOCK_CHAIN_ANCHOR_B64,
|
|
3746
|
-
noteIds: ['0xnote1', '0xnote2'],
|
|
3747
|
-
description: '',
|
|
3748
|
-
});
|
|
3749
|
-
expect(proposal.metadata?.proposalType).toBe('consume_notes');
|
|
3750
|
-
});
|
|
3751
|
-
it('should create p2id proposal', async () => {
|
|
3752
|
-
const config = {
|
|
3753
|
-
threshold: 1,
|
|
3754
|
-
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
3755
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
3756
|
-
};
|
|
3757
|
-
const multisig = createTestMultisig(config);
|
|
3758
|
-
const mockDelta = {
|
|
3759
|
-
account_id: '0x' + 'a'.repeat(30),
|
|
3760
|
-
nonce: 1,
|
|
3761
|
-
prev_commitment: '0x' + 'b'.repeat(64),
|
|
3762
|
-
delta_payload: {
|
|
3763
|
-
tx_summary: { data: 'AQID' },
|
|
3764
|
-
signatures: [],
|
|
3765
|
-
metadata: {
|
|
3766
|
-
proposal_type: 'add_signer',
|
|
3767
|
-
chain_anchor: MOCK_CHAIN_ANCHOR_B64,
|
|
3768
|
-
target_threshold: 1,
|
|
3769
|
-
signer_commitments: ['0x' + 'a'.repeat(64)],
|
|
3770
|
-
description: '',
|
|
3771
|
-
},
|
|
3772
|
-
},
|
|
3773
|
-
status: {
|
|
3774
|
-
status: 'pending',
|
|
3775
|
-
timestamp: '2024-01-01T00:00:00Z',
|
|
3776
|
-
proposer_id: '0x' + 'c'.repeat(64),
|
|
3777
|
-
cosigner_sigs: [],
|
|
3778
|
-
},
|
|
3779
|
-
};
|
|
3780
|
-
mockFetch.mockResolvedValueOnce({
|
|
3781
|
-
ok: true,
|
|
3782
|
-
json: async () => ({
|
|
3783
|
-
delta: mockDelta,
|
|
3784
|
-
commitment: '0x' + 'c'.repeat(64),
|
|
3785
|
-
}),
|
|
3786
|
-
});
|
|
3787
|
-
const proposal = await multisig.createProposal(1, 'AQID', {
|
|
3788
|
-
proposalType: 'p2id',
|
|
3789
|
-
chainAnchor: MOCK_CHAIN_ANCHOR_B64,
|
|
3790
|
-
recipientId: '0xrecipient',
|
|
3791
|
-
faucetId: '0xfaucet',
|
|
3792
|
-
amount: '100',
|
|
3793
|
-
description: '',
|
|
3794
|
-
});
|
|
3795
|
-
expect(proposal.metadata?.proposalType).toBe('p2id');
|
|
3796
|
-
});
|
|
3797
|
-
it('should create switch_guardian proposal', async () => {
|
|
3798
|
-
const config = {
|
|
3799
|
-
threshold: 1,
|
|
3800
|
-
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
3801
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
3802
|
-
};
|
|
3803
|
-
const multisig = createTestMultisig(config);
|
|
3804
|
-
const mockDelta = {
|
|
3805
|
-
account_id: '0x' + 'a'.repeat(30),
|
|
3806
|
-
nonce: 1,
|
|
3807
|
-
prev_commitment: '0x' + 'b'.repeat(64),
|
|
3808
|
-
delta_payload: {
|
|
3809
|
-
tx_summary: { data: 'AQID' },
|
|
3810
|
-
signatures: [],
|
|
3811
|
-
metadata: {
|
|
3812
|
-
proposalType: 'add_signer',
|
|
3813
|
-
chainAnchor: MOCK_CHAIN_ANCHOR_B64,
|
|
3814
|
-
targetThreshold: 2,
|
|
3815
|
-
targetSignerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
|
|
3816
|
-
description: '',
|
|
3817
|
-
},
|
|
3818
|
-
},
|
|
3819
|
-
status: {
|
|
3820
|
-
status: 'pending',
|
|
3821
|
-
timestamp: '2024-01-01T00:00:00Z',
|
|
3822
|
-
proposer_id: '0x' + 'c'.repeat(64),
|
|
3823
|
-
cosigner_sigs: [],
|
|
3824
|
-
},
|
|
3825
|
-
};
|
|
3826
|
-
mockFetch.mockResolvedValueOnce({
|
|
3827
|
-
ok: true,
|
|
3828
|
-
json: async () => ({
|
|
3829
|
-
delta: mockDelta,
|
|
3830
|
-
commitment: '0x' + 'c'.repeat(64),
|
|
3831
|
-
}),
|
|
3832
|
-
});
|
|
3833
|
-
const proposal = await multisig.createProposal(1, 'AQID', {
|
|
3834
|
-
proposalType: 'switch_guardian',
|
|
3835
|
-
chainAnchor: MOCK_CHAIN_ANCHOR_B64,
|
|
3836
|
-
newGuardianPubkey: '0xnewpubkey',
|
|
3837
|
-
newGuardianEndpoint: 'http://new-guardian.com',
|
|
3838
|
-
description: '',
|
|
3839
|
-
});
|
|
3840
|
-
expect(proposal.metadata?.proposalType).toBe('switch_guardian');
|
|
3841
|
-
});
|
|
3842
|
-
});
|
|
3843
|
-
describe('proposal status transitions', () => {
|
|
3844
|
-
it('should transition from pending to ready when threshold met', async () => {
|
|
3845
|
-
const config = {
|
|
3846
|
-
threshold: 2,
|
|
3847
|
-
signerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
|
|
3848
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
3849
|
-
};
|
|
3850
|
-
const multisig = createTestMultisig(config);
|
|
3851
|
-
// First sync with 1 signature (pending)
|
|
3852
|
-
const mockProposalsPending = [
|
|
3853
|
-
{
|
|
3854
|
-
account_id: '0x' + 'a'.repeat(30),
|
|
3855
|
-
nonce: 1,
|
|
3856
|
-
prev_commitment: '0x' + 'b'.repeat(64),
|
|
3857
|
-
delta_payload: {
|
|
3858
|
-
tx_summary: { data: 'AQID' },
|
|
3859
|
-
signatures: [],
|
|
3860
|
-
metadata: {
|
|
3861
|
-
proposal_type: 'add_signer',
|
|
3862
|
-
chain_anchor: MOCK_CHAIN_ANCHOR_B64,
|
|
3863
|
-
target_threshold: 2,
|
|
3864
|
-
signer_commitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
|
|
3865
|
-
description: '',
|
|
3866
|
-
},
|
|
3867
|
-
},
|
|
3868
|
-
status: {
|
|
3869
|
-
status: 'pending',
|
|
3870
|
-
timestamp: '2024-01-01T00:00:00Z',
|
|
3871
|
-
proposer_id: '0x' + 'c'.repeat(64),
|
|
3872
|
-
cosigner_sigs: [
|
|
3873
|
-
{
|
|
3874
|
-
signer_id: '0x' + 'a'.repeat(64),
|
|
3875
|
-
signature: { scheme: 'falcon', signature: '0x' + 'sig'.repeat(40) },
|
|
3876
|
-
timestamp: '2024-01-01T00:00:00Z',
|
|
3877
|
-
},
|
|
3878
|
-
],
|
|
3879
|
-
},
|
|
3880
|
-
},
|
|
3881
|
-
];
|
|
3882
|
-
mockFetch.mockResolvedValueOnce({
|
|
3883
|
-
ok: true,
|
|
3884
|
-
json: async () => ({ proposals: mockProposalsPending }),
|
|
3885
|
-
});
|
|
3886
|
-
let proposals = await multisig.syncProposals();
|
|
3887
|
-
expect(proposals[0].status).toBe('pending');
|
|
3888
|
-
// Second sync with 2 signatures (ready)
|
|
3889
|
-
const mockProposalsReady = [
|
|
3890
|
-
{
|
|
3891
|
-
...mockProposalsPending[0],
|
|
3892
|
-
delta_payload: {
|
|
3893
|
-
...mockProposalsPending[0].delta_payload,
|
|
3894
|
-
metadata: {
|
|
3895
|
-
proposal_type: 'add_signer',
|
|
3896
|
-
chain_anchor: MOCK_CHAIN_ANCHOR_B64,
|
|
3897
|
-
target_threshold: 2,
|
|
3898
|
-
signer_commitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
|
|
3899
|
-
description: '',
|
|
3900
|
-
},
|
|
3901
|
-
},
|
|
3902
|
-
status: {
|
|
3903
|
-
status: 'pending',
|
|
3904
|
-
timestamp: '2024-01-01T00:00:00Z',
|
|
3905
|
-
proposer_id: '0x' + 'c'.repeat(64),
|
|
3906
|
-
cosigner_sigs: [
|
|
3907
|
-
{
|
|
3908
|
-
signer_id: '0x' + 'a'.repeat(64),
|
|
3909
|
-
signature: { scheme: 'falcon', signature: '0x' + 'sig'.repeat(40) },
|
|
3910
|
-
timestamp: '2024-01-01T00:00:00Z',
|
|
3911
|
-
},
|
|
3912
|
-
{
|
|
3913
|
-
signer_id: '0x' + 'b'.repeat(64),
|
|
3914
|
-
signature: { scheme: 'falcon', signature: '0x' + 'sig2'.repeat(40) },
|
|
3915
|
-
timestamp: '2024-01-01T01:00:00Z',
|
|
3916
|
-
},
|
|
3917
|
-
],
|
|
3918
|
-
},
|
|
3919
|
-
},
|
|
3920
|
-
];
|
|
3921
|
-
mockFetch.mockResolvedValueOnce({
|
|
3922
|
-
ok: true,
|
|
3923
|
-
json: async () => ({ proposals: mockProposalsReady }),
|
|
3924
|
-
});
|
|
3925
|
-
proposals = await multisig.syncProposals();
|
|
3926
|
-
expect(proposals[0].status).toBe('ready');
|
|
3927
|
-
});
|
|
3928
|
-
});
|
|
3929
|
-
describe('getters', () => {
|
|
3930
|
-
it('should expose threshold', () => {
|
|
3931
|
-
const config = {
|
|
3932
|
-
threshold: 3,
|
|
3933
|
-
signerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64), '0x' + 'c'.repeat(64)],
|
|
3934
|
-
guardianCommitment: '0x' + 'd'.repeat(64),
|
|
3935
|
-
};
|
|
3936
|
-
const multisig = createTestMultisig(config);
|
|
3937
|
-
expect(multisig.threshold).toBe(3);
|
|
3938
|
-
});
|
|
3939
|
-
it('should expose signerCommitments', () => {
|
|
3940
|
-
const signerCommitments = ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)];
|
|
3941
|
-
const config = {
|
|
3942
|
-
threshold: 2,
|
|
3943
|
-
signerCommitments,
|
|
3944
|
-
guardianCommitment: '0x' + 'd'.repeat(64),
|
|
3945
|
-
};
|
|
3946
|
-
const multisig = createTestMultisig(config);
|
|
3947
|
-
expect(multisig.signerCommitments).toEqual(signerCommitments);
|
|
3948
|
-
});
|
|
3949
|
-
it('should expose guardianCommitment', () => {
|
|
3950
|
-
const guardianCommitment = '0x' + 'guardian'.repeat(20);
|
|
3951
|
-
const config = {
|
|
3952
|
-
threshold: 1,
|
|
3953
|
-
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
3954
|
-
guardianCommitment,
|
|
3955
|
-
};
|
|
3956
|
-
const multisig = createTestMultisig(config);
|
|
3957
|
-
expect(multisig.guardianCommitment).toBe(guardianCommitment);
|
|
3958
|
-
});
|
|
3959
|
-
it('should expose account when provided', () => {
|
|
3960
|
-
const config = {
|
|
3961
|
-
threshold: 1,
|
|
3962
|
-
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
3963
|
-
guardianCommitment: '0x' + 'd'.repeat(64),
|
|
3964
|
-
};
|
|
3965
|
-
const multisig = createTestMultisig(config);
|
|
3966
|
-
expect(multisig.account).toBe(mockAccount);
|
|
3967
|
-
});
|
|
3968
|
-
});
|
|
3969
|
-
describe('cross-client compatibility: sync with snake_case metadata', () => {
|
|
3970
|
-
it('should parse Rust client proposals with snake_case metadata', async () => {
|
|
3971
|
-
const config = {
|
|
3972
|
-
threshold: 2,
|
|
3973
|
-
signerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
|
|
3974
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
3975
|
-
};
|
|
3976
|
-
const multisig = createTestMultisig(config);
|
|
3977
|
-
// Simulates a GUARDIAN response with canonical snake_case metadata
|
|
3978
|
-
const rustProposals = [
|
|
3979
|
-
{
|
|
3980
|
-
account_id: '0x' + 'a'.repeat(30),
|
|
3981
|
-
nonce: 1,
|
|
3982
|
-
prev_commitment: '0x' + 'b'.repeat(64),
|
|
3983
|
-
delta_payload: {
|
|
3984
|
-
tx_summary: { data: 'AQID' },
|
|
3985
|
-
signatures: [],
|
|
3986
|
-
metadata: {
|
|
3987
|
-
proposal_type: 'change_threshold',
|
|
3988
|
-
chain_anchor: MOCK_CHAIN_ANCHOR_B64,
|
|
3989
|
-
target_threshold: 3,
|
|
3990
|
-
signer_commitments: ['0xa', '0xb', '0xc'],
|
|
3991
|
-
salt: '0xlegacysalt',
|
|
3992
|
-
},
|
|
3993
|
-
},
|
|
3994
|
-
status: {
|
|
3995
|
-
status: 'pending',
|
|
3996
|
-
timestamp: '2024-01-01T00:00:00Z',
|
|
3997
|
-
proposer_id: '0x' + 'rust_client'.repeat(5),
|
|
3998
|
-
cosigner_sigs: [],
|
|
3999
|
-
},
|
|
4000
|
-
},
|
|
4001
|
-
];
|
|
4002
|
-
mockFetch.mockResolvedValueOnce({
|
|
4003
|
-
ok: true,
|
|
4004
|
-
json: async () => ({ proposals: rustProposals }),
|
|
4005
|
-
});
|
|
4006
|
-
const proposals = await multisig.syncProposals();
|
|
4007
|
-
expect(proposals.length).toBe(1);
|
|
4008
|
-
// The TS client should normalize snake_case to camelCase
|
|
4009
|
-
expect(proposals[0].metadata?.proposalType).toBe('change_threshold');
|
|
4010
|
-
if (proposals[0].metadata?.proposalType === 'change_threshold') {
|
|
4011
|
-
expect(proposals[0].metadata.targetThreshold).toBe(3);
|
|
4012
|
-
expect(proposals[0].metadata.targetSignerCommitments).toEqual(['0xa', '0xb', '0xc']);
|
|
4013
|
-
}
|
|
4014
|
-
});
|
|
4015
|
-
it('should parse Rust client P2ID proposal with snake_case fields', async () => {
|
|
4016
|
-
const config = {
|
|
4017
|
-
threshold: 1,
|
|
4018
|
-
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
4019
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
4020
|
-
};
|
|
4021
|
-
const multisig = createTestMultisig(config);
|
|
4022
|
-
// P2ID proposal with canonical snake_case fields
|
|
4023
|
-
const p2idProposals = [
|
|
4024
|
-
{
|
|
4025
|
-
account_id: '0x' + 'a'.repeat(30),
|
|
4026
|
-
nonce: 1,
|
|
4027
|
-
prev_commitment: '0x' + 'b'.repeat(64),
|
|
4028
|
-
delta_payload: {
|
|
4029
|
-
tx_summary: { data: 'AQID' },
|
|
4030
|
-
signatures: [],
|
|
4031
|
-
metadata: {
|
|
4032
|
-
proposal_type: 'p2id',
|
|
4033
|
-
chain_anchor: MOCK_CHAIN_ANCHOR_B64,
|
|
4034
|
-
recipient_id: '0xrecipient',
|
|
4035
|
-
faucet_id: '0xfaucet',
|
|
4036
|
-
amount: '12345',
|
|
4037
|
-
salt: '0xsalt',
|
|
4038
|
-
},
|
|
4039
|
-
},
|
|
4040
|
-
status: {
|
|
4041
|
-
status: 'pending',
|
|
4042
|
-
timestamp: '2024-01-01T00:00:00Z',
|
|
4043
|
-
proposer_id: '0x' + 'c'.repeat(64),
|
|
4044
|
-
cosigner_sigs: [
|
|
4045
|
-
{
|
|
4046
|
-
signer_id: '0x' + 'a'.repeat(64),
|
|
4047
|
-
signature: { scheme: 'falcon', signature: '0x' + 'sig'.repeat(40) },
|
|
4048
|
-
timestamp: '2024-01-01T00:00:00Z',
|
|
4049
|
-
},
|
|
4050
|
-
],
|
|
4051
|
-
},
|
|
4052
|
-
},
|
|
4053
|
-
];
|
|
4054
|
-
mockFetch.mockResolvedValueOnce({
|
|
4055
|
-
ok: true,
|
|
4056
|
-
json: async () => ({ proposals: p2idProposals }),
|
|
4057
|
-
});
|
|
4058
|
-
const proposals = await multisig.syncProposals();
|
|
4059
|
-
expect(proposals.length).toBe(1);
|
|
4060
|
-
expect(proposals[0].metadata?.proposalType).toBe('p2id');
|
|
4061
|
-
if (proposals[0].metadata?.proposalType === 'p2id') {
|
|
4062
|
-
expect(proposals[0].metadata.recipientId).toBe('0xrecipient');
|
|
4063
|
-
expect(proposals[0].metadata.faucetId).toBe('0xfaucet');
|
|
4064
|
-
expect(proposals[0].metadata.amount).toBe('12345');
|
|
4065
|
-
}
|
|
4066
|
-
});
|
|
4067
|
-
it('should parse switch_guardian proposal with snake_case fields', async () => {
|
|
4068
|
-
const config = {
|
|
4069
|
-
threshold: 1,
|
|
4070
|
-
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
4071
|
-
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
4072
|
-
};
|
|
4073
|
-
const multisig = createTestMultisig(config);
|
|
4074
|
-
const switchGuardianProposals = [
|
|
4075
|
-
{
|
|
4076
|
-
account_id: '0x' + 'a'.repeat(30),
|
|
4077
|
-
nonce: 1,
|
|
4078
|
-
prev_commitment: '0x' + 'b'.repeat(64),
|
|
4079
|
-
delta_payload: {
|
|
4080
|
-
tx_summary: { data: 'AQID' },
|
|
4081
|
-
signatures: [],
|
|
4082
|
-
metadata: {
|
|
4083
|
-
proposal_type: 'switch_guardian',
|
|
4084
|
-
chain_anchor: MOCK_CHAIN_ANCHOR_B64,
|
|
4085
|
-
new_guardian_pubkey: '0xnewpubkey',
|
|
4086
|
-
new_guardian_endpoint: 'http://new-guardian.com',
|
|
4087
|
-
salt: '0xsalt',
|
|
4088
|
-
},
|
|
4089
|
-
},
|
|
4090
|
-
status: {
|
|
4091
|
-
status: 'pending',
|
|
4092
|
-
timestamp: '2024-01-01T00:00:00Z',
|
|
4093
|
-
proposer_id: '0x' + 'c'.repeat(64),
|
|
4094
|
-
cosigner_sigs: [],
|
|
4095
|
-
},
|
|
4096
|
-
},
|
|
4097
|
-
];
|
|
4098
|
-
mockFetch.mockResolvedValueOnce({
|
|
4099
|
-
ok: true,
|
|
4100
|
-
json: async () => ({ proposals: switchGuardianProposals }),
|
|
4101
|
-
});
|
|
4102
|
-
const proposals = await multisig.syncProposals();
|
|
4103
|
-
expect(proposals.length).toBe(1);
|
|
4104
|
-
expect(proposals[0].metadata?.proposalType).toBe('switch_guardian');
|
|
4105
|
-
if (proposals[0].metadata?.proposalType === 'switch_guardian') {
|
|
4106
|
-
expect(proposals[0].metadata.newGuardianPubkey).toBe('0xnewpubkey');
|
|
4107
|
-
expect(proposals[0].metadata.newGuardianEndpoint).toBe('http://new-guardian.com');
|
|
4108
|
-
}
|
|
4109
|
-
});
|
|
4110
|
-
});
|
|
4111
|
-
});
|
|
4112
|
-
//# sourceMappingURL=multisig.test.js.map
|