@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
package/src/procedures.ts CHANGED
@@ -11,13 +11,12 @@
11
11
  * human-readable encoding and should not be copied into this table.
12
12
  */
13
13
  export const PROCEDURE_ROOTS = {
14
- update_signers: '0x34963b067dbba634e57b416bc2f2a9a8d4ac24147f40b2900148c9ba44774274',
15
- update_procedure_threshold: '0xec74c4b96ce593c11017ae54dec9c0ae5e0d242e8b3074eb3908d961300aed67',
16
- auth_tx: '0x0708020dce7b91b61116e3eb27e5d686e129a83df3c540e0a7693b4523814e72',
17
- update_guardian: '0xeceb1f2c2d7d20312dbaf091e9a27a2b63f9fcba120948043069793a5715bc96',
18
- verify_guardian: '0xe6a8a62d37117f55a79b5345aa3d263ab16e973d486bac9a1612663dfdecf82d',
19
- send_asset: '0xfb1c73d10de1954e9e8948964e3e77cf4e33759d2e012cb00eb10c50f2974eb4',
20
- receive_asset: '0x6170fd6d682d91777b551fd866258f43cc657f1291f8f071500f4e56e9c153da',
14
+ update_signers: '0xa261cfd3c8791ac5abe1e78e14eade2f20789d73ab1c23c430418de59bc3380e',
15
+ update_procedure_threshold: '0x97587c61d49313b1d5a3c8b7437e0080e67ed9bd9d3e7206bcae562f934ccd03',
16
+ auth_tx: '0xa6aa6f69d9358535272ba433cd48d20628a5c69598e00c6dd01a22e83a5f15df',
17
+ update_guardian: '0x0a614ff7c81a561cbd2a4c2d9482031a7a841ca5de33349daed23a9d871b3675',
18
+ send_asset: '0x595bc83258726a66bd904912cfd5186c07cbd902dfbc115b7d6bc8105efc57e3',
19
+ receive_asset: '0x34a56dd18f6fe5aab63198b9dcfc6467e793ebabb37d56b994b902504635da13',
21
20
  } as const;
22
21
 
23
22
  /**
@@ -157,3 +157,79 @@ describe('ProposalMetadataCodec p2id noteType (issue #322)', () => {
157
157
  expect(() => ProposalMetadataCodec.validate(md)).toThrow(/unsupported noteType/);
158
158
  });
159
159
  });
160
+
161
+ describe('ProposalMetadataCodec p2id P2IDE heights (issue #366)', () => {
162
+ const baseWire: GuardianProposalMetadata = {
163
+ proposalType: 'p2id',
164
+ recipientId: '0xrecipient',
165
+ faucetId: '0xfaucet',
166
+ amount: '1000',
167
+ };
168
+
169
+ it('round-trips reclaim and timelock heights through the codec', () => {
170
+ const md = ProposalMetadataCodec.fromGuardian({
171
+ ...baseWire,
172
+ reclaimHeight: 12345,
173
+ timelockHeight: 700,
174
+ }) as P2IdProposalMetadata;
175
+ expect(md.reclaimHeight).toBe(12345);
176
+ expect(md.timelockHeight).toBe(700);
177
+
178
+ const wire = ProposalMetadataCodec.toGuardian(md);
179
+ expect(wire.reclaimHeight).toBe(12345);
180
+ expect(wire.timelockHeight).toBe(700);
181
+ });
182
+
183
+ it('leaves heights absent for plain P2ID proposals (pre-#366 wire shape)', () => {
184
+ const md = ProposalMetadataCodec.fromGuardian(baseWire) as P2IdProposalMetadata;
185
+ expect(md.reclaimHeight).toBeUndefined();
186
+ expect(md.timelockHeight).toBeUndefined();
187
+
188
+ const wire = ProposalMetadataCodec.toGuardian(md);
189
+ expect(wire.reclaimHeight).toBeUndefined();
190
+ expect(wire.timelockHeight).toBeUndefined();
191
+ });
192
+
193
+ it('fromGuardian rejects a zero height (encodes "no constraint" on-chain)', () => {
194
+ expect(() =>
195
+ ProposalMetadataCodec.fromGuardian({ ...baseWire, reclaimHeight: 0 }),
196
+ ).toThrow(/unsupported reclaimHeight/);
197
+ });
198
+
199
+ it('fromGuardian rejects a non-integer or out-of-range height', () => {
200
+ expect(() =>
201
+ ProposalMetadataCodec.fromGuardian({ ...baseWire, timelockHeight: 1.5 }),
202
+ ).toThrow(/unsupported timelockHeight/);
203
+ expect(() =>
204
+ ProposalMetadataCodec.fromGuardian({ ...baseWire, reclaimHeight: 0x1_0000_0000 }),
205
+ ).toThrow(/unsupported reclaimHeight/);
206
+ });
207
+
208
+ it('validate rejects an invalid height', () => {
209
+ const md = {
210
+ proposalType: 'p2id',
211
+ description: '',
212
+ recipientId: '0xrecipient',
213
+ faucetId: '0xfaucet',
214
+ amount: '1000',
215
+ reclaimHeight: -1,
216
+ } as unknown as P2IdProposalMetadata;
217
+ expect(() => ProposalMetadataCodec.validate(md)).toThrow(/unsupported reclaimHeight/);
218
+ });
219
+
220
+ it('toGuardian rejects an invalid height instead of emitting it on the wire', () => {
221
+ const base = {
222
+ proposalType: 'p2id',
223
+ description: '',
224
+ recipientId: '0xrecipient',
225
+ faucetId: '0xfaucet',
226
+ amount: '1000',
227
+ };
228
+ expect(() =>
229
+ ProposalMetadataCodec.toGuardian({ ...base, reclaimHeight: 0 } as P2IdProposalMetadata),
230
+ ).toThrow(/unsupported reclaimHeight/);
231
+ expect(() =>
232
+ ProposalMetadataCodec.toGuardian({ ...base, timelockHeight: 1.5 } as P2IdProposalMetadata),
233
+ ).toThrow(/unsupported timelockHeight/);
234
+ });
235
+ });
@@ -1,7 +1,7 @@
1
1
  import type { ProposalMetadata as GuardianProposalMetadata } from '@openzeppelin/guardian-client';
2
2
  import type { ProposalMetadata } from '../types.js';
3
3
  import { isProcedureName } from '../procedures.js';
4
- import { isP2idNoteVisibility } from '../types/proposal.js';
4
+ import { isP2idNoteVisibility, parseP2ideHeight } from '../types/proposal.js';
5
5
 
6
6
  export class ProposalMetadataCodec {
7
7
  static toGuardian(metadata: ProposalMetadata): GuardianProposalMetadata {
@@ -10,6 +10,7 @@ export class ProposalMetadataCodec {
10
10
  description: metadata.description,
11
11
  salt: metadata.saltHex,
12
12
  requiredSignatures: metadata.requiredSignatures,
13
+ chainAnchor: metadata.chainAnchor,
13
14
  };
14
15
 
15
16
  switch (metadata.proposalType) {
@@ -30,6 +31,11 @@ export class ProposalMetadataCodec {
30
31
  // keeps the pre-#322 wire shape and matches the Rust encoder (which
31
32
  // round-trips through the NoteType enum). Absent => public.
32
33
  noteType: metadata.noteType === 'private' ? 'private' : undefined,
34
+ // Validated on encode too: the create path already rejects bad
35
+ // heights when building the note, but metadata handed directly to
36
+ // the codec must not reach the wire with a value cosigners reject.
37
+ reclaimHeight: parseP2ideHeight('reclaimHeight', metadata.reclaimHeight),
38
+ timelockHeight: parseP2ideHeight('timelockHeight', metadata.timelockHeight),
33
39
  };
34
40
  case 'switch_guardian':
35
41
  return {
@@ -68,6 +74,7 @@ export class ProposalMetadataCodec {
68
74
  description: guardian.description ?? '',
69
75
  saltHex: guardian.salt,
70
76
  requiredSignatures: guardian.requiredSignatures,
77
+ chainAnchor: guardian.chainAnchor,
71
78
  };
72
79
 
73
80
  switch (guardian.proposalType) {
@@ -87,6 +94,8 @@ export class ProposalMetadataCodec {
87
94
  faucetId: guardian.faucetId,
88
95
  amount: guardian.amount,
89
96
  noteType: guardian.noteType,
97
+ reclaimHeight: parseP2ideHeight('reclaimHeight', guardian.reclaimHeight),
98
+ timelockHeight: parseP2ideHeight('timelockHeight', guardian.timelockHeight),
90
99
  };
91
100
  case 'consume_notes':
92
101
  if (!guardian.noteIds || guardian.noteIds.length === 0) {
@@ -138,7 +147,7 @@ export class ProposalMetadataCodec {
138
147
  };
139
148
  default:
140
149
  // Any proposal type the SDK does not model collapses to the 'custom'
141
- // bucket while preserving the original label (issue #266).
150
+ // bucket while preserving the original label.
142
151
  return {
143
152
  ...base,
144
153
  proposalType: 'custom',
@@ -182,6 +191,8 @@ export class ProposalMetadataCodec {
182
191
  if (metadata.noteType !== undefined && !isP2idNoteVisibility(metadata.noteType)) {
183
192
  throw new Error(`p2id proposal has unsupported noteType '${metadata.noteType}'`);
184
193
  }
194
+ parseP2ideHeight('reclaimHeight', metadata.reclaimHeight);
195
+ parseP2ideHeight('timelockHeight', metadata.timelockHeight);
185
196
  return metadata;
186
197
  case 'custom':
187
198
  // Custom proposals are opaque to the SDK; nothing to validate beyond
@@ -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
  });
@@ -1,178 +1,10 @@
1
- type ErrorRecord = {
2
- cause?: unknown;
3
- code?: unknown;
4
- status?: unknown;
5
- statusCode?: unknown;
6
- message?: unknown;
7
- };
8
-
9
- type StructuredEvidence = 'transient' | 'permanent' | 'indeterminate';
10
-
11
- const TRANSIENT_GRPC = new Set([
12
- 'cancelled',
13
- 'canceled',
14
- 'deadlineexceeded',
15
- 'unavailable',
16
- 'resourceexhausted',
17
- ]);
18
-
19
- const PERMANENT_GRPC = new Set([
20
- 'invalidargument',
21
- 'failedprecondition',
22
- 'permissiondenied',
23
- 'unauthenticated',
24
- 'notfound',
25
- 'alreadyexists',
26
- 'outofrange',
27
- 'unimplemented',
28
- 'aborted',
29
- 'internal',
30
- 'dataloss',
31
- ]);
32
-
33
- const TRANSIENT_HTTP = new Set([408, 429, 502, 503, 504]);
34
- const NUMERIC_GRPC_CODES = new Map<number, string>([
35
- [0, 'ok'],
36
- [1, 'cancelled'],
37
- [2, 'unknown'],
38
- [3, 'invalidargument'],
39
- [4, 'deadlineexceeded'],
40
- [5, 'notfound'],
41
- [6, 'alreadyexists'],
42
- [7, 'permissiondenied'],
43
- [8, 'resourceexhausted'],
44
- [9, 'failedprecondition'],
45
- [10, 'aborted'],
46
- [11, 'outofrange'],
47
- [12, 'unimplemented'],
48
- [13, 'internal'],
49
- [14, 'unavailable'],
50
- [15, 'dataloss'],
51
- [16, 'unauthenticated'],
52
- ]);
53
-
54
- function normalizeCode(value: unknown): string | undefined {
55
- if (typeof value === 'number') {
56
- return NUMERIC_GRPC_CODES.get(value);
57
- }
58
- return typeof value === 'string' ? value.replaceAll(/[\s_-]/g, '').toLowerCase() : undefined;
59
- }
60
-
61
- function grpcEvidence(value: unknown): StructuredEvidence | undefined {
62
- const code = normalizeCode(value);
63
- if (code === undefined) {
64
- return undefined;
65
- }
66
- if (TRANSIENT_GRPC.has(code)) {
67
- return 'transient';
68
- }
69
- if (PERMANENT_GRPC.has(code)) {
70
- return 'permanent';
71
- }
72
- if (code === 'unknown' || code === 'ok') {
73
- return 'indeterminate';
74
- }
75
- return undefined;
76
- }
77
-
78
- function httpEvidence(value: unknown): StructuredEvidence | undefined {
79
- if (typeof value !== 'number' || !Number.isInteger(value) || value < 400 || value > 599) {
80
- return undefined;
81
- }
82
- return TRANSIENT_HTTP.has(value) ? 'transient' : 'permanent';
83
- }
84
-
85
- function httpMessageEvidence(message: string): StructuredEvidence | undefined {
86
- let hasTransient = false;
87
- for (const match of message.matchAll(
88
- /(?:\bhttp(?:\s+status)?|\bstatus\s*:?)\s*(\d{3})\b/g,
89
- )) {
90
- const evidence = httpEvidence(Number(match[1]));
91
- if (evidence === 'permanent') {
92
- return 'permanent';
93
- }
94
- hasTransient ||= evidence === 'transient';
95
- }
96
- return hasTransient ? 'transient' : undefined;
97
- }
98
-
99
- function flattenedTransient(message: string): boolean {
100
- return [
101
- 'cancelled',
102
- 'canceled',
103
- 'deadline exceeded',
104
- 'timeout',
105
- 'unavailable',
106
- 'resource exhausted',
107
- 'request timeout',
108
- 'too many requests',
109
- 'rate limited',
110
- 'rate limit',
111
- 'bad gateway',
112
- 'service unavailable',
113
- 'gateway timeout',
114
- 'i/o timeout',
115
- 'io timeout',
116
- 'connection reset',
117
- 'broken pipe',
118
- ].some((signal) => message.includes(signal));
119
- }
120
-
121
- function asRecord(value: unknown): ErrorRecord | undefined {
122
- return typeof value === 'object' && value !== null ? value as ErrorRecord : undefined;
123
- }
1
+ import { isTransientError } from '../retry/classify.js';
124
2
 
3
+ /**
4
+ * Prover-policy classification: the shared classifier with no transport-text
5
+ * extras — a bare "connection error" from a prover is treated as its
6
+ * considered answer.
7
+ */
125
8
  export function isTransientProverError(error: unknown): boolean {
126
- const seen = new Set<object>();
127
- const messages: string[] = [];
128
- let hasTransient = false;
129
- let hasPermanent = false;
130
- let current: unknown = error;
131
-
132
- while (current !== undefined && current !== null) {
133
- const record = asRecord(current);
134
- if (record !== undefined) {
135
- if (seen.has(record)) {
136
- break;
137
- }
138
- seen.add(record);
139
- for (const evidence of [
140
- grpcEvidence(record.code),
141
- httpEvidence(record.status),
142
- httpEvidence(record.statusCode),
143
- ]) {
144
- hasTransient ||= evidence === 'transient';
145
- hasPermanent ||= evidence === 'permanent';
146
- }
147
- if (typeof record.message === 'string') {
148
- const message = record.message.toLowerCase();
149
- messages.push(message);
150
- const evidence = httpMessageEvidence(message);
151
- hasTransient ||= evidence === 'transient';
152
- hasPermanent ||= evidence === 'permanent';
153
- } else {
154
- const message = String(current).toLowerCase();
155
- messages.push(message);
156
- const evidence = httpMessageEvidence(message);
157
- hasTransient ||= evidence === 'transient';
158
- hasPermanent ||= evidence === 'permanent';
159
- }
160
- current = record.cause;
161
- } else {
162
- const message = String(current).toLowerCase();
163
- messages.push(message);
164
- const evidence = httpMessageEvidence(message);
165
- hasTransient ||= evidence === 'transient';
166
- hasPermanent ||= evidence === 'permanent';
167
- break;
168
- }
169
- }
170
-
171
- if (hasPermanent) {
172
- return false;
173
- }
174
- if (hasTransient) {
175
- return true;
176
- }
177
- return messages.some(flattenedTransient);
9
+ return isTransientError(error);
178
10
  }
@@ -1,6 +1,6 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { describe, expect, it } from 'vitest';
3
- import { retryDelay } from './retry.js';
3
+ import { retryDelay } from '../retry/runtime.js';
4
4
 
5
5
  interface Fixtures {
6
6
  delays: Array<{ retryIndex: number; unitRandom: number; delayMs: number }>;
@@ -1,46 +1,21 @@
1
1
  import type { TransactionExecution, TransactionProof } from '@miden-sdk/miden-sdk';
2
2
  import type { ResolvedProverConfig } from './config.js';
3
3
  import { isTransientProverError } from './errors.js';
4
-
5
- const BASE_DELAY_MS = 500;
6
- const MAX_DELAY_MS = 8_000;
7
-
8
- export interface RetryRuntime {
9
- sleep(delayMs: number): Promise<void>;
10
- unitRandom(): number;
11
- }
12
-
13
- export const productionRetryRuntime: RetryRuntime = {
14
- sleep: (delayMs) => new Promise((resolve) => setTimeout(resolve, delayMs)),
15
- unitRandom: () => Math.random(),
16
- };
17
-
18
- export function retryDelay(retryIndex: number, unitRandom: number): number {
19
- const exponent = Math.min(retryIndex, 1023);
20
- const raw = BASE_DELAY_MS * 2 ** exponent;
21
- const finiteRandom = Number.isFinite(unitRandom) ? unitRandom : 0.5;
22
- const boundedRandom = Math.min(Math.max(finiteRandom, 0), 1 - Number.EPSILON);
23
- return Math.min(Math.floor(raw * (0.75 + boundedRandom * 0.5)), MAX_DELAY_MS);
24
- }
4
+ import type { RetryRuntime } from '../retry/runtime.js';
5
+ import { productionRetryRuntime, retryTransient } from '../retry/runtime.js';
25
6
 
26
7
  export async function proveWithRetry(
27
8
  execution: TransactionExecution,
28
9
  config: ResolvedProverConfig,
29
10
  runtime: RetryRuntime = productionRetryRuntime,
30
11
  ): Promise<TransactionProof> {
31
- for (let attempt = 0; attempt < config.maxAttempts; attempt += 1) {
32
- try {
12
+ return retryTransient(
13
+ async () => {
33
14
  const prover = config.createProver();
34
- return prover === undefined
35
- ? await execution.prove()
36
- : await execution.prove({ prover });
37
- } catch (error) {
38
- if (!isTransientProverError(error) || attempt + 1 >= config.maxAttempts) {
39
- throw error;
40
- }
41
- await runtime.sleep(retryDelay(attempt, runtime.unitRandom()));
42
- }
43
- }
44
-
45
- throw new Error('unreachable prover retry state');
15
+ return prover === undefined ? await execution.prove() : await execution.prove({ prover });
16
+ },
17
+ config.maxAttempts,
18
+ isTransientProverError,
19
+ runtime,
20
+ );
46
21
  }
@@ -1,18 +1,21 @@
1
1
  import type {
2
2
  AccountId,
3
+ ChainAnchor,
3
4
  MidenClient,
4
5
  TransactionProver,
5
6
  TransactionRequest,
6
7
  } from '@miden-sdk/miden-sdk';
7
8
  import { describe, expect, it, vi } from 'vitest';
8
9
  import type { ResolvedProverConfig } from './config.js';
9
- import type { RetryRuntime } from './retry.js';
10
+ import type { RetryRuntime } from '../retry/runtime.js';
10
11
  import { ProverWorkflow } from './workflow.js';
11
12
 
12
13
  function asType<T>(value: unknown): T {
13
14
  return value as T;
14
15
  }
15
16
 
17
+ const anchor = asType<ChainAnchor>({ marker: 'anchor' });
18
+
16
19
  describe('ProverWorkflow', () => {
17
20
  it('executes once, retries proof with fresh provers, submits once, and applies once', async () => {
18
21
  const transient = Object.assign(new Error('temporarily unavailable'), {
@@ -47,12 +50,14 @@ describe('ProverWorkflow', () => {
47
50
  runtime,
48
51
  );
49
52
 
50
- await workflow.submit(
53
+ await workflow.submitAt(
51
54
  asType<AccountId>({}),
52
55
  asType<TransactionRequest>({}),
56
+ anchor,
53
57
  );
54
58
 
55
59
  expect(executeRequest).toHaveBeenCalledTimes(1);
60
+ expect(executeRequest.mock.calls[0][2]).toEqual({ anchor });
56
61
  expect(prove).toHaveBeenCalledTimes(2);
57
62
  expect(provers).toHaveLength(2);
58
63
  expect(provers[0]).not.toBe(provers[1]);
@@ -83,12 +88,43 @@ describe('ProverWorkflow', () => {
83
88
  );
84
89
 
85
90
  await expect(
86
- workflow.submit(asType<AccountId>({}), asType<TransactionRequest>({})),
91
+ workflow.submitAt(asType<AccountId>({}), asType<TransactionRequest>({}), anchor),
87
92
  ).rejects.toBe(final);
88
93
  expect(prove).toHaveBeenCalledTimes(2);
89
94
  expect(runtime.sleep).toHaveBeenCalledTimes(1);
90
95
  });
91
96
 
97
+ it('never re-submits when submission fails with transient-looking wording', async () => {
98
+ const rateLimited = Object.assign(new Error('Too Many Requests!'), {
99
+ code: 'ResourceExhausted',
100
+ });
101
+ const submit = vi.fn().mockRejectedValue(rateLimited);
102
+ const prove = vi.fn().mockResolvedValue({ submit });
103
+ const client = {
104
+ transactions: { executeRequest: vi.fn().mockResolvedValue({ prove }) },
105
+ };
106
+ const runtime: RetryRuntime = {
107
+ sleep: vi.fn().mockResolvedValue(undefined),
108
+ unitRandom: () => 0.5,
109
+ };
110
+ const workflow = new ProverWorkflow(
111
+ asType<MidenClient>(client),
112
+ {
113
+ kind: 'remote',
114
+ maxAttempts: 5,
115
+ createProver: () => asType<TransactionProver>({}),
116
+ },
117
+ runtime,
118
+ );
119
+
120
+ await expect(
121
+ workflow.submitAt(asType<AccountId>({}), asType<TransactionRequest>({}), anchor),
122
+ ).rejects.toBe(rateLimited);
123
+ expect(prove).toHaveBeenCalledTimes(1);
124
+ expect(submit).toHaveBeenCalledTimes(1);
125
+ expect(runtime.sleep).not.toHaveBeenCalled();
126
+ });
127
+
92
128
  it('uses the injected prover directly when no cloneable remote override exists', async () => {
93
129
  const apply = vi.fn().mockResolvedValue({});
94
130
  const submit = vi.fn().mockResolvedValue({ apply });
@@ -102,7 +138,7 @@ describe('ProverWorkflow', () => {
102
138
  createProver: () => undefined,
103
139
  });
104
140
 
105
- await workflow.submit(asType<AccountId>({}), asType<TransactionRequest>({}));
141
+ await workflow.submitAt(asType<AccountId>({}), asType<TransactionRequest>({}), anchor);
106
142
 
107
143
  expect(prove).toHaveBeenCalledWith();
108
144
  });
@@ -1,6 +1,11 @@
1
- import type { AccountId, MidenClient, TransactionRequest } from '@miden-sdk/miden-sdk';
1
+ import type {
2
+ AccountId,
3
+ ChainAnchor,
4
+ MidenClient,
5
+ TransactionRequest,
6
+ } from '@miden-sdk/miden-sdk';
2
7
  import type { ResolvedProverConfig } from './config.js';
3
- import type { RetryRuntime } from './retry.js';
8
+ import type { RetryRuntime } from '../retry/runtime.js';
4
9
  import { proveWithRetry } from './retry.js';
5
10
 
6
11
  export class ProverWorkflow {
@@ -10,8 +15,15 @@ export class ProverWorkflow {
10
15
  private readonly runtime?: RetryRuntime,
11
16
  ) {}
12
17
 
13
- async submit(accountId: AccountId, request: TransactionRequest): Promise<void> {
14
- const execution = await this.client.transactions.executeRequest(accountId, request);
18
+ /** Proves, submits, and applies a request at its signed chain anchor. */
19
+ async submitAt(
20
+ accountId: AccountId,
21
+ request: TransactionRequest,
22
+ anchor: ChainAnchor,
23
+ ): Promise<void> {
24
+ const execution = await this.client.transactions.executeRequest(accountId, request, {
25
+ anchor,
26
+ });
15
27
  const proof = await proveWithRetry(execution, this.config, this.runtime);
16
28
  const submission = await proof.submit();
17
29
  await submission.apply();