@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 @@
|
|
|
1
|
+
{"version":3,"file":"runtime.js","sourceRoot":"","sources":["../../src/retry/runtime.ts"],"names":[],"mappings":"AAAA,MAAM,aAAa,GAAG,GAAG,CAAC;AAC1B,MAAM,YAAY,GAAG,KAAK,CAAC;AAO3B,MAAM,CAAC,MAAM,sBAAsB,GAAiB;IAClD,KAAK,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IAC1E,UAAU,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,MAAM,EAAE;CAChC,CAAC;AAEF,MAAM,UAAU,UAAU,CAAC,UAAkB,EAAE,UAAkB;IAC/D,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC;IAC5C,MAAM,GAAG,GAAG,aAAa,GAAG,CAAC,IAAI,QAAQ,CAAC;IAC1C,MAAM,YAAY,GAAG,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,GAAG,CAAC;IACpE,MAAM,aAAa,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,YAAY,EAAE,CAAC,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC;IAC9E,OAAO,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,IAAI,GAAG,aAAa,GAAG,GAAG,CAAC,CAAC,EAAE,YAAY,CAAC,CAAC;AAChF,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,SAA2B,EAC3B,WAAmB,EACnB,WAAwC,EACxC,OAAqB;IAErB,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,GAAG,WAAW,EAAE,OAAO,IAAI,CAAC,EAAE,CAAC;QAC1D,IAAI,CAAC;YACH,OAAO,MAAM,SAAS,EAAE,CAAC;QAC3B,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,OAAO,GAAG,CAAC,IAAI,WAAW,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,EAAE,CAAC;gBACtD,MAAM,KAAK,CAAC;YACd,CAAC;YACD,MAAM,OAAO,CAAC,KAAK,CAAC,UAAU,CAAC,OAAO,EAAE,OAAO,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC;QACjE,CAAC;IACH,CAAC;IAED,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAC;AAC7C,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface RpcRetryPolicy {
|
|
2
|
+
maxAttempts?: number;
|
|
3
|
+
}
|
|
4
|
+
export interface RpcConfig {
|
|
5
|
+
retry?: RpcRetryPolicy;
|
|
6
|
+
}
|
|
7
|
+
export interface ResolvedRpcConfig {
|
|
8
|
+
readonly maxAttempts: number;
|
|
9
|
+
}
|
|
10
|
+
export declare function resolveRpcConfig(config: RpcConfig | undefined): ResolvedRpcConfig;
|
|
11
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/rpc/config.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,cAAc;IAC7B,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,SAAS;IACxB,KAAK,CAAC,EAAE,cAAc,CAAC;CACxB;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B;AAeD,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,SAAS,GAAG,SAAS,GAAG,iBAAiB,CAIjF"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const DEFAULT_MAX_ATTEMPTS = 2;
|
|
2
|
+
const MAX_U32 = 4_294_967_295;
|
|
3
|
+
function normalizeMaxAttempts(value) {
|
|
4
|
+
if (value === undefined) {
|
|
5
|
+
return DEFAULT_MAX_ATTEMPTS;
|
|
6
|
+
}
|
|
7
|
+
if (!Number.isFinite(value) || !Number.isInteger(value) || value < 0 || value > MAX_U32) {
|
|
8
|
+
throw new Error('rpc.retry.maxAttempts must be an integer between 0 and 4294967295');
|
|
9
|
+
}
|
|
10
|
+
return Math.max(1, value);
|
|
11
|
+
}
|
|
12
|
+
export function resolveRpcConfig(config) {
|
|
13
|
+
return {
|
|
14
|
+
maxAttempts: normalizeMaxAttempts(config?.retry?.maxAttempts),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/rpc/config.ts"],"names":[],"mappings":"AAYA,MAAM,oBAAoB,GAAG,CAAC,CAAC;AAC/B,MAAM,OAAO,GAAG,aAAa,CAAC;AAE9B,SAAS,oBAAoB,CAAC,KAAyB;IACrD,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QACxB,OAAO,oBAAoB,CAAC;IAC9B,CAAC;IACD,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC,IAAI,KAAK,GAAG,OAAO,EAAE,CAAC;QACxF,MAAM,IAAI,KAAK,CAAC,mEAAmE,CAAC,CAAC;IACvF,CAAC;IACD,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;AAC5B,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,MAA6B;IAC5D,OAAO;QACL,WAAW,EAAE,oBAAoB,CAAC,MAAM,EAAE,KAAK,EAAE,WAAW,CAAC;KAC9D,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.test.d.ts","sourceRoot":"","sources":["../../src/rpc/config.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { resolveRpcConfig } from './config.js';
|
|
4
|
+
function fixtures() {
|
|
5
|
+
return JSON.parse(readFileSync(new URL('../../../../fixtures/miden-multisig-client/rpc-policy-fixtures.json', import.meta.url), 'utf8'));
|
|
6
|
+
}
|
|
7
|
+
describe('resolveRpcConfig', () => {
|
|
8
|
+
it('matches shared attempt-budget vectors', () => {
|
|
9
|
+
for (const fixture of fixtures().attemptBudgets) {
|
|
10
|
+
const retry = fixture.input === null ? undefined : { maxAttempts: fixture.input };
|
|
11
|
+
expect(resolveRpcConfig({ retry }).maxAttempts).toBe(fixture.normalized);
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
it('defaults to one retry when no config is given', () => {
|
|
15
|
+
expect(resolveRpcConfig(undefined).maxAttempts).toBe(2);
|
|
16
|
+
});
|
|
17
|
+
it('treats an explicit single attempt as a full opt-out', () => {
|
|
18
|
+
expect(resolveRpcConfig({ retry: { maxAttempts: 1 } }).maxAttempts).toBe(1);
|
|
19
|
+
});
|
|
20
|
+
it.each([-1, 1.5, Number.NaN, Number.POSITIVE_INFINITY, 4_294_967_296])('rejects invalid maxAttempts value %s', (maxAttempts) => {
|
|
21
|
+
expect(() => resolveRpcConfig({ retry: { maxAttempts } })).toThrow('rpc.retry.maxAttempts');
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
//# sourceMappingURL=config.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.test.js","sourceRoot":"","sources":["../../src/rpc/config.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAM/C,SAAS,QAAQ;IACf,OAAO,IAAI,CAAC,KAAK,CACf,YAAY,CACV,IAAI,GAAG,CACL,qEAAqE,EACrE,MAAM,CAAC,IAAI,CAAC,GAAG,CAChB,EACD,MAAM,CACP,CACU,CAAC;AAChB,CAAC;AAED,QAAQ,CAAC,kBAAkB,EAAE,GAAG,EAAE;IAChC,EAAE,CAAC,uCAAuC,EAAE,GAAG,EAAE;QAC/C,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC,cAAc,EAAE,CAAC;YAChD,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,OAAO,CAAC,KAAK,EAAE,CAAC;YAClF,MAAM,CAAC,gBAAgB,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;QAC3E,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,+CAA+C,EAAE,GAAG,EAAE;QACvD,MAAM,CAAC,gBAAgB,CAAC,SAAS,CAAC,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC1D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,qDAAqD,EAAE,GAAG,EAAE;QAC7D,MAAM,CAAC,gBAAgB,CAAC,EAAE,KAAK,EAAE,EAAE,WAAW,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC9E,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,iBAAiB,EAAE,aAAa,CAAC,CAAC,CACrE,sCAAsC,EACtC,CAAC,WAAW,EAAE,EAAE;QACd,MAAM,CAAC,GAAG,EAAE,CAAC,gBAAgB,CAAC,EAAE,KAAK,EAAE,EAAE,WAAW,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAChE,uBAAuB,CACxB,CAAC;IACJ,CAAC,CACF,CAAC;AACJ,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/rpc/errors.ts"],"names":[],"mappings":"AASA,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAE3D"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { isTransientError } from '../retry/classify.js';
|
|
2
|
+
/**
|
|
3
|
+
* The node's transport layer renders dropped connections with wording the
|
|
4
|
+
* prover policy deliberately rejects, so these extras apply to node-RPC
|
|
5
|
+
* classification only. Guarded by the negative classification fixtures.
|
|
6
|
+
*/
|
|
7
|
+
const RPC_TRANSPORT_SIGNALS = ['connection error', 'transport error'];
|
|
8
|
+
export function isTransientRpcError(error) {
|
|
9
|
+
return isTransientError(error, RPC_TRANSPORT_SIGNALS);
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/rpc/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAExD;;;;GAIG;AACH,MAAM,qBAAqB,GAAG,CAAC,kBAAkB,EAAE,iBAAiB,CAAC,CAAC;AAEtE,MAAM,UAAU,mBAAmB,CAAC,KAAc;IAChD,OAAO,gBAAgB,CAAC,KAAK,EAAE,qBAAqB,CAAC,CAAC;AACxD,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.test.d.ts","sourceRoot":"","sources":["../../src/rpc/errors.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { isTransientRpcError } from './errors.js';
|
|
4
|
+
function fixtures() {
|
|
5
|
+
return JSON.parse(readFileSync(new URL('../../../../fixtures/miden-multisig-client/rpc-policy-fixtures.json', import.meta.url), 'utf8'));
|
|
6
|
+
}
|
|
7
|
+
describe('isTransientRpcError', () => {
|
|
8
|
+
it('matches every shared classification vector', () => {
|
|
9
|
+
for (const fixture of fixtures().classifications) {
|
|
10
|
+
const error = fixture.chain.reduceRight((cause, item) => ({ ...item, cause }), undefined);
|
|
11
|
+
expect(isTransientRpcError(error), fixture.name).toBe(fixture.transient);
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
it('stops safely on cyclic cause graphs', () => {
|
|
15
|
+
const error = { code: 'Unknown', message: 'not retryable' };
|
|
16
|
+
error.cause = error;
|
|
17
|
+
expect(isTransientRpcError(error)).toBe(false);
|
|
18
|
+
});
|
|
19
|
+
it('recognizes numeric gRPC status codes', () => {
|
|
20
|
+
expect(isTransientRpcError({ code: 14, message: 'unavailable' })).toBe(true);
|
|
21
|
+
expect(isTransientRpcError({ code: 3, message: 'timeout text' })).toBe(false);
|
|
22
|
+
});
|
|
23
|
+
it('keeps permanent status evidence ahead of transport wording', () => {
|
|
24
|
+
expect(isTransientRpcError({
|
|
25
|
+
code: 'FailedPrecondition',
|
|
26
|
+
message: 'transport error while checking preconditions',
|
|
27
|
+
})).toBe(false);
|
|
28
|
+
});
|
|
29
|
+
it('reads grpc code wording out of plain message text', () => {
|
|
30
|
+
expect(isTransientRpcError(new Error('grpc code: InvalidArgument; request timeout'))).toBe(false);
|
|
31
|
+
expect(isTransientRpcError(new Error('grpc code: Unavailable'))).toBe(true);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
//# sourceMappingURL=errors.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.test.js","sourceRoot":"","sources":["../../src/rpc/errors.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAiBlD,SAAS,QAAQ;IACf,OAAO,IAAI,CAAC,KAAK,CACf,YAAY,CACV,IAAI,GAAG,CACL,qEAAqE,EACrE,MAAM,CAAC,IAAI,CAAC,GAAG,CAChB,EACD,MAAM,CACP,CACU,CAAC;AAChB,CAAC;AAED,QAAQ,CAAC,qBAAqB,EAAE,GAAG,EAAE;IACnC,EAAE,CAAC,4CAA4C,EAAE,GAAG,EAAE;QACpD,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC,eAAe,EAAE,CAAC;YACjD,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,WAAW,CACrC,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,KAAK,EAAE,CAAC,EACrC,SAAS,CACV,CAAC;YACF,MAAM,CAAC,mBAAmB,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;QAC3E,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,qCAAqC,EAAE,GAAG,EAAE;QAC7C,MAAM,KAAK,GAAiB,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,eAAe,EAAE,CAAC;QAC1E,KAAK,CAAC,KAAK,GAAG,KAAK,CAAC;QACpB,MAAM,CAAC,mBAAmB,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACjD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,sCAAsC,EAAE,GAAG,EAAE;QAC9C,MAAM,CAAC,mBAAmB,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,OAAO,EAAE,aAAa,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC7E,MAAM,CAAC,mBAAmB,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,OAAO,EAAE,cAAc,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAChF,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,4DAA4D,EAAE,GAAG,EAAE;QACpE,MAAM,CACJ,mBAAmB,CAAC;YAClB,IAAI,EAAE,oBAAoB;YAC1B,OAAO,EAAE,8CAA8C;SACxD,CAAC,CACH,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAChB,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,mDAAmD,EAAE,GAAG,EAAE;QAC3D,MAAM,CAAC,mBAAmB,CAAC,IAAI,KAAK,CAAC,6CAA6C,CAAC,CAAC,CAAC,CAAC,IAAI,CACxF,KAAK,CACN,CAAC;QACF,MAAM,CAAC,mBAAmB,CAAC,IAAI,KAAK,CAAC,wBAAwB,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC9E,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ResolvedRpcConfig } from './config.js';
|
|
2
|
+
import type { RetryRuntime } from '../retry/runtime.js';
|
|
3
|
+
export declare function retryRpcRead<T>(operation: () => Promise<T>, config: ResolvedRpcConfig, runtime?: RetryRuntime): Promise<T>;
|
|
4
|
+
//# sourceMappingURL=retry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"retry.d.ts","sourceRoot":"","sources":["../../src/rpc/retry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAErD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAGxD,wBAAsB,YAAY,CAAC,CAAC,EAClC,SAAS,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAC3B,MAAM,EAAE,iBAAiB,EACzB,OAAO,GAAE,YAAqC,GAC7C,OAAO,CAAC,CAAC,CAAC,CAEZ"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { isTransientRpcError } from './errors.js';
|
|
2
|
+
import { productionRetryRuntime, retryTransient } from '../retry/runtime.js';
|
|
3
|
+
export async function retryRpcRead(operation, config, runtime = productionRetryRuntime) {
|
|
4
|
+
return retryTransient(operation, config.maxAttempts, isTransientRpcError, runtime);
|
|
5
|
+
}
|
|
6
|
+
//# sourceMappingURL=retry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"retry.js","sourceRoot":"","sources":["../../src/rpc/retry.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAElD,OAAO,EAAE,sBAAsB,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAE7E,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,SAA2B,EAC3B,MAAyB,EACzB,UAAwB,sBAAsB;IAE9C,OAAO,cAAc,CAAC,SAAS,EAAE,MAAM,CAAC,WAAW,EAAE,mBAAmB,EAAE,OAAO,CAAC,CAAC;AACrF,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"retry.test.d.ts","sourceRoot":"","sources":["../../src/rpc/retry.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,98 @@
|
|
|
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 { retryDelay } from '../retry/runtime.js';
|
|
6
|
+
function fixtures() {
|
|
7
|
+
return JSON.parse(readFileSync(new URL('../../../../fixtures/miden-multisig-client/rpc-policy-fixtures.json', import.meta.url), 'utf8'));
|
|
8
|
+
}
|
|
9
|
+
function recordingRuntime() {
|
|
10
|
+
const sleeps = [];
|
|
11
|
+
return {
|
|
12
|
+
sleeps,
|
|
13
|
+
sleep: (delayMs) => {
|
|
14
|
+
sleeps.push(delayMs);
|
|
15
|
+
return Promise.resolve();
|
|
16
|
+
},
|
|
17
|
+
unitRandom: () => 0.5,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function rateLimitError() {
|
|
21
|
+
return Object.assign(new Error('Too Many Requests!'), {
|
|
22
|
+
code: 'ResourceExhausted',
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
describe('retryRpcRead', () => {
|
|
26
|
+
it('matches every shared delay vector', () => {
|
|
27
|
+
for (const fixture of fixtures().delays) {
|
|
28
|
+
expect(retryDelay(fixture.retryIndex, fixture.unitRandom)).toBe(fixture.delayMs);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
it('waits out rate limiting and returns the eventual result', async () => {
|
|
32
|
+
const runtime = recordingRuntime();
|
|
33
|
+
const operation = vi
|
|
34
|
+
.fn()
|
|
35
|
+
.mockRejectedValueOnce(rateLimitError())
|
|
36
|
+
.mockRejectedValueOnce(rateLimitError())
|
|
37
|
+
.mockResolvedValueOnce('synced');
|
|
38
|
+
const result = await retryRpcRead(operation, resolveRpcConfig({ retry: { maxAttempts: 3 } }), runtime);
|
|
39
|
+
expect(result).toBe('synced');
|
|
40
|
+
expect(operation).toHaveBeenCalledTimes(3);
|
|
41
|
+
expect(runtime.sleeps).toEqual([retryDelay(0, 0.5), retryDelay(1, 0.5)]);
|
|
42
|
+
});
|
|
43
|
+
it('throws a permanent error immediately without sleeping', async () => {
|
|
44
|
+
const runtime = recordingRuntime();
|
|
45
|
+
const permanent = Object.assign(new Error('malformed account id'), {
|
|
46
|
+
code: 'InvalidArgument',
|
|
47
|
+
});
|
|
48
|
+
const operation = vi.fn().mockRejectedValue(permanent);
|
|
49
|
+
await expect(retryRpcRead(operation, resolveRpcConfig({ retry: { maxAttempts: 5 } }), runtime)).rejects.toBe(permanent);
|
|
50
|
+
expect(operation).toHaveBeenCalledTimes(1);
|
|
51
|
+
expect(runtime.sleeps).toEqual([]);
|
|
52
|
+
});
|
|
53
|
+
it('rethrows the final upstream error unchanged after budget exhaustion', async () => {
|
|
54
|
+
const runtime = recordingRuntime();
|
|
55
|
+
const final = Object.assign(new Error('still rate limited'), {
|
|
56
|
+
code: 'ResourceExhausted',
|
|
57
|
+
});
|
|
58
|
+
const operation = vi
|
|
59
|
+
.fn()
|
|
60
|
+
.mockRejectedValueOnce(rateLimitError())
|
|
61
|
+
.mockRejectedValueOnce(final);
|
|
62
|
+
await expect(retryRpcRead(operation, resolveRpcConfig({ retry: { maxAttempts: 2 } }), runtime)).rejects.toBe(final);
|
|
63
|
+
expect(operation).toHaveBeenCalledTimes(2);
|
|
64
|
+
expect(runtime.sleeps).toHaveLength(1);
|
|
65
|
+
});
|
|
66
|
+
it('retries a transient read once under the default config', async () => {
|
|
67
|
+
const runtime = recordingRuntime();
|
|
68
|
+
const operation = vi
|
|
69
|
+
.fn()
|
|
70
|
+
.mockRejectedValueOnce(rateLimitError())
|
|
71
|
+
.mockResolvedValueOnce('synced');
|
|
72
|
+
await expect(retryRpcRead(operation, resolveRpcConfig(undefined), runtime)).resolves.toBe('synced');
|
|
73
|
+
expect(operation).toHaveBeenCalledTimes(2);
|
|
74
|
+
expect(runtime.sleeps).toHaveLength(1);
|
|
75
|
+
});
|
|
76
|
+
it('never retries under an explicit single-attempt config', async () => {
|
|
77
|
+
const runtime = recordingRuntime();
|
|
78
|
+
const operation = vi.fn().mockRejectedValue(rateLimitError());
|
|
79
|
+
await expect(retryRpcRead(operation, resolveRpcConfig({ retry: { maxAttempts: 1 } }), runtime)).rejects.toThrow('Too Many Requests!');
|
|
80
|
+
expect(operation).toHaveBeenCalledTimes(1);
|
|
81
|
+
expect(runtime.sleeps).toEqual([]);
|
|
82
|
+
});
|
|
83
|
+
it('completes 64 concurrent reads against a rate-limited node', async () => {
|
|
84
|
+
const runtime = recordingRuntime();
|
|
85
|
+
let rejected = 0;
|
|
86
|
+
const operation = () => {
|
|
87
|
+
if (rejected < 40) {
|
|
88
|
+
rejected += 1;
|
|
89
|
+
return Promise.reject(rateLimitError());
|
|
90
|
+
}
|
|
91
|
+
return Promise.resolve('synced');
|
|
92
|
+
};
|
|
93
|
+
const results = await Promise.all(Array.from({ length: 64 }, () => retryRpcRead(operation, resolveRpcConfig({ retry: { maxAttempts: 4 } }), runtime)));
|
|
94
|
+
expect(results).toEqual(Array.from({ length: 64 }, () => 'synced'));
|
|
95
|
+
expect(rejected).toBe(40);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
//# sourceMappingURL=retry.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"retry.test.js","sourceRoot":"","sources":["../../src/rpc/retry.test.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openzeppelin/miden-multisig-client",
|
|
3
|
-
"version": "0.16.
|
|
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.
|
|
42
|
+
"@openzeppelin/guardian-client": "^0.16.2"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"typescript": "^5.7.2",
|
package/src/client.test.ts
CHANGED
|
@@ -239,12 +239,18 @@ describe('MultisigClient', () => {
|
|
|
239
239
|
ok: false,
|
|
240
240
|
status: 404,
|
|
241
241
|
statusText: 'Not Found',
|
|
242
|
-
|
|
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
|
@@ -18,6 +18,11 @@ import {
|
|
|
18
18
|
type ProverConfig,
|
|
19
19
|
type ResolvedProverConfig,
|
|
20
20
|
} from './prover/config.js';
|
|
21
|
+
import {
|
|
22
|
+
resolveRpcConfig,
|
|
23
|
+
type RpcConfig,
|
|
24
|
+
type ResolvedRpcConfig,
|
|
25
|
+
} from './rpc/config.js';
|
|
21
26
|
|
|
22
27
|
interface AccountKeyBindingSigner {
|
|
23
28
|
bindAccountKey?(midenClient: MidenClient, accountId: string): Promise<void>;
|
|
@@ -48,6 +53,8 @@ export interface MultisigClientConfig {
|
|
|
48
53
|
midenRpcEndpoint: string;
|
|
49
54
|
/** Multisig-owned remote prover override and proof retry policy. */
|
|
50
55
|
prover?: ProverConfig;
|
|
56
|
+
/** Retry policy for idempotent Miden node reads; submission is never retried. */
|
|
57
|
+
rpc?: RpcConfig;
|
|
51
58
|
}
|
|
52
59
|
|
|
53
60
|
/**
|
|
@@ -95,12 +102,14 @@ export class MultisigClient {
|
|
|
95
102
|
private readonly midenClient: MidenClient;
|
|
96
103
|
private readonly midenRpcEndpoint: string;
|
|
97
104
|
private readonly proverConfig: ResolvedProverConfig;
|
|
105
|
+
private readonly rpcConfig: ResolvedRpcConfig;
|
|
98
106
|
private _guardianClient: GuardianHttpClient;
|
|
99
107
|
|
|
100
108
|
constructor(midenClient: MidenClient, config: MultisigClientConfig) {
|
|
101
109
|
this.midenClient = midenClient;
|
|
102
110
|
this.midenRpcEndpoint = requireMidenRpcEndpoint(config?.midenRpcEndpoint);
|
|
103
111
|
this.proverConfig = resolveProverConfig(config?.prover, midenClient.defaultProver);
|
|
112
|
+
this.rpcConfig = resolveRpcConfig(config?.rpc);
|
|
104
113
|
this._guardianClient = new GuardianHttpClient(
|
|
105
114
|
requireConfigValue('guardianEndpoint', config?.guardianEndpoint),
|
|
106
115
|
);
|
|
@@ -176,6 +185,7 @@ export class MultisigClient {
|
|
|
176
185
|
undefined,
|
|
177
186
|
this.midenRpcEndpoint,
|
|
178
187
|
this.proverConfig,
|
|
188
|
+
this.rpcConfig,
|
|
179
189
|
);
|
|
180
190
|
}
|
|
181
191
|
|
|
@@ -229,6 +239,7 @@ export class MultisigClient {
|
|
|
229
239
|
accountId,
|
|
230
240
|
this.midenRpcEndpoint,
|
|
231
241
|
this.proverConfig,
|
|
242
|
+
this.rpcConfig,
|
|
232
243
|
);
|
|
233
244
|
}
|
|
234
245
|
}
|
package/src/index.ts
CHANGED
|
@@ -50,6 +50,7 @@ export {
|
|
|
50
50
|
type RecoveredAccount,
|
|
51
51
|
} from './client.js';
|
|
52
52
|
export type { ProverConfig, ProverRetryPolicy } from './prover/config.js';
|
|
53
|
+
export type { RpcConfig, RpcRetryPolicy } from './rpc/config.js';
|
|
53
54
|
export { lookupAuthDigest } from './lookupAuth.js';
|
|
54
55
|
export { Multisig, type AccountState } from './multisig.js';
|
|
55
56
|
export { AccountInspector, type DetectedMultisigConfig, type VaultBalance } from './inspector.js';
|
package/src/multisig.test.ts
CHANGED
|
@@ -2293,6 +2293,7 @@ describe('Multisig', () => {
|
|
|
2293
2293
|
ok: false,
|
|
2294
2294
|
status: 404,
|
|
2295
2295
|
statusText: 'Not Found',
|
|
2296
|
+
headers: new Headers(),
|
|
2296
2297
|
// Feature 009: only a conforming { code, message, meta } envelope is
|
|
2297
2298
|
// folded into the error message; raw text bodies are dropped.
|
|
2298
2299
|
text: async () =>
|
package/src/multisig.ts
CHANGED
|
@@ -88,6 +88,11 @@ import {
|
|
|
88
88
|
type ResolvedProverConfig,
|
|
89
89
|
} from './prover/config.js';
|
|
90
90
|
import { ProverWorkflow } from './prover/workflow.js';
|
|
91
|
+
import {
|
|
92
|
+
resolveRpcConfig,
|
|
93
|
+
type ResolvedRpcConfig,
|
|
94
|
+
} from './rpc/config.js';
|
|
95
|
+
import { retryRpcRead } from './rpc/retry.js';
|
|
91
96
|
|
|
92
97
|
/**
|
|
93
98
|
* Result of fetching account state from GUARDIAN.
|
|
@@ -151,6 +156,7 @@ export class Multisig {
|
|
|
151
156
|
private readonly midenClient: MidenClient;
|
|
152
157
|
private readonly rawClientPromise: Promise<WasmWebClient>;
|
|
153
158
|
private readonly proverWorkflow: ProverWorkflow;
|
|
159
|
+
private readonly rpcConfig: ResolvedRpcConfig;
|
|
154
160
|
private readonly _accountId: string;
|
|
155
161
|
private readonly midenRpcEndpoint: string;
|
|
156
162
|
private proposals: Map<string, Proposal> = new Map();
|
|
@@ -164,6 +170,7 @@ export class Multisig {
|
|
|
164
170
|
accountId: string | undefined,
|
|
165
171
|
midenRpcEndpoint: string,
|
|
166
172
|
proverConfig?: ResolvedProverConfig,
|
|
173
|
+
rpcConfig?: ResolvedRpcConfig,
|
|
167
174
|
) {
|
|
168
175
|
this.account = account;
|
|
169
176
|
this.threshold = config.threshold;
|
|
@@ -183,6 +190,7 @@ export class Multisig {
|
|
|
183
190
|
this.midenClient,
|
|
184
191
|
proverConfig ?? resolveProverConfig(undefined, getTransactionProver(midenClient)),
|
|
185
192
|
);
|
|
193
|
+
this.rpcConfig = rpcConfig ?? resolveRpcConfig(undefined);
|
|
186
194
|
}
|
|
187
195
|
|
|
188
196
|
private getMidenRpcEndpoint(): string {
|
|
@@ -238,7 +246,11 @@ export class Multisig {
|
|
|
238
246
|
*/
|
|
239
247
|
async getStoreAccount(): Promise<Account> {
|
|
240
248
|
const webClient = await this.getRawClient();
|
|
241
|
-
|
|
249
|
+
const stored = await retryRpcRead(
|
|
250
|
+
() => webClient.getAccount(AccountId.fromHex(this._accountId)),
|
|
251
|
+
this.rpcConfig,
|
|
252
|
+
);
|
|
253
|
+
return stored ?? this.account;
|
|
242
254
|
}
|
|
243
255
|
|
|
244
256
|
/**
|
|
@@ -326,7 +338,10 @@ export class Multisig {
|
|
|
326
338
|
const state = await this.fetchState();
|
|
327
339
|
const accountId = AccountId.fromHex(this._accountId);
|
|
328
340
|
const webClient = await this.getRawClient();
|
|
329
|
-
const localAccount = await
|
|
341
|
+
const localAccount = await retryRpcRead(
|
|
342
|
+
() => webClient.getAccount(accountId),
|
|
343
|
+
this.rpcConfig,
|
|
344
|
+
);
|
|
330
345
|
let accountForConfigRefresh: Account | null = localAccount ?? null;
|
|
331
346
|
|
|
332
347
|
const guardianCommitment = normalizeHexWord(state.commitment);
|
|
@@ -351,7 +366,10 @@ export class Multisig {
|
|
|
351
366
|
async verifyStateCommitment(): Promise<AccountStateVerificationResult> {
|
|
352
367
|
const accountId = AccountId.fromHex(this._accountId);
|
|
353
368
|
const webClient = await this.getRawClient();
|
|
354
|
-
const localAccount = await
|
|
369
|
+
const localAccount = await retryRpcRead(
|
|
370
|
+
() => webClient.getAccount(accountId),
|
|
371
|
+
this.rpcConfig,
|
|
372
|
+
);
|
|
355
373
|
|
|
356
374
|
if (!localAccount) {
|
|
357
375
|
throw new Error(
|
|
@@ -434,7 +452,10 @@ export class Multisig {
|
|
|
434
452
|
const rpcClient = new RpcClient(new Endpoint(this.getMidenRpcEndpoint()));
|
|
435
453
|
|
|
436
454
|
try {
|
|
437
|
-
const accountDetails = await
|
|
455
|
+
const accountDetails = await retryRpcRead(
|
|
456
|
+
() => rpcClient.getAccountDetails(accountId),
|
|
457
|
+
this.rpcConfig,
|
|
458
|
+
);
|
|
438
459
|
// If the account is not found or its commitment is zero, means that the account is not deployed yet
|
|
439
460
|
if (!accountDetails) {
|
|
440
461
|
return null;
|
|
@@ -1228,15 +1249,25 @@ export class Multisig {
|
|
|
1228
1249
|
...switchDelta,
|
|
1229
1250
|
deltaPayload: switchDelta.deltaPayload.txSummary,
|
|
1230
1251
|
});
|
|
1231
|
-
} catch {
|
|
1232
|
-
//
|
|
1252
|
+
} catch (error) {
|
|
1253
|
+
// Best-effort — see above — but the failure must be visible: a
|
|
1254
|
+
// silently lost push leaves the pre-switch GUARDIAN serving this
|
|
1255
|
+
// account (split-brain, issue #305) with nothing to diagnose by.
|
|
1256
|
+
console.warn(
|
|
1257
|
+
'SwitchGuardian delta push to the pre-switch GUARDIAN failed; it ' +
|
|
1258
|
+
'will keep serving this account until reconciliation',
|
|
1259
|
+
error,
|
|
1260
|
+
);
|
|
1233
1261
|
}
|
|
1234
1262
|
|
|
1235
1263
|
try {
|
|
1236
1264
|
const webClient = await this.getRawClient();
|
|
1237
|
-
await webClient.syncState();
|
|
1265
|
+
await retryRpcRead(() => webClient.syncState(), this.rpcConfig);
|
|
1238
1266
|
|
|
1239
|
-
const updatedAccount = await
|
|
1267
|
+
const updatedAccount = await retryRpcRead(
|
|
1268
|
+
() => webClient.getAccount(accountId),
|
|
1269
|
+
this.rpcConfig,
|
|
1270
|
+
);
|
|
1240
1271
|
if (!updatedAccount) {
|
|
1241
1272
|
throw new Error(
|
|
1242
1273
|
`Updated account ${this._accountId} is missing from local client`
|
|
@@ -50,4 +50,12 @@ describe('isTransientProverError', () => {
|
|
|
50
50
|
expect(isTransientProverError({ code: 14, message: 'unavailable' })).toBe(true);
|
|
51
51
|
expect(isTransientProverError({ code: 3, message: 'timeout text' })).toBe(false);
|
|
52
52
|
});
|
|
53
|
+
|
|
54
|
+
it('reads grpc code wording out of plain message text', () => {
|
|
55
|
+
expect(isTransientProverError(new Error('grpc code: NotFound'))).toBe(false);
|
|
56
|
+
expect(
|
|
57
|
+
isTransientProverError(new Error('grpc code: Internal; nested timeout')),
|
|
58
|
+
).toBe(false);
|
|
59
|
+
expect(isTransientProverError(new Error('grpc code: Unavailable'))).toBe(true);
|
|
60
|
+
});
|
|
53
61
|
});
|