@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
@@ -1,6 +1,6 @@
1
1
  export { buildConsumeNotesTransactionRequest, } from './transaction/consumeNotes.js';
2
2
  export { executeForSummary } from './transaction/summary.js';
3
- export { buildP2idTransactionRequest, parseP2idNoteType, p2idNoteTypeToMetadata, type P2idTransactionOptions, } from './transaction/p2id.js';
3
+ export { buildP2idNoteFromMetadata, buildP2idTransactionRequest, parseP2idNoteType, p2idNoteTypeToMetadata, type P2idTransactionOptions, } from './transaction/p2id.js';
4
4
  export { buildUpdateGuardianTransactionRequest, } from './transaction/updateGuardian.js';
5
5
  export { buildUpdateProcedureThresholdTransactionRequest, } from './transaction/updateProcedureThreshold.js';
6
6
  export { buildUpdateSignersTransactionRequest, } from './transaction/updateSigners.js';
@@ -1 +1 @@
1
- {"version":3,"file":"transaction.d.ts","sourceRoot":"","sources":["../src/transaction.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,mCAAmC,GACpC,MAAM,+BAA+B,CAAC;AACvC,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,EACL,2BAA2B,EAC3B,iBAAiB,EACjB,sBAAsB,EACtB,KAAK,sBAAsB,GAC5B,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACL,qCAAqC,GACtC,MAAM,iCAAiC,CAAC;AACzC,OAAO,EACL,+CAA+C,GAChD,MAAM,2CAA2C,CAAC;AACnD,OAAO,EACL,oCAAoC,GACrC,MAAM,gCAAgC,CAAC"}
1
+ {"version":3,"file":"transaction.d.ts","sourceRoot":"","sources":["../src/transaction.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,mCAAmC,GACpC,MAAM,+BAA+B,CAAC;AACvC,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,EACL,yBAAyB,EACzB,2BAA2B,EAC3B,iBAAiB,EACjB,sBAAsB,EACtB,KAAK,sBAAsB,GAC5B,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACL,qCAAqC,GACtC,MAAM,iCAAiC,CAAC;AACzC,OAAO,EACL,+CAA+C,GAChD,MAAM,2CAA2C,CAAC;AACnD,OAAO,EACL,oCAAoC,GACrC,MAAM,gCAAgC,CAAC"}
@@ -1,6 +1,6 @@
1
1
  export { buildConsumeNotesTransactionRequest, } from './transaction/consumeNotes.js';
2
2
  export { executeForSummary } from './transaction/summary.js';
3
- export { buildP2idTransactionRequest, parseP2idNoteType, p2idNoteTypeToMetadata, } from './transaction/p2id.js';
3
+ export { buildP2idNoteFromMetadata, buildP2idTransactionRequest, parseP2idNoteType, p2idNoteTypeToMetadata, } from './transaction/p2id.js';
4
4
  export { buildUpdateGuardianTransactionRequest, } from './transaction/updateGuardian.js';
5
5
  export { buildUpdateProcedureThresholdTransactionRequest, } from './transaction/updateProcedureThreshold.js';
6
6
  export { buildUpdateSignersTransactionRequest, } from './transaction/updateSigners.js';
@@ -1 +1 @@
1
- {"version":3,"file":"transaction.js","sourceRoot":"","sources":["../src/transaction.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,mCAAmC,GACpC,MAAM,+BAA+B,CAAC;AACvC,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,EACL,2BAA2B,EAC3B,iBAAiB,EACjB,sBAAsB,GAEvB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACL,qCAAqC,GACtC,MAAM,iCAAiC,CAAC;AACzC,OAAO,EACL,+CAA+C,GAChD,MAAM,2CAA2C,CAAC;AACnD,OAAO,EACL,oCAAoC,GACrC,MAAM,gCAAgC,CAAC"}
1
+ {"version":3,"file":"transaction.js","sourceRoot":"","sources":["../src/transaction.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,mCAAmC,GACpC,MAAM,+BAA+B,CAAC;AACvC,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,EACL,yBAAyB,EACzB,2BAA2B,EAC3B,iBAAiB,EACjB,sBAAsB,GAEvB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACL,qCAAqC,GACtC,MAAM,iCAAiC,CAAC;AACzC,OAAO,EACL,+CAA+C,GAChD,MAAM,2CAA2C,CAAC;AACnD,OAAO,EACL,oCAAoC,GACrC,MAAM,gCAAgC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openzeppelin/miden-multisig-client",
3
- "version": "0.16.0",
3
+ "version": "0.16.2",
4
4
  "description": "TypeScript SDK for Miden multisig accounts with GUARDIAN integration",
5
5
  "repository": {
6
6
  "type": "git",
@@ -39,7 +39,7 @@
39
39
  "@miden-sdk/miden-sdk": "^0.15.8",
40
40
  "@noble/curves": "^1.9.7",
41
41
  "@noble/hashes": "^2.0.1",
42
- "@openzeppelin/guardian-client": "^0.16.0"
42
+ "@openzeppelin/guardian-client": "^0.16.2"
43
43
  },
44
44
  "devDependencies": {
45
45
  "typescript": "^5.7.2",
@@ -239,12 +239,18 @@ describe('MultisigClient', () => {
239
239
  ok: false,
240
240
  status: 404,
241
241
  statusText: 'Not Found',
242
- text: async () => 'Account not found',
242
+ headers: new Headers(),
243
+ text: async () =>
244
+ JSON.stringify({
245
+ code: 'GUARDIAN_ACCOUNT_NOT_FOUND',
246
+ message: 'Account not found',
247
+ meta: { retryable: false },
248
+ }),
243
249
  });
244
250
 
245
251
  await expect(
246
252
  client.load('0xnonexistent', mockSigner)
247
- ).rejects.toThrow();
253
+ ).rejects.toThrow('Account not found');
248
254
  });
249
255
 
250
256
  it('should allow registerOnGuardian after load without explicit initial state', async () => {
package/src/client.ts CHANGED
@@ -13,6 +13,16 @@ import { createMultisigAccount } from './account/index.js';
13
13
  import { AccountInspector } from './inspector.js';
14
14
  import { getRawMidenClient, requireConfigValue, requireMidenRpcEndpoint } from './raw-client.js';
15
15
  import type { MultisigConfig, Signer } from './types.js';
16
+ import {
17
+ resolveProverConfig,
18
+ type ProverConfig,
19
+ type ResolvedProverConfig,
20
+ } from './prover/config.js';
21
+ import {
22
+ resolveRpcConfig,
23
+ type RpcConfig,
24
+ type ResolvedRpcConfig,
25
+ } from './rpc/config.js';
16
26
 
17
27
  interface AccountKeyBindingSigner {
18
28
  bindAccountKey?(midenClient: MidenClient, accountId: string): Promise<void>;
@@ -41,6 +51,10 @@ export interface MultisigClientConfig {
41
51
  * the injected `MidenClient`; there is no default.
42
52
  */
43
53
  midenRpcEndpoint: string;
54
+ /** Multisig-owned remote prover override and proof retry policy. */
55
+ prover?: ProverConfig;
56
+ /** Retry policy for idempotent Miden node reads; submission is never retried. */
57
+ rpc?: RpcConfig;
44
58
  }
45
59
 
46
60
  /**
@@ -70,6 +84,10 @@ export interface RecoveredAccount {
70
84
  * const client = new MultisigClient(midenClient, {
71
85
  * guardianEndpoint: 'http://localhost:3000',
72
86
  * midenRpcEndpoint: 'https://rpc.devnet.miden.io',
87
+ * prover: {
88
+ * url: 'https://prover.example',
89
+ * retry: { maxAttempts: 4 },
90
+ * },
73
91
  * });
74
92
  *
75
93
  * // Get GUARDIAN pubkey for config
@@ -83,11 +101,15 @@ export interface RecoveredAccount {
83
101
  export class MultisigClient {
84
102
  private readonly midenClient: MidenClient;
85
103
  private readonly midenRpcEndpoint: string;
104
+ private readonly proverConfig: ResolvedProverConfig;
105
+ private readonly rpcConfig: ResolvedRpcConfig;
86
106
  private _guardianClient: GuardianHttpClient;
87
107
 
88
108
  constructor(midenClient: MidenClient, config: MultisigClientConfig) {
89
109
  this.midenClient = midenClient;
90
110
  this.midenRpcEndpoint = requireMidenRpcEndpoint(config?.midenRpcEndpoint);
111
+ this.proverConfig = resolveProverConfig(config?.prover, midenClient.defaultProver);
112
+ this.rpcConfig = resolveRpcConfig(config?.rpc);
91
113
  this._guardianClient = new GuardianHttpClient(
92
114
  requireConfigValue('guardianEndpoint', config?.guardianEndpoint),
93
115
  );
@@ -161,7 +183,9 @@ export class MultisigClient {
161
183
  signer,
162
184
  this.midenClient,
163
185
  undefined,
164
- this.midenRpcEndpoint
186
+ this.midenRpcEndpoint,
187
+ this.proverConfig,
188
+ this.rpcConfig,
165
189
  );
166
190
  }
167
191
 
@@ -213,7 +237,9 @@ export class MultisigClient {
213
237
  signer,
214
238
  this.midenClient,
215
239
  accountId,
216
- this.midenRpcEndpoint
240
+ this.midenRpcEndpoint,
241
+ this.proverConfig,
242
+ this.rpcConfig,
217
243
  );
218
244
  }
219
245
  }
package/src/index.ts CHANGED
@@ -25,6 +25,10 @@
25
25
  * const client = new MultisigClient(midenClient, {
26
26
  * guardianEndpoint: 'http://localhost:3000',
27
27
  * midenRpcEndpoint: 'https://rpc.devnet.miden.io',
28
+ * prover: {
29
+ * url: 'https://prover.example',
30
+ * retry: { maxAttempts: 4 },
31
+ * },
28
32
  * });
29
33
  *
30
34
  * // Get GUARDIAN pubkey for config
@@ -45,6 +49,8 @@ export {
45
49
  type MultisigClientConfig,
46
50
  type RecoveredAccount,
47
51
  } from './client.js';
52
+ export type { ProverConfig, ProverRetryPolicy } from './prover/config.js';
53
+ export type { RpcConfig, RpcRetryPolicy } from './rpc/config.js';
48
54
  export { lookupAuthDigest } from './lookupAuth.js';
49
55
  export { Multisig, type AccountState } from './multisig.js';
50
56
  export { AccountInspector, type DetectedMultisigConfig, type VaultBalance } from './inspector.js';
@@ -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
@@ -26,6 +27,14 @@ vi.mock('@miden-sdk/miden-sdk', () => ({
26
27
  Private: 0,
27
28
  Public: 1,
28
29
  },
30
+ NoteExportFormat: {
31
+ Id: 0,
32
+ Full: 1,
33
+ Details: 2,
34
+ },
35
+ NoteFile: {
36
+ deserialize: mockNoteFileDeserialize,
37
+ },
29
38
  TransactionSummary: {
30
39
  deserialize: vi.fn().mockReturnValue({
31
40
  toCommitment: () => ({
@@ -91,6 +100,9 @@ vi.mock('./transaction.js', () => ({
91
100
  request: {},
92
101
  salt: { toHex: () => '0x' + 'd'.repeat(64) },
93
102
  }),
103
+ buildP2idNoteFromMetadata: vi.fn().mockReturnValue({
104
+ id: () => ({ toString: () => '0x' + 'ab'.repeat(32) }),
105
+ }),
94
106
  // Mirrors the real implementations against the mocked NoteType values
95
107
  // (Private = 0, Public = 1).
96
108
  parseP2idNoteType: vi.fn((value?: string) => {
@@ -156,7 +168,8 @@ describe('Multisig', () => {
156
168
  function createTestMultisig(
157
169
  config: ConstructorParameters<typeof Multisig>[1],
158
170
  signer: Signer = mockSigner,
159
- accountId?: string
171
+ accountId?: string,
172
+ proverConfig?: ConstructorParameters<typeof Multisig>[7],
160
173
  ): Multisig {
161
174
  return new Multisig(
162
175
  mockAccount,
@@ -165,7 +178,8 @@ describe('Multisig', () => {
165
178
  signer,
166
179
  mockWebClient,
167
180
  accountId,
168
- MIDEN_RPC_ENDPOINT
181
+ MIDEN_RPC_ENDPOINT,
182
+ proverConfig,
169
183
  );
170
184
  }
171
185
 
@@ -226,13 +240,38 @@ describe('Multisig', () => {
226
240
  submitNewTransaction: vi.fn(),
227
241
  submitNewTransactionWithProver: vi.fn(),
228
242
  transactions: {
229
- submit: vi.fn(),
243
+ executeRequest: vi.fn(),
230
244
  },
231
245
  getConsumableNotes: vi.fn().mockResolvedValue([]),
232
246
  syncState: vi.fn(),
233
247
  getAccount: vi.fn().mockResolvedValue(null),
234
248
  newAccount: vi.fn(),
235
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
+ );
236
275
  });
237
276
 
238
277
  describe('constructor', () => {
@@ -1398,6 +1437,154 @@ describe('Multisig', () => {
1398
1437
  });
1399
1438
  });
1400
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
+ });
1586
+ });
1587
+
1401
1588
  describe('createChangeThresholdProposal', () => {
1402
1589
  it('passes the signer scheme to update-signers requests', async () => {
1403
1590
  vi.mocked(executeForSummary).mockResolvedValue({
@@ -2106,6 +2293,7 @@ describe('Multisig', () => {
2106
2293
  ok: false,
2107
2294
  status: 404,
2108
2295
  statusText: 'Not Found',
2296
+ headers: new Headers(),
2109
2297
  // Feature 009: only a conforming { code, message, meta } envelope is
2110
2298
  // folded into the error message; raw text bodies are dropped.
2111
2299
  text: async () =>
@@ -2971,7 +3159,11 @@ describe('Multisig', () => {
2971
3159
  publicKey: '0x' + '2'.repeat(66),
2972
3160
  };
2973
3161
 
2974
- const multisig = createTestMultisig(config, ecdsaSigner);
3162
+ const multisig = createTestMultisig(config, ecdsaSigner, undefined, {
3163
+ kind: 'remote',
3164
+ maxAttempts: 2,
3165
+ createProver: () => ({} as never),
3166
+ });
2975
3167
  const proposalId = '0x' + 'c'.repeat(64);
2976
3168
  const cosignerPubkey = '0x' + '3'.repeat(66);
2977
3169
  const ackPubkey = '0x' + '4'.repeat(66);
@@ -3036,10 +3228,21 @@ describe('Multisig', () => {
3036
3228
  ack_scheme: 'ecdsa',
3037
3229
  }),
3038
3230
  });
3039
- mockWebClient.transactions.submit.mockResolvedValueOnce({});
3040
-
3041
- await expect(multisig.executeProposal(proposalId)).resolves.toBeUndefined();
3042
- expect(mockWebClient.transactions.submit).toHaveBeenCalledTimes(1);
3231
+ mockWebClient.proveTransaction
3232
+ .mockRejectedValueOnce(Object.assign(new Error('unavailable'), { code: 'Unavailable' }))
3233
+ .mockResolvedValueOnce({});
3234
+ vi.useFakeTimers();
3235
+ try {
3236
+ const execution = multisig.executeProposal(proposalId);
3237
+ await vi.runAllTimersAsync();
3238
+ await expect(execution).resolves.toBeUndefined();
3239
+ } finally {
3240
+ vi.useRealTimers();
3241
+ }
3242
+ expect(mockWebClient.executeTransaction).toHaveBeenCalledTimes(1);
3243
+ expect(mockWebClient.proveTransaction).toHaveBeenCalledTimes(2);
3244
+ expect(mockWebClient.submitProvenTransaction).toHaveBeenCalledTimes(1);
3245
+ expect(mockWebClient.applyTransaction).toHaveBeenCalledTimes(1);
3043
3246
 
3044
3247
  expect(vi.mocked(signatureHexToBytes)).toHaveBeenNthCalledWith(
3045
3248
  1,
@@ -3151,9 +3354,11 @@ describe('Multisig', () => {
3151
3354
  ack_scheme: 'ecdsa',
3152
3355
  }),
3153
3356
  });
3154
- mockWebClient.transactions.submit.mockResolvedValueOnce({});
3155
-
3156
3357
  await expect(multisig.executeProposal(proposalId)).resolves.toBeUndefined();
3358
+ expect(mockWebClient.executeTransaction).toHaveBeenCalledTimes(1);
3359
+ expect(mockWebClient.proveTransaction).toHaveBeenCalledTimes(1);
3360
+ expect(mockWebClient.submitProvenTransaction).toHaveBeenCalledTimes(1);
3361
+ expect(mockWebClient.applyTransaction).toHaveBeenCalledTimes(1);
3157
3362
 
3158
3363
  expect(vi.mocked(signatureHexToBytes)).toHaveBeenNthCalledWith(
3159
3364
  1,
@@ -3260,10 +3465,11 @@ describe('Multisig', () => {
3260
3465
  ok: true,
3261
3466
  json: async () => ({ success: true, message: 'ok', ack_pubkey: '0x' + 'f'.repeat(64) }),
3262
3467
  });
3263
- mockWebClient.transactions.submit.mockResolvedValueOnce({});
3264
-
3265
3468
  await expect(multisig.executeProposal(proposalId)).resolves.toBeUndefined();
3266
- expect(mockWebClient.transactions.submit).toHaveBeenCalledTimes(1);
3469
+ expect(mockWebClient.executeTransaction).toHaveBeenCalledTimes(1);
3470
+ expect(mockWebClient.proveTransaction).toHaveBeenCalledTimes(1);
3471
+ expect(mockWebClient.submitProvenTransaction).toHaveBeenCalledTimes(1);
3472
+ expect(mockWebClient.applyTransaction).toHaveBeenCalledTimes(1);
3267
3473
  });
3268
3474
 
3269
3475
  it('should still switch GUARDIAN when the pre-switch canonicalization push fails', async () => {
@@ -3311,10 +3517,11 @@ describe('Multisig', () => {
3311
3517
  ok: true,
3312
3518
  json: async () => ({ success: true, message: 'ok', ack_pubkey: '0x' + 'f'.repeat(64) }),
3313
3519
  });
3314
- mockWebClient.transactions.submit.mockResolvedValueOnce({});
3315
-
3316
3520
  await expect(multisig.executeProposal(proposalId)).resolves.toBeUndefined();
3317
- expect(mockWebClient.transactions.submit).toHaveBeenCalledTimes(1);
3521
+ expect(mockWebClient.executeTransaction).toHaveBeenCalledTimes(1);
3522
+ expect(mockWebClient.proveTransaction).toHaveBeenCalledTimes(1);
3523
+ expect(mockWebClient.submitProvenTransaction).toHaveBeenCalledTimes(1);
3524
+ expect(mockWebClient.applyTransaction).toHaveBeenCalledTimes(1);
3318
3525
  });
3319
3526
 
3320
3527
  it('should reject switch_guardian execution when endpoint commitment mismatches', async () => {
@@ -3453,6 +3660,45 @@ describe('Multisig', () => {
3453
3660
  });
3454
3661
  });
3455
3662
 
3663
+ describe('submitTransaction', () => {
3664
+ it('uses the configured total proof-attempt budget without repeating other stages', async () => {
3665
+ const multisig = createTestMultisig(
3666
+ {
3667
+ threshold: 1,
3668
+ signerCommitments: ['0x' + 'a'.repeat(64)],
3669
+ guardianCommitment: '0x' + 'c'.repeat(64),
3670
+ },
3671
+ mockSigner,
3672
+ undefined,
3673
+ {
3674
+ kind: 'remote',
3675
+ maxAttempts: 4,
3676
+ createProver: () => ({} as never),
3677
+ },
3678
+ );
3679
+ const transient = Object.assign(new Error('unavailable'), { code: 'Unavailable' });
3680
+ mockWebClient.proveTransaction
3681
+ .mockRejectedValueOnce(transient)
3682
+ .mockRejectedValueOnce(transient)
3683
+ .mockRejectedValueOnce(transient)
3684
+ .mockResolvedValueOnce({});
3685
+
3686
+ vi.useFakeTimers();
3687
+ try {
3688
+ const submission = multisig.submitTransaction({} as never);
3689
+ await vi.runAllTimersAsync();
3690
+ await submission;
3691
+ } finally {
3692
+ vi.useRealTimers();
3693
+ }
3694
+
3695
+ expect(mockWebClient.executeTransaction).toHaveBeenCalledTimes(1);
3696
+ expect(mockWebClient.proveTransaction).toHaveBeenCalledTimes(4);
3697
+ expect(mockWebClient.submitProvenTransaction).toHaveBeenCalledTimes(1);
3698
+ expect(mockWebClient.applyTransaction).toHaveBeenCalledTimes(1);
3699
+ });
3700
+ });
3701
+
3456
3702
  describe('prepareCustomExecution', () => {
3457
3703
  const requestBytes = new Uint8Array([9, 8, 7]);
3458
3704