@openzeppelin/miden-multisig-client 0.16.1 → 0.17.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (207) hide show
  1. package/README.md +192 -11
  2. package/dist/account/builder.d.ts.map +1 -1
  3. package/dist/account/builder.js +32 -25
  4. package/dist/account/builder.js.map +1 -1
  5. package/dist/account/builder.test.js +73 -21
  6. package/dist/account/builder.test.js.map +1 -1
  7. package/dist/account/layout.d.ts +31 -0
  8. package/dist/account/layout.d.ts.map +1 -0
  9. package/dist/account/layout.js +31 -0
  10. package/dist/account/layout.js.map +1 -0
  11. package/dist/account/masm/account-components/auth.d.ts +1 -4
  12. package/dist/account/masm/account-components/auth.d.ts.map +1 -1
  13. package/dist/account/masm/account-components/auth.js +36 -53
  14. package/dist/account/masm/account-components/auth.js.map +1 -1
  15. package/dist/account/masm/index.d.ts +0 -1
  16. package/dist/account/masm/index.d.ts.map +1 -1
  17. package/dist/account/masm/index.js +0 -1
  18. package/dist/account/masm/index.js.map +1 -1
  19. package/dist/account/storage.d.ts +4 -0
  20. package/dist/account/storage.d.ts.map +1 -1
  21. package/dist/account/storage.js +9 -20
  22. package/dist/account/storage.js.map +1 -1
  23. package/dist/client.d.ts +4 -0
  24. package/dist/client.d.ts.map +1 -1
  25. package/dist/client.js +10 -5
  26. package/dist/client.js.map +1 -1
  27. package/dist/client.test.js +74 -16
  28. package/dist/client.test.js.map +1 -1
  29. package/dist/index.d.ts +5 -3
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +3 -3
  32. package/dist/index.js.map +1 -1
  33. package/dist/inspector.d.ts +59 -1
  34. package/dist/inspector.d.ts.map +1 -1
  35. package/dist/inspector.js +157 -29
  36. package/dist/inspector.js.map +1 -1
  37. package/dist/inspector.test.js +246 -37
  38. package/dist/inspector.test.js.map +1 -1
  39. package/dist/multisig.d.ts +109 -33
  40. package/dist/multisig.d.ts.map +1 -1
  41. package/dist/multisig.js +307 -104
  42. package/dist/multisig.js.map +1 -1
  43. package/dist/multisig.test.js +582 -58
  44. package/dist/multisig.test.js.map +1 -1
  45. package/dist/procedures.d.ts +6 -7
  46. package/dist/procedures.d.ts.map +1 -1
  47. package/dist/procedures.js +6 -7
  48. package/dist/procedures.js.map +1 -1
  49. package/dist/proposal/metadata.d.ts.map +1 -1
  50. package/dist/proposal/metadata.js +13 -2
  51. package/dist/proposal/metadata.js.map +1 -1
  52. package/dist/proposal/metadata.test.js +57 -0
  53. package/dist/proposal/metadata.test.js.map +1 -1
  54. package/dist/prover/errors.d.ts +5 -0
  55. package/dist/prover/errors.d.ts.map +1 -1
  56. package/dist/prover/errors.js +7 -155
  57. package/dist/prover/errors.js.map +1 -1
  58. package/dist/prover/errors.test.js +5 -0
  59. package/dist/prover/errors.test.js.map +1 -1
  60. package/dist/prover/retry.d.ts +1 -6
  61. package/dist/prover/retry.d.ts.map +1 -1
  62. package/dist/prover/retry.js +5 -28
  63. package/dist/prover/retry.js.map +1 -1
  64. package/dist/prover/retry.test.js +1 -1
  65. package/dist/prover/retry.test.js.map +1 -1
  66. package/dist/prover/workflow.d.ts +4 -3
  67. package/dist/prover/workflow.d.ts.map +1 -1
  68. package/dist/prover/workflow.js +5 -2
  69. package/dist/prover/workflow.js.map +1 -1
  70. package/dist/prover/workflow.test.js +28 -3
  71. package/dist/prover/workflow.test.js.map +1 -1
  72. package/dist/retry/classify.d.ts +16 -0
  73. package/dist/retry/classify.d.ts.map +1 -0
  74. package/dist/retry/classify.js +187 -0
  75. package/dist/retry/classify.js.map +1 -0
  76. package/dist/retry/runtime.d.ts +13 -0
  77. package/dist/retry/runtime.d.ts.map +1 -0
  78. package/dist/retry/runtime.js +33 -0
  79. package/dist/retry/runtime.js.map +1 -0
  80. package/dist/rpc/config.d.ts +11 -0
  81. package/dist/rpc/config.d.ts.map +1 -0
  82. package/dist/rpc/config.js +17 -0
  83. package/dist/rpc/config.js.map +1 -0
  84. package/dist/rpc/config.test.d.ts +2 -0
  85. package/dist/rpc/config.test.d.ts.map +1 -0
  86. package/dist/rpc/config.test.js +24 -0
  87. package/dist/rpc/config.test.js.map +1 -0
  88. package/dist/rpc/errors.d.ts +2 -0
  89. package/dist/rpc/errors.d.ts.map +1 -0
  90. package/dist/rpc/errors.js +11 -0
  91. package/dist/rpc/errors.js.map +1 -0
  92. package/dist/rpc/errors.test.d.ts +2 -0
  93. package/dist/rpc/errors.test.d.ts.map +1 -0
  94. package/dist/rpc/errors.test.js +34 -0
  95. package/dist/rpc/errors.test.js.map +1 -0
  96. package/dist/rpc/retry.d.ts +4 -0
  97. package/dist/rpc/retry.d.ts.map +1 -0
  98. package/dist/rpc/retry.js +6 -0
  99. package/dist/rpc/retry.js.map +1 -0
  100. package/dist/rpc/retry.test.d.ts +2 -0
  101. package/dist/rpc/retry.test.d.ts.map +1 -0
  102. package/dist/rpc/retry.test.js +98 -0
  103. package/dist/rpc/retry.test.js.map +1 -0
  104. package/dist/transaction/index.d.ts +1 -1
  105. package/dist/transaction/index.d.ts.map +1 -1
  106. package/dist/transaction/index.js +1 -1
  107. package/dist/transaction/index.js.map +1 -1
  108. package/dist/transaction/p2id.d.ts +17 -6
  109. package/dist/transaction/p2id.d.ts.map +1 -1
  110. package/dist/transaction/p2id.js +22 -31
  111. package/dist/transaction/p2id.js.map +1 -1
  112. package/dist/transaction/p2id.test.js +62 -24
  113. package/dist/transaction/p2id.test.js.map +1 -1
  114. package/dist/transaction/summary.d.ts +45 -2
  115. package/dist/transaction/summary.d.ts.map +1 -1
  116. package/dist/transaction/summary.js +42 -2
  117. package/dist/transaction/summary.js.map +1 -1
  118. package/dist/transaction/summary.test.d.ts +2 -0
  119. package/dist/transaction/summary.test.d.ts.map +1 -0
  120. package/dist/transaction/summary.test.js +26 -0
  121. package/dist/transaction/summary.test.js.map +1 -0
  122. package/dist/transaction/updateGuardian.d.ts.map +1 -1
  123. package/dist/transaction/updateGuardian.js +16 -18
  124. package/dist/transaction/updateGuardian.js.map +1 -1
  125. package/dist/transaction/updateProcedureThreshold.d.ts.map +1 -1
  126. package/dist/transaction/updateProcedureThreshold.js +15 -17
  127. package/dist/transaction/updateProcedureThreshold.js.map +1 -1
  128. package/dist/transaction/updateSigners.d.ts +6 -1
  129. package/dist/transaction/updateSigners.d.ts.map +1 -1
  130. package/dist/transaction/updateSigners.js +20 -17
  131. package/dist/transaction/updateSigners.js.map +1 -1
  132. package/dist/transaction.d.ts +2 -2
  133. package/dist/transaction.d.ts.map +1 -1
  134. package/dist/transaction.js +1 -1
  135. package/dist/transaction.js.map +1 -1
  136. package/dist/types/proposal.d.ts +28 -4
  137. package/dist/types/proposal.d.ts.map +1 -1
  138. package/dist/types/proposal.js +18 -0
  139. package/dist/types/proposal.js.map +1 -1
  140. package/dist/types.d.ts +0 -1
  141. package/dist/types.d.ts.map +1 -1
  142. package/dist/utils/signature.d.ts +11 -7
  143. package/dist/utils/signature.d.ts.map +1 -1
  144. package/dist/utils/signature.js +24 -58
  145. package/dist/utils/signature.js.map +1 -1
  146. package/dist/utils/word.d.ts +7 -0
  147. package/dist/utils/word.d.ts.map +1 -1
  148. package/dist/utils/word.js +15 -0
  149. package/dist/utils/word.js.map +1 -1
  150. package/masm/account_components/auth/guarded_multisig.masm +42 -0
  151. package/package.json +7 -4
  152. package/src/account/builder.test.ts +111 -45
  153. package/src/account/builder.ts +45 -33
  154. package/src/account/layout.ts +33 -0
  155. package/src/account/masm/account-components/auth.ts +36 -56
  156. package/src/account/masm/index.ts +0 -1
  157. package/src/account/storage.ts +9 -22
  158. package/src/client.test.ts +87 -16
  159. package/src/client.ts +16 -3
  160. package/src/index.ts +27 -1
  161. package/src/inspector.test.ts +330 -38
  162. package/src/inspector.ts +196 -33
  163. package/src/multisig.test.ts +680 -63
  164. package/src/multisig.ts +400 -115
  165. package/src/procedures.ts +6 -7
  166. package/src/proposal/metadata.test.ts +76 -0
  167. package/src/proposal/metadata.ts +13 -2
  168. package/src/prover/errors.test.ts +8 -0
  169. package/src/prover/errors.ts +7 -175
  170. package/src/prover/retry.test.ts +1 -1
  171. package/src/prover/retry.ts +10 -35
  172. package/src/prover/workflow.test.ts +40 -4
  173. package/src/prover/workflow.ts +16 -4
  174. package/src/retry/classify.ts +220 -0
  175. package/src/retry/runtime.ts +45 -0
  176. package/src/rpc/config.test.ts +45 -0
  177. package/src/rpc/config.ts +30 -0
  178. package/src/rpc/errors.test.ts +69 -0
  179. package/src/rpc/errors.ts +12 -0
  180. package/src/rpc/retry.test.ts +144 -0
  181. package/src/rpc/retry.ts +12 -0
  182. package/src/transaction/index.ts +7 -1
  183. package/src/transaction/p2id.test.ts +112 -31
  184. package/src/transaction/p2id.ts +39 -38
  185. package/src/transaction/summary.test.ts +32 -0
  186. package/src/transaction/summary.ts +83 -4
  187. package/src/transaction/updateGuardian.ts +15 -25
  188. package/src/transaction/updateProcedureThreshold.ts +13 -29
  189. package/src/transaction/updateSigners.ts +38 -30
  190. package/src/transaction.ts +8 -1
  191. package/src/types/proposal.ts +43 -4
  192. package/src/types.ts +0 -1
  193. package/src/utils/signature.ts +32 -65
  194. package/src/utils/word.ts +17 -0
  195. package/dist/account/masm/auth.d.ts +0 -5
  196. package/dist/account/masm/auth.d.ts.map +0 -1
  197. package/dist/account/masm/auth.js +0 -1509
  198. package/dist/account/masm/auth.js.map +0 -1
  199. package/masm/account_components/auth/multisig.masm +0 -12
  200. package/masm/account_components/auth/multisig_ecdsa.masm +0 -12
  201. package/masm/account_components/auth/multisig_guardian.masm +0 -16
  202. package/masm/account_components/auth/multisig_guardian_ecdsa.masm +0 -16
  203. package/masm/auth/guardian.masm +0 -199
  204. package/masm/auth/guardian_ecdsa.masm +0 -195
  205. package/masm/auth/multisig.masm +0 -554
  206. package/masm/auth/multisig_ecdsa.masm +0 -554
  207. package/src/account/masm/auth.ts +0 -1512
@@ -0,0 +1,220 @@
1
+ /**
2
+ * Transient/permanent error classification shared by the prover and node-RPC
3
+ * retry policies, mirroring `is_transient_error_with` in the Rust
4
+ * `guardian-shared` crate: the whole cause chain is walked accumulating
5
+ * evidence, permanent evidence anywhere vetoes transient anywhere, and the
6
+ * transient-wording fallback fires only when no status-level evidence exists
7
+ * in any link. Behavior is pinned by the cross-SDK fixture corpus.
8
+ */
9
+
10
+ type ErrorRecord = {
11
+ cause?: unknown;
12
+ code?: unknown;
13
+ status?: unknown;
14
+ statusCode?: unknown;
15
+ message?: unknown;
16
+ };
17
+
18
+ type StructuredEvidence = 'transient' | 'permanent' | 'indeterminate';
19
+
20
+ const TRANSIENT_GRPC = new Set([
21
+ 'cancelled',
22
+ 'canceled',
23
+ 'deadlineexceeded',
24
+ 'unavailable',
25
+ 'resourceexhausted',
26
+ ]);
27
+
28
+ const PERMANENT_GRPC = new Set([
29
+ 'invalidargument',
30
+ 'failedprecondition',
31
+ 'permissiondenied',
32
+ 'unauthenticated',
33
+ 'notfound',
34
+ 'alreadyexists',
35
+ 'outofrange',
36
+ 'unimplemented',
37
+ 'aborted',
38
+ 'internal',
39
+ 'dataloss',
40
+ ]);
41
+
42
+ const TRANSIENT_HTTP = new Set([408, 429, 502, 503, 504]);
43
+ const NUMERIC_GRPC_CODES = new Map<number, string>([
44
+ [0, 'ok'],
45
+ [1, 'cancelled'],
46
+ [2, 'unknown'],
47
+ [3, 'invalidargument'],
48
+ [4, 'deadlineexceeded'],
49
+ [5, 'notfound'],
50
+ [6, 'alreadyexists'],
51
+ [7, 'permissiondenied'],
52
+ [8, 'resourceexhausted'],
53
+ [9, 'failedprecondition'],
54
+ [10, 'aborted'],
55
+ [11, 'outofrange'],
56
+ [12, 'unimplemented'],
57
+ [13, 'internal'],
58
+ [14, 'unavailable'],
59
+ [15, 'dataloss'],
60
+ [16, 'unauthenticated'],
61
+ ]);
62
+
63
+ const GRPC_MESSAGE_TOKENS: ReadonlyArray<{ token: string; evidence: StructuredEvidence }> = [
64
+ ...[...TRANSIENT_GRPC].map((code) => ({ token: `grpccode${code}`, evidence: 'transient' as const })),
65
+ ...[...PERMANENT_GRPC].map((code) => ({ token: `grpccode${code}`, evidence: 'permanent' as const })),
66
+ ];
67
+
68
+ function normalizeCode(value: unknown): string | undefined {
69
+ if (typeof value === 'number') {
70
+ return NUMERIC_GRPC_CODES.get(value);
71
+ }
72
+ return typeof value === 'string' ? value.replaceAll(/[\s_-]/g, '').toLowerCase() : undefined;
73
+ }
74
+
75
+ function grpcEvidence(value: unknown): StructuredEvidence | undefined {
76
+ const code = normalizeCode(value);
77
+ if (code === undefined) {
78
+ return undefined;
79
+ }
80
+ if (TRANSIENT_GRPC.has(code)) {
81
+ return 'transient';
82
+ }
83
+ if (PERMANENT_GRPC.has(code)) {
84
+ return 'permanent';
85
+ }
86
+ if (code === 'unknown' || code === 'ok') {
87
+ return 'indeterminate';
88
+ }
89
+ return undefined;
90
+ }
91
+
92
+ function httpEvidence(value: unknown): StructuredEvidence | undefined {
93
+ if (typeof value !== 'number' || !Number.isInteger(value) || value < 400 || value > 599) {
94
+ return undefined;
95
+ }
96
+ return TRANSIENT_HTTP.has(value) ? 'transient' : 'permanent';
97
+ }
98
+
99
+ function httpMessageEvidence(message: string): StructuredEvidence | undefined {
100
+ let hasTransient = false;
101
+ for (const match of message.matchAll(
102
+ /(?:\bhttp(?:\s+status)?|\bstatus)(?:\s+code)?\s*:?\s*(\d{3})\b/g,
103
+ )) {
104
+ const evidence = httpEvidence(Number(match[1]));
105
+ if (evidence === 'permanent') {
106
+ return 'permanent';
107
+ }
108
+ hasTransient ||= evidence === 'transient';
109
+ }
110
+ return hasTransient ? 'transient' : undefined;
111
+ }
112
+
113
+ function grpcMessageEvidence(message: string): StructuredEvidence | undefined {
114
+ const normalized = message.replaceAll(/[^a-z0-9]/g, '');
115
+ let hasTransient = false;
116
+ for (const { token, evidence } of GRPC_MESSAGE_TOKENS) {
117
+ if (normalized.includes(token)) {
118
+ if (evidence === 'permanent') {
119
+ return 'permanent';
120
+ }
121
+ hasTransient ||= evidence === 'transient';
122
+ }
123
+ }
124
+ return hasTransient ? 'transient' : undefined;
125
+ }
126
+
127
+ /**
128
+ * Last-resort transient wording, consulted only when the whole chain carried
129
+ * no status-level evidence. Guarded by the negative classification fixtures
130
+ * (server rejections must never match) — extend with care.
131
+ */
132
+ function flattenedTransient(message: string): boolean {
133
+ return [
134
+ 'cancelled',
135
+ 'canceled',
136
+ 'deadline exceeded',
137
+ 'timeout',
138
+ 'unavailable',
139
+ 'resource exhausted',
140
+ 'request timeout',
141
+ 'too many requests',
142
+ 'rate limited',
143
+ 'rate limit',
144
+ 'bad gateway',
145
+ 'service unavailable',
146
+ 'gateway timeout',
147
+ 'i/o timeout',
148
+ 'io timeout',
149
+ 'connection reset',
150
+ 'broken pipe',
151
+ ].some((signal) => message.includes(signal));
152
+ }
153
+
154
+ function asRecord(value: unknown): ErrorRecord | undefined {
155
+ return typeof value === 'object' && value !== null ? value as ErrorRecord : undefined;
156
+ }
157
+
158
+ /**
159
+ * Walks the error's cause chain and reports whether every piece of evidence
160
+ * points at a transient failure. `extraTransientSignals` extends the
161
+ * transient-wording fallback for one policy without leaking into others,
162
+ * under the same permanent-wins rule.
163
+ */
164
+ export function isTransientError(
165
+ error: unknown,
166
+ extraTransientSignals: readonly string[] = [],
167
+ ): boolean {
168
+ const seen = new Set<object>();
169
+ const messages: string[] = [];
170
+ let hasTransient = false;
171
+ let hasPermanent = false;
172
+ let current: unknown = error;
173
+
174
+ const recordMessage = (message: string): void => {
175
+ messages.push(message);
176
+ for (const evidence of [httpMessageEvidence(message), grpcMessageEvidence(message)]) {
177
+ hasTransient ||= evidence === 'transient';
178
+ hasPermanent ||= evidence === 'permanent';
179
+ }
180
+ };
181
+
182
+ while (current !== undefined && current !== null) {
183
+ const record = asRecord(current);
184
+ if (record !== undefined) {
185
+ if (seen.has(record)) {
186
+ break;
187
+ }
188
+ seen.add(record);
189
+ for (const evidence of [
190
+ grpcEvidence(record.code),
191
+ httpEvidence(record.status),
192
+ httpEvidence(record.statusCode),
193
+ ]) {
194
+ hasTransient ||= evidence === 'transient';
195
+ hasPermanent ||= evidence === 'permanent';
196
+ }
197
+ recordMessage(
198
+ typeof record.message === 'string'
199
+ ? record.message.toLowerCase()
200
+ : String(current).toLowerCase(),
201
+ );
202
+ current = record.cause;
203
+ } else {
204
+ recordMessage(String(current).toLowerCase());
205
+ break;
206
+ }
207
+ }
208
+
209
+ if (hasPermanent) {
210
+ return false;
211
+ }
212
+ if (hasTransient) {
213
+ return true;
214
+ }
215
+ return messages.some(
216
+ (message) =>
217
+ flattenedTransient(message) ||
218
+ extraTransientSignals.some((signal) => message.includes(signal)),
219
+ );
220
+ }
@@ -0,0 +1,45 @@
1
+ const BASE_DELAY_MS = 500;
2
+ const MAX_DELAY_MS = 8_000;
3
+
4
+ export interface RetryRuntime {
5
+ sleep(delayMs: number): Promise<void>;
6
+ unitRandom(): number;
7
+ }
8
+
9
+ export const productionRetryRuntime: RetryRuntime = {
10
+ sleep: (delayMs) => new Promise((resolve) => setTimeout(resolve, delayMs)),
11
+ unitRandom: () => Math.random(),
12
+ };
13
+
14
+ export function retryDelay(retryIndex: number, unitRandom: number): number {
15
+ const exponent = Math.min(retryIndex, 1023);
16
+ const raw = BASE_DELAY_MS * 2 ** exponent;
17
+ const finiteRandom = Number.isFinite(unitRandom) ? unitRandom : 0.5;
18
+ const boundedRandom = Math.min(Math.max(finiteRandom, 0), 1 - Number.EPSILON);
19
+ return Math.min(Math.floor(raw * (0.75 + boundedRandom * 0.5)), MAX_DELAY_MS);
20
+ }
21
+
22
+ /**
23
+ * Runs `operation` under an attempt budget: the budget is consulted before
24
+ * the error is classified, transient failures back off with `retryDelay`,
25
+ * and permanent failures or the final attempt rethrow the error unchanged.
26
+ */
27
+ export async function retryTransient<T>(
28
+ operation: () => Promise<T>,
29
+ maxAttempts: number,
30
+ isTransient: (error: unknown) => boolean,
31
+ runtime: RetryRuntime,
32
+ ): Promise<T> {
33
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
34
+ try {
35
+ return await operation();
36
+ } catch (error) {
37
+ if (attempt + 1 >= maxAttempts || !isTransient(error)) {
38
+ throw error;
39
+ }
40
+ await runtime.sleep(retryDelay(attempt, runtime.unitRandom()));
41
+ }
42
+ }
43
+
44
+ throw new Error('unreachable retry state');
45
+ }
@@ -0,0 +1,45 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { describe, expect, it } from 'vitest';
3
+ import { resolveRpcConfig } from './config.js';
4
+
5
+ interface Fixtures {
6
+ attemptBudgets: Array<{ input: number | null; normalized: number }>;
7
+ }
8
+
9
+ function fixtures(): Fixtures {
10
+ return JSON.parse(
11
+ readFileSync(
12
+ new URL(
13
+ '../../../../fixtures/miden-multisig-client/rpc-policy-fixtures.json',
14
+ import.meta.url,
15
+ ),
16
+ 'utf8',
17
+ ),
18
+ ) as Fixtures;
19
+ }
20
+
21
+ describe('resolveRpcConfig', () => {
22
+ it('matches shared attempt-budget vectors', () => {
23
+ for (const fixture of fixtures().attemptBudgets) {
24
+ const retry = fixture.input === null ? undefined : { maxAttempts: fixture.input };
25
+ expect(resolveRpcConfig({ retry }).maxAttempts).toBe(fixture.normalized);
26
+ }
27
+ });
28
+
29
+ it('defaults to one retry when no config is given', () => {
30
+ expect(resolveRpcConfig(undefined).maxAttempts).toBe(2);
31
+ });
32
+
33
+ it('treats an explicit single attempt as a full opt-out', () => {
34
+ expect(resolveRpcConfig({ retry: { maxAttempts: 1 } }).maxAttempts).toBe(1);
35
+ });
36
+
37
+ it.each([-1, 1.5, Number.NaN, Number.POSITIVE_INFINITY, 4_294_967_296])(
38
+ 'rejects invalid maxAttempts value %s',
39
+ (maxAttempts) => {
40
+ expect(() => resolveRpcConfig({ retry: { maxAttempts } })).toThrow(
41
+ 'rpc.retry.maxAttempts',
42
+ );
43
+ },
44
+ );
45
+ });
@@ -0,0 +1,30 @@
1
+ export interface RpcRetryPolicy {
2
+ maxAttempts?: number;
3
+ }
4
+
5
+ export interface RpcConfig {
6
+ retry?: RpcRetryPolicy;
7
+ }
8
+
9
+ export interface ResolvedRpcConfig {
10
+ readonly maxAttempts: number;
11
+ }
12
+
13
+ const DEFAULT_MAX_ATTEMPTS = 2;
14
+ const MAX_U32 = 4_294_967_295;
15
+
16
+ function normalizeMaxAttempts(value: number | undefined): number {
17
+ if (value === undefined) {
18
+ return DEFAULT_MAX_ATTEMPTS;
19
+ }
20
+ if (!Number.isFinite(value) || !Number.isInteger(value) || value < 0 || value > MAX_U32) {
21
+ throw new Error('rpc.retry.maxAttempts must be an integer between 0 and 4294967295');
22
+ }
23
+ return Math.max(1, value);
24
+ }
25
+
26
+ export function resolveRpcConfig(config: RpcConfig | undefined): ResolvedRpcConfig {
27
+ return {
28
+ maxAttempts: normalizeMaxAttempts(config?.retry?.maxAttempts),
29
+ };
30
+ }
@@ -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
+ });
@@ -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
+ }
@@ -1,4 +1,10 @@
1
- export { executeForSummary } from './summary.js';
1
+ export {
2
+ chainAnchorFromBase64,
3
+ chainAnchorToBase64,
4
+ executeForSummary,
5
+ executeForSummaryAt,
6
+ summarySalt,
7
+ } from './summary.js';
2
8
  export { buildUpdateSignersTransactionRequest } from './updateSigners.js';
3
9
  export { buildUpdateProcedureThresholdTransactionRequest } from './updateProcedureThreshold.js';
4
10
  export { buildUpdateGuardianTransactionRequest } from './updateGuardian.js';