@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.
Files changed (84) hide show
  1. package/README.md +48 -4
  2. package/dist/client.d.ts +4 -0
  3. package/dist/client.d.ts.map +1 -1
  4. package/dist/client.js +5 -2
  5. package/dist/client.js.map +1 -1
  6. package/dist/client.test.js +7 -2
  7. package/dist/client.test.js.map +1 -1
  8. package/dist/index.d.ts +1 -0
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js.map +1 -1
  11. package/dist/multisig.d.ts +3 -1
  12. package/dist/multisig.d.ts.map +1 -1
  13. package/dist/multisig.js +18 -9
  14. package/dist/multisig.js.map +1 -1
  15. package/dist/multisig.test.js +1 -0
  16. package/dist/multisig.test.js.map +1 -1
  17. package/dist/prover/errors.d.ts +5 -0
  18. package/dist/prover/errors.d.ts.map +1 -1
  19. package/dist/prover/errors.js +7 -155
  20. package/dist/prover/errors.js.map +1 -1
  21. package/dist/prover/errors.test.js +5 -0
  22. package/dist/prover/errors.test.js.map +1 -1
  23. package/dist/prover/retry.d.ts +1 -6
  24. package/dist/prover/retry.d.ts.map +1 -1
  25. package/dist/prover/retry.js +5 -28
  26. package/dist/prover/retry.js.map +1 -1
  27. package/dist/prover/retry.test.js +1 -1
  28. package/dist/prover/retry.test.js.map +1 -1
  29. package/dist/prover/workflow.d.ts +1 -1
  30. package/dist/prover/workflow.d.ts.map +1 -1
  31. package/dist/prover/workflow.test.js +23 -0
  32. package/dist/prover/workflow.test.js.map +1 -1
  33. package/dist/retry/classify.d.ts +16 -0
  34. package/dist/retry/classify.d.ts.map +1 -0
  35. package/dist/retry/classify.js +187 -0
  36. package/dist/retry/classify.js.map +1 -0
  37. package/dist/retry/runtime.d.ts +13 -0
  38. package/dist/retry/runtime.d.ts.map +1 -0
  39. package/dist/retry/runtime.js +33 -0
  40. package/dist/retry/runtime.js.map +1 -0
  41. package/dist/rpc/config.d.ts +11 -0
  42. package/dist/rpc/config.d.ts.map +1 -0
  43. package/dist/rpc/config.js +17 -0
  44. package/dist/rpc/config.js.map +1 -0
  45. package/dist/rpc/config.test.d.ts +2 -0
  46. package/dist/rpc/config.test.d.ts.map +1 -0
  47. package/dist/rpc/config.test.js +24 -0
  48. package/dist/rpc/config.test.js.map +1 -0
  49. package/dist/rpc/errors.d.ts +2 -0
  50. package/dist/rpc/errors.d.ts.map +1 -0
  51. package/dist/rpc/errors.js +11 -0
  52. package/dist/rpc/errors.js.map +1 -0
  53. package/dist/rpc/errors.test.d.ts +2 -0
  54. package/dist/rpc/errors.test.d.ts.map +1 -0
  55. package/dist/rpc/errors.test.js +34 -0
  56. package/dist/rpc/errors.test.js.map +1 -0
  57. package/dist/rpc/retry.d.ts +4 -0
  58. package/dist/rpc/retry.d.ts.map +1 -0
  59. package/dist/rpc/retry.js +6 -0
  60. package/dist/rpc/retry.js.map +1 -0
  61. package/dist/rpc/retry.test.d.ts +2 -0
  62. package/dist/rpc/retry.test.d.ts.map +1 -0
  63. package/dist/rpc/retry.test.js +98 -0
  64. package/dist/rpc/retry.test.js.map +1 -0
  65. package/package.json +2 -2
  66. package/src/client.test.ts +8 -2
  67. package/src/client.ts +11 -0
  68. package/src/index.ts +1 -0
  69. package/src/multisig.test.ts +1 -0
  70. package/src/multisig.ts +39 -8
  71. package/src/prover/errors.test.ts +8 -0
  72. package/src/prover/errors.ts +7 -175
  73. package/src/prover/retry.test.ts +1 -1
  74. package/src/prover/retry.ts +10 -35
  75. package/src/prover/workflow.test.ts +32 -1
  76. package/src/prover/workflow.ts +1 -1
  77. package/src/retry/classify.ts +220 -0
  78. package/src/retry/runtime.ts +45 -0
  79. package/src/rpc/config.test.ts +45 -0
  80. package/src/rpc/config.ts +30 -0
  81. package/src/rpc/errors.test.ts +69 -0
  82. package/src/rpc/errors.ts +12 -0
  83. package/src/rpc/retry.test.ts +144 -0
  84. 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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=config.test.d.ts.map
@@ -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,2 @@
1
+ export declare function isTransientRpcError(error: unknown): boolean;
2
+ //# sourceMappingURL=errors.d.ts.map
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=errors.test.d.ts.map
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=retry.test.d.ts.map
@@ -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.1",
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.1"
42
+ "@openzeppelin/guardian-client": "^0.16.2"
43
43
  },
44
44
  "devDependencies": {
45
45
  "typescript": "^5.7.2",
@@ -239,12 +239,18 @@ describe('MultisigClient', () => {
239
239
  ok: false,
240
240
  status: 404,
241
241
  statusText: 'Not Found',
242
- text: async () => 'Account not found',
242
+ headers: new Headers(),
243
+ text: async () =>
244
+ JSON.stringify({
245
+ code: 'GUARDIAN_ACCOUNT_NOT_FOUND',
246
+ message: 'Account not found',
247
+ meta: { retryable: false },
248
+ }),
243
249
  });
244
250
 
245
251
  await expect(
246
252
  client.load('0xnonexistent', mockSigner)
247
- ).rejects.toThrow();
253
+ ).rejects.toThrow('Account not found');
248
254
  });
249
255
 
250
256
  it('should allow registerOnGuardian after load without explicit initial state', async () => {
package/src/client.ts CHANGED
@@ -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';
@@ -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
- return (await webClient.getAccount(AccountId.fromHex(this._accountId))) ?? this.account;
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 webClient.getAccount(accountId);
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 webClient.getAccount(accountId);
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 rpcClient.getAccountDetails(accountId);
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
- // best-effort; see above
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 webClient.getAccount(accountId);
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
  });