@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/src/multisig.ts CHANGED
@@ -18,10 +18,7 @@ import type {
18
18
  } from './types.js';
19
19
  import { ProposalSaltMalformedError } from './multisig/authArgErrors.js';
20
20
  import type { ProcedureName } from './procedures.js';
21
- import type {
22
- MidenClient,
23
- WasmWebClient,
24
- } from '@miden-sdk/miden-sdk';
21
+ import type { MidenClient, OutputNoteRecord } from '@miden-sdk/miden-sdk';
25
22
  import {
26
23
  Account,
27
24
  AccountId,
@@ -44,7 +41,10 @@ import {
44
41
  chainAnchorFromBase64,
45
42
  chainAnchorToBase64,
46
43
  executeForSummary,
47
- executeForSummaryAt,
44
+ executeForSummaryAtTip,
45
+ isStaleChainError,
46
+ prepareTipExecution,
47
+ syncToBoundBlock,
48
48
  summaryApprovalExpirationBlockNum,
49
49
  summarySalt,
50
50
  buildUpdateSignersTransactionRequest,
@@ -110,11 +110,7 @@ import {
110
110
  type NoteRecoverySteps,
111
111
  type RecoverNotesOptions,
112
112
  } from './recovery/recoverNotes.js';
113
- import {
114
- getRawMidenClient,
115
- getTransactionProver,
116
- requireMidenRpcEndpoint,
117
- } from './raw-client.js';
113
+ import { requireMidenRpcEndpoint } from './config.js';
118
114
  import {
119
115
  resolveProverConfig,
120
116
  type ResolvedProverConfig,
@@ -142,6 +138,20 @@ export interface AccountState {
142
138
  updatedAt: string;
143
139
  }
144
140
 
141
+ /**
142
+ * Outcome of {@link Multisig.syncState}.
143
+ *
144
+ * `'guardian'`: GUARDIAN's canonical nonce was above the local nonce (or the
145
+ * local store had no account, or the same nonce carried a different
146
+ * commitment), so the state was fetched and reconciled; `state` is what
147
+ * GUARDIAN served. `'local'`: GUARDIAN's canonical nonce was below the local
148
+ * nonce, or equal to it at the same commitment, so the state fetch was
149
+ * skipped and the local account stands.
150
+ */
151
+ export type SyncStateResult =
152
+ | { source: 'guardian'; state: AccountState }
153
+ | { source: 'local'; localNonce: bigint; guardianNonce: bigint };
154
+
145
155
  export interface AccountStateVerificationResult {
146
156
  accountId: string;
147
157
  localCommitment: string;
@@ -157,7 +167,7 @@ export interface CreateProposalOptions {
157
167
  /** Proposal nonce; defaults to `Date.now()`. */
158
168
  nonce?: number;
159
169
  /**
160
- * Blocks after the proposal's anchor block by which the transaction must be
170
+ * Blocks after the block the proposal binds by which the transaction must be
161
171
  * included; past that the approvers' signatures no longer authorize it. The
162
172
  * summary binds it, so the executing party can neither shorten nor extend it.
163
173
  * Omitted, the approval never expires (the upstream default).
@@ -324,7 +334,6 @@ export class Multisig {
324
334
  private guardian: GuardianHttpClient;
325
335
  private readonly signer: Signer;
326
336
  private readonly midenClient: MidenClient;
327
- private readonly rawClientPromise: Promise<WasmWebClient>;
328
337
  private readonly proverWorkflow: ProverWorkflow;
329
338
  private readonly rpcConfig: ResolvedRpcConfig;
330
339
  private readonly _accountId: string;
@@ -373,10 +382,9 @@ export class Multisig {
373
382
  this.midenClient = midenClient;
374
383
  this._accountId = accountId ?? (account ? accountIdToHex(account) : '');
375
384
  this.midenRpcEndpoint = requireMidenRpcEndpoint(midenRpcEndpoint);
376
- this.rawClientPromise = getRawMidenClient(midenClient, this.midenRpcEndpoint);
377
385
  this.proverWorkflow = new ProverWorkflow(
378
386
  this.midenClient,
379
- proverConfig ?? resolveProverConfig(undefined, getTransactionProver(midenClient)),
387
+ proverConfig ?? resolveProverConfig(undefined, midenClient.defaultProver),
380
388
  );
381
389
  this.rpcConfig = rpcConfig ?? resolveRpcConfig(undefined);
382
390
  }
@@ -385,10 +393,6 @@ export class Multisig {
385
393
  return this.midenRpcEndpoint;
386
394
  }
387
395
 
388
- private async getRawClient(): Promise<WasmWebClient> {
389
- return this.rawClientPromise;
390
- }
391
-
392
396
  private proposalFactory(): ProposalFactory {
393
397
  return new ProposalFactory({
394
398
  accountId: this._accountId,
@@ -425,7 +429,7 @@ export class Multisig {
425
429
  }
426
430
 
427
431
  /**
428
- * Resolve the account from the web client's store, falling back to the
432
+ * Resolve the account from the Miden client's store, falling back to the
429
433
  * `account` snapshot when the store has no record.
430
434
  *
431
435
  * Transaction execution reads the store, and other flows (e.g. consume-notes
@@ -433,12 +437,11 @@ export class Multisig {
433
437
  * source from the store to see the same state execution will.
434
438
  */
435
439
  async getStoreAccount(): Promise<Account> {
436
- const webClient = await this.getRawClient();
437
- const stored = await retryRpcRead(
438
- () => webClient.getAccount(AccountId.fromHex(this._accountId)),
439
- this.rpcConfig,
440
- );
441
- return stored ?? this.account;
440
+ return (await this.readStoreAccount()) ?? this.account;
441
+ }
442
+
443
+ private readStoreAccount(): Promise<Account | null> {
444
+ return retryRpcRead(() => this.midenClient.accounts.get(this._accountId), this.rpcConfig);
442
445
  }
443
446
 
444
447
  /**
@@ -607,24 +610,37 @@ export class Multisig {
607
610
  /**
608
611
  * Sync account state from GUARDIAN into the local Miden client store.
609
612
  *
610
- * If the GUARDIAN commitment differs from the local commitment (or the account
611
- * is missing locally) and the GUARDIAN state is safe to import, the local store
612
- * is overwritten with the GUARDIAN state. When the GUARDIAN is merely *behind*
613
+ * Starts with the canonical-nonce pre-check (OpenZeppelin/guardian#191):
614
+ * GUARDIAN reports the nonce and commitment of its canonical state, and
615
+ * when that nonce is not above the local account's — at a matching
616
+ * commitment when equal — nothing newer exists to pull, so the state
617
+ * fetch is skipped and the local account stands (`source: 'local'`).
618
+ *
619
+ * Otherwise the state is fetched (`source: 'guardian'`). If the GUARDIAN
620
+ * commitment differs from the local commitment (or the account is missing
621
+ * locally) and the GUARDIAN state is safe to import, the local store is
622
+ * overwritten with the GUARDIAN state. When the GUARDIAN is merely *behind*
613
623
  * local — e.g. the pushed execution delta has not been canonicalized yet
614
624
  * (see OpenZeppelin/guardian#316) — the local state is already ahead and
615
625
  * on-chain-verifiable, so it is kept as authoritative. Either way, config is
616
626
  * refreshed from the resulting account so callers reading `Multisig.account`
617
627
  * (e.g. the UI) observe the current state instead of a stale snapshot.
618
628
  */
619
- async syncState(): Promise<AccountState> {
629
+ async syncState(): Promise<SyncStateResult> {
630
+ const localAccount = await this.readStoreAccount();
631
+
632
+ if (localAccount) {
633
+ const head = await this.guardian.getCanonicalNonce(this._accountId);
634
+ const localNonce = localAccount.nonce().asInt();
635
+ const guardianNonce = BigInt(head.nonce);
636
+ if (this.guardianIsNotAhead(localAccount, localNonce, head.commitment, guardianNonce)) {
637
+ this.refreshConfigFromAccount(localAccount);
638
+ return { source: 'local', localNonce, guardianNonce };
639
+ }
640
+ }
641
+
620
642
  const state = await this.fetchState();
621
- const accountId = AccountId.fromHex(this._accountId);
622
- const webClient = await this.getRawClient();
623
- const localAccount = await retryRpcRead(
624
- () => webClient.getAccount(accountId),
625
- this.rpcConfig,
626
- );
627
- let accountForConfigRefresh: Account | null = localAccount ?? null;
643
+ let accountForConfigRefresh: Account | null = localAccount;
628
644
 
629
645
  const guardianCommitment = normalizeHexWord(state.commitment);
630
646
  const localCommitment = localAccount
@@ -634,24 +650,44 @@ export class Multisig {
634
650
  if (!localAccount || localCommitment !== guardianCommitment) {
635
651
  const accountBytes = base64ToUint8Array(state.stateDataBase64);
636
652
  const incomingAccount = Account.deserialize(accountBytes);
637
- if (await this.isSafeToOverwriteLocalState(incomingAccount, localAccount)) {
638
- await webClient.newAccount(incomingAccount, true);
653
+ if (await this.isSafeToOverwriteLocalState(incomingAccount, localAccount ?? undefined)) {
654
+ await this.midenClient.accounts.insert({ account: incomingAccount, overwrite: true });
639
655
  accountForConfigRefresh = incomingAccount;
640
656
  }
641
657
  }
642
658
 
643
659
  this.refreshConfigFromAccount(accountForConfigRefresh);
644
660
 
645
- return state;
661
+ return { source: 'guardian', state };
662
+ }
663
+
664
+ /**
665
+ * The pre-check decision: GUARDIAN has nothing newer when its canonical
666
+ * nonce is below the local nonce, or equal to it at the same commitment.
667
+ * An equal nonce with a different commitment is divergence, which the
668
+ * full fetch reports (see `isSafeToOverwriteLocalState`).
669
+ */
670
+ private guardianIsNotAhead(
671
+ localAccount: Account,
672
+ localNonce: bigint,
673
+ guardianCommitment: string,
674
+ guardianNonce: bigint,
675
+ ): boolean {
676
+ if (guardianNonce < localNonce) {
677
+ return true;
678
+ }
679
+ if (guardianNonce > localNonce) {
680
+ return false;
681
+ }
682
+ return (
683
+ normalizeHexWord(guardianCommitment) ===
684
+ normalizeHexWord(localAccount.to_commitment().toHex())
685
+ );
646
686
  }
647
687
 
648
688
  async verifyStateCommitment(): Promise<AccountStateVerificationResult> {
649
689
  const accountId = AccountId.fromHex(this._accountId);
650
- const webClient = await this.getRawClient();
651
- const localAccount = await retryRpcRead(
652
- () => webClient.getAccount(accountId),
653
- this.rpcConfig,
654
- );
690
+ const localAccount = await this.readStoreAccount();
655
691
 
656
692
  if (!localAccount) {
657
693
  throw new Error(
@@ -722,14 +758,10 @@ export class Multisig {
722
758
  * fail.
723
759
  */
724
760
  private async syncNetworkOnly(): Promise<Account | null> {
725
- const webClient = await this.getRawClient();
726
- await retryRpcRead(() => webClient.syncState(), this.rpcConfig);
727
- const account = await retryRpcRead(
728
- () => webClient.getAccount(AccountId.fromHex(this._accountId)),
729
- this.rpcConfig,
730
- );
731
- this.refreshConfigFromAccount(account ?? null);
732
- return account ?? null;
761
+ await this.syncChain();
762
+ const account = await this.readStoreAccount();
763
+ this.refreshConfigFromAccount(account);
764
+ return account;
733
765
  }
734
766
 
735
767
  private refreshConfigFromAccount(account: Account | null): void {
@@ -808,11 +840,12 @@ export class Multisig {
808
840
  * Every synced proposal's metadata is checked against its signed summary
809
841
  * and the outcome is recorded in {@link Proposal.verification}. One that
810
842
  * fails is still cached and returned, so a single stale or corrupt
811
- * proposal cannot hide the others (issue #462: once the node prunes a
812
- * proposal's anchor block its re-execution fails for everyone). `failed`
813
- * with `retryable: true` means a transient node error, worth syncing
814
- * again; `retryable: false` means the proposal cannot be reproduced and
815
- * must be re-proposed. `signProposal` and `executeProposal` re-verify and
843
+ * proposal cannot hide the others (issue #462). Verification re-executes
844
+ * each proposal at the chain tip, so the Miden client is synced once first.
845
+ * `failed` with `retryable: true` means a transient node error or chain
846
+ * state this client had not caught up with, worth syncing again;
847
+ * `retryable: false` means the proposal cannot be reproduced and must be
848
+ * re-proposed. `signProposal` and `executeProposal` re-verify and
816
849
  * refuse a failed proposal. A failed proposal still counts as reported, so
817
850
  * it is pruned like any other once GUARDIAN stops listing it.
818
851
  *
@@ -852,6 +885,14 @@ export class Multisig {
852
885
  const factory = this.proposalFactory();
853
886
 
854
887
  const reported = new Map<string, { delta: (typeof deltas)[number]; verified: Proposal }>();
888
+ if (deltas.length > 0) {
889
+ // Verification re-executes at the store's sync height; bring it to the
890
+ // tip once for the whole listing. Best effort: a node outage then shows
891
+ // up on each proposal as a retryable failure instead of failing the sync.
892
+ await this.syncChain().catch((error) =>
893
+ console.warn('Could not sync the Miden client before verifying proposals', error),
894
+ );
895
+ }
855
896
  for (const delta of deltas) {
856
897
  const proposalId = normalizeHexWord(
857
898
  computeCommitmentFromTxSummary(delta.deltaPayload.txSummary.data)
@@ -1035,7 +1076,6 @@ export class Multisig {
1035
1076
  ...legacyArgs: never[]
1036
1077
  ): Promise<Proposal> {
1037
1078
  const proposalNonce = resolveProposalNonce('createAddSignerProposal', options, legacyArgs);
1038
- const webClient = await this.getRawClient();
1039
1079
  const targetThreshold = options.newThreshold ?? this.threshold;
1040
1080
  const targetSignerCommitments = [...this.signerCommitments, newCommitment];
1041
1081
  // What `update_signers_and_threshold` rejects on-chain, and what the auth
@@ -1049,13 +1089,13 @@ export class Multisig {
1049
1089
  this.warnOnOverrideDilution(targetSignerCommitments.length);
1050
1090
 
1051
1091
  const { request, salt } = await buildUpdateSignersTransactionRequest(
1052
- webClient,
1092
+ this.midenClient,
1053
1093
  targetThreshold,
1054
1094
  targetSignerCommitments,
1055
1095
  this.proposalRequestOptions(options),
1056
1096
  );
1057
1097
 
1058
- const { summary, anchor } = await executeForSummary(webClient, this._accountId, request);
1098
+ const { summary, anchor } = await executeForSummary(this.midenClient, this._accountId, request);
1059
1099
  const chainAnchor = chainAnchorToBase64(anchor);
1060
1100
  anchor.free();
1061
1101
  const summaryBase64 = uint8ArrayToBase64(summary.serialize());
@@ -1086,7 +1126,6 @@ export class Multisig {
1086
1126
  ...legacyArgs: never[]
1087
1127
  ): Promise<Proposal> {
1088
1128
  const proposalNonce = resolveProposalNonce('createRemoveSignerProposal', options, legacyArgs);
1089
- const webClient = await this.getRawClient();
1090
1129
  const normalizedRemove = signerToRemove.toLowerCase();
1091
1130
  const targetSignerCommitments = this.signerCommitments.filter(
1092
1131
  (c) => c.toLowerCase() !== normalizedRemove
@@ -1108,13 +1147,13 @@ export class Multisig {
1108
1147
  }
1109
1148
 
1110
1149
  const { request, salt } = await buildUpdateSignersTransactionRequest(
1111
- webClient,
1150
+ this.midenClient,
1112
1151
  targetThreshold,
1113
1152
  targetSignerCommitments,
1114
1153
  this.proposalRequestOptions(options),
1115
1154
  );
1116
1155
 
1117
- const { summary, anchor } = await executeForSummary(webClient, this._accountId, request);
1156
+ const { summary, anchor } = await executeForSummary(this.midenClient, this._accountId, request);
1118
1157
  const chainAnchor = chainAnchorToBase64(anchor);
1119
1158
  anchor.free();
1120
1159
  const summaryBase64 = uint8ArrayToBase64(summary.serialize());
@@ -1143,7 +1182,6 @@ export class Multisig {
1143
1182
  options: CreateProposalOptions = {},
1144
1183
  ): Promise<Proposal> {
1145
1184
  const proposalNonce = resolveProposalNonce('createChangeThresholdProposal', options);
1146
- const webClient = await this.getRawClient();
1147
1185
  if (newThreshold < 1 || newThreshold > this.signerCommitments.length) {
1148
1186
  throw new Error(
1149
1187
  `Invalid threshold ${newThreshold}. Must be between 1 and ${this.signerCommitments.length}`
@@ -1155,13 +1193,13 @@ export class Multisig {
1155
1193
  }
1156
1194
 
1157
1195
  const { request, salt } = await buildUpdateSignersTransactionRequest(
1158
- webClient,
1196
+ this.midenClient,
1159
1197
  newThreshold,
1160
1198
  this.signerCommitments,
1161
1199
  this.proposalRequestOptions(options),
1162
1200
  );
1163
1201
 
1164
- const { summary, anchor } = await executeForSummary(webClient, this._accountId, request);
1202
+ const { summary, anchor } = await executeForSummary(this.midenClient, this._accountId, request);
1165
1203
  const chainAnchor = chainAnchorToBase64(anchor);
1166
1204
  anchor.free();
1167
1205
  const summaryBase64 = uint8ArrayToBase64(summary.serialize());
@@ -1185,7 +1223,6 @@ export class Multisig {
1185
1223
  options: CreateProposalOptions = {},
1186
1224
  ): Promise<Proposal> {
1187
1225
  const proposalNonce = resolveProposalNonce('createUpdateProcedureThresholdProposal', options);
1188
- const webClient = await this.getRawClient();
1189
1226
  if (targetThreshold < 0 || targetThreshold > this.signerCommitments.length) {
1190
1227
  throw new Error(
1191
1228
  `Invalid threshold ${targetThreshold}. Must be between 0 and ${this.signerCommitments.length}`
@@ -1204,13 +1241,13 @@ export class Multisig {
1204
1241
  }
1205
1242
 
1206
1243
  const { request, salt } = await buildUpdateProcedureThresholdTransactionRequest(
1207
- webClient,
1244
+ this.midenClient,
1208
1245
  targetProcedure,
1209
1246
  targetThreshold,
1210
1247
  this.proposalRequestOptions(options),
1211
1248
  );
1212
1249
 
1213
- const { summary, anchor } = await executeForSummary(webClient, this._accountId, request);
1250
+ const { summary, anchor } = await executeForSummary(this.midenClient, this._accountId, request);
1214
1251
  const chainAnchor = chainAnchorToBase64(anchor);
1215
1252
  anchor.free();
1216
1253
  const summaryBase64 = uint8ArrayToBase64(summary.serialize());
@@ -1267,7 +1304,6 @@ export class Multisig {
1267
1304
  newGuardianPubkey: string,
1268
1305
  approvalExpirationDelta: number | undefined,
1269
1306
  ): Promise<{ summaryBase64: string; metadata: ProposalMetadata }> {
1270
- const webClient = await this.getRawClient();
1271
1307
  // What `auth_tx_guarded_multisig` asserts after a guardian rotation, checked
1272
1308
  // before any signature is collected.
1273
1309
  validateMultisigConfig({
@@ -1278,7 +1314,7 @@ export class Multisig {
1278
1314
  await this.verifyGuardianEndpointCommitment(newGuardianEndpoint, newGuardianPubkey);
1279
1315
 
1280
1316
  const { request, salt } = await buildUpdateGuardianTransactionRequest(
1281
- webClient,
1317
+ this.midenClient,
1282
1318
  newGuardianPubkey,
1283
1319
  {
1284
1320
  accountId: this._accountId,
@@ -1287,7 +1323,7 @@ export class Multisig {
1287
1323
  },
1288
1324
  );
1289
1325
 
1290
- const { summary, anchor } = await executeForSummary(webClient, this._accountId, request);
1326
+ const { summary, anchor } = await executeForSummary(this.midenClient, this._accountId, request);
1291
1327
  const chainAnchor = chainAnchorToBase64(anchor);
1292
1328
  anchor.free();
1293
1329
  const summaryBase64 = uint8ArrayToBase64(summary.serialize());
@@ -1378,16 +1414,14 @@ export class Multisig {
1378
1414
  options: CreateProposalOptions = {},
1379
1415
  ): Promise<Proposal> {
1380
1416
  const proposalNonce = resolveProposalNonce('createConsumeNotesProposal', options);
1381
- const webClient = await this.getRawClient();
1382
1417
  if (noteIds.length === 0) {
1383
1418
  throw new Error('At least one note ID is required');
1384
1419
  }
1385
1420
 
1386
1421
  // Fetch notes locally (proposer has them per FR-012); embed for v2 verification.
1387
- const rawClient = await getRawMidenClient(webClient);
1388
1422
  const fetchedNotes: Note[] = [];
1389
1423
  for (const noteIdHex of noteIds) {
1390
- const inputNoteRecord = await rawClient.getInputNote(noteIdHex);
1424
+ const inputNoteRecord = await this.midenClient.notes.get(noteIdHex);
1391
1425
  if (!inputNoteRecord) {
1392
1426
  throw new LegacyConsumeNotesNoteMissingError(noteIdHex);
1393
1427
  }
@@ -1400,12 +1434,12 @@ export class Multisig {
1400
1434
  const embeddedNotes = fetchedNotes.map((n) => noteToBase64(n));
1401
1435
 
1402
1436
  const { request, salt } = await buildConsumeNotesTransactionRequestFromNotes(
1403
- webClient,
1437
+ this.midenClient,
1404
1438
  fetchedNotes,
1405
1439
  this.proposalRequestOptions(options),
1406
1440
  );
1407
1441
 
1408
- const { summary, anchor } = await executeForSummary(webClient, this._accountId, request);
1442
+ const { summary, anchor } = await executeForSummary(this.midenClient, this._accountId, request);
1409
1443
  const chainAnchor = chainAnchorToBase64(anchor);
1410
1444
  anchor.free();
1411
1445
  const summaryBase64 = uint8ArrayToBase64(summary.serialize());
@@ -1452,7 +1486,6 @@ export class Multisig {
1452
1486
  ...legacyArgs: never[]
1453
1487
  ): Promise<Proposal> {
1454
1488
  const proposalNonce = resolveProposalNonce('createP2idProposal', options, legacyArgs);
1455
- const webClient = await this.getRawClient();
1456
1489
  if (amount <= 0n) {
1457
1490
  throw new Error('Amount must be greater than 0');
1458
1491
  }
@@ -1461,7 +1494,7 @@ export class Multisig {
1461
1494
  // CreateP2idProposalOptions can't be silently dropped before the builder.
1462
1495
  const { nonce: _nonce, ...noteOptions } = options;
1463
1496
  const { request, salt } = await buildP2idTransactionRequest(
1464
- webClient,
1497
+ this.midenClient,
1465
1498
  this._accountId,
1466
1499
  recipientId,
1467
1500
  faucetId,
@@ -1469,7 +1502,7 @@ export class Multisig {
1469
1502
  noteOptions,
1470
1503
  );
1471
1504
 
1472
- const { summary, anchor } = await executeForSummary(webClient, this._accountId, request);
1505
+ const { summary, anchor } = await executeForSummary(this.midenClient, this._accountId, request);
1473
1506
  const chainAnchor = chainAnchorToBase64(anchor);
1474
1507
  anchor.free();
1475
1508
  const summaryBase64 = uint8ArrayToBase64(summary.serialize());
@@ -1496,49 +1529,25 @@ export class Multisig {
1496
1529
  /**
1497
1530
  * Get notes that can be consumed by this multisig account.
1498
1531
  *
1499
- * Returns a list of notes that are committed on-chain and can be consumed
1500
- * immediately by the multisig account.
1532
+ * Returns the notes the Miden client's last sync screened as consumable by
1533
+ * this account right now; block-locked notes are left out.
1501
1534
  */
1502
1535
  async getConsumableNotes(): Promise<ConsumableNote[]> {
1503
- const accountId = AccountId.fromHex(this._accountId);
1504
- const webClient = await this.getRawClient();
1536
+ const records = await this.midenClient.notes.listAvailable({ account: this._accountId });
1505
1537
 
1506
- // Get consumable notes for this account
1507
- const consumableRecords = await webClient.getConsumableNotes(accountId);
1508
-
1509
- // Convert to our simplified ConsumableNote type
1510
1538
  const notes: ConsumableNote[] = [];
1511
- for (const record of consumableRecords) {
1512
- const inputNote = record.inputNoteRecord();
1513
- const consumability = record.noteConsumability();
1514
-
1515
- // Only include notes that can be consumed now (consumableAfterBlock is undefined/null)
1516
- const canConsumeNow = consumability.some(
1517
- (c) => c.accountId().toString().toLowerCase() === this._accountId.toLowerCase() &&
1518
- c.consumptionStatus().consumableAfterBlock() === undefined
1519
- );
1520
-
1521
- if (canConsumeNow) {
1522
- // Miden 0.15: InputNoteRecord.id() is `NoteId | undefined`; skip id-less records.
1523
- const id = inputNote.id();
1524
- if (id === undefined) {
1525
- continue;
1526
- }
1527
- const noteId = id.toString();
1528
- const details = inputNote.details();
1529
- const fungibleAssets = details.assets().fungibleAssets();
1530
-
1531
- // Extract assets
1532
- const assets: NoteAsset[] = [];
1533
- for (const asset of fungibleAssets) {
1534
- assets.push({
1535
- faucetId: asset.faucetId().toString(),
1536
- amount: asset.amount(),
1537
- });
1538
- }
1539
-
1540
- notes.push({ id: noteId, assets });
1539
+ for (const inputNote of records) {
1540
+ // Miden 0.15: InputNoteRecord.id() is `NoteId | undefined`; skip id-less records.
1541
+ const id = inputNote.id();
1542
+ if (id === undefined) {
1543
+ continue;
1541
1544
  }
1545
+ const assets: NoteAsset[] = inputNote
1546
+ .details()
1547
+ .assets()
1548
+ .fungibleAssets()
1549
+ .map((asset) => ({ faucetId: asset.faucetId().toString(), amount: asset.amount() }));
1550
+ notes.push({ id: id.toString(), assets });
1542
1551
  }
1543
1552
 
1544
1553
  return notes;
@@ -1562,12 +1571,11 @@ export class Multisig {
1562
1571
  * @returns Serialized note file bytes
1563
1572
  */
1564
1573
  async exportNoteToBytes(noteId: string): Promise<Uint8Array> {
1565
- const webClient = await this.getRawClient();
1566
1574
  const trimmedNoteId = noteId.trim();
1567
1575
 
1568
- let record;
1576
+ let record: OutputNoteRecord | undefined;
1569
1577
  try {
1570
- record = await webClient.getOutputNote(trimmedNoteId);
1578
+ [record] = await this.midenClient.notes.listSent({ ids: [trimmedNoteId] });
1571
1579
  } catch (err) {
1572
1580
  const detail = err instanceof Error ? err.message : String(err);
1573
1581
  throw new Error(
@@ -1583,7 +1591,7 @@ export class Multisig {
1583
1591
  const format = record.inclusionProof()
1584
1592
  ? NoteExportFormat.Full
1585
1593
  : NoteExportFormat.Details;
1586
- const noteFile = await webClient.exportNoteFile(trimmedNoteId, format);
1594
+ const noteFile = await this.midenClient.notes.export(trimmedNoteId, { format });
1587
1595
  return noteFile.serialize();
1588
1596
  }
1589
1597
 
@@ -1630,8 +1638,6 @@ export class Multisig {
1630
1638
  * commitment for a details-only file
1631
1639
  */
1632
1640
  async importNoteFromBytes(noteBytes: Uint8Array): Promise<string> {
1633
- const webClient = await this.getRawClient();
1634
-
1635
1641
  let noteFile: NoteFile;
1636
1642
  try {
1637
1643
  noteFile = NoteFile.deserialize(noteBytes);
@@ -1640,7 +1646,7 @@ export class Multisig {
1640
1646
  throw new Error(`failed to decode note file: ${detail}`);
1641
1647
  }
1642
1648
 
1643
- return webClient.importNoteFile(noteFile);
1649
+ return this.midenClient.notes.import(noteFile);
1644
1650
  }
1645
1651
 
1646
1652
  /**
@@ -1972,6 +1978,8 @@ export class Multisig {
1972
1978
 
1973
1979
  async signProposal(proposalId: string): Promise<Proposal> {
1974
1980
  const normalizedProposalId = normalizeHexWord(proposalId);
1981
+ // Verification re-executes the proposal at the store's sync height.
1982
+ await this.syncChain();
1975
1983
  const existingProposal = await this.getProposalForSigning(proposalId, normalizedProposalId);
1976
1984
  if (!existingProposal) {
1977
1985
  throw new Error(`Proposal not found: ${proposalId}`);
@@ -2019,9 +2027,14 @@ export class Multisig {
2019
2027
  return this.proposals.get(proposalId) ?? this.proposals.get(normalizedProposalId);
2020
2028
  }
2021
2029
 
2030
+ /**
2031
+ * Builds the final, fully signed request for a ready proposal, for a caller
2032
+ * that proves and submits it with its own pipeline. The request declares the
2033
+ * block its summary binds, so execute it at the chain tip, without an
2034
+ * anchor, on a client synced to at least that block.
2035
+ */
2022
2036
  async createTransactionProposalRequest(proposalId: string): Promise<TransactionRequest> {
2023
- const { finalRequest, anchor } = await this.prepareProposalExecution(proposalId);
2024
- anchor.free();
2037
+ const { finalRequest } = await this.prepareProposalExecution(proposalId);
2025
2038
  return finalRequest;
2026
2039
  }
2027
2040
 
@@ -2031,25 +2044,19 @@ export class Multisig {
2031
2044
  * @param proposalId - The proposal commitment/ID
2032
2045
  */
2033
2046
  async executeProposal(proposalId: string): Promise<void> {
2034
- const { metadata, finalRequest, proposal, anchor } =
2035
- await this.prepareProposalExecution(proposalId);
2036
-
2037
- try {
2038
- if (metadata.proposalType === 'switch_guardian') {
2039
- // #417: import notes embedded in pending proposals from the old
2040
- // GUARDIAN. Must run before the switch executes and repoints;
2041
- // best-effort and bounded — see preservePreSwitchProposalNotes.
2042
- await this.preservePreSwitchProposalNotes();
2043
- }
2047
+ const { metadata, finalRequest, proposal } = await this.prepareProposalExecution(proposalId);
2044
2048
 
2045
- // Execute at the proposal's anchored reference block, so the summary the
2046
- // cosigners signed reproduces exactly. The anchor was already checked
2047
- // against the summary's block commitment during binding verification.
2048
- await this.proverWorkflow.submitAt(AccountId.fromHex(this._accountId), finalRequest, anchor);
2049
- } finally {
2050
- anchor.free();
2049
+ if (metadata.proposalType === 'switch_guardian') {
2050
+ // #417: import notes embedded in pending proposals from the old
2051
+ // GUARDIAN. Must run before the switch executes and repoints;
2052
+ // best-effort and bounded — see preservePreSwitchProposalNotes.
2053
+ await this.preservePreSwitchProposalNotes();
2051
2054
  }
2052
2055
 
2056
+ // Execute at the chain tip. The request declares the block the signed
2057
+ // summary binds, so the summary the cosigners signed reproduces there.
2058
+ await this.submitAtTip(finalRequest);
2059
+
2053
2060
  if (metadata.proposalType === 'switch_guardian') {
2054
2061
  if (!metadata.newGuardianEndpoint || !metadata.newGuardianPubkey) {
2055
2062
  throw new Error('Switch GUARDIAN proposal metadata is incomplete after execution');
@@ -2109,11 +2116,12 @@ export class Multisig {
2109
2116
  * Submit an integration-built transaction (advice already injected). Mirrors
2110
2117
  * the Rust `submit_transaction`; used by the custom proposal producer flow
2111
2118
  * after `prepareCustomExecution` rebuilds its request with the returned advice.
2112
- * The transaction is executed at the proposal's anchored reference block,
2113
- * since the collected signatures only authorize the summary produced there.
2119
+ * The transaction is executed at the chain tip, so the request has to declare
2120
+ * the block its auth args bind (`feeAwareTransactionRequestBuilder` does).
2114
2121
  */
2115
2122
  async submitTransaction(proposalId: string, request: TransactionRequest): Promise<void> {
2116
2123
  const normalizedProposalId = normalizeHexWord(proposalId);
2124
+ await this.syncChain();
2117
2125
  const delta = await this.guardian.getDeltaProposal(this._accountId, normalizedProposalId);
2118
2126
  const existing = this.getLocalProposal(proposalId);
2119
2127
  const proposal = this.proposalFactory().fromDelta(
@@ -2135,11 +2143,33 @@ export class Multisig {
2135
2143
  `Proposal ${proposalId} chain anchor does not match the block commitment bound into its tx_summary`,
2136
2144
  );
2137
2145
  }
2138
-
2139
- await this.proverWorkflow.submitAt(AccountId.fromHex(this._accountId), request, anchor);
2140
2146
  } finally {
2141
2147
  anchor.free();
2142
2148
  }
2149
+ await this.submitAtTip(request);
2150
+ }
2151
+
2152
+ /**
2153
+ * Executes `request` at the chain tip, then proves, submits and applies it,
2154
+ * syncing first when this client is still below the block the request binds.
2155
+ */
2156
+ private async submitAtTip(request: TransactionRequest): Promise<void> {
2157
+ await prepareTipExecution(this.midenClient, request, () => this.syncChain());
2158
+ await this.proverWorkflow.submit(AccountId.fromHex(this._accountId), request);
2159
+ }
2160
+
2161
+ /**
2162
+ * Brings the Miden client to the chain tip before a proposal is re-executed:
2163
+ * an execution loads foreign accounts, the fee faucet among them, at the
2164
+ * store's sync height, which a node prunes about 50 blocks later.
2165
+ */
2166
+ private async syncChain(): Promise<void> {
2167
+ await retryRpcRead(() => this.midenClient.syncChain(), this.rpcConfig);
2168
+ }
2169
+
2170
+ /** {@link syncToBoundBlock} with this client's RPC retry policy. */
2171
+ private async syncToBoundBlock(boundBlockNum: number): Promise<void> {
2172
+ await syncToBoundBlock(this.midenClient, boundBlockNum, () => this.syncChain());
2143
2173
  }
2144
2174
 
2145
2175
  /**
@@ -2170,9 +2200,8 @@ export class Multisig {
2170
2200
  );
2171
2201
  }
2172
2202
 
2173
- const webClient = await this.getRawClient();
2174
2203
  const request = deserializeTransactionRequest(transactionRequestBytes);
2175
- const { summary, anchor } = await executeForSummary(webClient, this._accountId, request);
2204
+ const { summary, anchor } = await executeForSummary(this.midenClient, this._accountId, request);
2176
2205
  const chainAnchor = chainAnchorToBase64(anchor);
2177
2206
  anchor.free();
2178
2207
  const summaryBase64 = uint8ArrayToBase64(summary.serialize());
@@ -2203,6 +2232,8 @@ export class Multisig {
2203
2232
  transactionRequestBytes: Uint8Array,
2204
2233
  ): Promise<AdviceMap> {
2205
2234
  const normalizedProposalId = normalizeHexWord(proposalId);
2235
+ // The binding probe re-executes the producer's request at the store's sync height.
2236
+ await this.syncChain();
2206
2237
  const delta = await this.guardian.getDeltaProposal(this._accountId, normalizedProposalId);
2207
2238
  const existing = this.getLocalProposal(proposalId);
2208
2239
  const proposal = this.proposalFactory().fromDelta(
@@ -2237,13 +2268,11 @@ export class Multisig {
2237
2268
 
2238
2269
  const bindingRequest = deserializeTransactionRequest(transactionRequestBytes);
2239
2270
 
2240
- // Probe at the proposal's anchored reference block: the signed summary
2241
- // binds that block's commitment, so probing at the local sync height would
2242
- // never reproduce it. The anchor arrives from an untrusted party via
2243
- // GUARDIAN, so its block commitment is checked against the signed summary
2244
- // before executing against it.
2271
+ // The anchor arrives from an untrusted party via GUARDIAN, so its block
2272
+ // commitment is checked against the signed summary: it has to name the
2273
+ // block the summary binds. The probe itself runs at the chain tip, where
2274
+ // the request's declared bound block reproduces the signed summary.
2245
2275
  const anchor = this.requireProposalAnchor(proposalId, proposal.metadata);
2246
- let derivedCommitmentHex: string;
2247
2276
  try {
2248
2277
  const anchorCommitment = normalizeHexWord(anchor.commitment().toHex());
2249
2278
  const summaryBlockCommitment = normalizeHexWord(txSummary.blockCommitment().toHex());
@@ -2252,12 +2281,11 @@ export class Multisig {
2252
2281
  `Custom proposal ${proposalId} chain anchor does not match the block commitment bound into its tx_summary`,
2253
2282
  );
2254
2283
  }
2255
- const webClient = await this.getRawClient();
2256
- const derived = await executeForSummaryAt(webClient, this._accountId, bindingRequest, anchor);
2257
- derivedCommitmentHex = normalizeHexWord(derived.toCommitment().toHex());
2258
2284
  } finally {
2259
2285
  anchor.free();
2260
2286
  }
2287
+ const derived = await executeForSummaryAtTip(this.midenClient, this._accountId, bindingRequest);
2288
+ const derivedCommitmentHex = normalizeHexWord(derived.toCommitment().toHex());
2261
2289
  if (derivedCommitmentHex !== signedCommitmentHex) {
2262
2290
  throw new Error(
2263
2291
  `Custom proposal binding mismatch: expected ${signedCommitmentHex}, got ${derivedCommitmentHex}`,
@@ -2396,14 +2424,13 @@ export class Multisig {
2396
2424
  }
2397
2425
 
2398
2426
  /**
2399
- * The returned `anchor` is the block the request has to execute at; the
2400
- * caller owns it and frees it once submitted.
2427
+ * The returned request declares the block the signed summary binds, and
2428
+ * executes at the chain tip.
2401
2429
  */
2402
2430
  private async prepareProposalExecution(proposalId: string): Promise<{
2403
2431
  finalRequest: TransactionRequest;
2404
2432
  metadata: ProposalMetadata;
2405
2433
  proposal: Proposal;
2406
- anchor: ChainAnchor;
2407
2434
  }> {
2408
2435
  const proposal = this.getLocalProposal(proposalId);
2409
2436
  if (!proposal) {
@@ -2411,6 +2438,8 @@ export class Multisig {
2411
2438
  }
2412
2439
 
2413
2440
  this.proposalFactory().assertAccountId(proposal.accountId);
2441
+ // Verification and execution run at the store's sync height.
2442
+ await this.syncChain();
2414
2443
  await this.verifyProposalMetadataBinding(proposal);
2415
2444
 
2416
2445
  const metadata = proposal.metadata;
@@ -2560,17 +2589,21 @@ export class Multisig {
2560
2589
  }
2561
2590
 
2562
2591
  const anchor = this.requireProposalAnchor(proposalId, metadata);
2592
+ let binding: ProposalRequestBinding;
2563
2593
  try {
2564
- const finalRequest = await this.buildTransactionRequestFromMetadata(
2565
- metadata,
2566
- proposalRequestBinding(txSummary, anchor, saltHex),
2567
- adviceMap,
2568
- );
2569
- return { finalRequest, metadata, proposal, anchor };
2570
- } catch (error) {
2594
+ binding = proposalRequestBinding(txSummary, anchor, saltHex);
2595
+ } finally {
2571
2596
  anchor.free();
2572
- throw error;
2573
2597
  }
2598
+ // A switch_guardian proposal is verified without a rebuild, so this may be
2599
+ // the first time this client needs the chain at the bound block.
2600
+ await this.syncToBoundBlock(binding.boundBlockNum);
2601
+ const finalRequest = await this.buildTransactionRequestFromMetadata(
2602
+ metadata,
2603
+ binding,
2604
+ adviceMap,
2605
+ );
2606
+ return { finalRequest, metadata, proposal };
2574
2607
  }
2575
2608
 
2576
2609
  /**
@@ -2655,6 +2688,10 @@ export class Multisig {
2655
2688
 
2656
2689
  const proposal = this.proposalFactory().fromExported(exported);
2657
2690
 
2691
+ // Verification re-executes every built-in type at the store's sync height.
2692
+ if (proposal.metadata.proposalType !== 'custom') {
2693
+ await this.syncChain();
2694
+ }
2658
2695
  await this.verifyProposalMetadataBinding(proposal);
2659
2696
  this.proposals.set(proposal.id, proposal);
2660
2697
 
@@ -2695,6 +2732,10 @@ export class Multisig {
2695
2732
  throw new Error('You have already signed this proposal');
2696
2733
  }
2697
2734
 
2735
+ // Verification re-executes every built-in type at the store's sync height.
2736
+ if (proposal.metadata?.proposalType !== 'custom') {
2737
+ await this.syncChain();
2738
+ }
2698
2739
  const commitmentToSign = await this.verifyProposalMetadataBinding(proposal);
2699
2740
 
2700
2741
  // Sign the commitment
@@ -2758,7 +2799,7 @@ export class Multisig {
2758
2799
  } catch (error) {
2759
2800
  proposal.verification = {
2760
2801
  status: 'failed',
2761
- retryable: isTransientRpcError(error),
2802
+ retryable: isTransientRpcError(error) || isStaleChainError(error),
2762
2803
  message: error instanceof Error ? error.message : String(error),
2763
2804
  };
2764
2805
  throw error;
@@ -2770,10 +2811,11 @@ export class Multisig {
2770
2811
 
2771
2812
  const summary = TransactionSummary.deserialize(base64ToUint8Array(proposal.txSummary));
2772
2813
 
2773
- // The anchor arrives from an untrusted party via GUARDIAN, so check its
2774
- // block commitment against the one bound into the signed summary before
2775
- // anything executes against it. `ChainAnchor.deserialize` already enforced
2776
- // internal header/chain consistency.
2814
+ // The anchor arrives from an untrusted party via GUARDIAN, so check that it
2815
+ // names the block the signed summary binds: the rebuild below binds the
2816
+ // block it names. Nothing executes against it; the rebuild runs at the
2817
+ // chain tip. `ChainAnchor.deserialize` already enforced internal
2818
+ // header/chain consistency.
2777
2819
  const anchor = this.requireProposalAnchor(proposal.id, proposal.metadata);
2778
2820
  try {
2779
2821
  const anchorCommitment = normalizeHexWord(anchor.commitment().toHex());
@@ -2812,6 +2854,12 @@ export class Multisig {
2812
2854
  return txSummaryCommitment;
2813
2855
  }
2814
2856
 
2857
+ // The rebuild reads the chain's fee faucet from the synced protocol
2858
+ // configuration and executes at the tip, so the store has to have synced
2859
+ // to the block the summary binds; a cosigner that has only just loaded
2860
+ // the account has not.
2861
+ await this.syncToBoundBlock(binding.boundBlockNum);
2862
+
2815
2863
  // A consume-notes summary commits to *authenticated* consumption (see
2816
2864
  // ensureNotesAuthenticated), which miden-client decides from this store
2817
2865
  // alone. Put the store in that mode before the rebuild, or a cosigner
@@ -2824,8 +2872,7 @@ export class Multisig {
2824
2872
  }
2825
2873
 
2826
2874
  const request = await this.buildTransactionRequestFromMetadata(proposal.metadata, binding);
2827
- const webClient = await this.getRawClient();
2828
- const reconstructed = await executeForSummaryAt(webClient, this._accountId, request, anchor);
2875
+ const reconstructed = await executeForSummaryAtTip(this.midenClient, this._accountId, request);
2829
2876
  const reconstructedCommitment = normalizeHexWord(reconstructed.toCommitment().toHex());
2830
2877
 
2831
2878
  if (reconstructedCommitment !== txSummaryCommitment) {
@@ -2838,12 +2885,6 @@ export class Multisig {
2838
2885
  }
2839
2886
  }
2840
2887
 
2841
- /**
2842
- * Decodes a proposal's chain anchor. Throws when absent: a proposal without
2843
- * an anchor was created at an unknown reference block, so its signed summary
2844
- * cannot be reproduced, verified, or executed. The caller owns the returned
2845
- * anchor and must `free()` it once done.
2846
- */
2847
2888
  /**
2848
2889
  * Reads a proposal's salt. Throws when absent, because there is nothing to fall
2849
2890
  * back to.
@@ -2892,12 +2933,17 @@ export class Multisig {
2892
2933
  return saltHex;
2893
2934
  }
2894
2935
 
2936
+ /**
2937
+ * Decodes a proposal's chain anchor, which names the block its signed summary
2938
+ * binds. Throws when absent: every proposal carries one, so a proposal
2939
+ * without it is malformed and is neither verified nor executed. The caller
2940
+ * owns the returned anchor and must `free()` it once done.
2941
+ */
2895
2942
  private requireProposalAnchor(proposalId: string, metadata: ProposalMetadata): ChainAnchor {
2896
2943
  if (!metadata.chainAnchor) {
2897
2944
  throw new Error(
2898
- `Proposal ${proposalId} has no chain anchor; it was created without ` +
2899
- 'chain-anchored execution and its signed summary cannot be reproduced ' +
2900
- 'at the original reference block',
2945
+ `Proposal ${proposalId} has no chain anchor, which names the block its signed ` +
2946
+ 'summary binds; it cannot be verified or executed',
2901
2947
  );
2902
2948
  }
2903
2949
  return chainAnchorFromBase64(metadata.chainAnchor);
@@ -2916,8 +2962,7 @@ export class Multisig {
2916
2962
  if (expirationBlockNum === undefined) {
2917
2963
  return;
2918
2964
  }
2919
- const webClient = await this.getRawClient();
2920
- const syncHeight = await webClient.getSyncHeight();
2965
+ const syncHeight = await this.midenClient.getSyncHeight();
2921
2966
  if (syncHeight >= expirationBlockNum) {
2922
2967
  throw new Error(
2923
2968
  `Proposal ${proposalId} approval expired at block ${expirationBlockNum}; the chain is at ` +
@@ -2956,13 +3001,12 @@ export class Multisig {
2956
3001
  metadata: ProposalMetadata,
2957
3002
  requestOptions: MultisigRequestOptions,
2958
3003
  ): Promise<TransactionRequest> {
2959
- const webClient = await this.getRawClient();
2960
3004
  switch (metadata.proposalType) {
2961
3005
  case 'add_signer':
2962
3006
  case 'remove_signer':
2963
3007
  case 'change_threshold': {
2964
3008
  const { request } = await buildUpdateSignersTransactionRequest(
2965
- webClient,
3009
+ this.midenClient,
2966
3010
  metadata.targetThreshold,
2967
3011
  metadata.targetSignerCommitments,
2968
3012
  requestOptions,
@@ -2971,7 +3015,7 @@ export class Multisig {
2971
3015
  }
2972
3016
  case 'switch_guardian': {
2973
3017
  const { request } = await buildUpdateGuardianTransactionRequest(
2974
- webClient,
3018
+ this.midenClient,
2975
3019
  metadata.newGuardianPubkey,
2976
3020
  requestOptions,
2977
3021
  );
@@ -2979,7 +3023,7 @@ export class Multisig {
2979
3023
  }
2980
3024
  case 'update_procedure_threshold': {
2981
3025
  const { request } = await buildUpdateProcedureThresholdTransactionRequest(
2982
- webClient,
3026
+ this.midenClient,
2983
3027
  metadata.targetProcedure,
2984
3028
  metadata.targetThreshold,
2985
3029
  requestOptions,
@@ -2992,7 +3036,7 @@ export class Multisig {
2992
3036
  if (version === CONSUME_NOTES_METADATA_VERSION_V2) {
2993
3037
  const decoded = decodeEmbeddedConsumeNotes(metadata);
2994
3038
  const { request } = await buildConsumeNotesTransactionRequestFromNotes(
2995
- webClient,
3039
+ this.midenClient,
2996
3040
  decoded,
2997
3041
  requestOptions,
2998
3042
  );
@@ -3005,7 +3049,7 @@ export class Multisig {
3005
3049
  throw new UnsupportedMetadataVersionError(version);
3006
3050
  }
3007
3051
  const { request } = await buildConsumeNotesTransactionRequest(
3008
- webClient,
3052
+ this.midenClient,
3009
3053
  metadata.noteIds,
3010
3054
  requestOptions,
3011
3055
  );
@@ -3015,7 +3059,7 @@ export class Multisig {
3015
3059
  }
3016
3060
  case 'p2id': {
3017
3061
  const { request } = await buildP2idTransactionRequest(
3018
- webClient,
3062
+ this.midenClient,
3019
3063
  this._accountId,
3020
3064
  metadata.recipientId,
3021
3065
  metadata.faucetId,