@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
@@ -55,6 +55,10 @@ vi.mock('./account/index.js', () => ({
55
55
  const mockFetch = vi.fn();
56
56
  vi.stubGlobal('fetch', mockFetch);
57
57
 
58
+ const GUARDIAN_URL = 'http://localhost:3000';
59
+ const MIDEN_RPC = 'http://localhost:57291';
60
+ const CLIENT_CONFIG = { guardianEndpoint: GUARDIAN_URL, midenRpcEndpoint: MIDEN_RPC };
61
+
58
62
  describe('MultisigClient', () => {
59
63
  let webClient: any;
60
64
  let mockSigner: Signer;
@@ -83,27 +87,75 @@ describe('MultisigClient', () => {
83
87
  });
84
88
 
85
89
  describe('constructor', () => {
86
- it('should create client with default GUARDIAN endpoint', () => {
87
- const client = new MultisigClient(webClient);
90
+ it('should create client when both endpoints are supplied', () => {
91
+ const client = new MultisigClient(webClient, CLIENT_CONFIG);
88
92
  expect(client).toBeInstanceOf(MultisigClient);
89
93
  });
90
94
 
91
95
  it('should create client with custom GUARDIAN endpoint', () => {
92
- const client = new MultisigClient(webClient, { guardianEndpoint: 'http://custom:8080' });
96
+ const client = new MultisigClient(webClient, {
97
+ guardianEndpoint: 'http://custom:8080',
98
+ midenRpcEndpoint: MIDEN_RPC,
99
+ });
93
100
  expect(client).toBeInstanceOf(MultisigClient);
94
101
  });
102
+
103
+ it('throws when the config object is omitted', () => {
104
+ expect(() => new (MultisigClient as any)(webClient)).toThrow(
105
+ 'missing required configuration: midenRpcEndpoint',
106
+ );
107
+ });
108
+
109
+ it.each([undefined, null, 42, '', ' '])(
110
+ 'throws before any network or store access when midenRpcEndpoint is %j',
111
+ (endpoint) => {
112
+ expect(
113
+ () =>
114
+ new MultisigClient(webClient, {
115
+ guardianEndpoint: GUARDIAN_URL,
116
+ midenRpcEndpoint: endpoint as any,
117
+ }),
118
+ ).toThrow('missing required configuration: midenRpcEndpoint');
119
+ expect(mockFetch).not.toHaveBeenCalled();
120
+ expect(webClient.accounts.get).not.toHaveBeenCalled();
121
+ expect(webClient.accounts.insert).not.toHaveBeenCalled();
122
+ },
123
+ );
124
+
125
+ it.each([undefined, null, 42, '', ' '])(
126
+ 'throws before any network or store access when guardianEndpoint is %j',
127
+ (endpoint) => {
128
+ expect(
129
+ () =>
130
+ new MultisigClient(webClient, {
131
+ guardianEndpoint: endpoint as any,
132
+ midenRpcEndpoint: MIDEN_RPC,
133
+ }),
134
+ ).toThrow('missing required configuration: guardianEndpoint');
135
+ expect(mockFetch).not.toHaveBeenCalled();
136
+ expect(webClient.accounts.get).not.toHaveBeenCalled();
137
+ expect(webClient.accounts.insert).not.toHaveBeenCalled();
138
+ },
139
+ );
140
+
141
+ it('rejects a blank endpoint passed to setGuardianEndpoint', () => {
142
+ const client = new MultisigClient(webClient, CLIENT_CONFIG);
143
+ expect(() => client.setGuardianEndpoint(' ')).toThrow(
144
+ 'missing required configuration: guardianEndpoint',
145
+ );
146
+ });
95
147
  });
96
148
 
97
149
  describe('guardianClient getter', () => {
98
150
  it('should expose GUARDIAN client for getting pubkey', () => {
99
- const client = new MultisigClient(webClient);
151
+ const client = new MultisigClient(webClient, CLIENT_CONFIG);
100
152
  expect(client.guardianClient).toBeDefined();
101
153
  });
102
154
  });
103
155
 
104
156
  describe('create', () => {
105
157
  it('should create multisig and return Multisig instance', async () => {
106
- const client = new MultisigClient(webClient);
158
+ const client = new MultisigClient(webClient, CLIENT_CONFIG);
107
159
 
108
160
  const config = {
109
161
  threshold: 2,
@@ -120,7 +172,7 @@ describe('MultisigClient', () => {
120
172
  });
121
173
 
122
174
  it('should set signer on GUARDIAN client', async () => {
123
- const client = new MultisigClient(webClient);
175
+ const client = new MultisigClient(webClient, CLIENT_CONFIG);
124
176
 
125
177
  const config = {
126
178
  threshold: 1,
@@ -133,7 +185,7 @@ describe('MultisigClient', () => {
133
185
  });
134
186
 
135
187
  it('binds the signer auth key to the created account when supported', async () => {
136
- const client = new MultisigClient(webClient);
188
+ const client = new MultisigClient(webClient, CLIENT_CONFIG);
137
189
  const bindAccountKey = vi.fn().mockResolvedValue(undefined);
138
190
  const bindingSigner = {
139
191
  ...mockSigner,
@@ -152,7 +204,7 @@ describe('MultisigClient', () => {
152
204
 
153
205
  describe('load', () => {
154
206
  it('should load existing multisig account and detect config', async () => {
155
- const client = new MultisigClient(webClient);
207
+ const client = new MultisigClient(webClient, CLIENT_CONFIG);
156
208
 
157
209
  // Mock getState response
158
210
  mockFetch.mockResolvedValueOnce({
@@ -181,7 +233,7 @@ describe('MultisigClient', () => {
181
233
  });
182
234
 
183
235
  it('should throw if account not found on GUARDIAN', async () => {
184
- const client = new MultisigClient(webClient);
236
+ const client = new MultisigClient(webClient, CLIENT_CONFIG);
185
237
 
186
238
  mockFetch.mockResolvedValueOnce({
187
239
  ok: false,
@@ -196,7 +248,7 @@ describe('MultisigClient', () => {
196
248
  });
197
249
 
198
250
  it('should allow registerOnGuardian after load without explicit initial state', async () => {
199
- const client = new MultisigClient(webClient);
251
+ const client = new MultisigClient(webClient, CLIENT_CONFIG);
200
252
 
201
253
  mockFetch.mockResolvedValueOnce({
202
254
  ok: true,
@@ -227,7 +279,7 @@ describe('MultisigClient', () => {
227
279
  });
228
280
 
229
281
  it('binds the signer auth key after loading an account when supported', async () => {
230
- const client = new MultisigClient(webClient);
282
+ const client = new MultisigClient(webClient, CLIENT_CONFIG);
231
283
  const bindAccountKey = vi.fn().mockResolvedValue(undefined);
232
284
  const bindingSigner = {
233
285
  ...mockSigner,
@@ -289,7 +341,7 @@ describe('MultisigClient', () => {
289
341
  }
290
342
 
291
343
  it('returns one (accountId, state) pair when lookup matches a single account', async () => {
292
- const client = new MultisigClient(webClient);
344
+ const client = new MultisigClient(webClient, CLIENT_CONFIG);
293
345
  const signer = makeLookupCapableSigner();
294
346
  const accountId = '0x7bfb0f38b0fafa103f86a805594170';
295
347
 
@@ -311,7 +363,7 @@ describe('MultisigClient', () => {
311
363
  });
312
364
 
313
365
  it('returns multiple (accountId, state) pairs when one commitment authorizes several accounts', async () => {
314
- const client = new MultisigClient(webClient);
366
+ const client = new MultisigClient(webClient, CLIENT_CONFIG);
315
367
  const signer = makeLookupCapableSigner();
316
368
  const accountA = '0xaaa1';
317
369
  const accountB = '0xbbb2';
@@ -328,7 +380,7 @@ describe('MultisigClient', () => {
328
380
  });
329
381
 
330
382
  it('returns empty array when no account authorizes the commitment', async () => {
331
- const client = new MultisigClient(webClient);
383
+ const client = new MultisigClient(webClient, CLIENT_CONFIG);
332
384
  const signer = makeLookupCapableSigner();
333
385
 
334
386
  mockServerLookupResponse([]);
@@ -341,7 +393,7 @@ describe('MultisigClient', () => {
341
393
  });
342
394
 
343
395
  it('throws a clear error when the signer does not implement signLookupMessage', async () => {
344
- const client = new MultisigClient(webClient);
396
+ const client = new MultisigClient(webClient, CLIENT_CONFIG);
345
397
  // mockSigner from the outer beforeEach lacks signLookupMessage.
346
398
  await expect(client.recoverByKey(mockSigner)).rejects.toThrow(/signLookupMessage/);
347
399
  expect(mockFetch).not.toHaveBeenCalled();
package/src/client.ts CHANGED
@@ -11,8 +11,13 @@ import type { StateObject } from '@openzeppelin/guardian-client';
11
11
  import { Multisig } from './multisig.js';
12
12
  import { createMultisigAccount } from './account/index.js';
13
13
  import { AccountInspector } from './inspector.js';
14
- import { getRawMidenClient, resolveMidenRpcEndpoint } from './raw-client.js';
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';
16
21
 
17
22
  interface AccountKeyBindingSigner {
18
23
  bindAccountKey?(midenClient: MidenClient, accountId: string): Promise<void>;
@@ -33,10 +38,16 @@ async function bindSignerAccountKey(
33
38
  * Configuration for MultisigClient.
34
39
  */
35
40
  export interface MultisigClientConfig {
36
- /** GUARDIAN server endpoint */
37
- guardianEndpoint?: string;
38
- /** Miden node RPC endpoint used for state commitment verification */
39
- midenRpcEndpoint?: string;
41
+ /** GUARDIAN server endpoint. Required — there is no default. */
42
+ guardianEndpoint: string;
43
+ /**
44
+ * Miden node RPC endpoint used for proposal execution and state
45
+ * commitment verification. Required — must point at the same network as
46
+ * the injected `MidenClient`; there is no default.
47
+ */
48
+ midenRpcEndpoint: string;
49
+ /** Multisig-owned remote prover override and proof retry policy. */
50
+ prover?: ProverConfig;
40
51
  }
41
52
 
42
53
  /**
@@ -66,6 +77,10 @@ export interface RecoveredAccount {
66
77
  * const client = new MultisigClient(midenClient, {
67
78
  * guardianEndpoint: 'http://localhost:3000',
68
79
  * midenRpcEndpoint: 'https://rpc.devnet.miden.io',
80
+ * prover: {
81
+ * url: 'https://prover.example',
82
+ * retry: { maxAttempts: 4 },
83
+ * },
69
84
  * });
70
85
  *
71
86
  * // Get GUARDIAN pubkey for config
@@ -79,21 +94,27 @@ export interface RecoveredAccount {
79
94
  export class MultisigClient {
80
95
  private readonly midenClient: MidenClient;
81
96
  private readonly midenRpcEndpoint: string;
97
+ private readonly proverConfig: ResolvedProverConfig;
82
98
  private _guardianClient: GuardianHttpClient;
83
99
 
84
- constructor(midenClient: MidenClient, config: MultisigClientConfig = {}) {
100
+ constructor(midenClient: MidenClient, config: MultisigClientConfig) {
85
101
  this.midenClient = midenClient;
86
- this.midenRpcEndpoint = resolveMidenRpcEndpoint(config.midenRpcEndpoint);
87
- this._guardianClient = new GuardianHttpClient(config.guardianEndpoint ?? 'http://localhost:3000');
102
+ this.midenRpcEndpoint = requireMidenRpcEndpoint(config?.midenRpcEndpoint);
103
+ this.proverConfig = resolveProverConfig(config?.prover, midenClient.defaultProver);
104
+ this._guardianClient = new GuardianHttpClient(
105
+ requireConfigValue('guardianEndpoint', config?.guardianEndpoint),
106
+ );
88
107
  }
89
108
 
90
109
  /**
91
110
  * Change the GUARDIAN endpoint.
92
- *
111
+ *
93
112
  * @param endpoint - The new GUARDIAN server endpoint URL
94
113
  */
95
114
  setGuardianEndpoint(endpoint: string): void {
96
- this._guardianClient = new GuardianHttpClient(endpoint);
115
+ this._guardianClient = new GuardianHttpClient(
116
+ requireConfigValue('guardianEndpoint', endpoint),
117
+ );
97
118
  }
98
119
 
99
120
  /**
@@ -153,7 +174,8 @@ export class MultisigClient {
153
174
  signer,
154
175
  this.midenClient,
155
176
  undefined,
156
- this.midenRpcEndpoint
177
+ this.midenRpcEndpoint,
178
+ this.proverConfig,
157
179
  );
158
180
  }
159
181
 
@@ -205,7 +227,8 @@ export class MultisigClient {
205
227
  signer,
206
228
  this.midenClient,
207
229
  accountId,
208
- this.midenRpcEndpoint
230
+ this.midenRpcEndpoint,
231
+ this.proverConfig,
209
232
  );
210
233
  }
211
234
  }
@@ -0,0 +1,67 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { GuardianHttpError } from '@openzeppelin/guardian-client';
3
+ import { isLikelyNetworkError, toUserFacingError } from './connectivity.js';
4
+
5
+ describe('isLikelyNetworkError', () => {
6
+ it('flags codeless transport failures', () => {
7
+ for (const m of [
8
+ 'Failed to fetch',
9
+ 'NetworkError when attempting to fetch resource',
10
+ 'Load failed',
11
+ 'The operation was aborted',
12
+ 'request timed out',
13
+ 'connection refused',
14
+ 'getaddrinfo ENOTFOUND guardian.example',
15
+ ]) {
16
+ expect(isLikelyNetworkError(new TypeError(m))).toBe(true);
17
+ }
18
+ });
19
+
20
+ it('does not flag semantic errors', () => {
21
+ expect(isLikelyNetworkError(new Error('account is paused'))).toBe(false);
22
+ expect(isLikelyNetworkError(new Error('insufficient signatures'))).toBe(false);
23
+ });
24
+ });
25
+
26
+ describe('toUserFacingError', () => {
27
+ it('uses the server code + user-safe message when Guardian was reached', () => {
28
+ const body = JSON.stringify({
29
+ code: 'account_paused',
30
+ message: "This account is paused and can't approve transactions right now.",
31
+ meta: { retryable: false },
32
+ });
33
+ const result = toUserFacingError(new GuardianHttpError(409, 'Conflict', body));
34
+ expect(result.code).toBe('account_paused');
35
+ expect(result.userMessage).toContain('paused');
36
+ expect(result.category).toBeUndefined();
37
+ });
38
+
39
+ it('classifies a codeless transport failure as connectivity', () => {
40
+ const result = toUserFacingError(new TypeError('Failed to fetch'));
41
+ expect(result.code).toBeUndefined();
42
+ expect(result.category).toBe('unreachable');
43
+ expect(result.userMessage).toContain("Can't reach Guardian");
44
+ // The raw transport text is never the primary message.
45
+ expect(result.userMessage).not.toContain('Failed to fetch');
46
+ });
47
+
48
+ it('classifies timeouts and aborts as the timeout category', () => {
49
+ for (const m of ['request timed out', 'The operation was aborted']) {
50
+ const result = toUserFacingError(new Error(m));
51
+ expect(result.category).toBe('timeout');
52
+ expect(result.userMessage).toContain("Can't reach Guardian");
53
+ }
54
+ });
55
+
56
+ it('treats a reachable proxy 5xx with no Guardian body as connectivity', () => {
57
+ const result = toUserFacingError(new GuardianHttpError(502, 'Bad Gateway', '<html>nope</html>'));
58
+ expect(result.category).toBe('unreachable');
59
+ expect(result.userMessage).toContain("Can't reach Guardian");
60
+ });
61
+
62
+ it('falls back to a generic message for unknown non-Guardian errors', () => {
63
+ const result = toUserFacingError(new Error('totally unexpected'));
64
+ expect(result.userMessage).toBe('Something went wrong. Please try again.');
65
+ expect(result.userMessage).not.toContain('totally unexpected');
66
+ });
67
+ });
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Classification for codeless transport failures (feature
3
+ * `009-human-readable-errors`, User Story 3).
4
+ *
5
+ * When Guardian *is* reached, errors arrive as a {@link GuardianHttpError}
6
+ * carrying the server's stable `code` and user-safe `message` — use those.
7
+ * When Guardian is *not* reached (connection refused, DNS, timeout, TLS, or a
8
+ * proxy 5xx with no Guardian body) no error object exists, so the wallet-facing
9
+ * client must classify the failure and supply a friendly connectivity message
10
+ * rather than surfacing raw `"Failed to fetch"` / `"NetworkError"` text.
11
+ *
12
+ * The detection is intentionally string-matching, mirroring the 0xMiden/wallet
13
+ * `connectivity-classify.ts` heuristic: fetch / DOMException / TypeError each
14
+ * surface failures with different shapes, so the message string is the only
15
+ * stable join key.
16
+ */
17
+ import { GuardianHttpError } from '@openzeppelin/guardian-client';
18
+
19
+ /**
20
+ * Connectivity category for a codeless transport failure:
21
+ * - `network` — the browser/OS reports no connectivity at all
22
+ * (`navigator.onLine === false`); the problem is on the user's side.
23
+ * - `timeout` — the request was sent but timed out or was aborted.
24
+ * - `unreachable` — everything else: Guardian (or an intermediary) could not
25
+ * be reached or did not answer with a Guardian error object.
26
+ */
27
+ export type ConnectivityCategory = 'network' | 'unreachable' | 'timeout';
28
+
29
+ const CONNECTIVITY_MESSAGE = "Can't reach Guardian right now. Check your connection and try again.";
30
+ const GENERIC_MESSAGE = 'Something went wrong. Please try again.';
31
+
32
+ /**
33
+ * `navigator.onLine` is unreliable as a positive signal, but a `false` means
34
+ * the browser/OS is certain there is no connectivity — categorize as
35
+ * `network` rather than `unreachable` (mirrors the wallet's
36
+ * `isDefinitelyOffline`). Guarded for non-browser (Node) contexts.
37
+ */
38
+ function isDefinitelyOffline(): boolean {
39
+ if (typeof navigator === 'undefined') return false;
40
+ if (typeof navigator.onLine !== 'boolean') return false;
41
+ return navigator.onLine === false;
42
+ }
43
+
44
+ /** Classify a codeless transport failure into a {@link ConnectivityCategory}. */
45
+ function classifyTransportError(err: unknown): ConnectivityCategory {
46
+ if (isDefinitelyOffline()) return 'network';
47
+ const message = (err as { message?: string } | null | undefined)?.message ?? String(err ?? '');
48
+ const lower = message.toLowerCase();
49
+ if (lower.includes('timeout') || lower.includes('timed out') || lower.includes('abort')) {
50
+ return 'timeout';
51
+ }
52
+ return 'unreachable';
53
+ }
54
+
55
+ /**
56
+ * Does this error look like a codeless transport/connectivity failure (vs a
57
+ * semantic Guardian error)? String heuristic — see module docs.
58
+ */
59
+ export function isLikelyNetworkError(err: unknown): boolean {
60
+ const message = (err as { message?: string } | null | undefined)?.message ?? String(err ?? '');
61
+ const lower = message.toLowerCase();
62
+ if (lower.includes('failed to fetch')) return true;
63
+ if (lower.includes('networkerror')) return true;
64
+ if (lower.includes('network error')) return true;
65
+ if (lower.includes('load failed')) return true;
66
+ if (lower.includes('abort')) return true;
67
+ if (lower.includes('timeout') || lower.includes('timed out')) return true;
68
+ if (lower.includes('connection')) return true;
69
+ if (lower.includes('econnrefused') || lower.includes('enotfound')) return true;
70
+ if (lower.includes('dns')) return true;
71
+ return false;
72
+ }
73
+
74
+ /** A normalized, wallet-displayable view of any error thrown by a Guardian call. */
75
+ export interface UserFacingError {
76
+ /** Stable Guardian error code, when the server was reached. */
77
+ code?: string;
78
+ /** Connectivity category, when this was a codeless transport failure. */
79
+ category?: ConnectivityCategory;
80
+ /** Short message safe to display verbatim in a wallet UI. */
81
+ userMessage: string;
82
+ /** The original error, for logging/diagnostics. */
83
+ cause: unknown;
84
+ }
85
+
86
+ /**
87
+ * Normalize any error thrown by a Guardian call into a user-facing shape.
88
+ *
89
+ * - A {@link GuardianHttpError} with a parsed `{ code, message }` → the
90
+ * server's stable code + user-safe message (feature 009).
91
+ * - A reachable host that returned no Guardian error object (e.g. a proxy 5xx
92
+ * with an HTML body) → treated as a connectivity failure.
93
+ * - A codeless transport rejection (server never reached) → classified and
94
+ * given the generic connectivity message; the raw transport text is never
95
+ * surfaced as the primary message.
96
+ */
97
+ export function toUserFacingError(err: unknown): UserFacingError {
98
+ if (err instanceof GuardianHttpError) {
99
+ if (err.code && err.userMessage) {
100
+ return { code: err.code, userMessage: err.userMessage, cause: err };
101
+ }
102
+ if (err.status >= 500) {
103
+ return { category: 'unreachable', userMessage: CONNECTIVITY_MESSAGE, cause: err };
104
+ }
105
+ return { userMessage: GENERIC_MESSAGE, cause: err };
106
+ }
107
+ if (isLikelyNetworkError(err)) {
108
+ return { category: classifyTransportError(err), userMessage: CONNECTIVITY_MESSAGE, cause: err };
109
+ }
110
+ return { userMessage: GENERIC_MESSAGE, cause: err };
111
+ }
package/src/index.ts CHANGED
@@ -20,10 +20,15 @@
20
20
  * // Create a signer
21
21
  * const signer = new FalconSigner(secretKey);
22
22
  *
23
- * // Create multisig client
23
+ * // Create multisig client. Both endpoints are required; midenRpcEndpoint
24
+ * // must point at the same network as the injected MidenClient.
24
25
  * const client = new MultisigClient(midenClient, {
25
26
  * guardianEndpoint: 'http://localhost:3000',
26
27
  * midenRpcEndpoint: 'https://rpc.devnet.miden.io',
28
+ * prover: {
29
+ * url: 'https://prover.example',
30
+ * retry: { maxAttempts: 4 },
31
+ * },
27
32
  * });
28
33
  *
29
34
  * // Get GUARDIAN pubkey for config
@@ -44,6 +49,7 @@ export {
44
49
  type MultisigClientConfig,
45
50
  type RecoveredAccount,
46
51
  } from './client.js';
52
+ export type { ProverConfig, ProverRetryPolicy } from './prover/config.js';
47
53
  export { lookupAuthDigest } from './lookupAuth.js';
48
54
  export { Multisig, type AccountState } from './multisig.js';
49
55
  export { AccountInspector, type DetectedMultisigConfig, type VaultBalance } from './inspector.js';
@@ -54,9 +60,25 @@ export {
54
60
  buildUpdateGuardianTransactionRequest,
55
61
  buildConsumeNotesTransactionRequest,
56
62
  buildP2idTransactionRequest,
63
+ parseP2idNoteType,
64
+ p2idNoteTypeToMetadata,
65
+ type P2idTransactionOptions,
57
66
  } from './transaction.js';
58
67
 
59
68
  export { GuardianHttpClient, GuardianHttpError } from '@openzeppelin/guardian-client';
69
+ export type { GuardianErrorMeta } from '@openzeppelin/guardian-client';
70
+ // Typed error-code vocabulary (issue #318): branch on GuardianErrorCode,
71
+ // never on message text; unknown wire codes surface via rawCode.
72
+ export {
73
+ GUARDIAN_ERROR_CODES,
74
+ isGuardianErrorCode,
75
+ normalizeGuardianErrorCode,
76
+ } from '@openzeppelin/guardian-client';
77
+ export type { GuardianErrorCode } from '@openzeppelin/guardian-client';
78
+
79
+ // Codeless transport-failure classification (feature 009, User Story 3).
80
+ export { isLikelyNetworkError, toUserFacingError } from './connectivity.js';
81
+ export type { ConnectivityCategory, UserFacingError } from './connectivity.js';
60
82
 
61
83
  export {
62
84
  FalconSigner,
@@ -84,6 +106,8 @@ export {
84
106
  MAX_CONSUME_NOTES_METADATA_BYTES,
85
107
  isConsumeNotesV1,
86
108
  isConsumeNotesV2,
109
+ isP2idNoteVisibility,
110
+ type P2idNoteVisibility,
87
111
  } from './types/proposal.js';
88
112
 
89
113
  export {