@openzeppelin/miden-multisig-client 0.18.0-rc.1 → 0.18.0-rc.3

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 (106) hide show
  1. package/README.md +71 -17
  2. package/dist/account/builder.d.ts +3 -4
  3. package/dist/account/builder.d.ts.map +1 -1
  4. package/dist/account/builder.js +6 -13
  5. package/dist/account/builder.js.map +1 -1
  6. package/dist/client.d.ts +11 -4
  7. package/dist/client.d.ts.map +1 -1
  8. package/dist/client.js +8 -3
  9. package/dist/client.js.map +1 -1
  10. package/dist/config.d.ts +3 -0
  11. package/dist/config.d.ts.map +1 -0
  12. package/dist/config.js +14 -0
  13. package/dist/config.js.map +1 -0
  14. package/dist/index.d.ts +5 -5
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +3 -3
  17. package/dist/index.js.map +1 -1
  18. package/dist/multisig/authArgErrors.d.ts +15 -1
  19. package/dist/multisig/authArgErrors.d.ts.map +1 -1
  20. package/dist/multisig/authArgErrors.js +20 -0
  21. package/dist/multisig/authArgErrors.js.map +1 -1
  22. package/dist/multisig.d.ts +76 -25
  23. package/dist/multisig.d.ts.map +1 -1
  24. package/dist/multisig.js +196 -156
  25. package/dist/multisig.js.map +1 -1
  26. package/dist/procedures.d.ts +5 -5
  27. package/dist/procedures.js +5 -5
  28. package/dist/prover/workflow.d.ts +7 -3
  29. package/dist/prover/workflow.d.ts.map +1 -1
  30. package/dist/prover/workflow.js +7 -5
  31. package/dist/prover/workflow.js.map +1 -1
  32. package/dist/recovery/proposalNoteImport.d.ts +5 -6
  33. package/dist/recovery/proposalNoteImport.d.ts.map +1 -1
  34. package/dist/recovery/proposalNoteImport.js +12 -13
  35. package/dist/recovery/proposalNoteImport.js.map +1 -1
  36. package/dist/recovery/publicNoteBackfill.d.ts +2 -3
  37. package/dist/recovery/publicNoteBackfill.d.ts.map +1 -1
  38. package/dist/recovery/publicNoteBackfill.js +4 -5
  39. package/dist/recovery/publicNoteBackfill.js.map +1 -1
  40. package/dist/transaction/authArgs.d.ts +2 -3
  41. package/dist/transaction/authArgs.d.ts.map +1 -1
  42. package/dist/transaction/authArgs.js +7 -14
  43. package/dist/transaction/authArgs.js.map +1 -1
  44. package/dist/transaction/consumeNotes.d.ts +4 -12
  45. package/dist/transaction/consumeNotes.d.ts.map +1 -1
  46. package/dist/transaction/consumeNotes.js +10 -4
  47. package/dist/transaction/consumeNotes.js.map +1 -1
  48. package/dist/transaction/noteAuthentication.d.ts +2 -3
  49. package/dist/transaction/noteAuthentication.d.ts.map +1 -1
  50. package/dist/transaction/noteAuthentication.js +11 -11
  51. package/dist/transaction/noteAuthentication.js.map +1 -1
  52. package/dist/transaction/options.d.ts +0 -7
  53. package/dist/transaction/options.d.ts.map +1 -1
  54. package/dist/transaction/p2id.d.ts +2 -3
  55. package/dist/transaction/p2id.d.ts.map +1 -1
  56. package/dist/transaction/p2id.js.map +1 -1
  57. package/dist/transaction/summary.d.ts +94 -22
  58. package/dist/transaction/summary.d.ts.map +1 -1
  59. package/dist/transaction/summary.js +146 -12
  60. package/dist/transaction/summary.js.map +1 -1
  61. package/dist/transaction/updateGuardian.d.ts +3 -7
  62. package/dist/transaction/updateGuardian.d.ts.map +1 -1
  63. package/dist/transaction/updateGuardian.js +3 -4
  64. package/dist/transaction/updateGuardian.js.map +1 -1
  65. package/dist/transaction/updateProcedureThreshold.d.ts +3 -8
  66. package/dist/transaction/updateProcedureThreshold.d.ts.map +1 -1
  67. package/dist/transaction/updateProcedureThreshold.js +3 -4
  68. package/dist/transaction/updateProcedureThreshold.js.map +1 -1
  69. package/dist/transaction/updateSigners.d.ts +3 -8
  70. package/dist/transaction/updateSigners.d.ts.map +1 -1
  71. package/dist/transaction/updateSigners.js +3 -4
  72. package/dist/transaction/updateSigners.js.map +1 -1
  73. package/dist/transaction.d.ts +1 -1
  74. package/dist/transaction.d.ts.map +1 -1
  75. package/dist/transaction.js +1 -1
  76. package/dist/transaction.js.map +1 -1
  77. package/dist/types/proposal.d.ts +9 -8
  78. package/dist/types/proposal.d.ts.map +1 -1
  79. package/dist/types/proposal.js.map +1 -1
  80. package/package.json +4 -4
  81. package/src/account/builder.ts +6 -13
  82. package/src/client.ts +13 -10
  83. package/src/config.ts +14 -0
  84. package/src/index.ts +5 -3
  85. package/src/multisig/authArgErrors.ts +28 -1
  86. package/src/multisig.ts +238 -194
  87. package/src/procedures.ts +5 -5
  88. package/src/prover/workflow.ts +7 -10
  89. package/src/recovery/proposalNoteImport.ts +13 -15
  90. package/src/recovery/publicNoteBackfill.ts +6 -6
  91. package/src/transaction/authArgs.ts +15 -21
  92. package/src/transaction/consumeNotes.ts +6 -34
  93. package/src/transaction/noteAuthentication.ts +18 -13
  94. package/src/transaction/options.ts +0 -9
  95. package/src/transaction/p2id.ts +2 -3
  96. package/src/transaction/summary.ts +160 -57
  97. package/src/transaction/updateGuardian.ts +5 -23
  98. package/src/transaction/updateProcedureThreshold.ts +5 -25
  99. package/src/transaction/updateSigners.ts +5 -22
  100. package/src/transaction.ts +6 -0
  101. package/src/types/proposal.ts +9 -8
  102. package/dist/raw-client.d.ts +0 -14
  103. package/dist/raw-client.d.ts.map +0 -1
  104. package/dist/raw-client.js +0 -60
  105. package/dist/raw-client.js.map +0 -1
  106. package/src/raw-client.ts +0 -89
package/dist/multisig.js CHANGED
@@ -7,7 +7,7 @@
7
7
  import { GuardianHttpClient } from '@openzeppelin/guardian-client';
8
8
  import { ProposalSaltMalformedError } from './multisig/authArgErrors.js';
9
9
  import { Account, AccountId, AdviceMap, FeltArray, Note, NoteExportFormat, NoteFile, Signature, TransactionRequest, TransactionSummary, Word, } from '@miden-sdk/miden-sdk';
10
- import { chainAnchorFromBase64, chainAnchorToBase64, executeForSummary, executeForSummaryAt, summaryApprovalExpirationBlockNum, summarySalt, buildUpdateSignersTransactionRequest, buildUpdateProcedureThresholdTransactionRequest, buildUpdateGuardianTransactionRequest, buildConsumeNotesTransactionRequest, buildP2idNoteFromMetadata, buildP2idTransactionRequest, parseP2idNoteType, p2idNoteTypeToMetadata, } from './transaction.js';
10
+ import { chainAnchorFromBase64, chainAnchorToBase64, executeForSummary, executeForSummaryAtTip, isStaleChainError, prepareTipExecution, syncToBoundBlock, summaryApprovalExpirationBlockNum, summarySalt, buildUpdateSignersTransactionRequest, buildUpdateProcedureThresholdTransactionRequest, buildUpdateGuardianTransactionRequest, buildConsumeNotesTransactionRequest, buildP2idNoteFromMetadata, buildP2idTransactionRequest, parseP2idNoteType, p2idNoteTypeToMetadata, } from './transaction.js';
11
11
  import { buildConsumeNotesTransactionRequestFromNotes } from './transaction/consumeNotes.js';
12
12
  import { validateMultisigConfig } from './account/builder.js';
13
13
  import { ensureNotesAuthenticated } from './transaction/noteAuthentication.js';
@@ -27,7 +27,7 @@ import { importNotesFromProposals as importNotesFromProposalsStandalone, throwIf
27
27
  import { backfillPublicNotesByTag as backfillPublicNotesByTagStandalone, } from './recovery/publicNoteBackfill.js';
28
28
  import { drainPrivateNoteBacklog } from './recovery/transportDrain.js';
29
29
  import { GUARDIAN_SWITCH_RECOVERY_OPTIONS, runNoteRecovery, } from './recovery/recoverNotes.js';
30
- import { getRawMidenClient, getTransactionProver, requireMidenRpcEndpoint, } from './raw-client.js';
30
+ import { requireMidenRpcEndpoint } from './config.js';
31
31
  import { resolveProverConfig, } from './prover/config.js';
32
32
  import { ProverWorkflow } from './prover/workflow.js';
33
33
  import { resolveRpcConfig, } from './rpc/config.js';
@@ -141,7 +141,6 @@ export class Multisig {
141
141
  guardian;
142
142
  signer;
143
143
  midenClient;
144
- rawClientPromise;
145
144
  proverWorkflow;
146
145
  rpcConfig;
147
146
  _accountId;
@@ -177,16 +176,12 @@ export class Multisig {
177
176
  this.midenClient = midenClient;
178
177
  this._accountId = accountId ?? (account ? accountIdToHex(account) : '');
179
178
  this.midenRpcEndpoint = requireMidenRpcEndpoint(midenRpcEndpoint);
180
- this.rawClientPromise = getRawMidenClient(midenClient, this.midenRpcEndpoint);
181
- this.proverWorkflow = new ProverWorkflow(this.midenClient, proverConfig ?? resolveProverConfig(undefined, getTransactionProver(midenClient)));
179
+ this.proverWorkflow = new ProverWorkflow(this.midenClient, proverConfig ?? resolveProverConfig(undefined, midenClient.defaultProver));
182
180
  this.rpcConfig = rpcConfig ?? resolveRpcConfig(undefined);
183
181
  }
184
182
  getMidenRpcEndpoint() {
185
183
  return this.midenRpcEndpoint;
186
184
  }
187
- async getRawClient() {
188
- return this.rawClientPromise;
189
- }
190
185
  proposalFactory() {
191
186
  return new ProposalFactory({
192
187
  accountId: this._accountId,
@@ -215,7 +210,7 @@ export class Multisig {
215
210
  return this.signer.commitment;
216
211
  }
217
212
  /**
218
- * Resolve the account from the web client's store, falling back to the
213
+ * Resolve the account from the Miden client's store, falling back to the
219
214
  * `account` snapshot when the store has no record.
220
215
  *
221
216
  * Transaction execution reads the store, and other flows (e.g. consume-notes
@@ -223,9 +218,10 @@ export class Multisig {
223
218
  * source from the store to see the same state execution will.
224
219
  */
225
220
  async getStoreAccount() {
226
- const webClient = await this.getRawClient();
227
- const stored = await retryRpcRead(() => webClient.getAccount(AccountId.fromHex(this._accountId)), this.rpcConfig);
228
- return stored ?? this.account;
221
+ return (await this.readStoreAccount()) ?? this.account;
222
+ }
223
+ readStoreAccount() {
224
+ return retryRpcRead(() => this.midenClient.accounts.get(this._accountId), this.rpcConfig);
229
225
  }
230
226
  /**
231
227
  * Read the current ordered signer public-key commitments from account
@@ -375,9 +371,16 @@ export class Multisig {
375
371
  /**
376
372
  * Sync account state from GUARDIAN into the local Miden client store.
377
373
  *
378
- * If the GUARDIAN commitment differs from the local commitment (or the account
379
- * is missing locally) and the GUARDIAN state is safe to import, the local store
380
- * is overwritten with the GUARDIAN state. When the GUARDIAN is merely *behind*
374
+ * Starts with the canonical-nonce pre-check (OpenZeppelin/guardian#191):
375
+ * GUARDIAN reports the nonce and commitment of its canonical state, and
376
+ * when that nonce is not above the local account's — at a matching
377
+ * commitment when equal — nothing newer exists to pull, so the state
378
+ * fetch is skipped and the local account stands (`source: 'local'`).
379
+ *
380
+ * Otherwise the state is fetched (`source: 'guardian'`). If the GUARDIAN
381
+ * commitment differs from the local commitment (or the account is missing
382
+ * locally) and the GUARDIAN state is safe to import, the local store is
383
+ * overwritten with the GUARDIAN state. When the GUARDIAN is merely *behind*
381
384
  * local — e.g. the pushed execution delta has not been canonicalized yet
382
385
  * (see OpenZeppelin/guardian#316) — the local state is already ahead and
383
386
  * on-chain-verifiable, so it is kept as authoritative. Either way, config is
@@ -385,11 +388,18 @@ export class Multisig {
385
388
  * (e.g. the UI) observe the current state instead of a stale snapshot.
386
389
  */
387
390
  async syncState() {
391
+ const localAccount = await this.readStoreAccount();
392
+ if (localAccount) {
393
+ const head = await this.guardian.getCanonicalNonce(this._accountId);
394
+ const localNonce = localAccount.nonce().asInt();
395
+ const guardianNonce = BigInt(head.nonce);
396
+ if (this.guardianIsNotAhead(localAccount, localNonce, head.commitment, guardianNonce)) {
397
+ this.refreshConfigFromAccount(localAccount);
398
+ return { source: 'local', localNonce, guardianNonce };
399
+ }
400
+ }
388
401
  const state = await this.fetchState();
389
- const accountId = AccountId.fromHex(this._accountId);
390
- const webClient = await this.getRawClient();
391
- const localAccount = await retryRpcRead(() => webClient.getAccount(accountId), this.rpcConfig);
392
- let accountForConfigRefresh = localAccount ?? null;
402
+ let accountForConfigRefresh = localAccount;
393
403
  const guardianCommitment = normalizeHexWord(state.commitment);
394
404
  const localCommitment = localAccount
395
405
  ? normalizeHexWord(localAccount.to_commitment().toHex())
@@ -397,18 +407,33 @@ export class Multisig {
397
407
  if (!localAccount || localCommitment !== guardianCommitment) {
398
408
  const accountBytes = base64ToUint8Array(state.stateDataBase64);
399
409
  const incomingAccount = Account.deserialize(accountBytes);
400
- if (await this.isSafeToOverwriteLocalState(incomingAccount, localAccount)) {
401
- await webClient.newAccount(incomingAccount, true);
410
+ if (await this.isSafeToOverwriteLocalState(incomingAccount, localAccount ?? undefined)) {
411
+ await this.midenClient.accounts.insert({ account: incomingAccount, overwrite: true });
402
412
  accountForConfigRefresh = incomingAccount;
403
413
  }
404
414
  }
405
415
  this.refreshConfigFromAccount(accountForConfigRefresh);
406
- return state;
416
+ return { source: 'guardian', state };
417
+ }
418
+ /**
419
+ * The pre-check decision: GUARDIAN has nothing newer when its canonical
420
+ * nonce is below the local nonce, or equal to it at the same commitment.
421
+ * An equal nonce with a different commitment is divergence, which the
422
+ * full fetch reports (see `isSafeToOverwriteLocalState`).
423
+ */
424
+ guardianIsNotAhead(localAccount, localNonce, guardianCommitment, guardianNonce) {
425
+ if (guardianNonce < localNonce) {
426
+ return true;
427
+ }
428
+ if (guardianNonce > localNonce) {
429
+ return false;
430
+ }
431
+ return (normalizeHexWord(guardianCommitment) ===
432
+ normalizeHexWord(localAccount.to_commitment().toHex()));
407
433
  }
408
434
  async verifyStateCommitment() {
409
435
  const accountId = AccountId.fromHex(this._accountId);
410
- const webClient = await this.getRawClient();
411
- const localAccount = await retryRpcRead(() => webClient.getAccount(accountId), this.rpcConfig);
436
+ const localAccount = await this.readStoreAccount();
412
437
  if (!localAccount) {
413
438
  throw new Error(`Local account state not found for account ${this._accountId}. Sync the account before verifying.`);
414
439
  }
@@ -464,11 +489,10 @@ export class Multisig {
464
489
  * fail.
465
490
  */
466
491
  async syncNetworkOnly() {
467
- const webClient = await this.getRawClient();
468
- await retryRpcRead(() => webClient.syncState(), this.rpcConfig);
469
- const account = await retryRpcRead(() => webClient.getAccount(AccountId.fromHex(this._accountId)), this.rpcConfig);
470
- this.refreshConfigFromAccount(account ?? null);
471
- return account ?? null;
492
+ await this.syncChain();
493
+ const account = await this.readStoreAccount();
494
+ this.refreshConfigFromAccount(account);
495
+ return account;
472
496
  }
473
497
  refreshConfigFromAccount(account) {
474
498
  if (!account) {
@@ -539,11 +563,12 @@ export class Multisig {
539
563
  * Every synced proposal's metadata is checked against its signed summary
540
564
  * and the outcome is recorded in {@link Proposal.verification}. One that
541
565
  * fails is still cached and returned, so a single stale or corrupt
542
- * proposal cannot hide the others (issue #462: once the node prunes a
543
- * proposal's anchor block its re-execution fails for everyone). `failed`
544
- * with `retryable: true` means a transient node error, worth syncing
545
- * again; `retryable: false` means the proposal cannot be reproduced and
546
- * must be re-proposed. `signProposal` and `executeProposal` re-verify and
566
+ * proposal cannot hide the others (issue #462). Verification re-executes
567
+ * each proposal at the chain tip, so the Miden client is synced once first.
568
+ * `failed` with `retryable: true` means a transient node error or chain
569
+ * state this client had not caught up with, worth syncing again;
570
+ * `retryable: false` means the proposal cannot be reproduced and must be
571
+ * re-proposed. `signProposal` and `executeProposal` re-verify and
547
572
  * refuse a failed proposal. A failed proposal still counts as reported, so
548
573
  * it is pruned like any other once GUARDIAN stops listing it.
549
574
  *
@@ -581,6 +606,12 @@ export class Multisig {
581
606
  const deltas = await this.guardian.getDeltaProposals(this._accountId);
582
607
  const factory = this.proposalFactory();
583
608
  const reported = new Map();
609
+ if (deltas.length > 0) {
610
+ // Verification re-executes at the store's sync height; bring it to the
611
+ // tip once for the whole listing. Best effort: a node outage then shows
612
+ // up on each proposal as a retryable failure instead of failing the sync.
613
+ await this.syncChain().catch((error) => console.warn('Could not sync the Miden client before verifying proposals', error));
614
+ }
584
615
  for (const delta of deltas) {
585
616
  const proposalId = normalizeHexWord(computeCommitmentFromTxSummary(delta.deltaPayload.txSummary.data));
586
617
  const existingProposal = this.proposals.get(proposalId);
@@ -729,7 +760,6 @@ export class Multisig {
729
760
  */
730
761
  async createAddSignerProposal(newCommitment, options = {}, ...legacyArgs) {
731
762
  const proposalNonce = resolveProposalNonce('createAddSignerProposal', options, legacyArgs);
732
- const webClient = await this.getRawClient();
733
763
  const targetThreshold = options.newThreshold ?? this.threshold;
734
764
  const targetSignerCommitments = [...this.signerCommitments, newCommitment];
735
765
  // What `update_signers_and_threshold` rejects on-chain, and what the auth
@@ -741,8 +771,8 @@ export class Multisig {
741
771
  guardianCommitment: this.guardianCommitment,
742
772
  });
743
773
  this.warnOnOverrideDilution(targetSignerCommitments.length);
744
- const { request, salt } = await buildUpdateSignersTransactionRequest(webClient, targetThreshold, targetSignerCommitments, this.proposalRequestOptions(options));
745
- const { summary, anchor } = await executeForSummary(webClient, this._accountId, request);
774
+ const { request, salt } = await buildUpdateSignersTransactionRequest(this.midenClient, targetThreshold, targetSignerCommitments, this.proposalRequestOptions(options));
775
+ const { summary, anchor } = await executeForSummary(this.midenClient, this._accountId, request);
746
776
  const chainAnchor = chainAnchorToBase64(anchor);
747
777
  anchor.free();
748
778
  const summaryBase64 = uint8ArrayToBase64(summary.serialize());
@@ -766,7 +796,6 @@ export class Multisig {
766
796
  */
767
797
  async createRemoveSignerProposal(signerToRemove, options = {}, ...legacyArgs) {
768
798
  const proposalNonce = resolveProposalNonce('createRemoveSignerProposal', options, legacyArgs);
769
- const webClient = await this.getRawClient();
770
799
  const normalizedRemove = signerToRemove.toLowerCase();
771
800
  const targetSignerCommitments = this.signerCommitments.filter((c) => c.toLowerCase() !== normalizedRemove);
772
801
  if (targetSignerCommitments.length === this.signerCommitments.length) {
@@ -779,8 +808,8 @@ export class Multisig {
779
808
  if (targetThreshold < 1 || targetThreshold > targetSignerCommitments.length) {
780
809
  throw new Error(`Invalid threshold ${targetThreshold}. Must be between 1 and ${targetSignerCommitments.length}`);
781
810
  }
782
- const { request, salt } = await buildUpdateSignersTransactionRequest(webClient, targetThreshold, targetSignerCommitments, this.proposalRequestOptions(options));
783
- const { summary, anchor } = await executeForSummary(webClient, this._accountId, request);
811
+ const { request, salt } = await buildUpdateSignersTransactionRequest(this.midenClient, targetThreshold, targetSignerCommitments, this.proposalRequestOptions(options));
812
+ const { summary, anchor } = await executeForSummary(this.midenClient, this._accountId, request);
784
813
  const chainAnchor = chainAnchorToBase64(anchor);
785
814
  anchor.free();
786
815
  const summaryBase64 = uint8ArrayToBase64(summary.serialize());
@@ -803,15 +832,14 @@ export class Multisig {
803
832
  */
804
833
  async createChangeThresholdProposal(newThreshold, options = {}) {
805
834
  const proposalNonce = resolveProposalNonce('createChangeThresholdProposal', options);
806
- const webClient = await this.getRawClient();
807
835
  if (newThreshold < 1 || newThreshold > this.signerCommitments.length) {
808
836
  throw new Error(`Invalid threshold ${newThreshold}. Must be between 1 and ${this.signerCommitments.length}`);
809
837
  }
810
838
  if (newThreshold === this.threshold) {
811
839
  throw new Error('New threshold is the same as current threshold');
812
840
  }
813
- const { request, salt } = await buildUpdateSignersTransactionRequest(webClient, newThreshold, this.signerCommitments, this.proposalRequestOptions(options));
814
- const { summary, anchor } = await executeForSummary(webClient, this._accountId, request);
841
+ const { request, salt } = await buildUpdateSignersTransactionRequest(this.midenClient, newThreshold, this.signerCommitments, this.proposalRequestOptions(options));
842
+ const { summary, anchor } = await executeForSummary(this.midenClient, this._accountId, request);
815
843
  const chainAnchor = chainAnchorToBase64(anchor);
816
844
  anchor.free();
817
845
  const summaryBase64 = uint8ArrayToBase64(summary.serialize());
@@ -828,7 +856,6 @@ export class Multisig {
828
856
  }
829
857
  async createUpdateProcedureThresholdProposal(targetProcedure, targetThreshold, options = {}) {
830
858
  const proposalNonce = resolveProposalNonce('createUpdateProcedureThresholdProposal', options);
831
- const webClient = await this.getRawClient();
832
859
  if (targetThreshold < 0 || targetThreshold > this.signerCommitments.length) {
833
860
  throw new Error(`Invalid threshold ${targetThreshold}. Must be between 0 and ${this.signerCommitments.length}`);
834
861
  }
@@ -839,8 +866,8 @@ export class Multisig {
839
866
  if (currentOverride !== undefined && currentOverride === targetThreshold) {
840
867
  throw new Error(`Procedure ${targetProcedure} already has threshold override ${targetThreshold}`);
841
868
  }
842
- const { request, salt } = await buildUpdateProcedureThresholdTransactionRequest(webClient, targetProcedure, targetThreshold, this.proposalRequestOptions(options));
843
- const { summary, anchor } = await executeForSummary(webClient, this._accountId, request);
869
+ const { request, salt } = await buildUpdateProcedureThresholdTransactionRequest(this.midenClient, targetProcedure, targetThreshold, this.proposalRequestOptions(options));
870
+ const { summary, anchor } = await executeForSummary(this.midenClient, this._accountId, request);
844
871
  const chainAnchor = chainAnchorToBase64(anchor);
845
872
  anchor.free();
846
873
  const summaryBase64 = uint8ArrayToBase64(summary.serialize());
@@ -880,7 +907,6 @@ export class Multisig {
880
907
  * proposals for the same operation can never drift apart.
881
908
  */
882
909
  async buildSwitchGuardianSummary(newGuardianEndpoint, newGuardianPubkey, approvalExpirationDelta) {
883
- const webClient = await this.getRawClient();
884
910
  // What `auth_tx_guarded_multisig` asserts after a guardian rotation, checked
885
911
  // before any signature is collected.
886
912
  validateMultisigConfig({
@@ -889,12 +915,12 @@ export class Multisig {
889
915
  guardianCommitment: newGuardianPubkey,
890
916
  });
891
917
  await this.verifyGuardianEndpointCommitment(newGuardianEndpoint, newGuardianPubkey);
892
- const { request, salt } = await buildUpdateGuardianTransactionRequest(webClient, newGuardianPubkey, {
918
+ const { request, salt } = await buildUpdateGuardianTransactionRequest(this.midenClient, newGuardianPubkey, {
893
919
  accountId: this._accountId,
894
920
  approvalExpirationDelta,
895
921
  signatureScheme: this.signer.scheme,
896
922
  });
897
- const { summary, anchor } = await executeForSummary(webClient, this._accountId, request);
923
+ const { summary, anchor } = await executeForSummary(this.midenClient, this._accountId, request);
898
924
  const chainAnchor = chainAnchorToBase64(anchor);
899
925
  anchor.free();
900
926
  const summaryBase64 = uint8ArrayToBase64(summary.serialize());
@@ -966,15 +992,13 @@ export class Multisig {
966
992
  */
967
993
  async createConsumeNotesProposal(noteIds, options = {}) {
968
994
  const proposalNonce = resolveProposalNonce('createConsumeNotesProposal', options);
969
- const webClient = await this.getRawClient();
970
995
  if (noteIds.length === 0) {
971
996
  throw new Error('At least one note ID is required');
972
997
  }
973
998
  // Fetch notes locally (proposer has them per FR-012); embed for v2 verification.
974
- const rawClient = await getRawMidenClient(webClient);
975
999
  const fetchedNotes = [];
976
1000
  for (const noteIdHex of noteIds) {
977
- const inputNoteRecord = await rawClient.getInputNote(noteIdHex);
1001
+ const inputNoteRecord = await this.midenClient.notes.get(noteIdHex);
978
1002
  if (!inputNoteRecord) {
979
1003
  throw new LegacyConsumeNotesNoteMissingError(noteIdHex);
980
1004
  }
@@ -985,8 +1009,8 @@ export class Multisig {
985
1009
  // the notes are authenticated here first, before the anchor is captured.
986
1010
  await this.ensureNotesAuthenticated(fetchedNotes);
987
1011
  const embeddedNotes = fetchedNotes.map((n) => noteToBase64(n));
988
- const { request, salt } = await buildConsumeNotesTransactionRequestFromNotes(webClient, fetchedNotes, this.proposalRequestOptions(options));
989
- const { summary, anchor } = await executeForSummary(webClient, this._accountId, request);
1012
+ const { request, salt } = await buildConsumeNotesTransactionRequestFromNotes(this.midenClient, fetchedNotes, this.proposalRequestOptions(options));
1013
+ const { summary, anchor } = await executeForSummary(this.midenClient, this._accountId, request);
990
1014
  const chainAnchor = chainAnchorToBase64(anchor);
991
1015
  anchor.free();
992
1016
  const summaryBase64 = uint8ArrayToBase64(summary.serialize());
@@ -1023,15 +1047,14 @@ export class Multisig {
1023
1047
  */
1024
1048
  async createP2idProposal(recipientId, faucetId, amount, options = {}, ...legacyArgs) {
1025
1049
  const proposalNonce = resolveProposalNonce('createP2idProposal', options, legacyArgs);
1026
- const webClient = await this.getRawClient();
1027
1050
  if (amount <= 0n) {
1028
1051
  throw new Error('Amount must be greater than 0');
1029
1052
  }
1030
1053
  // Forward everything but the nonce, so a note option added to
1031
1054
  // CreateP2idProposalOptions can't be silently dropped before the builder.
1032
1055
  const { nonce: _nonce, ...noteOptions } = options;
1033
- const { request, salt } = await buildP2idTransactionRequest(webClient, this._accountId, recipientId, faucetId, amount, noteOptions);
1034
- const { summary, anchor } = await executeForSummary(webClient, this._accountId, request);
1056
+ const { request, salt } = await buildP2idTransactionRequest(this.midenClient, this._accountId, recipientId, faucetId, amount, noteOptions);
1057
+ const { summary, anchor } = await executeForSummary(this.midenClient, this._accountId, request);
1035
1058
  const chainAnchor = chainAnchorToBase64(anchor);
1036
1059
  anchor.free();
1037
1060
  const summaryBase64 = uint8ArrayToBase64(summary.serialize());
@@ -1055,41 +1078,24 @@ export class Multisig {
1055
1078
  /**
1056
1079
  * Get notes that can be consumed by this multisig account.
1057
1080
  *
1058
- * Returns a list of notes that are committed on-chain and can be consumed
1059
- * immediately by the multisig account.
1081
+ * Returns the notes the Miden client's last sync screened as consumable by
1082
+ * this account right now; block-locked notes are left out.
1060
1083
  */
1061
1084
  async getConsumableNotes() {
1062
- const accountId = AccountId.fromHex(this._accountId);
1063
- const webClient = await this.getRawClient();
1064
- // Get consumable notes for this account
1065
- const consumableRecords = await webClient.getConsumableNotes(accountId);
1066
- // Convert to our simplified ConsumableNote type
1085
+ const records = await this.midenClient.notes.listAvailable({ account: this._accountId });
1067
1086
  const notes = [];
1068
- for (const record of consumableRecords) {
1069
- const inputNote = record.inputNoteRecord();
1070
- const consumability = record.noteConsumability();
1071
- // Only include notes that can be consumed now (consumableAfterBlock is undefined/null)
1072
- const canConsumeNow = consumability.some((c) => c.accountId().toString().toLowerCase() === this._accountId.toLowerCase() &&
1073
- c.consumptionStatus().consumableAfterBlock() === undefined);
1074
- if (canConsumeNow) {
1075
- // Miden 0.15: InputNoteRecord.id() is `NoteId | undefined`; skip id-less records.
1076
- const id = inputNote.id();
1077
- if (id === undefined) {
1078
- continue;
1079
- }
1080
- const noteId = id.toString();
1081
- const details = inputNote.details();
1082
- const fungibleAssets = details.assets().fungibleAssets();
1083
- // Extract assets
1084
- const assets = [];
1085
- for (const asset of fungibleAssets) {
1086
- assets.push({
1087
- faucetId: asset.faucetId().toString(),
1088
- amount: asset.amount(),
1089
- });
1090
- }
1091
- notes.push({ id: noteId, assets });
1087
+ for (const inputNote of records) {
1088
+ // Miden 0.15: InputNoteRecord.id() is `NoteId | undefined`; skip id-less records.
1089
+ const id = inputNote.id();
1090
+ if (id === undefined) {
1091
+ continue;
1092
1092
  }
1093
+ const assets = inputNote
1094
+ .details()
1095
+ .assets()
1096
+ .fungibleAssets()
1097
+ .map((asset) => ({ faucetId: asset.faucetId().toString(), amount: asset.amount() }));
1098
+ notes.push({ id: id.toString(), assets });
1093
1099
  }
1094
1100
  return notes;
1095
1101
  }
@@ -1111,11 +1117,10 @@ export class Multisig {
1111
1117
  * @returns Serialized note file bytes
1112
1118
  */
1113
1119
  async exportNoteToBytes(noteId) {
1114
- const webClient = await this.getRawClient();
1115
1120
  const trimmedNoteId = noteId.trim();
1116
1121
  let record;
1117
1122
  try {
1118
- record = await webClient.getOutputNote(trimmedNoteId);
1123
+ [record] = await this.midenClient.notes.listSent({ ids: [trimmedNoteId] });
1119
1124
  }
1120
1125
  catch (err) {
1121
1126
  const detail = err instanceof Error ? err.message : String(err);
@@ -1127,7 +1132,7 @@ export class Multisig {
1127
1132
  const format = record.inclusionProof()
1128
1133
  ? NoteExportFormat.Full
1129
1134
  : NoteExportFormat.Details;
1130
- const noteFile = await webClient.exportNoteFile(trimmedNoteId, format);
1135
+ const noteFile = await this.midenClient.notes.export(trimmedNoteId, { format });
1131
1136
  return noteFile.serialize();
1132
1137
  }
1133
1138
  /**
@@ -1171,7 +1176,6 @@ export class Multisig {
1171
1176
  * commitment for a details-only file
1172
1177
  */
1173
1178
  async importNoteFromBytes(noteBytes) {
1174
- const webClient = await this.getRawClient();
1175
1179
  let noteFile;
1176
1180
  try {
1177
1181
  noteFile = NoteFile.deserialize(noteBytes);
@@ -1180,7 +1184,7 @@ export class Multisig {
1180
1184
  const detail = err instanceof Error ? err.message : String(err);
1181
1185
  throw new Error(`failed to decode note file: ${detail}`);
1182
1186
  }
1183
- return webClient.importNoteFile(noteFile);
1187
+ return this.midenClient.notes.import(noteFile);
1184
1188
  }
1185
1189
  /**
1186
1190
  * Import a note file received out-of-band from a browser
@@ -1464,6 +1468,8 @@ export class Multisig {
1464
1468
  }
1465
1469
  async signProposal(proposalId) {
1466
1470
  const normalizedProposalId = normalizeHexWord(proposalId);
1471
+ // Verification re-executes the proposal at the store's sync height.
1472
+ await this.syncChain();
1467
1473
  const existingProposal = await this.getProposalForSigning(proposalId, normalizedProposalId);
1468
1474
  if (!existingProposal) {
1469
1475
  throw new Error(`Proposal not found: ${proposalId}`);
@@ -1492,9 +1498,14 @@ export class Multisig {
1492
1498
  await this.syncProposals();
1493
1499
  return this.proposals.get(proposalId) ?? this.proposals.get(normalizedProposalId);
1494
1500
  }
1501
+ /**
1502
+ * Builds the final, fully signed request for a ready proposal, for a caller
1503
+ * that proves and submits it with its own pipeline. The request declares the
1504
+ * block its summary binds, so execute it at the chain tip, without an
1505
+ * anchor, on a client synced to at least that block.
1506
+ */
1495
1507
  async createTransactionProposalRequest(proposalId) {
1496
- const { finalRequest, anchor } = await this.prepareProposalExecution(proposalId);
1497
- anchor.free();
1508
+ const { finalRequest } = await this.prepareProposalExecution(proposalId);
1498
1509
  return finalRequest;
1499
1510
  }
1500
1511
  /**
@@ -1503,22 +1514,16 @@ export class Multisig {
1503
1514
  * @param proposalId - The proposal commitment/ID
1504
1515
  */
1505
1516
  async executeProposal(proposalId) {
1506
- const { metadata, finalRequest, proposal, anchor } = await this.prepareProposalExecution(proposalId);
1507
- try {
1508
- if (metadata.proposalType === 'switch_guardian') {
1509
- // #417: import notes embedded in pending proposals from the old
1510
- // GUARDIAN. Must run before the switch executes and repoints;
1511
- // best-effort and bounded — see preservePreSwitchProposalNotes.
1512
- await this.preservePreSwitchProposalNotes();
1513
- }
1514
- // Execute at the proposal's anchored reference block, so the summary the
1515
- // cosigners signed reproduces exactly. The anchor was already checked
1516
- // against the summary's block commitment during binding verification.
1517
- await this.proverWorkflow.submitAt(AccountId.fromHex(this._accountId), finalRequest, anchor);
1518
- }
1519
- finally {
1520
- anchor.free();
1521
- }
1517
+ const { metadata, finalRequest, proposal } = await this.prepareProposalExecution(proposalId);
1518
+ if (metadata.proposalType === 'switch_guardian') {
1519
+ // #417: import notes embedded in pending proposals from the old
1520
+ // GUARDIAN. Must run before the switch executes and repoints;
1521
+ // best-effort and bounded — see preservePreSwitchProposalNotes.
1522
+ await this.preservePreSwitchProposalNotes();
1523
+ }
1524
+ // Execute at the chain tip. The request declares the block the signed
1525
+ // summary binds, so the summary the cosigners signed reproduces there.
1526
+ await this.submitAtTip(finalRequest);
1522
1527
  if (metadata.proposalType === 'switch_guardian') {
1523
1528
  if (!metadata.newGuardianEndpoint || !metadata.newGuardianPubkey) {
1524
1529
  throw new Error('Switch GUARDIAN proposal metadata is incomplete after execution');
@@ -1564,11 +1569,12 @@ export class Multisig {
1564
1569
  * Submit an integration-built transaction (advice already injected). Mirrors
1565
1570
  * the Rust `submit_transaction`; used by the custom proposal producer flow
1566
1571
  * after `prepareCustomExecution` rebuilds its request with the returned advice.
1567
- * The transaction is executed at the proposal's anchored reference block,
1568
- * since the collected signatures only authorize the summary produced there.
1572
+ * The transaction is executed at the chain tip, so the request has to declare
1573
+ * the block its auth args bind (`feeAwareTransactionRequestBuilder` does).
1569
1574
  */
1570
1575
  async submitTransaction(proposalId, request) {
1571
1576
  const normalizedProposalId = normalizeHexWord(proposalId);
1577
+ await this.syncChain();
1572
1578
  const delta = await this.guardian.getDeltaProposal(this._accountId, normalizedProposalId);
1573
1579
  const existing = this.getLocalProposal(proposalId);
1574
1580
  const proposal = this.proposalFactory().fromDelta(delta, normalizedProposalId, existing?.metadata, existing?.signatures ?? []);
@@ -1580,11 +1586,31 @@ export class Multisig {
1580
1586
  if (anchorCommitment !== summaryBlockCommitment) {
1581
1587
  throw new Error(`Proposal ${proposalId} chain anchor does not match the block commitment bound into its tx_summary`);
1582
1588
  }
1583
- await this.proverWorkflow.submitAt(AccountId.fromHex(this._accountId), request, anchor);
1584
1589
  }
1585
1590
  finally {
1586
1591
  anchor.free();
1587
1592
  }
1593
+ await this.submitAtTip(request);
1594
+ }
1595
+ /**
1596
+ * Executes `request` at the chain tip, then proves, submits and applies it,
1597
+ * syncing first when this client is still below the block the request binds.
1598
+ */
1599
+ async submitAtTip(request) {
1600
+ await prepareTipExecution(this.midenClient, request, () => this.syncChain());
1601
+ await this.proverWorkflow.submit(AccountId.fromHex(this._accountId), request);
1602
+ }
1603
+ /**
1604
+ * Brings the Miden client to the chain tip before a proposal is re-executed:
1605
+ * an execution loads foreign accounts, the fee faucet among them, at the
1606
+ * store's sync height, which a node prunes about 50 blocks later.
1607
+ */
1608
+ async syncChain() {
1609
+ await retryRpcRead(() => this.midenClient.syncChain(), this.rpcConfig);
1610
+ }
1611
+ /** {@link syncToBoundBlock} with this client's RPC retry policy. */
1612
+ async syncToBoundBlock(boundBlockNum) {
1613
+ await syncToBoundBlock(this.midenClient, boundBlockNum, () => this.syncChain());
1588
1614
  }
1589
1615
  /**
1590
1616
  * Create a proposal from a producer-built transaction the SDK does not model.
@@ -1605,9 +1631,8 @@ export class Multisig {
1605
1631
  if (BUILTIN_PROPOSAL_TYPES.has(label)) {
1606
1632
  throw new Error(`'${label}' is a built-in proposal type; use the typed proposal API instead`);
1607
1633
  }
1608
- const webClient = await this.getRawClient();
1609
1634
  const request = deserializeTransactionRequest(transactionRequestBytes);
1610
- const { summary, anchor } = await executeForSummary(webClient, this._accountId, request);
1635
+ const { summary, anchor } = await executeForSummary(this.midenClient, this._accountId, request);
1611
1636
  const chainAnchor = chainAnchorToBase64(anchor);
1612
1637
  anchor.free();
1613
1638
  const summaryBase64 = uint8ArrayToBase64(summary.serialize());
@@ -1632,6 +1657,8 @@ export class Multisig {
1632
1657
  */
1633
1658
  async prepareCustomExecution(proposalId, transactionRequestBytes) {
1634
1659
  const normalizedProposalId = normalizeHexWord(proposalId);
1660
+ // The binding probe re-executes the producer's request at the store's sync height.
1661
+ await this.syncChain();
1635
1662
  const delta = await this.guardian.getDeltaProposal(this._accountId, normalizedProposalId);
1636
1663
  const existing = this.getLocalProposal(proposalId);
1637
1664
  const proposal = this.proposalFactory().fromDelta(delta, normalizedProposalId, existing?.metadata, existing?.signatures ?? []);
@@ -1646,26 +1673,23 @@ export class Multisig {
1646
1673
  const txSummary = TransactionSummary.deserialize(base64ToUint8Array(delta.deltaPayload.txSummary.data));
1647
1674
  const signedCommitmentHex = normalizeHexWord(txSummary.toCommitment().toHex());
1648
1675
  const bindingRequest = deserializeTransactionRequest(transactionRequestBytes);
1649
- // Probe at the proposal's anchored reference block: the signed summary
1650
- // binds that block's commitment, so probing at the local sync height would
1651
- // never reproduce it. The anchor arrives from an untrusted party via
1652
- // GUARDIAN, so its block commitment is checked against the signed summary
1653
- // before executing against it.
1676
+ // The anchor arrives from an untrusted party via GUARDIAN, so its block
1677
+ // commitment is checked against the signed summary: it has to name the
1678
+ // block the summary binds. The probe itself runs at the chain tip, where
1679
+ // the request's declared bound block reproduces the signed summary.
1654
1680
  const anchor = this.requireProposalAnchor(proposalId, proposal.metadata);
1655
- let derivedCommitmentHex;
1656
1681
  try {
1657
1682
  const anchorCommitment = normalizeHexWord(anchor.commitment().toHex());
1658
1683
  const summaryBlockCommitment = normalizeHexWord(txSummary.blockCommitment().toHex());
1659
1684
  if (anchorCommitment !== summaryBlockCommitment) {
1660
1685
  throw new Error(`Custom proposal ${proposalId} chain anchor does not match the block commitment bound into its tx_summary`);
1661
1686
  }
1662
- const webClient = await this.getRawClient();
1663
- const derived = await executeForSummaryAt(webClient, this._accountId, bindingRequest, anchor);
1664
- derivedCommitmentHex = normalizeHexWord(derived.toCommitment().toHex());
1665
1687
  }
1666
1688
  finally {
1667
1689
  anchor.free();
1668
1690
  }
1691
+ const derived = await executeForSummaryAtTip(this.midenClient, this._accountId, bindingRequest);
1692
+ const derivedCommitmentHex = normalizeHexWord(derived.toCommitment().toHex());
1669
1693
  if (derivedCommitmentHex !== signedCommitmentHex) {
1670
1694
  throw new Error(`Custom proposal binding mismatch: expected ${signedCommitmentHex}, got ${derivedCommitmentHex}`);
1671
1695
  }
@@ -1753,8 +1777,8 @@ export class Multisig {
1753
1777
  return this.proposals.get(proposalId) ?? this.proposals.get(normalizedProposalId);
1754
1778
  }
1755
1779
  /**
1756
- * The returned `anchor` is the block the request has to execute at; the
1757
- * caller owns it and frees it once submitted.
1780
+ * The returned request declares the block the signed summary binds, and
1781
+ * executes at the chain tip.
1758
1782
  */
1759
1783
  async prepareProposalExecution(proposalId) {
1760
1784
  const proposal = this.getLocalProposal(proposalId);
@@ -1762,6 +1786,8 @@ export class Multisig {
1762
1786
  throw new Error(`Proposal not found: ${proposalId}`);
1763
1787
  }
1764
1788
  this.proposalFactory().assertAccountId(proposal.accountId);
1789
+ // Verification and execution run at the store's sync height.
1790
+ await this.syncChain();
1765
1791
  await this.verifyProposalMetadataBinding(proposal);
1766
1792
  const metadata = proposal.metadata;
1767
1793
  // Reject custom proposals before any advice assembly or GUARDIAN ack push:
@@ -1873,14 +1899,18 @@ export class Multisig {
1873
1899
  await this.verifyGuardianEndpointCommitment(metadata.newGuardianEndpoint, metadata.newGuardianPubkey);
1874
1900
  }
1875
1901
  const anchor = this.requireProposalAnchor(proposalId, metadata);
1902
+ let binding;
1876
1903
  try {
1877
- const finalRequest = await this.buildTransactionRequestFromMetadata(metadata, proposalRequestBinding(txSummary, anchor, saltHex), adviceMap);
1878
- return { finalRequest, metadata, proposal, anchor };
1904
+ binding = proposalRequestBinding(txSummary, anchor, saltHex);
1879
1905
  }
1880
- catch (error) {
1906
+ finally {
1881
1907
  anchor.free();
1882
- throw error;
1883
1908
  }
1909
+ // A switch_guardian proposal is verified without a rebuild, so this may be
1910
+ // the first time this client needs the chain at the bound block.
1911
+ await this.syncToBoundBlock(binding.boundBlockNum);
1912
+ const finalRequest = await this.buildTransactionRequestFromMetadata(metadata, binding, adviceMap);
1913
+ return { finalRequest, metadata, proposal };
1884
1914
  }
1885
1915
  /**
1886
1916
  * Export a proposal for offline signing
@@ -1950,6 +1980,10 @@ export class Multisig {
1950
1980
  throw new Error('Invalid proposal JSON: missing required fields');
1951
1981
  }
1952
1982
  const proposal = this.proposalFactory().fromExported(exported);
1983
+ // Verification re-executes every built-in type at the store's sync height.
1984
+ if (proposal.metadata.proposalType !== 'custom') {
1985
+ await this.syncChain();
1986
+ }
1953
1987
  await this.verifyProposalMetadataBinding(proposal);
1954
1988
  this.proposals.set(proposal.id, proposal);
1955
1989
  return proposal;
@@ -1982,6 +2016,10 @@ export class Multisig {
1982
2016
  if (alreadySigned) {
1983
2017
  throw new Error('You have already signed this proposal');
1984
2018
  }
2019
+ // Verification re-executes every built-in type at the store's sync height.
2020
+ if (proposal.metadata?.proposalType !== 'custom') {
2021
+ await this.syncChain();
2022
+ }
1985
2023
  const commitmentToSign = await this.verifyProposalMetadataBinding(proposal);
1986
2024
  // Sign the commitment
1987
2025
  const signature = await buildGuardianSignatureFromSigner(this.signer, commitmentToSign);
@@ -2033,7 +2071,7 @@ export class Multisig {
2033
2071
  catch (error) {
2034
2072
  proposal.verification = {
2035
2073
  status: 'failed',
2036
- retryable: isTransientRpcError(error),
2074
+ retryable: isTransientRpcError(error) || isStaleChainError(error),
2037
2075
  message: error instanceof Error ? error.message : String(error),
2038
2076
  };
2039
2077
  throw error;
@@ -2042,10 +2080,11 @@ export class Multisig {
2042
2080
  async checkProposalMetadataBinding(proposal) {
2043
2081
  const txSummaryCommitment = this.ensureProposalCommitmentMatchesSummary(proposal);
2044
2082
  const summary = TransactionSummary.deserialize(base64ToUint8Array(proposal.txSummary));
2045
- // The anchor arrives from an untrusted party via GUARDIAN, so check its
2046
- // block commitment against the one bound into the signed summary before
2047
- // anything executes against it. `ChainAnchor.deserialize` already enforced
2048
- // internal header/chain consistency.
2083
+ // The anchor arrives from an untrusted party via GUARDIAN, so check that it
2084
+ // names the block the signed summary binds: the rebuild below binds the
2085
+ // block it names. Nothing executes against it; the rebuild runs at the
2086
+ // chain tip. `ChainAnchor.deserialize` already enforced internal
2087
+ // header/chain consistency.
2049
2088
  const anchor = this.requireProposalAnchor(proposal.id, proposal.metadata);
2050
2089
  try {
2051
2090
  const anchorCommitment = normalizeHexWord(anchor.commitment().toHex());
@@ -2072,6 +2111,11 @@ export class Multisig {
2072
2111
  // binding checks for this type.
2073
2112
  return txSummaryCommitment;
2074
2113
  }
2114
+ // The rebuild reads the chain's fee faucet from the synced protocol
2115
+ // configuration and executes at the tip, so the store has to have synced
2116
+ // to the block the summary binds; a cosigner that has only just loaded
2117
+ // the account has not.
2118
+ await this.syncToBoundBlock(binding.boundBlockNum);
2075
2119
  // A consume-notes summary commits to *authenticated* consumption (see
2076
2120
  // ensureNotesAuthenticated), which miden-client decides from this store
2077
2121
  // alone. Put the store in that mode before the rebuild, or a cosigner
@@ -2081,8 +2125,7 @@ export class Multisig {
2081
2125
  await this.ensureNotesAuthenticated(decodeEmbeddedConsumeNotes(proposal.metadata));
2082
2126
  }
2083
2127
  const request = await this.buildTransactionRequestFromMetadata(proposal.metadata, binding);
2084
- const webClient = await this.getRawClient();
2085
- const reconstructed = await executeForSummaryAt(webClient, this._accountId, request, anchor);
2128
+ const reconstructed = await executeForSummaryAtTip(this.midenClient, this._accountId, request);
2086
2129
  const reconstructedCommitment = normalizeHexWord(reconstructed.toCommitment().toHex());
2087
2130
  if (reconstructedCommitment !== txSummaryCommitment) {
2088
2131
  throw new Error(`Invalid proposal: metadata does not match tx_summary for ${proposal.id}`);
@@ -2093,12 +2136,6 @@ export class Multisig {
2093
2136
  anchor.free();
2094
2137
  }
2095
2138
  }
2096
- /**
2097
- * Decodes a proposal's chain anchor. Throws when absent: a proposal without
2098
- * an anchor was created at an unknown reference block, so its signed summary
2099
- * cannot be reproduced, verified, or executed. The caller owns the returned
2100
- * anchor and must `free()` it once done.
2101
- */
2102
2139
  /**
2103
2140
  * Reads a proposal's salt. Throws when absent, because there is nothing to fall
2104
2141
  * back to.
@@ -2140,11 +2177,16 @@ export class Multisig {
2140
2177
  }
2141
2178
  return saltHex;
2142
2179
  }
2180
+ /**
2181
+ * Decodes a proposal's chain anchor, which names the block its signed summary
2182
+ * binds. Throws when absent: every proposal carries one, so a proposal
2183
+ * without it is malformed and is neither verified nor executed. The caller
2184
+ * owns the returned anchor and must `free()` it once done.
2185
+ */
2143
2186
  requireProposalAnchor(proposalId, metadata) {
2144
2187
  if (!metadata.chainAnchor) {
2145
- throw new Error(`Proposal ${proposalId} has no chain anchor; it was created without ` +
2146
- 'chain-anchored execution and its signed summary cannot be reproduced ' +
2147
- 'at the original reference block');
2188
+ throw new Error(`Proposal ${proposalId} has no chain anchor, which names the block its signed ` +
2189
+ 'summary binds; it cannot be verified or executed');
2148
2190
  }
2149
2191
  return chainAnchorFromBase64(metadata.chainAnchor);
2150
2192
  }
@@ -2158,8 +2200,7 @@ export class Multisig {
2158
2200
  if (expirationBlockNum === undefined) {
2159
2201
  return;
2160
2202
  }
2161
- const webClient = await this.getRawClient();
2162
- const syncHeight = await webClient.getSyncHeight();
2203
+ const syncHeight = await this.midenClient.getSyncHeight();
2163
2204
  if (syncHeight >= expirationBlockNum) {
2164
2205
  throw new Error(`Proposal ${proposalId} approval expired at block ${expirationBlockNum}; the chain is at ` +
2165
2206
  `block ${syncHeight}, so the collected signatures no longer authorize it`);
@@ -2188,20 +2229,19 @@ export class Multisig {
2188
2229
  }
2189
2230
  }
2190
2231
  async buildTransactionRequestWithOptions(metadata, requestOptions) {
2191
- const webClient = await this.getRawClient();
2192
2232
  switch (metadata.proposalType) {
2193
2233
  case 'add_signer':
2194
2234
  case 'remove_signer':
2195
2235
  case 'change_threshold': {
2196
- const { request } = await buildUpdateSignersTransactionRequest(webClient, metadata.targetThreshold, metadata.targetSignerCommitments, requestOptions);
2236
+ const { request } = await buildUpdateSignersTransactionRequest(this.midenClient, metadata.targetThreshold, metadata.targetSignerCommitments, requestOptions);
2197
2237
  return request;
2198
2238
  }
2199
2239
  case 'switch_guardian': {
2200
- const { request } = await buildUpdateGuardianTransactionRequest(webClient, metadata.newGuardianPubkey, requestOptions);
2240
+ const { request } = await buildUpdateGuardianTransactionRequest(this.midenClient, metadata.newGuardianPubkey, requestOptions);
2201
2241
  return request;
2202
2242
  }
2203
2243
  case 'update_procedure_threshold': {
2204
- const { request } = await buildUpdateProcedureThresholdTransactionRequest(webClient, metadata.targetProcedure, metadata.targetThreshold, requestOptions);
2244
+ const { request } = await buildUpdateProcedureThresholdTransactionRequest(this.midenClient, metadata.targetProcedure, metadata.targetThreshold, requestOptions);
2205
2245
  return request;
2206
2246
  }
2207
2247
  case 'consume_notes': {
@@ -2209,7 +2249,7 @@ export class Multisig {
2209
2249
  const version = metadata.metadataVersion;
2210
2250
  if (version === CONSUME_NOTES_METADATA_VERSION_V2) {
2211
2251
  const decoded = decodeEmbeddedConsumeNotes(metadata);
2212
- const { request } = await buildConsumeNotesTransactionRequestFromNotes(webClient, decoded, requestOptions);
2252
+ const { request } = await buildConsumeNotesTransactionRequestFromNotes(this.midenClient, decoded, requestOptions);
2213
2253
  return request;
2214
2254
  }
2215
2255
  if (version === undefined || version === 1) {
@@ -2218,13 +2258,13 @@ export class Multisig {
2218
2258
  // operator which legacy shape was rejected.
2219
2259
  throw new UnsupportedMetadataVersionError(version);
2220
2260
  }
2221
- const { request } = await buildConsumeNotesTransactionRequest(webClient, metadata.noteIds, requestOptions);
2261
+ const { request } = await buildConsumeNotesTransactionRequest(this.midenClient, metadata.noteIds, requestOptions);
2222
2262
  return request;
2223
2263
  }
2224
2264
  throw new UnsupportedMetadataVersionError(version);
2225
2265
  }
2226
2266
  case 'p2id': {
2227
- const { request } = await buildP2idTransactionRequest(webClient, this._accountId, metadata.recipientId, metadata.faucetId, BigInt(metadata.amount), {
2267
+ const { request } = await buildP2idTransactionRequest(this.midenClient, this._accountId, metadata.recipientId, metadata.faucetId, BigInt(metadata.amount), {
2228
2268
  ...requestOptions,
2229
2269
  noteType: parseP2idNoteType(metadata.noteType),
2230
2270
  reclaimHeight: metadata.reclaimHeight,