@openzeppelin/miden-multisig-client 0.16.0 → 0.16.2

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 (123) hide show
  1. package/README.md +58 -2
  2. package/dist/client.d.ts +12 -0
  3. package/dist/client.d.ts.map +1 -1
  4. package/dist/client.js +12 -2
  5. package/dist/client.js.map +1 -1
  6. package/dist/client.test.js +7 -2
  7. package/dist/client.test.js.map +1 -1
  8. package/dist/index.d.ts +6 -0
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +4 -0
  11. package/dist/index.js.map +1 -1
  12. package/dist/multisig.d.ts +65 -2
  13. package/dist/multisig.d.ts.map +1 -1
  14. package/dist/multisig.js +148 -15
  15. package/dist/multisig.js.map +1 -1
  16. package/dist/multisig.test.js +217 -13
  17. package/dist/multisig.test.js.map +1 -1
  18. package/dist/proposal/factory.d.ts.map +1 -1
  19. package/dist/proposal/factory.js +10 -0
  20. package/dist/proposal/factory.js.map +1 -1
  21. package/dist/proposal/factory.test.d.ts +2 -0
  22. package/dist/proposal/factory.test.d.ts.map +1 -0
  23. package/dist/proposal/factory.test.js +32 -0
  24. package/dist/proposal/factory.test.js.map +1 -0
  25. package/dist/prover/config.d.ts +16 -0
  26. package/dist/prover/config.d.ts.map +1 -0
  27. package/dist/prover/config.js +54 -0
  28. package/dist/prover/config.js.map +1 -0
  29. package/dist/prover/config.test.d.ts +2 -0
  30. package/dist/prover/config.test.d.ts.map +1 -0
  31. package/dist/prover/config.test.js +54 -0
  32. package/dist/prover/config.test.js.map +1 -0
  33. package/dist/prover/errors.d.ts +7 -0
  34. package/dist/prover/errors.d.ts.map +1 -0
  35. package/dist/prover/errors.js +10 -0
  36. package/dist/prover/errors.js.map +1 -0
  37. package/dist/prover/errors.test.d.ts +2 -0
  38. package/dist/prover/errors.test.d.ts.map +1 -0
  39. package/dist/prover/errors.test.js +29 -0
  40. package/dist/prover/errors.test.js.map +1 -0
  41. package/dist/prover/retry.d.ts +5 -0
  42. package/dist/prover/retry.d.ts.map +1 -0
  43. package/dist/prover/retry.js +9 -0
  44. package/dist/prover/retry.js.map +1 -0
  45. package/dist/prover/retry.test.d.ts +2 -0
  46. package/dist/prover/retry.test.d.ts.map +1 -0
  47. package/dist/prover/retry.test.js +20 -0
  48. package/dist/prover/retry.test.js.map +1 -0
  49. package/dist/prover/workflow.d.ts +11 -0
  50. package/dist/prover/workflow.d.ts.map +1 -0
  51. package/dist/prover/workflow.js +18 -0
  52. package/dist/prover/workflow.js.map +1 -0
  53. package/dist/prover/workflow.test.d.ts +2 -0
  54. package/dist/prover/workflow.test.d.ts.map +1 -0
  55. package/dist/prover/workflow.test.js +103 -0
  56. package/dist/prover/workflow.test.js.map +1 -0
  57. package/dist/retry/classify.d.ts +16 -0
  58. package/dist/retry/classify.d.ts.map +1 -0
  59. package/dist/retry/classify.js +187 -0
  60. package/dist/retry/classify.js.map +1 -0
  61. package/dist/retry/runtime.d.ts +13 -0
  62. package/dist/retry/runtime.d.ts.map +1 -0
  63. package/dist/retry/runtime.js +33 -0
  64. package/dist/retry/runtime.js.map +1 -0
  65. package/dist/rpc/config.d.ts +11 -0
  66. package/dist/rpc/config.d.ts.map +1 -0
  67. package/dist/rpc/config.js +17 -0
  68. package/dist/rpc/config.js.map +1 -0
  69. package/dist/rpc/config.test.d.ts +2 -0
  70. package/dist/rpc/config.test.d.ts.map +1 -0
  71. package/dist/rpc/config.test.js +24 -0
  72. package/dist/rpc/config.test.js.map +1 -0
  73. package/dist/rpc/errors.d.ts +2 -0
  74. package/dist/rpc/errors.d.ts.map +1 -0
  75. package/dist/rpc/errors.js +11 -0
  76. package/dist/rpc/errors.js.map +1 -0
  77. package/dist/rpc/errors.test.d.ts +2 -0
  78. package/dist/rpc/errors.test.d.ts.map +1 -0
  79. package/dist/rpc/errors.test.js +34 -0
  80. package/dist/rpc/errors.test.js.map +1 -0
  81. package/dist/rpc/retry.d.ts +4 -0
  82. package/dist/rpc/retry.d.ts.map +1 -0
  83. package/dist/rpc/retry.js +6 -0
  84. package/dist/rpc/retry.js.map +1 -0
  85. package/dist/rpc/retry.test.d.ts +2 -0
  86. package/dist/rpc/retry.test.d.ts.map +1 -0
  87. package/dist/rpc/retry.test.js +98 -0
  88. package/dist/rpc/retry.test.js.map +1 -0
  89. package/dist/transaction/p2id.d.ts +8 -1
  90. package/dist/transaction/p2id.d.ts.map +1 -1
  91. package/dist/transaction/p2id.js +12 -3
  92. package/dist/transaction/p2id.js.map +1 -1
  93. package/dist/transaction.d.ts +1 -1
  94. package/dist/transaction.d.ts.map +1 -1
  95. package/dist/transaction.js +1 -1
  96. package/dist/transaction.js.map +1 -1
  97. package/package.json +2 -2
  98. package/src/client.test.ts +8 -2
  99. package/src/client.ts +28 -2
  100. package/src/index.ts +6 -0
  101. package/src/multisig.test.ts +263 -17
  102. package/src/multisig.ts +201 -14
  103. package/src/proposal/factory.test.ts +40 -0
  104. package/src/proposal/factory.ts +11 -1
  105. package/src/prover/config.test.ts +90 -0
  106. package/src/prover/config.ts +78 -0
  107. package/src/prover/errors.test.ts +61 -0
  108. package/src/prover/errors.ts +10 -0
  109. package/src/prover/retry.test.ts +38 -0
  110. package/src/prover/retry.ts +21 -0
  111. package/src/prover/test-node.d.ts +6 -0
  112. package/src/prover/workflow.test.ts +140 -0
  113. package/src/prover/workflow.ts +19 -0
  114. package/src/retry/classify.ts +220 -0
  115. package/src/retry/runtime.ts +45 -0
  116. package/src/rpc/config.test.ts +45 -0
  117. package/src/rpc/config.ts +30 -0
  118. package/src/rpc/errors.test.ts +69 -0
  119. package/src/rpc/errors.ts +12 -0
  120. package/src/rpc/retry.test.ts +144 -0
  121. package/src/rpc/retry.ts +12 -0
  122. package/src/transaction/p2id.ts +29 -10
  123. package/src/transaction.ts +1 -0
@@ -2,10 +2,11 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
2
  import { Multisig } from './multisig.js';
3
3
  import { GuardianHttpClient } from '@openzeppelin/guardian-client';
4
4
  import { buildUpdateProcedureThresholdTransactionRequest, buildUpdateGuardianTransactionRequest, buildUpdateSignersTransactionRequest, executeForSummary, } from './transaction.js';
5
- const { mockRpcGetAccountDetails, mockAccountDeserialize, mockDetectConfig } = vi.hoisted(() => ({
5
+ const { mockRpcGetAccountDetails, mockAccountDeserialize, mockDetectConfig, mockNoteFileDeserialize } = vi.hoisted(() => ({
6
6
  mockRpcGetAccountDetails: vi.fn(),
7
7
  mockAccountDeserialize: vi.fn(),
8
8
  mockDetectConfig: vi.fn(),
9
+ mockNoteFileDeserialize: vi.fn(),
9
10
  }));
10
11
  // Mock the Miden SDK
11
12
  vi.mock('@miden-sdk/miden-sdk', () => ({
@@ -19,6 +20,14 @@ vi.mock('@miden-sdk/miden-sdk', () => ({
19
20
  Private: 0,
20
21
  Public: 1,
21
22
  },
23
+ NoteExportFormat: {
24
+ Id: 0,
25
+ Full: 1,
26
+ Details: 2,
27
+ },
28
+ NoteFile: {
29
+ deserialize: mockNoteFileDeserialize,
30
+ },
22
31
  TransactionSummary: {
23
32
  deserialize: vi.fn().mockReturnValue({
24
33
  toCommitment: () => ({
@@ -83,6 +92,9 @@ vi.mock('./transaction.js', () => ({
83
92
  request: {},
84
93
  salt: { toHex: () => '0x' + 'd'.repeat(64) },
85
94
  }),
95
+ buildP2idNoteFromMetadata: vi.fn().mockReturnValue({
96
+ id: () => ({ toString: () => '0x' + 'ab'.repeat(32) }),
97
+ }),
86
98
  // Mirrors the real implementations against the mocked NoteType values
87
99
  // (Private = 0, Public = 1).
88
100
  parseP2idNoteType: vi.fn((value) => {
@@ -139,8 +151,8 @@ describe('Multisig', () => {
139
151
  let mockSigner;
140
152
  let mockAccount;
141
153
  let mockWebClient;
142
- function createTestMultisig(config, signer = mockSigner, accountId) {
143
- return new Multisig(mockAccount, config, guardian, signer, mockWebClient, accountId, MIDEN_RPC_ENDPOINT);
154
+ function createTestMultisig(config, signer = mockSigner, accountId, proverConfig) {
155
+ return new Multisig(mockAccount, config, guardian, signer, mockWebClient, accountId, MIDEN_RPC_ENDPOINT, proverConfig);
144
156
  }
145
157
  beforeEach(() => {
146
158
  mockFetch.mockReset();
@@ -194,13 +206,36 @@ describe('Multisig', () => {
194
206
  submitNewTransaction: vi.fn(),
195
207
  submitNewTransactionWithProver: vi.fn(),
196
208
  transactions: {
197
- submit: vi.fn(),
209
+ executeRequest: vi.fn(),
198
210
  },
199
211
  getConsumableNotes: vi.fn().mockResolvedValue([]),
200
212
  syncState: vi.fn(),
201
213
  getAccount: vi.fn().mockResolvedValue(null),
202
214
  newAccount: vi.fn(),
203
215
  };
216
+ mockWebClient.transactions.executeRequest.mockImplementation(async (accountId, request) => {
217
+ const result = await mockWebClient.executeTransaction(accountId, request);
218
+ return {
219
+ result,
220
+ prove: async (options) => {
221
+ const proof = options?.prover === undefined
222
+ ? await mockWebClient.proveTransaction(result)
223
+ : await mockWebClient.proveTransaction(result, options.prover);
224
+ return {
225
+ proof,
226
+ result,
227
+ submit: async () => {
228
+ const blockNumber = await mockWebClient.submitProvenTransaction(proof, result);
229
+ return {
230
+ blockNumber,
231
+ result,
232
+ apply: () => mockWebClient.applyTransaction(result, blockNumber),
233
+ };
234
+ },
235
+ };
236
+ },
237
+ };
238
+ });
204
239
  });
205
240
  describe('constructor', () => {
206
241
  it('should create Multisig with account', () => {
@@ -1090,6 +1125,118 @@ describe('Multisig', () => {
1090
1125
  expect(proposal.metadata.noteType).toBe('private');
1091
1126
  });
1092
1127
  });
1128
+ describe('exportNoteToBytes / importNoteFromBytes (issue #356)', () => {
1129
+ const config = {
1130
+ threshold: 1,
1131
+ signerCommitments: ['0x' + '1'.repeat(64)],
1132
+ guardianCommitment: '0x' + '3'.repeat(64),
1133
+ };
1134
+ it('exports the full note with proof when the inclusion proof is known', async () => {
1135
+ const noteFile = { serialize: () => new Uint8Array([9, 9, 9]) };
1136
+ mockWebClient.getOutputNote = vi.fn().mockResolvedValue({
1137
+ inclusionProof: () => ({}),
1138
+ });
1139
+ mockWebClient.exportNoteFile = vi.fn().mockResolvedValue(noteFile);
1140
+ const multisig = createTestMultisig(config);
1141
+ const bytes = await multisig.exportNoteToBytes('0x' + 'ab'.repeat(32));
1142
+ expect(bytes).toEqual(new Uint8Array([9, 9, 9]));
1143
+ // NoteExportFormat.Full = 1 in the SDK mock
1144
+ expect(mockWebClient.exportNoteFile).toHaveBeenCalledWith('0x' + 'ab'.repeat(32), 1);
1145
+ });
1146
+ it('falls back to a details-only export before the note commits on chain', async () => {
1147
+ const noteFile = { serialize: () => new Uint8Array([7]) };
1148
+ mockWebClient.getOutputNote = vi.fn().mockResolvedValue({
1149
+ inclusionProof: () => undefined,
1150
+ });
1151
+ mockWebClient.exportNoteFile = vi.fn().mockResolvedValue(noteFile);
1152
+ const multisig = createTestMultisig(config);
1153
+ await multisig.exportNoteToBytes(' 0x' + 'ab'.repeat(32) + ' ');
1154
+ // NoteExportFormat.Details = 2 in the SDK mock; the id is trimmed
1155
+ expect(mockWebClient.exportNoteFile).toHaveBeenCalledWith('0x' + 'ab'.repeat(32), 2);
1156
+ });
1157
+ it('rejects exporting a note the local store does not know', async () => {
1158
+ mockWebClient.getOutputNote = vi.fn().mockRejectedValue(new Error('no such note'));
1159
+ mockWebClient.exportNoteFile = vi.fn();
1160
+ const multisig = createTestMultisig(config);
1161
+ await expect(multisig.exportNoteToBytes('0x' + 'ab'.repeat(32))).rejects.toThrow(/not found in the local store/);
1162
+ expect(mockWebClient.exportNoteFile).not.toHaveBeenCalled();
1163
+ });
1164
+ it('rejects exporting when the store resolves no record', async () => {
1165
+ mockWebClient.getOutputNote = vi.fn().mockResolvedValue(undefined);
1166
+ mockWebClient.exportNoteFile = vi.fn();
1167
+ const multisig = createTestMultisig(config);
1168
+ await expect(multisig.exportNoteToBytes('0x' + 'ab'.repeat(32))).rejects.toThrow(/not found in the local store/);
1169
+ expect(mockWebClient.exportNoteFile).not.toHaveBeenCalled();
1170
+ });
1171
+ it('imports note file bytes and returns the resolved identifier', async () => {
1172
+ const decoded = { marker: 'note-file' };
1173
+ mockNoteFileDeserialize.mockReturnValue(decoded);
1174
+ mockWebClient.importNoteFile = vi.fn().mockResolvedValue('0x' + 'cd'.repeat(32));
1175
+ const multisig = createTestMultisig(config);
1176
+ const noteId = await multisig.importNoteFromBytes(new Uint8Array([1, 2, 3]));
1177
+ expect(mockNoteFileDeserialize).toHaveBeenCalledWith(new Uint8Array([1, 2, 3]));
1178
+ expect(mockWebClient.importNoteFile).toHaveBeenCalledWith(decoded);
1179
+ expect(noteId).toBe('0x' + 'cd'.repeat(32));
1180
+ });
1181
+ it('rejects bytes that do not decode as a note file', async () => {
1182
+ mockNoteFileDeserialize.mockImplementation(() => {
1183
+ throw new Error('bad bytes');
1184
+ });
1185
+ mockWebClient.importNoteFile = vi.fn();
1186
+ const multisig = createTestMultisig(config);
1187
+ await expect(multisig.importNoteFromBytes(new Uint8Array([0]))).rejects.toThrow(/failed to decode note file: bad bytes/);
1188
+ expect(mockWebClient.importNoteFile).not.toHaveBeenCalled();
1189
+ });
1190
+ });
1191
+ describe('exportNoteToFile / importNoteFromFile (issue #356)', () => {
1192
+ const config = {
1193
+ threshold: 1,
1194
+ signerCommitments: ['0x' + '1'.repeat(64)],
1195
+ guardianCommitment: '0x' + '3'.repeat(64),
1196
+ };
1197
+ it('rejects exportNoteToFile outside a browser environment', async () => {
1198
+ const multisig = createTestMultisig(config);
1199
+ await expect(multisig.exportNoteToFile('0x' + 'ab'.repeat(32))).rejects.toThrow(/requires a browser environment/);
1200
+ });
1201
+ it('imports from a File/Blob by delegating to importNoteFromBytes', async () => {
1202
+ const decoded = { marker: 'note-file' };
1203
+ mockNoteFileDeserialize.mockReturnValue(decoded);
1204
+ mockWebClient.importNoteFile = vi.fn().mockResolvedValue('0x' + 'cd'.repeat(32));
1205
+ const multisig = createTestMultisig(config);
1206
+ const noteId = await multisig.importNoteFromFile(new Blob([new Uint8Array([1, 2, 3])]));
1207
+ expect(mockNoteFileDeserialize).toHaveBeenCalledWith(new Uint8Array([1, 2, 3]));
1208
+ expect(noteId).toBe('0x' + 'cd'.repeat(32));
1209
+ });
1210
+ });
1211
+ describe('getP2idNoteId (issue #356)', () => {
1212
+ const config = {
1213
+ threshold: 1,
1214
+ signerCommitments: ['0x' + '1'.repeat(64)],
1215
+ guardianCommitment: '0x' + '3'.repeat(64),
1216
+ };
1217
+ it('computes the deterministic note ID from p2id proposal metadata', async () => {
1218
+ const multisig = createTestMultisig(config);
1219
+ const proposal = {
1220
+ metadata: {
1221
+ proposalType: 'p2id',
1222
+ recipientId: '0x' + 'b'.repeat(30),
1223
+ faucetId: '0x' + 'c'.repeat(30),
1224
+ amount: '100',
1225
+ saltHex: '0x' + 'd'.repeat(64),
1226
+ noteType: 'private',
1227
+ },
1228
+ };
1229
+ const noteId = await multisig.getP2idNoteId(proposal);
1230
+ expect(noteId).toBe('0x' + 'ab'.repeat(32));
1231
+ });
1232
+ it('rejects non-p2id proposals', async () => {
1233
+ const multisig = createTestMultisig(config);
1234
+ const proposal = {
1235
+ metadata: { proposalType: 'consume_notes' },
1236
+ };
1237
+ await expect(multisig.getP2idNoteId(proposal)).rejects.toThrow(/requires a P2ID proposal/);
1238
+ });
1239
+ });
1093
1240
  describe('createChangeThresholdProposal', () => {
1094
1241
  it('passes the signer scheme to update-signers requests', async () => {
1095
1242
  vi.mocked(executeForSummary).mockResolvedValue({
@@ -1675,6 +1822,7 @@ describe('Multisig', () => {
1675
1822
  ok: false,
1676
1823
  status: 404,
1677
1824
  statusText: 'Not Found',
1825
+ headers: new Headers(),
1678
1826
  // Feature 009: only a conforming { code, message, meta } envelope is
1679
1827
  // folded into the error message; raw text bodies are dropped.
1680
1828
  text: async () => JSON.stringify({
@@ -2393,7 +2541,11 @@ describe('Multisig', () => {
2393
2541
  scheme: 'ecdsa',
2394
2542
  publicKey: '0x' + '2'.repeat(66),
2395
2543
  };
2396
- const multisig = createTestMultisig(config, ecdsaSigner);
2544
+ const multisig = createTestMultisig(config, ecdsaSigner, undefined, {
2545
+ kind: 'remote',
2546
+ maxAttempts: 2,
2547
+ createProver: () => ({}),
2548
+ });
2397
2549
  const proposalId = '0x' + 'c'.repeat(64);
2398
2550
  const cosignerPubkey = '0x' + '3'.repeat(66);
2399
2551
  const ackPubkey = '0x' + '4'.repeat(66);
@@ -2456,9 +2608,22 @@ describe('Multisig', () => {
2456
2608
  ack_scheme: 'ecdsa',
2457
2609
  }),
2458
2610
  });
2459
- mockWebClient.transactions.submit.mockResolvedValueOnce({});
2460
- await expect(multisig.executeProposal(proposalId)).resolves.toBeUndefined();
2461
- expect(mockWebClient.transactions.submit).toHaveBeenCalledTimes(1);
2611
+ mockWebClient.proveTransaction
2612
+ .mockRejectedValueOnce(Object.assign(new Error('unavailable'), { code: 'Unavailable' }))
2613
+ .mockResolvedValueOnce({});
2614
+ vi.useFakeTimers();
2615
+ try {
2616
+ const execution = multisig.executeProposal(proposalId);
2617
+ await vi.runAllTimersAsync();
2618
+ await expect(execution).resolves.toBeUndefined();
2619
+ }
2620
+ finally {
2621
+ vi.useRealTimers();
2622
+ }
2623
+ expect(mockWebClient.executeTransaction).toHaveBeenCalledTimes(1);
2624
+ expect(mockWebClient.proveTransaction).toHaveBeenCalledTimes(2);
2625
+ expect(mockWebClient.submitProvenTransaction).toHaveBeenCalledTimes(1);
2626
+ expect(mockWebClient.applyTransaction).toHaveBeenCalledTimes(1);
2462
2627
  expect(vi.mocked(signatureHexToBytes)).toHaveBeenNthCalledWith(1, cosignerSignature, 'ecdsa');
2463
2628
  expect(vi.mocked(signatureHexToBytes)).toHaveBeenNthCalledWith(2, ackSignature, 'ecdsa');
2464
2629
  expect(vi.mocked(buildSignatureAdviceEntry)).toHaveBeenNthCalledWith(1, expect.anything(), expect.anything(), expect.anything(), cosignerPubkey, cosignerSignature);
@@ -2539,8 +2704,11 @@ describe('Multisig', () => {
2539
2704
  ack_scheme: 'ecdsa',
2540
2705
  }),
2541
2706
  });
2542
- mockWebClient.transactions.submit.mockResolvedValueOnce({});
2543
2707
  await expect(multisig.executeProposal(proposalId)).resolves.toBeUndefined();
2708
+ expect(mockWebClient.executeTransaction).toHaveBeenCalledTimes(1);
2709
+ expect(mockWebClient.proveTransaction).toHaveBeenCalledTimes(1);
2710
+ expect(mockWebClient.submitProvenTransaction).toHaveBeenCalledTimes(1);
2711
+ expect(mockWebClient.applyTransaction).toHaveBeenCalledTimes(1);
2544
2712
  expect(vi.mocked(signatureHexToBytes)).toHaveBeenNthCalledWith(1, cosignerSignature, 'ecdsa');
2545
2713
  expect(vi.mocked(signatureHexToBytes)).toHaveBeenNthCalledWith(2, ackSignature, 'ecdsa');
2546
2714
  expect(vi.mocked(buildSignatureAdviceEntry)).toHaveBeenNthCalledWith(1, expect.anything(), expect.anything(), expect.anything(), cosignerPubkey, cosignerSignature);
@@ -2620,9 +2788,11 @@ describe('Multisig', () => {
2620
2788
  ok: true,
2621
2789
  json: async () => ({ success: true, message: 'ok', ack_pubkey: '0x' + 'f'.repeat(64) }),
2622
2790
  });
2623
- mockWebClient.transactions.submit.mockResolvedValueOnce({});
2624
2791
  await expect(multisig.executeProposal(proposalId)).resolves.toBeUndefined();
2625
- expect(mockWebClient.transactions.submit).toHaveBeenCalledTimes(1);
2792
+ expect(mockWebClient.executeTransaction).toHaveBeenCalledTimes(1);
2793
+ expect(mockWebClient.proveTransaction).toHaveBeenCalledTimes(1);
2794
+ expect(mockWebClient.submitProvenTransaction).toHaveBeenCalledTimes(1);
2795
+ expect(mockWebClient.applyTransaction).toHaveBeenCalledTimes(1);
2626
2796
  });
2627
2797
  it('should still switch GUARDIAN when the pre-switch canonicalization push fails', async () => {
2628
2798
  const config = {
@@ -2666,9 +2836,11 @@ describe('Multisig', () => {
2666
2836
  ok: true,
2667
2837
  json: async () => ({ success: true, message: 'ok', ack_pubkey: '0x' + 'f'.repeat(64) }),
2668
2838
  });
2669
- mockWebClient.transactions.submit.mockResolvedValueOnce({});
2670
2839
  await expect(multisig.executeProposal(proposalId)).resolves.toBeUndefined();
2671
- expect(mockWebClient.transactions.submit).toHaveBeenCalledTimes(1);
2840
+ expect(mockWebClient.executeTransaction).toHaveBeenCalledTimes(1);
2841
+ expect(mockWebClient.proveTransaction).toHaveBeenCalledTimes(1);
2842
+ expect(mockWebClient.submitProvenTransaction).toHaveBeenCalledTimes(1);
2843
+ expect(mockWebClient.applyTransaction).toHaveBeenCalledTimes(1);
2672
2844
  });
2673
2845
  it('should reject switch_guardian execution when endpoint commitment mismatches', async () => {
2674
2846
  const config = {
@@ -2786,6 +2958,38 @@ describe('Multisig', () => {
2786
2958
  await expect(multisig.executeProposal(proposalId)).rejects.toThrow('Duplicate advice-map key detected');
2787
2959
  });
2788
2960
  });
2961
+ describe('submitTransaction', () => {
2962
+ it('uses the configured total proof-attempt budget without repeating other stages', async () => {
2963
+ const multisig = createTestMultisig({
2964
+ threshold: 1,
2965
+ signerCommitments: ['0x' + 'a'.repeat(64)],
2966
+ guardianCommitment: '0x' + 'c'.repeat(64),
2967
+ }, mockSigner, undefined, {
2968
+ kind: 'remote',
2969
+ maxAttempts: 4,
2970
+ createProver: () => ({}),
2971
+ });
2972
+ const transient = Object.assign(new Error('unavailable'), { code: 'Unavailable' });
2973
+ mockWebClient.proveTransaction
2974
+ .mockRejectedValueOnce(transient)
2975
+ .mockRejectedValueOnce(transient)
2976
+ .mockRejectedValueOnce(transient)
2977
+ .mockResolvedValueOnce({});
2978
+ vi.useFakeTimers();
2979
+ try {
2980
+ const submission = multisig.submitTransaction({});
2981
+ await vi.runAllTimersAsync();
2982
+ await submission;
2983
+ }
2984
+ finally {
2985
+ vi.useRealTimers();
2986
+ }
2987
+ expect(mockWebClient.executeTransaction).toHaveBeenCalledTimes(1);
2988
+ expect(mockWebClient.proveTransaction).toHaveBeenCalledTimes(4);
2989
+ expect(mockWebClient.submitProvenTransaction).toHaveBeenCalledTimes(1);
2990
+ expect(mockWebClient.applyTransaction).toHaveBeenCalledTimes(1);
2991
+ });
2992
+ });
2789
2993
  describe('prepareCustomExecution', () => {
2790
2994
  const requestBytes = new Uint8Array([9, 8, 7]);
2791
2995
  function customDelta(proposalType, cosignerSigs) {