@openzeppelin/miden-multisig-client 0.15.2 → 0.16.1

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 (148) hide show
  1. package/README.md +37 -1
  2. package/dist/account/builder.d.ts +2 -1
  3. package/dist/account/builder.d.ts.map +1 -1
  4. package/dist/account/builder.js +1 -0
  5. package/dist/account/builder.js.map +1 -1
  6. package/dist/account/builder.test.js +2 -2
  7. package/dist/account/builder.test.js.map +1 -1
  8. package/dist/client.d.ts +17 -5
  9. package/dist/client.d.ts.map +1 -1
  10. package/dist/client.js +14 -7
  11. package/dist/client.js.map +1 -1
  12. package/dist/client.test.js +46 -15
  13. package/dist/client.test.js.map +1 -1
  14. package/dist/connectivity.d.ts +38 -0
  15. package/dist/connectivity.d.ts.map +1 -0
  16. package/dist/connectivity.js +97 -0
  17. package/dist/connectivity.js.map +1 -0
  18. package/dist/connectivity.test.d.ts +2 -0
  19. package/dist/connectivity.test.d.ts.map +1 -0
  20. package/dist/connectivity.test.js +61 -0
  21. package/dist/connectivity.test.js.map +1 -0
  22. package/dist/index.d.ts +14 -3
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +13 -3
  25. package/dist/index.js.map +1 -1
  26. package/dist/multisig.d.ts +130 -8
  27. package/dist/multisig.d.ts.map +1 -1
  28. package/dist/multisig.js +220 -24
  29. package/dist/multisig.js.map +1 -1
  30. package/dist/multisig.test.js +427 -89
  31. package/dist/multisig.test.js.map +1 -1
  32. package/dist/proposal/factory.d.ts.map +1 -1
  33. package/dist/proposal/factory.js +10 -0
  34. package/dist/proposal/factory.js.map +1 -1
  35. package/dist/proposal/factory.test.d.ts +2 -0
  36. package/dist/proposal/factory.test.d.ts.map +1 -0
  37. package/dist/proposal/factory.test.js +32 -0
  38. package/dist/proposal/factory.test.js.map +1 -0
  39. package/dist/proposal/metadata.d.ts.map +1 -1
  40. package/dist/proposal/metadata.js +12 -0
  41. package/dist/proposal/metadata.js.map +1 -1
  42. package/dist/proposal/metadata.test.js +49 -0
  43. package/dist/proposal/metadata.test.js.map +1 -1
  44. package/dist/prover/config.d.ts +16 -0
  45. package/dist/prover/config.d.ts.map +1 -0
  46. package/dist/prover/config.js +54 -0
  47. package/dist/prover/config.js.map +1 -0
  48. package/dist/prover/config.test.d.ts +2 -0
  49. package/dist/prover/config.test.d.ts.map +1 -0
  50. package/dist/prover/config.test.js +54 -0
  51. package/dist/prover/config.test.js.map +1 -0
  52. package/dist/prover/errors.d.ts +2 -0
  53. package/dist/prover/errors.d.ts.map +1 -0
  54. package/dist/prover/errors.js +158 -0
  55. package/dist/prover/errors.js.map +1 -0
  56. package/dist/prover/errors.test.d.ts +2 -0
  57. package/dist/prover/errors.test.d.ts.map +1 -0
  58. package/dist/prover/errors.test.js +24 -0
  59. package/dist/prover/errors.test.js.map +1 -0
  60. package/dist/prover/retry.d.ts +10 -0
  61. package/dist/prover/retry.d.ts.map +1 -0
  62. package/dist/prover/retry.js +32 -0
  63. package/dist/prover/retry.js.map +1 -0
  64. package/dist/prover/retry.test.d.ts +2 -0
  65. package/dist/prover/retry.test.d.ts.map +1 -0
  66. package/dist/prover/retry.test.js +20 -0
  67. package/dist/prover/retry.test.js.map +1 -0
  68. package/dist/prover/workflow.d.ts +11 -0
  69. package/dist/prover/workflow.d.ts.map +1 -0
  70. package/dist/prover/workflow.js +18 -0
  71. package/dist/prover/workflow.js.map +1 -0
  72. package/dist/prover/workflow.test.d.ts +2 -0
  73. package/dist/prover/workflow.test.d.ts.map +1 -0
  74. package/dist/prover/workflow.test.js +80 -0
  75. package/dist/prover/workflow.test.js.map +1 -0
  76. package/dist/raw-client.d.ts +2 -2
  77. package/dist/raw-client.d.ts.map +1 -1
  78. package/dist/raw-client.js +14 -4
  79. package/dist/raw-client.js.map +1 -1
  80. package/dist/raw-client.test.js +25 -3
  81. package/dist/raw-client.test.js.map +1 -1
  82. package/dist/transaction/consumeNotes.d.ts +6 -2
  83. package/dist/transaction/consumeNotes.d.ts.map +1 -1
  84. package/dist/transaction/consumeNotes.js +0 -4
  85. package/dist/transaction/consumeNotes.js.map +1 -1
  86. package/dist/transaction/options.d.ts +3 -0
  87. package/dist/transaction/options.d.ts.map +1 -1
  88. package/dist/transaction/p2id.d.ts +27 -2
  89. package/dist/transaction/p2id.d.ts.map +1 -1
  90. package/dist/transaction/p2id.js +56 -4
  91. package/dist/transaction/p2id.js.map +1 -1
  92. package/dist/transaction/p2id.test.js +66 -6
  93. package/dist/transaction/p2id.test.js.map +1 -1
  94. package/dist/transaction/summary.d.ts +2 -1
  95. package/dist/transaction/summary.d.ts.map +1 -1
  96. package/dist/transaction/summary.js.map +1 -1
  97. package/dist/transaction/updateGuardian.d.ts +6 -2
  98. package/dist/transaction/updateGuardian.d.ts.map +1 -1
  99. package/dist/transaction/updateGuardian.js.map +1 -1
  100. package/dist/transaction/updateProcedureThreshold.d.ts +7 -2
  101. package/dist/transaction/updateProcedureThreshold.d.ts.map +1 -1
  102. package/dist/transaction/updateProcedureThreshold.js.map +1 -1
  103. package/dist/transaction/updateSigners.d.ts +7 -2
  104. package/dist/transaction/updateSigners.d.ts.map +1 -1
  105. package/dist/transaction/updateSigners.js.map +1 -1
  106. package/dist/transaction.d.ts +1 -1
  107. package/dist/transaction.d.ts.map +1 -1
  108. package/dist/transaction.js +1 -1
  109. package/dist/transaction.js.map +1 -1
  110. package/dist/types/proposal.d.ts +5 -0
  111. package/dist/types/proposal.d.ts.map +1 -1
  112. package/dist/types/proposal.js +3 -0
  113. package/dist/types/proposal.js.map +1 -1
  114. package/package.json +3 -3
  115. package/src/account/builder.test.ts +19 -11
  116. package/src/account/builder.ts +2 -1
  117. package/src/client.test.ts +67 -15
  118. package/src/client.ts +35 -12
  119. package/src/connectivity.test.ts +67 -0
  120. package/src/connectivity.ts +111 -0
  121. package/src/index.ts +25 -1
  122. package/src/multisig.test.ts +526 -94
  123. package/src/multisig.ts +270 -25
  124. package/src/proposal/factory.test.ts +40 -0
  125. package/src/proposal/factory.ts +11 -1
  126. package/src/proposal/metadata.test.ts +60 -0
  127. package/src/proposal/metadata.ts +14 -0
  128. package/src/prover/config.test.ts +90 -0
  129. package/src/prover/config.ts +78 -0
  130. package/src/prover/errors.test.ts +53 -0
  131. package/src/prover/errors.ts +178 -0
  132. package/src/prover/retry.test.ts +38 -0
  133. package/src/prover/retry.ts +46 -0
  134. package/src/prover/test-node.d.ts +6 -0
  135. package/src/prover/workflow.test.ts +109 -0
  136. package/src/prover/workflow.ts +19 -0
  137. package/src/raw-client.test.ts +41 -3
  138. package/src/raw-client.ts +15 -5
  139. package/src/transaction/consumeNotes.ts +11 -1
  140. package/src/transaction/options.ts +4 -0
  141. package/src/transaction/p2id.test.ts +98 -8
  142. package/src/transaction/p2id.ts +91 -12
  143. package/src/transaction/summary.ts +12 -0
  144. package/src/transaction/updateGuardian.ts +11 -1
  145. package/src/transaction/updateProcedureThreshold.ts +13 -1
  146. package/src/transaction/updateSigners.ts +13 -1
  147. package/src/transaction.ts +4 -0
  148. package/src/types/proposal.ts +9 -0
@@ -8,10 +8,11 @@ import {
8
8
  executeForSummary,
9
9
  } from './transaction.js';
10
10
 
11
- const { mockRpcGetAccountDetails, mockAccountDeserialize, mockDetectConfig } = vi.hoisted(() => ({
11
+ const { mockRpcGetAccountDetails, mockAccountDeserialize, mockDetectConfig, mockNoteFileDeserialize } = vi.hoisted(() => ({
12
12
  mockRpcGetAccountDetails: vi.fn(),
13
13
  mockAccountDeserialize: vi.fn(),
14
14
  mockDetectConfig: vi.fn(),
15
+ mockNoteFileDeserialize: vi.fn(),
15
16
  }));
16
17
 
17
18
  // Mock the Miden SDK
@@ -22,6 +23,18 @@ vi.mock('@miden-sdk/miden-sdk', () => ({
22
23
  AccountId: {
23
24
  fromHex: vi.fn((hex: string) => ({ toString: () => hex })),
24
25
  },
26
+ NoteType: {
27
+ Private: 0,
28
+ Public: 1,
29
+ },
30
+ NoteExportFormat: {
31
+ Id: 0,
32
+ Full: 1,
33
+ Details: 2,
34
+ },
35
+ NoteFile: {
36
+ deserialize: mockNoteFileDeserialize,
37
+ },
25
38
  TransactionSummary: {
26
39
  deserialize: vi.fn().mockReturnValue({
27
40
  toCommitment: () => ({
@@ -87,6 +100,17 @@ vi.mock('./transaction.js', () => ({
87
100
  request: {},
88
101
  salt: { toHex: () => '0x' + 'd'.repeat(64) },
89
102
  }),
103
+ buildP2idNoteFromMetadata: vi.fn().mockReturnValue({
104
+ id: () => ({ toString: () => '0x' + 'ab'.repeat(32) }),
105
+ }),
106
+ // Mirrors the real implementations against the mocked NoteType values
107
+ // (Private = 0, Public = 1).
108
+ parseP2idNoteType: vi.fn((value?: string) => {
109
+ if (value === undefined || value === 'public') return 1;
110
+ if (value === 'private') return 0;
111
+ throw new Error(`unsupported metadata.noteType '${value}': expected 'public' or 'private'`);
112
+ }),
113
+ p2idNoteTypeToMetadata: vi.fn((noteType?: number) => (noteType === 0 ? 'private' : undefined)),
90
114
  }));
91
115
 
92
116
  vi.mock('./utils/signature.js', async () => {
@@ -119,6 +143,8 @@ vi.mock('./inspector.js', () => ({
119
143
  const mockFetch = vi.fn();
120
144
  vi.stubGlobal('fetch', mockFetch);
121
145
 
146
+ const MIDEN_RPC_ENDPOINT = 'https://rpc.devnet.miden.io';
147
+
122
148
  function mockedAccount(commitmentHex: string, nonce = 0): any {
123
149
  return {
124
150
  commitment: () => ({
@@ -139,6 +165,24 @@ describe('Multisig', () => {
139
165
  let mockAccount: any;
140
166
  let mockWebClient: any;
141
167
 
168
+ function createTestMultisig(
169
+ config: ConstructorParameters<typeof Multisig>[1],
170
+ signer: Signer = mockSigner,
171
+ accountId?: string,
172
+ proverConfig?: ConstructorParameters<typeof Multisig>[7],
173
+ ): Multisig {
174
+ return new Multisig(
175
+ mockAccount,
176
+ config,
177
+ guardian,
178
+ signer,
179
+ mockWebClient,
180
+ accountId,
181
+ MIDEN_RPC_ENDPOINT,
182
+ proverConfig,
183
+ );
184
+ }
185
+
142
186
  beforeEach(() => {
143
187
  mockFetch.mockReset();
144
188
  vi.mocked(executeForSummary).mockResolvedValue({
@@ -196,13 +240,38 @@ describe('Multisig', () => {
196
240
  submitNewTransaction: vi.fn(),
197
241
  submitNewTransactionWithProver: vi.fn(),
198
242
  transactions: {
199
- submit: vi.fn(),
243
+ executeRequest: vi.fn(),
200
244
  },
201
245
  getConsumableNotes: vi.fn().mockResolvedValue([]),
202
246
  syncState: vi.fn(),
203
247
  getAccount: vi.fn().mockResolvedValue(null),
204
248
  newAccount: vi.fn(),
205
249
  };
250
+ mockWebClient.transactions.executeRequest.mockImplementation(
251
+ async (accountId: unknown, request: unknown) => {
252
+ const result = await mockWebClient.executeTransaction(accountId, request);
253
+ return {
254
+ result,
255
+ prove: async (options?: { prover?: unknown }) => {
256
+ const proof = options?.prover === undefined
257
+ ? await mockWebClient.proveTransaction(result)
258
+ : await mockWebClient.proveTransaction(result, options.prover);
259
+ return {
260
+ proof,
261
+ result,
262
+ submit: async () => {
263
+ const blockNumber = await mockWebClient.submitProvenTransaction(proof, result);
264
+ return {
265
+ blockNumber,
266
+ result,
267
+ apply: () => mockWebClient.applyTransaction(result, blockNumber),
268
+ };
269
+ },
270
+ };
271
+ },
272
+ };
273
+ },
274
+ );
206
275
  });
207
276
 
208
277
  describe('constructor', () => {
@@ -213,7 +282,7 @@ describe('Multisig', () => {
213
282
  guardianCommitment: '0x' + 'c'.repeat(64),
214
283
  };
215
284
 
216
- const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
285
+ const multisig = createTestMultisig(config);
217
286
 
218
287
  expect(multisig.threshold).toBe(2);
219
288
  expect(multisig.signerCommitments).toEqual(config.signerCommitments);
@@ -229,11 +298,31 @@ describe('Multisig', () => {
229
298
  };
230
299
 
231
300
  const accountId = '0x' + 'd'.repeat(30);
232
- const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient, accountId);
301
+ const multisig = createTestMultisig(config, mockSigner, accountId);
233
302
 
234
303
  expect(multisig.account).toBe(mockAccount);
235
304
  expect(multisig.accountId).toBe(accountId);
236
305
  });
306
+
307
+ it('should reject a missing Miden RPC endpoint immediately', () => {
308
+ const config = {
309
+ threshold: 2,
310
+ signerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
311
+ guardianCommitment: '0x' + 'c'.repeat(64),
312
+ };
313
+
314
+ expect(
315
+ () => new Multisig(
316
+ mockAccount,
317
+ config,
318
+ guardian,
319
+ mockSigner,
320
+ mockWebClient,
321
+ undefined,
322
+ undefined as unknown as string
323
+ )
324
+ ).toThrow('missing required configuration: midenRpcEndpoint');
325
+ });
237
326
  });
238
327
 
239
328
  describe('accountId', () => {
@@ -244,7 +333,7 @@ describe('Multisig', () => {
244
333
  guardianCommitment: '0x' + 'c'.repeat(64),
245
334
  };
246
335
 
247
- const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
336
+ const multisig = createTestMultisig(config);
248
337
  expect(multisig.accountId).toBe('0x' + 'a'.repeat(30));
249
338
  });
250
339
 
@@ -256,7 +345,7 @@ describe('Multisig', () => {
256
345
  };
257
346
 
258
347
  const accountId = '0x' + 'e'.repeat(30);
259
- const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient, accountId);
348
+ const multisig = createTestMultisig(config, mockSigner, accountId);
260
349
  expect(multisig.accountId).toBe(accountId);
261
350
  });
262
351
  });
@@ -269,7 +358,7 @@ describe('Multisig', () => {
269
358
  guardianCommitment: '0x' + 'c'.repeat(64),
270
359
  };
271
360
 
272
- const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
361
+ const multisig = createTestMultisig(config);
273
362
  expect(multisig.signerCommitment).toBe(mockSigner.commitment);
274
363
  });
275
364
  });
@@ -282,7 +371,7 @@ describe('Multisig', () => {
282
371
  guardianCommitment: '0x' + 'c'.repeat(64),
283
372
  };
284
373
 
285
- const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
374
+ const multisig = createTestMultisig(config);
286
375
 
287
376
  mockFetch.mockResolvedValueOnce({
288
377
  ok: true,
@@ -503,7 +592,7 @@ describe('Multisig', () => {
503
592
  expect(mockWebClient.newAccount).not.toHaveBeenCalled();
504
593
  });
505
594
 
506
- it('should throw when incoming state nonce is lower than local nonce', async () => {
595
+ it('keeps local state and refreshes config from it when GUARDIAN nonce is behind local', async () => {
507
596
  const config = {
508
597
  threshold: 1,
509
598
  signerCommitments: ['0x' + 'a'.repeat(64)],
@@ -520,7 +609,8 @@ describe('Multisig', () => {
520
609
  'https://rpc.devnet.miden.io'
521
610
  );
522
611
 
523
- mockWebClient.getAccount.mockResolvedValueOnce(mockedAccount('0x' + 'a'.repeat(64), 3));
612
+ const localAccount = mockedAccount('0x' + 'a'.repeat(64), 3);
613
+ mockWebClient.getAccount.mockResolvedValueOnce(localAccount);
524
614
  mockAccountDeserialize.mockReturnValueOnce(mockedAccount('0x' + 'b'.repeat(64), 2));
525
615
  mockFetch.mockResolvedValueOnce({
526
616
  ok: true,
@@ -532,11 +622,24 @@ describe('Multisig', () => {
532
622
  updated_at: '2024-01-02T00:00:00Z',
533
623
  }),
534
624
  });
625
+ mockDetectConfig.mockReturnValueOnce({
626
+ threshold: 2,
627
+ numSigners: 2,
628
+ signerCommitments: ['0x' + '1'.repeat(64), '0x' + '2'.repeat(64)],
629
+ guardianEnabled: true,
630
+ guardianCommitment: '0x' + 'd'.repeat(64),
631
+ vaultBalances: [],
632
+ procedureThresholds: new Map(),
633
+ });
535
634
 
536
- await expect(multisig.syncState()).rejects.toThrow(
537
- 'incoming nonce 2 is not greater than local nonce 3'
538
- );
635
+ // GUARDIAN behind local (nonce 2 < 3): no throw, local kept, no overwrite,
636
+ // and the decision needs no on-chain round-trip.
637
+ await expect(multisig.syncState()).resolves.toBeDefined();
539
638
  expect(mockWebClient.newAccount).not.toHaveBeenCalled();
639
+ expect(mockRpcGetAccountDetails).not.toHaveBeenCalled();
640
+ // Config refreshed from the authoritative local account (UI unfreezes).
641
+ expect(multisig.account).toBe(localAccount);
642
+ expect(multisig.threshold).toBe(2);
540
643
  });
541
644
 
542
645
  it('should throw when incoming state nonce equals local nonce but commitment differs', async () => {
@@ -570,10 +673,49 @@ describe('Multisig', () => {
570
673
  });
571
674
 
572
675
  await expect(multisig.syncState()).rejects.toThrow(
573
- 'incoming nonce 2 is not greater than local nonce 2'
676
+ 'incoming nonce 2 equals local nonce 2 but commitments differ'
574
677
  );
575
678
  expect(mockWebClient.newAccount).not.toHaveBeenCalled();
576
679
  });
680
+
681
+ it('unfreezes Multisig.account after execute when GUARDIAN still lags by one nonce (regression, #343)', async () => {
682
+ const config = {
683
+ threshold: 1,
684
+ signerCommitments: ['0x' + 'a'.repeat(64)],
685
+ guardianCommitment: '0x' + 'c'.repeat(64),
686
+ };
687
+
688
+ const multisig = new Multisig(
689
+ mockAccount,
690
+ config,
691
+ guardian,
692
+ mockSigner,
693
+ mockWebClient,
694
+ undefined,
695
+ 'https://rpc.devnet.miden.io'
696
+ );
697
+
698
+ // Post-execute: local advanced to nonce 1, GUARDIAN still reports nonce 0
699
+ // (candidate not canonicalized yet). Before the fix this threw and left
700
+ // Multisig.account frozen at the pre-execute snapshot.
701
+ const localAccount = mockedAccount('0x' + 'a'.repeat(64), 1);
702
+ mockWebClient.getAccount.mockResolvedValueOnce(localAccount);
703
+ mockAccountDeserialize.mockReturnValueOnce(mockedAccount('0x' + 'b'.repeat(64), 0));
704
+ mockFetch.mockResolvedValueOnce({
705
+ ok: true,
706
+ json: async () => ({
707
+ account_id: multisig.accountId,
708
+ commitment: '0x' + 'b'.repeat(64),
709
+ state_json: { data: 'AQID' },
710
+ created_at: '2024-01-01T00:00:00Z',
711
+ updated_at: '2024-01-02T00:00:00Z',
712
+ }),
713
+ });
714
+
715
+ await expect(multisig.syncState()).resolves.toBeDefined();
716
+ expect(mockWebClient.newAccount).not.toHaveBeenCalled();
717
+ expect(multisig.account).toBe(localAccount);
718
+ });
577
719
  });
578
720
 
579
721
  describe('verifyStateCommitment', () => {
@@ -662,7 +804,7 @@ describe('Multisig', () => {
662
804
  guardianCommitment: '0x' + 'c'.repeat(64),
663
805
  };
664
806
 
665
- const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
807
+ const multisig = createTestMultisig(config);
666
808
 
667
809
  mockFetch.mockResolvedValueOnce({
668
810
  ok: true,
@@ -690,7 +832,7 @@ describe('Multisig', () => {
690
832
  };
691
833
 
692
834
  guardian.setSigner(ecdsaSigner);
693
- const multisig = new Multisig(mockAccount, config, guardian, ecdsaSigner, mockWebClient);
835
+ const multisig = createTestMultisig(config, ecdsaSigner);
694
836
 
695
837
  mockFetch.mockResolvedValueOnce({
696
838
  ok: true,
@@ -726,6 +868,7 @@ describe('Multisig', () => {
726
868
  mockSigner,
727
869
  mockWebClient,
728
870
  '0x' + 'e'.repeat(30),
871
+ MIDEN_RPC_ENDPOINT,
729
872
  );
730
873
 
731
874
  mockFetch.mockResolvedValueOnce({
@@ -746,7 +889,7 @@ describe('Multisig', () => {
746
889
  guardianCommitment: '0x' + 'c'.repeat(64),
747
890
  };
748
891
 
749
- const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
892
+ const multisig = createTestMultisig(config);
750
893
 
751
894
  mockFetch.mockResolvedValueOnce({
752
895
  ok: true,
@@ -768,7 +911,7 @@ describe('Multisig', () => {
768
911
  guardianCommitment: '0x' + 'c'.repeat(64),
769
912
  };
770
913
 
771
- const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
914
+ const multisig = createTestMultisig(config);
772
915
 
773
916
  const mockProposals = [
774
917
  {
@@ -819,7 +962,7 @@ describe('Multisig', () => {
819
962
  guardianCommitment: '0x' + 'c'.repeat(64),
820
963
  };
821
964
 
822
- const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
965
+ const multisig = createTestMultisig(config);
823
966
 
824
967
  const mockProposals = [
825
968
  {
@@ -868,7 +1011,7 @@ describe('Multisig', () => {
868
1011
  guardianCommitment: '0x' + 'c'.repeat(64),
869
1012
  };
870
1013
 
871
- const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
1014
+ const multisig = createTestMultisig(config);
872
1015
 
873
1016
  mockFetch.mockResolvedValueOnce({
874
1017
  ok: true,
@@ -917,7 +1060,7 @@ describe('Multisig', () => {
917
1060
  guardianCommitment: '0x' + 'c'.repeat(64),
918
1061
  };
919
1062
 
920
- const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
1063
+ const multisig = createTestMultisig(config);
921
1064
 
922
1065
  const mockProposals = [
923
1066
  {
@@ -964,7 +1107,7 @@ describe('Multisig', () => {
964
1107
  guardianCommitment: '0x' + 'c'.repeat(64),
965
1108
  };
966
1109
 
967
- const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
1110
+ const multisig = createTestMultisig(config);
968
1111
 
969
1112
  const mockProposals = [
970
1113
  {
@@ -1018,7 +1161,7 @@ describe('Multisig', () => {
1018
1161
  guardianCommitment: '0x' + 'c'.repeat(64),
1019
1162
  };
1020
1163
 
1021
- const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
1164
+ const multisig = createTestMultisig(config);
1022
1165
  expect(multisig.listProposals()).toEqual([]);
1023
1166
  });
1024
1167
  });
@@ -1031,7 +1174,7 @@ describe('Multisig', () => {
1031
1174
  guardianCommitment: '0x' + 'c'.repeat(64),
1032
1175
  };
1033
1176
 
1034
- const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
1177
+ const multisig = createTestMultisig(config);
1035
1178
 
1036
1179
  const mockDelta = {
1037
1180
  account_id: '0x' + 'a'.repeat(30),
@@ -1075,7 +1218,7 @@ describe('Multisig', () => {
1075
1218
  guardianCommitment: '0x' + 'c'.repeat(64),
1076
1219
  };
1077
1220
 
1078
- const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
1221
+ const multisig = createTestMultisig(config);
1079
1222
 
1080
1223
  const mockDelta = {
1081
1224
  account_id: '0x' + 'a'.repeat(30),
@@ -1120,7 +1263,7 @@ describe('Multisig', () => {
1120
1263
  guardianCommitment: '0x' + 'c'.repeat(64),
1121
1264
  };
1122
1265
 
1123
- const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
1266
+ const multisig = createTestMultisig(config);
1124
1267
 
1125
1268
  const mockDelta = {
1126
1269
  account_id: '0x' + 'a'.repeat(30),
@@ -1179,7 +1322,7 @@ describe('Multisig', () => {
1179
1322
  guardianCommitment: '0x' + 'c'.repeat(64),
1180
1323
  };
1181
1324
 
1182
- const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
1325
+ const multisig = createTestMultisig(config);
1183
1326
 
1184
1327
  const mockDelta = {
1185
1328
  account_id: '0x' + 'a'.repeat(30),
@@ -1216,6 +1359,230 @@ describe('Multisig', () => {
1216
1359
 
1217
1360
  expect(proposal.metadata.description).toBe('Send 100 of asset 0xfaucet... to 0xrecipien...');
1218
1361
  });
1362
+
1363
+ it('threads a private noteType into the request and wire metadata (issue #322)', async () => {
1364
+ const { executeForSummary, buildP2idTransactionRequest } = await import('./transaction.js');
1365
+ const { NoteType } = await import('@miden-sdk/miden-sdk');
1366
+ vi.mocked(executeForSummary).mockResolvedValue({
1367
+ toCommitment: () => ({
1368
+ toHex: () => '0x' + 'c'.repeat(64),
1369
+ }),
1370
+ serialize: () => new Uint8Array([1, 2, 3]),
1371
+ } as any);
1372
+
1373
+ const config = {
1374
+ threshold: 1,
1375
+ signerCommitments: ['0x' + 'a'.repeat(64)],
1376
+ guardianCommitment: '0x' + 'c'.repeat(64),
1377
+ };
1378
+
1379
+ const multisig = createTestMultisig(config);
1380
+
1381
+ const mockDelta = {
1382
+ account_id: '0x' + 'a'.repeat(30),
1383
+ nonce: 1,
1384
+ prev_commitment: '0x' + 'b'.repeat(64),
1385
+ delta_payload: {
1386
+ tx_summary: { data: 'AQID' },
1387
+ signatures: [],
1388
+ metadata: {
1389
+ proposal_type: 'p2id',
1390
+ recipient_id: '0xrecipient',
1391
+ faucet_id: '0xfaucet',
1392
+ amount: '100',
1393
+ note_type: 'private',
1394
+ description: '',
1395
+ },
1396
+ },
1397
+ status: {
1398
+ status: 'pending',
1399
+ timestamp: '2024-01-01T00:00:00Z',
1400
+ proposer_id: '0x' + 'c'.repeat(64),
1401
+ cosigner_sigs: [],
1402
+ },
1403
+ };
1404
+
1405
+ mockFetch.mockResolvedValueOnce({
1406
+ ok: true,
1407
+ json: async () => ({
1408
+ delta: mockDelta,
1409
+ commitment: '0x' + 'c'.repeat(64),
1410
+ }),
1411
+ });
1412
+
1413
+ const proposal = await multisig.createP2idProposal('0xrecipient', '0xfaucet', 100n, 1, {
1414
+ noteType: NoteType.Private,
1415
+ });
1416
+
1417
+ // Propose path builds the private note...
1418
+ expect(vi.mocked(buildP2idTransactionRequest)).toHaveBeenCalledWith(
1419
+ expect.any(String),
1420
+ '0xrecipient',
1421
+ '0xfaucet',
1422
+ 100n,
1423
+ expect.anything(),
1424
+ { noteType: NoteType.Private },
1425
+ );
1426
+ // ...and the rebuild-from-metadata path parses note_type back to Private.
1427
+ const lastCall = vi.mocked(buildP2idTransactionRequest).mock.calls.at(-1)!;
1428
+ expect(lastCall[5]).toMatchObject({ noteType: NoteType.Private });
1429
+
1430
+ // The pushed wire metadata carries note_type so cosigners rebuild the
1431
+ // same private note at verification/execution.
1432
+ const pushBody = JSON.parse(mockFetch.mock.calls.at(-1)![1].body as string);
1433
+ expect(pushBody.delta_payload.metadata.note_type).toBe('private');
1434
+
1435
+ expect(proposal.metadata.proposalType).toBe('p2id');
1436
+ expect((proposal.metadata as { noteType?: string }).noteType).toBe('private');
1437
+ });
1438
+ });
1439
+
1440
+ describe('exportNoteToBytes / importNoteFromBytes (issue #356)', () => {
1441
+ const config = {
1442
+ threshold: 1,
1443
+ signerCommitments: ['0x' + '1'.repeat(64)],
1444
+ guardianCommitment: '0x' + '3'.repeat(64),
1445
+ };
1446
+
1447
+ it('exports the full note with proof when the inclusion proof is known', async () => {
1448
+ const noteFile = { serialize: () => new Uint8Array([9, 9, 9]) };
1449
+ mockWebClient.getOutputNote = vi.fn().mockResolvedValue({
1450
+ inclusionProof: () => ({}),
1451
+ });
1452
+ mockWebClient.exportNoteFile = vi.fn().mockResolvedValue(noteFile);
1453
+
1454
+ const multisig = createTestMultisig(config);
1455
+ const bytes = await multisig.exportNoteToBytes('0x' + 'ab'.repeat(32));
1456
+
1457
+ expect(bytes).toEqual(new Uint8Array([9, 9, 9]));
1458
+ // NoteExportFormat.Full = 1 in the SDK mock
1459
+ expect(mockWebClient.exportNoteFile).toHaveBeenCalledWith('0x' + 'ab'.repeat(32), 1);
1460
+ });
1461
+
1462
+ it('falls back to a details-only export before the note commits on chain', async () => {
1463
+ const noteFile = { serialize: () => new Uint8Array([7]) };
1464
+ mockWebClient.getOutputNote = vi.fn().mockResolvedValue({
1465
+ inclusionProof: () => undefined,
1466
+ });
1467
+ mockWebClient.exportNoteFile = vi.fn().mockResolvedValue(noteFile);
1468
+
1469
+ const multisig = createTestMultisig(config);
1470
+ await multisig.exportNoteToBytes(' 0x' + 'ab'.repeat(32) + ' ');
1471
+
1472
+ // NoteExportFormat.Details = 2 in the SDK mock; the id is trimmed
1473
+ expect(mockWebClient.exportNoteFile).toHaveBeenCalledWith('0x' + 'ab'.repeat(32), 2);
1474
+ });
1475
+
1476
+ it('rejects exporting a note the local store does not know', async () => {
1477
+ mockWebClient.getOutputNote = vi.fn().mockRejectedValue(new Error('no such note'));
1478
+ mockWebClient.exportNoteFile = vi.fn();
1479
+
1480
+ const multisig = createTestMultisig(config);
1481
+ await expect(multisig.exportNoteToBytes('0x' + 'ab'.repeat(32))).rejects.toThrow(
1482
+ /not found in the local store/,
1483
+ );
1484
+ expect(mockWebClient.exportNoteFile).not.toHaveBeenCalled();
1485
+ });
1486
+
1487
+ it('rejects exporting when the store resolves no record', async () => {
1488
+ mockWebClient.getOutputNote = vi.fn().mockResolvedValue(undefined);
1489
+ mockWebClient.exportNoteFile = vi.fn();
1490
+
1491
+ const multisig = createTestMultisig(config);
1492
+ await expect(multisig.exportNoteToBytes('0x' + 'ab'.repeat(32))).rejects.toThrow(
1493
+ /not found in the local store/,
1494
+ );
1495
+ expect(mockWebClient.exportNoteFile).not.toHaveBeenCalled();
1496
+ });
1497
+
1498
+ it('imports note file bytes and returns the resolved identifier', async () => {
1499
+ const decoded = { marker: 'note-file' };
1500
+ mockNoteFileDeserialize.mockReturnValue(decoded);
1501
+ mockWebClient.importNoteFile = vi.fn().mockResolvedValue('0x' + 'cd'.repeat(32));
1502
+
1503
+ const multisig = createTestMultisig(config);
1504
+ const noteId = await multisig.importNoteFromBytes(new Uint8Array([1, 2, 3]));
1505
+
1506
+ expect(mockNoteFileDeserialize).toHaveBeenCalledWith(new Uint8Array([1, 2, 3]));
1507
+ expect(mockWebClient.importNoteFile).toHaveBeenCalledWith(decoded);
1508
+ expect(noteId).toBe('0x' + 'cd'.repeat(32));
1509
+ });
1510
+
1511
+ it('rejects bytes that do not decode as a note file', async () => {
1512
+ mockNoteFileDeserialize.mockImplementation(() => {
1513
+ throw new Error('bad bytes');
1514
+ });
1515
+ mockWebClient.importNoteFile = vi.fn();
1516
+
1517
+ const multisig = createTestMultisig(config);
1518
+ await expect(multisig.importNoteFromBytes(new Uint8Array([0]))).rejects.toThrow(
1519
+ /failed to decode note file: bad bytes/,
1520
+ );
1521
+ expect(mockWebClient.importNoteFile).not.toHaveBeenCalled();
1522
+ });
1523
+ });
1524
+
1525
+ describe('exportNoteToFile / importNoteFromFile (issue #356)', () => {
1526
+ const config = {
1527
+ threshold: 1,
1528
+ signerCommitments: ['0x' + '1'.repeat(64)],
1529
+ guardianCommitment: '0x' + '3'.repeat(64),
1530
+ };
1531
+
1532
+ it('rejects exportNoteToFile outside a browser environment', async () => {
1533
+ const multisig = createTestMultisig(config);
1534
+ await expect(multisig.exportNoteToFile('0x' + 'ab'.repeat(32))).rejects.toThrow(
1535
+ /requires a browser environment/,
1536
+ );
1537
+ });
1538
+
1539
+ it('imports from a File/Blob by delegating to importNoteFromBytes', async () => {
1540
+ const decoded = { marker: 'note-file' };
1541
+ mockNoteFileDeserialize.mockReturnValue(decoded);
1542
+ mockWebClient.importNoteFile = vi.fn().mockResolvedValue('0x' + 'cd'.repeat(32));
1543
+
1544
+ const multisig = createTestMultisig(config);
1545
+ const noteId = await multisig.importNoteFromFile(new Blob([new Uint8Array([1, 2, 3])]));
1546
+
1547
+ expect(mockNoteFileDeserialize).toHaveBeenCalledWith(new Uint8Array([1, 2, 3]));
1548
+ expect(noteId).toBe('0x' + 'cd'.repeat(32));
1549
+ });
1550
+ });
1551
+
1552
+ describe('getP2idNoteId (issue #356)', () => {
1553
+ const config = {
1554
+ threshold: 1,
1555
+ signerCommitments: ['0x' + '1'.repeat(64)],
1556
+ guardianCommitment: '0x' + '3'.repeat(64),
1557
+ };
1558
+
1559
+ it('computes the deterministic note ID from p2id proposal metadata', async () => {
1560
+ const multisig = createTestMultisig(config);
1561
+ const proposal = {
1562
+ metadata: {
1563
+ proposalType: 'p2id',
1564
+ recipientId: '0x' + 'b'.repeat(30),
1565
+ faucetId: '0x' + 'c'.repeat(30),
1566
+ amount: '100',
1567
+ saltHex: '0x' + 'd'.repeat(64),
1568
+ noteType: 'private',
1569
+ },
1570
+ } as any;
1571
+
1572
+ const noteId = await multisig.getP2idNoteId(proposal);
1573
+ expect(noteId).toBe('0x' + 'ab'.repeat(32));
1574
+ });
1575
+
1576
+ it('rejects non-p2id proposals', async () => {
1577
+ const multisig = createTestMultisig(config);
1578
+ const proposal = {
1579
+ metadata: { proposalType: 'consume_notes' },
1580
+ } as any;
1581
+
1582
+ await expect(multisig.getP2idNoteId(proposal)).rejects.toThrow(
1583
+ /requires a P2ID proposal/,
1584
+ );
1585
+ });
1219
1586
  });
1220
1587
 
1221
1588
  describe('createChangeThresholdProposal', () => {
@@ -1269,7 +1636,7 @@ describe('Multisig', () => {
1269
1636
  }),
1270
1637
  });
1271
1638
 
1272
- const multisig = new Multisig(mockAccount, config, guardian, ecdsaSigner, mockWebClient);
1639
+ const multisig = createTestMultisig(config, ecdsaSigner);
1273
1640
  await multisig.createChangeThresholdProposal(2, 1);
1274
1641
 
1275
1642
  expect(buildUpdateSignersTransactionRequest).toHaveBeenCalledWith(
@@ -1293,7 +1660,7 @@ describe('Multisig', () => {
1293
1660
  guardianCommitment: '0x' + 'c'.repeat(64),
1294
1661
  };
1295
1662
 
1296
- const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
1663
+ const multisig = createTestMultisig(config);
1297
1664
  const newGuardianPubkey = '0x' + '1'.repeat(64);
1298
1665
 
1299
1666
  mockFetch.mockResolvedValueOnce({
@@ -1342,7 +1709,7 @@ describe('Multisig', () => {
1342
1709
  guardianCommitment: '0x' + 'c'.repeat(64),
1343
1710
  };
1344
1711
 
1345
- const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
1712
+ const multisig = createTestMultisig(config);
1346
1713
 
1347
1714
  mockFetch.mockResolvedValueOnce({
1348
1715
  ok: true,
@@ -1372,7 +1739,7 @@ describe('Multisig', () => {
1372
1739
  guardianCommitment: '0x' + 'c'.repeat(64),
1373
1740
  };
1374
1741
 
1375
- const multisig = new Multisig(mockAccount, config, guardian, ecdsaSigner, mockWebClient);
1742
+ const multisig = createTestMultisig(config, ecdsaSigner);
1376
1743
  const newGuardianCommitment = '0x' + '1'.repeat(64);
1377
1744
 
1378
1745
  mockFetch.mockResolvedValueOnce({
@@ -1427,7 +1794,7 @@ describe('Multisig', () => {
1427
1794
  guardianCommitment: '0x' + 'c'.repeat(64),
1428
1795
  };
1429
1796
 
1430
- const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
1797
+ const multisig = createTestMultisig(config);
1431
1798
 
1432
1799
  const mockDelta = {
1433
1800
  account_id: '0x' + 'a'.repeat(30),
@@ -1495,7 +1862,7 @@ describe('Multisig', () => {
1495
1862
  guardianCommitment: '0x' + 'c'.repeat(64),
1496
1863
  };
1497
1864
 
1498
- const multisig = new Multisig(mockAccount, config, guardian, ecdsaSigner, mockWebClient);
1865
+ const multisig = createTestMultisig(config, ecdsaSigner);
1499
1866
 
1500
1867
  const mockDelta = {
1501
1868
  account_id: '0x' + 'a'.repeat(30),
@@ -1546,7 +1913,7 @@ describe('Multisig', () => {
1546
1913
  guardianCommitment: '0x' + 'c'.repeat(64),
1547
1914
  };
1548
1915
 
1549
- const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
1916
+ const multisig = createTestMultisig(config);
1550
1917
 
1551
1918
  // First create a proposal
1552
1919
  const mockDelta = {
@@ -1624,7 +1991,7 @@ describe('Multisig', () => {
1624
1991
  guardianCommitment: '0x' + 'c'.repeat(64),
1625
1992
  };
1626
1993
 
1627
- const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
1994
+ const multisig = createTestMultisig(config);
1628
1995
 
1629
1996
  const mockDelta = {
1630
1997
  account_id: '0x' + 'a'.repeat(30),
@@ -1675,7 +2042,7 @@ describe('Multisig', () => {
1675
2042
  guardianCommitment: '0x' + 'c'.repeat(64),
1676
2043
  };
1677
2044
 
1678
- const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
2045
+ const multisig = createTestMultisig(config);
1679
2046
  const proposalId = '0x' + 'd'.repeat(64);
1680
2047
 
1681
2048
  mockFetch.mockResolvedValueOnce({
@@ -1722,7 +2089,7 @@ describe('Multisig', () => {
1722
2089
  guardianCommitment: '0x' + 'c'.repeat(64),
1723
2090
  };
1724
2091
 
1725
- const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
2092
+ const multisig = createTestMultisig(config);
1726
2093
 
1727
2094
  vi.mocked(executeForSummary).mockResolvedValueOnce({
1728
2095
  toCommitment: () => ({
@@ -1758,7 +2125,7 @@ describe('Multisig', () => {
1758
2125
  guardianCommitment: '0x' + 'c'.repeat(64),
1759
2126
  };
1760
2127
 
1761
- const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
2128
+ const multisig = createTestMultisig(config);
1762
2129
 
1763
2130
  vi.mocked(executeForSummary).mockResolvedValueOnce({
1764
2131
  toCommitment: () => ({
@@ -1809,7 +2176,7 @@ describe('Multisig', () => {
1809
2176
  guardianCommitment: '0x' + 'c'.repeat(64),
1810
2177
  };
1811
2178
 
1812
- const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
2179
+ const multisig = createTestMultisig(config);
1813
2180
 
1814
2181
  const mockProposals = [
1815
2182
  {
@@ -1862,7 +2229,7 @@ describe('Multisig', () => {
1862
2229
  guardianCommitment: '0x' + 'c'.repeat(64),
1863
2230
  };
1864
2231
 
1865
- const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
2232
+ const multisig = createTestMultisig(config);
1866
2233
  const publicKey = '0x' + 'd'.repeat(66);
1867
2234
 
1868
2235
  mockFetch.mockResolvedValueOnce({
@@ -1920,13 +2287,20 @@ describe('Multisig', () => {
1920
2287
  guardianCommitment: '0x' + 'c'.repeat(64),
1921
2288
  };
1922
2289
 
1923
- const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
2290
+ const multisig = createTestMultisig(config);
1924
2291
 
1925
2292
  mockFetch.mockResolvedValueOnce({
1926
2293
  ok: false,
1927
2294
  status: 404,
1928
2295
  statusText: 'Not Found',
1929
- text: async () => 'Proposal not found',
2296
+ // Feature 009: only a conforming { code, message, meta } envelope is
2297
+ // folded into the error message; raw text bodies are dropped.
2298
+ text: async () =>
2299
+ JSON.stringify({
2300
+ code: 'GUARDIAN_PROPOSAL_NOT_FOUND',
2301
+ message: 'Proposal not found',
2302
+ meta: { retryable: false },
2303
+ }),
1930
2304
  });
1931
2305
 
1932
2306
  await expect(
@@ -1943,7 +2317,7 @@ describe('Multisig', () => {
1943
2317
  guardianCommitment: '0x' + 'c'.repeat(64),
1944
2318
  };
1945
2319
 
1946
- const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
2320
+ const multisig = createTestMultisig(config);
1947
2321
 
1948
2322
  const exported = {
1949
2323
  accountId: multisig.accountId,
@@ -1976,7 +2350,7 @@ describe('Multisig', () => {
1976
2350
  guardianCommitment: '0x' + 'c'.repeat(64),
1977
2351
  };
1978
2352
 
1979
- const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
2353
+ const multisig = createTestMultisig(config);
1980
2354
  const publicKey = '0x' + 'd'.repeat(66);
1981
2355
 
1982
2356
  const proposal = await multisig.importProposal(
@@ -2023,7 +2397,7 @@ describe('Multisig', () => {
2023
2397
  guardianCommitment: '0x' + 'c'.repeat(64),
2024
2398
  };
2025
2399
 
2026
- const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
2400
+ const multisig = createTestMultisig(config);
2027
2401
 
2028
2402
  await expect(
2029
2403
  multisig.importProposal(
@@ -2057,7 +2431,7 @@ describe('Multisig', () => {
2057
2431
  guardianCommitment: '0x' + 'c'.repeat(64),
2058
2432
  };
2059
2433
 
2060
- const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
2434
+ const multisig = createTestMultisig(config);
2061
2435
 
2062
2436
  const exported = {
2063
2437
  accountId: multisig.accountId,
@@ -2102,7 +2476,7 @@ describe('Multisig', () => {
2102
2476
  publicKey: '0x' + '2'.repeat(66),
2103
2477
  };
2104
2478
 
2105
- const multisig = new Multisig(mockAccount, config, guardian, ecdsaSigner, mockWebClient);
2479
+ const multisig = createTestMultisig(config, ecdsaSigner);
2106
2480
  const cachedProposalId = '0x' + 'c'.repeat(64);
2107
2481
  const requestedProposalId = '0x' + 'C'.repeat(64);
2108
2482
  const cosignerPubkey = '0x' + '3'.repeat(66);
@@ -2225,7 +2599,7 @@ describe('Multisig', () => {
2225
2599
  guardianCommitment: '0x' + 'c'.repeat(64),
2226
2600
  };
2227
2601
 
2228
- const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
2602
+ const multisig = createTestMultisig(config);
2229
2603
  const proposalId = '0x' + 'c'.repeat(64);
2230
2604
  const newGuardianPubkey = '0x' + '1'.repeat(64);
2231
2605
  const finalRequest = { kind: 'final-switch-guardian-request' };
@@ -2280,7 +2654,7 @@ describe('Multisig', () => {
2280
2654
  guardianCommitment: '0x' + 'c'.repeat(64),
2281
2655
  };
2282
2656
 
2283
- const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
2657
+ const multisig = createTestMultisig(config);
2284
2658
 
2285
2659
  await expect(
2286
2660
  multisig.createTransactionProposalRequest('0x' + 'nonexistent'.repeat(5))
@@ -2294,7 +2668,7 @@ describe('Multisig', () => {
2294
2668
  guardianCommitment: '0x' + 'c'.repeat(64),
2295
2669
  };
2296
2670
 
2297
- const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
2671
+ const multisig = createTestMultisig(config);
2298
2672
 
2299
2673
  const mockProposals = [
2300
2674
  {
@@ -2345,7 +2719,7 @@ describe('Multisig', () => {
2345
2719
  guardianCommitment: '0x' + 'c'.repeat(64),
2346
2720
  };
2347
2721
 
2348
- const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
2722
+ const multisig = createTestMultisig(config);
2349
2723
  const proposalId = '0x' + 'c'.repeat(64);
2350
2724
 
2351
2725
  vi.mocked(executeForSummary).mockResolvedValueOnce({
@@ -2388,7 +2762,7 @@ describe('Multisig', () => {
2388
2762
  guardianCommitment: '0x' + 'c'.repeat(64),
2389
2763
  };
2390
2764
 
2391
- const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
2765
+ const multisig = createTestMultisig(config);
2392
2766
  const proposalId = '0x' + 'c'.repeat(64);
2393
2767
 
2394
2768
  (multisig as any).proposals.set(proposalId, {
@@ -2430,7 +2804,7 @@ describe('Multisig', () => {
2430
2804
  guardianCommitment: '0x' + 'c'.repeat(64),
2431
2805
  };
2432
2806
 
2433
- const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
2807
+ const multisig = createTestMultisig(config);
2434
2808
  const proposalId = '0x' + 'c'.repeat(64);
2435
2809
 
2436
2810
  (multisig as any).proposals.set(proposalId, {
@@ -2502,7 +2876,7 @@ describe('Multisig', () => {
2502
2876
  guardianCommitment: '0x' + 'c'.repeat(64),
2503
2877
  };
2504
2878
 
2505
- const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
2879
+ const multisig = createTestMultisig(config);
2506
2880
  const proposalId = '0x' + 'c'.repeat(64);
2507
2881
  const finalRequest = { kind: 'fresh-message-word-request' };
2508
2882
 
@@ -2601,7 +2975,7 @@ describe('Multisig', () => {
2601
2975
  guardianCommitment: '0x' + 'c'.repeat(64),
2602
2976
  };
2603
2977
 
2604
- const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
2978
+ const multisig = createTestMultisig(config);
2605
2979
  const proposalId = '0x' + 'c'.repeat(64);
2606
2980
 
2607
2981
  (multisig as any).proposals.set(proposalId, {
@@ -2644,7 +3018,7 @@ describe('Multisig', () => {
2644
3018
  guardianCommitment: '0x' + 'c'.repeat(64),
2645
3019
  };
2646
3020
 
2647
- const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
3021
+ const multisig = createTestMultisig(config);
2648
3022
 
2649
3023
  await expect(
2650
3024
  multisig.executeProposal('0x' + 'nonexistent'.repeat(5))
@@ -2658,7 +3032,7 @@ describe('Multisig', () => {
2658
3032
  guardianCommitment: '0x' + 'c'.repeat(64),
2659
3033
  };
2660
3034
 
2661
- const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
3035
+ const multisig = createTestMultisig(config);
2662
3036
 
2663
3037
  // Sync with pending proposal (only 1 signature)
2664
3038
  const mockProposals = [
@@ -2711,7 +3085,7 @@ describe('Multisig', () => {
2711
3085
  guardianCommitment: '0x' + 'c'.repeat(64),
2712
3086
  };
2713
3087
 
2714
- const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
3088
+ const multisig = createTestMultisig(config);
2715
3089
 
2716
3090
  const readyDelta = {
2717
3091
  account_id: '0x' + 'a'.repeat(30),
@@ -2784,7 +3158,11 @@ describe('Multisig', () => {
2784
3158
  publicKey: '0x' + '2'.repeat(66),
2785
3159
  };
2786
3160
 
2787
- const multisig = new Multisig(mockAccount, config, guardian, ecdsaSigner, mockWebClient);
3161
+ const multisig = createTestMultisig(config, ecdsaSigner, undefined, {
3162
+ kind: 'remote',
3163
+ maxAttempts: 2,
3164
+ createProver: () => ({} as never),
3165
+ });
2788
3166
  const proposalId = '0x' + 'c'.repeat(64);
2789
3167
  const cosignerPubkey = '0x' + '3'.repeat(66);
2790
3168
  const ackPubkey = '0x' + '4'.repeat(66);
@@ -2849,10 +3227,21 @@ describe('Multisig', () => {
2849
3227
  ack_scheme: 'ecdsa',
2850
3228
  }),
2851
3229
  });
2852
- mockWebClient.transactions.submit.mockResolvedValueOnce({});
2853
-
2854
- await expect(multisig.executeProposal(proposalId)).resolves.toBeUndefined();
2855
- expect(mockWebClient.transactions.submit).toHaveBeenCalledTimes(1);
3230
+ mockWebClient.proveTransaction
3231
+ .mockRejectedValueOnce(Object.assign(new Error('unavailable'), { code: 'Unavailable' }))
3232
+ .mockResolvedValueOnce({});
3233
+ vi.useFakeTimers();
3234
+ try {
3235
+ const execution = multisig.executeProposal(proposalId);
3236
+ await vi.runAllTimersAsync();
3237
+ await expect(execution).resolves.toBeUndefined();
3238
+ } finally {
3239
+ vi.useRealTimers();
3240
+ }
3241
+ expect(mockWebClient.executeTransaction).toHaveBeenCalledTimes(1);
3242
+ expect(mockWebClient.proveTransaction).toHaveBeenCalledTimes(2);
3243
+ expect(mockWebClient.submitProvenTransaction).toHaveBeenCalledTimes(1);
3244
+ expect(mockWebClient.applyTransaction).toHaveBeenCalledTimes(1);
2856
3245
 
2857
3246
  expect(vi.mocked(signatureHexToBytes)).toHaveBeenNthCalledWith(
2858
3247
  1,
@@ -2900,7 +3289,7 @@ describe('Multisig', () => {
2900
3289
  publicKey: '0x' + '2'.repeat(66),
2901
3290
  };
2902
3291
 
2903
- const multisig = new Multisig(mockAccount, config, guardian, ecdsaSigner, mockWebClient);
3292
+ const multisig = createTestMultisig(config, ecdsaSigner);
2904
3293
  const proposalId = '0x' + 'c'.repeat(64);
2905
3294
  const cosignerPubkey = '0x' + '3'.repeat(66);
2906
3295
  const ackPubkey = '0x' + '4'.repeat(66);
@@ -2964,9 +3353,11 @@ describe('Multisig', () => {
2964
3353
  ack_scheme: 'ecdsa',
2965
3354
  }),
2966
3355
  });
2967
- mockWebClient.transactions.submit.mockResolvedValueOnce({});
2968
-
2969
3356
  await expect(multisig.executeProposal(proposalId)).resolves.toBeUndefined();
3357
+ expect(mockWebClient.executeTransaction).toHaveBeenCalledTimes(1);
3358
+ expect(mockWebClient.proveTransaction).toHaveBeenCalledTimes(1);
3359
+ expect(mockWebClient.submitProvenTransaction).toHaveBeenCalledTimes(1);
3360
+ expect(mockWebClient.applyTransaction).toHaveBeenCalledTimes(1);
2970
3361
 
2971
3362
  expect(vi.mocked(signatureHexToBytes)).toHaveBeenNthCalledWith(
2972
3363
  1,
@@ -3003,7 +3394,7 @@ describe('Multisig', () => {
3003
3394
  guardianCommitment: '0x' + 'c'.repeat(64),
3004
3395
  };
3005
3396
 
3006
- const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
3397
+ const multisig = createTestMultisig(config);
3007
3398
  const proposalId = '0x' + 'c'.repeat(64);
3008
3399
  const newGuardianPubkey = '0x' + '1'.repeat(64);
3009
3400
 
@@ -3073,10 +3464,11 @@ describe('Multisig', () => {
3073
3464
  ok: true,
3074
3465
  json: async () => ({ success: true, message: 'ok', ack_pubkey: '0x' + 'f'.repeat(64) }),
3075
3466
  });
3076
- mockWebClient.transactions.submit.mockResolvedValueOnce({});
3077
-
3078
3467
  await expect(multisig.executeProposal(proposalId)).resolves.toBeUndefined();
3079
- expect(mockWebClient.transactions.submit).toHaveBeenCalledTimes(1);
3468
+ expect(mockWebClient.executeTransaction).toHaveBeenCalledTimes(1);
3469
+ expect(mockWebClient.proveTransaction).toHaveBeenCalledTimes(1);
3470
+ expect(mockWebClient.submitProvenTransaction).toHaveBeenCalledTimes(1);
3471
+ expect(mockWebClient.applyTransaction).toHaveBeenCalledTimes(1);
3080
3472
  });
3081
3473
 
3082
3474
  it('should still switch GUARDIAN when the pre-switch canonicalization push fails', async () => {
@@ -3086,7 +3478,7 @@ describe('Multisig', () => {
3086
3478
  guardianCommitment: '0x' + 'c'.repeat(64),
3087
3479
  };
3088
3480
 
3089
- const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
3481
+ const multisig = createTestMultisig(config);
3090
3482
  const proposalId = '0x' + 'c'.repeat(64);
3091
3483
  const newGuardianPubkey = '0x' + '1'.repeat(64);
3092
3484
 
@@ -3124,10 +3516,11 @@ describe('Multisig', () => {
3124
3516
  ok: true,
3125
3517
  json: async () => ({ success: true, message: 'ok', ack_pubkey: '0x' + 'f'.repeat(64) }),
3126
3518
  });
3127
- mockWebClient.transactions.submit.mockResolvedValueOnce({});
3128
-
3129
3519
  await expect(multisig.executeProposal(proposalId)).resolves.toBeUndefined();
3130
- expect(mockWebClient.transactions.submit).toHaveBeenCalledTimes(1);
3520
+ expect(mockWebClient.executeTransaction).toHaveBeenCalledTimes(1);
3521
+ expect(mockWebClient.proveTransaction).toHaveBeenCalledTimes(1);
3522
+ expect(mockWebClient.submitProvenTransaction).toHaveBeenCalledTimes(1);
3523
+ expect(mockWebClient.applyTransaction).toHaveBeenCalledTimes(1);
3131
3524
  });
3132
3525
 
3133
3526
  it('should reject switch_guardian execution when endpoint commitment mismatches', async () => {
@@ -3137,7 +3530,7 @@ describe('Multisig', () => {
3137
3530
  guardianCommitment: '0x' + 'c'.repeat(64),
3138
3531
  };
3139
3532
 
3140
- const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
3533
+ const multisig = createTestMultisig(config);
3141
3534
  const proposalId = '0x' + 'c'.repeat(64);
3142
3535
 
3143
3536
  (multisig as any).proposals.set(proposalId, {
@@ -3179,7 +3572,7 @@ describe('Multisig', () => {
3179
3572
  guardianCommitment: '0x' + 'c'.repeat(64),
3180
3573
  };
3181
3574
 
3182
- const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
3575
+ const multisig = createTestMultisig(config);
3183
3576
  const proposalId = '0x' + 'c'.repeat(64);
3184
3577
 
3185
3578
  (multisig as any).proposals.set(proposalId, {
@@ -3231,7 +3624,7 @@ describe('Multisig', () => {
3231
3624
  guardianCommitment: '0x' + 'c'.repeat(64),
3232
3625
  };
3233
3626
 
3234
- const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
3627
+ const multisig = createTestMultisig(config);
3235
3628
  const proposalId = '0x' + 'c'.repeat(64);
3236
3629
 
3237
3630
  (multisig as any).proposals.set(proposalId, {
@@ -3266,6 +3659,45 @@ describe('Multisig', () => {
3266
3659
  });
3267
3660
  });
3268
3661
 
3662
+ describe('submitTransaction', () => {
3663
+ it('uses the configured total proof-attempt budget without repeating other stages', async () => {
3664
+ const multisig = createTestMultisig(
3665
+ {
3666
+ threshold: 1,
3667
+ signerCommitments: ['0x' + 'a'.repeat(64)],
3668
+ guardianCommitment: '0x' + 'c'.repeat(64),
3669
+ },
3670
+ mockSigner,
3671
+ undefined,
3672
+ {
3673
+ kind: 'remote',
3674
+ maxAttempts: 4,
3675
+ createProver: () => ({} as never),
3676
+ },
3677
+ );
3678
+ const transient = Object.assign(new Error('unavailable'), { code: 'Unavailable' });
3679
+ mockWebClient.proveTransaction
3680
+ .mockRejectedValueOnce(transient)
3681
+ .mockRejectedValueOnce(transient)
3682
+ .mockRejectedValueOnce(transient)
3683
+ .mockResolvedValueOnce({});
3684
+
3685
+ vi.useFakeTimers();
3686
+ try {
3687
+ const submission = multisig.submitTransaction({} as never);
3688
+ await vi.runAllTimersAsync();
3689
+ await submission;
3690
+ } finally {
3691
+ vi.useRealTimers();
3692
+ }
3693
+
3694
+ expect(mockWebClient.executeTransaction).toHaveBeenCalledTimes(1);
3695
+ expect(mockWebClient.proveTransaction).toHaveBeenCalledTimes(4);
3696
+ expect(mockWebClient.submitProvenTransaction).toHaveBeenCalledTimes(1);
3697
+ expect(mockWebClient.applyTransaction).toHaveBeenCalledTimes(1);
3698
+ });
3699
+ });
3700
+
3269
3701
  describe('prepareCustomExecution', () => {
3270
3702
  const requestBytes = new Uint8Array([9, 8, 7]);
3271
3703
 
@@ -3308,7 +3740,7 @@ describe('Multisig', () => {
3308
3740
  signerCommitments: ['0x' + 'a'.repeat(64)],
3309
3741
  guardianCommitment: '0x' + 'c'.repeat(64),
3310
3742
  };
3311
- const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
3743
+ const multisig = createTestMultisig(config);
3312
3744
 
3313
3745
  const builtinDelta = {
3314
3746
  ...customDelta('change_threshold', [falconSig('0x' + 'a'.repeat(64))]),
@@ -3336,7 +3768,7 @@ describe('Multisig', () => {
3336
3768
  signerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
3337
3769
  guardianCommitment: '0x' + 'c'.repeat(64),
3338
3770
  };
3339
- const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
3771
+ const multisig = createTestMultisig(config);
3340
3772
 
3341
3773
  mockFetch.mockResolvedValueOnce({
3342
3774
  ok: true,
@@ -3354,7 +3786,7 @@ describe('Multisig', () => {
3354
3786
  signerCommitments: ['0x' + 'a'.repeat(64)],
3355
3787
  guardianCommitment: '0x' + 'c'.repeat(64),
3356
3788
  };
3357
- const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
3789
+ const multisig = createTestMultisig(config);
3358
3790
 
3359
3791
  // Signed commitment comes from TransactionSummary.deserialize -> 'c' * 64.
3360
3792
  // Make the binding request derive a different commitment so the check fails.
@@ -3381,7 +3813,7 @@ describe('Multisig', () => {
3381
3813
  signerCommitments: ['0x' + 'a'.repeat(64)],
3382
3814
  guardianCommitment: '0x' + 'c'.repeat(64),
3383
3815
  };
3384
- const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
3816
+ const multisig = createTestMultisig(config);
3385
3817
 
3386
3818
  const ready = customDelta('b2agg', [falconSig('0x' + 'a'.repeat(64))]);
3387
3819
 
@@ -3410,7 +3842,7 @@ describe('Multisig', () => {
3410
3842
  guardianCommitment: '0x' + 'c'.repeat(64),
3411
3843
  };
3412
3844
 
3413
- const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
3845
+ const multisig = createTestMultisig(config);
3414
3846
 
3415
3847
  // Create a proposal with metadata
3416
3848
  const mockDelta = {
@@ -3472,7 +3904,7 @@ describe('Multisig', () => {
3472
3904
  guardianCommitment: '0x' + 'c'.repeat(64),
3473
3905
  };
3474
3906
 
3475
- const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
3907
+ const multisig = createTestMultisig(config);
3476
3908
 
3477
3909
  // Sync proposals - no local proposals exist
3478
3910
  const mockProposals = [
@@ -3519,7 +3951,7 @@ describe('Multisig', () => {
3519
3951
  guardianCommitment: '0x' + 'c'.repeat(64),
3520
3952
  };
3521
3953
 
3522
- const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
3954
+ const multisig = createTestMultisig(config);
3523
3955
 
3524
3956
  const mockDelta = {
3525
3957
  account_id: '0x' + 'a'.repeat(30),
@@ -3567,7 +3999,7 @@ describe('Multisig', () => {
3567
3999
  guardianCommitment: '0x' + 'c'.repeat(64),
3568
4000
  };
3569
4001
 
3570
- const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
4002
+ const multisig = createTestMultisig(config);
3571
4003
 
3572
4004
  const mockDelta = {
3573
4005
  account_id: '0x' + 'a'.repeat(30),
@@ -3617,7 +4049,7 @@ describe('Multisig', () => {
3617
4049
  guardianCommitment: '0x' + 'c'.repeat(64),
3618
4050
  };
3619
4051
 
3620
- const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
4052
+ const multisig = createTestMultisig(config);
3621
4053
 
3622
4054
  const mockDelta = {
3623
4055
  account_id: '0x' + 'a'.repeat(30),
@@ -3668,7 +4100,7 @@ describe('Multisig', () => {
3668
4100
  guardianCommitment: '0x' + 'c'.repeat(64),
3669
4101
  };
3670
4102
 
3671
- const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
4103
+ const multisig = createTestMultisig(config);
3672
4104
 
3673
4105
  // First sync with 1 signature (pending)
3674
4106
  const mockProposalsPending = [
@@ -3760,7 +4192,7 @@ describe('Multisig', () => {
3760
4192
  guardianCommitment: '0x' + 'd'.repeat(64),
3761
4193
  };
3762
4194
 
3763
- const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
4195
+ const multisig = createTestMultisig(config);
3764
4196
  expect(multisig.threshold).toBe(3);
3765
4197
  });
3766
4198
 
@@ -3772,7 +4204,7 @@ describe('Multisig', () => {
3772
4204
  guardianCommitment: '0x' + 'd'.repeat(64),
3773
4205
  };
3774
4206
 
3775
- const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
4207
+ const multisig = createTestMultisig(config);
3776
4208
  expect(multisig.signerCommitments).toEqual(signerCommitments);
3777
4209
  });
3778
4210
 
@@ -3784,7 +4216,7 @@ describe('Multisig', () => {
3784
4216
  guardianCommitment,
3785
4217
  };
3786
4218
 
3787
- const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
4219
+ const multisig = createTestMultisig(config);
3788
4220
  expect(multisig.guardianCommitment).toBe(guardianCommitment);
3789
4221
  });
3790
4222
 
@@ -3795,7 +4227,7 @@ describe('Multisig', () => {
3795
4227
  guardianCommitment: '0x' + 'd'.repeat(64),
3796
4228
  };
3797
4229
 
3798
- const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
4230
+ const multisig = createTestMultisig(config);
3799
4231
  expect(multisig.account).toBe(mockAccount);
3800
4232
  });
3801
4233
  });
@@ -3808,7 +4240,7 @@ describe('Multisig', () => {
3808
4240
  guardianCommitment: '0x' + 'c'.repeat(64),
3809
4241
  };
3810
4242
 
3811
- const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
4243
+ const multisig = createTestMultisig(config);
3812
4244
 
3813
4245
  // Simulates a GUARDIAN response with canonical snake_case metadata
3814
4246
  const rustProposals = [
@@ -3858,7 +4290,7 @@ describe('Multisig', () => {
3858
4290
  guardianCommitment: '0x' + 'c'.repeat(64),
3859
4291
  };
3860
4292
 
3861
- const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
4293
+ const multisig = createTestMultisig(config);
3862
4294
 
3863
4295
  // P2ID proposal with canonical snake_case fields
3864
4296
  const p2idProposals = [
@@ -3915,7 +4347,7 @@ describe('Multisig', () => {
3915
4347
  guardianCommitment: '0x' + 'c'.repeat(64),
3916
4348
  };
3917
4349
 
3918
- const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
4350
+ const multisig = createTestMultisig(config);
3919
4351
 
3920
4352
  const switchGuardianProposals = [
3921
4353
  {