@openzeppelin/miden-multisig-client 0.12.6
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 +189 -0
- package/dist/account/builder.d.ts +23 -0
- package/dist/account/builder.d.ts.map +1 -0
- package/dist/account/builder.js +85 -0
- package/dist/account/builder.js.map +1 -0
- package/dist/account/index.d.ts +6 -0
- package/dist/account/index.d.ts.map +1 -0
- package/dist/account/index.js +6 -0
- package/dist/account/index.js.map +1 -0
- package/dist/account/masm.d.ts +3 -0
- package/dist/account/masm.d.ts.map +1 -0
- package/dist/account/masm.js +605 -0
- package/dist/account/masm.js.map +1 -0
- package/dist/account/storage.d.ts +10 -0
- package/dist/account/storage.d.ts.map +1 -0
- package/dist/account/storage.js +56 -0
- package/dist/account/storage.js.map +1 -0
- package/dist/account.d.ts +104 -0
- package/dist/account.d.ts.map +1 -0
- package/dist/account.js +136 -0
- package/dist/account.js.map +1 -0
- package/dist/builder.d.ts +54 -0
- package/dist/builder.d.ts.map +1 -0
- package/dist/builder.js +78 -0
- package/dist/builder.js.map +1 -0
- package/dist/client.d.ts +73 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +104 -0
- package/dist/client.js.map +1 -0
- package/dist/client.test.d.ts +2 -0
- package/dist/client.test.d.ts.map +1 -0
- package/dist/client.test.js +150 -0
- package/dist/client.test.js.map +1 -0
- package/dist/http.d.ts +64 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +162 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +53 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +52 -0
- package/dist/index.js.map +1 -0
- package/dist/inspector.d.ts +49 -0
- package/dist/inspector.d.ts.map +1 -0
- package/dist/inspector.js +120 -0
- package/dist/inspector.js.map +1 -0
- package/dist/inspector.test.d.ts +2 -0
- package/dist/inspector.test.d.ts.map +1 -0
- package/dist/inspector.test.js +205 -0
- package/dist/inspector.test.js.map +1 -0
- package/dist/keystore.d.ts +65 -0
- package/dist/keystore.d.ts.map +1 -0
- package/dist/keystore.js +121 -0
- package/dist/keystore.js.map +1 -0
- package/dist/miden.d.ts +9 -0
- package/dist/miden.d.ts.map +1 -0
- package/dist/miden.js +26 -0
- package/dist/miden.js.map +1 -0
- package/dist/multisig/helpers.d.ts +4 -0
- package/dist/multisig/helpers.d.ts.map +1 -0
- package/dist/multisig/helpers.js +21 -0
- package/dist/multisig/helpers.js.map +1 -0
- package/dist/multisig/helpers.test.d.ts +2 -0
- package/dist/multisig/helpers.test.d.ts.map +1 -0
- package/dist/multisig/helpers.test.js +94 -0
- package/dist/multisig/helpers.test.js.map +1 -0
- package/dist/multisig/metadata.d.ts +18 -0
- package/dist/multisig/metadata.d.ts.map +1 -0
- package/dist/multisig/metadata.js +100 -0
- package/dist/multisig/metadata.js.map +1 -0
- package/dist/multisig/metadata.test.d.ts +2 -0
- package/dist/multisig/metadata.test.d.ts.map +1 -0
- package/dist/multisig/metadata.test.js +323 -0
- package/dist/multisig/metadata.test.js.map +1 -0
- package/dist/multisig.d.ts +199 -0
- package/dist/multisig.d.ts.map +1 -0
- package/dist/multisig.js +821 -0
- package/dist/multisig.js.map +1 -0
- package/dist/multisig.test.d.ts +2 -0
- package/dist/multisig.test.d.ts.map +1 -0
- package/dist/multisig.test.js +1121 -0
- package/dist/multisig.test.js.map +1 -0
- package/dist/procedures.d.ts +57 -0
- package/dist/procedures.d.ts.map +1 -0
- package/dist/procedures.js +62 -0
- package/dist/procedures.js.map +1 -0
- package/dist/signer.d.ts +36 -0
- package/dist/signer.d.ts.map +1 -0
- package/dist/signer.js +68 -0
- package/dist/signer.js.map +1 -0
- package/dist/signer.test.d.ts +2 -0
- package/dist/signer.test.d.ts.map +1 -0
- package/dist/signer.test.js +117 -0
- package/dist/signer.test.js.map +1 -0
- package/dist/transaction/consumeNotes.d.ts +7 -0
- package/dist/transaction/consumeNotes.d.ts.map +1 -0
- package/dist/transaction/consumeNotes.js +28 -0
- package/dist/transaction/consumeNotes.js.map +1 -0
- package/dist/transaction/index.d.ts +7 -0
- package/dist/transaction/index.d.ts.map +1 -0
- package/dist/transaction/index.js +6 -0
- package/dist/transaction/index.js.map +1 -0
- package/dist/transaction/options.d.ts +6 -0
- package/dist/transaction/options.d.ts.map +1 -0
- package/dist/transaction/options.js +2 -0
- package/dist/transaction/options.js.map +1 -0
- package/dist/transaction/p2id.d.ts +7 -0
- package/dist/transaction/p2id.d.ts.map +1 -0
- package/dist/transaction/p2id.js +43 -0
- package/dist/transaction/p2id.js.map +1 -0
- package/dist/transaction/summary.d.ts +3 -0
- package/dist/transaction/summary.d.ts.map +1 -0
- package/dist/transaction/summary.js +6 -0
- package/dist/transaction/summary.js.map +1 -0
- package/dist/transaction/updatePsm.d.ts +7 -0
- package/dist/transaction/updatePsm.d.ts.map +1 -0
- package/dist/transaction/updatePsm.js +43 -0
- package/dist/transaction/updatePsm.js.map +1 -0
- package/dist/transaction/updateSigners.d.ts +8 -0
- package/dist/transaction/updateSigners.d.ts.map +1 -0
- package/dist/transaction/updateSigners.js +60 -0
- package/dist/transaction/updateSigners.js.map +1 -0
- package/dist/transaction.d.ts +6 -0
- package/dist/transaction.d.ts.map +1 -0
- package/dist/transaction.js +8 -0
- package/dist/transaction.js.map +1 -0
- package/dist/transaction.test.d.ts +2 -0
- package/dist/transaction.test.d.ts.map +1 -0
- package/dist/transaction.test.js +117 -0
- package/dist/transaction.test.js.map +1 -0
- package/dist/transport/http.d.ts +64 -0
- package/dist/transport/http.d.ts.map +1 -0
- package/dist/transport/http.js +162 -0
- package/dist/transport/http.js.map +1 -0
- package/dist/transport/index.d.ts +5 -0
- package/dist/transport/index.d.ts.map +1 -0
- package/dist/transport/index.js +5 -0
- package/dist/transport/index.js.map +1 -0
- package/dist/types/proposal.d.ts +74 -0
- package/dist/types/proposal.d.ts.map +1 -0
- package/dist/types/proposal.js +2 -0
- package/dist/types/proposal.js.map +1 -0
- package/dist/types.d.ts +60 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/encoding.d.ts +7 -0
- package/dist/utils/encoding.d.ts.map +1 -0
- package/dist/utils/encoding.js +54 -0
- package/dist/utils/encoding.js.map +1 -0
- package/dist/utils/encoding.test.d.ts +2 -0
- package/dist/utils/encoding.test.d.ts.map +1 -0
- package/dist/utils/encoding.test.js +185 -0
- package/dist/utils/encoding.test.js.map +1 -0
- package/dist/utils/index.d.ts +5 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +5 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/random.d.ts +3 -0
- package/dist/utils/random.d.ts.map +1 -0
- package/dist/utils/random.js +14 -0
- package/dist/utils/random.js.map +1 -0
- package/dist/utils/signature.d.ts +12 -0
- package/dist/utils/signature.d.ts.map +1 -0
- package/dist/utils/signature.js +28 -0
- package/dist/utils/signature.js.map +1 -0
- package/dist/utils/signature.test.d.ts +2 -0
- package/dist/utils/signature.test.d.ts.map +1 -0
- package/dist/utils/signature.test.js +149 -0
- package/dist/utils/signature.test.js.map +1 -0
- package/dist/utils/word.d.ts +4 -0
- package/dist/utils/word.d.ts.map +1 -0
- package/dist/utils/word.js +8 -0
- package/dist/utils/word.js.map +1 -0
- package/dist/utils/word.test.d.ts +2 -0
- package/dist/utils/word.test.d.ts.map +1 -0
- package/dist/utils/word.test.js +54 -0
- package/dist/utils/word.test.js.map +1 -0
- package/masm/multisig.masm +423 -0
- package/masm/psm.masm +178 -0
- package/package.json +42 -0
- package/src/account/builder.ts +112 -0
- package/src/account/index.ts +15 -0
- package/src/account/masm.ts +606 -0
- package/src/account/storage.ts +70 -0
- package/src/client.test.ts +177 -0
- package/src/client.ts +129 -0
- package/src/index.ts +118 -0
- package/src/inspector.test.ts +237 -0
- package/src/inspector.ts +142 -0
- package/src/multisig/helpers.test.ts +114 -0
- package/src/multisig/helpers.ts +23 -0
- package/src/multisig.test.ts +1294 -0
- package/src/multisig.ts +1097 -0
- package/src/procedures.ts +72 -0
- package/src/signer.test.ts +135 -0
- package/src/signer.ts +75 -0
- package/src/transaction/consumeNotes.ts +41 -0
- package/src/transaction/index.ts +7 -0
- package/src/transaction/options.ts +7 -0
- package/src/transaction/p2id.ts +105 -0
- package/src/transaction/summary.ts +12 -0
- package/src/transaction/updatePsm.ts +69 -0
- package/src/transaction/updateSigners.ts +96 -0
- package/src/transaction.test.ts +136 -0
- package/src/transaction.ts +16 -0
- package/src/types/proposal.ts +80 -0
- package/src/types.ts +81 -0
- package/src/utils/encoding.test.ts +233 -0
- package/src/utils/encoding.ts +60 -0
- package/src/utils/index.ts +5 -0
- package/src/utils/random.ts +15 -0
- package/src/utils/signature.test.ts +187 -0
- package/src/utils/signature.ts +36 -0
- package/src/utils/word.test.ts +64 -0
- package/src/utils/word.ts +11 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { MultisigClient } from './client.js';
|
|
3
|
+
import type { Signer } from './types.js';
|
|
4
|
+
|
|
5
|
+
// Mock the Miden SDK
|
|
6
|
+
vi.mock('@demox-labs/miden-sdk', () => ({
|
|
7
|
+
AccountId: {
|
|
8
|
+
fromHex: vi.fn((hex: string) => ({ toString: () => hex })),
|
|
9
|
+
},
|
|
10
|
+
Account: {
|
|
11
|
+
deserialize: vi.fn(() => ({
|
|
12
|
+
id: () => ({
|
|
13
|
+
toString: () => '0x' + 'd'.repeat(30),
|
|
14
|
+
prefix: () => ({ asInt: () => BigInt(1) }),
|
|
15
|
+
suffix: () => ({ asInt: () => BigInt(2) }),
|
|
16
|
+
}),
|
|
17
|
+
storage: vi.fn(),
|
|
18
|
+
vault: vi.fn(),
|
|
19
|
+
})),
|
|
20
|
+
},
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
// Mock the AccountInspector
|
|
24
|
+
vi.mock('./inspector.js', () => ({
|
|
25
|
+
AccountInspector: {
|
|
26
|
+
fromAccount: vi.fn(() => ({
|
|
27
|
+
threshold: 2,
|
|
28
|
+
numSigners: 2,
|
|
29
|
+
signerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
|
|
30
|
+
psmEnabled: true,
|
|
31
|
+
psmCommitment: '0x' + 'c'.repeat(64),
|
|
32
|
+
vaultBalances: [],
|
|
33
|
+
procedureThresholds: new Map(),
|
|
34
|
+
})),
|
|
35
|
+
},
|
|
36
|
+
}));
|
|
37
|
+
|
|
38
|
+
// Mock the account creation module
|
|
39
|
+
vi.mock('./account/index.js', () => ({
|
|
40
|
+
createMultisigAccount: vi.fn().mockResolvedValue({
|
|
41
|
+
account: {
|
|
42
|
+
id: () => ({
|
|
43
|
+
toString: () => '0x' + 'a'.repeat(30),
|
|
44
|
+
prefix: () => ({ asInt: () => BigInt(1) }),
|
|
45
|
+
suffix: () => ({ asInt: () => BigInt(2) }),
|
|
46
|
+
}),
|
|
47
|
+
serialize: () => new Uint8Array([1, 2, 3]),
|
|
48
|
+
},
|
|
49
|
+
seed: new Uint8Array([4, 5, 6]),
|
|
50
|
+
}),
|
|
51
|
+
}));
|
|
52
|
+
|
|
53
|
+
// Mock fetch for PSM client
|
|
54
|
+
const mockFetch = vi.fn();
|
|
55
|
+
vi.stubGlobal('fetch', mockFetch);
|
|
56
|
+
|
|
57
|
+
describe('MultisigClient', () => {
|
|
58
|
+
let webClient: any;
|
|
59
|
+
let mockSigner: Signer;
|
|
60
|
+
|
|
61
|
+
beforeEach(() => {
|
|
62
|
+
mockFetch.mockReset();
|
|
63
|
+
|
|
64
|
+
webClient = {
|
|
65
|
+
createScriptBuilder: vi.fn(),
|
|
66
|
+
executeTransaction: vi.fn(),
|
|
67
|
+
proveTransaction: vi.fn(),
|
|
68
|
+
submitProvenTransaction: vi.fn(),
|
|
69
|
+
applyTransaction: vi.fn(),
|
|
70
|
+
syncState: vi.fn(),
|
|
71
|
+
newAccount: vi.fn(),
|
|
72
|
+
getAccount: vi.fn().mockResolvedValue(null),
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
mockSigner = {
|
|
76
|
+
commitment: '0x' + '1'.repeat(64),
|
|
77
|
+
publicKey: '0x' + '2'.repeat(64),
|
|
78
|
+
signAccountIdWithTimestamp: vi.fn().mockReturnValue('0x' + 'a'.repeat(128)),
|
|
79
|
+
signCommitment: vi.fn().mockReturnValue('0x' + 'b'.repeat(128)),
|
|
80
|
+
};
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe('constructor', () => {
|
|
84
|
+
it('should create client with default PSM endpoint', () => {
|
|
85
|
+
const client = new MultisigClient(webClient);
|
|
86
|
+
expect(client).toBeInstanceOf(MultisigClient);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should create client with custom PSM endpoint', () => {
|
|
90
|
+
const client = new MultisigClient(webClient, { psmEndpoint: 'http://custom:8080' });
|
|
91
|
+
expect(client).toBeInstanceOf(MultisigClient);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe('psmClient getter', () => {
|
|
96
|
+
it('should expose PSM client for getting pubkey', () => {
|
|
97
|
+
const client = new MultisigClient(webClient);
|
|
98
|
+
expect(client.psmClient).toBeDefined();
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe('create', () => {
|
|
103
|
+
it('should create multisig and return Multisig instance', async () => {
|
|
104
|
+
const client = new MultisigClient(webClient);
|
|
105
|
+
|
|
106
|
+
const config = {
|
|
107
|
+
threshold: 2,
|
|
108
|
+
signerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
|
|
109
|
+
psmCommitment: '0x' + 'c'.repeat(64),
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const multisig = await client.create(config, mockSigner);
|
|
113
|
+
|
|
114
|
+
expect(multisig).toBeDefined();
|
|
115
|
+
expect(multisig.threshold).toBe(2);
|
|
116
|
+
expect(multisig.signerCommitments).toEqual(config.signerCommitments);
|
|
117
|
+
expect(multisig.psmCommitment).toBe(config.psmCommitment);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should set signer on PSM client', async () => {
|
|
121
|
+
const client = new MultisigClient(webClient);
|
|
122
|
+
|
|
123
|
+
const config = {
|
|
124
|
+
threshold: 1,
|
|
125
|
+
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
126
|
+
psmCommitment: '0x' + 'c'.repeat(64),
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const multisig = await client.create(config, mockSigner);
|
|
130
|
+
expect(multisig.signerCommitment).toBe(mockSigner.commitment);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe('load', () => {
|
|
135
|
+
it('should load existing multisig account and detect config', async () => {
|
|
136
|
+
const client = new MultisigClient(webClient);
|
|
137
|
+
|
|
138
|
+
// Mock getState response
|
|
139
|
+
mockFetch.mockResolvedValueOnce({
|
|
140
|
+
ok: true,
|
|
141
|
+
json: async () => ({
|
|
142
|
+
account_id: '0x' + 'd'.repeat(30),
|
|
143
|
+
commitment: '0x' + 'e'.repeat(64),
|
|
144
|
+
state_json: { data: 'base64state' },
|
|
145
|
+
created_at: '2024-01-01T00:00:00Z',
|
|
146
|
+
updated_at: '2024-01-02T00:00:00Z',
|
|
147
|
+
}),
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const accountId = '0x' + 'd'.repeat(30);
|
|
151
|
+
const multisig = await client.load(accountId, mockSigner);
|
|
152
|
+
|
|
153
|
+
expect(multisig).toBeDefined();
|
|
154
|
+
expect(multisig.accountId).toBe(accountId);
|
|
155
|
+
// Config is detected from account storage via AccountInspector
|
|
156
|
+
expect(multisig.threshold).toBe(2);
|
|
157
|
+
expect(multisig.signerCommitments).toEqual(['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)]);
|
|
158
|
+
expect(multisig.psmCommitment).toBe('0x' + 'c'.repeat(64));
|
|
159
|
+
expect(multisig.account).toBeNull(); // Loaded accounts don't have the SDK Account
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('should throw if account not found on PSM', async () => {
|
|
163
|
+
const client = new MultisigClient(webClient);
|
|
164
|
+
|
|
165
|
+
mockFetch.mockResolvedValueOnce({
|
|
166
|
+
ok: false,
|
|
167
|
+
status: 404,
|
|
168
|
+
statusText: 'Not Found',
|
|
169
|
+
text: async () => 'Account not found',
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
await expect(
|
|
173
|
+
client.load('0xnonexistent', mockSigner)
|
|
174
|
+
).rejects.toThrow();
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
});
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MultisigClient - Factory for creating and loading multisig accounts.
|
|
3
|
+
*
|
|
4
|
+
* This is the main entry point for the multisig SDK. It provides methods
|
|
5
|
+
* to create new multisig accounts and load existing ones.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { type WebClient, Account, AccountId } from '@demox-labs/miden-sdk';
|
|
9
|
+
import { PsmHttpClient } from '@openzeppelin/psm-client';
|
|
10
|
+
import { Multisig } from './multisig.js';
|
|
11
|
+
import { createMultisigAccount } from './account/index.js';
|
|
12
|
+
import { AccountInspector } from './inspector.js';
|
|
13
|
+
import type { MultisigConfig, Signer } from './types.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Configuration for MultisigClient.
|
|
17
|
+
*/
|
|
18
|
+
export interface MultisigClientConfig {
|
|
19
|
+
/** PSM server endpoint */
|
|
20
|
+
psmEndpoint?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Client for creating and loading multisig accounts.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```typescript
|
|
28
|
+
* import { MultisigClient, FalconSigner } from '@openzeppelin/miden-multisig-client';
|
|
29
|
+
* import { WebClient, SecretKey } from '@demox-labs/miden-sdk';
|
|
30
|
+
*
|
|
31
|
+
* // Initialize
|
|
32
|
+
* const webClient = await WebClient.createClient('https://rpc.testnet.miden.io:443');
|
|
33
|
+
* const secretKey = SecretKey.rpoFalconWithRNG(seed);
|
|
34
|
+
* const signer = new FalconSigner(secretKey);
|
|
35
|
+
*
|
|
36
|
+
* // Create client
|
|
37
|
+
* const client = new MultisigClient(webClient, { psmEndpoint: 'http://localhost:3000' });
|
|
38
|
+
*
|
|
39
|
+
* // Get PSM pubkey for config
|
|
40
|
+
* const psmCommitment = await client.psmClient.getPubkey();
|
|
41
|
+
*
|
|
42
|
+
* // Create multisig
|
|
43
|
+
* const config = { threshold: 2, signerCommitments: [...], psmCommitment };
|
|
44
|
+
* const multisig = await client.create(config, signer);
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export class MultisigClient {
|
|
48
|
+
private readonly webClient: WebClient;
|
|
49
|
+
private _psmClient: PsmHttpClient;
|
|
50
|
+
|
|
51
|
+
constructor(webClient: WebClient, config: MultisigClientConfig = {}) {
|
|
52
|
+
this.webClient = webClient;
|
|
53
|
+
this._psmClient = new PsmHttpClient(config.psmEndpoint ?? 'http://localhost:3000');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Change the PSM endpoint.
|
|
58
|
+
*
|
|
59
|
+
* @param endpoint - The new PSM server endpoint URL
|
|
60
|
+
*/
|
|
61
|
+
setPsmEndpoint(endpoint: string): void {
|
|
62
|
+
this._psmClient = new PsmHttpClient(endpoint);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Access the internal PSM client.
|
|
67
|
+
*/
|
|
68
|
+
get psmClient(): PsmHttpClient {
|
|
69
|
+
return this._psmClient;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Create a new multisig account.
|
|
74
|
+
*
|
|
75
|
+
* @param config - Multisig configuration (threshold, signers, PSM commitment)
|
|
76
|
+
* @param signer - The signer for this client (one of the cosigners)
|
|
77
|
+
* @returns A Multisig instance wrapping the created account
|
|
78
|
+
*/
|
|
79
|
+
async create(config: MultisigConfig, signer: Signer): Promise<Multisig> {
|
|
80
|
+
this._psmClient.setSigner(signer);
|
|
81
|
+
|
|
82
|
+
const { account } = await createMultisigAccount(this.webClient, config);
|
|
83
|
+
|
|
84
|
+
return new Multisig(account, config, this._psmClient, signer, this.webClient);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Load an existing multisig account from PSM.
|
|
89
|
+
*
|
|
90
|
+
* @param accountId - The account ID to load
|
|
91
|
+
* @param signer - The signer for this client
|
|
92
|
+
* @returns A Multisig instance for the loaded account
|
|
93
|
+
*/
|
|
94
|
+
async load(accountId: string, signer: Signer): Promise<Multisig> {
|
|
95
|
+
this._psmClient.setSigner(signer);
|
|
96
|
+
|
|
97
|
+
const stateResponse = await this._psmClient.getState(accountId);
|
|
98
|
+
|
|
99
|
+
const accountBase64 = stateResponse.stateJson.data;
|
|
100
|
+
if (!accountBase64) {
|
|
101
|
+
throw new Error('No account data found in PSM state');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const binaryString = atob(accountBase64);
|
|
105
|
+
const accountBytes = new Uint8Array(binaryString.length);
|
|
106
|
+
for (let i = 0; i < binaryString.length; i++) {
|
|
107
|
+
accountBytes[i] = binaryString.charCodeAt(i);
|
|
108
|
+
}
|
|
109
|
+
const account = Account.deserialize(accountBytes);
|
|
110
|
+
|
|
111
|
+
const detected = AccountInspector.fromAccount(account);
|
|
112
|
+
const config: MultisigConfig = {
|
|
113
|
+
threshold: detected.threshold,
|
|
114
|
+
signerCommitments: detected.signerCommitments,
|
|
115
|
+
psmCommitment: detected.psmCommitment ?? '',
|
|
116
|
+
psmEnabled: detected.psmEnabled,
|
|
117
|
+
procedureThresholds: Array.from(detected.procedureThresholds.entries()).map(
|
|
118
|
+
([procedure, threshold]) => ({ procedure, threshold })
|
|
119
|
+
),
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const existingAccount = await this.webClient.getAccount(AccountId.fromHex(accountId));
|
|
123
|
+
if (!existingAccount) {
|
|
124
|
+
await this.webClient.newAccount(account, true);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return new Multisig(null, config, this._psmClient, signer, this.webClient, accountId);
|
|
128
|
+
}
|
|
129
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @openzeppelin/miden-multisig-client
|
|
3
|
+
*
|
|
4
|
+
* TypeScript SDK for Miden multisig accounts with PSM (Private State Manager) integration.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```typescript
|
|
8
|
+
* import {
|
|
9
|
+
* MultisigClient,
|
|
10
|
+
* FalconSigner,
|
|
11
|
+
* } from '@openzeppelin/miden-multisig-client';
|
|
12
|
+
* import { WebClient, SecretKey } from '@demox-labs/miden-sdk';
|
|
13
|
+
*
|
|
14
|
+
* // Initialize WebClient
|
|
15
|
+
* const webClient = await WebClient.createClient('https://rpc.testnet.miden.io:443');
|
|
16
|
+
* await webClient.syncState();
|
|
17
|
+
*
|
|
18
|
+
* // Generate a key dynamically
|
|
19
|
+
* const seed = new Uint8Array(32);
|
|
20
|
+
* crypto.getRandomValues(seed);
|
|
21
|
+
* const secretKey = SecretKey.rpoFalconWithRNG(seed);
|
|
22
|
+
*
|
|
23
|
+
* // Store in miden-sdk's keystore
|
|
24
|
+
* await webClient.addAccountSecretKeyToWebStore(secretKey);
|
|
25
|
+
*
|
|
26
|
+
* // Create a signer
|
|
27
|
+
* const signer = new FalconSigner(secretKey);
|
|
28
|
+
*
|
|
29
|
+
* // Create multisig client
|
|
30
|
+
* const client = new MultisigClient(webClient, { psmEndpoint: 'http://localhost:3000' });
|
|
31
|
+
*
|
|
32
|
+
* // Get PSM pubkey for config
|
|
33
|
+
* const psmCommitment = await client.psmClient.getPubkey();
|
|
34
|
+
*
|
|
35
|
+
* // Create multisig account
|
|
36
|
+
* const config = { threshold: 2, signerCommitments: [signer.commitment, ...], psmCommitment };
|
|
37
|
+
* const multisig = await client.create(config, signer);
|
|
38
|
+
*
|
|
39
|
+
* // Register on PSM and work with proposals
|
|
40
|
+
* await multisig.registerOnPsm();
|
|
41
|
+
* await multisig.syncProposals();
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
export { MultisigClient, type MultisigClientConfig } from './client.js';
|
|
46
|
+
export { Multisig, type AccountState } from './multisig.js';
|
|
47
|
+
export { AccountInspector, type DetectedMultisigConfig, type VaultBalance } from './inspector.js';
|
|
48
|
+
export {
|
|
49
|
+
executeForSummary,
|
|
50
|
+
buildUpdateSignersTransactionRequest,
|
|
51
|
+
buildUpdatePsmTransactionRequest,
|
|
52
|
+
buildConsumeNotesTransactionRequest,
|
|
53
|
+
buildP2idTransactionRequest,
|
|
54
|
+
} from './transaction.js';
|
|
55
|
+
|
|
56
|
+
export { PsmHttpClient, PsmHttpError } from '@openzeppelin/psm-client';
|
|
57
|
+
|
|
58
|
+
export { FalconSigner } from './signer.js';
|
|
59
|
+
|
|
60
|
+
export {
|
|
61
|
+
createMultisigAccount,
|
|
62
|
+
validateMultisigConfig,
|
|
63
|
+
buildMultisigStorageSlots,
|
|
64
|
+
buildPsmStorageSlots,
|
|
65
|
+
storageLayoutBuilder,
|
|
66
|
+
StorageLayoutBuilder,
|
|
67
|
+
} from './account/index.js';
|
|
68
|
+
|
|
69
|
+
export {
|
|
70
|
+
PROCEDURE_ROOTS,
|
|
71
|
+
getProcedureRoot,
|
|
72
|
+
isProcedureName,
|
|
73
|
+
getProcedureNames,
|
|
74
|
+
type ProcedureName,
|
|
75
|
+
} from './procedures.js';
|
|
76
|
+
|
|
77
|
+
export type {
|
|
78
|
+
// Account types
|
|
79
|
+
MultisigAccountState,
|
|
80
|
+
MultisigConfig,
|
|
81
|
+
CreateAccountResult,
|
|
82
|
+
ProcedureThreshold,
|
|
83
|
+
|
|
84
|
+
// Proposal types
|
|
85
|
+
Proposal,
|
|
86
|
+
ProposalStatus,
|
|
87
|
+
ProposalSignatureEntry,
|
|
88
|
+
ProposalMetadata,
|
|
89
|
+
ProposalType,
|
|
90
|
+
ExportedProposal,
|
|
91
|
+
|
|
92
|
+
// Transaction types
|
|
93
|
+
TransactionType,
|
|
94
|
+
|
|
95
|
+
// Note types
|
|
96
|
+
ConsumableNote,
|
|
97
|
+
NoteAsset,
|
|
98
|
+
|
|
99
|
+
// Signature types
|
|
100
|
+
FalconSignature,
|
|
101
|
+
Signer,
|
|
102
|
+
|
|
103
|
+
// PSM API types
|
|
104
|
+
AuthConfig,
|
|
105
|
+
DeltaObject,
|
|
106
|
+
DeltaStatus,
|
|
107
|
+
StateObject,
|
|
108
|
+
CosignerSignature,
|
|
109
|
+
|
|
110
|
+
// Request/Response types
|
|
111
|
+
ConfigureRequest,
|
|
112
|
+
ConfigureResponse,
|
|
113
|
+
DeltaProposalRequest,
|
|
114
|
+
DeltaProposalResponse,
|
|
115
|
+
ProposalsResponse,
|
|
116
|
+
PubkeyResponse,
|
|
117
|
+
SignProposalRequest,
|
|
118
|
+
} from './types.js';
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { AccountInspector } from './inspector.js';
|
|
3
|
+
|
|
4
|
+
// Mock the Miden SDK
|
|
5
|
+
vi.mock('@demox-labs/miden-sdk', () => {
|
|
6
|
+
const createMockWord = (values: bigint[]) => ({
|
|
7
|
+
toU64s: () => values,
|
|
8
|
+
toHex: () => '0x' + values.map(v => v.toString(16).padStart(16, '0')).join(''),
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
const createMockStorage = (slots: Map<number, any>, maps: Map<number, Map<string, any>>) => ({
|
|
12
|
+
getItem: (index: number) => slots.get(index) ?? createMockWord([0n, 0n, 0n, 0n]),
|
|
13
|
+
getMapItem: (slotIndex: number, key: any) => {
|
|
14
|
+
const map = maps.get(slotIndex);
|
|
15
|
+
if (!map) throw new Error('Map not found');
|
|
16
|
+
const keyStr = key.toU64s?.()[0]?.toString() ?? '0';
|
|
17
|
+
const value = map.get(keyStr);
|
|
18
|
+
if (!value) throw new Error('Key not found');
|
|
19
|
+
return value;
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const createMockVault = (assets: Array<{ faucetId: string; amount: bigint }>) => ({
|
|
24
|
+
fungibleAssets: () => assets.map(a => ({
|
|
25
|
+
faucetId: () => ({ toString: () => a.faucetId }),
|
|
26
|
+
amount: () => a.amount,
|
|
27
|
+
})),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
Account: {
|
|
32
|
+
deserialize: vi.fn((bytes: Uint8Array) => {
|
|
33
|
+
// Return different mocked accounts based on test scenario
|
|
34
|
+
// Default: 2-of-3 multisig with PSM enabled
|
|
35
|
+
const slot0 = createMockWord([2n, 3n, 0n, 0n]); // threshold=2, numSigners=3
|
|
36
|
+
const slot4 = createMockWord([1n, 0n, 0n, 0n]); // PSM enabled
|
|
37
|
+
|
|
38
|
+
const signerMap = new Map<string, any>();
|
|
39
|
+
signerMap.set('0', createMockWord([BigInt('0x1111111111111111'), BigInt('0x2222222222222222'), BigInt('0x3333333333333333'), BigInt('0x4444444444444444')]));
|
|
40
|
+
signerMap.set('1', createMockWord([BigInt('0x5555555555555555'), BigInt('0x6666666666666666'), BigInt('0x7777777777777777'), BigInt('0x8888888888888888')]));
|
|
41
|
+
signerMap.set('2', createMockWord([BigInt('0xaaaaaaaaaaaaaaaa'), BigInt('0xbbbbbbbbbbbbbbbb'), BigInt('0xcccccccccccccccc'), BigInt('0xdddddddddddddddd')]));
|
|
42
|
+
|
|
43
|
+
const psmMap = new Map<string, any>();
|
|
44
|
+
psmMap.set('0', createMockWord([BigInt('0xeeeeeeeeeeeeeeee'), BigInt('0xffffffffffffffff'), BigInt('0x0000000000000001'), BigInt('0x0000000000000002')]));
|
|
45
|
+
|
|
46
|
+
const slots = new Map<number, any>();
|
|
47
|
+
slots.set(0, slot0);
|
|
48
|
+
slots.set(4, slot4);
|
|
49
|
+
|
|
50
|
+
const maps = new Map<number, Map<string, any>>();
|
|
51
|
+
maps.set(1, signerMap);
|
|
52
|
+
maps.set(5, psmMap);
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
storage: () => createMockStorage(slots, maps),
|
|
56
|
+
vault: () => createMockVault([
|
|
57
|
+
{ faucetId: '0xfaucet1', amount: 1000n },
|
|
58
|
+
{ faucetId: '0xfaucet2', amount: 500n },
|
|
59
|
+
]),
|
|
60
|
+
};
|
|
61
|
+
}),
|
|
62
|
+
},
|
|
63
|
+
Word: vi.fn().mockImplementation((arr: BigUint64Array) => ({
|
|
64
|
+
toU64s: () => Array.from(arr),
|
|
65
|
+
toHex: () => '0x' + Array.from(arr).map(v => v.toString(16).padStart(16, '0')).join(''),
|
|
66
|
+
})),
|
|
67
|
+
};
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe('AccountInspector', () => {
|
|
71
|
+
beforeEach(() => {
|
|
72
|
+
vi.clearAllMocks();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe('fromBase64', () => {
|
|
76
|
+
it('deserializes account from base64 and extracts config', () => {
|
|
77
|
+
const base64 = btoa(String.fromCharCode(...new Uint8Array([1, 2, 3])));
|
|
78
|
+
const config = AccountInspector.fromBase64(base64);
|
|
79
|
+
|
|
80
|
+
expect(config.threshold).toBe(2);
|
|
81
|
+
expect(config.numSigners).toBe(3);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('extracts PSM status', () => {
|
|
85
|
+
const base64 = btoa(String.fromCharCode(...new Uint8Array([1, 2, 3])));
|
|
86
|
+
const config = AccountInspector.fromBase64(base64);
|
|
87
|
+
|
|
88
|
+
expect(config.psmEnabled).toBe(true);
|
|
89
|
+
expect(config.psmCommitment).toMatch(/^0x[a-f0-9]+$/);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('extracts signer commitments', () => {
|
|
93
|
+
const base64 = btoa(String.fromCharCode(...new Uint8Array([1, 2, 3])));
|
|
94
|
+
const config = AccountInspector.fromBase64(base64);
|
|
95
|
+
|
|
96
|
+
expect(config.signerCommitments).toHaveLength(3);
|
|
97
|
+
config.signerCommitments.forEach(commitment => {
|
|
98
|
+
expect(commitment).toMatch(/^0x[a-f0-9]+$/);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('extracts vault balances', () => {
|
|
103
|
+
const base64 = btoa(String.fromCharCode(...new Uint8Array([1, 2, 3])));
|
|
104
|
+
const config = AccountInspector.fromBase64(base64);
|
|
105
|
+
|
|
106
|
+
expect(config.vaultBalances).toHaveLength(2);
|
|
107
|
+
expect(config.vaultBalances[0]).toEqual({ faucetId: '0xfaucet1', amount: 1000n });
|
|
108
|
+
expect(config.vaultBalances[1]).toEqual({ faucetId: '0xfaucet2', amount: 500n });
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe('fromAccount', () => {
|
|
113
|
+
it('extracts threshold from slot 0', async () => {
|
|
114
|
+
const { Account } = await import('@demox-labs/miden-sdk');
|
|
115
|
+
const account = Account.deserialize(new Uint8Array([1, 2, 3]));
|
|
116
|
+
const config = AccountInspector.fromAccount(account);
|
|
117
|
+
|
|
118
|
+
expect(config.threshold).toBe(2);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('extracts numSigners from slot 0', async () => {
|
|
122
|
+
const { Account } = await import('@demox-labs/miden-sdk');
|
|
123
|
+
const account = Account.deserialize(new Uint8Array([1, 2, 3]));
|
|
124
|
+
const config = AccountInspector.fromAccount(account);
|
|
125
|
+
|
|
126
|
+
expect(config.numSigners).toBe(3);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe('AccountInspector edge cases', () => {
|
|
132
|
+
beforeEach(() => {
|
|
133
|
+
vi.clearAllMocks();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('handles account with PSM disabled', async () => {
|
|
137
|
+
const { Account } = await import('@demox-labs/miden-sdk');
|
|
138
|
+
|
|
139
|
+
// Override mock for this test
|
|
140
|
+
vi.mocked(Account.deserialize).mockReturnValueOnce({
|
|
141
|
+
storage: () => ({
|
|
142
|
+
getItem: (index: number) => {
|
|
143
|
+
if (index === 0) return { toU64s: () => [1n, 1n, 0n, 0n] };
|
|
144
|
+
if (index === 4) return { toU64s: () => [0n, 0n, 0n, 0n] }; // PSM disabled
|
|
145
|
+
return { toU64s: () => [0n, 0n, 0n, 0n] };
|
|
146
|
+
},
|
|
147
|
+
getMapItem: (slotIndex: number, key: any) => {
|
|
148
|
+
if (slotIndex === 1 && key.toU64s?.()[0] === 0n) {
|
|
149
|
+
return {
|
|
150
|
+
toHex: () => '0x' + 'a'.repeat(64),
|
|
151
|
+
toU64s: () => [1n, 2n, 3n, 4n],
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
throw new Error('Not found');
|
|
155
|
+
},
|
|
156
|
+
}),
|
|
157
|
+
vault: () => ({
|
|
158
|
+
fungibleAssets: () => [],
|
|
159
|
+
}),
|
|
160
|
+
} as any);
|
|
161
|
+
|
|
162
|
+
const account = Account.deserialize(new Uint8Array([1, 2, 3]));
|
|
163
|
+
const config = AccountInspector.fromAccount(account);
|
|
164
|
+
|
|
165
|
+
expect(config.psmEnabled).toBe(false);
|
|
166
|
+
expect(config.psmCommitment).toBeNull();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('handles account with empty vault', async () => {
|
|
170
|
+
const { Account } = await import('@demox-labs/miden-sdk');
|
|
171
|
+
|
|
172
|
+
vi.mocked(Account.deserialize).mockReturnValueOnce({
|
|
173
|
+
storage: () => ({
|
|
174
|
+
getItem: () => ({ toU64s: () => [1n, 1n, 0n, 0n] }),
|
|
175
|
+
getMapItem: () => {
|
|
176
|
+
throw new Error('Not found');
|
|
177
|
+
},
|
|
178
|
+
}),
|
|
179
|
+
vault: () => ({
|
|
180
|
+
fungibleAssets: () => [],
|
|
181
|
+
}),
|
|
182
|
+
} as any);
|
|
183
|
+
|
|
184
|
+
const account = Account.deserialize(new Uint8Array([1, 2, 3]));
|
|
185
|
+
const config = AccountInspector.fromAccount(account);
|
|
186
|
+
|
|
187
|
+
expect(config.vaultBalances).toEqual([]);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('handles missing signer map entries gracefully', async () => {
|
|
191
|
+
const { Account } = await import('@demox-labs/miden-sdk');
|
|
192
|
+
|
|
193
|
+
vi.mocked(Account.deserialize).mockReturnValueOnce({
|
|
194
|
+
storage: () => ({
|
|
195
|
+
getItem: (index: number) => {
|
|
196
|
+
if (index === 0) return { toU64s: () => [2n, 5n, 0n, 0n] }; // threshold=2, numSigners=5
|
|
197
|
+
if (index === 4) return { toU64s: () => [0n, 0n, 0n, 0n] };
|
|
198
|
+
return { toU64s: () => [0n, 0n, 0n, 0n] };
|
|
199
|
+
},
|
|
200
|
+
getMapItem: () => {
|
|
201
|
+
throw new Error('Map entry not found');
|
|
202
|
+
},
|
|
203
|
+
}),
|
|
204
|
+
vault: () => ({
|
|
205
|
+
fungibleAssets: () => [],
|
|
206
|
+
}),
|
|
207
|
+
} as any);
|
|
208
|
+
|
|
209
|
+
const account = Account.deserialize(new Uint8Array([1, 2, 3]));
|
|
210
|
+
const config = AccountInspector.fromAccount(account);
|
|
211
|
+
|
|
212
|
+
// Should gracefully handle missing entries
|
|
213
|
+
expect(config.numSigners).toBe(5);
|
|
214
|
+
expect(config.signerCommitments).toEqual([]); // All entries missing
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('handles vault access error gracefully', async () => {
|
|
218
|
+
const { Account } = await import('@demox-labs/miden-sdk');
|
|
219
|
+
|
|
220
|
+
vi.mocked(Account.deserialize).mockReturnValueOnce({
|
|
221
|
+
storage: () => ({
|
|
222
|
+
getItem: () => ({ toU64s: () => [1n, 1n, 0n, 0n] }),
|
|
223
|
+
getMapItem: () => {
|
|
224
|
+
throw new Error('Not found');
|
|
225
|
+
},
|
|
226
|
+
}),
|
|
227
|
+
vault: () => {
|
|
228
|
+
throw new Error('Vault access failed');
|
|
229
|
+
},
|
|
230
|
+
} as any);
|
|
231
|
+
|
|
232
|
+
const account = Account.deserialize(new Uint8Array([1, 2, 3]));
|
|
233
|
+
const config = AccountInspector.fromAccount(account);
|
|
234
|
+
|
|
235
|
+
expect(config.vaultBalances).toEqual([]);
|
|
236
|
+
});
|
|
237
|
+
});
|