Problem
The offline switch path exists so an account can leave an unreachable operator: executeProposal for switch_guardian uses the locally cached tx summary, skips the GUARDIAN ack, treats the old-operator push as best-effort, and registers on the new operator from local state. The Rust client exposes the full trio for this (create_proposal_offline / sign_imported_proposal / execute_imported_proposal, crates/miden-multisig-client/src/client/offline.rs), documented as "Only SwitchGuardian transactions can be executed fully offline."
The TS package has import/sign/execute (importProposal, signProposalOffline) but no creation counterpart. createSwitchGuardianProposal funnels into createProposal, which calls pushDeltaProposal against the account's current GUARDIAN before caching the proposal locally (packages/miden-multisig-client/src/multisig.ts). Against a dead operator the call throws and the locally built summary/metadata are lost — so the one proposal type designed to work without the current operator cannot be created without it.
A TS consumer can work around this by hand-assembling the ExportedProposal JSON for importProposal from the exported primitives (buildUpdateGuardianTransactionRequest, executeForSummary, chainAnchorToBase64), but the commitment has to be derived via the SDK (TransactionSummary.toCommitment()) because computeCommitmentFromTxSummary is not exported. This asymmetry is a direct contributor to 0xMiden/wallet#782, where the wallet routed the switch through the outgoing operator's HTTP API and an account got stuck on a network-dead operator.
Proposal
- Add
createSwitchGuardianProposalOffline(newGuardianEndpoint, newGuardianPubkey, options) (or a general createProposalOffline gated to switch, mirroring Rust): build the request and summary locally, verify the new endpoint's /pubkey commitment, include the proposer's signature, cache the proposal locally, and return the ExportedProposal — no push to the current operator.
- Export
computeCommitmentFromTxSummary (or document the SDK-based derivation) so hand-rolled flows are at least possible without guesswork.
- Unit tests mirroring the Rust offline tests: create → sign (second signer) → execute with the current-operator client pointed at an unreachable endpoint.
Problem
The offline switch path exists so an account can leave an unreachable operator:
executeProposalforswitch_guardianuses the locally cached tx summary, skips the GUARDIAN ack, treats the old-operator push as best-effort, and registers on the new operator from local state. The Rust client exposes the full trio for this (create_proposal_offline/sign_imported_proposal/execute_imported_proposal,crates/miden-multisig-client/src/client/offline.rs), documented as "OnlySwitchGuardiantransactions can be executed fully offline."The TS package has import/sign/execute (
importProposal,signProposalOffline) but no creation counterpart.createSwitchGuardianProposalfunnels intocreateProposal, which callspushDeltaProposalagainst the account's current GUARDIAN before caching the proposal locally (packages/miden-multisig-client/src/multisig.ts). Against a dead operator the call throws and the locally built summary/metadata are lost — so the one proposal type designed to work without the current operator cannot be created without it.A TS consumer can work around this by hand-assembling the
ExportedProposalJSON forimportProposalfrom the exported primitives (buildUpdateGuardianTransactionRequest,executeForSummary,chainAnchorToBase64), but the commitment has to be derived via the SDK (TransactionSummary.toCommitment()) becausecomputeCommitmentFromTxSummaryis not exported. This asymmetry is a direct contributor to 0xMiden/wallet#782, where the wallet routed the switch through the outgoing operator's HTTP API and an account got stuck on a network-dead operator.Proposal
createSwitchGuardianProposalOffline(newGuardianEndpoint, newGuardianPubkey, options)(or a generalcreateProposalOfflinegated to switch, mirroring Rust): build the request and summary locally, verify the new endpoint's/pubkeycommitment, include the proposer's signature, cache the proposal locally, and return theExportedProposal— no push to the current operator.computeCommitmentFromTxSummary(or document the SDK-based derivation) so hand-rolled flows are at least possible without guesswork.