@openzeppelin/miden-multisig-client 0.17.0-rc.2 → 0.17.0

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 (278) hide show
  1. package/README.md +172 -47
  2. package/dist/account/builder.d.ts +2 -2
  3. package/dist/account/builder.d.ts.map +1 -1
  4. package/dist/account/builder.js +49 -24
  5. package/dist/account/builder.js.map +1 -1
  6. package/dist/connectivity.d.ts +2 -0
  7. package/dist/connectivity.d.ts.map +1 -1
  8. package/dist/connectivity.js +7 -4
  9. package/dist/connectivity.js.map +1 -1
  10. package/dist/index.d.ts +8 -1
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +7 -1
  13. package/dist/index.js.map +1 -1
  14. package/dist/multisig/authArgErrors.d.ts +59 -0
  15. package/dist/multisig/authArgErrors.d.ts.map +1 -0
  16. package/dist/multisig/authArgErrors.js +115 -0
  17. package/dist/multisig/authArgErrors.js.map +1 -0
  18. package/dist/multisig/helpers.d.ts +5 -0
  19. package/dist/multisig/helpers.d.ts.map +1 -1
  20. package/dist/multisig/helpers.js +7 -2
  21. package/dist/multisig/helpers.js.map +1 -1
  22. package/dist/multisig.d.ts +140 -0
  23. package/dist/multisig.d.ts.map +1 -1
  24. package/dist/multisig.js +419 -12
  25. package/dist/multisig.js.map +1 -1
  26. package/dist/procedures.d.ts +1 -1
  27. package/dist/procedures.js +1 -1
  28. package/dist/recovery/proposalNoteImport.d.ts +173 -0
  29. package/dist/recovery/proposalNoteImport.d.ts.map +1 -0
  30. package/dist/recovery/proposalNoteImport.js +411 -0
  31. package/dist/recovery/proposalNoteImport.js.map +1 -0
  32. package/dist/recovery/publicNoteBackfill.d.ts +168 -0
  33. package/dist/recovery/publicNoteBackfill.d.ts.map +1 -0
  34. package/dist/recovery/publicNoteBackfill.js +368 -0
  35. package/dist/recovery/publicNoteBackfill.js.map +1 -0
  36. package/dist/recovery/recoverNotes.d.ts +147 -0
  37. package/dist/recovery/recoverNotes.d.ts.map +1 -0
  38. package/dist/recovery/recoverNotes.js +163 -0
  39. package/dist/recovery/recoverNotes.js.map +1 -0
  40. package/dist/recovery/transportDrain.d.ts +124 -0
  41. package/dist/recovery/transportDrain.d.ts.map +1 -0
  42. package/dist/recovery/transportDrain.js +262 -0
  43. package/dist/recovery/transportDrain.js.map +1 -0
  44. package/dist/transaction/consumeNotes.d.ts.map +1 -1
  45. package/dist/transaction/consumeNotes.js +4 -1
  46. package/dist/transaction/consumeNotes.js.map +1 -1
  47. package/dist/transaction/p2id.d.ts.map +1 -1
  48. package/dist/transaction/p2id.js +4 -1
  49. package/dist/transaction/p2id.js.map +1 -1
  50. package/dist/transaction/summary.d.ts +10 -4
  51. package/dist/transaction/summary.d.ts.map +1 -1
  52. package/dist/transaction/summary.js +13 -7
  53. package/dist/transaction/summary.js.map +1 -1
  54. package/dist/transaction/updateGuardian.js +4 -1
  55. package/dist/transaction/updateGuardian.js.map +1 -1
  56. package/dist/transaction/updateProcedureThreshold.js +4 -1
  57. package/dist/transaction/updateProcedureThreshold.js.map +1 -1
  58. package/dist/transaction/updateSigners.js +4 -1
  59. package/dist/transaction/updateSigners.js.map +1 -1
  60. package/dist/transaction.d.ts +1 -1
  61. package/dist/transaction.d.ts.map +1 -1
  62. package/dist/transaction.js +1 -1
  63. package/dist/transaction.js.map +1 -1
  64. package/package.json +14 -10
  65. package/src/account/builder.ts +87 -35
  66. package/src/connectivity.ts +9 -4
  67. package/src/index.ts +34 -1
  68. package/src/multisig/authArgErrors.ts +138 -0
  69. package/src/multisig/helpers.ts +7 -2
  70. package/src/multisig.ts +513 -20
  71. package/src/procedures.ts +1 -1
  72. package/src/recovery/proposalNoteImport.ts +548 -0
  73. package/src/recovery/publicNoteBackfill.ts +502 -0
  74. package/src/recovery/recoverNotes.ts +288 -0
  75. package/src/recovery/transportDrain.ts +330 -0
  76. package/src/transaction/consumeNotes.ts +4 -1
  77. package/src/transaction/p2id.ts +4 -1
  78. package/src/transaction/summary.ts +13 -7
  79. package/src/transaction/updateGuardian.ts +4 -1
  80. package/src/transaction/updateProcedureThreshold.ts +4 -1
  81. package/src/transaction/updateSigners.ts +4 -1
  82. package/src/transaction.ts +1 -1
  83. package/dist/account/builder.test.d.ts +0 -2
  84. package/dist/account/builder.test.d.ts.map +0 -1
  85. package/dist/account/builder.test.js +0 -166
  86. package/dist/account/builder.test.js.map +0 -1
  87. package/dist/account/masm/account-components/auth.d.ts +0 -2
  88. package/dist/account/masm/account-components/auth.d.ts.map +0 -1
  89. package/dist/account/masm/account-components/auth.js +0 -46
  90. package/dist/account/masm/account-components/auth.js.map +0 -1
  91. package/dist/account/masm/index.d.ts +0 -2
  92. package/dist/account/masm/index.d.ts.map +0 -1
  93. package/dist/account/masm/index.js +0 -4
  94. package/dist/account/masm/index.js.map +0 -1
  95. package/dist/account/masm.d.ts +0 -2
  96. package/dist/account/masm.d.ts.map +0 -1
  97. package/dist/account/masm.js +0 -4
  98. package/dist/account/masm.js.map +0 -1
  99. package/dist/account/storage.test.d.ts +0 -2
  100. package/dist/account/storage.test.d.ts.map +0 -1
  101. package/dist/account/storage.test.js +0 -73
  102. package/dist/account/storage.test.js.map +0 -1
  103. package/dist/client.test.d.ts +0 -2
  104. package/dist/client.test.d.ts.map +0 -1
  105. package/dist/client.test.js +0 -380
  106. package/dist/client.test.js.map +0 -1
  107. package/dist/connectivity.test.d.ts +0 -2
  108. package/dist/connectivity.test.d.ts.map +0 -1
  109. package/dist/connectivity.test.js +0 -61
  110. package/dist/connectivity.test.js.map +0 -1
  111. package/dist/inspector.test.d.ts +0 -2
  112. package/dist/inspector.test.d.ts.map +0 -1
  113. package/dist/inspector.test.js +0 -425
  114. package/dist/inspector.test.js.map +0 -1
  115. package/dist/lookupAuth.test.d.ts +0 -2
  116. package/dist/lookupAuth.test.d.ts.map +0 -1
  117. package/dist/lookupAuth.test.js +0 -138
  118. package/dist/lookupAuth.test.js.map +0 -1
  119. package/dist/multisig/consumeNotesErrors.test.d.ts +0 -2
  120. package/dist/multisig/consumeNotesErrors.test.d.ts.map +0 -1
  121. package/dist/multisig/consumeNotesErrors.test.js +0 -28
  122. package/dist/multisig/consumeNotesErrors.test.js.map +0 -1
  123. package/dist/multisig/helpers.test.d.ts +0 -2
  124. package/dist/multisig/helpers.test.d.ts.map +0 -1
  125. package/dist/multisig/helpers.test.js +0 -94
  126. package/dist/multisig/helpers.test.js.map +0 -1
  127. package/dist/multisig.test.d.ts +0 -2
  128. package/dist/multisig.test.d.ts.map +0 -1
  129. package/dist/multisig.test.js +0 -4112
  130. package/dist/multisig.test.js.map +0 -1
  131. package/dist/proposal/factory.test.d.ts +0 -2
  132. package/dist/proposal/factory.test.d.ts.map +0 -1
  133. package/dist/proposal/factory.test.js +0 -32
  134. package/dist/proposal/factory.test.js.map +0 -1
  135. package/dist/proposal/metadata.test.d.ts +0 -2
  136. package/dist/proposal/metadata.test.d.ts.map +0 -1
  137. package/dist/proposal/metadata.test.js +0 -193
  138. package/dist/proposal/metadata.test.js.map +0 -1
  139. package/dist/prover/config.test.d.ts +0 -2
  140. package/dist/prover/config.test.d.ts.map +0 -1
  141. package/dist/prover/config.test.js +0 -54
  142. package/dist/prover/config.test.js.map +0 -1
  143. package/dist/prover/errors.test.d.ts +0 -2
  144. package/dist/prover/errors.test.d.ts.map +0 -1
  145. package/dist/prover/errors.test.js +0 -29
  146. package/dist/prover/errors.test.js.map +0 -1
  147. package/dist/prover/retry.test.d.ts +0 -2
  148. package/dist/prover/retry.test.d.ts.map +0 -1
  149. package/dist/prover/retry.test.js +0 -20
  150. package/dist/prover/retry.test.js.map +0 -1
  151. package/dist/prover/workflow.test.d.ts +0 -2
  152. package/dist/prover/workflow.test.d.ts.map +0 -1
  153. package/dist/prover/workflow.test.js +0 -105
  154. package/dist/prover/workflow.test.js.map +0 -1
  155. package/dist/raw-client.test.d.ts +0 -2
  156. package/dist/raw-client.test.d.ts.map +0 -1
  157. package/dist/raw-client.test.js +0 -111
  158. package/dist/raw-client.test.js.map +0 -1
  159. package/dist/rpc/config.test.d.ts +0 -2
  160. package/dist/rpc/config.test.d.ts.map +0 -1
  161. package/dist/rpc/config.test.js +0 -24
  162. package/dist/rpc/config.test.js.map +0 -1
  163. package/dist/rpc/errors.test.d.ts +0 -2
  164. package/dist/rpc/errors.test.d.ts.map +0 -1
  165. package/dist/rpc/errors.test.js +0 -34
  166. package/dist/rpc/errors.test.js.map +0 -1
  167. package/dist/rpc/retry.test.d.ts +0 -2
  168. package/dist/rpc/retry.test.d.ts.map +0 -1
  169. package/dist/rpc/retry.test.js +0 -98
  170. package/dist/rpc/retry.test.js.map +0 -1
  171. package/dist/signers/ecdsa.test.d.ts +0 -2
  172. package/dist/signers/ecdsa.test.d.ts.map +0 -1
  173. package/dist/signers/ecdsa.test.js +0 -88
  174. package/dist/signers/ecdsa.test.js.map +0 -1
  175. package/dist/signers/falcon.test.d.ts +0 -2
  176. package/dist/signers/falcon.test.d.ts.map +0 -1
  177. package/dist/signers/falcon.test.js +0 -161
  178. package/dist/signers/falcon.test.js.map +0 -1
  179. package/dist/signers/miden-wallet.ecdsa-recovery.test.d.ts +0 -2
  180. package/dist/signers/miden-wallet.ecdsa-recovery.test.d.ts.map +0 -1
  181. package/dist/signers/miden-wallet.ecdsa-recovery.test.js +0 -148
  182. package/dist/signers/miden-wallet.ecdsa-recovery.test.js.map +0 -1
  183. package/dist/signers/miden-wallet.test.d.ts +0 -2
  184. package/dist/signers/miden-wallet.test.d.ts.map +0 -1
  185. package/dist/signers/miden-wallet.test.js +0 -164
  186. package/dist/signers/miden-wallet.test.js.map +0 -1
  187. package/dist/signers/para.test.d.ts +0 -2
  188. package/dist/signers/para.test.d.ts.map +0 -1
  189. package/dist/signers/para.test.js +0 -146
  190. package/dist/signers/para.test.js.map +0 -1
  191. package/dist/transaction/index.d.ts +0 -8
  192. package/dist/transaction/index.d.ts.map +0 -1
  193. package/dist/transaction/index.js +0 -7
  194. package/dist/transaction/index.js.map +0 -1
  195. package/dist/transaction/p2id.test.d.ts +0 -2
  196. package/dist/transaction/p2id.test.d.ts.map +0 -1
  197. package/dist/transaction/p2id.test.js +0 -246
  198. package/dist/transaction/p2id.test.js.map +0 -1
  199. package/dist/transaction/rpoRandomCoin.test.d.ts +0 -2
  200. package/dist/transaction/rpoRandomCoin.test.d.ts.map +0 -1
  201. package/dist/transaction/rpoRandomCoin.test.js +0 -52
  202. package/dist/transaction/rpoRandomCoin.test.js.map +0 -1
  203. package/dist/transaction/summary.test.d.ts +0 -2
  204. package/dist/transaction/summary.test.d.ts.map +0 -1
  205. package/dist/transaction/summary.test.js +0 -26
  206. package/dist/transaction/summary.test.js.map +0 -1
  207. package/dist/transaction.test.d.ts +0 -2
  208. package/dist/transaction.test.d.ts.map +0 -1
  209. package/dist/transaction.test.js +0 -122
  210. package/dist/transaction.test.js.map +0 -1
  211. package/dist/types/proposal.test.d.ts +0 -2
  212. package/dist/types/proposal.test.d.ts.map +0 -1
  213. package/dist/types/proposal.test.js +0 -56
  214. package/dist/types/proposal.test.js.map +0 -1
  215. package/dist/utils/digest.test.d.ts +0 -2
  216. package/dist/utils/digest.test.d.ts.map +0 -1
  217. package/dist/utils/digest.test.js +0 -48
  218. package/dist/utils/digest.test.js.map +0 -1
  219. package/dist/utils/ecdsa.test.d.ts +0 -2
  220. package/dist/utils/ecdsa.test.d.ts.map +0 -1
  221. package/dist/utils/ecdsa.test.js +0 -131
  222. package/dist/utils/ecdsa.test.js.map +0 -1
  223. package/dist/utils/encoding.test.d.ts +0 -2
  224. package/dist/utils/encoding.test.d.ts.map +0 -1
  225. package/dist/utils/encoding.test.js +0 -185
  226. package/dist/utils/encoding.test.js.map +0 -1
  227. package/dist/utils/key.test.d.ts +0 -2
  228. package/dist/utils/key.test.d.ts.map +0 -1
  229. package/dist/utils/key.test.js +0 -82
  230. package/dist/utils/key.test.js.map +0 -1
  231. package/dist/utils/signature.test.d.ts +0 -2
  232. package/dist/utils/signature.test.d.ts.map +0 -1
  233. package/dist/utils/signature.test.js +0 -164
  234. package/dist/utils/signature.test.js.map +0 -1
  235. package/dist/utils/word.test.d.ts +0 -2
  236. package/dist/utils/word.test.d.ts.map +0 -1
  237. package/dist/utils/word.test.js +0 -54
  238. package/dist/utils/word.test.js.map +0 -1
  239. package/masm/account_components/auth/guarded_multisig.masm +0 -42
  240. package/src/account/builder.test.ts +0 -238
  241. package/src/account/masm/account-components/auth.ts +0 -46
  242. package/src/account/masm/index.ts +0 -4
  243. package/src/account/masm.ts +0 -4
  244. package/src/account/storage.test.ts +0 -90
  245. package/src/client.test.ts +0 -473
  246. package/src/connectivity.test.ts +0 -67
  247. package/src/inspector.test.ts +0 -542
  248. package/src/lookupAuth.test.ts +0 -185
  249. package/src/multisig/consumeNotesErrors.test.ts +0 -43
  250. package/src/multisig/helpers.test.ts +0 -114
  251. package/src/multisig.test.ts +0 -5008
  252. package/src/proposal/factory.test.ts +0 -40
  253. package/src/proposal/metadata.test.ts +0 -235
  254. package/src/prover/config.test.ts +0 -90
  255. package/src/prover/errors.test.ts +0 -61
  256. package/src/prover/retry.test.ts +0 -38
  257. package/src/prover/workflow.test.ts +0 -145
  258. package/src/raw-client.test.ts +0 -171
  259. package/src/rpc/config.test.ts +0 -45
  260. package/src/rpc/errors.test.ts +0 -69
  261. package/src/rpc/retry.test.ts +0 -144
  262. package/src/signers/ecdsa.test.ts +0 -111
  263. package/src/signers/falcon.test.ts +0 -197
  264. package/src/signers/miden-wallet.ecdsa-recovery.test.ts +0 -201
  265. package/src/signers/miden-wallet.test.ts +0 -206
  266. package/src/signers/para.test.ts +0 -186
  267. package/src/transaction/index.ts +0 -13
  268. package/src/transaction/p2id.test.ts +0 -371
  269. package/src/transaction/rpoRandomCoin.test.ts +0 -64
  270. package/src/transaction/summary.test.ts +0 -32
  271. package/src/transaction.test.ts +0 -142
  272. package/src/types/proposal.test.ts +0 -68
  273. package/src/utils/digest.test.ts +0 -55
  274. package/src/utils/ecdsa.test.ts +0 -164
  275. package/src/utils/encoding.test.ts +0 -233
  276. package/src/utils/key.test.ts +0 -91
  277. package/src/utils/signature.test.ts +0 -210
  278. package/src/utils/word.test.ts +0 -64
@@ -0,0 +1,502 @@
1
+ /**
2
+ * Historical public-note backfill by tag.
3
+ *
4
+ * Public notes addressed to an account are on chain, but normal forward sync
5
+ * starts from the store's **global** cursor: in a shared dirty store the
6
+ * cursor may already be past blocks containing a recovered account's notes,
7
+ * and a fresh store has no efficient path to them at all. This module
8
+ * rescans a historical block range with the account's standard note tag and
9
+ * imports what it finds with on-chain inclusion proofs, without ever
10
+ * touching the global sync height.
11
+ */
12
+
13
+ import {
14
+ AccountId,
15
+ type CommittedNote,
16
+ Endpoint,
17
+ type InputNoteRecord,
18
+ type Note,
19
+ type NoteInclusionProof,
20
+ NoteScript,
21
+ NoteTag,
22
+ NoteType,
23
+ RpcClient,
24
+ } from '@miden-sdk/miden-sdk';
25
+
26
+ import { errorMessage } from '../connectivity.js';
27
+ import {
28
+ collectExistingRecords,
29
+ detailsKeyOf,
30
+ errorDetail,
31
+ importNoteWithProof,
32
+ type NoteImportOutcome,
33
+ reclassifyConsumedImports,
34
+ } from './proposalNoteImport.js';
35
+ import { getRawMidenClient, requireMidenRpcEndpoint, type RawClientSource } from '../raw-client.js';
36
+ import { resolveRpcConfig, type RpcConfig } from '../rpc/config.js';
37
+ import { isTransientRpcError } from '../rpc/errors.js';
38
+ import { retryRpcRead } from '../rpc/retry.js';
39
+ import { normalizeHexWord } from '../utils/encoding.js';
40
+
41
+ /**
42
+ * `RpcError::PaginationError`'s Display prefix in the WASM error chain: the
43
+ * node caps internal pagination per `syncNotes` request, and the scan splits
44
+ * the range client-side when it trips. Exported for the drift-guard test,
45
+ * which pins it against the shipped WASM binary.
46
+ */
47
+ export const RPC_PAGINATION_FRAGMENT = 'rpc pagination error';
48
+
49
+ /**
50
+ * Upper bound on `syncNotes` requests per backfill. Splitting around the
51
+ * node's pagination cap halves ranges, so the budget is only approachable
52
+ * when nearly every sub-range is dense enough to trip the cap; exhausting it
53
+ * reports the remaining ranges as uncovered instead of scanning forever.
54
+ */
55
+ const MAX_SCAN_REQUESTS = 128;
56
+
57
+ /** Block numbers are u32 on chain; out-of-range JS numbers would silently
58
+ * wrap modulo 2^32 at the WASM boundary and scan the wrong range. */
59
+ const MAX_BLOCK_NUMBER = 4_294_967_295;
60
+
61
+ /**
62
+ * The requested scan range is invalid (out-of-range bound, or inverted
63
+ * against the resolved chain tip) — a caller error that retrying cannot fix,
64
+ * as opposed to the transient chain-tip-lookup failures this function also
65
+ * throws. `Multisig.recoverNotes` keys its problem retryability on this,
66
+ * mirroring the Rust orchestrator's `InvalidConfig` check.
67
+ */
68
+ export class BackfillRangeError extends Error {}
69
+
70
+ function requireBlockNumber(name: string, value: number): void {
71
+ if (!Number.isInteger(value) || value < 0 || value > MAX_BLOCK_NUMBER) {
72
+ throw new BackfillRangeError(
73
+ `${name} must be an integer in [0, ${MAX_BLOCK_NUMBER}], got ${value}`,
74
+ );
75
+ }
76
+ }
77
+
78
+ /** Verdict of the static relevance screen. */
79
+ export type ScreenVerdict = 'relevant' | 'irrelevant' | 'unscreenable';
80
+
81
+ /**
82
+ * Static relevance screen for a discovered public note. The Rust SDK screens
83
+ * with the execution-based `NoteScreener` normal sync uses; the WASM surface
84
+ * does not expose it, so this mirrors its verdict for the well-known note
85
+ * scripts: a note is `relevant` when it is a P2ID/P2IDE note whose target
86
+ * (or P2IDE reclaimer) is the scanned account, and `irrelevant` when it is
87
+ * one of those scripts addressed at someone else. Notes with other scripts
88
+ * are `unscreenable` — this screen cannot judge them, and they are
89
+ * conservatively not imported (tags are shared, truncated filters, and
90
+ * importing unscreened tag matches would let anyone pollute the store), but
91
+ * the report counts them separately so "screened out" and "not screenable"
92
+ * stay distinguishable. Exported for the drift-guard test, which pins the
93
+ * root and storage-layout assumptions against real WASM-built notes.
94
+ */
95
+ export function screenNoteForAccount(note: Note, account: AccountId): ScreenVerdict {
96
+ const root = normalizeHexWord(note.script().root().toHex());
97
+ const items = note.recipient().storage().items();
98
+ const prefix = account.prefix().asInt();
99
+ const suffix = account.suffix().asInt();
100
+ const accountAt = (index: number): boolean =>
101
+ items.length > index + 1 &&
102
+ items[index].asInt() === suffix &&
103
+ items[index + 1].asInt() === prefix;
104
+ if (root === normalizeHexWord(NoteScript.p2id().root().toHex())) {
105
+ // P2ID note storage: [target.suffix, target.prefix].
106
+ return accountAt(0) ? 'relevant' : 'irrelevant';
107
+ }
108
+ if (root === normalizeHexWord(NoteScript.p2ide().root().toHex())) {
109
+ // P2IDE note storage: [reclaimer.suffix, reclaimer.prefix,
110
+ // target.suffix, target.prefix, reclaim, timelock].
111
+ return accountAt(2) || accountAt(0) ? 'relevant' : 'irrelevant';
112
+ }
113
+ return 'unscreenable';
114
+ }
115
+
116
+ /** A contiguous block range, inclusive on both ends. */
117
+ export interface BlockRange {
118
+ /** First block of the range. */
119
+ from: number;
120
+ /** Last block of the range. */
121
+ to: number;
122
+ }
123
+
124
+ /**
125
+ * Result of {@link backfillPublicNotesByTag}.
126
+ *
127
+ * Scan problems are reported here rather than thrown so a partially failing
128
+ * scan never aborts the rest of a recovery flow: notes discovered in the
129
+ * covered ranges are imported regardless.
130
+ */
131
+ export interface PublicBackfillReport {
132
+ /** First block of the requested scan range. */
133
+ scannedFrom: number;
134
+ /** Last block of the requested scan range. */
135
+ scannedTo: number;
136
+ /** Unique tag-matching notes the scan discovered, of every visibility. */
137
+ discovered: number;
138
+ /** Unique non-public matches skipped: the chain does not hold their
139
+ * bodies, so they cannot be rebuilt from a scan. Private notes are covered
140
+ * by the transport drain and proposal-import primitives instead. */
141
+ skippedPrivate: number;
142
+ /** Unique public matches the relevance screen rejected: tags are
143
+ * best-effort, truncated filters, so unrelated notes can carry this
144
+ * account's tag. Like normal sync, only notes the account could actually
145
+ * consume are imported; the rest are counted here. (This SDK screens
146
+ * statically against the well-known P2ID/P2IDE scripts; the Rust SDK uses
147
+ * the execution-based screener.) */
148
+ skippedIrrelevant: number;
149
+ /** Unique public matches this SDK's static screen could not judge (custom
150
+ * note scripts). They are conservatively not imported, but counted apart
151
+ * from `skippedIrrelevant` so callers can tell "screened out" from "not
152
+ * screenable". Always `0` in the Rust SDK, whose execution-based screener
153
+ * judges every note. */
154
+ skippedUnscreenable: number;
155
+ /** One outcome per unique public note that passed the relevance screen —
156
+ * `outcomes.length === discovered - skippedPrivate - skippedIrrelevant -
157
+ * skippedUnscreenable`. Screened-out, unscreenable, and private matches
158
+ * get no outcome, only their counters. */
159
+ outcomes: NoteImportOutcome[];
160
+ /** Sub-ranges of `[scannedFrom, scannedTo]` the scan could not cover (RPC
161
+ * failures, or the scan budget ran out while splitting around the node's
162
+ * pagination cap). Empty when the whole range was scanned. Notes committed
163
+ * in these ranges may be missing from `outcomes`. */
164
+ uncovered: BlockRange[];
165
+ /** Whether rerunning the backfill can plausibly improve the result: cover
166
+ * `uncovered` ranges, or retry outcomes whose own `retryable` flag is set.
167
+ * Always `false` when the scan fully covered the range and no outcome is
168
+ * retryable. */
169
+ retryable: boolean;
170
+ /** Human-readable cause when the scan did not cover the whole range. */
171
+ reason?: string;
172
+ }
173
+
174
+ export interface BackfillPublicNotesOptions {
175
+ /** Hex ID of the account whose standard note tag should be scanned. */
176
+ accountId: string;
177
+ /** Miden node RPC endpoint used for the scan and body fetches. Must point
178
+ * at the same network as the injected Miden client. */
179
+ midenRpcEndpoint: string;
180
+ /** First block of the scan range (default: genesis). */
181
+ fromBlock?: number;
182
+ /** Last block of the scan range (default: the current chain tip). */
183
+ toBlock?: number;
184
+ /** Node RPC read-retry configuration (defaults match the rest of the SDK). */
185
+ rpc?: RpcConfig;
186
+ }
187
+
188
+ /**
189
+ * Scans a historical block range for public notes addressed at an account's
190
+ * standard note tag and imports what it finds with their on-chain inclusion
191
+ * proofs. Counterpart of
192
+ * `MultisigClient::backfill_public_notes_by_tag` in the Rust SDK.
193
+ *
194
+ * Use after account recovery: normal forward sync starts from the store's
195
+ * **global** cursor, so in a shared dirty store the cursor may already be
196
+ * past blocks containing the recovered account's notes, and a fresh store
197
+ * would need to replay the whole chain state to see them. The scan is
198
+ * tag-scoped and its cost grows with the number of matching notes, not the
199
+ * range length, which makes genesis an acceptable default lower
200
+ * bound. The global sync height is never touched — run normal sync
201
+ * afterwards to verify the imported notes. The store must have synced at
202
+ * least once (`Multisig.recoverNotes` syncs the chain before this strategy
203
+ * runs): importing a proof into a store that has never seen the chain
204
+ * fails, and such failures surface as `failed` outcomes.
205
+ *
206
+ * Notes are discovered by tag only — a best-effort filter: notes sent with
207
+ * unrelated custom tags are outside this scan's guarantee, and, like normal
208
+ * sync, every new discovery is screened for relevance before import —
209
+ * tag-colliding notes the account cannot consume are counted as
210
+ * `skippedIrrelevant` instead of polluting the store. This SDK screens
211
+ * statically against the well-known P2ID/P2IDE scripts (the WASM surface
212
+ * does not expose the execution-based screener the Rust SDK uses), so notes
213
+ * with custom scripts are conservatively not imported and counted as
214
+ * `skippedUnscreenable`. Only public notes can be
215
+ * rebuilt from chain data; private matches are counted as `skippedPrivate`
216
+ * and are covered by the transport drain and proposal-import primitives
217
+ * instead.
218
+ *
219
+ * A range dense enough to trip the node's internal pagination cap is split
220
+ * client-side and rescanned as narrower requests; ranges that still cannot
221
+ * be covered are reported in {@link PublicBackfillReport.uncovered} rather
222
+ * than failing the recovery flow. This function throws only when the scan
223
+ * range itself cannot be established (chain-tip lookup failed, an invalid
224
+ * account ID, a block bound that is not a u32 integer, or
225
+ * `fromBlock > toBlock`).
226
+ *
227
+ * Prefer the `Multisig.backfillPublicNotesByTag` convenience method, which
228
+ * reuses the client's endpoint and retry configuration.
229
+ *
230
+ * @example
231
+ * ```typescript
232
+ * const report = await multisig.backfillPublicNotesByTag();
233
+ * console.log(report.discovered, 'discovered,', report.outcomes.length, 'public');
234
+ * await multisig.syncState(); // verifies the imported notes
235
+ * ```
236
+ */
237
+ export async function backfillPublicNotesByTag(
238
+ midenClient: RawClientSource,
239
+ options: BackfillPublicNotesOptions,
240
+ ): Promise<PublicBackfillReport> {
241
+ const midenRpcEndpoint = requireMidenRpcEndpoint(options.midenRpcEndpoint);
242
+ const rpcConfig = resolveRpcConfig(options.rpc);
243
+ const webClient = await getRawMidenClient(midenClient, midenRpcEndpoint);
244
+ const rpcClient = new RpcClient(new Endpoint(midenRpcEndpoint));
245
+ // Parse eagerly so a malformed account ID throws before any network work.
246
+ AccountId.fromHex(options.accountId);
247
+
248
+ const from = options.fromBlock ?? 0;
249
+ requireBlockNumber('fromBlock', from);
250
+ let to: number;
251
+ if (options.toBlock !== undefined) {
252
+ requireBlockNumber('toBlock', options.toBlock);
253
+ to = options.toBlock;
254
+ } else {
255
+ try {
256
+ const tip = await retryRpcRead(() => rpcClient.getBlockHeaderByNumber(), rpcConfig);
257
+ to = tip.blockNum();
258
+ } catch (error) {
259
+ throw new Error(
260
+ `failed to resolve the chain tip for the backfill scan: ${errorDetail(error)}`,
261
+ );
262
+ }
263
+ }
264
+ if (from > to) {
265
+ throw new BackfillRangeError(`backfill range is inverted: fromBlock ${from} > toBlock ${to}`);
266
+ }
267
+
268
+ // Work queue of inclusive sub-ranges, split in half whenever the node
269
+ // reports its pagination cap for one of them. WASM call arguments are
270
+ // consumed by the bridge, so the tag is rebuilt per request.
271
+ const scanTag = (): NoteTag => NoteTag.withAccountTarget(AccountId.fromHex(options.accountId));
272
+ const queue: Array<[number, number]> = [[from, to]];
273
+ const discovered = new Map<string, CommittedNote>();
274
+ const uncovered: BlockRange[] = [];
275
+ const scanReasons: string[] = [];
276
+ let retryable = false;
277
+ let requests = 0;
278
+ let budgetExhausted = false;
279
+
280
+ while (queue.length > 0) {
281
+ const [lo, hi] = queue.shift() as [number, number];
282
+ if (requests >= MAX_SCAN_REQUESTS) {
283
+ budgetExhausted = true;
284
+ uncovered.push({ from: lo, to: hi });
285
+ continue;
286
+ }
287
+ requests += 1;
288
+ try {
289
+ const info = await retryRpcRead(() => rpcClient.syncNotes(lo, hi, [scanTag()]), rpcConfig);
290
+ for (const committed of info.notes()) {
291
+ const idHex = normalizeHexWord(committed.noteId().toString());
292
+ if (!discovered.has(idHex)) {
293
+ // The wrapper is kept (not a one-shot accessor result) so fresh
294
+ // NoteId handles can be minted per body-fetch attempt below.
295
+ discovered.set(idHex, committed);
296
+ }
297
+ }
298
+ } catch (error) {
299
+ // The node caps internal pagination per request rather than
300
+ // truncating; a single-block range cannot be split further (and
301
+ // cannot realistically hold that many pages), so only splittable
302
+ // ranges take this branch.
303
+ if (errorMessage(error).toLowerCase().includes(RPC_PAGINATION_FRAGMENT) && lo < hi) {
304
+ const mid = lo + Math.floor((hi - lo) / 2);
305
+ queue.unshift([lo, mid], [mid + 1, hi]);
306
+ continue;
307
+ }
308
+ retryable ||= isTransientRpcError(error);
309
+ scanReasons.push(`blocks [${lo}, ${hi}]: ${errorDetail(error)}`);
310
+ uncovered.push({ from: lo, to: hi });
311
+ }
312
+ }
313
+ if (budgetExhausted) {
314
+ retryable = true;
315
+ scanReasons.push(
316
+ `scan budget of ${MAX_SCAN_REQUESTS} requests exhausted while splitting around the node's pagination cap; rerun the backfill over the uncovered ranges`,
317
+ );
318
+ }
319
+
320
+ const publicNotes: Array<{ idHex: string; committed: CommittedNote }> = [];
321
+ for (const [idHex, committed] of discovered) {
322
+ if (committed.noteType() === NoteType.Public) {
323
+ publicNotes.push({ idHex, committed });
324
+ }
325
+ }
326
+ const skippedPrivate = discovered.size - publicNotes.length;
327
+ let skippedIrrelevant = 0;
328
+ let skippedUnscreenable = 0;
329
+
330
+ const outcomes: NoteImportOutcome[] = [];
331
+ const buildReport = (): PublicBackfillReport => {
332
+ let reason: string | undefined;
333
+ if (scanReasons.length > 0) {
334
+ reason =
335
+ scanReasons.length <= 3
336
+ ? scanReasons.join('; ')
337
+ : `${scanReasons.slice(0, 3).join('; ')}; …and ${scanReasons.length - 3} more`;
338
+ }
339
+ return {
340
+ scannedFrom: from,
341
+ scannedTo: to,
342
+ discovered: discovered.size,
343
+ skippedPrivate,
344
+ skippedIrrelevant,
345
+ skippedUnscreenable,
346
+ outcomes,
347
+ uncovered,
348
+ // Rerunning can help when scan ranges were left uncovered OR when any
349
+ // per-note outcome is itself retryable — surface both at report level
350
+ // so orchestration keyed on the report alone reruns when it should.
351
+ retryable: retryable || outcomes.some((outcome) => outcome.retryable === true),
352
+ ...(reason === undefined ? {} : { reason }),
353
+ };
354
+ };
355
+
356
+ interface BackfillCandidate {
357
+ idHex: string;
358
+ note: Note;
359
+ proof: NoteInclusionProof;
360
+ detailsKey: string;
361
+ }
362
+ const pending: BackfillCandidate[] = [];
363
+ if (publicNotes.length > 0) {
364
+ try {
365
+ // One batched body fetch — the upstream client chunks internally by
366
+ // the node's negotiated note-ids limit, and the node returns full
367
+ // bodies for public notes, so the scan's ID + proof is all this path
368
+ // needs. The WASM bridge consumes call arguments, so fresh NoteId
369
+ // handles are minted from the kept wrappers on every retry attempt.
370
+ const fetchedNotes = await retryRpcRead(
371
+ () =>
372
+ rpcClient.getNotesById(publicNotes.map((candidate) => candidate.committed.noteId())),
373
+ rpcConfig,
374
+ );
375
+ const bodies = new Map<string, { note: Note; proof: NoteInclusionProof }>();
376
+ for (const fetched of fetchedNotes) {
377
+ if (fetched.note) {
378
+ bodies.set(normalizeHexWord(fetched.noteId.toString()), {
379
+ note: fetched.note,
380
+ proof: fetched.inclusionProof,
381
+ });
382
+ }
383
+ }
384
+ for (const { idHex } of publicNotes) {
385
+ const body = bodies.get(idHex);
386
+ if (body) {
387
+ pending.push({
388
+ idHex,
389
+ note: body.note,
390
+ proof: body.proof,
391
+ detailsKey: detailsKeyOf(
392
+ normalizeHexWord(body.note.recipient().digest().toHex()),
393
+ body.note.assets(),
394
+ ),
395
+ });
396
+ } else {
397
+ // Discovered as public by the scan but returned without a body —
398
+ // not expected for a committed public note.
399
+ outcomes.push({
400
+ identifier: idHex,
401
+ source: 'backfill',
402
+ status: 'failed',
403
+ retryable: true,
404
+ reason: 'the node did not return a body for this public note',
405
+ });
406
+ }
407
+ }
408
+ } catch (error) {
409
+ const fetchRetryable = isTransientRpcError(error);
410
+ const reason = `failed to fetch note bodies: ${errorDetail(error)}`;
411
+ for (const { idHex } of publicNotes) {
412
+ outcomes.push({
413
+ identifier: idHex,
414
+ source: 'backfill',
415
+ status: 'failed',
416
+ retryable: fetchRetryable,
417
+ reason,
418
+ });
419
+ }
420
+ }
421
+ }
422
+
423
+ if (pending.length === 0) {
424
+ return buildReport();
425
+ }
426
+
427
+ let existing: Map<string, InputNoteRecord>;
428
+ try {
429
+ existing = await collectExistingRecords(webClient);
430
+ } catch (error) {
431
+ const reason = `failed to read local store: ${errorDetail(error)}`;
432
+ for (const candidate of pending) {
433
+ outcomes.push({
434
+ identifier: candidate.idHex,
435
+ source: 'backfill',
436
+ status: 'failed',
437
+ reason,
438
+ });
439
+ }
440
+ return buildReport();
441
+ }
442
+
443
+ // Provisionally `imported` outcomes, re-classified in one batched
444
+ // post-import state check below.
445
+ const screenAccount = AccountId.fromHex(options.accountId);
446
+ const imported: Array<{ index: number; idHex: string; detailsKey: string }> = [];
447
+ for (const candidate of pending) {
448
+ // Skip decisions key on an exact note-ID match only: a details-key
449
+ // match is a lossy approximation (see {@link detailsKeyOf} in the
450
+ // proposal-import module), and the upstream import dedupes exactly by
451
+ // the real details commitment, so importing "again" is safe while
452
+ // pre-skipping on the approximation could silently drop a genuinely
453
+ // new note. Unlike the proposal import, a proof-less (expected) record
454
+ // is NOT skipped here even on an ID match: this primitive exists
455
+ // because forward sync will never revisit the note's block, so the
456
+ // freshly fetched proof is applied to upgrade the record in place (the
457
+ // WASM import handles existing records).
458
+ const record = existing.get(candidate.idHex);
459
+ if (record && (record.isConsumed() || record.inclusionProof() !== undefined)) {
460
+ outcomes.push({
461
+ identifier: candidate.idHex,
462
+ source: 'backfill',
463
+ status: record.isConsumed() ? 'already-consumed' : 'already-present',
464
+ });
465
+ continue;
466
+ }
467
+ // Screen genuinely new discoveries for relevance, exactly like normal
468
+ // sync does before it stores a tag match. Records the store already
469
+ // tracks (by ID, or a metadata-less record matching on details) are
470
+ // material the user chose to track and skip the screen.
471
+ if (!record && !existing.has(candidate.detailsKey)) {
472
+ const verdict = screenNoteForAccount(candidate.note, screenAccount);
473
+ if (verdict === 'irrelevant') {
474
+ skippedIrrelevant += 1;
475
+ continue;
476
+ }
477
+ if (verdict === 'unscreenable') {
478
+ skippedUnscreenable += 1;
479
+ continue;
480
+ }
481
+ }
482
+ const { outcome, wasImported } = await importNoteWithProof(
483
+ webClient,
484
+ 'backfill',
485
+ candidate.idHex,
486
+ candidate.note,
487
+ candidate.proof,
488
+ );
489
+ if (wasImported) {
490
+ imported.push({
491
+ index: outcomes.length,
492
+ idHex: candidate.idHex,
493
+ detailsKey: candidate.detailsKey,
494
+ });
495
+ }
496
+ outcomes.push(outcome);
497
+ }
498
+
499
+ await reclassifyConsumedImports(webClient, imported, outcomes);
500
+
501
+ return buildReport();
502
+ }