@openzeppelin/miden-multisig-client 0.16.1 → 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.
- package/README.md +48 -4
- package/dist/client.d.ts +4 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +5 -2
- package/dist/client.js.map +1 -1
- package/dist/client.test.js +7 -2
- package/dist/client.test.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/multisig.d.ts +3 -1
- package/dist/multisig.d.ts.map +1 -1
- package/dist/multisig.js +18 -9
- package/dist/multisig.js.map +1 -1
- package/dist/multisig.test.js +1 -0
- package/dist/multisig.test.js.map +1 -1
- package/dist/prover/errors.d.ts +5 -0
- package/dist/prover/errors.d.ts.map +1 -1
- package/dist/prover/errors.js +7 -155
- package/dist/prover/errors.js.map +1 -1
- package/dist/prover/errors.test.js +5 -0
- package/dist/prover/errors.test.js.map +1 -1
- package/dist/prover/retry.d.ts +1 -6
- package/dist/prover/retry.d.ts.map +1 -1
- package/dist/prover/retry.js +5 -28
- package/dist/prover/retry.js.map +1 -1
- package/dist/prover/retry.test.js +1 -1
- package/dist/prover/retry.test.js.map +1 -1
- package/dist/prover/workflow.d.ts +1 -1
- package/dist/prover/workflow.d.ts.map +1 -1
- package/dist/prover/workflow.test.js +23 -0
- package/dist/prover/workflow.test.js.map +1 -1
- package/dist/retry/classify.d.ts +16 -0
- package/dist/retry/classify.d.ts.map +1 -0
- package/dist/retry/classify.js +187 -0
- package/dist/retry/classify.js.map +1 -0
- package/dist/retry/runtime.d.ts +13 -0
- package/dist/retry/runtime.d.ts.map +1 -0
- package/dist/retry/runtime.js +33 -0
- package/dist/retry/runtime.js.map +1 -0
- package/dist/rpc/config.d.ts +11 -0
- package/dist/rpc/config.d.ts.map +1 -0
- package/dist/rpc/config.js +17 -0
- package/dist/rpc/config.js.map +1 -0
- package/dist/rpc/config.test.d.ts +2 -0
- package/dist/rpc/config.test.d.ts.map +1 -0
- package/dist/rpc/config.test.js +24 -0
- package/dist/rpc/config.test.js.map +1 -0
- package/dist/rpc/errors.d.ts +2 -0
- package/dist/rpc/errors.d.ts.map +1 -0
- package/dist/rpc/errors.js +11 -0
- package/dist/rpc/errors.js.map +1 -0
- package/dist/rpc/errors.test.d.ts +2 -0
- package/dist/rpc/errors.test.d.ts.map +1 -0
- package/dist/rpc/errors.test.js +34 -0
- package/dist/rpc/errors.test.js.map +1 -0
- package/dist/rpc/retry.d.ts +4 -0
- package/dist/rpc/retry.d.ts.map +1 -0
- package/dist/rpc/retry.js +6 -0
- package/dist/rpc/retry.js.map +1 -0
- package/dist/rpc/retry.test.d.ts +2 -0
- package/dist/rpc/retry.test.d.ts.map +1 -0
- package/dist/rpc/retry.test.js +98 -0
- package/dist/rpc/retry.test.js.map +1 -0
- package/package.json +2 -2
- package/src/client.test.ts +8 -2
- package/src/client.ts +11 -0
- package/src/index.ts +1 -0
- package/src/multisig.test.ts +1 -0
- package/src/multisig.ts +39 -8
- package/src/prover/errors.test.ts +8 -0
- package/src/prover/errors.ts +7 -175
- package/src/prover/retry.test.ts +1 -1
- package/src/prover/retry.ts +10 -35
- package/src/prover/workflow.test.ts +32 -1
- package/src/prover/workflow.ts +1 -1
- package/src/retry/classify.ts +220 -0
- package/src/retry/runtime.ts +45 -0
- package/src/rpc/config.test.ts +45 -0
- package/src/rpc/config.ts +30 -0
- package/src/rpc/errors.test.ts +69 -0
- package/src/rpc/errors.ts +12 -0
- package/src/rpc/retry.test.ts +144 -0
- package/src/rpc/retry.ts +12 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { isTransientRpcError } from './errors.js';
|
|
4
|
+
|
|
5
|
+
interface FixtureError {
|
|
6
|
+
code?: string;
|
|
7
|
+
status?: number;
|
|
8
|
+
message: string;
|
|
9
|
+
cause?: FixtureError;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface Fixtures {
|
|
13
|
+
classifications: Array<{
|
|
14
|
+
name: string;
|
|
15
|
+
chain: FixtureError[];
|
|
16
|
+
transient: boolean;
|
|
17
|
+
}>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function fixtures(): Fixtures {
|
|
21
|
+
return JSON.parse(
|
|
22
|
+
readFileSync(
|
|
23
|
+
new URL(
|
|
24
|
+
'../../../../fixtures/miden-multisig-client/rpc-policy-fixtures.json',
|
|
25
|
+
import.meta.url,
|
|
26
|
+
),
|
|
27
|
+
'utf8',
|
|
28
|
+
),
|
|
29
|
+
) as Fixtures;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe('isTransientRpcError', () => {
|
|
33
|
+
it('matches every shared classification vector', () => {
|
|
34
|
+
for (const fixture of fixtures().classifications) {
|
|
35
|
+
const error = fixture.chain.reduceRight<FixtureError | undefined>(
|
|
36
|
+
(cause, item) => ({ ...item, cause }),
|
|
37
|
+
undefined,
|
|
38
|
+
);
|
|
39
|
+
expect(isTransientRpcError(error), fixture.name).toBe(fixture.transient);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('stops safely on cyclic cause graphs', () => {
|
|
44
|
+
const error: FixtureError = { code: 'Unknown', message: 'not retryable' };
|
|
45
|
+
error.cause = error;
|
|
46
|
+
expect(isTransientRpcError(error)).toBe(false);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('recognizes numeric gRPC status codes', () => {
|
|
50
|
+
expect(isTransientRpcError({ code: 14, message: 'unavailable' })).toBe(true);
|
|
51
|
+
expect(isTransientRpcError({ code: 3, message: 'timeout text' })).toBe(false);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('keeps permanent status evidence ahead of transport wording', () => {
|
|
55
|
+
expect(
|
|
56
|
+
isTransientRpcError({
|
|
57
|
+
code: 'FailedPrecondition',
|
|
58
|
+
message: 'transport error while checking preconditions',
|
|
59
|
+
}),
|
|
60
|
+
).toBe(false);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('reads grpc code wording out of plain message text', () => {
|
|
64
|
+
expect(isTransientRpcError(new Error('grpc code: InvalidArgument; request timeout'))).toBe(
|
|
65
|
+
false,
|
|
66
|
+
);
|
|
67
|
+
expect(isTransientRpcError(new Error('grpc code: Unavailable'))).toBe(true);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { isTransientError } from '../retry/classify.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The node's transport layer renders dropped connections with wording the
|
|
5
|
+
* prover policy deliberately rejects, so these extras apply to node-RPC
|
|
6
|
+
* classification only. Guarded by the negative classification fixtures.
|
|
7
|
+
*/
|
|
8
|
+
const RPC_TRANSPORT_SIGNALS = ['connection error', 'transport error'];
|
|
9
|
+
|
|
10
|
+
export function isTransientRpcError(error: unknown): boolean {
|
|
11
|
+
return isTransientError(error, RPC_TRANSPORT_SIGNALS);
|
|
12
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { resolveRpcConfig } from './config.js';
|
|
4
|
+
import { retryRpcRead } from './retry.js';
|
|
5
|
+
import type { RetryRuntime } from '../retry/runtime.js';
|
|
6
|
+
import { retryDelay } from '../retry/runtime.js';
|
|
7
|
+
|
|
8
|
+
interface Fixtures {
|
|
9
|
+
delays: Array<{ retryIndex: number; unitRandom: number; delayMs: number }>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function fixtures(): Fixtures {
|
|
13
|
+
return JSON.parse(
|
|
14
|
+
readFileSync(
|
|
15
|
+
new URL(
|
|
16
|
+
'../../../../fixtures/miden-multisig-client/rpc-policy-fixtures.json',
|
|
17
|
+
import.meta.url,
|
|
18
|
+
),
|
|
19
|
+
'utf8',
|
|
20
|
+
),
|
|
21
|
+
) as Fixtures;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function recordingRuntime(): RetryRuntime & { sleeps: number[] } {
|
|
25
|
+
const sleeps: number[] = [];
|
|
26
|
+
return {
|
|
27
|
+
sleeps,
|
|
28
|
+
sleep: (delayMs) => {
|
|
29
|
+
sleeps.push(delayMs);
|
|
30
|
+
return Promise.resolve();
|
|
31
|
+
},
|
|
32
|
+
unitRandom: () => 0.5,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function rateLimitError(): Error {
|
|
37
|
+
return Object.assign(new Error('Too Many Requests!'), {
|
|
38
|
+
code: 'ResourceExhausted',
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe('retryRpcRead', () => {
|
|
43
|
+
it('matches every shared delay vector', () => {
|
|
44
|
+
for (const fixture of fixtures().delays) {
|
|
45
|
+
expect(retryDelay(fixture.retryIndex, fixture.unitRandom)).toBe(fixture.delayMs);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('waits out rate limiting and returns the eventual result', async () => {
|
|
50
|
+
const runtime = recordingRuntime();
|
|
51
|
+
const operation = vi
|
|
52
|
+
.fn()
|
|
53
|
+
.mockRejectedValueOnce(rateLimitError())
|
|
54
|
+
.mockRejectedValueOnce(rateLimitError())
|
|
55
|
+
.mockResolvedValueOnce('synced');
|
|
56
|
+
|
|
57
|
+
const result = await retryRpcRead(
|
|
58
|
+
operation,
|
|
59
|
+
resolveRpcConfig({ retry: { maxAttempts: 3 } }),
|
|
60
|
+
runtime,
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
expect(result).toBe('synced');
|
|
64
|
+
expect(operation).toHaveBeenCalledTimes(3);
|
|
65
|
+
expect(runtime.sleeps).toEqual([retryDelay(0, 0.5), retryDelay(1, 0.5)]);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('throws a permanent error immediately without sleeping', async () => {
|
|
69
|
+
const runtime = recordingRuntime();
|
|
70
|
+
const permanent = Object.assign(new Error('malformed account id'), {
|
|
71
|
+
code: 'InvalidArgument',
|
|
72
|
+
});
|
|
73
|
+
const operation = vi.fn().mockRejectedValue(permanent);
|
|
74
|
+
|
|
75
|
+
await expect(
|
|
76
|
+
retryRpcRead(operation, resolveRpcConfig({ retry: { maxAttempts: 5 } }), runtime),
|
|
77
|
+
).rejects.toBe(permanent);
|
|
78
|
+
expect(operation).toHaveBeenCalledTimes(1);
|
|
79
|
+
expect(runtime.sleeps).toEqual([]);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('rethrows the final upstream error unchanged after budget exhaustion', async () => {
|
|
83
|
+
const runtime = recordingRuntime();
|
|
84
|
+
const final = Object.assign(new Error('still rate limited'), {
|
|
85
|
+
code: 'ResourceExhausted',
|
|
86
|
+
});
|
|
87
|
+
const operation = vi
|
|
88
|
+
.fn()
|
|
89
|
+
.mockRejectedValueOnce(rateLimitError())
|
|
90
|
+
.mockRejectedValueOnce(final);
|
|
91
|
+
|
|
92
|
+
await expect(
|
|
93
|
+
retryRpcRead(operation, resolveRpcConfig({ retry: { maxAttempts: 2 } }), runtime),
|
|
94
|
+
).rejects.toBe(final);
|
|
95
|
+
expect(operation).toHaveBeenCalledTimes(2);
|
|
96
|
+
expect(runtime.sleeps).toHaveLength(1);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('retries a transient read once under the default config', async () => {
|
|
100
|
+
const runtime = recordingRuntime();
|
|
101
|
+
const operation = vi
|
|
102
|
+
.fn()
|
|
103
|
+
.mockRejectedValueOnce(rateLimitError())
|
|
104
|
+
.mockResolvedValueOnce('synced');
|
|
105
|
+
|
|
106
|
+
await expect(retryRpcRead(operation, resolveRpcConfig(undefined), runtime)).resolves.toBe(
|
|
107
|
+
'synced',
|
|
108
|
+
);
|
|
109
|
+
expect(operation).toHaveBeenCalledTimes(2);
|
|
110
|
+
expect(runtime.sleeps).toHaveLength(1);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('never retries under an explicit single-attempt config', async () => {
|
|
114
|
+
const runtime = recordingRuntime();
|
|
115
|
+
const operation = vi.fn().mockRejectedValue(rateLimitError());
|
|
116
|
+
|
|
117
|
+
await expect(
|
|
118
|
+
retryRpcRead(operation, resolveRpcConfig({ retry: { maxAttempts: 1 } }), runtime),
|
|
119
|
+
).rejects.toThrow('Too Many Requests!');
|
|
120
|
+
expect(operation).toHaveBeenCalledTimes(1);
|
|
121
|
+
expect(runtime.sleeps).toEqual([]);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('completes 64 concurrent reads against a rate-limited node', async () => {
|
|
125
|
+
const runtime = recordingRuntime();
|
|
126
|
+
let rejected = 0;
|
|
127
|
+
const operation = () => {
|
|
128
|
+
if (rejected < 40) {
|
|
129
|
+
rejected += 1;
|
|
130
|
+
return Promise.reject(rateLimitError());
|
|
131
|
+
}
|
|
132
|
+
return Promise.resolve('synced');
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const results = await Promise.all(
|
|
136
|
+
Array.from({ length: 64 }, () =>
|
|
137
|
+
retryRpcRead(operation, resolveRpcConfig({ retry: { maxAttempts: 4 } }), runtime),
|
|
138
|
+
),
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
expect(results).toEqual(Array.from({ length: 64 }, () => 'synced'));
|
|
142
|
+
expect(rejected).toBe(40);
|
|
143
|
+
});
|
|
144
|
+
});
|
package/src/rpc/retry.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ResolvedRpcConfig } from './config.js';
|
|
2
|
+
import { isTransientRpcError } from './errors.js';
|
|
3
|
+
import type { RetryRuntime } from '../retry/runtime.js';
|
|
4
|
+
import { productionRetryRuntime, retryTransient } from '../retry/runtime.js';
|
|
5
|
+
|
|
6
|
+
export async function retryRpcRead<T>(
|
|
7
|
+
operation: () => Promise<T>,
|
|
8
|
+
config: ResolvedRpcConfig,
|
|
9
|
+
runtime: RetryRuntime = productionRetryRuntime,
|
|
10
|
+
): Promise<T> {
|
|
11
|
+
return retryTransient(operation, config.maxAttempts, isTransientRpcError, runtime);
|
|
12
|
+
}
|