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}