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}