Skip to main content

miden_multisig_client/
builder.rs

1//! Builder pattern for constructing MultisigClient instances.
2
3use std::path::PathBuf;
4use std::sync::Arc;
5
6use miden_client::DebugMode;
7use miden_client::builder::ClientBuilder;
8use miden_client::keystore::FilesystemKeyStore;
9use miden_client::rpc::Endpoint;
10use miden_client_sqlite_store::SqliteStore;
11use miden_protocol::crypto::dsa::ecdsa_k256_keccak::SigningKey as EcdsaSecretKey;
12use miden_protocol::crypto::dsa::falcon512_poseidon2::SecretKey;
13use miden_protocol::crypto::rand::RandomCoin;
14
15use crate::MidenSdkClient;
16use crate::client::MultisigClient;
17use crate::error::{MultisigError, Result};
18use crate::keystore::{EcdsaGuardianKeyStore, GuardianKeyStore, KeyManager};
19
20fn configured_client_builder(endpoint: &Endpoint) -> ClientBuilder<FilesystemKeyStore> {
21    if endpoint == &Endpoint::devnet() {
22        ClientBuilder::<FilesystemKeyStore>::for_devnet()
23    } else if endpoint == &Endpoint::testnet() {
24        ClientBuilder::<FilesystemKeyStore>::for_testnet()
25    } else if endpoint == &Endpoint::localhost() {
26        ClientBuilder::<FilesystemKeyStore>::for_localhost()
27    } else {
28        ClientBuilder::<FilesystemKeyStore>::new().grpc_client(endpoint, Some(20_000))
29    }
30}
31
32/// Builder for constructing MultisigClient instances.
33///
34/// # Example
35///
36/// ```ignore
37/// use miden_multisig_client::MultisigClient;
38/// use miden_client::rpc::Endpoint;
39///
40/// let client = MultisigClient::builder()
41///     .miden_endpoint(Endpoint::new("http://localhost:57291"))
42///     .guardian_endpoint("http://localhost:50051")
43///     .account_dir("/tmp/multisig-client")
44///     .generate_key()
45///     .build()
46///     .await?;
47/// ```
48pub struct MultisigClientBuilder {
49    miden_endpoint: Option<Endpoint>,
50    guardian_endpoint: Option<String>,
51    account_dir: Option<PathBuf>,
52    key_manager: Option<Arc<dyn KeyManager>>,
53}
54
55impl Default for MultisigClientBuilder {
56    fn default() -> Self {
57        Self::new()
58    }
59}
60
61impl MultisigClientBuilder {
62    /// Creates a new builder with default settings.
63    pub fn new() -> Self {
64        Self {
65            miden_endpoint: None,
66            guardian_endpoint: None,
67            account_dir: None,
68            key_manager: None,
69        }
70    }
71
72    /// Sets the Miden node RPC endpoint.
73    pub fn miden_endpoint(mut self, endpoint: Endpoint) -> Self {
74        self.miden_endpoint = Some(endpoint);
75        self
76    }
77
78    /// Sets the GUARDIAN server endpoint.
79    pub fn guardian_endpoint(mut self, endpoint: impl Into<String>) -> Self {
80        self.guardian_endpoint = Some(endpoint.into());
81        self
82    }
83
84    /// Sets the account directory for miden-client storage.
85    ///
86    /// This directory will contain the SQLite database for account and transaction data.
87    pub fn account_dir(mut self, path: impl Into<PathBuf>) -> Self {
88        self.account_dir = Some(path.into());
89        self
90    }
91
92    /// Sets a custom key manager for GUARDIAN authentication and proposal signing.
93    pub fn key_manager(mut self, key_manager: Box<dyn KeyManager>) -> Self {
94        self.key_manager = Some(key_manager.into());
95        self
96    }
97
98    /// Uses a FalconKeyStore with the given secret key.
99    pub fn with_secret_key(mut self, secret_key: SecretKey) -> Self {
100        self.key_manager = Some(Arc::new(GuardianKeyStore::new(secret_key)));
101        self
102    }
103
104    /// Uses an ECDSA key store with the given secret key.
105    pub fn with_ecdsa_secret_key(mut self, secret_key: EcdsaSecretKey) -> Self {
106        self.key_manager = Some(Arc::new(EcdsaGuardianKeyStore::new(secret_key)));
107        self
108    }
109
110    /// Generates a new random key for GUARDIAN authentication.
111    pub fn generate_key(mut self) -> Self {
112        self.key_manager = Some(Arc::new(GuardianKeyStore::generate()));
113        self
114    }
115
116    /// Generates a new random ECDSA key for GUARDIAN authentication.
117    pub fn generate_ecdsa_key(mut self) -> Self {
118        self.key_manager = Some(Arc::new(EcdsaGuardianKeyStore::generate()));
119        self
120    }
121
122    /// Builds the MultisigClient.
123    pub async fn build(self) -> Result<MultisigClient> {
124        let miden_endpoint = self
125            .miden_endpoint
126            .ok_or_else(|| MultisigError::MissingConfig("miden_endpoint".to_string()))?;
127
128        let guardian_endpoint = self
129            .guardian_endpoint
130            .ok_or_else(|| MultisigError::MissingConfig("guardian_endpoint".to_string()))?;
131
132        let account_dir = self
133            .account_dir
134            .ok_or_else(|| MultisigError::MissingConfig("account_dir".to_string()))?;
135
136        let key_manager = self.key_manager.ok_or(MultisigError::NoSigner)?;
137
138        // Ensure account directory exists
139        std::fs::create_dir_all(&account_dir).map_err(|e| {
140            MultisigError::MidenClient(format!("failed to create account dir: {}", e))
141        })?;
142
143        let miden_client = create_miden_client(&account_dir, &miden_endpoint).await?;
144
145        Ok(MultisigClient::new(
146            miden_client,
147            key_manager,
148            guardian_endpoint,
149            account_dir,
150            miden_endpoint,
151        ))
152    }
153}
154
155/// Creates a miden-client instance with SQLite storage.
156///
157/// Each call creates a fresh database with a unique filename to ensure
158/// no accumulated state from previous sessions.
159pub(crate) async fn create_miden_client(
160    account_dir: &std::path::Path,
161    endpoint: &Endpoint,
162) -> Result<MidenSdkClient> {
163    let timestamp = std::time::SystemTime::now()
164        .duration_since(std::time::UNIX_EPOCH)
165        .unwrap_or_default()
166        .as_millis();
167    let random_suffix: u32 = rand::random();
168    let store_path = account_dir.join(format!(
169        "miden-client-{}-{}.sqlite",
170        timestamp, random_suffix
171    ));
172    let store = SqliteStore::new(store_path)
173        .await
174        .map_err(|e| MultisigError::MidenClient(format!("failed to open SQLite store: {}", e)))?;
175    let store = Arc::new(store);
176
177    let rng_seed: [u32; 4] = rand::random();
178    let rng = Box::new(RandomCoin::new(rng_seed.into()));
179
180    configured_client_builder(endpoint)
181        .store(store)
182        .rng(rng)
183        .in_debug_mode(DebugMode::Enabled)
184        .tx_discard_delta(Some(20))
185        .max_block_number_delta(256)
186        .build()
187        .await
188        .map_err(|e| MultisigError::MidenClient(format!("failed to create miden client: {}", e)))
189}