@openzeppelin/miden-multisig-client 0.13.1 → 0.13.3
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 +98 -119
- package/dist/account/builder.d.ts +18 -0
- package/dist/account/builder.d.ts.map +1 -1
- package/dist/account/builder.js +59 -29
- package/dist/account/builder.js.map +1 -1
- package/dist/account/builder.test.js +108 -32
- package/dist/account/builder.test.js.map +1 -1
- package/dist/account/index.d.ts +1 -1
- package/dist/account/index.d.ts.map +1 -1
- package/dist/account/index.js +1 -1
- package/dist/account/index.js.map +1 -1
- package/dist/account/masm/account-components/auth.d.ts +5 -0
- package/dist/account/masm/account-components/auth.d.ts.map +1 -0
- package/dist/account/masm/account-components/auth.js +67 -0
- package/dist/account/masm/account-components/auth.js.map +1 -0
- package/dist/account/masm/auth.d.ts +5 -0
- package/dist/account/masm/auth.d.ts.map +1 -0
- package/dist/account/masm/auth.js +1395 -0
- package/dist/account/masm/auth.js.map +1 -0
- package/dist/account/masm/index.d.ts +3 -0
- package/dist/account/masm/index.d.ts.map +1 -0
- package/dist/account/masm/index.js +5 -0
- package/dist/account/masm/index.js.map +1 -0
- package/dist/account/masm.d.ts +1 -4
- package/dist/account/masm.d.ts.map +1 -1
- package/dist/account/masm.js +3 -1034
- package/dist/account/masm.js.map +1 -1
- package/dist/account/storage.d.ts +2 -2
- package/dist/account/storage.d.ts.map +1 -1
- package/dist/account/storage.js +18 -16
- package/dist/account/storage.js.map +1 -1
- package/dist/client.d.ts +67 -9
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +73 -22
- package/dist/client.js.map +1 -1
- package/dist/client.test.js +31 -32
- package/dist/client.test.js.map +1 -1
- package/dist/index.d.ts +54 -12
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +53 -9
- package/dist/index.js.map +1 -1
- package/dist/inspector.d.ts +32 -6
- package/dist/inspector.d.ts.map +1 -1
- package/dist/inspector.js +58 -23
- package/dist/inspector.js.map +1 -1
- package/dist/inspector.test.js +18 -18
- package/dist/inspector.test.js.map +1 -1
- package/dist/multisig/proposal/execution.d.ts +4 -4
- package/dist/multisig/proposal/execution.d.ts.map +1 -1
- package/dist/multisig/proposal/execution.js +42 -31
- package/dist/multisig/proposal/execution.js.map +1 -1
- package/dist/multisig/proposal/parser.d.ts +3 -3
- package/dist/multisig/proposal/parser.d.ts.map +1 -1
- package/dist/multisig/proposal/parser.js +23 -23
- package/dist/multisig/proposal/parser.js.map +1 -1
- package/dist/multisig/signing.d.ts +3 -3
- package/dist/multisig/signing.d.ts.map +1 -1
- package/dist/multisig/signing.js +3 -3
- package/dist/multisig/signing.js.map +1 -1
- package/dist/multisig.d.ts +188 -48
- package/dist/multisig.d.ts.map +1 -1
- package/dist/multisig.js +650 -308
- package/dist/multisig.js.map +1 -1
- package/dist/multisig.test.js +1680 -241
- package/dist/multisig.test.js.map +1 -1
- package/dist/procedures.d.ts +46 -24
- package/dist/procedures.d.ts.map +1 -1
- package/dist/procedures.js +46 -21
- package/dist/procedures.js.map +1 -1
- package/dist/proposal/factory.d.ts +18 -0
- package/dist/proposal/factory.d.ts.map +1 -0
- package/dist/proposal/factory.js +99 -0
- package/dist/proposal/factory.js.map +1 -0
- package/dist/proposal/metadata.d.ts +5 -12
- package/dist/proposal/metadata.d.ts.map +1 -1
- package/dist/proposal/metadata.js +118 -174
- package/dist/proposal/metadata.js.map +1 -1
- package/dist/proposal/signatures.d.ts +10 -0
- package/dist/proposal/signatures.d.ts.map +1 -0
- package/dist/proposal/signatures.js +54 -0
- package/dist/proposal/signatures.js.map +1 -0
- package/dist/signer.d.ts +2 -0
- package/dist/signer.d.ts.map +1 -1
- package/dist/signer.js +2 -0
- package/dist/signer.js.map +1 -1
- package/dist/signers/ecdsa.d.ts +3 -0
- package/dist/signers/ecdsa.d.ts.map +1 -1
- package/dist/signers/ecdsa.js +8 -4
- package/dist/signers/ecdsa.js.map +1 -1
- package/dist/signers/ecdsa.test.d.ts +2 -0
- package/dist/signers/ecdsa.test.d.ts.map +1 -0
- package/dist/signers/ecdsa.test.js +65 -0
- package/dist/signers/ecdsa.test.js.map +1 -0
- package/dist/signers/falcon.d.ts +3 -0
- package/dist/signers/falcon.d.ts.map +1 -1
- package/dist/signers/falcon.js +8 -4
- package/dist/signers/falcon.js.map +1 -1
- package/dist/signers/falcon.test.js +12 -0
- package/dist/signers/falcon.test.js.map +1 -1
- package/dist/signers/miden-wallet.d.ts +3 -1
- package/dist/signers/miden-wallet.d.ts.map +1 -1
- package/dist/signers/miden-wallet.js +11 -2
- package/dist/signers/miden-wallet.js.map +1 -1
- package/dist/signers/miden-wallet.test.js +26 -0
- package/dist/signers/miden-wallet.test.js.map +1 -1
- package/dist/signers/para.d.ts +3 -3
- package/dist/signers/para.d.ts.map +1 -1
- package/dist/signers/para.js +15 -6
- package/dist/signers/para.js.map +1 -1
- package/dist/signers/para.test.js +39 -0
- package/dist/signers/para.test.js.map +1 -1
- package/dist/transaction/consumeNotes.d.ts.map +1 -1
- package/dist/transaction/consumeNotes.js +1 -0
- package/dist/transaction/consumeNotes.js.map +1 -1
- package/dist/transaction/index.d.ts +2 -1
- package/dist/transaction/index.d.ts.map +1 -1
- package/dist/transaction/index.js +2 -1
- package/dist/transaction/index.js.map +1 -1
- package/dist/transaction/options.d.ts +1 -1
- package/dist/transaction/options.d.ts.map +1 -1
- package/dist/transaction/p2id.d.ts +1 -0
- package/dist/transaction/p2id.d.ts.map +1 -1
- package/dist/transaction/p2id.js +9 -3
- package/dist/transaction/p2id.js.map +1 -1
- package/dist/transaction/updateGuardian.d.ts +7 -0
- package/dist/transaction/updateGuardian.d.ts.map +1 -0
- package/dist/transaction/{updatePsm.js → updateGuardian.js} +15 -29
- package/dist/transaction/updateGuardian.js.map +1 -0
- package/dist/transaction/updateProcedureThreshold.d.ts +9 -0
- package/dist/transaction/updateProcedureThreshold.d.ts.map +1 -0
- package/dist/transaction/updateProcedureThreshold.js +55 -0
- package/dist/transaction/updateProcedureThreshold.js.map +1 -0
- package/dist/transaction/updateSigners.d.ts.map +1 -1
- package/dist/transaction/updateSigners.js +5 -23
- package/dist/transaction/updateSigners.js.map +1 -1
- package/dist/transaction.d.ts +2 -1
- package/dist/transaction.d.ts.map +1 -1
- package/dist/transaction.js +2 -1
- package/dist/transaction.js.map +1 -1
- package/dist/types/proposal.d.ts +32 -15
- package/dist/types/proposal.d.ts.map +1 -1
- package/dist/types.d.ts +24 -7
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/digest.d.ts +5 -0
- package/dist/utils/digest.d.ts.map +1 -1
- package/dist/utils/digest.js +34 -1
- package/dist/utils/digest.js.map +1 -1
- package/dist/utils/digest.test.d.ts +2 -0
- package/dist/utils/digest.test.d.ts.map +1 -0
- package/dist/utils/digest.test.js +48 -0
- package/dist/utils/digest.test.js.map +1 -0
- package/dist/utils/signature.d.ts +3 -1
- package/dist/utils/signature.d.ts.map +1 -1
- package/dist/utils/signature.js +31 -6
- package/dist/utils/signature.js.map +1 -1
- package/dist/utils/word.test.js +1 -52
- package/dist/utils/word.test.js.map +1 -1
- package/masm/account_components/auth/multisig.masm +13 -0
- package/masm/account_components/auth/multisig_ecdsa.masm +13 -0
- package/masm/account_components/auth/multisig_guardian.masm +17 -0
- package/masm/account_components/auth/multisig_guardian_ecdsa.masm +17 -0
- package/masm/auth/guardian.masm +177 -0
- package/masm/auth/guardian_ecdsa.masm +178 -0
- package/masm/{multisig.masm → auth/multisig.masm} +149 -55
- package/masm/{multisig_ecdsa.masm → auth/multisig_ecdsa.masm} +148 -56
- package/package.json +11 -9
- package/src/account/builder.test.ts +165 -0
- package/src/account/builder.ts +71 -34
- package/src/account/index.ts +1 -1
- package/src/account/masm/account-components/auth.ts +70 -0
- package/src/account/masm/auth.ts +1398 -0
- package/src/account/masm/index.ts +5 -0
- package/src/account/masm.ts +3 -1040
- package/src/account/storage.ts +18 -16
- package/src/client.test.ts +31 -35
- package/src/client.ts +98 -26
- package/src/index.ts +84 -22
- package/src/inspector.test.ts +17 -17
- package/src/inspector.ts +63 -29
- package/src/multisig/proposal/execution.ts +66 -49
- package/src/multisig/proposal/parser.ts +26 -26
- package/src/multisig/signing.ts +4 -4
- package/src/multisig.test.ts +2097 -248
- package/src/multisig.ts +882 -388
- package/src/procedures.ts +50 -26
- package/src/proposal/factory.ts +171 -0
- package/src/proposal/metadata.ts +165 -0
- package/src/proposal/signatures.ts +68 -0
- package/src/signer.ts +2 -0
- package/src/signers/ecdsa.test.ts +80 -0
- package/src/signers/ecdsa.ts +15 -4
- package/src/signers/falcon.test.ts +19 -0
- package/src/signers/falcon.ts +16 -5
- package/src/signers/miden-wallet.test.ts +47 -0
- package/src/signers/miden-wallet.ts +18 -1
- package/src/signers/para.test.ts +57 -0
- package/src/signers/para.ts +26 -7
- package/src/transaction/consumeNotes.ts +1 -0
- package/src/transaction/index.ts +2 -2
- package/src/transaction/options.ts +1 -2
- package/src/transaction/p2id.test.ts +190 -0
- package/src/transaction/p2id.ts +11 -5
- package/src/transaction/rpoRandomCoin.test.ts +64 -0
- package/src/transaction/rpoRandomCoin.ts +24 -0
- package/src/transaction/{updatePsm.ts → updateGuardian.ts} +19 -32
- package/src/transaction/updateProcedureThreshold.ts +97 -0
- package/src/transaction/updateSigners.ts +12 -27
- package/src/transaction.ts +5 -3
- package/src/types/proposal.ts +43 -11
- package/src/types.ts +29 -11
- package/src/utils/digest.test.ts +55 -0
- package/src/utils/digest.ts +51 -1
- package/src/utils/signature.ts +47 -9
- package/src/utils/word.test.ts +1 -55
- package/dist/account.d.ts +0 -104
- package/dist/account.d.ts.map +0 -1
- package/dist/account.js +0 -136
- package/dist/account.js.map +0 -1
- package/dist/builder.d.ts +0 -54
- package/dist/builder.d.ts.map +0 -1
- package/dist/builder.js +0 -78
- package/dist/builder.js.map +0 -1
- package/dist/http.d.ts +0 -64
- package/dist/http.d.ts.map +0 -1
- package/dist/http.js +0 -162
- package/dist/http.js.map +0 -1
- package/dist/keystore.d.ts +0 -65
- package/dist/keystore.d.ts.map +0 -1
- package/dist/keystore.js +0 -121
- package/dist/keystore.js.map +0 -1
- package/dist/miden.d.ts +0 -9
- package/dist/miden.d.ts.map +0 -1
- package/dist/miden.js +0 -26
- package/dist/miden.js.map +0 -1
- package/dist/multisig/metadata.d.ts +0 -18
- package/dist/multisig/metadata.d.ts.map +0 -1
- package/dist/multisig/metadata.js +0 -100
- package/dist/multisig/metadata.js.map +0 -1
- package/dist/multisig/metadata.test.d.ts +0 -2
- package/dist/multisig/metadata.test.d.ts.map +0 -1
- package/dist/multisig/metadata.test.js +0 -323
- package/dist/multisig/metadata.test.js.map +0 -1
- package/dist/proposal/exported.d.ts +0 -12
- package/dist/proposal/exported.d.ts.map +0 -1
- package/dist/proposal/exported.js +0 -88
- package/dist/proposal/exported.js.map +0 -1
- package/dist/signer.test.d.ts +0 -2
- package/dist/signer.test.d.ts.map +0 -1
- package/dist/signer.test.js +0 -123
- package/dist/signer.test.js.map +0 -1
- package/dist/transaction/p2id.interop.test.d.ts +0 -2
- package/dist/transaction/p2id.interop.test.d.ts.map +0 -1
- package/dist/transaction/p2id.interop.test.js +0 -36
- package/dist/transaction/p2id.interop.test.js.map +0 -1
- package/dist/transaction/updatePsm.d.ts +0 -7
- package/dist/transaction/updatePsm.d.ts.map +0 -1
- package/dist/transaction/updatePsm.js.map +0 -1
- package/dist/transport/http.d.ts +0 -64
- package/dist/transport/http.d.ts.map +0 -1
- package/dist/transport/http.js +0 -162
- package/dist/transport/http.js.map +0 -1
- package/dist/transport/index.d.ts +0 -5
- package/dist/transport/index.d.ts.map +0 -1
- package/dist/transport/index.js +0 -5
- package/dist/transport/index.js.map +0 -1
- package/dist/utils/state-validation.d.ts +0 -5
- package/dist/utils/state-validation.d.ts.map +0 -1
- package/dist/utils/state-validation.js +0 -30
- package/dist/utils/state-validation.js.map +0 -1
- package/dist/utils/validation.d.ts +0 -6
- package/dist/utils/validation.d.ts.map +0 -1
- package/dist/utils/validation.js +0 -52
- package/dist/utils/validation.js.map +0 -1
- package/masm/psm.masm +0 -178
- package/masm/psm_ecdsa.masm +0 -179
package/dist/multisig.test.js
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
2
|
import { Multisig } from './multisig.js';
|
|
3
|
-
import {
|
|
3
|
+
import { GuardianHttpClient } from '@openzeppelin/guardian-client';
|
|
4
|
+
import { buildUpdateProcedureThresholdTransactionRequest, buildUpdateGuardianTransactionRequest, buildUpdateSignersTransactionRequest, executeForSummary, } from './transaction.js';
|
|
5
|
+
const { mockRpcGetAccountDetails, mockAccountDeserialize, mockDetectConfig } = vi.hoisted(() => ({
|
|
6
|
+
mockRpcGetAccountDetails: vi.fn(),
|
|
7
|
+
mockAccountDeserialize: vi.fn(),
|
|
8
|
+
mockDetectConfig: vi.fn(),
|
|
9
|
+
}));
|
|
4
10
|
// Mock the Miden SDK
|
|
5
11
|
vi.mock('@miden-sdk/miden-sdk', () => ({
|
|
12
|
+
Account: {
|
|
13
|
+
deserialize: mockAccountDeserialize,
|
|
14
|
+
},
|
|
6
15
|
AccountId: {
|
|
7
16
|
fromHex: vi.fn((hex) => ({ toString: () => hex })),
|
|
8
17
|
},
|
|
@@ -37,6 +46,10 @@ vi.mock('@miden-sdk/miden-sdk', () => ({
|
|
|
37
46
|
toHex: () => '0x' + 'e'.repeat(64),
|
|
38
47
|
}),
|
|
39
48
|
},
|
|
49
|
+
Endpoint: vi.fn().mockImplementation((url) => ({ url })),
|
|
50
|
+
RpcClient: vi.fn().mockImplementation(() => ({
|
|
51
|
+
getAccountDetails: mockRpcGetAccountDetails,
|
|
52
|
+
})),
|
|
40
53
|
}));
|
|
41
54
|
// Mock transaction module
|
|
42
55
|
vi.mock('./transaction.js', () => ({
|
|
@@ -46,7 +59,12 @@ vi.mock('./transaction.js', () => ({
|
|
|
46
59
|
salt: { toHex: () => '0x' + 'd'.repeat(64) },
|
|
47
60
|
configHash: { toHex: () => '0x' + 'e'.repeat(64) },
|
|
48
61
|
}),
|
|
49
|
-
|
|
62
|
+
buildUpdateProcedureThresholdTransactionRequest: vi.fn().mockResolvedValue({
|
|
63
|
+
request: {},
|
|
64
|
+
salt: { toHex: () => '0x' + 'd'.repeat(64) },
|
|
65
|
+
configHash: { toHex: () => '0x' + 'e'.repeat(64) },
|
|
66
|
+
}),
|
|
67
|
+
buildUpdateGuardianTransactionRequest: vi.fn().mockResolvedValue({
|
|
50
68
|
request: {},
|
|
51
69
|
salt: { toHex: () => '0x' + 'd'.repeat(64) },
|
|
52
70
|
}),
|
|
@@ -59,43 +77,83 @@ vi.mock('./transaction.js', () => ({
|
|
|
59
77
|
salt: { toHex: () => '0x' + 'd'.repeat(64) },
|
|
60
78
|
}),
|
|
61
79
|
}));
|
|
62
|
-
vi.mock('./utils/signature.js', () =>
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
80
|
+
vi.mock('./utils/signature.js', async () => {
|
|
81
|
+
const actual = await vi.importActual('./utils/signature.js');
|
|
82
|
+
return {
|
|
83
|
+
...actual,
|
|
84
|
+
buildSignatureAdviceEntry: vi.fn().mockImplementation((signerCommitment) => ({
|
|
85
|
+
key: { toHex: () => signerCommitment.toHex ? signerCommitment.toHex() : '0x' + 'f'.repeat(64) },
|
|
86
|
+
values: [1, 2, 3],
|
|
87
|
+
})),
|
|
88
|
+
signatureHexToBytes: vi.fn((hex) => new Uint8Array([0, 1, 2, 3])),
|
|
89
|
+
};
|
|
90
|
+
});
|
|
69
91
|
vi.mock('./utils/encoding.js', async () => {
|
|
70
92
|
const actual = await vi.importActual('./utils/encoding.js');
|
|
71
93
|
return {
|
|
72
|
-
|
|
73
|
-
normalizeHexWord: vi.fn((hex) => '0x' + hex.replace(/^0x/i, '').padStart(64, '0')),
|
|
74
|
-
bytesToHex: actual.bytesToHex,
|
|
75
|
-
hexToBytes: actual.hexToBytes,
|
|
76
|
-
uint8ArrayToBase64: actual.uint8ArrayToBase64,
|
|
77
|
-
base64ToUint8Array: actual.base64ToUint8Array,
|
|
94
|
+
...actual,
|
|
95
|
+
normalizeHexWord: vi.fn((hex) => '0x' + hex.replace(/^0x/i, '').toLowerCase().padStart(64, '0')),
|
|
78
96
|
};
|
|
79
97
|
});
|
|
80
|
-
|
|
98
|
+
vi.mock('./inspector.js', () => ({
|
|
99
|
+
AccountInspector: {
|
|
100
|
+
fromAccount: mockDetectConfig,
|
|
101
|
+
},
|
|
102
|
+
}));
|
|
103
|
+
// Mock fetch for GUARDIAN client
|
|
81
104
|
const mockFetch = vi.fn();
|
|
82
105
|
vi.stubGlobal('fetch', mockFetch);
|
|
106
|
+
function mockedAccount(commitmentHex, nonce = 0) {
|
|
107
|
+
return {
|
|
108
|
+
commitment: () => ({
|
|
109
|
+
toHex: () => commitmentHex,
|
|
110
|
+
}),
|
|
111
|
+
nonce: () => ({
|
|
112
|
+
asInt: () => BigInt(nonce),
|
|
113
|
+
}),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
83
116
|
describe('Multisig', () => {
|
|
84
|
-
let
|
|
117
|
+
let guardian;
|
|
85
118
|
let mockSigner;
|
|
86
119
|
let mockAccount;
|
|
87
120
|
let mockWebClient;
|
|
88
121
|
beforeEach(() => {
|
|
89
122
|
mockFetch.mockReset();
|
|
90
|
-
|
|
123
|
+
vi.mocked(executeForSummary).mockResolvedValue({
|
|
124
|
+
toCommitment: () => ({
|
|
125
|
+
toHex: () => '0x' + 'c'.repeat(64),
|
|
126
|
+
}),
|
|
127
|
+
serialize: () => new Uint8Array([1, 2, 3]),
|
|
128
|
+
});
|
|
129
|
+
mockRpcGetAccountDetails.mockReset();
|
|
130
|
+
mockAccountDeserialize.mockReset();
|
|
131
|
+
mockRpcGetAccountDetails.mockResolvedValue({
|
|
132
|
+
commitment: () => ({
|
|
133
|
+
toHex: () => '0x' + 'b'.repeat(64),
|
|
134
|
+
}),
|
|
135
|
+
});
|
|
136
|
+
mockAccountDeserialize.mockReturnValue(mockedAccount('0x' + 'b'.repeat(64), 1));
|
|
137
|
+
mockDetectConfig.mockReset();
|
|
138
|
+
mockDetectConfig.mockReturnValue({
|
|
139
|
+
threshold: 1,
|
|
140
|
+
numSigners: 1,
|
|
141
|
+
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
142
|
+
guardianEnabled: true,
|
|
143
|
+
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
144
|
+
vaultBalances: [],
|
|
145
|
+
procedureThresholds: new Map(),
|
|
146
|
+
});
|
|
147
|
+
guardian = new GuardianHttpClient('http://localhost:3000');
|
|
91
148
|
mockSigner = {
|
|
92
|
-
scheme: 'falcon',
|
|
93
149
|
commitment: '0x' + '1'.repeat(64),
|
|
94
150
|
publicKey: '0x' + '2'.repeat(64),
|
|
151
|
+
scheme: 'falcon',
|
|
95
152
|
signAccountIdWithTimestamp: vi.fn().mockResolvedValue('0x' + 'a'.repeat(128)),
|
|
96
|
-
|
|
153
|
+
signRequest: vi.fn().mockReturnValue('0x' + 'a'.repeat(128)),
|
|
154
|
+
signCommitment: vi.fn().mockReturnValue('0x' + 'b'.repeat(128)),
|
|
97
155
|
};
|
|
98
|
-
|
|
156
|
+
guardian.setSigner(mockSigner);
|
|
99
157
|
mockAccount = {
|
|
100
158
|
id: () => ({
|
|
101
159
|
toString: () => '0x' + 'a'.repeat(30),
|
|
@@ -111,6 +169,8 @@ describe('Multisig', () => {
|
|
|
111
169
|
applyTransaction: vi.fn(),
|
|
112
170
|
getConsumableNotes: vi.fn().mockResolvedValue([]),
|
|
113
171
|
syncState: vi.fn(),
|
|
172
|
+
getAccount: vi.fn().mockResolvedValue(null),
|
|
173
|
+
newAccount: vi.fn(),
|
|
114
174
|
};
|
|
115
175
|
});
|
|
116
176
|
describe('constructor', () => {
|
|
@@ -118,23 +178,23 @@ describe('Multisig', () => {
|
|
|
118
178
|
const config = {
|
|
119
179
|
threshold: 2,
|
|
120
180
|
signerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
|
|
121
|
-
|
|
181
|
+
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
122
182
|
};
|
|
123
|
-
const multisig = new Multisig(mockAccount, config,
|
|
183
|
+
const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
|
|
124
184
|
expect(multisig.threshold).toBe(2);
|
|
125
185
|
expect(multisig.signerCommitments).toEqual(config.signerCommitments);
|
|
126
|
-
expect(multisig.
|
|
186
|
+
expect(multisig.guardianCommitment).toBe(config.guardianCommitment);
|
|
127
187
|
expect(multisig.account).toBe(mockAccount);
|
|
128
188
|
});
|
|
129
|
-
it('should create Multisig
|
|
189
|
+
it('should create Multisig with explicit accountId override', () => {
|
|
130
190
|
const config = {
|
|
131
191
|
threshold: 2,
|
|
132
192
|
signerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
|
|
133
|
-
|
|
193
|
+
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
134
194
|
};
|
|
135
195
|
const accountId = '0x' + 'd'.repeat(30);
|
|
136
|
-
const multisig = new Multisig(
|
|
137
|
-
expect(multisig.account).
|
|
196
|
+
const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient, accountId);
|
|
197
|
+
expect(multisig.account).toBe(mockAccount);
|
|
138
198
|
expect(multisig.accountId).toBe(accountId);
|
|
139
199
|
});
|
|
140
200
|
});
|
|
@@ -143,19 +203,19 @@ describe('Multisig', () => {
|
|
|
143
203
|
const config = {
|
|
144
204
|
threshold: 1,
|
|
145
205
|
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
146
|
-
|
|
206
|
+
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
147
207
|
};
|
|
148
|
-
const multisig = new Multisig(mockAccount, config,
|
|
208
|
+
const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
|
|
149
209
|
expect(multisig.accountId).toBe('0x' + 'a'.repeat(30));
|
|
150
210
|
});
|
|
151
|
-
it('should return provided account ID
|
|
211
|
+
it('should return provided account ID when constructor override is set', () => {
|
|
152
212
|
const config = {
|
|
153
213
|
threshold: 1,
|
|
154
214
|
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
155
|
-
|
|
215
|
+
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
156
216
|
};
|
|
157
217
|
const accountId = '0x' + 'e'.repeat(30);
|
|
158
|
-
const multisig = new Multisig(
|
|
218
|
+
const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient, accountId);
|
|
159
219
|
expect(multisig.accountId).toBe(accountId);
|
|
160
220
|
});
|
|
161
221
|
});
|
|
@@ -164,20 +224,20 @@ describe('Multisig', () => {
|
|
|
164
224
|
const config = {
|
|
165
225
|
threshold: 1,
|
|
166
226
|
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
167
|
-
|
|
227
|
+
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
168
228
|
};
|
|
169
|
-
const multisig = new Multisig(mockAccount, config,
|
|
229
|
+
const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
|
|
170
230
|
expect(multisig.signerCommitment).toBe(mockSigner.commitment);
|
|
171
231
|
});
|
|
172
232
|
});
|
|
173
233
|
describe('fetchState', () => {
|
|
174
|
-
it('should fetch account state from
|
|
234
|
+
it('should fetch account state from GUARDIAN', async () => {
|
|
175
235
|
const config = {
|
|
176
236
|
threshold: 1,
|
|
177
237
|
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
178
|
-
|
|
238
|
+
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
179
239
|
};
|
|
180
|
-
const multisig = new Multisig(mockAccount, config,
|
|
240
|
+
const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
|
|
181
241
|
mockFetch.mockResolvedValueOnce({
|
|
182
242
|
ok: true,
|
|
183
243
|
json: async () => ({
|
|
@@ -194,14 +254,244 @@ describe('Multisig', () => {
|
|
|
194
254
|
expect(state.stateDataBase64).toBe('base64state');
|
|
195
255
|
});
|
|
196
256
|
});
|
|
197
|
-
describe('
|
|
198
|
-
it('should
|
|
257
|
+
describe('syncState', () => {
|
|
258
|
+
it('should overwrite local state when account is missing locally', async () => {
|
|
259
|
+
const config = {
|
|
260
|
+
threshold: 1,
|
|
261
|
+
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
262
|
+
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
263
|
+
};
|
|
264
|
+
const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient, undefined, 'https://rpc.devnet.miden.io');
|
|
265
|
+
mockWebClient.getAccount.mockResolvedValueOnce(null);
|
|
266
|
+
mockFetch.mockResolvedValueOnce({
|
|
267
|
+
ok: true,
|
|
268
|
+
json: async () => ({
|
|
269
|
+
account_id: multisig.accountId,
|
|
270
|
+
commitment: '0x' + 'b'.repeat(64),
|
|
271
|
+
state_json: { data: 'AQID' },
|
|
272
|
+
created_at: '2024-01-01T00:00:00Z',
|
|
273
|
+
updated_at: '2024-01-02T00:00:00Z',
|
|
274
|
+
}),
|
|
275
|
+
});
|
|
276
|
+
await multisig.syncState();
|
|
277
|
+
expect(mockWebClient.newAccount).toHaveBeenCalledTimes(1);
|
|
278
|
+
expect(mockRpcGetAccountDetails).toHaveBeenCalledTimes(1);
|
|
279
|
+
});
|
|
280
|
+
it('should overwrite local state when incoming commitment matches on-chain commitment', async () => {
|
|
281
|
+
const config = {
|
|
282
|
+
threshold: 1,
|
|
283
|
+
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
284
|
+
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
285
|
+
};
|
|
286
|
+
const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient, undefined, 'https://rpc.devnet.miden.io');
|
|
287
|
+
mockWebClient.getAccount.mockResolvedValueOnce(mockedAccount('0x' + 'a'.repeat(64), 0));
|
|
288
|
+
mockRpcGetAccountDetails.mockResolvedValueOnce({
|
|
289
|
+
commitment: () => ({
|
|
290
|
+
toHex: () => '0x' + 'b'.repeat(64),
|
|
291
|
+
}),
|
|
292
|
+
});
|
|
293
|
+
mockFetch.mockResolvedValueOnce({
|
|
294
|
+
ok: true,
|
|
295
|
+
json: async () => ({
|
|
296
|
+
account_id: multisig.accountId,
|
|
297
|
+
commitment: '0x' + 'b'.repeat(64),
|
|
298
|
+
state_json: { data: 'AQID' },
|
|
299
|
+
created_at: '2024-01-01T00:00:00Z',
|
|
300
|
+
updated_at: '2024-01-02T00:00:00Z',
|
|
301
|
+
}),
|
|
302
|
+
});
|
|
303
|
+
await multisig.syncState();
|
|
304
|
+
expect(mockWebClient.newAccount).toHaveBeenCalledTimes(1);
|
|
305
|
+
});
|
|
306
|
+
it('refreshes multisig config from synced account state', async () => {
|
|
307
|
+
const config = {
|
|
308
|
+
threshold: 1,
|
|
309
|
+
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
310
|
+
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
311
|
+
};
|
|
312
|
+
const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient, undefined, 'https://rpc.devnet.miden.io');
|
|
313
|
+
mockWebClient.getAccount.mockResolvedValueOnce({
|
|
314
|
+
commitment: () => ({
|
|
315
|
+
toHex: () => '0x' + 'b'.repeat(64),
|
|
316
|
+
}),
|
|
317
|
+
});
|
|
318
|
+
mockFetch.mockResolvedValueOnce({
|
|
319
|
+
ok: true,
|
|
320
|
+
json: async () => ({
|
|
321
|
+
account_id: multisig.accountId,
|
|
322
|
+
commitment: '0x' + 'b'.repeat(64),
|
|
323
|
+
state_json: { data: 'AQID' },
|
|
324
|
+
created_at: '2024-01-01T00:00:00Z',
|
|
325
|
+
updated_at: '2024-01-02T00:00:00Z',
|
|
326
|
+
}),
|
|
327
|
+
});
|
|
328
|
+
mockDetectConfig.mockReturnValueOnce({
|
|
329
|
+
threshold: 2,
|
|
330
|
+
numSigners: 2,
|
|
331
|
+
signerCommitments: ['0x' + '1'.repeat(64), '0x' + '2'.repeat(64)],
|
|
332
|
+
guardianEnabled: true,
|
|
333
|
+
guardianCommitment: '0x' + 'd'.repeat(64),
|
|
334
|
+
vaultBalances: [],
|
|
335
|
+
procedureThresholds: new Map(),
|
|
336
|
+
});
|
|
337
|
+
await multisig.syncState();
|
|
338
|
+
expect(multisig.threshold).toBe(2);
|
|
339
|
+
expect(multisig.signerCommitments).toEqual([
|
|
340
|
+
'0x' + '1'.repeat(64),
|
|
341
|
+
'0x' + '2'.repeat(64),
|
|
342
|
+
]);
|
|
343
|
+
expect(multisig.guardianCommitment).toBe('0x' + 'd'.repeat(64));
|
|
344
|
+
expect(mockWebClient.newAccount).not.toHaveBeenCalled();
|
|
345
|
+
});
|
|
346
|
+
it('should overwrite local state when account is not found on-chain', async () => {
|
|
347
|
+
const config = {
|
|
348
|
+
threshold: 1,
|
|
349
|
+
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
350
|
+
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
351
|
+
};
|
|
352
|
+
const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient, undefined, 'https://rpc.devnet.miden.io');
|
|
353
|
+
mockWebClient.getAccount.mockResolvedValueOnce(mockedAccount('0x' + 'a'.repeat(64), 0));
|
|
354
|
+
mockRpcGetAccountDetails.mockRejectedValueOnce(new Error('No account header record found for given ID'));
|
|
355
|
+
mockFetch.mockResolvedValueOnce({
|
|
356
|
+
ok: true,
|
|
357
|
+
json: async () => ({
|
|
358
|
+
account_id: multisig.accountId,
|
|
359
|
+
commitment: '0x' + 'b'.repeat(64),
|
|
360
|
+
state_json: { data: 'AQID' },
|
|
361
|
+
created_at: '2024-01-01T00:00:00Z',
|
|
362
|
+
updated_at: '2024-01-02T00:00:00Z',
|
|
363
|
+
}),
|
|
364
|
+
});
|
|
365
|
+
await multisig.syncState();
|
|
366
|
+
expect(mockWebClient.newAccount).toHaveBeenCalledTimes(1);
|
|
367
|
+
});
|
|
368
|
+
it('should throw when incoming commitment does not match on-chain commitment', async () => {
|
|
369
|
+
const config = {
|
|
370
|
+
threshold: 1,
|
|
371
|
+
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
372
|
+
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
373
|
+
};
|
|
374
|
+
const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient, undefined, 'https://rpc.devnet.miden.io');
|
|
375
|
+
mockWebClient.getAccount.mockResolvedValueOnce(mockedAccount('0x' + 'a'.repeat(64), 0));
|
|
376
|
+
mockAccountDeserialize.mockReturnValueOnce(mockedAccount('0x' + 'b'.repeat(64), 1));
|
|
377
|
+
mockRpcGetAccountDetails.mockResolvedValueOnce({
|
|
378
|
+
commitment: () => ({
|
|
379
|
+
toHex: () => '0x' + 'c'.repeat(64),
|
|
380
|
+
}),
|
|
381
|
+
});
|
|
382
|
+
mockFetch.mockResolvedValueOnce({
|
|
383
|
+
ok: true,
|
|
384
|
+
json: async () => ({
|
|
385
|
+
account_id: multisig.accountId,
|
|
386
|
+
commitment: '0x' + 'b'.repeat(64),
|
|
387
|
+
state_json: { data: 'AQID' },
|
|
388
|
+
created_at: '2024-01-01T00:00:00Z',
|
|
389
|
+
updated_at: '2024-01-02T00:00:00Z',
|
|
390
|
+
}),
|
|
391
|
+
});
|
|
392
|
+
await expect(multisig.syncState()).rejects.toThrow('Refusing to overwrite local state');
|
|
393
|
+
expect(mockWebClient.newAccount).not.toHaveBeenCalled();
|
|
394
|
+
});
|
|
395
|
+
it('should throw when incoming state nonce is lower than local nonce', async () => {
|
|
396
|
+
const config = {
|
|
397
|
+
threshold: 1,
|
|
398
|
+
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
399
|
+
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
400
|
+
};
|
|
401
|
+
const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient, undefined, 'https://rpc.devnet.miden.io');
|
|
402
|
+
mockWebClient.getAccount.mockResolvedValueOnce(mockedAccount('0x' + 'a'.repeat(64), 3));
|
|
403
|
+
mockAccountDeserialize.mockReturnValueOnce(mockedAccount('0x' + 'b'.repeat(64), 2));
|
|
404
|
+
mockFetch.mockResolvedValueOnce({
|
|
405
|
+
ok: true,
|
|
406
|
+
json: async () => ({
|
|
407
|
+
account_id: multisig.accountId,
|
|
408
|
+
commitment: '0x' + 'b'.repeat(64),
|
|
409
|
+
state_json: { data: 'AQID' },
|
|
410
|
+
created_at: '2024-01-01T00:00:00Z',
|
|
411
|
+
updated_at: '2024-01-02T00:00:00Z',
|
|
412
|
+
}),
|
|
413
|
+
});
|
|
414
|
+
await expect(multisig.syncState()).rejects.toThrow('incoming nonce 2 is not greater than local nonce 3');
|
|
415
|
+
expect(mockWebClient.newAccount).not.toHaveBeenCalled();
|
|
416
|
+
});
|
|
417
|
+
it('should throw when incoming state nonce equals local nonce but commitment differs', async () => {
|
|
418
|
+
const config = {
|
|
419
|
+
threshold: 1,
|
|
420
|
+
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
421
|
+
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
422
|
+
};
|
|
423
|
+
const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient, undefined, 'https://rpc.devnet.miden.io');
|
|
424
|
+
mockWebClient.getAccount.mockResolvedValueOnce(mockedAccount('0x' + 'a'.repeat(64), 2));
|
|
425
|
+
mockAccountDeserialize.mockReturnValueOnce(mockedAccount('0x' + 'b'.repeat(64), 2));
|
|
426
|
+
mockFetch.mockResolvedValueOnce({
|
|
427
|
+
ok: true,
|
|
428
|
+
json: async () => ({
|
|
429
|
+
account_id: multisig.accountId,
|
|
430
|
+
commitment: '0x' + 'b'.repeat(64),
|
|
431
|
+
state_json: { data: 'AQID' },
|
|
432
|
+
created_at: '2024-01-01T00:00:00Z',
|
|
433
|
+
updated_at: '2024-01-02T00:00:00Z',
|
|
434
|
+
}),
|
|
435
|
+
});
|
|
436
|
+
await expect(multisig.syncState()).rejects.toThrow('incoming nonce 2 is not greater than local nonce 2');
|
|
437
|
+
expect(mockWebClient.newAccount).not.toHaveBeenCalled();
|
|
438
|
+
});
|
|
439
|
+
});
|
|
440
|
+
describe('verifyStateCommitment', () => {
|
|
441
|
+
it('should pass when local and on-chain commitments match', async () => {
|
|
442
|
+
const config = {
|
|
443
|
+
threshold: 1,
|
|
444
|
+
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
445
|
+
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
446
|
+
};
|
|
447
|
+
mockWebClient.getAccount.mockResolvedValueOnce({
|
|
448
|
+
commitment: () => ({
|
|
449
|
+
toHex: () => '0x' + 'b'.repeat(64),
|
|
450
|
+
}),
|
|
451
|
+
});
|
|
452
|
+
const multisigWithRpc = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient, undefined, 'https://rpc.devnet.miden.io');
|
|
453
|
+
await expect(multisigWithRpc.verifyStateCommitment()).resolves.toMatchObject({
|
|
454
|
+
accountId: multisigWithRpc.accountId,
|
|
455
|
+
});
|
|
456
|
+
});
|
|
457
|
+
it('should throw when local account state is missing', async () => {
|
|
458
|
+
const config = {
|
|
459
|
+
threshold: 1,
|
|
460
|
+
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
461
|
+
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
462
|
+
};
|
|
463
|
+
mockWebClient.getAccount.mockResolvedValueOnce(null);
|
|
464
|
+
const multisigWithRpc = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient, undefined, 'https://rpc.devnet.miden.io');
|
|
465
|
+
await expect(multisigWithRpc.verifyStateCommitment()).rejects.toThrow('Local account state not found');
|
|
466
|
+
});
|
|
467
|
+
it('should throw when local and on-chain commitments differ', async () => {
|
|
468
|
+
const config = {
|
|
469
|
+
threshold: 1,
|
|
470
|
+
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
471
|
+
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
472
|
+
};
|
|
473
|
+
mockWebClient.getAccount.mockResolvedValueOnce({
|
|
474
|
+
commitment: () => ({
|
|
475
|
+
toHex: () => '0x' + 'f'.repeat(64),
|
|
476
|
+
}),
|
|
477
|
+
});
|
|
478
|
+
mockRpcGetAccountDetails.mockResolvedValueOnce({
|
|
479
|
+
commitment: () => ({
|
|
480
|
+
toHex: () => '0x' + 'b'.repeat(64),
|
|
481
|
+
}),
|
|
482
|
+
});
|
|
483
|
+
const multisigWithRpc = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient, undefined, 'https://rpc.devnet.miden.io');
|
|
484
|
+
await expect(multisigWithRpc.verifyStateCommitment()).rejects.toThrow('Local account commitment does not match on-chain commitment');
|
|
485
|
+
});
|
|
486
|
+
});
|
|
487
|
+
describe('registerOnGuardian', () => {
|
|
488
|
+
it('should register account on GUARDIAN', async () => {
|
|
199
489
|
const config = {
|
|
200
490
|
threshold: 1,
|
|
201
491
|
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
202
|
-
|
|
492
|
+
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
203
493
|
};
|
|
204
|
-
const multisig = new Multisig(mockAccount, config,
|
|
494
|
+
const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
|
|
205
495
|
mockFetch.mockResolvedValueOnce({
|
|
206
496
|
ok: true,
|
|
207
497
|
json: async () => ({
|
|
@@ -210,24 +500,45 @@ describe('Multisig', () => {
|
|
|
210
500
|
ack_pubkey: '0x' + 'd'.repeat(64),
|
|
211
501
|
}),
|
|
212
502
|
});
|
|
213
|
-
await expect(multisig.
|
|
503
|
+
await expect(multisig.registerOnGuardian()).resolves.toBeUndefined();
|
|
214
504
|
});
|
|
215
|
-
it('should
|
|
505
|
+
it('should register ECDSA accounts with MidenEcdsa auth', async () => {
|
|
216
506
|
const config = {
|
|
217
507
|
threshold: 1,
|
|
218
508
|
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
219
|
-
|
|
509
|
+
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
220
510
|
};
|
|
221
|
-
const
|
|
222
|
-
|
|
511
|
+
const ecdsaSigner = {
|
|
512
|
+
...mockSigner,
|
|
513
|
+
publicKey: '0x' + '2'.repeat(66),
|
|
514
|
+
scheme: 'ecdsa',
|
|
515
|
+
};
|
|
516
|
+
guardian.setSigner(ecdsaSigner);
|
|
517
|
+
const multisig = new Multisig(mockAccount, config, guardian, ecdsaSigner, mockWebClient);
|
|
518
|
+
mockFetch.mockResolvedValueOnce({
|
|
519
|
+
ok: true,
|
|
520
|
+
json: async () => ({
|
|
521
|
+
success: true,
|
|
522
|
+
message: 'Account configured',
|
|
523
|
+
ack_pubkey: '0x' + 'd'.repeat(66),
|
|
524
|
+
}),
|
|
525
|
+
});
|
|
526
|
+
await expect(multisig.registerOnGuardian()).resolves.toBeUndefined();
|
|
527
|
+
const [, requestInit] = mockFetch.mock.calls[0];
|
|
528
|
+
const body = JSON.parse(String(requestInit.body));
|
|
529
|
+
expect(body.auth).toEqual({
|
|
530
|
+
MidenEcdsa: {
|
|
531
|
+
cosigner_commitments: config.signerCommitments,
|
|
532
|
+
},
|
|
533
|
+
});
|
|
223
534
|
});
|
|
224
|
-
it('should accept initial state base64', async () => {
|
|
535
|
+
it('should accept explicit initial state base64', async () => {
|
|
225
536
|
const config = {
|
|
226
537
|
threshold: 1,
|
|
227
538
|
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
228
|
-
|
|
539
|
+
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
229
540
|
};
|
|
230
|
-
const multisig = new Multisig(
|
|
541
|
+
const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient, '0x' + 'e'.repeat(30));
|
|
231
542
|
mockFetch.mockResolvedValueOnce({
|
|
232
543
|
ok: true,
|
|
233
544
|
json: async () => ({
|
|
@@ -235,15 +546,15 @@ describe('Multisig', () => {
|
|
|
235
546
|
message: 'Account configured',
|
|
236
547
|
}),
|
|
237
548
|
});
|
|
238
|
-
await expect(multisig.
|
|
549
|
+
await expect(multisig.registerOnGuardian('base64initialstate')).resolves.toBeUndefined();
|
|
239
550
|
});
|
|
240
|
-
it('should throw on
|
|
551
|
+
it('should throw on GUARDIAN registration failure', async () => {
|
|
241
552
|
const config = {
|
|
242
553
|
threshold: 1,
|
|
243
554
|
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
244
|
-
|
|
555
|
+
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
245
556
|
};
|
|
246
|
-
const multisig = new Multisig(mockAccount, config,
|
|
557
|
+
const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
|
|
247
558
|
mockFetch.mockResolvedValueOnce({
|
|
248
559
|
ok: true,
|
|
249
560
|
json: async () => ({
|
|
@@ -251,17 +562,17 @@ describe('Multisig', () => {
|
|
|
251
562
|
message: 'Account already exists',
|
|
252
563
|
}),
|
|
253
564
|
});
|
|
254
|
-
await expect(multisig.
|
|
565
|
+
await expect(multisig.registerOnGuardian()).rejects.toThrow('Failed to register on GUARDIAN');
|
|
255
566
|
});
|
|
256
567
|
});
|
|
257
|
-
describe('
|
|
258
|
-
it('should sync proposals from
|
|
568
|
+
describe('syncProposals', () => {
|
|
569
|
+
it('should sync proposals from GUARDIAN', async () => {
|
|
259
570
|
const config = {
|
|
260
571
|
threshold: 2,
|
|
261
572
|
signerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
|
|
262
|
-
|
|
573
|
+
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
263
574
|
};
|
|
264
|
-
const multisig = new Multisig(mockAccount, config,
|
|
575
|
+
const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
|
|
265
576
|
const mockProposals = [
|
|
266
577
|
{
|
|
267
578
|
account_id: '0x' + 'a'.repeat(30),
|
|
@@ -283,7 +594,7 @@ describe('Multisig', () => {
|
|
|
283
594
|
proposer_id: '0x' + 'c'.repeat(64),
|
|
284
595
|
cosigner_sigs: [
|
|
285
596
|
{
|
|
286
|
-
signer_id: '0x' + '
|
|
597
|
+
signer_id: '0x' + 'a'.repeat(64),
|
|
287
598
|
signature: { scheme: 'falcon', signature: '0x' + 'e'.repeat(128) },
|
|
288
599
|
timestamp: '2024-01-01T00:00:00Z',
|
|
289
600
|
},
|
|
@@ -295,19 +606,18 @@ describe('Multisig', () => {
|
|
|
295
606
|
ok: true,
|
|
296
607
|
json: async () => ({ proposals: mockProposals }),
|
|
297
608
|
});
|
|
298
|
-
const proposals = await multisig.
|
|
609
|
+
const proposals = await multisig.syncProposals();
|
|
299
610
|
expect(proposals.length).toBe(1);
|
|
300
611
|
expect(proposals[0].nonce).toBe(1);
|
|
301
|
-
expect(proposals[0].status
|
|
302
|
-
expect(proposals[0].commitment).toBe(proposals[0].id);
|
|
612
|
+
expect(proposals[0].status).toBe('pending');
|
|
303
613
|
});
|
|
304
614
|
it('should return ready status when enough signatures', async () => {
|
|
305
615
|
const config = {
|
|
306
616
|
threshold: 1, // Only 1 signature needed
|
|
307
617
|
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
308
|
-
|
|
618
|
+
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
309
619
|
};
|
|
310
|
-
const multisig = new Multisig(mockAccount, config,
|
|
620
|
+
const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
|
|
311
621
|
const mockProposals = [
|
|
312
622
|
{
|
|
313
623
|
account_id: '0x' + 'a'.repeat(30),
|
|
@@ -329,7 +639,7 @@ describe('Multisig', () => {
|
|
|
329
639
|
proposer_id: '0x' + 'c'.repeat(64),
|
|
330
640
|
cosigner_sigs: [
|
|
331
641
|
{
|
|
332
|
-
signer_id: '0x' + '
|
|
642
|
+
signer_id: '0x' + 'a'.repeat(64),
|
|
333
643
|
signature: { scheme: 'falcon', signature: '0x' + 'e'.repeat(128) },
|
|
334
644
|
timestamp: '2024-01-01T00:00:00Z',
|
|
335
645
|
},
|
|
@@ -341,99 +651,571 @@ describe('Multisig', () => {
|
|
|
341
651
|
ok: true,
|
|
342
652
|
json: async () => ({ proposals: mockProposals }),
|
|
343
653
|
});
|
|
344
|
-
const proposals = await multisig.
|
|
345
|
-
expect(proposals[0].status
|
|
346
|
-
});
|
|
347
|
-
});
|
|
348
|
-
describe('listTransactionProposals', () => {
|
|
349
|
-
it('should return empty list initially', () => {
|
|
350
|
-
const config = {
|
|
351
|
-
threshold: 1,
|
|
352
|
-
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
353
|
-
psmCommitment: '0x' + 'c'.repeat(64),
|
|
354
|
-
};
|
|
355
|
-
const multisig = new Multisig(mockAccount, config, psm, mockSigner, mockWebClient);
|
|
356
|
-
expect(multisig.listTransactionProposals()).toEqual([]);
|
|
654
|
+
const proposals = await multisig.syncProposals();
|
|
655
|
+
expect(proposals[0].status).toBe('ready');
|
|
357
656
|
});
|
|
358
|
-
|
|
359
|
-
describe('createProposal', () => {
|
|
360
|
-
it('should create a new proposal', async () => {
|
|
657
|
+
it('should reject proposals whose metadata does not match tx_summary', async () => {
|
|
361
658
|
const config = {
|
|
362
659
|
threshold: 1,
|
|
363
660
|
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
364
|
-
|
|
365
|
-
};
|
|
366
|
-
const multisig = new Multisig(mockAccount, config, psm, mockSigner, mockWebClient);
|
|
367
|
-
const mockDelta = {
|
|
368
|
-
account_id: '0x' + 'a'.repeat(30),
|
|
369
|
-
nonce: 1,
|
|
370
|
-
prev_commitment: '0x' + 'b'.repeat(64),
|
|
371
|
-
delta_payload: {
|
|
372
|
-
tx_summary: { data: 'base64summary' },
|
|
373
|
-
signatures: [],
|
|
374
|
-
},
|
|
375
|
-
status: {
|
|
376
|
-
status: 'pending',
|
|
377
|
-
timestamp: '2024-01-01T00:00:00Z',
|
|
378
|
-
proposer_id: '0x' + 'c'.repeat(64),
|
|
379
|
-
cosigner_sigs: [],
|
|
380
|
-
},
|
|
661
|
+
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
381
662
|
};
|
|
663
|
+
const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
|
|
382
664
|
mockFetch.mockResolvedValueOnce({
|
|
383
665
|
ok: true,
|
|
384
666
|
json: async () => ({
|
|
385
|
-
|
|
386
|
-
|
|
667
|
+
proposals: [
|
|
668
|
+
{
|
|
669
|
+
account_id: '0x' + 'a'.repeat(30),
|
|
670
|
+
nonce: 1,
|
|
671
|
+
prev_commitment: '0x' + 'b'.repeat(64),
|
|
672
|
+
delta_payload: {
|
|
673
|
+
tx_summary: { data: 'AQID' },
|
|
674
|
+
signatures: [],
|
|
675
|
+
metadata: {
|
|
676
|
+
proposal_type: 'add_signer',
|
|
677
|
+
target_threshold: 1,
|
|
678
|
+
signer_commitments: ['0x' + 'a'.repeat(64)],
|
|
679
|
+
description: '',
|
|
680
|
+
},
|
|
681
|
+
},
|
|
682
|
+
status: {
|
|
683
|
+
status: 'pending',
|
|
684
|
+
timestamp: '2024-01-01T00:00:00Z',
|
|
685
|
+
proposer_id: '0x' + 'c'.repeat(64),
|
|
686
|
+
cosigner_sigs: [],
|
|
687
|
+
},
|
|
688
|
+
},
|
|
689
|
+
],
|
|
387
690
|
}),
|
|
388
691
|
});
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
description: '',
|
|
692
|
+
vi.mocked(executeForSummary).mockResolvedValueOnce({
|
|
693
|
+
toCommitment: () => ({
|
|
694
|
+
toHex: () => '0x' + 'f'.repeat(64),
|
|
695
|
+
}),
|
|
394
696
|
});
|
|
395
|
-
expect(
|
|
396
|
-
expect(proposal.id).toBe('0x' + 'd'.repeat(64));
|
|
697
|
+
await expect(multisig.syncProposals()).rejects.toThrow('Invalid proposal: metadata does not match tx_summary');
|
|
397
698
|
});
|
|
398
|
-
|
|
399
|
-
describe('signTransactionProposal', () => {
|
|
400
|
-
it('should sign a proposal', async () => {
|
|
699
|
+
it('should reject non-32-byte signer IDs from GUARDIAN proposals', async () => {
|
|
401
700
|
const config = {
|
|
402
701
|
threshold: 1,
|
|
403
702
|
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
404
|
-
|
|
703
|
+
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
405
704
|
};
|
|
406
|
-
const multisig = new Multisig(mockAccount, config,
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
705
|
+
const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
|
|
706
|
+
const mockProposals = [
|
|
707
|
+
{
|
|
708
|
+
account_id: '0x' + 'a'.repeat(30),
|
|
709
|
+
nonce: 1,
|
|
710
|
+
prev_commitment: '0x' + 'b'.repeat(64),
|
|
711
|
+
delta_payload: {
|
|
712
|
+
tx_summary: { data: 'AQID' },
|
|
713
|
+
signatures: [],
|
|
714
|
+
metadata: {
|
|
715
|
+
proposal_type: 'add_signer',
|
|
716
|
+
target_threshold: 1,
|
|
717
|
+
signer_commitments: ['0x' + 'a'.repeat(64)],
|
|
718
|
+
description: '',
|
|
719
|
+
},
|
|
720
|
+
},
|
|
721
|
+
status: {
|
|
722
|
+
status: 'pending',
|
|
723
|
+
timestamp: '2024-01-01T00:00:00Z',
|
|
724
|
+
proposer_id: '0x' + 'c'.repeat(64),
|
|
725
|
+
cosigner_sigs: [
|
|
726
|
+
{
|
|
727
|
+
signer_id: '0x1',
|
|
728
|
+
signature: { scheme: 'falcon', signature: '0x' + 'e'.repeat(128) },
|
|
729
|
+
timestamp: '2024-01-01T00:00:00Z',
|
|
730
|
+
},
|
|
731
|
+
],
|
|
732
|
+
},
|
|
421
733
|
},
|
|
422
|
-
|
|
734
|
+
];
|
|
423
735
|
mockFetch.mockResolvedValueOnce({
|
|
424
736
|
ok: true,
|
|
425
|
-
json: async () => ({
|
|
426
|
-
delta: mockDelta,
|
|
427
|
-
commitment: '0x' + 'd'.repeat(64),
|
|
428
|
-
}),
|
|
737
|
+
json: async () => ({ proposals: mockProposals }),
|
|
429
738
|
});
|
|
430
|
-
await multisig.
|
|
431
|
-
|
|
739
|
+
await expect(multisig.syncProposals()).rejects.toThrow('expected signerId as 32-byte hex');
|
|
740
|
+
});
|
|
741
|
+
it('should reject duplicate normalized signer IDs from GUARDIAN proposals', async () => {
|
|
742
|
+
const config = {
|
|
743
|
+
threshold: 2,
|
|
744
|
+
signerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
|
|
745
|
+
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
746
|
+
};
|
|
747
|
+
const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
|
|
748
|
+
const mockProposals = [
|
|
749
|
+
{
|
|
750
|
+
account_id: '0x' + 'a'.repeat(30),
|
|
751
|
+
nonce: 1,
|
|
752
|
+
prev_commitment: '0x' + 'b'.repeat(64),
|
|
753
|
+
delta_payload: {
|
|
754
|
+
tx_summary: { data: 'AQID' },
|
|
755
|
+
signatures: [],
|
|
756
|
+
metadata: {
|
|
757
|
+
proposal_type: 'add_signer',
|
|
758
|
+
target_threshold: 2,
|
|
759
|
+
signer_commitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
|
|
760
|
+
description: '',
|
|
761
|
+
},
|
|
762
|
+
},
|
|
763
|
+
status: {
|
|
764
|
+
status: 'pending',
|
|
765
|
+
timestamp: '2024-01-01T00:00:00Z',
|
|
766
|
+
proposer_id: '0x' + 'c'.repeat(64),
|
|
767
|
+
cosigner_sigs: [
|
|
768
|
+
{
|
|
769
|
+
signer_id: '0x' + 'A'.repeat(64),
|
|
770
|
+
signature: { scheme: 'falcon', signature: '0x' + 'e'.repeat(128) },
|
|
771
|
+
timestamp: '2024-01-01T00:00:00Z',
|
|
772
|
+
},
|
|
773
|
+
{
|
|
774
|
+
signer_id: '0x' + 'a'.repeat(64),
|
|
775
|
+
signature: { scheme: 'falcon', signature: '0x' + 'f'.repeat(128) },
|
|
776
|
+
timestamp: '2024-01-01T00:00:01Z',
|
|
777
|
+
},
|
|
778
|
+
],
|
|
779
|
+
},
|
|
780
|
+
},
|
|
781
|
+
];
|
|
782
|
+
mockFetch.mockResolvedValueOnce({
|
|
783
|
+
ok: true,
|
|
784
|
+
json: async () => ({ proposals: mockProposals }),
|
|
785
|
+
});
|
|
786
|
+
await expect(multisig.syncProposals()).rejects.toThrow('duplicate signatures for signer');
|
|
787
|
+
});
|
|
788
|
+
});
|
|
789
|
+
describe('listProposals', () => {
|
|
790
|
+
it('should return empty list initially', () => {
|
|
791
|
+
const config = {
|
|
792
|
+
threshold: 1,
|
|
793
|
+
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
794
|
+
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
795
|
+
};
|
|
796
|
+
const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
|
|
797
|
+
expect(multisig.listProposals()).toEqual([]);
|
|
798
|
+
});
|
|
799
|
+
});
|
|
800
|
+
describe('createProposal', () => {
|
|
801
|
+
it('should create a new proposal', async () => {
|
|
802
|
+
const config = {
|
|
803
|
+
threshold: 1,
|
|
804
|
+
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
805
|
+
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
806
|
+
};
|
|
807
|
+
const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
|
|
808
|
+
const mockDelta = {
|
|
809
|
+
account_id: '0x' + 'a'.repeat(30),
|
|
810
|
+
nonce: 1,
|
|
811
|
+
prev_commitment: '0x' + 'b'.repeat(64),
|
|
812
|
+
delta_payload: {
|
|
813
|
+
tx_summary: { data: 'AQID' },
|
|
814
|
+
signatures: [],
|
|
815
|
+
},
|
|
816
|
+
status: {
|
|
817
|
+
status: 'pending',
|
|
818
|
+
timestamp: '2024-01-01T00:00:00Z',
|
|
819
|
+
proposer_id: '0x' + 'c'.repeat(64),
|
|
820
|
+
cosigner_sigs: [],
|
|
821
|
+
},
|
|
822
|
+
};
|
|
823
|
+
mockFetch.mockResolvedValueOnce({
|
|
824
|
+
ok: true,
|
|
825
|
+
json: async () => ({
|
|
826
|
+
delta: mockDelta,
|
|
827
|
+
commitment: '0x' + 'c'.repeat(64),
|
|
828
|
+
}),
|
|
829
|
+
});
|
|
830
|
+
const proposal = await multisig.createProposal(1, 'AQID', {
|
|
831
|
+
proposalType: 'add_signer',
|
|
832
|
+
targetThreshold: 1,
|
|
833
|
+
targetSignerCommitments: ['0x' + 'a'.repeat(64)],
|
|
834
|
+
description: '',
|
|
835
|
+
});
|
|
836
|
+
expect(proposal.nonce).toBe(1);
|
|
837
|
+
expect(proposal.id).toBe('0x' + 'c'.repeat(64));
|
|
838
|
+
});
|
|
839
|
+
it('should reject a mismatched returned commitment', async () => {
|
|
840
|
+
const config = {
|
|
841
|
+
threshold: 1,
|
|
842
|
+
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
843
|
+
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
844
|
+
};
|
|
845
|
+
const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
|
|
846
|
+
const mockDelta = {
|
|
847
|
+
account_id: '0x' + 'a'.repeat(30),
|
|
848
|
+
nonce: 1,
|
|
849
|
+
prev_commitment: '0x' + 'b'.repeat(64),
|
|
850
|
+
delta_payload: {
|
|
851
|
+
tx_summary: { data: 'AQID' },
|
|
852
|
+
signatures: [],
|
|
853
|
+
},
|
|
854
|
+
status: {
|
|
855
|
+
status: 'pending',
|
|
856
|
+
timestamp: '2024-01-01T00:00:00Z',
|
|
857
|
+
proposer_id: '0x' + 'c'.repeat(64),
|
|
858
|
+
cosigner_sigs: [],
|
|
859
|
+
},
|
|
860
|
+
};
|
|
861
|
+
mockFetch.mockResolvedValueOnce({
|
|
862
|
+
ok: true,
|
|
863
|
+
json: async () => ({
|
|
864
|
+
delta: mockDelta,
|
|
865
|
+
commitment: '0x' + 'd'.repeat(64),
|
|
866
|
+
}),
|
|
867
|
+
});
|
|
868
|
+
await expect(multisig.createProposal(1, 'AQID', {
|
|
869
|
+
proposalType: 'add_signer',
|
|
870
|
+
targetThreshold: 1,
|
|
871
|
+
targetSignerCommitments: ['0x' + 'a'.repeat(64)],
|
|
872
|
+
description: '',
|
|
873
|
+
})).rejects.toThrow('Invalid proposal: commitment 0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd does not match tx_summary 0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc');
|
|
874
|
+
});
|
|
875
|
+
it('should reject a response whose tx_summary does not match the provided metadata', async () => {
|
|
876
|
+
const config = {
|
|
877
|
+
threshold: 1,
|
|
878
|
+
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
879
|
+
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
880
|
+
};
|
|
881
|
+
const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
|
|
882
|
+
const mockDelta = {
|
|
883
|
+
account_id: '0x' + 'a'.repeat(30),
|
|
884
|
+
nonce: 1,
|
|
885
|
+
prev_commitment: '0x' + 'b'.repeat(64),
|
|
886
|
+
delta_payload: {
|
|
887
|
+
tx_summary: { data: 'AQID' },
|
|
888
|
+
signatures: [],
|
|
889
|
+
},
|
|
890
|
+
status: {
|
|
891
|
+
status: 'pending',
|
|
892
|
+
timestamp: '2024-01-01T00:00:00Z',
|
|
893
|
+
proposer_id: '0x' + 'c'.repeat(64),
|
|
894
|
+
cosigner_sigs: [],
|
|
895
|
+
},
|
|
896
|
+
};
|
|
897
|
+
mockFetch.mockResolvedValueOnce({
|
|
898
|
+
ok: true,
|
|
899
|
+
json: async () => ({
|
|
900
|
+
delta: mockDelta,
|
|
901
|
+
commitment: '0x' + 'c'.repeat(64),
|
|
902
|
+
}),
|
|
903
|
+
});
|
|
904
|
+
vi.mocked(executeForSummary).mockResolvedValueOnce({
|
|
905
|
+
toCommitment: () => ({
|
|
906
|
+
toHex: () => '0x' + 'f'.repeat(64),
|
|
907
|
+
}),
|
|
908
|
+
});
|
|
909
|
+
await expect(multisig.createProposal(1, 'AQID', {
|
|
910
|
+
proposalType: 'add_signer',
|
|
911
|
+
targetThreshold: 1,
|
|
912
|
+
targetSignerCommitments: ['0x' + 'a'.repeat(64)],
|
|
913
|
+
description: '',
|
|
914
|
+
})).rejects.toThrow('Invalid proposal: metadata does not match tx_summary');
|
|
915
|
+
});
|
|
916
|
+
});
|
|
917
|
+
describe('createP2idProposal', () => {
|
|
918
|
+
it('should include the faucet asset in the proposal description', async () => {
|
|
919
|
+
const { executeForSummary } = await import('./transaction.js');
|
|
920
|
+
vi.mocked(executeForSummary).mockResolvedValue({
|
|
921
|
+
toCommitment: () => ({
|
|
922
|
+
toHex: () => '0x' + 'c'.repeat(64),
|
|
923
|
+
}),
|
|
924
|
+
serialize: () => new Uint8Array([1, 2, 3]),
|
|
925
|
+
});
|
|
926
|
+
const config = {
|
|
927
|
+
threshold: 1,
|
|
928
|
+
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
929
|
+
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
930
|
+
};
|
|
931
|
+
const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
|
|
932
|
+
const mockDelta = {
|
|
933
|
+
account_id: '0x' + 'a'.repeat(30),
|
|
934
|
+
nonce: 1,
|
|
935
|
+
prev_commitment: '0x' + 'b'.repeat(64),
|
|
936
|
+
delta_payload: {
|
|
937
|
+
tx_summary: { data: 'AQID' },
|
|
938
|
+
signatures: [],
|
|
939
|
+
metadata: {
|
|
940
|
+
proposal_type: 'p2id',
|
|
941
|
+
recipient_id: '0xrecipient',
|
|
942
|
+
faucet_id: '0xfaucet',
|
|
943
|
+
amount: '100',
|
|
944
|
+
description: '',
|
|
945
|
+
},
|
|
946
|
+
},
|
|
947
|
+
status: {
|
|
948
|
+
status: 'pending',
|
|
949
|
+
timestamp: '2024-01-01T00:00:00Z',
|
|
950
|
+
proposer_id: '0x' + 'c'.repeat(64),
|
|
951
|
+
cosigner_sigs: [],
|
|
952
|
+
},
|
|
953
|
+
};
|
|
954
|
+
mockFetch.mockResolvedValueOnce({
|
|
955
|
+
ok: true,
|
|
956
|
+
json: async () => ({
|
|
957
|
+
delta: mockDelta,
|
|
958
|
+
commitment: '0x' + 'c'.repeat(64),
|
|
959
|
+
}),
|
|
960
|
+
});
|
|
961
|
+
const proposal = await multisig.createP2idProposal('0xrecipient', '0xfaucet', 100n, 1);
|
|
962
|
+
expect(proposal.metadata.description).toBe('Send 100 of asset 0xfaucet... to 0xrecipien...');
|
|
963
|
+
});
|
|
964
|
+
});
|
|
965
|
+
describe('createChangeThresholdProposal', () => {
|
|
966
|
+
it('passes the signer scheme to update-signers requests', async () => {
|
|
967
|
+
vi.mocked(executeForSummary).mockResolvedValue({
|
|
968
|
+
toCommitment: () => ({
|
|
969
|
+
toHex: () => '0x' + 'c'.repeat(64),
|
|
970
|
+
}),
|
|
971
|
+
serialize: () => new Uint8Array([1, 2, 3]),
|
|
972
|
+
});
|
|
973
|
+
const ecdsaSigner = {
|
|
974
|
+
...mockSigner,
|
|
975
|
+
publicKey: '0x' + '2'.repeat(66),
|
|
976
|
+
scheme: 'ecdsa',
|
|
977
|
+
};
|
|
978
|
+
guardian.setSigner(ecdsaSigner);
|
|
979
|
+
const config = {
|
|
980
|
+
threshold: 1,
|
|
981
|
+
signerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
|
|
982
|
+
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
983
|
+
};
|
|
984
|
+
const mockDelta = {
|
|
985
|
+
account_id: '0x' + 'a'.repeat(30),
|
|
986
|
+
nonce: 1,
|
|
987
|
+
prev_commitment: '0x' + 'b'.repeat(64),
|
|
988
|
+
delta_payload: {
|
|
989
|
+
tx_summary: { data: 'AQID' },
|
|
990
|
+
signatures: [],
|
|
991
|
+
metadata: {
|
|
992
|
+
proposal_type: 'change_threshold',
|
|
993
|
+
target_threshold: 2,
|
|
994
|
+
description: '',
|
|
995
|
+
},
|
|
996
|
+
},
|
|
997
|
+
status: {
|
|
998
|
+
status: 'pending',
|
|
999
|
+
timestamp: '2024-01-01T00:00:00Z',
|
|
1000
|
+
proposer_id: '0x' + 'c'.repeat(64),
|
|
1001
|
+
cosigner_sigs: [],
|
|
1002
|
+
},
|
|
1003
|
+
};
|
|
1004
|
+
mockFetch.mockResolvedValueOnce({
|
|
1005
|
+
ok: true,
|
|
1006
|
+
json: async () => ({
|
|
1007
|
+
delta: mockDelta,
|
|
1008
|
+
commitment: '0x' + 'c'.repeat(64),
|
|
1009
|
+
}),
|
|
1010
|
+
});
|
|
1011
|
+
const multisig = new Multisig(mockAccount, config, guardian, ecdsaSigner, mockWebClient);
|
|
1012
|
+
await multisig.createChangeThresholdProposal(2, 1);
|
|
1013
|
+
expect(buildUpdateSignersTransactionRequest).toHaveBeenCalledWith(mockWebClient, 2, config.signerCommitments, { signatureScheme: 'ecdsa' });
|
|
1014
|
+
});
|
|
1015
|
+
});
|
|
1016
|
+
describe('createSwitchGuardianProposal', () => {
|
|
1017
|
+
it('should verify new endpoint commitment before creating proposal', async () => {
|
|
1018
|
+
vi.mocked(executeForSummary).mockResolvedValue({
|
|
1019
|
+
serialize: () => new Uint8Array([1, 2, 3]),
|
|
1020
|
+
});
|
|
1021
|
+
const config = {
|
|
1022
|
+
threshold: 1,
|
|
1023
|
+
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
1024
|
+
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
1025
|
+
};
|
|
1026
|
+
const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
|
|
1027
|
+
const newGuardianPubkey = '0x' + '1'.repeat(64);
|
|
1028
|
+
mockFetch.mockResolvedValueOnce({
|
|
1029
|
+
ok: true,
|
|
1030
|
+
json: async () => ({ commitment: newGuardianPubkey }),
|
|
1031
|
+
});
|
|
1032
|
+
const proposal = await multisig.createSwitchGuardianProposal('http://new-guardian.com', newGuardianPubkey);
|
|
1033
|
+
expect(proposal.metadata?.proposalType).toBe('switch_guardian');
|
|
1034
|
+
if (proposal.metadata?.proposalType === 'switch_guardian') {
|
|
1035
|
+
expect(proposal.metadata.newGuardianEndpoint).toBe('http://new-guardian.com');
|
|
1036
|
+
}
|
|
1037
|
+
expect(mockFetch).toHaveBeenCalledWith('http://new-guardian.com/pubkey?scheme=falcon', expect.objectContaining({ method: 'GET' }));
|
|
1038
|
+
});
|
|
1039
|
+
it('should reject switch proposal when endpoint commitment does not match', async () => {
|
|
1040
|
+
vi.mocked(executeForSummary).mockResolvedValue({
|
|
1041
|
+
serialize: () => new Uint8Array([1, 2, 3]),
|
|
1042
|
+
});
|
|
1043
|
+
const config = {
|
|
1044
|
+
threshold: 1,
|
|
1045
|
+
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
1046
|
+
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
1047
|
+
};
|
|
1048
|
+
const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
|
|
1049
|
+
mockFetch.mockResolvedValueOnce({
|
|
1050
|
+
ok: true,
|
|
1051
|
+
json: async () => ({ commitment: '0x' + '2'.repeat(64) }),
|
|
1052
|
+
});
|
|
1053
|
+
await expect(multisig.createSwitchGuardianProposal('http://new-guardian.com', '0x' + '1'.repeat(64))).rejects.toThrow('Refusing to use GUARDIAN endpoint');
|
|
1054
|
+
});
|
|
1055
|
+
it('should use the signer scheme when resolving new GUARDIAN commitments', async () => {
|
|
1056
|
+
vi.mocked(executeForSummary).mockResolvedValue({
|
|
1057
|
+
serialize: () => new Uint8Array([1, 2, 3]),
|
|
1058
|
+
});
|
|
1059
|
+
const ecdsaSigner = {
|
|
1060
|
+
...mockSigner,
|
|
1061
|
+
publicKey: '0x' + '2'.repeat(66),
|
|
1062
|
+
scheme: 'ecdsa',
|
|
1063
|
+
};
|
|
1064
|
+
guardian.setSigner(ecdsaSigner);
|
|
1065
|
+
const config = {
|
|
1066
|
+
threshold: 1,
|
|
1067
|
+
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
1068
|
+
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
1069
|
+
};
|
|
1070
|
+
const multisig = new Multisig(mockAccount, config, guardian, ecdsaSigner, mockWebClient);
|
|
1071
|
+
const newGuardianCommitment = '0x' + '1'.repeat(64);
|
|
1072
|
+
mockFetch.mockResolvedValueOnce({
|
|
1073
|
+
ok: true,
|
|
1074
|
+
json: async () => ({ commitment: newGuardianCommitment }),
|
|
1075
|
+
});
|
|
1076
|
+
await multisig.createSwitchGuardianProposal('http://new-guardian.com', newGuardianCommitment);
|
|
1077
|
+
expect(mockFetch).toHaveBeenCalledWith('http://new-guardian.com/pubkey?scheme=ecdsa', expect.objectContaining({ method: 'GET' }));
|
|
1078
|
+
expect(buildUpdateGuardianTransactionRequest).toHaveBeenCalledWith(mockWebClient, newGuardianCommitment, { signatureScheme: 'ecdsa' });
|
|
1079
|
+
});
|
|
1080
|
+
});
|
|
1081
|
+
describe('createUpdateProcedureThresholdProposal', () => {
|
|
1082
|
+
it('should create procedure-threshold update proposals', async () => {
|
|
1083
|
+
vi.mocked(executeForSummary).mockResolvedValue({
|
|
1084
|
+
toCommitment: () => ({
|
|
1085
|
+
toHex: () => '0x' + 'c'.repeat(64),
|
|
1086
|
+
}),
|
|
1087
|
+
serialize: () => new Uint8Array([1, 2, 3]),
|
|
1088
|
+
});
|
|
1089
|
+
const config = {
|
|
1090
|
+
threshold: 2,
|
|
1091
|
+
signerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
|
|
1092
|
+
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
1093
|
+
};
|
|
1094
|
+
const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
|
|
1095
|
+
const mockDelta = {
|
|
1096
|
+
account_id: '0x' + 'a'.repeat(30),
|
|
1097
|
+
nonce: 1,
|
|
1098
|
+
prev_commitment: '0x' + 'b'.repeat(64),
|
|
1099
|
+
delta_payload: {
|
|
1100
|
+
tx_summary: { data: 'AQID' },
|
|
1101
|
+
signatures: [],
|
|
1102
|
+
metadata: {
|
|
1103
|
+
proposal_type: 'update_procedure_threshold',
|
|
1104
|
+
target_threshold: 1,
|
|
1105
|
+
target_procedure: 'send_asset',
|
|
1106
|
+
description: '',
|
|
1107
|
+
},
|
|
1108
|
+
},
|
|
1109
|
+
status: {
|
|
1110
|
+
status: 'pending',
|
|
1111
|
+
timestamp: '2024-01-01T00:00:00Z',
|
|
1112
|
+
proposer_id: '0x' + 'c'.repeat(64),
|
|
1113
|
+
cosigner_sigs: [],
|
|
1114
|
+
},
|
|
1115
|
+
};
|
|
1116
|
+
mockFetch.mockResolvedValueOnce({
|
|
1117
|
+
ok: true,
|
|
1118
|
+
json: async () => ({
|
|
1119
|
+
delta: mockDelta,
|
|
1120
|
+
commitment: '0x' + 'c'.repeat(64),
|
|
1121
|
+
}),
|
|
1122
|
+
});
|
|
1123
|
+
const proposal = await multisig.createUpdateProcedureThresholdProposal('send_asset', 1, 1);
|
|
1124
|
+
expect(buildUpdateProcedureThresholdTransactionRequest).toHaveBeenCalledWith(mockWebClient, 'send_asset', 1, { signatureScheme: 'falcon' });
|
|
1125
|
+
expect(proposal.metadata.proposalType).toBe('update_procedure_threshold');
|
|
1126
|
+
if (proposal.metadata.proposalType === 'update_procedure_threshold') {
|
|
1127
|
+
expect(proposal.metadata.targetProcedure).toBe('send_asset');
|
|
1128
|
+
expect(proposal.metadata.targetThreshold).toBe(1);
|
|
1129
|
+
}
|
|
1130
|
+
});
|
|
1131
|
+
it('passes the signer scheme to ECDSA procedure-threshold updates', async () => {
|
|
1132
|
+
vi.mocked(executeForSummary).mockResolvedValue({
|
|
1133
|
+
toCommitment: () => ({
|
|
1134
|
+
toHex: () => '0x' + 'c'.repeat(64),
|
|
1135
|
+
}),
|
|
1136
|
+
serialize: () => new Uint8Array([1, 2, 3]),
|
|
1137
|
+
});
|
|
1138
|
+
const ecdsaSigner = {
|
|
1139
|
+
...mockSigner,
|
|
1140
|
+
publicKey: '0x' + '2'.repeat(66),
|
|
1141
|
+
scheme: 'ecdsa',
|
|
1142
|
+
};
|
|
1143
|
+
guardian.setSigner(ecdsaSigner);
|
|
1144
|
+
const config = {
|
|
1145
|
+
threshold: 2,
|
|
1146
|
+
signerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
|
|
1147
|
+
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
1148
|
+
};
|
|
1149
|
+
const multisig = new Multisig(mockAccount, config, guardian, ecdsaSigner, mockWebClient);
|
|
1150
|
+
const mockDelta = {
|
|
1151
|
+
account_id: '0x' + 'a'.repeat(30),
|
|
1152
|
+
nonce: 1,
|
|
1153
|
+
prev_commitment: '0x' + 'b'.repeat(64),
|
|
1154
|
+
delta_payload: {
|
|
1155
|
+
tx_summary: { data: 'AQID' },
|
|
1156
|
+
signatures: [],
|
|
1157
|
+
metadata: {
|
|
1158
|
+
proposal_type: 'update_procedure_threshold',
|
|
1159
|
+
target_threshold: 1,
|
|
1160
|
+
target_procedure: 'send_asset',
|
|
1161
|
+
description: '',
|
|
1162
|
+
},
|
|
1163
|
+
},
|
|
1164
|
+
status: {
|
|
1165
|
+
status: 'pending',
|
|
1166
|
+
timestamp: '2024-01-01T00:00:00Z',
|
|
1167
|
+
proposer_id: '0x' + 'c'.repeat(64),
|
|
1168
|
+
cosigner_sigs: [],
|
|
1169
|
+
},
|
|
1170
|
+
};
|
|
1171
|
+
mockFetch.mockResolvedValueOnce({
|
|
1172
|
+
ok: true,
|
|
1173
|
+
json: async () => ({
|
|
1174
|
+
delta: mockDelta,
|
|
1175
|
+
commitment: '0x' + 'c'.repeat(64),
|
|
1176
|
+
}),
|
|
1177
|
+
});
|
|
1178
|
+
await multisig.createUpdateProcedureThresholdProposal('send_asset', 1, 1);
|
|
1179
|
+
expect(buildUpdateProcedureThresholdTransactionRequest).toHaveBeenCalledWith(mockWebClient, 'send_asset', 1, { signatureScheme: 'ecdsa' });
|
|
1180
|
+
});
|
|
1181
|
+
});
|
|
1182
|
+
describe('signProposal', () => {
|
|
1183
|
+
it('should sign a proposal', async () => {
|
|
1184
|
+
const config = {
|
|
1185
|
+
threshold: 1,
|
|
1186
|
+
signerCommitments: [mockSigner.commitment],
|
|
1187
|
+
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
1188
|
+
};
|
|
1189
|
+
const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
|
|
1190
|
+
// First create a proposal
|
|
1191
|
+
const mockDelta = {
|
|
1192
|
+
account_id: '0x' + 'a'.repeat(30),
|
|
1193
|
+
nonce: 1,
|
|
1194
|
+
prev_commitment: '0x' + 'b'.repeat(64),
|
|
1195
|
+
delta_payload: {
|
|
1196
|
+
tx_summary: { data: 'AQID' },
|
|
1197
|
+
signatures: [],
|
|
1198
|
+
},
|
|
1199
|
+
status: {
|
|
1200
|
+
status: 'pending',
|
|
1201
|
+
timestamp: '2024-01-01T00:00:00Z',
|
|
1202
|
+
proposer_id: '0x' + 'c'.repeat(64),
|
|
1203
|
+
cosigner_sigs: [],
|
|
1204
|
+
},
|
|
1205
|
+
};
|
|
1206
|
+
mockFetch.mockResolvedValueOnce({
|
|
1207
|
+
ok: true,
|
|
1208
|
+
json: async () => ({
|
|
1209
|
+
delta: mockDelta,
|
|
1210
|
+
commitment: '0x' + 'c'.repeat(64),
|
|
1211
|
+
}),
|
|
1212
|
+
});
|
|
1213
|
+
await multisig.createProposal(1, 'AQID', {
|
|
1214
|
+
proposalType: 'add_signer',
|
|
432
1215
|
targetThreshold: 1,
|
|
433
1216
|
targetSignerCommitments: ['0x' + 'a'.repeat(64)],
|
|
434
1217
|
description: '',
|
|
435
1218
|
});
|
|
436
|
-
// Now sign it
|
|
437
1219
|
const signedDelta = {
|
|
438
1220
|
...mockDelta,
|
|
439
1221
|
status: {
|
|
@@ -458,33 +1240,173 @@ describe('Multisig', () => {
|
|
|
458
1240
|
},
|
|
459
1241
|
},
|
|
460
1242
|
};
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
1243
|
+
mockFetch.mockResolvedValueOnce({
|
|
1244
|
+
ok: true,
|
|
1245
|
+
json: async () => signedDelta,
|
|
1246
|
+
});
|
|
1247
|
+
const proposalId = '0x' + 'c'.repeat(64);
|
|
1248
|
+
const signedProposal = await multisig.signProposal(proposalId);
|
|
1249
|
+
expect(mockSigner.signCommitment).toHaveBeenCalledWith(proposalId);
|
|
1250
|
+
expect(signedProposal.signatures.length).toBe(1);
|
|
1251
|
+
});
|
|
1252
|
+
it('should reject signing when metadata does not match tx_summary', async () => {
|
|
1253
|
+
const config = {
|
|
1254
|
+
threshold: 1,
|
|
1255
|
+
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
1256
|
+
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
1257
|
+
};
|
|
1258
|
+
const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
|
|
1259
|
+
const mockDelta = {
|
|
1260
|
+
account_id: '0x' + 'a'.repeat(30),
|
|
1261
|
+
nonce: 1,
|
|
1262
|
+
prev_commitment: '0x' + 'b'.repeat(64),
|
|
1263
|
+
delta_payload: {
|
|
1264
|
+
tx_summary: { data: 'AQID' },
|
|
1265
|
+
signatures: [],
|
|
1266
|
+
},
|
|
1267
|
+
status: {
|
|
1268
|
+
status: 'pending',
|
|
1269
|
+
timestamp: '2024-01-01T00:00:00Z',
|
|
1270
|
+
proposer_id: '0x' + 'c'.repeat(64),
|
|
1271
|
+
cosigner_sigs: [],
|
|
1272
|
+
},
|
|
1273
|
+
};
|
|
1274
|
+
mockFetch.mockResolvedValueOnce({
|
|
1275
|
+
ok: true,
|
|
1276
|
+
json: async () => ({
|
|
1277
|
+
delta: mockDelta,
|
|
1278
|
+
commitment: '0x' + 'c'.repeat(64),
|
|
1279
|
+
}),
|
|
1280
|
+
});
|
|
1281
|
+
await multisig.createProposal(1, 'AQID', {
|
|
1282
|
+
proposalType: 'add_signer',
|
|
1283
|
+
targetThreshold: 1,
|
|
1284
|
+
targetSignerCommitments: ['0x' + 'a'.repeat(64)],
|
|
1285
|
+
description: '',
|
|
1286
|
+
});
|
|
1287
|
+
vi.mocked(executeForSummary).mockResolvedValueOnce({
|
|
1288
|
+
toCommitment: () => ({
|
|
1289
|
+
toHex: () => '0x' + 'f'.repeat(64),
|
|
1290
|
+
}),
|
|
1291
|
+
});
|
|
1292
|
+
await expect(multisig.signProposal('0x' + 'c'.repeat(64))).rejects.toThrow('Invalid proposal: metadata does not match tx_summary');
|
|
1293
|
+
});
|
|
1294
|
+
it('should reject proposals for a different account before signing', async () => {
|
|
1295
|
+
const config = {
|
|
1296
|
+
threshold: 1,
|
|
1297
|
+
signerCommitments: [mockSigner.commitment],
|
|
1298
|
+
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
1299
|
+
};
|
|
1300
|
+
const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
|
|
1301
|
+
const proposalId = '0x' + 'd'.repeat(64);
|
|
1302
|
+
mockFetch.mockResolvedValueOnce({
|
|
1303
|
+
ok: true,
|
|
1304
|
+
json: async () => ({
|
|
1305
|
+
proposals: [
|
|
1306
|
+
{
|
|
1307
|
+
account_id: '0x' + 'f'.repeat(30),
|
|
1308
|
+
nonce: 1,
|
|
1309
|
+
prev_commitment: '0x' + 'b'.repeat(64),
|
|
1310
|
+
delta_payload: {
|
|
1311
|
+
tx_summary: { data: 'AQID' },
|
|
1312
|
+
signatures: [],
|
|
1313
|
+
metadata: {
|
|
1314
|
+
proposal_type: 'add_signer',
|
|
1315
|
+
description: '',
|
|
1316
|
+
target_threshold: 1,
|
|
1317
|
+
signer_commitments: [mockSigner.commitment],
|
|
1318
|
+
},
|
|
1319
|
+
},
|
|
1320
|
+
status: {
|
|
1321
|
+
status: 'pending',
|
|
1322
|
+
timestamp: '2024-01-01T00:00:00Z',
|
|
1323
|
+
proposer_id: '0x' + 'c'.repeat(64),
|
|
1324
|
+
cosigner_sigs: [],
|
|
1325
|
+
},
|
|
1326
|
+
},
|
|
1327
|
+
],
|
|
1328
|
+
}),
|
|
1329
|
+
});
|
|
1330
|
+
await expect(multisig.signProposal(proposalId)).rejects.toThrow('Proposal is for a different account: 0x' + 'f'.repeat(30));
|
|
1331
|
+
expect(mockSigner.signCommitment).not.toHaveBeenCalled();
|
|
1332
|
+
});
|
|
1333
|
+
});
|
|
1334
|
+
describe('importProposal', () => {
|
|
1335
|
+
it('should reject imported proposals whose metadata does not match tx_summary', async () => {
|
|
1336
|
+
const config = {
|
|
1337
|
+
threshold: 1,
|
|
1338
|
+
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
1339
|
+
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
1340
|
+
};
|
|
1341
|
+
const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
|
|
1342
|
+
vi.mocked(executeForSummary).mockResolvedValueOnce({
|
|
1343
|
+
toCommitment: () => ({
|
|
1344
|
+
toHex: () => '0x' + 'f'.repeat(64),
|
|
1345
|
+
}),
|
|
1346
|
+
});
|
|
1347
|
+
await expect(multisig.importProposal(JSON.stringify({
|
|
1348
|
+
accountId: '0x' + 'a'.repeat(30),
|
|
1349
|
+
nonce: 1,
|
|
1350
|
+
commitment: '0x' + 'c'.repeat(64),
|
|
1351
|
+
txSummaryBase64: 'AQID',
|
|
1352
|
+
signatures: [],
|
|
1353
|
+
metadata: {
|
|
1354
|
+
proposalType: 'add_signer',
|
|
1355
|
+
targetThreshold: 1,
|
|
1356
|
+
targetSignerCommitments: ['0x' + 'a'.repeat(64)],
|
|
1357
|
+
description: '',
|
|
1358
|
+
},
|
|
1359
|
+
}))).rejects.toThrow('Invalid proposal: metadata does not match tx_summary');
|
|
1360
|
+
});
|
|
1361
|
+
});
|
|
1362
|
+
describe('signProposalOffline', () => {
|
|
1363
|
+
it('should reject signing imported proposals whose metadata does not match tx_summary', async () => {
|
|
1364
|
+
const config = {
|
|
1365
|
+
threshold: 1,
|
|
1366
|
+
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
1367
|
+
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
1368
|
+
};
|
|
1369
|
+
const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
|
|
1370
|
+
vi.mocked(executeForSummary).mockResolvedValueOnce({
|
|
1371
|
+
toCommitment: () => ({
|
|
1372
|
+
toHex: () => '0x' + 'c'.repeat(64),
|
|
1373
|
+
}),
|
|
465
1374
|
});
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
1375
|
+
const proposal = await multisig.importProposal(JSON.stringify({
|
|
1376
|
+
accountId: '0x' + 'a'.repeat(30),
|
|
1377
|
+
nonce: 1,
|
|
1378
|
+
commitment: '0x' + 'c'.repeat(64),
|
|
1379
|
+
txSummaryBase64: 'AQID',
|
|
1380
|
+
signatures: [],
|
|
1381
|
+
metadata: {
|
|
1382
|
+
proposalType: 'add_signer',
|
|
1383
|
+
targetThreshold: 1,
|
|
1384
|
+
targetSignerCommitments: ['0x' + 'a'.repeat(64)],
|
|
1385
|
+
description: '',
|
|
1386
|
+
},
|
|
1387
|
+
}));
|
|
1388
|
+
proposal.metadata = {
|
|
1389
|
+
proposalType: 'add_signer',
|
|
1390
|
+
targetThreshold: 2,
|
|
1391
|
+
targetSignerCommitments: ['0x' + 'a'.repeat(64)],
|
|
1392
|
+
description: '',
|
|
1393
|
+
};
|
|
1394
|
+
vi.mocked(executeForSummary).mockResolvedValueOnce({
|
|
1395
|
+
toCommitment: () => ({
|
|
1396
|
+
toHex: () => '0x' + 'f'.repeat(64),
|
|
1397
|
+
}),
|
|
470
1398
|
});
|
|
471
|
-
|
|
472
|
-
const proposals = await multisig.signTransactionProposal(proposalId);
|
|
473
|
-
expect(mockSigner.signCommitment).toHaveBeenCalledWith(proposalId);
|
|
474
|
-
expect(proposals.length).toBeGreaterThanOrEqual(1);
|
|
475
|
-
expect(proposals[0].signatures.length).toBe(1);
|
|
476
|
-
expect(proposals[0].commitment).toBe(proposals[0].id);
|
|
1399
|
+
await expect(multisig.signProposalOffline(proposal.id)).rejects.toThrow('Invalid proposal: metadata does not match tx_summary');
|
|
477
1400
|
});
|
|
478
1401
|
});
|
|
479
|
-
describe('
|
|
480
|
-
it('should export proposal
|
|
1402
|
+
describe('exportProposal', () => {
|
|
1403
|
+
it('should export proposal for offline signing', async () => {
|
|
481
1404
|
const config = {
|
|
482
1405
|
threshold: 2,
|
|
483
1406
|
signerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
|
|
484
|
-
|
|
1407
|
+
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
485
1408
|
};
|
|
486
|
-
const multisig = new Multisig(mockAccount, config,
|
|
487
|
-
// First sync proposals to populate local cache
|
|
1409
|
+
const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
|
|
488
1410
|
const mockProposals = [
|
|
489
1411
|
{
|
|
490
1412
|
account_id: '0x' + 'a'.repeat(30),
|
|
@@ -506,7 +1428,7 @@ describe('Multisig', () => {
|
|
|
506
1428
|
proposer_id: '0x' + 'c'.repeat(64),
|
|
507
1429
|
cosigner_sigs: [
|
|
508
1430
|
{
|
|
509
|
-
signer_id: '0x' + '
|
|
1431
|
+
signer_id: '0x' + 'a'.repeat(64),
|
|
510
1432
|
signature: { scheme: 'falcon', signature: '0x' + 'e'.repeat(128) },
|
|
511
1433
|
timestamp: '2024-01-01T00:00:00Z',
|
|
512
1434
|
},
|
|
@@ -516,45 +1438,223 @@ describe('Multisig', () => {
|
|
|
516
1438
|
];
|
|
517
1439
|
mockFetch.mockResolvedValueOnce({
|
|
518
1440
|
ok: true,
|
|
519
|
-
json: async () =>
|
|
1441
|
+
json: async () => mockProposals[0],
|
|
520
1442
|
});
|
|
521
|
-
await multisig.syncTransactionProposals();
|
|
522
1443
|
// The proposal ID is computed from tx_summary, which is mocked to return 'c'.repeat(64)
|
|
523
|
-
const
|
|
524
|
-
const exported = JSON.parse(json);
|
|
1444
|
+
const exported = await multisig.exportProposal('0x' + 'c'.repeat(64));
|
|
525
1445
|
expect(exported.accountId).toBe('0x' + 'a'.repeat(30));
|
|
526
1446
|
expect(exported.nonce).toBe(1);
|
|
527
1447
|
expect(exported.txSummaryBase64).toBe('AQID');
|
|
528
1448
|
expect(exported.signatures.length).toBe(1);
|
|
529
|
-
expect(exported.commitment).toBe('0x' + 'c'.repeat(64));
|
|
530
1449
|
});
|
|
531
|
-
it('should
|
|
1450
|
+
it('should preserve ECDSA signature metadata in exported proposals', async () => {
|
|
1451
|
+
const config = {
|
|
1452
|
+
threshold: 2,
|
|
1453
|
+
signerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
|
|
1454
|
+
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
1455
|
+
};
|
|
1456
|
+
const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
|
|
1457
|
+
const publicKey = '0x' + 'd'.repeat(66);
|
|
1458
|
+
mockFetch.mockResolvedValueOnce({
|
|
1459
|
+
ok: true,
|
|
1460
|
+
json: async () => ({
|
|
1461
|
+
account_id: '0x' + 'a'.repeat(30),
|
|
1462
|
+
nonce: 1,
|
|
1463
|
+
prev_commitment: '0x' + 'b'.repeat(64),
|
|
1464
|
+
delta_payload: {
|
|
1465
|
+
tx_summary: { data: 'AQID' },
|
|
1466
|
+
signatures: [],
|
|
1467
|
+
metadata: {
|
|
1468
|
+
proposal_type: 'change_threshold',
|
|
1469
|
+
description: '',
|
|
1470
|
+
target_threshold: 2,
|
|
1471
|
+
signer_commitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
|
|
1472
|
+
},
|
|
1473
|
+
},
|
|
1474
|
+
status: {
|
|
1475
|
+
status: 'pending',
|
|
1476
|
+
timestamp: '2024-01-01T00:00:00Z',
|
|
1477
|
+
proposer_id: '0x' + 'c'.repeat(64),
|
|
1478
|
+
cosigner_sigs: [
|
|
1479
|
+
{
|
|
1480
|
+
signer_id: '0x' + 'a'.repeat(64),
|
|
1481
|
+
signature: {
|
|
1482
|
+
scheme: 'ecdsa',
|
|
1483
|
+
signature: '0x' + 'e'.repeat(130),
|
|
1484
|
+
public_key: publicKey,
|
|
1485
|
+
},
|
|
1486
|
+
timestamp: '2024-01-01T00:00:00Z',
|
|
1487
|
+
},
|
|
1488
|
+
],
|
|
1489
|
+
},
|
|
1490
|
+
}),
|
|
1491
|
+
});
|
|
1492
|
+
const exported = await multisig.exportProposal('0x' + 'c'.repeat(64));
|
|
1493
|
+
expect(exported.signatures).toEqual([
|
|
1494
|
+
{
|
|
1495
|
+
commitment: '0x' + 'a'.repeat(64),
|
|
1496
|
+
signatureHex: '0x' + 'e'.repeat(130),
|
|
1497
|
+
scheme: 'ecdsa',
|
|
1498
|
+
publicKey,
|
|
1499
|
+
timestamp: '2024-01-01T00:00:00Z',
|
|
1500
|
+
},
|
|
1501
|
+
]);
|
|
1502
|
+
});
|
|
1503
|
+
it('should throw if proposal not found', async () => {
|
|
1504
|
+
const config = {
|
|
1505
|
+
threshold: 1,
|
|
1506
|
+
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
1507
|
+
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
1508
|
+
};
|
|
1509
|
+
const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
|
|
1510
|
+
mockFetch.mockResolvedValueOnce({
|
|
1511
|
+
ok: false,
|
|
1512
|
+
status: 404,
|
|
1513
|
+
statusText: 'Not Found',
|
|
1514
|
+
text: async () => 'Proposal not found',
|
|
1515
|
+
});
|
|
1516
|
+
await expect(multisig.exportProposal('0x' + 'nonexistent'.repeat(5))).rejects.toThrow('Proposal not found');
|
|
1517
|
+
});
|
|
1518
|
+
});
|
|
1519
|
+
describe('importProposal', () => {
|
|
1520
|
+
it('should reject imported signatures with non-32-byte signer IDs', async () => {
|
|
1521
|
+
const config = {
|
|
1522
|
+
threshold: 1,
|
|
1523
|
+
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
1524
|
+
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
1525
|
+
};
|
|
1526
|
+
const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
|
|
1527
|
+
const exported = {
|
|
1528
|
+
accountId: multisig.accountId,
|
|
1529
|
+
nonce: 1,
|
|
1530
|
+
commitment: '0x' + 'c'.repeat(64),
|
|
1531
|
+
txSummaryBase64: 'AQID',
|
|
1532
|
+
signatures: [
|
|
1533
|
+
{
|
|
1534
|
+
commitment: '0x1',
|
|
1535
|
+
signatureHex: '0x' + 'b'.repeat(128),
|
|
1536
|
+
},
|
|
1537
|
+
],
|
|
1538
|
+
metadata: {
|
|
1539
|
+
proposalType: 'add_signer',
|
|
1540
|
+
targetThreshold: 1,
|
|
1541
|
+
targetSignerCommitments: ['0x' + 'a'.repeat(64)],
|
|
1542
|
+
description: '',
|
|
1543
|
+
},
|
|
1544
|
+
};
|
|
1545
|
+
await expect(multisig.importProposal(JSON.stringify(exported))).rejects.toThrow('expected signerId as 32-byte hex');
|
|
1546
|
+
});
|
|
1547
|
+
it('should preserve ECDSA imported signature metadata', async () => {
|
|
1548
|
+
const config = {
|
|
1549
|
+
threshold: 1,
|
|
1550
|
+
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
1551
|
+
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
1552
|
+
};
|
|
1553
|
+
const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
|
|
1554
|
+
const publicKey = '0x' + 'd'.repeat(66);
|
|
1555
|
+
const proposal = await multisig.importProposal(JSON.stringify({
|
|
1556
|
+
accountId: multisig.accountId,
|
|
1557
|
+
nonce: 1,
|
|
1558
|
+
commitment: '0x' + 'c'.repeat(64),
|
|
1559
|
+
txSummaryBase64: 'AQID',
|
|
1560
|
+
signatures: [
|
|
1561
|
+
{
|
|
1562
|
+
commitment: '0x' + 'a'.repeat(64),
|
|
1563
|
+
signatureHex: '0x' + 'b'.repeat(130),
|
|
1564
|
+
scheme: 'ecdsa',
|
|
1565
|
+
publicKey,
|
|
1566
|
+
timestamp: '2024-01-01T00:00:00Z',
|
|
1567
|
+
},
|
|
1568
|
+
],
|
|
1569
|
+
metadata: {
|
|
1570
|
+
proposalType: 'change_threshold',
|
|
1571
|
+
targetThreshold: 1,
|
|
1572
|
+
targetSignerCommitments: ['0x' + 'a'.repeat(64)],
|
|
1573
|
+
description: '',
|
|
1574
|
+
},
|
|
1575
|
+
}));
|
|
1576
|
+
expect(proposal.signatures).toEqual([
|
|
1577
|
+
{
|
|
1578
|
+
signerId: '0x' + 'a'.repeat(64),
|
|
1579
|
+
signature: {
|
|
1580
|
+
scheme: 'ecdsa',
|
|
1581
|
+
signature: '0x' + 'b'.repeat(130),
|
|
1582
|
+
publicKey,
|
|
1583
|
+
},
|
|
1584
|
+
timestamp: '2024-01-01T00:00:00Z',
|
|
1585
|
+
},
|
|
1586
|
+
]);
|
|
1587
|
+
});
|
|
1588
|
+
it('should reject imported ECDSA signatures without a public key', async () => {
|
|
532
1589
|
const config = {
|
|
533
1590
|
threshold: 1,
|
|
534
1591
|
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
535
|
-
|
|
1592
|
+
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
1593
|
+
};
|
|
1594
|
+
const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
|
|
1595
|
+
await expect(multisig.importProposal(JSON.stringify({
|
|
1596
|
+
accountId: multisig.accountId,
|
|
1597
|
+
nonce: 1,
|
|
1598
|
+
commitment: '0x' + 'c'.repeat(64),
|
|
1599
|
+
txSummaryBase64: 'AQID',
|
|
1600
|
+
signatures: [
|
|
1601
|
+
{
|
|
1602
|
+
commitment: '0x' + 'a'.repeat(64),
|
|
1603
|
+
signatureHex: '0x' + 'b'.repeat(130),
|
|
1604
|
+
scheme: 'ecdsa',
|
|
1605
|
+
},
|
|
1606
|
+
],
|
|
1607
|
+
metadata: {
|
|
1608
|
+
proposalType: 'change_threshold',
|
|
1609
|
+
targetThreshold: 1,
|
|
1610
|
+
targetSignerCommitments: ['0x' + 'a'.repeat(64)],
|
|
1611
|
+
description: '',
|
|
1612
|
+
},
|
|
1613
|
+
}))).rejects.toThrow('ECDSA signature for 0x' + 'a'.repeat(64) + ' is missing publicKey');
|
|
1614
|
+
});
|
|
1615
|
+
it('should reject offline signing if an imported proposal account is changed', async () => {
|
|
1616
|
+
const config = {
|
|
1617
|
+
threshold: 2,
|
|
1618
|
+
signerCommitments: ['0x' + 'a'.repeat(64), mockSigner.commitment],
|
|
1619
|
+
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
536
1620
|
};
|
|
537
|
-
const multisig = new Multisig(mockAccount, config,
|
|
538
|
-
|
|
1621
|
+
const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
|
|
1622
|
+
const exported = {
|
|
1623
|
+
accountId: multisig.accountId,
|
|
1624
|
+
nonce: 1,
|
|
1625
|
+
commitment: '0x' + 'c'.repeat(64),
|
|
1626
|
+
txSummaryBase64: 'AQID',
|
|
1627
|
+
signatures: [],
|
|
1628
|
+
metadata: {
|
|
1629
|
+
proposalType: 'add_signer',
|
|
1630
|
+
targetThreshold: 2,
|
|
1631
|
+
targetSignerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
|
|
1632
|
+
description: '',
|
|
1633
|
+
},
|
|
1634
|
+
};
|
|
1635
|
+
const proposal = await multisig.importProposal(JSON.stringify(exported));
|
|
1636
|
+
proposal.accountId = '0x' + 'f'.repeat(30);
|
|
1637
|
+
await expect(multisig.signProposalOffline(proposal.id)).rejects.toThrow('Proposal is for a different account: 0x' + 'f'.repeat(30));
|
|
1638
|
+
expect(mockSigner.signCommitment).not.toHaveBeenCalled();
|
|
539
1639
|
});
|
|
540
1640
|
});
|
|
541
|
-
describe('
|
|
1641
|
+
describe('executeProposal', () => {
|
|
542
1642
|
it('should throw if proposal not found locally', async () => {
|
|
543
1643
|
const config = {
|
|
544
1644
|
threshold: 1,
|
|
545
1645
|
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
546
|
-
|
|
1646
|
+
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
547
1647
|
};
|
|
548
|
-
const multisig = new Multisig(mockAccount, config,
|
|
549
|
-
await expect(multisig.
|
|
1648
|
+
const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
|
|
1649
|
+
await expect(multisig.executeProposal('0x' + 'nonexistent'.repeat(5))).rejects.toThrow('Proposal not found');
|
|
550
1650
|
});
|
|
551
1651
|
it('should throw if proposal is still pending', async () => {
|
|
552
1652
|
const config = {
|
|
553
1653
|
threshold: 2, // Need 2 signatures
|
|
554
1654
|
signerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
|
|
555
|
-
|
|
1655
|
+
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
556
1656
|
};
|
|
557
|
-
const multisig = new Multisig(mockAccount, config,
|
|
1657
|
+
const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
|
|
558
1658
|
// Sync with pending proposal (only 1 signature)
|
|
559
1659
|
const mockProposals = [
|
|
560
1660
|
{
|
|
@@ -577,7 +1677,7 @@ describe('Multisig', () => {
|
|
|
577
1677
|
proposer_id: '0x' + 'c'.repeat(64),
|
|
578
1678
|
cosigner_sigs: [
|
|
579
1679
|
{
|
|
580
|
-
signer_id: '0x' + '
|
|
1680
|
+
signer_id: '0x' + 'a'.repeat(64),
|
|
581
1681
|
signature: { scheme: 'falcon', signature: '0x' + 'e'.repeat(128) },
|
|
582
1682
|
timestamp: '2024-01-01T00:00:00Z',
|
|
583
1683
|
},
|
|
@@ -589,17 +1689,17 @@ describe('Multisig', () => {
|
|
|
589
1689
|
ok: true,
|
|
590
1690
|
json: async () => ({ proposals: mockProposals }),
|
|
591
1691
|
});
|
|
592
|
-
await multisig.
|
|
1692
|
+
await multisig.syncProposals();
|
|
593
1693
|
// Proposal ID is mocked to return 'c'.repeat(64)
|
|
594
|
-
await expect(multisig.
|
|
1694
|
+
await expect(multisig.executeProposal('0x' + 'c'.repeat(64))).rejects.toThrow('not ready for execution');
|
|
595
1695
|
});
|
|
596
|
-
it('should fail when
|
|
1696
|
+
it('should fail when GUARDIAN ack signature is missing (selector ON)', async () => {
|
|
597
1697
|
const config = {
|
|
598
1698
|
threshold: 1,
|
|
599
1699
|
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
600
|
-
|
|
1700
|
+
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
601
1701
|
};
|
|
602
|
-
const multisig = new Multisig(mockAccount, config,
|
|
1702
|
+
const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
|
|
603
1703
|
const readyDelta = {
|
|
604
1704
|
account_id: '0x' + 'a'.repeat(30),
|
|
605
1705
|
nonce: 1,
|
|
@@ -620,7 +1720,7 @@ describe('Multisig', () => {
|
|
|
620
1720
|
proposer_id: '0x' + 'c'.repeat(64),
|
|
621
1721
|
cosigner_sigs: [
|
|
622
1722
|
{
|
|
623
|
-
signer_id: '0x' + '
|
|
1723
|
+
signer_id: '0x' + 'a'.repeat(64),
|
|
624
1724
|
signature: { scheme: 'falcon', signature: '0x' + 'e'.repeat(128) },
|
|
625
1725
|
timestamp: '2024-01-01T00:00:00Z',
|
|
626
1726
|
},
|
|
@@ -633,18 +1733,353 @@ describe('Multisig', () => {
|
|
|
633
1733
|
ok: true,
|
|
634
1734
|
json: async () => ({ proposals: [readyDelta] }),
|
|
635
1735
|
});
|
|
636
|
-
await multisig.
|
|
637
|
-
// executeProposal:
|
|
1736
|
+
await multisig.syncProposals();
|
|
1737
|
+
// executeProposal: getDeltaProposal
|
|
638
1738
|
mockFetch.mockResolvedValueOnce({
|
|
639
1739
|
ok: true,
|
|
640
|
-
json: async () =>
|
|
1740
|
+
json: async () => readyDelta,
|
|
641
1741
|
});
|
|
642
1742
|
// executeProposal: pushDelta without ack_sig
|
|
643
1743
|
mockFetch.mockResolvedValueOnce({
|
|
644
1744
|
ok: true,
|
|
645
1745
|
json: async () => ({ ...readyDelta, ack_sig: null }),
|
|
646
1746
|
});
|
|
647
|
-
await expect(multisig.
|
|
1747
|
+
await expect(multisig.executeProposal(proposalId)).rejects.toThrow('GUARDIAN did not return acknowledgment signature');
|
|
1748
|
+
});
|
|
1749
|
+
it('should encode ECDSA proposal and ack signatures with scheme-aware advice', async () => {
|
|
1750
|
+
const { buildSignatureAdviceEntry, signatureHexToBytes } = await import('./utils/signature.js');
|
|
1751
|
+
vi.mocked(signatureHexToBytes).mockClear();
|
|
1752
|
+
vi.mocked(buildSignatureAdviceEntry).mockClear();
|
|
1753
|
+
const config = {
|
|
1754
|
+
threshold: 1,
|
|
1755
|
+
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
1756
|
+
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
1757
|
+
guardianPublicKey: '0x' + '1'.repeat(66),
|
|
1758
|
+
};
|
|
1759
|
+
const ecdsaSigner = {
|
|
1760
|
+
...mockSigner,
|
|
1761
|
+
scheme: 'ecdsa',
|
|
1762
|
+
publicKey: '0x' + '2'.repeat(66),
|
|
1763
|
+
};
|
|
1764
|
+
const multisig = new Multisig(mockAccount, config, guardian, ecdsaSigner, mockWebClient);
|
|
1765
|
+
const proposalId = '0x' + 'c'.repeat(64);
|
|
1766
|
+
const cosignerPubkey = '0x' + '3'.repeat(66);
|
|
1767
|
+
const ackPubkey = '0x' + '4'.repeat(66);
|
|
1768
|
+
const cosignerSignature = '0x' + '5'.repeat(130);
|
|
1769
|
+
const ackSignature = '0x' + '6'.repeat(130);
|
|
1770
|
+
multisig.proposals.set(proposalId, {
|
|
1771
|
+
id: proposalId,
|
|
1772
|
+
accountId: multisig.accountId,
|
|
1773
|
+
nonce: 1,
|
|
1774
|
+
status: 'ready',
|
|
1775
|
+
txSummary: 'AQID',
|
|
1776
|
+
signatures: [
|
|
1777
|
+
{
|
|
1778
|
+
signerId: '0x' + 'a'.repeat(64),
|
|
1779
|
+
signature: {
|
|
1780
|
+
scheme: 'ecdsa',
|
|
1781
|
+
signature: cosignerSignature,
|
|
1782
|
+
publicKey: cosignerPubkey,
|
|
1783
|
+
},
|
|
1784
|
+
timestamp: '2024-01-01T00:00:00Z',
|
|
1785
|
+
},
|
|
1786
|
+
],
|
|
1787
|
+
metadata: {
|
|
1788
|
+
proposalType: 'change_threshold',
|
|
1789
|
+
targetThreshold: 1,
|
|
1790
|
+
targetSignerCommitments: ['0x' + 'a'.repeat(64)],
|
|
1791
|
+
description: '',
|
|
1792
|
+
},
|
|
1793
|
+
});
|
|
1794
|
+
mockFetch.mockResolvedValueOnce({
|
|
1795
|
+
ok: true,
|
|
1796
|
+
json: async () => ({
|
|
1797
|
+
account_id: multisig.accountId,
|
|
1798
|
+
nonce: 1,
|
|
1799
|
+
prev_commitment: '0x' + 'b'.repeat(64),
|
|
1800
|
+
delta_payload: {
|
|
1801
|
+
tx_summary: { data: 'AQID' },
|
|
1802
|
+
signatures: [],
|
|
1803
|
+
metadata: {
|
|
1804
|
+
proposal_type: 'change_threshold',
|
|
1805
|
+
target_threshold: 1,
|
|
1806
|
+
signer_commitments: ['0x' + 'a'.repeat(64)],
|
|
1807
|
+
},
|
|
1808
|
+
},
|
|
1809
|
+
status: {
|
|
1810
|
+
status: 'pending',
|
|
1811
|
+
timestamp: '2024-01-01T00:00:00Z',
|
|
1812
|
+
proposer_id: '0x' + 'a'.repeat(64),
|
|
1813
|
+
cosigner_sigs: [],
|
|
1814
|
+
},
|
|
1815
|
+
}),
|
|
1816
|
+
});
|
|
1817
|
+
mockFetch.mockResolvedValueOnce({
|
|
1818
|
+
ok: true,
|
|
1819
|
+
json: async () => ({
|
|
1820
|
+
account_id: multisig.accountId,
|
|
1821
|
+
nonce: 1,
|
|
1822
|
+
ack_sig: ackSignature,
|
|
1823
|
+
ack_pubkey: ackPubkey,
|
|
1824
|
+
ack_scheme: 'ecdsa',
|
|
1825
|
+
}),
|
|
1826
|
+
});
|
|
1827
|
+
mockWebClient.executeTransaction.mockResolvedValueOnce({});
|
|
1828
|
+
mockWebClient.proveTransaction.mockResolvedValueOnce({});
|
|
1829
|
+
mockWebClient.submitProvenTransaction.mockResolvedValueOnce(1n);
|
|
1830
|
+
mockWebClient.applyTransaction.mockResolvedValueOnce(undefined);
|
|
1831
|
+
await expect(multisig.executeProposal(proposalId)).resolves.toBeUndefined();
|
|
1832
|
+
expect(vi.mocked(signatureHexToBytes)).toHaveBeenNthCalledWith(1, cosignerSignature, 'ecdsa');
|
|
1833
|
+
expect(vi.mocked(signatureHexToBytes)).toHaveBeenNthCalledWith(2, ackSignature, 'ecdsa');
|
|
1834
|
+
expect(vi.mocked(buildSignatureAdviceEntry)).toHaveBeenNthCalledWith(1, expect.anything(), expect.anything(), expect.anything(), cosignerPubkey, cosignerSignature);
|
|
1835
|
+
expect(vi.mocked(buildSignatureAdviceEntry)).toHaveBeenNthCalledWith(2, expect.anything(), expect.anything(), expect.anything(), ackPubkey, ackSignature);
|
|
1836
|
+
});
|
|
1837
|
+
it('should execute imported ECDSA proposals with scheme-aware advice', async () => {
|
|
1838
|
+
const { buildSignatureAdviceEntry, signatureHexToBytes } = await import('./utils/signature.js');
|
|
1839
|
+
vi.mocked(signatureHexToBytes).mockClear();
|
|
1840
|
+
vi.mocked(buildSignatureAdviceEntry).mockClear();
|
|
1841
|
+
const config = {
|
|
1842
|
+
threshold: 1,
|
|
1843
|
+
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
1844
|
+
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
1845
|
+
guardianPublicKey: '0x' + '1'.repeat(66),
|
|
1846
|
+
};
|
|
1847
|
+
const ecdsaSigner = {
|
|
1848
|
+
...mockSigner,
|
|
1849
|
+
scheme: 'ecdsa',
|
|
1850
|
+
publicKey: '0x' + '2'.repeat(66),
|
|
1851
|
+
};
|
|
1852
|
+
const multisig = new Multisig(mockAccount, config, guardian, ecdsaSigner, mockWebClient);
|
|
1853
|
+
const proposalId = '0x' + 'c'.repeat(64);
|
|
1854
|
+
const cosignerPubkey = '0x' + '3'.repeat(66);
|
|
1855
|
+
const ackPubkey = '0x' + '4'.repeat(66);
|
|
1856
|
+
const cosignerSignature = '0x' + '5'.repeat(130);
|
|
1857
|
+
const ackSignature = '0x' + '6'.repeat(130);
|
|
1858
|
+
await multisig.importProposal(JSON.stringify({
|
|
1859
|
+
accountId: multisig.accountId,
|
|
1860
|
+
nonce: 1,
|
|
1861
|
+
commitment: proposalId,
|
|
1862
|
+
txSummaryBase64: 'AQID',
|
|
1863
|
+
signatures: [
|
|
1864
|
+
{
|
|
1865
|
+
commitment: '0x' + 'a'.repeat(64),
|
|
1866
|
+
signatureHex: cosignerSignature,
|
|
1867
|
+
scheme: 'ecdsa',
|
|
1868
|
+
publicKey: cosignerPubkey,
|
|
1869
|
+
timestamp: '2024-01-01T00:00:00Z',
|
|
1870
|
+
},
|
|
1871
|
+
],
|
|
1872
|
+
metadata: {
|
|
1873
|
+
proposalType: 'change_threshold',
|
|
1874
|
+
targetThreshold: 1,
|
|
1875
|
+
targetSignerCommitments: ['0x' + 'a'.repeat(64)],
|
|
1876
|
+
description: '',
|
|
1877
|
+
},
|
|
1878
|
+
}));
|
|
1879
|
+
mockFetch.mockResolvedValueOnce({
|
|
1880
|
+
ok: true,
|
|
1881
|
+
json: async () => ({
|
|
1882
|
+
account_id: multisig.accountId,
|
|
1883
|
+
nonce: 1,
|
|
1884
|
+
prev_commitment: '0x' + 'b'.repeat(64),
|
|
1885
|
+
delta_payload: {
|
|
1886
|
+
tx_summary: { data: 'AQID' },
|
|
1887
|
+
signatures: [],
|
|
1888
|
+
metadata: {
|
|
1889
|
+
proposal_type: 'change_threshold',
|
|
1890
|
+
target_threshold: 1,
|
|
1891
|
+
signer_commitments: ['0x' + 'a'.repeat(64)],
|
|
1892
|
+
},
|
|
1893
|
+
},
|
|
1894
|
+
status: {
|
|
1895
|
+
status: 'pending',
|
|
1896
|
+
timestamp: '2024-01-01T00:00:00Z',
|
|
1897
|
+
proposer_id: '0x' + 'a'.repeat(64),
|
|
1898
|
+
cosigner_sigs: [],
|
|
1899
|
+
},
|
|
1900
|
+
}),
|
|
1901
|
+
});
|
|
1902
|
+
mockFetch.mockResolvedValueOnce({
|
|
1903
|
+
ok: true,
|
|
1904
|
+
json: async () => ({
|
|
1905
|
+
account_id: multisig.accountId,
|
|
1906
|
+
nonce: 1,
|
|
1907
|
+
ack_sig: ackSignature,
|
|
1908
|
+
ack_pubkey: ackPubkey,
|
|
1909
|
+
ack_scheme: 'ecdsa',
|
|
1910
|
+
}),
|
|
1911
|
+
});
|
|
1912
|
+
mockWebClient.executeTransaction.mockResolvedValueOnce({});
|
|
1913
|
+
mockWebClient.proveTransaction.mockResolvedValueOnce({});
|
|
1914
|
+
mockWebClient.submitProvenTransaction.mockResolvedValueOnce(1n);
|
|
1915
|
+
mockWebClient.applyTransaction.mockResolvedValueOnce(undefined);
|
|
1916
|
+
await expect(multisig.executeProposal(proposalId)).resolves.toBeUndefined();
|
|
1917
|
+
expect(vi.mocked(signatureHexToBytes)).toHaveBeenNthCalledWith(1, cosignerSignature, 'ecdsa');
|
|
1918
|
+
expect(vi.mocked(signatureHexToBytes)).toHaveBeenNthCalledWith(2, ackSignature, 'ecdsa');
|
|
1919
|
+
expect(vi.mocked(buildSignatureAdviceEntry)).toHaveBeenNthCalledWith(1, expect.anything(), expect.anything(), expect.anything(), cosignerPubkey, cosignerSignature);
|
|
1920
|
+
expect(vi.mocked(buildSignatureAdviceEntry)).toHaveBeenNthCalledWith(2, expect.anything(), expect.anything(), expect.anything(), ackPubkey, ackSignature);
|
|
1921
|
+
});
|
|
1922
|
+
it('should verify switch_guardian endpoint commitment before execution', async () => {
|
|
1923
|
+
const config = {
|
|
1924
|
+
threshold: 1,
|
|
1925
|
+
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
1926
|
+
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
1927
|
+
};
|
|
1928
|
+
const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
|
|
1929
|
+
const proposalId = '0x' + 'c'.repeat(64);
|
|
1930
|
+
const newGuardianPubkey = '0x' + '1'.repeat(64);
|
|
1931
|
+
multisig.proposals.set(proposalId, {
|
|
1932
|
+
id: proposalId,
|
|
1933
|
+
accountId: multisig.accountId,
|
|
1934
|
+
nonce: 1,
|
|
1935
|
+
status: 'ready',
|
|
1936
|
+
txSummary: 'AQID',
|
|
1937
|
+
signatures: [
|
|
1938
|
+
{
|
|
1939
|
+
signerId: '0x' + 'a'.repeat(64),
|
|
1940
|
+
signature: { scheme: 'falcon', signature: '0x' + 'b'.repeat(128) },
|
|
1941
|
+
timestamp: '2024-01-01T00:00:00Z',
|
|
1942
|
+
},
|
|
1943
|
+
],
|
|
1944
|
+
metadata: {
|
|
1945
|
+
proposalType: 'switch_guardian',
|
|
1946
|
+
newGuardianPubkey,
|
|
1947
|
+
newGuardianEndpoint: 'http://new-guardian.com',
|
|
1948
|
+
description: '',
|
|
1949
|
+
},
|
|
1950
|
+
});
|
|
1951
|
+
mockFetch.mockResolvedValueOnce({
|
|
1952
|
+
ok: true,
|
|
1953
|
+
json: async () => ({ commitment: newGuardianPubkey }),
|
|
1954
|
+
});
|
|
1955
|
+
mockWebClient.getAccount.mockResolvedValueOnce({
|
|
1956
|
+
serialize: () => new Uint8Array([1, 2, 3]),
|
|
1957
|
+
});
|
|
1958
|
+
mockFetch.mockResolvedValueOnce({
|
|
1959
|
+
ok: true,
|
|
1960
|
+
json: async () => ({ success: true, message: 'ok', ack_pubkey: '0x' + 'f'.repeat(64) }),
|
|
1961
|
+
});
|
|
1962
|
+
mockWebClient.executeTransaction.mockResolvedValueOnce({});
|
|
1963
|
+
mockWebClient.proveTransaction.mockResolvedValueOnce({});
|
|
1964
|
+
mockWebClient.submitProvenTransaction.mockResolvedValueOnce(1n);
|
|
1965
|
+
mockWebClient.applyTransaction.mockResolvedValueOnce(undefined);
|
|
1966
|
+
await expect(multisig.executeProposal(proposalId)).resolves.toBeUndefined();
|
|
1967
|
+
expect(mockWebClient.executeTransaction).toHaveBeenCalledTimes(1);
|
|
1968
|
+
});
|
|
1969
|
+
it('should reject switch_guardian execution when endpoint commitment mismatches', async () => {
|
|
1970
|
+
const config = {
|
|
1971
|
+
threshold: 1,
|
|
1972
|
+
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
1973
|
+
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
1974
|
+
};
|
|
1975
|
+
const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
|
|
1976
|
+
const proposalId = '0x' + 'c'.repeat(64);
|
|
1977
|
+
multisig.proposals.set(proposalId, {
|
|
1978
|
+
id: proposalId,
|
|
1979
|
+
accountId: multisig.accountId,
|
|
1980
|
+
nonce: 1,
|
|
1981
|
+
status: 'ready',
|
|
1982
|
+
txSummary: 'AQID',
|
|
1983
|
+
signatures: [
|
|
1984
|
+
{
|
|
1985
|
+
signerId: '0x' + 'a'.repeat(64),
|
|
1986
|
+
signature: { scheme: 'falcon', signature: '0x' + 'b'.repeat(128) },
|
|
1987
|
+
timestamp: '2024-01-01T00:00:00Z',
|
|
1988
|
+
},
|
|
1989
|
+
],
|
|
1990
|
+
metadata: {
|
|
1991
|
+
proposalType: 'switch_guardian',
|
|
1992
|
+
newGuardianPubkey: '0x' + '1'.repeat(64),
|
|
1993
|
+
newGuardianEndpoint: 'http://new-guardian.com',
|
|
1994
|
+
description: '',
|
|
1995
|
+
},
|
|
1996
|
+
});
|
|
1997
|
+
mockFetch.mockResolvedValueOnce({
|
|
1998
|
+
ok: true,
|
|
1999
|
+
json: async () => ({ commitment: '0x' + '2'.repeat(64) }),
|
|
2000
|
+
});
|
|
2001
|
+
await expect(multisig.executeProposal(proposalId)).rejects.toThrow('Refusing to use GUARDIAN endpoint');
|
|
2002
|
+
expect(mockWebClient.executeTransaction).not.toHaveBeenCalled();
|
|
2003
|
+
});
|
|
2004
|
+
it('should reject duplicate normalized signer IDs during execution', async () => {
|
|
2005
|
+
const config = {
|
|
2006
|
+
threshold: 2,
|
|
2007
|
+
signerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
|
|
2008
|
+
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
2009
|
+
};
|
|
2010
|
+
const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
|
|
2011
|
+
const proposalId = '0x' + 'c'.repeat(64);
|
|
2012
|
+
multisig.proposals.set(proposalId, {
|
|
2013
|
+
id: proposalId,
|
|
2014
|
+
accountId: multisig.accountId,
|
|
2015
|
+
nonce: 1,
|
|
2016
|
+
status: 'ready',
|
|
2017
|
+
txSummary: 'AQID',
|
|
2018
|
+
signatures: [
|
|
2019
|
+
{
|
|
2020
|
+
signerId: '0x' + 'a'.repeat(64),
|
|
2021
|
+
signature: { scheme: 'falcon', signature: '0x' + 'b'.repeat(128) },
|
|
2022
|
+
timestamp: '2024-01-01T00:00:00Z',
|
|
2023
|
+
},
|
|
2024
|
+
{
|
|
2025
|
+
signerId: '0x' + 'A'.repeat(64),
|
|
2026
|
+
signature: { scheme: 'falcon', signature: '0x' + 'c'.repeat(128) },
|
|
2027
|
+
timestamp: '2024-01-01T00:00:01Z',
|
|
2028
|
+
},
|
|
2029
|
+
],
|
|
2030
|
+
metadata: {
|
|
2031
|
+
proposalType: 'switch_guardian',
|
|
2032
|
+
newGuardianPubkey: '0x' + '1'.repeat(64),
|
|
2033
|
+
newGuardianEndpoint: 'http://new-guardian.com',
|
|
2034
|
+
description: '',
|
|
2035
|
+
},
|
|
2036
|
+
});
|
|
2037
|
+
await expect(multisig.executeProposal(proposalId)).rejects.toThrow('duplicate signatures for signer');
|
|
2038
|
+
});
|
|
2039
|
+
it('should reject advice-map key collisions during execution', async () => {
|
|
2040
|
+
const { buildSignatureAdviceEntry } = await import('./utils/signature.js');
|
|
2041
|
+
vi.mocked(buildSignatureAdviceEntry)
|
|
2042
|
+
.mockImplementationOnce(() => ({
|
|
2043
|
+
key: { toHex: () => '0x' + 'f'.repeat(64) },
|
|
2044
|
+
values: [1, 2, 3],
|
|
2045
|
+
}))
|
|
2046
|
+
.mockImplementationOnce(() => ({
|
|
2047
|
+
key: { toHex: () => '0x' + 'f'.repeat(64) },
|
|
2048
|
+
values: [1, 2, 3],
|
|
2049
|
+
}));
|
|
2050
|
+
const config = {
|
|
2051
|
+
threshold: 2,
|
|
2052
|
+
signerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
|
|
2053
|
+
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
2054
|
+
};
|
|
2055
|
+
const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
|
|
2056
|
+
const proposalId = '0x' + 'c'.repeat(64);
|
|
2057
|
+
multisig.proposals.set(proposalId, {
|
|
2058
|
+
id: proposalId,
|
|
2059
|
+
accountId: multisig.accountId,
|
|
2060
|
+
nonce: 1,
|
|
2061
|
+
status: 'ready',
|
|
2062
|
+
txSummary: 'AQID',
|
|
2063
|
+
signatures: [
|
|
2064
|
+
{
|
|
2065
|
+
signerId: '0x' + 'a'.repeat(64),
|
|
2066
|
+
signature: { scheme: 'falcon', signature: '0x' + 'b'.repeat(128) },
|
|
2067
|
+
timestamp: '2024-01-01T00:00:00Z',
|
|
2068
|
+
},
|
|
2069
|
+
{
|
|
2070
|
+
signerId: '0x' + 'b'.repeat(64),
|
|
2071
|
+
signature: { scheme: 'falcon', signature: '0x' + 'c'.repeat(128) },
|
|
2072
|
+
timestamp: '2024-01-01T00:00:01Z',
|
|
2073
|
+
},
|
|
2074
|
+
],
|
|
2075
|
+
metadata: {
|
|
2076
|
+
proposalType: 'switch_guardian',
|
|
2077
|
+
newGuardianPubkey: '0x' + '1'.repeat(64),
|
|
2078
|
+
newGuardianEndpoint: 'http://new-guardian.com',
|
|
2079
|
+
description: '',
|
|
2080
|
+
},
|
|
2081
|
+
});
|
|
2082
|
+
await expect(multisig.executeProposal(proposalId)).rejects.toThrow('Duplicate advice-map key detected');
|
|
648
2083
|
});
|
|
649
2084
|
});
|
|
650
2085
|
describe('proposal metadata preservation', () => {
|
|
@@ -652,9 +2087,9 @@ describe('Multisig', () => {
|
|
|
652
2087
|
const config = {
|
|
653
2088
|
threshold: 1,
|
|
654
2089
|
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
655
|
-
|
|
2090
|
+
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
656
2091
|
};
|
|
657
|
-
const multisig = new Multisig(mockAccount, config,
|
|
2092
|
+
const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
|
|
658
2093
|
// Create a proposal with metadata
|
|
659
2094
|
const mockDelta = {
|
|
660
2095
|
account_id: '0x' + 'a'.repeat(30),
|
|
@@ -663,7 +2098,11 @@ describe('Multisig', () => {
|
|
|
663
2098
|
delta_payload: {
|
|
664
2099
|
tx_summary: { data: 'AQID' },
|
|
665
2100
|
signatures: [],
|
|
666
|
-
metadata: {
|
|
2101
|
+
metadata: {
|
|
2102
|
+
proposal_type: 'add_signer',
|
|
2103
|
+
target_threshold: 2,
|
|
2104
|
+
signer_commitments: ['0x1', '0x2'],
|
|
2105
|
+
},
|
|
667
2106
|
},
|
|
668
2107
|
status: {
|
|
669
2108
|
status: 'pending',
|
|
@@ -676,7 +2115,7 @@ describe('Multisig', () => {
|
|
|
676
2115
|
ok: true,
|
|
677
2116
|
json: async () => ({
|
|
678
2117
|
delta: mockDelta,
|
|
679
|
-
commitment: '0x' + '
|
|
2118
|
+
commitment: '0x' + 'c'.repeat(64),
|
|
680
2119
|
}),
|
|
681
2120
|
});
|
|
682
2121
|
const proposal = await multisig.createProposal(1, 'AQID', {
|
|
@@ -693,17 +2132,17 @@ describe('Multisig', () => {
|
|
|
693
2132
|
proposals: [mockDelta],
|
|
694
2133
|
}),
|
|
695
2134
|
});
|
|
696
|
-
const syncedProposals = await multisig.
|
|
2135
|
+
const syncedProposals = await multisig.syncProposals();
|
|
697
2136
|
const syncedProposal = syncedProposals.find(p => p.nonce === 1);
|
|
698
2137
|
expect(syncedProposal?.metadata?.proposalType).toBe('add_signer');
|
|
699
2138
|
});
|
|
700
|
-
it('should use
|
|
2139
|
+
it('should use GUARDIAN metadata for new proposals from other signers', async () => {
|
|
701
2140
|
const config = {
|
|
702
2141
|
threshold: 2,
|
|
703
2142
|
signerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
|
|
704
|
-
|
|
2143
|
+
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
705
2144
|
};
|
|
706
|
-
const multisig = new Multisig(mockAccount, config,
|
|
2145
|
+
const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
|
|
707
2146
|
// Sync proposals - no local proposals exist
|
|
708
2147
|
const mockProposals = [
|
|
709
2148
|
{
|
|
@@ -732,7 +2171,7 @@ describe('Multisig', () => {
|
|
|
732
2171
|
ok: true,
|
|
733
2172
|
json: async () => ({ proposals: mockProposals }),
|
|
734
2173
|
});
|
|
735
|
-
const proposals = await multisig.
|
|
2174
|
+
const proposals = await multisig.syncProposals();
|
|
736
2175
|
expect(proposals.length).toBe(1);
|
|
737
2176
|
expect(proposals[0].metadata?.proposalType).toBe('p2id');
|
|
738
2177
|
});
|
|
@@ -742,9 +2181,9 @@ describe('Multisig', () => {
|
|
|
742
2181
|
const config = {
|
|
743
2182
|
threshold: 1,
|
|
744
2183
|
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
745
|
-
|
|
2184
|
+
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
746
2185
|
};
|
|
747
|
-
const multisig = new Multisig(mockAccount, config,
|
|
2186
|
+
const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
|
|
748
2187
|
const mockDelta = {
|
|
749
2188
|
account_id: '0x' + 'a'.repeat(30),
|
|
750
2189
|
nonce: 1,
|
|
@@ -770,7 +2209,7 @@ describe('Multisig', () => {
|
|
|
770
2209
|
ok: true,
|
|
771
2210
|
json: async () => ({
|
|
772
2211
|
delta: mockDelta,
|
|
773
|
-
commitment: '0x' + '
|
|
2212
|
+
commitment: '0x' + 'c'.repeat(64),
|
|
774
2213
|
}),
|
|
775
2214
|
});
|
|
776
2215
|
const proposal = await multisig.createProposal(1, 'AQID', {
|
|
@@ -784,9 +2223,9 @@ describe('Multisig', () => {
|
|
|
784
2223
|
const config = {
|
|
785
2224
|
threshold: 1,
|
|
786
2225
|
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
787
|
-
|
|
2226
|
+
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
788
2227
|
};
|
|
789
|
-
const multisig = new Multisig(mockAccount, config,
|
|
2228
|
+
const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
|
|
790
2229
|
const mockDelta = {
|
|
791
2230
|
account_id: '0x' + 'a'.repeat(30),
|
|
792
2231
|
nonce: 1,
|
|
@@ -812,7 +2251,7 @@ describe('Multisig', () => {
|
|
|
812
2251
|
ok: true,
|
|
813
2252
|
json: async () => ({
|
|
814
2253
|
delta: mockDelta,
|
|
815
|
-
commitment: '0x' + '
|
|
2254
|
+
commitment: '0x' + 'c'.repeat(64),
|
|
816
2255
|
}),
|
|
817
2256
|
});
|
|
818
2257
|
const proposal = await multisig.createProposal(1, 'AQID', {
|
|
@@ -824,13 +2263,13 @@ describe('Multisig', () => {
|
|
|
824
2263
|
});
|
|
825
2264
|
expect(proposal.metadata?.proposalType).toBe('p2id');
|
|
826
2265
|
});
|
|
827
|
-
it('should create
|
|
2266
|
+
it('should create switch_guardian proposal', async () => {
|
|
828
2267
|
const config = {
|
|
829
2268
|
threshold: 1,
|
|
830
2269
|
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
831
|
-
|
|
2270
|
+
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
832
2271
|
};
|
|
833
|
-
const multisig = new Multisig(mockAccount, config,
|
|
2272
|
+
const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
|
|
834
2273
|
const mockDelta = {
|
|
835
2274
|
account_id: '0x' + 'a'.repeat(30),
|
|
836
2275
|
nonce: 1,
|
|
@@ -856,16 +2295,16 @@ describe('Multisig', () => {
|
|
|
856
2295
|
ok: true,
|
|
857
2296
|
json: async () => ({
|
|
858
2297
|
delta: mockDelta,
|
|
859
|
-
commitment: '0x' + '
|
|
2298
|
+
commitment: '0x' + 'c'.repeat(64),
|
|
860
2299
|
}),
|
|
861
2300
|
});
|
|
862
2301
|
const proposal = await multisig.createProposal(1, 'AQID', {
|
|
863
|
-
proposalType: '
|
|
864
|
-
|
|
865
|
-
|
|
2302
|
+
proposalType: 'switch_guardian',
|
|
2303
|
+
newGuardianPubkey: '0xnewpubkey',
|
|
2304
|
+
newGuardianEndpoint: 'http://new-guardian.com',
|
|
866
2305
|
description: '',
|
|
867
2306
|
});
|
|
868
|
-
expect(proposal.metadata?.proposalType).toBe('
|
|
2307
|
+
expect(proposal.metadata?.proposalType).toBe('switch_guardian');
|
|
869
2308
|
});
|
|
870
2309
|
});
|
|
871
2310
|
describe('proposal status transitions', () => {
|
|
@@ -873,9 +2312,9 @@ describe('Multisig', () => {
|
|
|
873
2312
|
const config = {
|
|
874
2313
|
threshold: 2,
|
|
875
2314
|
signerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
|
|
876
|
-
|
|
2315
|
+
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
877
2316
|
};
|
|
878
|
-
const multisig = new Multisig(mockAccount, config,
|
|
2317
|
+
const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
|
|
879
2318
|
// First sync with 1 signature (pending)
|
|
880
2319
|
const mockProposalsPending = [
|
|
881
2320
|
{
|
|
@@ -910,8 +2349,8 @@ describe('Multisig', () => {
|
|
|
910
2349
|
ok: true,
|
|
911
2350
|
json: async () => ({ proposals: mockProposalsPending }),
|
|
912
2351
|
});
|
|
913
|
-
let proposals = await multisig.
|
|
914
|
-
expect(proposals[0].status
|
|
2352
|
+
let proposals = await multisig.syncProposals();
|
|
2353
|
+
expect(proposals[0].status).toBe('pending');
|
|
915
2354
|
// Second sync with 2 signatures (ready)
|
|
916
2355
|
const mockProposalsReady = [
|
|
917
2356
|
{
|
|
@@ -948,8 +2387,8 @@ describe('Multisig', () => {
|
|
|
948
2387
|
ok: true,
|
|
949
2388
|
json: async () => ({ proposals: mockProposalsReady }),
|
|
950
2389
|
});
|
|
951
|
-
proposals = await multisig.
|
|
952
|
-
expect(proposals[0].status
|
|
2390
|
+
proposals = await multisig.syncProposals();
|
|
2391
|
+
expect(proposals[0].status).toBe('ready');
|
|
953
2392
|
});
|
|
954
2393
|
});
|
|
955
2394
|
describe('getters', () => {
|
|
@@ -957,9 +2396,9 @@ describe('Multisig', () => {
|
|
|
957
2396
|
const config = {
|
|
958
2397
|
threshold: 3,
|
|
959
2398
|
signerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64), '0x' + 'c'.repeat(64)],
|
|
960
|
-
|
|
2399
|
+
guardianCommitment: '0x' + 'd'.repeat(64),
|
|
961
2400
|
};
|
|
962
|
-
const multisig = new Multisig(mockAccount, config,
|
|
2401
|
+
const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
|
|
963
2402
|
expect(multisig.threshold).toBe(3);
|
|
964
2403
|
});
|
|
965
2404
|
it('should expose signerCommitments', () => {
|
|
@@ -967,28 +2406,28 @@ describe('Multisig', () => {
|
|
|
967
2406
|
const config = {
|
|
968
2407
|
threshold: 2,
|
|
969
2408
|
signerCommitments,
|
|
970
|
-
|
|
2409
|
+
guardianCommitment: '0x' + 'd'.repeat(64),
|
|
971
2410
|
};
|
|
972
|
-
const multisig = new Multisig(mockAccount, config,
|
|
2411
|
+
const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
|
|
973
2412
|
expect(multisig.signerCommitments).toEqual(signerCommitments);
|
|
974
2413
|
});
|
|
975
|
-
it('should expose
|
|
976
|
-
const
|
|
2414
|
+
it('should expose guardianCommitment', () => {
|
|
2415
|
+
const guardianCommitment = '0x' + 'guardian'.repeat(20);
|
|
977
2416
|
const config = {
|
|
978
2417
|
threshold: 1,
|
|
979
2418
|
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
980
|
-
|
|
2419
|
+
guardianCommitment,
|
|
981
2420
|
};
|
|
982
|
-
const multisig = new Multisig(mockAccount, config,
|
|
983
|
-
expect(multisig.
|
|
2421
|
+
const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
|
|
2422
|
+
expect(multisig.guardianCommitment).toBe(guardianCommitment);
|
|
984
2423
|
});
|
|
985
2424
|
it('should expose account when provided', () => {
|
|
986
2425
|
const config = {
|
|
987
2426
|
threshold: 1,
|
|
988
2427
|
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
989
|
-
|
|
2428
|
+
guardianCommitment: '0x' + 'd'.repeat(64),
|
|
990
2429
|
};
|
|
991
|
-
const multisig = new Multisig(mockAccount, config,
|
|
2430
|
+
const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
|
|
992
2431
|
expect(multisig.account).toBe(mockAccount);
|
|
993
2432
|
});
|
|
994
2433
|
});
|
|
@@ -997,10 +2436,10 @@ describe('Multisig', () => {
|
|
|
997
2436
|
const config = {
|
|
998
2437
|
threshold: 2,
|
|
999
2438
|
signerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
|
|
1000
|
-
|
|
2439
|
+
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
1001
2440
|
};
|
|
1002
|
-
const multisig = new Multisig(mockAccount, config,
|
|
1003
|
-
// Simulates a
|
|
2441
|
+
const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
|
|
2442
|
+
// Simulates a GUARDIAN response with canonical snake_case metadata
|
|
1004
2443
|
const rustProposals = [
|
|
1005
2444
|
{
|
|
1006
2445
|
account_id: '0x' + 'a'.repeat(30),
|
|
@@ -1028,7 +2467,7 @@ describe('Multisig', () => {
|
|
|
1028
2467
|
ok: true,
|
|
1029
2468
|
json: async () => ({ proposals: rustProposals }),
|
|
1030
2469
|
});
|
|
1031
|
-
const proposals = await multisig.
|
|
2470
|
+
const proposals = await multisig.syncProposals();
|
|
1032
2471
|
expect(proposals.length).toBe(1);
|
|
1033
2472
|
// The TS client should normalize snake_case to camelCase
|
|
1034
2473
|
expect(proposals[0].metadata?.proposalType).toBe('change_threshold');
|
|
@@ -1041,9 +2480,9 @@ describe('Multisig', () => {
|
|
|
1041
2480
|
const config = {
|
|
1042
2481
|
threshold: 1,
|
|
1043
2482
|
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
1044
|
-
|
|
2483
|
+
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
1045
2484
|
};
|
|
1046
|
-
const multisig = new Multisig(mockAccount, config,
|
|
2485
|
+
const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
|
|
1047
2486
|
// P2ID proposal with canonical snake_case fields
|
|
1048
2487
|
const p2idProposals = [
|
|
1049
2488
|
{
|
|
@@ -1079,7 +2518,7 @@ describe('Multisig', () => {
|
|
|
1079
2518
|
ok: true,
|
|
1080
2519
|
json: async () => ({ proposals: p2idProposals }),
|
|
1081
2520
|
});
|
|
1082
|
-
const proposals = await multisig.
|
|
2521
|
+
const proposals = await multisig.syncProposals();
|
|
1083
2522
|
expect(proposals.length).toBe(1);
|
|
1084
2523
|
expect(proposals[0].metadata?.proposalType).toBe('p2id');
|
|
1085
2524
|
if (proposals[0].metadata?.proposalType === 'p2id') {
|
|
@@ -1088,14 +2527,14 @@ describe('Multisig', () => {
|
|
|
1088
2527
|
expect(proposals[0].metadata.amount).toBe('12345');
|
|
1089
2528
|
}
|
|
1090
2529
|
});
|
|
1091
|
-
it('should parse
|
|
2530
|
+
it('should parse switch_guardian proposal with snake_case fields', async () => {
|
|
1092
2531
|
const config = {
|
|
1093
2532
|
threshold: 1,
|
|
1094
2533
|
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
1095
|
-
|
|
2534
|
+
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
1096
2535
|
};
|
|
1097
|
-
const multisig = new Multisig(mockAccount, config,
|
|
1098
|
-
const
|
|
2536
|
+
const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
|
|
2537
|
+
const switchGuardianProposals = [
|
|
1099
2538
|
{
|
|
1100
2539
|
account_id: '0x' + 'a'.repeat(30),
|
|
1101
2540
|
nonce: 1,
|
|
@@ -1104,9 +2543,9 @@ describe('Multisig', () => {
|
|
|
1104
2543
|
tx_summary: { data: 'AQID' },
|
|
1105
2544
|
signatures: [],
|
|
1106
2545
|
metadata: {
|
|
1107
|
-
proposal_type: '
|
|
1108
|
-
|
|
1109
|
-
|
|
2546
|
+
proposal_type: 'switch_guardian',
|
|
2547
|
+
new_guardian_pubkey: '0xnewpubkey',
|
|
2548
|
+
new_guardian_endpoint: 'http://new-guardian.com',
|
|
1110
2549
|
salt: '0xsalt',
|
|
1111
2550
|
},
|
|
1112
2551
|
},
|
|
@@ -1120,14 +2559,14 @@ describe('Multisig', () => {
|
|
|
1120
2559
|
];
|
|
1121
2560
|
mockFetch.mockResolvedValueOnce({
|
|
1122
2561
|
ok: true,
|
|
1123
|
-
json: async () => ({ proposals:
|
|
2562
|
+
json: async () => ({ proposals: switchGuardianProposals }),
|
|
1124
2563
|
});
|
|
1125
|
-
const proposals = await multisig.
|
|
2564
|
+
const proposals = await multisig.syncProposals();
|
|
1126
2565
|
expect(proposals.length).toBe(1);
|
|
1127
|
-
expect(proposals[0].metadata?.proposalType).toBe('
|
|
1128
|
-
if (proposals[0].metadata?.proposalType === '
|
|
1129
|
-
expect(proposals[0].metadata.
|
|
1130
|
-
expect(proposals[0].metadata.
|
|
2566
|
+
expect(proposals[0].metadata?.proposalType).toBe('switch_guardian');
|
|
2567
|
+
if (proposals[0].metadata?.proposalType === 'switch_guardian') {
|
|
2568
|
+
expect(proposals[0].metadata.newGuardianPubkey).toBe('0xnewpubkey');
|
|
2569
|
+
expect(proposals[0].metadata.newGuardianEndpoint).toBe('http://new-guardian.com');
|
|
1131
2570
|
}
|
|
1132
2571
|
});
|
|
1133
2572
|
});
|