Skip to main content

miden_multisig_client/client/
mod.rs

1//! Main MultisigClient implementation.
2//!
3//! This module provides the [`MultisigClient`] type for interacting with multisig accounts.
4//! The implementation is split across submodules for better organization:
5//!
6//! - `account` - Account lifecycle operations (create, pull, push, sync)
7//! - `proposals` - Proposal workflow (list, sign, execute, propose)
8//! - `offline` - Offline proposal operations
9//! - `notes` - Note filtering and listing
10//! - `io` - Export/import functionality
11//! - `helpers` - Internal GUARDIAN client helpers
12
13mod account;
14mod helpers;
15mod io;
16mod notes;
17mod offline;
18mod proposals;
19pub use proposals::{AbandonRequestState, AbandonStatus};
20
21use std::path::PathBuf;
22use std::sync::Arc;
23
24use guardian_client::GetStateResponse;
25use miden_client::rpc::Endpoint;
26use miden_protocol::Word;
27use miden_protocol::account::AccountId;
28
29use crate::MidenSdkClient;
30use crate::account::MultisigAccount;
31use crate::builder::MultisigClientBuilder;
32use crate::error::{MultisigError, Result};
33use crate::export::ExportedProposal;
34use crate::keystore::KeyManager;
35use crate::proposal::Proposal;
36use crate::prover::ProverConfig;
37use crate::rpc::RpcConfig;
38
39pub use notes::{ConsumableNote, NoteFilter};
40
41/// Result of a proposal creation attempt.
42///
43/// When creating a proposal, it may either succeed online (via GUARDIAN) or
44/// fall back to offline mode if GUARDIAN is unavailable.
45#[derive(Debug)]
46pub enum ProposalResult {
47    /// Proposal successfully created on GUARDIAN and ready for cosigners to sign.
48    Online(Box<Proposal>),
49    /// Offline proposal created when GUARDIAN is unavailable (`SwitchGuardian` transactions only).
50    Offline(Box<ExportedProposal>),
51}
52
53/// Result of explicit local-vs-on-chain account state verification.
54#[derive(Debug, Clone, PartialEq, Eq)]
55pub struct StateVerificationResult {
56    /// Account ID that was verified.
57    pub account_id: AccountId,
58    /// Local account commitment hex (with 0x prefix).
59    pub local_commitment_hex: String,
60    /// On-chain account commitment hex (with 0x prefix).
61    pub on_chain_commitment_hex: String,
62}
63
64/// Main client for interacting with multisig accounts.
65///
66/// This client manages a single multisig account connected to a GUARDIAN server,
67/// providing a high-level API for creating and managing multisig accounts,
68/// proposals, and transactions.
69///
70/// # Example
71///
72/// ```ignore
73/// use miden_multisig_client::MultisigClient;
74/// use miden_client::rpc::Endpoint;
75///
76///
77/// let mut client = MultisigClient::builder()
78///     .miden_endpoint(Endpoint::new("http://localhost:57291"))
79///     .guardian_endpoint("http://localhost:50051")
80///     .data_dir("/tmp/multisig")
81///     .generate_key()
82///     .build()
83///     .await?;
84///
85///
86/// let account = client.create_account(2, vec![signer1, signer2]).await?;
87/// ```
88pub struct MultisigClient {
89    pub(crate) miden_client: MidenSdkClient,
90    pub(crate) key_manager: Arc<dyn KeyManager>,
91    /// Guardian server endpoint.
92    pub(crate) guardian_endpoint: String,
93    /// The multisig account managed by this client.
94    pub(crate) account: Option<MultisigAccount>,
95    /// Account directory for miden-client storage (for recovery).
96    pub(crate) account_dir: PathBuf,
97    /// Miden node endpoint (for recovery).
98    pub(crate) miden_endpoint: Endpoint,
99    /// Note transport endpoint override (for recovery).
100    pub(crate) note_transport_endpoint: Option<String>,
101    /// Node client for direct commitment reads, built once so its channel is
102    /// reused across reads.
103    node_rpc_client: Arc<dyn miden_client::rpc::NodeRpcClient>,
104    /// Prover selection and retry configuration (for recovery).
105    pub(crate) prover_config: ProverConfig,
106    /// Node RPC timeout and read-retry configuration (for recovery).
107    pub(crate) rpc_config: RpcConfig,
108}
109
110impl MultisigClient {
111    /// Creates a new MultisigClientBuilder.
112    pub fn builder() -> MultisigClientBuilder {
113        MultisigClientBuilder::new()
114    }
115
116    /// Creates a new MultisigClient (internal use, prefer builder).
117    #[allow(clippy::too_many_arguments)]
118    pub(crate) fn new(
119        miden_client: MidenSdkClient,
120        key_manager: Arc<dyn KeyManager>,
121        guardian_endpoint: String,
122        account_dir: PathBuf,
123        miden_endpoint: Endpoint,
124        note_transport_endpoint: Option<String>,
125        prover_config: ProverConfig,
126        rpc_config: RpcConfig,
127    ) -> Self {
128        let node_rpc_client =
129            crate::builder::configured_node_rpc_client(&miden_endpoint, &rpc_config);
130        Self {
131            miden_client,
132            key_manager,
133            guardian_endpoint,
134            account: None,
135            account_dir,
136            miden_endpoint,
137            note_transport_endpoint,
138            node_rpc_client,
139            prover_config,
140            rpc_config,
141        }
142    }
143
144    pub(crate) fn node_rpc_client(&self) -> Arc<dyn miden_client::rpc::NodeRpcClient> {
145        Arc::clone(&self.node_rpc_client)
146    }
147
148    /// Returns the GUARDIAN endpoint.
149    pub fn guardian_endpoint(&self) -> &str {
150        &self.guardian_endpoint
151    }
152
153    /// Returns the current account, if any.
154    pub fn account(&self) -> Option<&MultisigAccount> {
155        self.account.as_ref()
156    }
157
158    /// Returns the current account ID, if any.
159    pub fn account_id(&self) -> Option<AccountId> {
160        self.account.as_ref().map(|a| a.id())
161    }
162
163    /// Returns true if an account is loaded.
164    pub fn has_account(&self) -> bool {
165        self.account.is_some()
166    }
167
168    /// Returns the user's public key commitment as a Word.
169    pub fn user_commitment(&self) -> Word {
170        self.key_manager.commitment()
171    }
172
173    /// Returns the user's public key commitment as a hex string.
174    pub fn user_commitment_hex(&self) -> String {
175        self.key_manager.commitment_hex()
176    }
177
178    /// Returns a reference to the key manager.
179    pub fn key_manager(&self) -> &dyn KeyManager {
180        self.key_manager.as_ref()
181    }
182
183    /// Recover the set of accounts the configured signer authorizes by
184    /// querying GUARDIAN's `/state/lookup` endpoint and fetching state for
185    /// each match. Mirrors `MultisigClient.recoverByKey` in the TS SDK.
186    /// Returns an empty list when no account on the configured GUARDIAN
187    /// authorizes this commitment (distinct from "wrong key", which fails
188    /// authentication first).
189    pub async fn recover_by_key(&self) -> Result<Vec<RecoveredAccount>> {
190        let mut guardian_client = self.create_authenticated_guardian_client().await?;
191        let commitment_hex = self.user_commitment_hex();
192
193        let lookup = guardian_client
194            .lookup_account_by_key_commitment(&commitment_hex)
195            .await
196            .map_err(|e| MultisigError::GuardianServer(format!("lookup failed: {}", e)))?;
197
198        let mut recovered = Vec::with_capacity(lookup.accounts.len());
199        for entry in lookup.accounts {
200            let account_id = AccountId::from_hex(&entry.account_id).map_err(|e| {
201                MultisigError::InvalidConfig(format!(
202                    "GUARDIAN returned non-AccountId hex '{}': {}",
203                    entry.account_id, e
204                ))
205            })?;
206            let state = guardian_client.get_state(&account_id).await.map_err(|e| {
207                MultisigError::GuardianServer(format!(
208                    "get_state failed for {}: {}",
209                    entry.account_id, e
210                ))
211            })?;
212            recovered.push(RecoveredAccount {
213                account_id: entry.account_id,
214                state,
215            });
216        }
217        Ok(recovered)
218    }
219}
220
221/// One match returned by [`MultisigClient::recover_by_key`]. Pairs the
222/// discovered `account_id` with the current state response so callers do not
223/// need to do a second round-trip per account.
224#[derive(Debug, Clone)]
225pub struct RecoveredAccount {
226    pub account_id: String,
227    pub state: GetStateResponse,
228}