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