@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
package/src/multisig.ts CHANGED
@@ -16,6 +16,7 @@ import type {
16
16
  ProposalSignatureEntry,
17
17
  ProposalType,
18
18
  } from './types.js';
19
+ import { ProposalSaltMalformedError } from './multisig/authArgErrors.js';
19
20
  import type { ProcedureName } from './procedures.js';
20
21
  import type {
21
22
  MidenClient,
@@ -43,7 +44,6 @@ import {
43
44
  chainAnchorToBase64,
44
45
  executeForSummary,
45
46
  executeForSummaryAt,
46
- summarySalt,
47
47
  buildUpdateSignersTransactionRequest,
48
48
  buildUpdateProcedureThresholdTransactionRequest,
49
49
  buildUpdateGuardianTransactionRequest,
@@ -85,6 +85,23 @@ import { AccountInspector, assertCompleteDetectedConfig } from './inspector.js';
85
85
  import { ProposalFactory } from './proposal/factory.js';
86
86
  import { ProposalMetadataCodec } from './proposal/metadata.js';
87
87
  import { ProposalSignatures } from './proposal/signatures.js';
88
+ import {
89
+ importNotesFromProposals as importNotesFromProposalsStandalone,
90
+ throwIfCancelled,
91
+ type NoteImportOutcome,
92
+ } from './recovery/proposalNoteImport.js';
93
+ import {
94
+ backfillPublicNotesByTag as backfillPublicNotesByTagStandalone,
95
+ type PublicBackfillReport,
96
+ } from './recovery/publicNoteBackfill.js';
97
+ import { drainPrivateNoteBacklog } from './recovery/transportDrain.js';
98
+ import {
99
+ GUARDIAN_SWITCH_RECOVERY_OPTIONS,
100
+ runNoteRecovery,
101
+ type NoteRecoveryReport,
102
+ type NoteRecoverySteps,
103
+ type RecoverNotesOptions,
104
+ } from './recovery/recoverNotes.js';
88
105
  import {
89
106
  getRawMidenClient,
90
107
  getTransactionProver,
@@ -194,6 +211,26 @@ function resolveProposalNonce(
194
211
  return options.nonce ?? Date.now();
195
212
  }
196
213
 
214
+ /**
215
+ * Deadline for `Multisig.preservePreSwitchProposalNotes`: no client in the
216
+ * stack applies request deadlines, and a half-dead old GUARDIAN must not
217
+ * stall the switch. Mirror of the Rust SDK's `PRE_SWITCH_IMPORT_TIMEOUT`.
218
+ */
219
+ const PRE_SWITCH_IMPORT_TIMEOUT_MS = 30_000;
220
+
221
+ /** Race sentinel for the pre-switch import timeout. */
222
+ const PRE_SWITCH_IMPORT_TIMED_OUT = Symbol('pre-switch proposal-note import timed out');
223
+
224
+ /**
225
+ * How long the switch waits after the timeout for the cancelled flow's one
226
+ * uninterruptible in-flight operation to settle, so it cannot overlap the
227
+ * switch transaction on the shared client.
228
+ */
229
+ const PRE_SWITCH_SETTLE_GRACE_MS = 5_000;
230
+
231
+ /** A `Word` is four field elements: 64 hex digits. Anything longer is not a salt. */
232
+ const MAX_SALT_HEX_DIGITS = 64;
233
+
197
234
  export class Multisig {
198
235
  account: Account;
199
236
  threshold: number;
@@ -412,6 +449,11 @@ export class Multisig {
412
449
  /**
413
450
  * Update the GUARDIAN client used by this Multisig instance.
414
451
  *
452
+ * When repointing to a different GUARDIAN provider after a switch, call
453
+ * {@link preservePreSwitchProposalNotes} first: pending proposals do not
454
+ * survive a switch, and the notes embedded in them can only be imported
455
+ * while the old GUARDIAN is still the current client.
456
+ *
415
457
  * @param guardianClient - The new GUARDIAN HTTP client
416
458
  */
417
459
  setGuardianClient(guardianClient: GuardianHttpClient): void {
@@ -593,6 +635,27 @@ export class Multisig {
593
635
  }
594
636
  }
595
637
 
638
+ /**
639
+ * Sync the local store with the Miden node, then reload the cached account
640
+ * and multisig config from it (mirrors the Rust `sync_network_only`).
641
+ * Without the refresh, a summary would be built against the freshly synced
642
+ * store while readiness thresholds and signature validation still read the
643
+ * stale cached config. Returns the store-backed account, or `null` when the
644
+ * store has no record for this account (the cached config is then kept, as
645
+ * in `getStoreAccount`); callers that require the account decide how to
646
+ * fail.
647
+ */
648
+ private async syncNetworkOnly(): Promise<Account | null> {
649
+ const webClient = await this.getRawClient();
650
+ await retryRpcRead(() => webClient.syncState(), this.rpcConfig);
651
+ const account = await retryRpcRead(
652
+ () => webClient.getAccount(AccountId.fromHex(this._accountId)),
653
+ this.rpcConfig,
654
+ );
655
+ this.refreshConfigFromAccount(account ?? null);
656
+ return account ?? null;
657
+ }
658
+
596
659
  private refreshConfigFromAccount(account: Account | null): void {
597
660
  if (!account) {
598
661
  return;
@@ -677,6 +740,67 @@ export class Multisig {
677
740
  return Array.from(this.proposals.values());
678
741
  }
679
742
 
743
+ /**
744
+ * {@link syncProposals} variant for the recovery flow: per-proposal
745
+ * failures (a payload that does not parse, a metadata binding that does
746
+ * not verify) are isolated as skip reasons instead of failing the whole
747
+ * listing, so one corrupt proposal cannot block recovering notes from the
748
+ * healthy ones. The strict listing stays the signing-path behavior, where
749
+ * a malformed proposal must surface loudly. Proposals at or below the
750
+ * account's committed nonce are dropped (already executed or superseded,
751
+ * matching the Rust SDK's listing), and the shared proposal cache is left
752
+ * untouched. GUARDIAN being unreachable still throws — there is nothing
753
+ * to isolate without a listing.
754
+ */
755
+ private async syncProposalsIsolatingFailures(cancelled?: () => boolean): Promise<{
756
+ proposals: Proposal[];
757
+ skipped: Array<{ identifier: string; reason: string }>;
758
+ }> {
759
+ const deltas = await this.guardian.getDeltaProposals(this._accountId);
760
+ const factory = this.proposalFactory();
761
+
762
+ let currentNonce: bigint | undefined;
763
+ try {
764
+ currentNonce = this.account.nonce().asInt();
765
+ } catch {
766
+ currentNonce = undefined;
767
+ }
768
+
769
+ const proposals: Proposal[] = [];
770
+ const skipped: Array<{ identifier: string; reason: string }> = [];
771
+ for (let position = 0; position < deltas.length; position += 1) {
772
+ throwIfCancelled(cancelled);
773
+ const delta = deltas[position];
774
+ const identifier = `proposal at nonce ${delta.nonce} (#${position})`;
775
+ try {
776
+ const proposalId = normalizeHexWord(
777
+ computeCommitmentFromTxSummary(delta.deltaPayload.txSummary.data)
778
+ );
779
+ const existingProposal = this.proposals.get(proposalId);
780
+ const proposal = factory.fromDelta(
781
+ delta,
782
+ proposalId,
783
+ existingProposal?.metadata,
784
+ existingProposal?.signatures ?? [],
785
+ );
786
+ // Stale-nonce check before the binding verification (matching the
787
+ // Rust listing): the verification re-executes consume proposals in
788
+ // the VM, which is wasted on proposals already executed/superseded.
789
+ if (currentNonce !== undefined && BigInt(proposal.nonce) <= currentNonce) {
790
+ continue;
791
+ }
792
+ await this.verifyProposalMetadataBinding(proposal);
793
+ proposals.push(proposal);
794
+ } catch (error) {
795
+ skipped.push({
796
+ identifier,
797
+ reason: error instanceof Error ? error.message : String(error),
798
+ });
799
+ }
800
+ }
801
+ return { proposals, skipped };
802
+ }
803
+
680
804
  /**
681
805
  * List all known proposals
682
806
  */
@@ -925,6 +1049,27 @@ export class Multisig {
925
1049
  options: CreateProposalOptions = {},
926
1050
  ): Promise<Proposal> {
927
1051
  const proposalNonce = resolveProposalNonce('createSwitchGuardianProposal', options);
1052
+ const { summaryBase64, metadata } = await this.buildSwitchGuardianSummary(
1053
+ newGuardianEndpoint,
1054
+ newGuardianPubkey,
1055
+ );
1056
+
1057
+ // SwitchGuardian is a regular delta proposal; push it to GUARDIAN so
1058
+ // sign/execute (which fetch from GUARDIAN) can find it. To leave an
1059
+ // unreachable GUARDIAN, use createSwitchGuardianProposalOffline instead.
1060
+ return this.createProposal(proposalNonce, summaryBase64, metadata);
1061
+ }
1062
+
1063
+ /**
1064
+ * Shared build step for both switch-GUARDIAN creation paths: verify the new
1065
+ * endpoint's `/pubkey` commitment, then execute the update-guardian request
1066
+ * for its summary and metadata. Kept in one place so the online and offline
1067
+ * proposals for the same operation can never drift apart.
1068
+ */
1069
+ private async buildSwitchGuardianSummary(
1070
+ newGuardianEndpoint: string,
1071
+ newGuardianPubkey: string,
1072
+ ): Promise<{ summaryBase64: string; metadata: ProposalMetadata }> {
928
1073
  const webClient = await this.getRawClient();
929
1074
  await this.verifyGuardianEndpointCommitment(newGuardianEndpoint, newGuardianPubkey);
930
1075
 
@@ -949,9 +1094,68 @@ export class Multisig {
949
1094
  description: `Switch GUARDIAN to ${newGuardianEndpoint}`,
950
1095
  };
951
1096
 
952
- // SwitchGuardian is a regular delta proposal; push it to GUARDIAN so
953
- // sign/execute (which fetch from GUARDIAN) can find it.
954
- return this.createProposal(proposalNonce, summaryBase64, metadata);
1097
+ return { summaryBase64, metadata };
1098
+ }
1099
+
1100
+ /**
1101
+ * Create a "switch GUARDIAN" proposal fully offline — nothing is pushed to
1102
+ * the current GUARDIAN, so an account can leave an unreachable operator
1103
+ * (issue #433; mirrors the Rust `create_proposal_offline`).
1104
+ *
1105
+ * The transaction summary is built and signed locally, the proposal is
1106
+ * cached for `signProposalOffline` / `executeProposal`, and the returned
1107
+ * `ExportedProposal` (which already includes the proposer's signature) can
1108
+ * be `JSON.stringify`-ed and shared with cosigners for `importProposal`.
1109
+ *
1110
+ * Only switch-GUARDIAN proposals can be created offline: every other
1111
+ * proposal type requires a GUARDIAN acknowledgment at execution, so a
1112
+ * proposal the GUARDIAN never saw could collect signatures but never
1113
+ * execute. The new endpoint must be reachable — its `/pubkey` commitment
1114
+ * is verified before anything is built or signed.
1115
+ *
1116
+ * Note: executeProposal's best-effort canonicalization push cannot reach a
1117
+ * proposal the current GUARDIAN never received, so even if that GUARDIAN is
1118
+ * back up at execution time it keeps serving the account until background
1119
+ * reconciliation (issue #305) — same outcome as executing while it is down.
1120
+ *
1121
+ * @param newGuardianEndpoint - The new GUARDIAN server endpoint URL
1122
+ * @param newGuardianPubkey - The new GUARDIAN server's public key commitment (hex)
1123
+ * @param options - Optional settings: `nonce`
1124
+ */
1125
+ async createSwitchGuardianProposalOffline(
1126
+ newGuardianEndpoint: string,
1127
+ newGuardianPubkey: string,
1128
+ options: CreateProposalOptions = {},
1129
+ ): Promise<ExportedProposal> {
1130
+ const proposalNonce = resolveProposalNonce('createSwitchGuardianProposalOffline', options);
1131
+
1132
+ // Sync with the Miden node and refresh the cached account/config before
1133
+ // building (mirrors the Rust `sync_network_only`): with no GUARDIAN push
1134
+ // to reject a stale delta at creation, a summary built from stale local
1135
+ // state — or a readiness threshold read from stale config — would only
1136
+ // fail at execution, after the whole side-channel cosigning ceremony.
1137
+ await this.syncNetworkOnly();
1138
+
1139
+ const { summaryBase64, metadata } = await this.buildSwitchGuardianSummary(
1140
+ newGuardianEndpoint,
1141
+ newGuardianPubkey,
1142
+ );
1143
+
1144
+ const exported: ExportedProposal = {
1145
+ accountId: this._accountId,
1146
+ nonce: proposalNonce,
1147
+ commitment: computeCommitmentFromTxSummary(summaryBase64),
1148
+ txSummaryBase64: summaryBase64,
1149
+ signatures: [],
1150
+ metadata,
1151
+ };
1152
+
1153
+ // Reuse the cosigner-side machinery end to end: importProposal validates
1154
+ // and caches exactly as it would on a cosigner's client, and
1155
+ // signProposalOffline adds the proposer's signature and re-exports.
1156
+ const proposal = await this.importProposal(JSON.stringify(exported));
1157
+ const signedJson = await this.signProposalOffline(proposal.id);
1158
+ return JSON.parse(signedJson) as ExportedProposal;
955
1159
  }
956
1160
 
957
1161
  /**
@@ -1231,6 +1435,218 @@ export class Multisig {
1231
1435
  return this.importNoteFromBytes(noteBytes);
1232
1436
  }
1233
1437
 
1438
+ /**
1439
+ * Proposal-import strategy of {@link recoverNotes}: import the notes
1440
+ * embedded in the given v2 consume-notes proposals into the local Miden
1441
+ * store, reusing this client's Miden RPC endpoint and retry
1442
+ * configuration.
1443
+ */
1444
+ private async importNotesFromProposals(
1445
+ proposals: ReadonlyArray<Pick<Proposal, 'id' | 'metadata'>>,
1446
+ cancelled?: () => boolean,
1447
+ ): Promise<NoteImportOutcome[]> {
1448
+ return importNotesFromProposalsStandalone(this.midenClient, proposals, {
1449
+ midenRpcEndpoint: this.getMidenRpcEndpoint(),
1450
+ rpc: { retry: { maxAttempts: this.rpcConfig.maxAttempts } },
1451
+ cancelled,
1452
+ });
1453
+ }
1454
+
1455
+ /**
1456
+ * Public-backfill strategy of {@link recoverNotes}: scan a historical
1457
+ * block range for public notes addressed at this account's standard note
1458
+ * tag and import them with their on-chain inclusion proofs, reusing this
1459
+ * client's Miden RPC endpoint and retry configuration.
1460
+ */
1461
+ private async backfillPublicNotesByTag(
1462
+ options: { fromBlock?: number; toBlock?: number } = {},
1463
+ ): Promise<PublicBackfillReport> {
1464
+ return backfillPublicNotesByTagStandalone(this.midenClient, {
1465
+ accountId: this._accountId,
1466
+ midenRpcEndpoint: this.getMidenRpcEndpoint(),
1467
+ rpc: { retry: { maxAttempts: this.rpcConfig.maxAttempts } },
1468
+ ...(options.fromBlock !== undefined ? { fromBlock: options.fromBlock } : {}),
1469
+ ...(options.toBlock !== undefined ? { toBlock: options.toBlock } : {}),
1470
+ });
1471
+ }
1472
+
1473
+ /**
1474
+ * Run the note-recovery strategies as a single wallet-facing flow,
1475
+ * typically right after key-based recovery loaded the account — the TS
1476
+ * counterpart of the Rust SDK's `MultisigClient::recover_notes`.
1477
+ *
1478
+ * By default every strategy runs — the private-note transport backlog
1479
+ * drain, the proposal-embedded note import, and the historical
1480
+ * public-note backfill over the whole chain — followed by a normal sync
1481
+ * (chain sync plus GUARDIAN state sync) that verifies whatever was
1482
+ * imported. Pass {@link RecoverNotesOptions} to choose strategies, bound
1483
+ * the backfill's block range, or skip the final sync.
1484
+ *
1485
+ * No strategy failure aborts the flow: each primitive already reports
1486
+ * per-note and per-source problems instead of throwing, and a strategy
1487
+ * that cannot run at all (GUARDIAN unreachable while listing proposals,
1488
+ * chain tip unresolvable, a broken local store) becomes a
1489
+ * `RecoveryStepProblem` entry in the report while the remaining
1490
+ * strategies still run. The flow is idempotent — rerunning re-imports
1491
+ * nothing that already arrived — so a report with `retryable: true` can
1492
+ * simply be retried. Throws only for an inverted backfill range.
1493
+ */
1494
+ async recoverNotes(options: RecoverNotesOptions = {}): Promise<NoteRecoveryReport> {
1495
+ return runNoteRecovery(options, this.buildNoteRecoverySteps(options));
1496
+ }
1497
+
1498
+ /**
1499
+ * The strategy implementations {@link recoverNotes} hands to
1500
+ * `runNoteRecovery`. The optional `cancelled` token is the pre-switch
1501
+ * import's cooperative cancellation: `runNoteRecovery` checks it before
1502
+ * starting each step, and the proposal-import step threads it into its
1503
+ * listing and import loops so they stop at their next checkpoint too.
1504
+ */
1505
+ private buildNoteRecoverySteps(
1506
+ options: RecoverNotesOptions,
1507
+ cancelled?: () => boolean,
1508
+ ): NoteRecoverySteps {
1509
+ return {
1510
+ transportDrain: () => drainPrivateNoteBacklog(this.midenClient),
1511
+ proposalImport: async () => {
1512
+ // The lenient listing isolates per-proposal parse/binding failures
1513
+ // as skip reasons, so one corrupt proposal cannot block recovering
1514
+ // notes from the healthy ones; those skips surface as `invalid`
1515
+ // outcomes alongside the per-note ones.
1516
+ const { proposals, skipped } = await this.syncProposalsIsolatingFailures(cancelled);
1517
+ const outcomes: NoteImportOutcome[] = skipped.map(({ identifier, reason }) => ({
1518
+ identifier,
1519
+ source: 'proposal',
1520
+ status: 'invalid',
1521
+ reason,
1522
+ }));
1523
+ throwIfCancelled(cancelled);
1524
+ outcomes.push(...(await this.importNotesFromProposals(proposals, cancelled)));
1525
+ return outcomes;
1526
+ },
1527
+ publicBackfill: async () => {
1528
+ // Importing a proof into a store that has never seen the chain
1529
+ // fails, and neither key-based recovery nor `load()` syncs on its
1530
+ // own — so sync the chain state first. Incremental, so cheap when
1531
+ // the store is already synced.
1532
+ try {
1533
+ await this.midenClient.syncChain();
1534
+ } catch (error) {
1535
+ throw new Error(
1536
+ `failed to sync the chain state the backfill imports against: ${
1537
+ error instanceof Error ? error.message : String(error)
1538
+ }`,
1539
+ );
1540
+ }
1541
+ return this.backfillPublicNotesByTag({
1542
+ ...(options.fromBlock !== undefined ? { fromBlock: options.fromBlock } : {}),
1543
+ ...(options.toBlock !== undefined ? { toBlock: options.toBlock } : {}),
1544
+ });
1545
+ },
1546
+ sync: async () => {
1547
+ // Parity with the Rust flow's `sync()`: the transport fetch plus the
1548
+ // chain sync (`MidenClient.sync()` runs both, fail-fast), then the
1549
+ // GUARDIAN state sync.
1550
+ await this.midenClient.sync();
1551
+ await this.syncState();
1552
+ },
1553
+ };
1554
+ }
1555
+
1556
+ /**
1557
+ * Import the notes embedded in the old GUARDIAN's pending consume-notes
1558
+ * proposals while they are still reachable: pending proposals do not
1559
+ * survive a guardian switch, making them the one recovery source
1560
+ * {@link recoverNotes} loses once the client repoints (issue #417).
1561
+ *
1562
+ * Run automatically by {@link executeProposal} on the switch path, before
1563
+ * the switch transaction executes; call it yourself before repointing a
1564
+ * client by hand via {@link setGuardianClient}. Best-effort by contract:
1565
+ * problems are warned, never thrown, and the flow runs under a timeout
1566
+ * with cooperative cancellation plus a bounded settle grace, so a hung
1567
+ * old GUARDIAN can neither block the switch nor overlap it on the shared
1568
+ * client. Returns the recovery report, or `undefined` when the flow could
1569
+ * not run or timed out. Mirror of the Rust SDK's
1570
+ * `preserve_pre_switch_proposal_notes`; full rationale and semantics in
1571
+ * "Preserving Notes Across a Guardian Switch" (docs/MULTISIG_SDK.md).
1572
+ */
1573
+ async preservePreSwitchProposalNotes(): Promise<NoteRecoveryReport | undefined> {
1574
+ let timer: ReturnType<typeof setTimeout> | undefined;
1575
+ let graceTimer: ReturnType<typeof setTimeout> | undefined;
1576
+ let timedOut = false;
1577
+ try {
1578
+ const cancelled = () => timedOut;
1579
+ const flow = runNoteRecovery(
1580
+ GUARDIAN_SWITCH_RECOVERY_OPTIONS,
1581
+ this.buildNoteRecoverySteps(GUARDIAN_SWITCH_RECOVERY_OPTIONS, cancelled),
1582
+ cancelled,
1583
+ );
1584
+ const outcome = await Promise.race([
1585
+ flow,
1586
+ new Promise<typeof PRE_SWITCH_IMPORT_TIMED_OUT>((resolve) => {
1587
+ timer = setTimeout(() => {
1588
+ timedOut = true;
1589
+ resolve(PRE_SWITCH_IMPORT_TIMED_OUT);
1590
+ }, PRE_SWITCH_IMPORT_TIMEOUT_MS);
1591
+ }),
1592
+ ]);
1593
+ if (outcome === PRE_SWITCH_IMPORT_TIMED_OUT) {
1594
+ console.warn(
1595
+ `Pre-switch proposal-note import timed out after ${PRE_SWITCH_IMPORT_TIMEOUT_MS}ms ` +
1596
+ 'and was cancelled; notes embedded in pending proposals may be ' +
1597
+ 'unrecoverable after the GUARDIAN switch',
1598
+ );
1599
+ // The token stops all new work; give the one uninterruptible
1600
+ // in-flight operation a bounded grace to settle. The result is
1601
+ // unobserved, and a rejection must not go unhandled.
1602
+ await Promise.race([
1603
+ flow.catch(() => {}),
1604
+ new Promise<void>((resolve) => {
1605
+ graceTimer = setTimeout(resolve, PRE_SWITCH_SETTLE_GRACE_MS);
1606
+ }),
1607
+ ]);
1608
+ return undefined;
1609
+ }
1610
+ for (const problem of outcome.problems) {
1611
+ console.warn(
1612
+ `Pre-switch proposal-note import step '${problem.step}' did not finish; ` +
1613
+ 'notes embedded in pending proposals may be unrecoverable after the ' +
1614
+ 'GUARDIAN switch',
1615
+ problem.reason,
1616
+ );
1617
+ }
1618
+ // Per-note failures never reach `problems` — and this is the last
1619
+ // moment the notes are reachable, so "retryable" cannot help: the
1620
+ // source is gone once the client repoints. They must be observable now.
1621
+ for (const noteOutcome of outcome.proposalImport ?? []) {
1622
+ if (noteOutcome.status === 'invalid' || noteOutcome.status === 'failed') {
1623
+ console.warn(
1624
+ `Pre-switch import could not preserve embedded note ${noteOutcome.identifier} ` +
1625
+ `(${noteOutcome.status}); it may be unrecoverable after the GUARDIAN switch`,
1626
+ noteOutcome.reason,
1627
+ );
1628
+ }
1629
+ }
1630
+ return outcome;
1631
+ } catch (error) {
1632
+ // runNoteRecovery only throws for caller errors (inverted backfill
1633
+ // range, never passed here), but the switch must survive anything.
1634
+ console.warn(
1635
+ 'Pre-switch proposal-note import could not run; notes embedded in pending ' +
1636
+ 'proposals may be unrecoverable after the GUARDIAN switch',
1637
+ error,
1638
+ );
1639
+ return undefined;
1640
+ } finally {
1641
+ if (timer !== undefined) {
1642
+ clearTimeout(timer);
1643
+ }
1644
+ if (graceTimer !== undefined) {
1645
+ clearTimeout(graceTimer);
1646
+ }
1647
+ }
1648
+ }
1649
+
1234
1650
  /**
1235
1651
  * Compute the ID of the note a P2ID proposal will create when executed.
1236
1652
  *
@@ -1253,6 +1669,12 @@ export class Multisig {
1253
1669
  throw new Error('getP2idNoteId requires a P2ID proposal with recipient, faucet, amount, and salt metadata');
1254
1670
  }
1255
1671
 
1672
+ // Validate before deriving. Unlike the rebuild paths there is no commitment check
1673
+ // downstream of this note id -- it goes straight out to a recipient — so a salt that
1674
+ // silently padded to the zero word would produce a plausible id for a note that does
1675
+ // not exist, with nothing to catch it.
1676
+ this.requireProposalSaltHex(proposal.id, metadata);
1677
+
1256
1678
  const note = buildP2idNoteFromMetadata(
1257
1679
  this._accountId,
1258
1680
  metadata.recipientId,
@@ -1379,6 +1801,13 @@ export class Multisig {
1379
1801
  async executeProposal(proposalId: string): Promise<void> {
1380
1802
  const { metadata, finalRequest, proposal } = await this.prepareProposalExecution(proposalId);
1381
1803
 
1804
+ if (metadata.proposalType === 'switch_guardian') {
1805
+ // #417: import notes embedded in pending proposals from the old
1806
+ // GUARDIAN. Must run before the switch executes and repoints;
1807
+ // best-effort and bounded — see preservePreSwitchProposalNotes.
1808
+ await this.preservePreSwitchProposalNotes();
1809
+ }
1810
+
1382
1811
  // Execute at the proposal's anchored reference block, so the summary the
1383
1812
  // cosigners signed reproduces exactly. The anchor was already checked
1384
1813
  // against the summary's block commitment during binding verification.
@@ -1421,13 +1850,7 @@ export class Multisig {
1421
1850
  }
1422
1851
 
1423
1852
  try {
1424
- const webClient = await this.getRawClient();
1425
- await retryRpcRead(() => webClient.syncState(), this.rpcConfig);
1426
-
1427
- const updatedAccount = await retryRpcRead(
1428
- () => webClient.getAccount(accountId),
1429
- this.rpcConfig,
1430
- );
1853
+ const updatedAccount = await this.syncNetworkOnly();
1431
1854
  if (!updatedAccount) {
1432
1855
  throw new Error(
1433
1856
  `Updated account ${this._accountId} is missing from local client`
@@ -1773,9 +2196,23 @@ export class Multisig {
1773
2196
 
1774
2197
  const txSummaryBytes = base64ToUint8Array(txSummaryBase64);
1775
2198
  const txSummary = TransactionSummary.deserialize(txSummaryBytes);
1776
- const saltHex = summarySalt(txSummary).toHex();
2199
+ const saltHex = this.requireProposalSaltHex(proposalId, metadata);
1777
2200
  const txCommitmentHex = txSummary.toCommitment().toHex();
1778
2201
  const normalizedTxCommitmentHex = normalizeHexWord(txCommitmentHex);
2202
+
2203
+ // This summary was re-fetched from GUARDIAN, not taken from the verified proposal:
2204
+ // `ensureProposalCommitmentMatchesSummary` pins the CACHED `proposal.txSummary` to the
2205
+ // id, and nothing pinned this one. It goes on to key the advice map and drive the
2206
+ // rebuild, so an unrelated summary served here would have signatures collected against
2207
+ // one transaction and advice assembled for another. On an ECDSA roster the
2208
+ // recoverability check would notice; on a Falcon roster nothing else compares them.
2209
+ if (normalizedTxCommitmentHex !== normalizeHexWord(proposalId)) {
2210
+ throw new Error(
2211
+ `Proposal ${proposalId} tx_summary commitment ${normalizedTxCommitmentHex} ` +
2212
+ 'does not match the proposal id it belongs to',
2213
+ );
2214
+ }
2215
+
1779
2216
  const normalizedSignerCommitments = new Set(
1780
2217
  this.signerCommitments.map((commitment) => normalizeHexWord(commitment)),
1781
2218
  );
@@ -1880,12 +2317,15 @@ export class Multisig {
1880
2317
  await this.verifyGuardianEndpointCommitment(metadata.newGuardianEndpoint, metadata.newGuardianPubkey);
1881
2318
  }
1882
2319
 
2320
+ // The builders read `.toHex()` and allocate their own Word, so this handle stays
2321
+ // ours; without the release it leaks once per execute.
1883
2322
  const executionSalt = Word.fromHex(normalizeHexWord(saltHex));
1884
- const finalRequest = await this.buildTransactionRequestFromMetadata(
1885
- metadata,
1886
- executionSalt,
1887
- adviceMap,
1888
- );
2323
+ let finalRequest;
2324
+ try {
2325
+ finalRequest = await this.buildTransactionRequestFromMetadata(metadata, executionSalt, adviceMap);
2326
+ } finally {
2327
+ executionSalt.free?.();
2328
+ }
1889
2329
 
1890
2330
  return { finalRequest, metadata, proposal };
1891
2331
  }
@@ -2085,9 +2525,9 @@ export class Multisig {
2085
2525
  return txSummaryCommitment;
2086
2526
  }
2087
2527
 
2088
- const salt = proposal.metadata.saltHex
2089
- ? Word.fromHex(normalizeHexWord(proposal.metadata.saltHex))
2090
- : summarySalt(summary);
2528
+ const salt = Word.fromHex(
2529
+ normalizeHexWord(this.requireProposalSaltHex(proposal.id, proposal.metadata)),
2530
+ );
2091
2531
 
2092
2532
  const request = await this.buildTransactionRequestFromMetadata(proposal.metadata, salt);
2093
2533
  const webClient = await this.getRawClient();
@@ -2110,6 +2550,59 @@ export class Multisig {
2110
2550
  * cannot be reproduced, verified, or executed. The caller owns the returned
2111
2551
  * anchor and must `free()` it once done.
2112
2552
  */
2553
+ /**
2554
+ * Reads a proposal's salt. Throws when absent, because there is nothing to fall
2555
+ * back to.
2556
+ *
2557
+ * The request declares this salt through `withFeeConversionSalt`, and miden-client
2558
+ * commits `hash(CONVERSION_INFO || SALT)` into the auth arg from it. The summary
2559
+ * therefore carries the COMMITMENT, and a commitment is not invertible to the salt
2560
+ * it was built from -- so `summaryAuthArg(summary)` cannot stand in here. It used
2561
+ * to: before the request declared a salt the auth arg WAS the bare salt, which is
2562
+ * why the fallback this replaces was correct when it was written.
2563
+ *
2564
+ * A declared salt also bypasses miden-client's zero-fee early return, so this holds
2565
+ * on a chain that charges nothing exactly as on one that charges.
2566
+ */
2567
+ private requireProposalSaltHex(proposalId: string, metadata: ProposalMetadata): string {
2568
+ const saltHex: unknown = metadata.saltHex;
2569
+
2570
+ if (saltHex === undefined || saltHex === null || saltHex === '') {
2571
+ throw new Error(
2572
+ `Proposal ${proposalId} has no salt; its request cannot be rebuilt because ` +
2573
+ 'the auth arg commits hash(CONVERSION_INFO || SALT) and is not invertible ' +
2574
+ 'to the salt',
2575
+ );
2576
+ }
2577
+
2578
+ // GUARDIAN serves this field and the response is cast, not parsed, so everything
2579
+ // below is untrusted input. A truthiness test is not enough: `normalizeHexWord`
2580
+ // left-pads, so `'0x'` and `'0X'` are truthy and pad to the ZERO word -- a salt
2581
+ // nobody chose, which rebuilds a different request and reports itself as a summary
2582
+ // mismatch. A non-string throws out of the hex helpers instead, and an unbounded
2583
+ // string is a logging hazard the error type already guards against.
2584
+ if (typeof saltHex !== 'string') {
2585
+ throw new ProposalSaltMalformedError({
2586
+ proposalId,
2587
+ saltHex,
2588
+ reason: `expected a hex string, got ${typeof saltHex}`,
2589
+ });
2590
+ }
2591
+ const digits = saltHex.replace(/^0[xX]/, '');
2592
+ if (digits.length === 0 || digits.length > MAX_SALT_HEX_DIGITS || !/^[0-9a-fA-F]+$/.test(digits)) {
2593
+ throw new ProposalSaltMalformedError({
2594
+ proposalId,
2595
+ saltHex,
2596
+ reason:
2597
+ digits.length === 0
2598
+ ? 'a hex prefix with no digits is the zero word, not a salt'
2599
+ : `expected 1 to ${MAX_SALT_HEX_DIGITS} hex digits`,
2600
+ });
2601
+ }
2602
+
2603
+ return saltHex;
2604
+ }
2605
+
2113
2606
  private requireProposalAnchor(proposalId: string, metadata: ProposalMetadata): ChainAnchor {
2114
2607
  if (!metadata.chainAnchor) {
2115
2608
  throw new Error(
package/src/procedures.ts CHANGED
@@ -13,7 +13,7 @@
13
13
  export const PROCEDURE_ROOTS = {
14
14
  update_signers: '0xa261cfd3c8791ac5abe1e78e14eade2f20789d73ab1c23c430418de59bc3380e',
15
15
  update_procedure_threshold: '0x97587c61d49313b1d5a3c8b7437e0080e67ed9bd9d3e7206bcae562f934ccd03',
16
- auth_tx: '0xa6aa6f69d9358535272ba433cd48d20628a5c69598e00c6dd01a22e83a5f15df',
16
+ auth_tx: '0x43fb07d62ed26993b7b13c7b411db62c5b5acffa2813e989608c41a72d7185ec',
17
17
  update_guardian: '0x0a614ff7c81a561cbd2a4c2d9482031a7a841ca5de33349daed23a9d871b3675',
18
18
  send_asset: '0x595bc83258726a66bd904912cfd5186c07cbd902dfbc115b7d6bc8105efc57e3',
19
19
  receive_asset: '0x34a56dd18f6fe5aab63198b9dcfc6467e793ebabb37d56b994b902504635da13',