@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.
Files changed (215) hide show
  1. package/README.md +189 -0
  2. package/dist/account/builder.d.ts +23 -0
  3. package/dist/account/builder.d.ts.map +1 -0
  4. package/dist/account/builder.js +85 -0
  5. package/dist/account/builder.js.map +1 -0
  6. package/dist/account/index.d.ts +6 -0
  7. package/dist/account/index.d.ts.map +1 -0
  8. package/dist/account/index.js +6 -0
  9. package/dist/account/index.js.map +1 -0
  10. package/dist/account/masm.d.ts +3 -0
  11. package/dist/account/masm.d.ts.map +1 -0
  12. package/dist/account/masm.js +605 -0
  13. package/dist/account/masm.js.map +1 -0
  14. package/dist/account/storage.d.ts +10 -0
  15. package/dist/account/storage.d.ts.map +1 -0
  16. package/dist/account/storage.js +56 -0
  17. package/dist/account/storage.js.map +1 -0
  18. package/dist/account.d.ts +104 -0
  19. package/dist/account.d.ts.map +1 -0
  20. package/dist/account.js +136 -0
  21. package/dist/account.js.map +1 -0
  22. package/dist/builder.d.ts +54 -0
  23. package/dist/builder.d.ts.map +1 -0
  24. package/dist/builder.js +78 -0
  25. package/dist/builder.js.map +1 -0
  26. package/dist/client.d.ts +73 -0
  27. package/dist/client.d.ts.map +1 -0
  28. package/dist/client.js +104 -0
  29. package/dist/client.js.map +1 -0
  30. package/dist/client.test.d.ts +2 -0
  31. package/dist/client.test.d.ts.map +1 -0
  32. package/dist/client.test.js +150 -0
  33. package/dist/client.test.js.map +1 -0
  34. package/dist/http.d.ts +64 -0
  35. package/dist/http.d.ts.map +1 -0
  36. package/dist/http.js +162 -0
  37. package/dist/http.js.map +1 -0
  38. package/dist/index.d.ts +53 -0
  39. package/dist/index.d.ts.map +1 -0
  40. package/dist/index.js +52 -0
  41. package/dist/index.js.map +1 -0
  42. package/dist/inspector.d.ts +49 -0
  43. package/dist/inspector.d.ts.map +1 -0
  44. package/dist/inspector.js +120 -0
  45. package/dist/inspector.js.map +1 -0
  46. package/dist/inspector.test.d.ts +2 -0
  47. package/dist/inspector.test.d.ts.map +1 -0
  48. package/dist/inspector.test.js +205 -0
  49. package/dist/inspector.test.js.map +1 -0
  50. package/dist/keystore.d.ts +65 -0
  51. package/dist/keystore.d.ts.map +1 -0
  52. package/dist/keystore.js +121 -0
  53. package/dist/keystore.js.map +1 -0
  54. package/dist/miden.d.ts +9 -0
  55. package/dist/miden.d.ts.map +1 -0
  56. package/dist/miden.js +26 -0
  57. package/dist/miden.js.map +1 -0
  58. package/dist/multisig/helpers.d.ts +4 -0
  59. package/dist/multisig/helpers.d.ts.map +1 -0
  60. package/dist/multisig/helpers.js +21 -0
  61. package/dist/multisig/helpers.js.map +1 -0
  62. package/dist/multisig/helpers.test.d.ts +2 -0
  63. package/dist/multisig/helpers.test.d.ts.map +1 -0
  64. package/dist/multisig/helpers.test.js +94 -0
  65. package/dist/multisig/helpers.test.js.map +1 -0
  66. package/dist/multisig/metadata.d.ts +18 -0
  67. package/dist/multisig/metadata.d.ts.map +1 -0
  68. package/dist/multisig/metadata.js +100 -0
  69. package/dist/multisig/metadata.js.map +1 -0
  70. package/dist/multisig/metadata.test.d.ts +2 -0
  71. package/dist/multisig/metadata.test.d.ts.map +1 -0
  72. package/dist/multisig/metadata.test.js +323 -0
  73. package/dist/multisig/metadata.test.js.map +1 -0
  74. package/dist/multisig.d.ts +199 -0
  75. package/dist/multisig.d.ts.map +1 -0
  76. package/dist/multisig.js +821 -0
  77. package/dist/multisig.js.map +1 -0
  78. package/dist/multisig.test.d.ts +2 -0
  79. package/dist/multisig.test.d.ts.map +1 -0
  80. package/dist/multisig.test.js +1121 -0
  81. package/dist/multisig.test.js.map +1 -0
  82. package/dist/procedures.d.ts +57 -0
  83. package/dist/procedures.d.ts.map +1 -0
  84. package/dist/procedures.js +62 -0
  85. package/dist/procedures.js.map +1 -0
  86. package/dist/signer.d.ts +36 -0
  87. package/dist/signer.d.ts.map +1 -0
  88. package/dist/signer.js +68 -0
  89. package/dist/signer.js.map +1 -0
  90. package/dist/signer.test.d.ts +2 -0
  91. package/dist/signer.test.d.ts.map +1 -0
  92. package/dist/signer.test.js +117 -0
  93. package/dist/signer.test.js.map +1 -0
  94. package/dist/transaction/consumeNotes.d.ts +7 -0
  95. package/dist/transaction/consumeNotes.d.ts.map +1 -0
  96. package/dist/transaction/consumeNotes.js +28 -0
  97. package/dist/transaction/consumeNotes.js.map +1 -0
  98. package/dist/transaction/index.d.ts +7 -0
  99. package/dist/transaction/index.d.ts.map +1 -0
  100. package/dist/transaction/index.js +6 -0
  101. package/dist/transaction/index.js.map +1 -0
  102. package/dist/transaction/options.d.ts +6 -0
  103. package/dist/transaction/options.d.ts.map +1 -0
  104. package/dist/transaction/options.js +2 -0
  105. package/dist/transaction/options.js.map +1 -0
  106. package/dist/transaction/p2id.d.ts +7 -0
  107. package/dist/transaction/p2id.d.ts.map +1 -0
  108. package/dist/transaction/p2id.js +43 -0
  109. package/dist/transaction/p2id.js.map +1 -0
  110. package/dist/transaction/summary.d.ts +3 -0
  111. package/dist/transaction/summary.d.ts.map +1 -0
  112. package/dist/transaction/summary.js +6 -0
  113. package/dist/transaction/summary.js.map +1 -0
  114. package/dist/transaction/updatePsm.d.ts +7 -0
  115. package/dist/transaction/updatePsm.d.ts.map +1 -0
  116. package/dist/transaction/updatePsm.js +43 -0
  117. package/dist/transaction/updatePsm.js.map +1 -0
  118. package/dist/transaction/updateSigners.d.ts +8 -0
  119. package/dist/transaction/updateSigners.d.ts.map +1 -0
  120. package/dist/transaction/updateSigners.js +60 -0
  121. package/dist/transaction/updateSigners.js.map +1 -0
  122. package/dist/transaction.d.ts +6 -0
  123. package/dist/transaction.d.ts.map +1 -0
  124. package/dist/transaction.js +8 -0
  125. package/dist/transaction.js.map +1 -0
  126. package/dist/transaction.test.d.ts +2 -0
  127. package/dist/transaction.test.d.ts.map +1 -0
  128. package/dist/transaction.test.js +117 -0
  129. package/dist/transaction.test.js.map +1 -0
  130. package/dist/transport/http.d.ts +64 -0
  131. package/dist/transport/http.d.ts.map +1 -0
  132. package/dist/transport/http.js +162 -0
  133. package/dist/transport/http.js.map +1 -0
  134. package/dist/transport/index.d.ts +5 -0
  135. package/dist/transport/index.d.ts.map +1 -0
  136. package/dist/transport/index.js +5 -0
  137. package/dist/transport/index.js.map +1 -0
  138. package/dist/types/proposal.d.ts +74 -0
  139. package/dist/types/proposal.d.ts.map +1 -0
  140. package/dist/types/proposal.js +2 -0
  141. package/dist/types/proposal.js.map +1 -0
  142. package/dist/types.d.ts +60 -0
  143. package/dist/types.d.ts.map +1 -0
  144. package/dist/types.js +2 -0
  145. package/dist/types.js.map +1 -0
  146. package/dist/utils/encoding.d.ts +7 -0
  147. package/dist/utils/encoding.d.ts.map +1 -0
  148. package/dist/utils/encoding.js +54 -0
  149. package/dist/utils/encoding.js.map +1 -0
  150. package/dist/utils/encoding.test.d.ts +2 -0
  151. package/dist/utils/encoding.test.d.ts.map +1 -0
  152. package/dist/utils/encoding.test.js +185 -0
  153. package/dist/utils/encoding.test.js.map +1 -0
  154. package/dist/utils/index.d.ts +5 -0
  155. package/dist/utils/index.d.ts.map +1 -0
  156. package/dist/utils/index.js +5 -0
  157. package/dist/utils/index.js.map +1 -0
  158. package/dist/utils/random.d.ts +3 -0
  159. package/dist/utils/random.d.ts.map +1 -0
  160. package/dist/utils/random.js +14 -0
  161. package/dist/utils/random.js.map +1 -0
  162. package/dist/utils/signature.d.ts +12 -0
  163. package/dist/utils/signature.d.ts.map +1 -0
  164. package/dist/utils/signature.js +28 -0
  165. package/dist/utils/signature.js.map +1 -0
  166. package/dist/utils/signature.test.d.ts +2 -0
  167. package/dist/utils/signature.test.d.ts.map +1 -0
  168. package/dist/utils/signature.test.js +149 -0
  169. package/dist/utils/signature.test.js.map +1 -0
  170. package/dist/utils/word.d.ts +4 -0
  171. package/dist/utils/word.d.ts.map +1 -0
  172. package/dist/utils/word.js +8 -0
  173. package/dist/utils/word.js.map +1 -0
  174. package/dist/utils/word.test.d.ts +2 -0
  175. package/dist/utils/word.test.d.ts.map +1 -0
  176. package/dist/utils/word.test.js +54 -0
  177. package/dist/utils/word.test.js.map +1 -0
  178. package/masm/multisig.masm +423 -0
  179. package/masm/psm.masm +178 -0
  180. package/package.json +42 -0
  181. package/src/account/builder.ts +112 -0
  182. package/src/account/index.ts +15 -0
  183. package/src/account/masm.ts +606 -0
  184. package/src/account/storage.ts +70 -0
  185. package/src/client.test.ts +177 -0
  186. package/src/client.ts +129 -0
  187. package/src/index.ts +118 -0
  188. package/src/inspector.test.ts +237 -0
  189. package/src/inspector.ts +142 -0
  190. package/src/multisig/helpers.test.ts +114 -0
  191. package/src/multisig/helpers.ts +23 -0
  192. package/src/multisig.test.ts +1294 -0
  193. package/src/multisig.ts +1097 -0
  194. package/src/procedures.ts +72 -0
  195. package/src/signer.test.ts +135 -0
  196. package/src/signer.ts +75 -0
  197. package/src/transaction/consumeNotes.ts +41 -0
  198. package/src/transaction/index.ts +7 -0
  199. package/src/transaction/options.ts +7 -0
  200. package/src/transaction/p2id.ts +105 -0
  201. package/src/transaction/summary.ts +12 -0
  202. package/src/transaction/updatePsm.ts +69 -0
  203. package/src/transaction/updateSigners.ts +96 -0
  204. package/src/transaction.test.ts +136 -0
  205. package/src/transaction.ts +16 -0
  206. package/src/types/proposal.ts +80 -0
  207. package/src/types.ts +81 -0
  208. package/src/utils/encoding.test.ts +233 -0
  209. package/src/utils/encoding.ts +60 -0
  210. package/src/utils/index.ts +5 -0
  211. package/src/utils/random.ts +15 -0
  212. package/src/utils/signature.test.ts +187 -0
  213. package/src/utils/signature.ts +36 -0
  214. package/src/utils/word.test.ts +64 -0
  215. package/src/utils/word.ts +11 -0
@@ -0,0 +1,1121 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { Multisig } from './multisig.js';
3
+ import { PsmHttpClient } from '@openzeppelin/psm-client';
4
+ // Mock the Miden SDK
5
+ vi.mock('@demox-labs/miden-sdk', () => ({
6
+ AccountId: {
7
+ fromHex: vi.fn((hex) => ({ toString: () => hex })),
8
+ },
9
+ TransactionSummary: {
10
+ deserialize: vi.fn().mockReturnValue({
11
+ toCommitment: () => ({
12
+ toHex: () => '0x' + 'c'.repeat(64),
13
+ }),
14
+ salt: () => ({
15
+ toHex: () => '0x' + 'd'.repeat(64),
16
+ }),
17
+ serialize: () => new Uint8Array([1, 2, 3]),
18
+ }),
19
+ },
20
+ Word: {
21
+ fromHex: vi.fn((hex) => ({
22
+ toHex: () => hex,
23
+ toFelts: () => [1, 2, 3, 4],
24
+ })),
25
+ },
26
+ Signature: {
27
+ deserialize: vi.fn().mockReturnValue({
28
+ toPreparedSignature: () => [1, 2, 3],
29
+ }),
30
+ },
31
+ AdviceMap: vi.fn().mockImplementation(() => ({
32
+ insert: vi.fn(),
33
+ })),
34
+ FeltArray: vi.fn().mockImplementation((arr) => arr),
35
+ Rpo256: {
36
+ hashElements: vi.fn().mockReturnValue({
37
+ toHex: () => '0x' + 'e'.repeat(64),
38
+ }),
39
+ },
40
+ }));
41
+ // Mock transaction module
42
+ vi.mock('./transaction.js', () => ({
43
+ executeForSummary: vi.fn(),
44
+ buildUpdateSignersTransactionRequest: vi.fn().mockResolvedValue({
45
+ request: {},
46
+ salt: { toHex: () => '0x' + 'd'.repeat(64) },
47
+ configHash: { toHex: () => '0x' + 'e'.repeat(64) },
48
+ }),
49
+ buildUpdatePsmTransactionRequest: vi.fn().mockResolvedValue({
50
+ request: {},
51
+ salt: { toHex: () => '0x' + 'd'.repeat(64) },
52
+ }),
53
+ buildConsumeNotesTransactionRequest: vi.fn().mockReturnValue({
54
+ request: {},
55
+ salt: { toHex: () => '0x' + 'd'.repeat(64) },
56
+ }),
57
+ buildP2idTransactionRequest: vi.fn().mockReturnValue({
58
+ request: {},
59
+ salt: { toHex: () => '0x' + 'd'.repeat(64) },
60
+ }),
61
+ }));
62
+ vi.mock('./utils/signature.js', () => ({
63
+ buildSignatureAdviceEntry: vi.fn().mockReturnValue({
64
+ key: { toHex: () => '0x' + 'f'.repeat(64) },
65
+ values: [1, 2, 3],
66
+ }),
67
+ signatureHexToBytes: vi.fn((hex) => new Uint8Array([0, 1, 2, 3])),
68
+ }));
69
+ vi.mock('./utils/encoding.js', async () => {
70
+ const actual = await vi.importActual('./utils/encoding.js');
71
+ return {
72
+ ...actual,
73
+ normalizeHexWord: vi.fn((hex) => '0x' + hex.replace(/^0x/i, '').padStart(64, '0')),
74
+ };
75
+ });
76
+ // Mock fetch for PSM client
77
+ const mockFetch = vi.fn();
78
+ vi.stubGlobal('fetch', mockFetch);
79
+ describe('Multisig', () => {
80
+ let psm;
81
+ let mockSigner;
82
+ let mockAccount;
83
+ let mockWebClient;
84
+ beforeEach(() => {
85
+ mockFetch.mockReset();
86
+ psm = new PsmHttpClient('http://localhost:3000');
87
+ mockSigner = {
88
+ commitment: '0x' + '1'.repeat(64),
89
+ publicKey: '0x' + '2'.repeat(64),
90
+ signAccountIdWithTimestamp: vi.fn().mockReturnValue('0x' + 'a'.repeat(128)),
91
+ signCommitment: vi.fn().mockReturnValue('0x' + 'b'.repeat(128)),
92
+ };
93
+ psm.setSigner(mockSigner);
94
+ mockAccount = {
95
+ id: () => ({
96
+ toString: () => '0x' + 'a'.repeat(30),
97
+ prefix: () => ({ asInt: () => BigInt(1) }),
98
+ suffix: () => ({ asInt: () => BigInt(2) }),
99
+ }),
100
+ serialize: () => new Uint8Array([1, 2, 3]),
101
+ };
102
+ mockWebClient = {
103
+ executeTransaction: vi.fn(),
104
+ proveTransaction: vi.fn(),
105
+ submitProvenTransaction: vi.fn(),
106
+ applyTransaction: vi.fn(),
107
+ getConsumableNotes: vi.fn().mockResolvedValue([]),
108
+ syncState: vi.fn(),
109
+ };
110
+ });
111
+ describe('constructor', () => {
112
+ it('should create Multisig with account', () => {
113
+ const config = {
114
+ threshold: 2,
115
+ signerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
116
+ psmCommitment: '0x' + 'c'.repeat(64),
117
+ };
118
+ const multisig = new Multisig(mockAccount, config, psm, mockSigner, mockWebClient);
119
+ expect(multisig.threshold).toBe(2);
120
+ expect(multisig.signerCommitments).toEqual(config.signerCommitments);
121
+ expect(multisig.psmCommitment).toBe(config.psmCommitment);
122
+ expect(multisig.account).toBe(mockAccount);
123
+ });
124
+ it('should create Multisig without account (loaded)', () => {
125
+ const config = {
126
+ threshold: 2,
127
+ signerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
128
+ psmCommitment: '0x' + 'c'.repeat(64),
129
+ };
130
+ const accountId = '0x' + 'd'.repeat(30);
131
+ const multisig = new Multisig(null, config, psm, mockSigner, mockWebClient, accountId);
132
+ expect(multisig.account).toBeNull();
133
+ expect(multisig.accountId).toBe(accountId);
134
+ });
135
+ });
136
+ describe('accountId', () => {
137
+ it('should return account ID from account', () => {
138
+ const config = {
139
+ threshold: 1,
140
+ signerCommitments: ['0x' + 'a'.repeat(64)],
141
+ psmCommitment: '0x' + 'c'.repeat(64),
142
+ };
143
+ const multisig = new Multisig(mockAccount, config, psm, mockSigner, mockWebClient);
144
+ expect(multisig.accountId).toBe('0x' + 'a'.repeat(30));
145
+ });
146
+ it('should return provided account ID for loaded multisig', () => {
147
+ const config = {
148
+ threshold: 1,
149
+ signerCommitments: ['0x' + 'a'.repeat(64)],
150
+ psmCommitment: '0x' + 'c'.repeat(64),
151
+ };
152
+ const accountId = '0x' + 'e'.repeat(30);
153
+ const multisig = new Multisig(null, config, psm, mockSigner, mockWebClient, accountId);
154
+ expect(multisig.accountId).toBe(accountId);
155
+ });
156
+ });
157
+ describe('signerCommitment', () => {
158
+ it('should return signer commitment', () => {
159
+ const config = {
160
+ threshold: 1,
161
+ signerCommitments: ['0x' + 'a'.repeat(64)],
162
+ psmCommitment: '0x' + 'c'.repeat(64),
163
+ };
164
+ const multisig = new Multisig(mockAccount, config, psm, mockSigner, mockWebClient);
165
+ expect(multisig.signerCommitment).toBe(mockSigner.commitment);
166
+ });
167
+ });
168
+ describe('fetchState', () => {
169
+ it('should fetch account state from PSM', async () => {
170
+ const config = {
171
+ threshold: 1,
172
+ signerCommitments: ['0x' + 'a'.repeat(64)],
173
+ psmCommitment: '0x' + 'c'.repeat(64),
174
+ };
175
+ const multisig = new Multisig(mockAccount, config, psm, mockSigner, mockWebClient);
176
+ mockFetch.mockResolvedValueOnce({
177
+ ok: true,
178
+ json: async () => ({
179
+ account_id: '0x' + 'a'.repeat(30),
180
+ commitment: '0x' + 'b'.repeat(64),
181
+ state_json: { data: 'base64state' },
182
+ created_at: '2024-01-01T00:00:00Z',
183
+ updated_at: '2024-01-02T00:00:00Z',
184
+ }),
185
+ });
186
+ const state = await multisig.fetchState();
187
+ expect(state.accountId).toBe('0x' + 'a'.repeat(30));
188
+ expect(state.commitment).toBe('0x' + 'b'.repeat(64));
189
+ expect(state.stateDataBase64).toBe('base64state');
190
+ });
191
+ });
192
+ describe('registerOnPsm', () => {
193
+ it('should register account on PSM', async () => {
194
+ const config = {
195
+ threshold: 1,
196
+ signerCommitments: ['0x' + 'a'.repeat(64)],
197
+ psmCommitment: '0x' + 'c'.repeat(64),
198
+ };
199
+ const multisig = new Multisig(mockAccount, config, psm, mockSigner, mockWebClient);
200
+ mockFetch.mockResolvedValueOnce({
201
+ ok: true,
202
+ json: async () => ({
203
+ success: true,
204
+ message: 'Account configured',
205
+ ack_pubkey: '0x' + 'd'.repeat(64),
206
+ }),
207
+ });
208
+ await expect(multisig.registerOnPsm()).resolves.toBeUndefined();
209
+ });
210
+ it('should throw when no account and no initial state', async () => {
211
+ const config = {
212
+ threshold: 1,
213
+ signerCommitments: ['0x' + 'a'.repeat(64)],
214
+ psmCommitment: '0x' + 'c'.repeat(64),
215
+ };
216
+ const multisig = new Multisig(null, config, psm, mockSigner, mockWebClient, '0x' + 'e'.repeat(30));
217
+ await expect(multisig.registerOnPsm()).rejects.toThrow('Cannot register on PSM');
218
+ });
219
+ it('should accept initial state base64', async () => {
220
+ const config = {
221
+ threshold: 1,
222
+ signerCommitments: ['0x' + 'a'.repeat(64)],
223
+ psmCommitment: '0x' + 'c'.repeat(64),
224
+ };
225
+ const multisig = new Multisig(null, config, psm, mockSigner, mockWebClient, '0x' + 'e'.repeat(30));
226
+ mockFetch.mockResolvedValueOnce({
227
+ ok: true,
228
+ json: async () => ({
229
+ success: true,
230
+ message: 'Account configured',
231
+ }),
232
+ });
233
+ await expect(multisig.registerOnPsm('base64initialstate')).resolves.toBeUndefined();
234
+ });
235
+ it('should throw on PSM registration failure', async () => {
236
+ const config = {
237
+ threshold: 1,
238
+ signerCommitments: ['0x' + 'a'.repeat(64)],
239
+ psmCommitment: '0x' + 'c'.repeat(64),
240
+ };
241
+ const multisig = new Multisig(mockAccount, config, psm, mockSigner, mockWebClient);
242
+ mockFetch.mockResolvedValueOnce({
243
+ ok: true,
244
+ json: async () => ({
245
+ success: false,
246
+ message: 'Account already exists',
247
+ }),
248
+ });
249
+ await expect(multisig.registerOnPsm()).rejects.toThrow('Failed to register on PSM');
250
+ });
251
+ });
252
+ describe('syncProposals', () => {
253
+ it('should sync proposals from PSM', async () => {
254
+ const config = {
255
+ threshold: 2,
256
+ signerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
257
+ psmCommitment: '0x' + 'c'.repeat(64),
258
+ };
259
+ const multisig = new Multisig(mockAccount, config, psm, mockSigner, mockWebClient);
260
+ const mockProposals = [
261
+ {
262
+ account_id: '0x' + 'a'.repeat(30),
263
+ nonce: 1,
264
+ prev_commitment: '0x' + 'b'.repeat(64),
265
+ delta_payload: {
266
+ tx_summary: { data: 'AQID' },
267
+ signatures: [],
268
+ metadata: {
269
+ proposal_type: 'add_signer',
270
+ target_threshold: 1,
271
+ signer_commitments: ['0x' + 'a'.repeat(64)],
272
+ description: '',
273
+ },
274
+ },
275
+ status: {
276
+ status: 'pending',
277
+ timestamp: '2024-01-01T00:00:00Z',
278
+ proposer_id: '0x' + 'c'.repeat(64),
279
+ cosigner_sigs: [
280
+ {
281
+ signer_id: '0x' + 'd'.repeat(64),
282
+ signature: { scheme: 'falcon', signature: '0x' + 'e'.repeat(128) },
283
+ timestamp: '2024-01-01T00:00:00Z',
284
+ },
285
+ ],
286
+ },
287
+ },
288
+ ];
289
+ mockFetch.mockResolvedValueOnce({
290
+ ok: true,
291
+ json: async () => ({ proposals: mockProposals }),
292
+ });
293
+ const proposals = await multisig.syncProposals();
294
+ expect(proposals.length).toBe(1);
295
+ expect(proposals[0].nonce).toBe(1);
296
+ expect(proposals[0].status.type).toBe('pending');
297
+ });
298
+ it('should return ready status when enough signatures', async () => {
299
+ const config = {
300
+ threshold: 1, // Only 1 signature needed
301
+ signerCommitments: ['0x' + 'a'.repeat(64)],
302
+ psmCommitment: '0x' + 'c'.repeat(64),
303
+ };
304
+ const multisig = new Multisig(mockAccount, config, psm, mockSigner, mockWebClient);
305
+ const mockProposals = [
306
+ {
307
+ account_id: '0x' + 'a'.repeat(30),
308
+ nonce: 1,
309
+ prev_commitment: '0x' + 'b'.repeat(64),
310
+ delta_payload: {
311
+ tx_summary: { data: 'AQID' },
312
+ signatures: [],
313
+ metadata: {
314
+ proposal_type: 'add_signer',
315
+ target_threshold: 1,
316
+ signer_commitments: ['0x' + 'a'.repeat(64)],
317
+ description: '',
318
+ },
319
+ },
320
+ status: {
321
+ status: 'pending',
322
+ timestamp: '2024-01-01T00:00:00Z',
323
+ proposer_id: '0x' + 'c'.repeat(64),
324
+ cosigner_sigs: [
325
+ {
326
+ signer_id: '0x' + 'd'.repeat(64),
327
+ signature: { scheme: 'falcon', signature: '0x' + 'e'.repeat(128) },
328
+ timestamp: '2024-01-01T00:00:00Z',
329
+ },
330
+ ],
331
+ },
332
+ },
333
+ ];
334
+ mockFetch.mockResolvedValueOnce({
335
+ ok: true,
336
+ json: async () => ({ proposals: mockProposals }),
337
+ });
338
+ const proposals = await multisig.syncProposals();
339
+ expect(proposals[0].status.type).toBe('ready');
340
+ });
341
+ });
342
+ describe('listProposals', () => {
343
+ it('should return empty list initially', () => {
344
+ const config = {
345
+ threshold: 1,
346
+ signerCommitments: ['0x' + 'a'.repeat(64)],
347
+ psmCommitment: '0x' + 'c'.repeat(64),
348
+ };
349
+ const multisig = new Multisig(mockAccount, config, psm, mockSigner, mockWebClient);
350
+ expect(multisig.listProposals()).toEqual([]);
351
+ });
352
+ });
353
+ describe('createProposal', () => {
354
+ it('should create a new proposal', async () => {
355
+ const config = {
356
+ threshold: 1,
357
+ signerCommitments: ['0x' + 'a'.repeat(64)],
358
+ psmCommitment: '0x' + 'c'.repeat(64),
359
+ };
360
+ const multisig = new Multisig(mockAccount, config, psm, mockSigner, mockWebClient);
361
+ const mockDelta = {
362
+ account_id: '0x' + 'a'.repeat(30),
363
+ nonce: 1,
364
+ prev_commitment: '0x' + 'b'.repeat(64),
365
+ delta_payload: {
366
+ tx_summary: { data: 'base64summary' },
367
+ signatures: [],
368
+ },
369
+ status: {
370
+ status: 'pending',
371
+ timestamp: '2024-01-01T00:00:00Z',
372
+ proposer_id: '0x' + 'c'.repeat(64),
373
+ cosigner_sigs: [],
374
+ },
375
+ };
376
+ mockFetch.mockResolvedValueOnce({
377
+ ok: true,
378
+ json: async () => ({
379
+ delta: mockDelta,
380
+ commitment: '0x' + 'd'.repeat(64),
381
+ }),
382
+ });
383
+ const proposal = await multisig.createProposal(1, 'AQID', {
384
+ proposalType: 'add_signer',
385
+ targetThreshold: 1,
386
+ targetSignerCommitments: ['0x' + 'a'.repeat(64)],
387
+ description: '',
388
+ });
389
+ expect(proposal.nonce).toBe(1);
390
+ expect(proposal.id).toBe('0x' + 'd'.repeat(64));
391
+ });
392
+ });
393
+ describe('signProposal', () => {
394
+ it('should sign a proposal', async () => {
395
+ const config = {
396
+ threshold: 1,
397
+ signerCommitments: ['0x' + 'a'.repeat(64)],
398
+ psmCommitment: '0x' + 'c'.repeat(64),
399
+ };
400
+ const multisig = new Multisig(mockAccount, config, psm, mockSigner, mockWebClient);
401
+ // First create a proposal
402
+ const mockDelta = {
403
+ account_id: '0x' + 'a'.repeat(30),
404
+ nonce: 1,
405
+ prev_commitment: '0x' + 'b'.repeat(64),
406
+ delta_payload: {
407
+ tx_summary: { data: 'base64summary' },
408
+ signatures: [],
409
+ },
410
+ status: {
411
+ status: 'pending',
412
+ timestamp: '2024-01-01T00:00:00Z',
413
+ proposer_id: '0x' + 'c'.repeat(64),
414
+ cosigner_sigs: [],
415
+ },
416
+ };
417
+ mockFetch.mockResolvedValueOnce({
418
+ ok: true,
419
+ json: async () => ({
420
+ delta: mockDelta,
421
+ commitment: '0x' + 'd'.repeat(64),
422
+ }),
423
+ });
424
+ await multisig.createProposal(1, 'AQID', {
425
+ proposalType: 'add_signer',
426
+ targetThreshold: 1,
427
+ targetSignerCommitments: ['0x' + 'a'.repeat(64)],
428
+ description: '',
429
+ });
430
+ // Now sign it
431
+ const signedDelta = {
432
+ ...mockDelta,
433
+ status: {
434
+ status: 'pending',
435
+ timestamp: '2024-01-01T00:00:00Z',
436
+ proposer_id: '0x' + 'c'.repeat(64),
437
+ cosigner_sigs: [
438
+ {
439
+ signer_id: mockSigner.commitment,
440
+ signature: { scheme: 'falcon', signature: '0x' + 'b'.repeat(128) },
441
+ timestamp: '2024-01-01T01:00:00Z',
442
+ },
443
+ ],
444
+ },
445
+ delta_payload: {
446
+ ...mockDelta.delta_payload,
447
+ metadata: {
448
+ proposal_type: 'add_signer',
449
+ description: '',
450
+ target_threshold: 1,
451
+ signer_commitments: ['0x' + 'a'.repeat(64)],
452
+ },
453
+ },
454
+ };
455
+ mockFetch.mockResolvedValueOnce({
456
+ ok: true,
457
+ json: async () => signedDelta,
458
+ });
459
+ const proposalId = '0x' + 'd'.repeat(64);
460
+ const signedProposal = await multisig.signProposal(proposalId);
461
+ expect(mockSigner.signCommitment).toHaveBeenCalledWith(proposalId);
462
+ expect(signedProposal.signatures.length).toBe(1);
463
+ });
464
+ });
465
+ describe('exportProposal', () => {
466
+ it('should export proposal for offline signing', async () => {
467
+ const config = {
468
+ threshold: 2,
469
+ signerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
470
+ psmCommitment: '0x' + 'c'.repeat(64),
471
+ };
472
+ const multisig = new Multisig(mockAccount, config, psm, mockSigner, mockWebClient);
473
+ const mockProposals = [
474
+ {
475
+ account_id: '0x' + 'a'.repeat(30),
476
+ nonce: 1,
477
+ prev_commitment: '0x' + 'b'.repeat(64),
478
+ delta_payload: {
479
+ tx_summary: { data: 'AQID' },
480
+ signatures: [],
481
+ metadata: {
482
+ proposal_type: 'add_signer',
483
+ description: '',
484
+ target_threshold: 1,
485
+ signer_commitments: ['0x' + 'a'.repeat(64)],
486
+ },
487
+ },
488
+ status: {
489
+ status: 'pending',
490
+ timestamp: '2024-01-01T00:00:00Z',
491
+ proposer_id: '0x' + 'c'.repeat(64),
492
+ cosigner_sigs: [
493
+ {
494
+ signer_id: '0x' + 'd'.repeat(64),
495
+ signature: { scheme: 'falcon', signature: '0x' + 'e'.repeat(128) },
496
+ timestamp: '2024-01-01T00:00:00Z',
497
+ },
498
+ ],
499
+ },
500
+ },
501
+ ];
502
+ mockFetch.mockResolvedValueOnce({
503
+ ok: true,
504
+ json: async () => ({ proposals: mockProposals }),
505
+ });
506
+ // The proposal ID is computed from tx_summary, which is mocked to return 'c'.repeat(64)
507
+ const exported = await multisig.exportProposal('0x' + 'c'.repeat(64));
508
+ expect(exported.accountId).toBe('0x' + 'a'.repeat(30));
509
+ expect(exported.nonce).toBe(1);
510
+ expect(exported.txSummaryBase64).toBe('AQID');
511
+ expect(exported.signatures.length).toBe(1);
512
+ });
513
+ it('should throw if proposal not found', async () => {
514
+ const config = {
515
+ threshold: 1,
516
+ signerCommitments: ['0x' + 'a'.repeat(64)],
517
+ psmCommitment: '0x' + 'c'.repeat(64),
518
+ };
519
+ const multisig = new Multisig(mockAccount, config, psm, mockSigner, mockWebClient);
520
+ mockFetch.mockResolvedValueOnce({
521
+ ok: true,
522
+ json: async () => ({ proposals: [] }),
523
+ });
524
+ await expect(multisig.exportProposal('0x' + 'nonexistent'.repeat(5))).rejects.toThrow('Proposal not found');
525
+ });
526
+ });
527
+ describe('executeProposal', () => {
528
+ it('should throw if proposal not found locally', async () => {
529
+ const config = {
530
+ threshold: 1,
531
+ signerCommitments: ['0x' + 'a'.repeat(64)],
532
+ psmCommitment: '0x' + 'c'.repeat(64),
533
+ };
534
+ const multisig = new Multisig(mockAccount, config, psm, mockSigner, mockWebClient);
535
+ await expect(multisig.executeProposal('0x' + 'nonexistent'.repeat(5))).rejects.toThrow('Proposal not found');
536
+ });
537
+ it('should throw if proposal is still pending', async () => {
538
+ const config = {
539
+ threshold: 2, // Need 2 signatures
540
+ signerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
541
+ psmCommitment: '0x' + 'c'.repeat(64),
542
+ };
543
+ const multisig = new Multisig(mockAccount, config, psm, mockSigner, mockWebClient);
544
+ // Sync with pending proposal (only 1 signature)
545
+ const mockProposals = [
546
+ {
547
+ account_id: '0x' + 'a'.repeat(30),
548
+ nonce: 1,
549
+ prev_commitment: '0x' + 'b'.repeat(64),
550
+ delta_payload: {
551
+ tx_summary: { data: 'AQID' },
552
+ signatures: [],
553
+ metadata: {
554
+ proposal_type: 'add_signer',
555
+ description: '',
556
+ target_threshold: 2,
557
+ signer_commitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
558
+ },
559
+ },
560
+ status: {
561
+ status: 'pending',
562
+ timestamp: '2024-01-01T00:00:00Z',
563
+ proposer_id: '0x' + 'c'.repeat(64),
564
+ cosigner_sigs: [
565
+ {
566
+ signer_id: '0x' + 'd'.repeat(64),
567
+ signature: { scheme: 'falcon', signature: '0x' + 'e'.repeat(128) },
568
+ timestamp: '2024-01-01T00:00:00Z',
569
+ },
570
+ ],
571
+ },
572
+ },
573
+ ];
574
+ mockFetch.mockResolvedValueOnce({
575
+ ok: true,
576
+ json: async () => ({ proposals: mockProposals }),
577
+ });
578
+ await multisig.syncProposals();
579
+ // Proposal ID is mocked to return 'c'.repeat(64)
580
+ await expect(multisig.executeProposal('0x' + 'c'.repeat(64))).rejects.toThrow('not ready for execution');
581
+ });
582
+ it('should fail when PSM ack signature is missing (selector ON)', async () => {
583
+ const config = {
584
+ threshold: 1,
585
+ signerCommitments: ['0x' + 'a'.repeat(64)],
586
+ psmCommitment: '0x' + 'c'.repeat(64),
587
+ };
588
+ const multisig = new Multisig(mockAccount, config, psm, mockSigner, mockWebClient);
589
+ const readyDelta = {
590
+ account_id: '0x' + 'a'.repeat(30),
591
+ nonce: 1,
592
+ prev_commitment: '0x' + 'b'.repeat(64),
593
+ delta_payload: {
594
+ tx_summary: { data: 'AQID' },
595
+ signatures: [],
596
+ metadata: {
597
+ proposal_type: 'add_signer',
598
+ description: '',
599
+ target_threshold: 1,
600
+ signer_commitments: ['0x' + 'a'.repeat(64)],
601
+ },
602
+ },
603
+ status: {
604
+ status: 'pending',
605
+ timestamp: '2024-01-01T00:00:00Z',
606
+ proposer_id: '0x' + 'c'.repeat(64),
607
+ cosigner_sigs: [
608
+ {
609
+ signer_id: '0x' + 'd'.repeat(64),
610
+ signature: { scheme: 'falcon', signature: '0x' + 'e'.repeat(128) },
611
+ timestamp: '2024-01-01T00:00:00Z',
612
+ },
613
+ ],
614
+ },
615
+ };
616
+ const proposalId = '0x' + 'c'.repeat(64);
617
+ // Prime local cache via syncProposals
618
+ mockFetch.mockResolvedValueOnce({
619
+ ok: true,
620
+ json: async () => ({ proposals: [readyDelta] }),
621
+ });
622
+ await multisig.syncProposals();
623
+ // executeProposal: getDeltaProposals
624
+ mockFetch.mockResolvedValueOnce({
625
+ ok: true,
626
+ json: async () => ({ proposals: [readyDelta] }),
627
+ });
628
+ // executeProposal: pushDelta without ack_sig
629
+ mockFetch.mockResolvedValueOnce({
630
+ ok: true,
631
+ json: async () => ({ ...readyDelta, ack_sig: null }),
632
+ });
633
+ await expect(multisig.executeProposal(proposalId)).rejects.toThrow('PSM did not return acknowledgment signature');
634
+ });
635
+ });
636
+ describe('proposal metadata preservation', () => {
637
+ it('should preserve local metadata when syncing proposals', async () => {
638
+ const config = {
639
+ threshold: 1,
640
+ signerCommitments: ['0x' + 'a'.repeat(64)],
641
+ psmCommitment: '0x' + 'c'.repeat(64),
642
+ };
643
+ const multisig = new Multisig(mockAccount, config, psm, mockSigner, mockWebClient);
644
+ // Create a proposal with metadata
645
+ const mockDelta = {
646
+ account_id: '0x' + 'a'.repeat(30),
647
+ nonce: 1,
648
+ prev_commitment: '0x' + 'b'.repeat(64),
649
+ delta_payload: {
650
+ tx_summary: { data: 'AQID' },
651
+ signatures: [],
652
+ metadata: { proposal_type: 'add_signer' },
653
+ },
654
+ status: {
655
+ status: 'pending',
656
+ timestamp: '2024-01-01T00:00:00Z',
657
+ proposer_id: '0x' + 'c'.repeat(64),
658
+ cosigner_sigs: [],
659
+ },
660
+ };
661
+ mockFetch.mockResolvedValueOnce({
662
+ ok: true,
663
+ json: async () => ({
664
+ delta: mockDelta,
665
+ commitment: '0x' + 'd'.repeat(64),
666
+ }),
667
+ });
668
+ const proposal = await multisig.createProposal(1, 'AQID', {
669
+ proposalType: 'add_signer',
670
+ targetThreshold: 2,
671
+ targetSignerCommitments: ['0x1', '0x2'],
672
+ description: '',
673
+ });
674
+ expect(proposal.metadata?.proposalType).toBe('add_signer');
675
+ // Now sync - should preserve local metadata
676
+ mockFetch.mockResolvedValueOnce({
677
+ ok: true,
678
+ json: async () => ({
679
+ proposals: [mockDelta],
680
+ }),
681
+ });
682
+ const syncedProposals = await multisig.syncProposals();
683
+ const syncedProposal = syncedProposals.find(p => p.nonce === 1);
684
+ expect(syncedProposal?.metadata?.proposalType).toBe('add_signer');
685
+ });
686
+ it('should use PSM metadata for new proposals from other signers', async () => {
687
+ const config = {
688
+ threshold: 2,
689
+ signerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
690
+ psmCommitment: '0x' + 'c'.repeat(64),
691
+ };
692
+ const multisig = new Multisig(mockAccount, config, psm, mockSigner, mockWebClient);
693
+ // Sync proposals - no local proposals exist
694
+ const mockProposals = [
695
+ {
696
+ account_id: '0x' + 'a'.repeat(30),
697
+ nonce: 1,
698
+ prev_commitment: '0x' + 'b'.repeat(64),
699
+ delta_payload: {
700
+ tx_summary: { data: 'AQID' },
701
+ signatures: [],
702
+ metadata: {
703
+ proposal_type: 'p2id',
704
+ recipient_id: '0xrecipient',
705
+ faucet_id: '0xfaucet',
706
+ amount: '100',
707
+ },
708
+ },
709
+ status: {
710
+ status: 'pending',
711
+ timestamp: '2024-01-01T00:00:00Z',
712
+ proposer_id: '0x' + 'other'.repeat(12),
713
+ cosigner_sigs: [],
714
+ },
715
+ },
716
+ ];
717
+ mockFetch.mockResolvedValueOnce({
718
+ ok: true,
719
+ json: async () => ({ proposals: mockProposals }),
720
+ });
721
+ const proposals = await multisig.syncProposals();
722
+ expect(proposals.length).toBe(1);
723
+ expect(proposals[0].metadata?.proposalType).toBe('p2id');
724
+ });
725
+ });
726
+ describe('createProposal with different metadata types', () => {
727
+ it('should create consume_notes proposal', async () => {
728
+ const config = {
729
+ threshold: 1,
730
+ signerCommitments: ['0x' + 'a'.repeat(64)],
731
+ psmCommitment: '0x' + 'c'.repeat(64),
732
+ };
733
+ const multisig = new Multisig(mockAccount, config, psm, mockSigner, mockWebClient);
734
+ const mockDelta = {
735
+ account_id: '0x' + 'a'.repeat(30),
736
+ nonce: 1,
737
+ prev_commitment: '0x' + 'b'.repeat(64),
738
+ delta_payload: {
739
+ tx_summary: { data: 'AQID' },
740
+ signatures: [],
741
+ metadata: {
742
+ proposal_type: 'add_signer',
743
+ target_threshold: 2,
744
+ signer_commitments: ['0x1', '0x2'],
745
+ description: '',
746
+ },
747
+ },
748
+ status: {
749
+ status: 'pending',
750
+ timestamp: '2024-01-01T00:00:00Z',
751
+ proposer_id: '0x' + 'c'.repeat(64),
752
+ cosigner_sigs: [],
753
+ },
754
+ };
755
+ mockFetch.mockResolvedValueOnce({
756
+ ok: true,
757
+ json: async () => ({
758
+ delta: mockDelta,
759
+ commitment: '0x' + 'd'.repeat(64),
760
+ }),
761
+ });
762
+ const proposal = await multisig.createProposal(1, 'AQID', {
763
+ proposalType: 'consume_notes',
764
+ noteIds: ['0xnote1', '0xnote2'],
765
+ description: '',
766
+ });
767
+ expect(proposal.metadata?.proposalType).toBe('consume_notes');
768
+ });
769
+ it('should create p2id proposal', async () => {
770
+ const config = {
771
+ threshold: 1,
772
+ signerCommitments: ['0x' + 'a'.repeat(64)],
773
+ psmCommitment: '0x' + 'c'.repeat(64),
774
+ };
775
+ const multisig = new Multisig(mockAccount, config, psm, mockSigner, mockWebClient);
776
+ const mockDelta = {
777
+ account_id: '0x' + 'a'.repeat(30),
778
+ nonce: 1,
779
+ prev_commitment: '0x' + 'b'.repeat(64),
780
+ delta_payload: {
781
+ tx_summary: { data: 'AQID' },
782
+ signatures: [],
783
+ metadata: {
784
+ proposal_type: 'add_signer',
785
+ target_threshold: 1,
786
+ signer_commitments: ['0x' + 'a'.repeat(64)],
787
+ description: '',
788
+ },
789
+ },
790
+ status: {
791
+ status: 'pending',
792
+ timestamp: '2024-01-01T00:00:00Z',
793
+ proposer_id: '0x' + 'c'.repeat(64),
794
+ cosigner_sigs: [],
795
+ },
796
+ };
797
+ mockFetch.mockResolvedValueOnce({
798
+ ok: true,
799
+ json: async () => ({
800
+ delta: mockDelta,
801
+ commitment: '0x' + 'd'.repeat(64),
802
+ }),
803
+ });
804
+ const proposal = await multisig.createProposal(1, 'AQID', {
805
+ proposalType: 'p2id',
806
+ recipientId: '0xrecipient',
807
+ faucetId: '0xfaucet',
808
+ amount: '100',
809
+ description: '',
810
+ });
811
+ expect(proposal.metadata?.proposalType).toBe('p2id');
812
+ });
813
+ it('should create switch_psm proposal', async () => {
814
+ const config = {
815
+ threshold: 1,
816
+ signerCommitments: ['0x' + 'a'.repeat(64)],
817
+ psmCommitment: '0x' + 'c'.repeat(64),
818
+ };
819
+ const multisig = new Multisig(mockAccount, config, psm, mockSigner, mockWebClient);
820
+ const mockDelta = {
821
+ account_id: '0x' + 'a'.repeat(30),
822
+ nonce: 1,
823
+ prev_commitment: '0x' + 'b'.repeat(64),
824
+ delta_payload: {
825
+ tx_summary: { data: 'AQID' },
826
+ signatures: [],
827
+ metadata: {
828
+ proposalType: 'add_signer',
829
+ targetThreshold: 2,
830
+ targetSignerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
831
+ description: '',
832
+ },
833
+ },
834
+ status: {
835
+ status: 'pending',
836
+ timestamp: '2024-01-01T00:00:00Z',
837
+ proposer_id: '0x' + 'c'.repeat(64),
838
+ cosigner_sigs: [],
839
+ },
840
+ };
841
+ mockFetch.mockResolvedValueOnce({
842
+ ok: true,
843
+ json: async () => ({
844
+ delta: mockDelta,
845
+ commitment: '0x' + 'd'.repeat(64),
846
+ }),
847
+ });
848
+ const proposal = await multisig.createProposal(1, 'AQID', {
849
+ proposalType: 'switch_psm',
850
+ newPsmPubkey: '0xnewpubkey',
851
+ newPsmEndpoint: 'http://new-psm.com',
852
+ description: '',
853
+ });
854
+ expect(proposal.metadata?.proposalType).toBe('switch_psm');
855
+ });
856
+ });
857
+ describe('proposal status transitions', () => {
858
+ it('should transition from pending to ready when threshold met', async () => {
859
+ const config = {
860
+ threshold: 2,
861
+ signerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
862
+ psmCommitment: '0x' + 'c'.repeat(64),
863
+ };
864
+ const multisig = new Multisig(mockAccount, config, psm, mockSigner, mockWebClient);
865
+ // First sync with 1 signature (pending)
866
+ const mockProposalsPending = [
867
+ {
868
+ account_id: '0x' + 'a'.repeat(30),
869
+ nonce: 1,
870
+ prev_commitment: '0x' + 'b'.repeat(64),
871
+ delta_payload: {
872
+ tx_summary: { data: 'AQID' },
873
+ signatures: [],
874
+ metadata: {
875
+ proposal_type: 'add_signer',
876
+ target_threshold: 2,
877
+ signer_commitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
878
+ description: '',
879
+ },
880
+ },
881
+ status: {
882
+ status: 'pending',
883
+ timestamp: '2024-01-01T00:00:00Z',
884
+ proposer_id: '0x' + 'c'.repeat(64),
885
+ cosigner_sigs: [
886
+ {
887
+ signer_id: '0x' + 'a'.repeat(64),
888
+ signature: { scheme: 'falcon', signature: '0x' + 'sig'.repeat(40) },
889
+ timestamp: '2024-01-01T00:00:00Z',
890
+ },
891
+ ],
892
+ },
893
+ },
894
+ ];
895
+ mockFetch.mockResolvedValueOnce({
896
+ ok: true,
897
+ json: async () => ({ proposals: mockProposalsPending }),
898
+ });
899
+ let proposals = await multisig.syncProposals();
900
+ expect(proposals[0].status.type).toBe('pending');
901
+ // Second sync with 2 signatures (ready)
902
+ const mockProposalsReady = [
903
+ {
904
+ ...mockProposalsPending[0],
905
+ delta_payload: {
906
+ ...mockProposalsPending[0].delta_payload,
907
+ metadata: {
908
+ proposal_type: 'add_signer',
909
+ target_threshold: 2,
910
+ signer_commitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
911
+ description: '',
912
+ },
913
+ },
914
+ status: {
915
+ status: 'pending',
916
+ timestamp: '2024-01-01T00:00:00Z',
917
+ proposer_id: '0x' + 'c'.repeat(64),
918
+ cosigner_sigs: [
919
+ {
920
+ signer_id: '0x' + 'a'.repeat(64),
921
+ signature: { scheme: 'falcon', signature: '0x' + 'sig'.repeat(40) },
922
+ timestamp: '2024-01-01T00:00:00Z',
923
+ },
924
+ {
925
+ signer_id: '0x' + 'b'.repeat(64),
926
+ signature: { scheme: 'falcon', signature: '0x' + 'sig2'.repeat(40) },
927
+ timestamp: '2024-01-01T01:00:00Z',
928
+ },
929
+ ],
930
+ },
931
+ },
932
+ ];
933
+ mockFetch.mockResolvedValueOnce({
934
+ ok: true,
935
+ json: async () => ({ proposals: mockProposalsReady }),
936
+ });
937
+ proposals = await multisig.syncProposals();
938
+ expect(proposals[0].status.type).toBe('ready');
939
+ });
940
+ });
941
+ describe('getters', () => {
942
+ it('should expose threshold', () => {
943
+ const config = {
944
+ threshold: 3,
945
+ signerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64), '0x' + 'c'.repeat(64)],
946
+ psmCommitment: '0x' + 'd'.repeat(64),
947
+ };
948
+ const multisig = new Multisig(mockAccount, config, psm, mockSigner, mockWebClient);
949
+ expect(multisig.threshold).toBe(3);
950
+ });
951
+ it('should expose signerCommitments', () => {
952
+ const signerCommitments = ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)];
953
+ const config = {
954
+ threshold: 2,
955
+ signerCommitments,
956
+ psmCommitment: '0x' + 'd'.repeat(64),
957
+ };
958
+ const multisig = new Multisig(mockAccount, config, psm, mockSigner, mockWebClient);
959
+ expect(multisig.signerCommitments).toEqual(signerCommitments);
960
+ });
961
+ it('should expose psmCommitment', () => {
962
+ const psmCommitment = '0x' + 'psm'.repeat(20);
963
+ const config = {
964
+ threshold: 1,
965
+ signerCommitments: ['0x' + 'a'.repeat(64)],
966
+ psmCommitment,
967
+ };
968
+ const multisig = new Multisig(mockAccount, config, psm, mockSigner, mockWebClient);
969
+ expect(multisig.psmCommitment).toBe(psmCommitment);
970
+ });
971
+ it('should expose account when provided', () => {
972
+ const config = {
973
+ threshold: 1,
974
+ signerCommitments: ['0x' + 'a'.repeat(64)],
975
+ psmCommitment: '0x' + 'd'.repeat(64),
976
+ };
977
+ const multisig = new Multisig(mockAccount, config, psm, mockSigner, mockWebClient);
978
+ expect(multisig.account).toBe(mockAccount);
979
+ });
980
+ });
981
+ describe('cross-client compatibility: sync with snake_case metadata', () => {
982
+ it('should parse Rust client proposals with snake_case metadata', async () => {
983
+ const config = {
984
+ threshold: 2,
985
+ signerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
986
+ psmCommitment: '0x' + 'c'.repeat(64),
987
+ };
988
+ const multisig = new Multisig(mockAccount, config, psm, mockSigner, mockWebClient);
989
+ // Simulates a PSM response with canonical snake_case metadata
990
+ const rustProposals = [
991
+ {
992
+ account_id: '0x' + 'a'.repeat(30),
993
+ nonce: 1,
994
+ prev_commitment: '0x' + 'b'.repeat(64),
995
+ delta_payload: {
996
+ tx_summary: { data: 'AQID' },
997
+ signatures: [],
998
+ metadata: {
999
+ proposal_type: 'change_threshold',
1000
+ target_threshold: 3,
1001
+ signer_commitments: ['0xa', '0xb', '0xc'],
1002
+ salt: '0xlegacysalt',
1003
+ },
1004
+ },
1005
+ status: {
1006
+ status: 'pending',
1007
+ timestamp: '2024-01-01T00:00:00Z',
1008
+ proposer_id: '0x' + 'rust_client'.repeat(5),
1009
+ cosigner_sigs: [],
1010
+ },
1011
+ },
1012
+ ];
1013
+ mockFetch.mockResolvedValueOnce({
1014
+ ok: true,
1015
+ json: async () => ({ proposals: rustProposals }),
1016
+ });
1017
+ const proposals = await multisig.syncProposals();
1018
+ expect(proposals.length).toBe(1);
1019
+ // The TS client should normalize snake_case to camelCase
1020
+ expect(proposals[0].metadata?.proposalType).toBe('change_threshold');
1021
+ if (proposals[0].metadata?.proposalType === 'change_threshold') {
1022
+ expect(proposals[0].metadata.targetThreshold).toBe(3);
1023
+ expect(proposals[0].metadata.targetSignerCommitments).toEqual(['0xa', '0xb', '0xc']);
1024
+ }
1025
+ });
1026
+ it('should parse Rust client P2ID proposal with snake_case fields', async () => {
1027
+ const config = {
1028
+ threshold: 1,
1029
+ signerCommitments: ['0x' + 'a'.repeat(64)],
1030
+ psmCommitment: '0x' + 'c'.repeat(64),
1031
+ };
1032
+ const multisig = new Multisig(mockAccount, config, psm, mockSigner, mockWebClient);
1033
+ // P2ID proposal with canonical snake_case fields
1034
+ const p2idProposals = [
1035
+ {
1036
+ account_id: '0x' + 'a'.repeat(30),
1037
+ nonce: 1,
1038
+ prev_commitment: '0x' + 'b'.repeat(64),
1039
+ delta_payload: {
1040
+ tx_summary: { data: 'AQID' },
1041
+ signatures: [],
1042
+ metadata: {
1043
+ proposal_type: 'p2id',
1044
+ recipient_id: '0xrecipient',
1045
+ faucet_id: '0xfaucet',
1046
+ amount: '12345',
1047
+ salt: '0xsalt',
1048
+ },
1049
+ },
1050
+ status: {
1051
+ status: 'pending',
1052
+ timestamp: '2024-01-01T00:00:00Z',
1053
+ proposer_id: '0x' + 'c'.repeat(64),
1054
+ cosigner_sigs: [
1055
+ {
1056
+ signer_id: '0x' + 'a'.repeat(64),
1057
+ signature: { scheme: 'falcon', signature: '0x' + 'sig'.repeat(40) },
1058
+ timestamp: '2024-01-01T00:00:00Z',
1059
+ },
1060
+ ],
1061
+ },
1062
+ },
1063
+ ];
1064
+ mockFetch.mockResolvedValueOnce({
1065
+ ok: true,
1066
+ json: async () => ({ proposals: p2idProposals }),
1067
+ });
1068
+ const proposals = await multisig.syncProposals();
1069
+ expect(proposals.length).toBe(1);
1070
+ expect(proposals[0].metadata?.proposalType).toBe('p2id');
1071
+ if (proposals[0].metadata?.proposalType === 'p2id') {
1072
+ expect(proposals[0].metadata.recipientId).toBe('0xrecipient');
1073
+ expect(proposals[0].metadata.faucetId).toBe('0xfaucet');
1074
+ expect(proposals[0].metadata.amount).toBe('12345');
1075
+ }
1076
+ });
1077
+ it('should parse switch_psm proposal with snake_case fields', async () => {
1078
+ const config = {
1079
+ threshold: 1,
1080
+ signerCommitments: ['0x' + 'a'.repeat(64)],
1081
+ psmCommitment: '0x' + 'c'.repeat(64),
1082
+ };
1083
+ const multisig = new Multisig(mockAccount, config, psm, mockSigner, mockWebClient);
1084
+ const switchPsmProposals = [
1085
+ {
1086
+ account_id: '0x' + 'a'.repeat(30),
1087
+ nonce: 1,
1088
+ prev_commitment: '0x' + 'b'.repeat(64),
1089
+ delta_payload: {
1090
+ tx_summary: { data: 'AQID' },
1091
+ signatures: [],
1092
+ metadata: {
1093
+ proposal_type: 'switch_psm',
1094
+ new_psm_pubkey: '0xnewpubkey',
1095
+ new_psm_endpoint: 'http://new-psm.com',
1096
+ salt: '0xsalt',
1097
+ },
1098
+ },
1099
+ status: {
1100
+ status: 'pending',
1101
+ timestamp: '2024-01-01T00:00:00Z',
1102
+ proposer_id: '0x' + 'c'.repeat(64),
1103
+ cosigner_sigs: [],
1104
+ },
1105
+ },
1106
+ ];
1107
+ mockFetch.mockResolvedValueOnce({
1108
+ ok: true,
1109
+ json: async () => ({ proposals: switchPsmProposals }),
1110
+ });
1111
+ const proposals = await multisig.syncProposals();
1112
+ expect(proposals.length).toBe(1);
1113
+ expect(proposals[0].metadata?.proposalType).toBe('switch_psm');
1114
+ if (proposals[0].metadata?.proposalType === 'switch_psm') {
1115
+ expect(proposals[0].metadata.newPsmPubkey).toBe('0xnewpubkey');
1116
+ expect(proposals[0].metadata.newPsmEndpoint).toBe('http://new-psm.com');
1117
+ }
1118
+ });
1119
+ });
1120
+ });
1121
+ //# sourceMappingURL=multisig.test.js.map