@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.
- package/README.md +37 -1
- package/dist/account/builder.d.ts +2 -1
- package/dist/account/builder.d.ts.map +1 -1
- package/dist/account/builder.js +1 -0
- package/dist/account/builder.js.map +1 -1
- package/dist/account/builder.test.js +2 -2
- package/dist/account/builder.test.js.map +1 -1
- package/dist/client.d.ts +17 -5
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +14 -7
- package/dist/client.js.map +1 -1
- package/dist/client.test.js +46 -15
- package/dist/client.test.js.map +1 -1
- package/dist/connectivity.d.ts +38 -0
- package/dist/connectivity.d.ts.map +1 -0
- package/dist/connectivity.js +97 -0
- package/dist/connectivity.js.map +1 -0
- package/dist/connectivity.test.d.ts +2 -0
- package/dist/connectivity.test.d.ts.map +1 -0
- package/dist/connectivity.test.js +61 -0
- package/dist/connectivity.test.js.map +1 -0
- package/dist/index.d.ts +14 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -3
- package/dist/index.js.map +1 -1
- package/dist/multisig.d.ts +130 -8
- package/dist/multisig.d.ts.map +1 -1
- package/dist/multisig.js +220 -24
- package/dist/multisig.js.map +1 -1
- package/dist/multisig.test.js +427 -89
- package/dist/multisig.test.js.map +1 -1
- package/dist/proposal/factory.d.ts.map +1 -1
- package/dist/proposal/factory.js +10 -0
- package/dist/proposal/factory.js.map +1 -1
- package/dist/proposal/factory.test.d.ts +2 -0
- package/dist/proposal/factory.test.d.ts.map +1 -0
- package/dist/proposal/factory.test.js +32 -0
- package/dist/proposal/factory.test.js.map +1 -0
- package/dist/proposal/metadata.d.ts.map +1 -1
- package/dist/proposal/metadata.js +12 -0
- package/dist/proposal/metadata.js.map +1 -1
- package/dist/proposal/metadata.test.js +49 -0
- package/dist/proposal/metadata.test.js.map +1 -1
- package/dist/prover/config.d.ts +16 -0
- package/dist/prover/config.d.ts.map +1 -0
- package/dist/prover/config.js +54 -0
- package/dist/prover/config.js.map +1 -0
- package/dist/prover/config.test.d.ts +2 -0
- package/dist/prover/config.test.d.ts.map +1 -0
- package/dist/prover/config.test.js +54 -0
- package/dist/prover/config.test.js.map +1 -0
- package/dist/prover/errors.d.ts +2 -0
- package/dist/prover/errors.d.ts.map +1 -0
- package/dist/prover/errors.js +158 -0
- package/dist/prover/errors.js.map +1 -0
- package/dist/prover/errors.test.d.ts +2 -0
- package/dist/prover/errors.test.d.ts.map +1 -0
- package/dist/prover/errors.test.js +24 -0
- package/dist/prover/errors.test.js.map +1 -0
- package/dist/prover/retry.d.ts +10 -0
- package/dist/prover/retry.d.ts.map +1 -0
- package/dist/prover/retry.js +32 -0
- package/dist/prover/retry.js.map +1 -0
- package/dist/prover/retry.test.d.ts +2 -0
- package/dist/prover/retry.test.d.ts.map +1 -0
- package/dist/prover/retry.test.js +20 -0
- package/dist/prover/retry.test.js.map +1 -0
- package/dist/prover/workflow.d.ts +11 -0
- package/dist/prover/workflow.d.ts.map +1 -0
- package/dist/prover/workflow.js +18 -0
- package/dist/prover/workflow.js.map +1 -0
- package/dist/prover/workflow.test.d.ts +2 -0
- package/dist/prover/workflow.test.d.ts.map +1 -0
- package/dist/prover/workflow.test.js +80 -0
- package/dist/prover/workflow.test.js.map +1 -0
- package/dist/raw-client.d.ts +2 -2
- package/dist/raw-client.d.ts.map +1 -1
- package/dist/raw-client.js +14 -4
- package/dist/raw-client.js.map +1 -1
- package/dist/raw-client.test.js +25 -3
- package/dist/raw-client.test.js.map +1 -1
- package/dist/transaction/consumeNotes.d.ts +6 -2
- package/dist/transaction/consumeNotes.d.ts.map +1 -1
- package/dist/transaction/consumeNotes.js +0 -4
- package/dist/transaction/consumeNotes.js.map +1 -1
- package/dist/transaction/options.d.ts +3 -0
- package/dist/transaction/options.d.ts.map +1 -1
- package/dist/transaction/p2id.d.ts +27 -2
- package/dist/transaction/p2id.d.ts.map +1 -1
- package/dist/transaction/p2id.js +56 -4
- package/dist/transaction/p2id.js.map +1 -1
- package/dist/transaction/p2id.test.js +66 -6
- package/dist/transaction/p2id.test.js.map +1 -1
- package/dist/transaction/summary.d.ts +2 -1
- package/dist/transaction/summary.d.ts.map +1 -1
- package/dist/transaction/summary.js.map +1 -1
- package/dist/transaction/updateGuardian.d.ts +6 -2
- package/dist/transaction/updateGuardian.d.ts.map +1 -1
- package/dist/transaction/updateGuardian.js.map +1 -1
- package/dist/transaction/updateProcedureThreshold.d.ts +7 -2
- package/dist/transaction/updateProcedureThreshold.d.ts.map +1 -1
- package/dist/transaction/updateProcedureThreshold.js.map +1 -1
- package/dist/transaction/updateSigners.d.ts +7 -2
- package/dist/transaction/updateSigners.d.ts.map +1 -1
- package/dist/transaction/updateSigners.js.map +1 -1
- package/dist/transaction.d.ts +1 -1
- package/dist/transaction.d.ts.map +1 -1
- package/dist/transaction.js +1 -1
- package/dist/transaction.js.map +1 -1
- package/dist/types/proposal.d.ts +5 -0
- package/dist/types/proposal.d.ts.map +1 -1
- package/dist/types/proposal.js +3 -0
- package/dist/types/proposal.js.map +1 -1
- package/package.json +3 -3
- package/src/account/builder.test.ts +19 -11
- package/src/account/builder.ts +2 -1
- package/src/client.test.ts +67 -15
- package/src/client.ts +35 -12
- package/src/connectivity.test.ts +67 -0
- package/src/connectivity.ts +111 -0
- package/src/index.ts +25 -1
- package/src/multisig.test.ts +526 -94
- package/src/multisig.ts +270 -25
- package/src/proposal/factory.test.ts +40 -0
- package/src/proposal/factory.ts +11 -1
- package/src/proposal/metadata.test.ts +60 -0
- package/src/proposal/metadata.ts +14 -0
- package/src/prover/config.test.ts +90 -0
- package/src/prover/config.ts +78 -0
- package/src/prover/errors.test.ts +53 -0
- package/src/prover/errors.ts +178 -0
- package/src/prover/retry.test.ts +38 -0
- package/src/prover/retry.ts +46 -0
- package/src/prover/test-node.d.ts +6 -0
- package/src/prover/workflow.test.ts +109 -0
- package/src/prover/workflow.ts +19 -0
- package/src/raw-client.test.ts +41 -3
- package/src/raw-client.ts +15 -5
- package/src/transaction/consumeNotes.ts +11 -1
- package/src/transaction/options.ts +4 -0
- package/src/transaction/p2id.test.ts +98 -8
- package/src/transaction/p2id.ts +91 -12
- package/src/transaction/summary.ts +12 -0
- package/src/transaction/updateGuardian.ts +11 -1
- package/src/transaction/updateProcedureThreshold.ts +13 -1
- package/src/transaction/updateSigners.ts +13 -1
- package/src/transaction.ts +4 -0
- package/src/types/proposal.ts +9 -0
package/src/client.test.ts
CHANGED
|
@@ -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
|
|
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, {
|
|
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,
|
|
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
|
|
38
|
-
/**
|
|
39
|
-
|
|
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 =
|
|
87
|
-
this.
|
|
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(
|
|
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 {
|