1use std::path::PathBuf;
4use std::sync::Arc;
5
6use miden_client::DebugMode;
7use miden_client::builder::ClientBuilder;
8use miden_client::grpc_support::{
9 DEFAULT_GRPC_TIMEOUT_MS, DEVNET_PROVER_ENDPOINT, TESTNET_PROVER_ENDPOINT,
10};
11use miden_client::keystore::FilesystemKeyStore;
12use miden_client::note_transport::grpc::GrpcNoteTransportClient;
13use miden_client::note_transport::{
14 NOTE_TRANSPORT_DEVNET_ENDPOINT, NOTE_TRANSPORT_TESTNET_ENDPOINT, NoteTransportClient,
15};
16use miden_client::rpc::{Endpoint, GrpcClient, NodeRpcClient};
17use miden_client_sqlite_store::SqliteStore;
18use miden_protocol::crypto::dsa::ecdsa_k256_keccak::SigningKey as EcdsaSecretKey;
19use miden_protocol::crypto::dsa::falcon512_poseidon2::SecretKey;
20use miden_protocol::crypto::rand::RandomCoin;
21
22use crate::MidenSdkClient;
23use crate::client::MultisigClient;
24use crate::error::{MultisigError, Result};
25use crate::keystore::{EcdsaGuardianKeyStore, GuardianKeyStore, KeyManager};
26use crate::prover::{ProverConfig, ProverSelection, RetryingTransactionProver};
27use crate::rpc::{
28 RetryingNodeRpcClient, RpcConfig, RpcSelection, configured_note_transport_client,
29};
30
31pub(crate) fn configured_node_rpc_client(
40 endpoint: &Endpoint,
41 rpc_config: &RpcConfig,
42) -> Arc<dyn NodeRpcClient> {
43 let (timeout_ms, retry_policy) = match rpc_config.resolve(DEFAULT_GRPC_TIMEOUT_MS) {
44 RpcSelection::Passthrough => (DEFAULT_GRPC_TIMEOUT_MS, None),
45 RpcSelection::Configured {
46 timeout_ms,
47 retry_policy,
48 } => (timeout_ms, Some(retry_policy)),
49 };
50 let grpc: Arc<dyn NodeRpcClient> =
51 Arc::new(GrpcClient::new(endpoint, timeout_ms).with_max_retries(0));
52 match retry_policy {
53 Some(policy) if policy.max_attempts() > 1 => {
54 Arc::new(RetryingNodeRpcClient::new(grpc, &policy))
55 }
56 _ => grpc,
57 }
58}
59
60fn preset_note_transport_endpoint(endpoint: &Endpoint) -> Option<&'static str> {
61 if endpoint == &Endpoint::testnet() {
62 Some(NOTE_TRANSPORT_TESTNET_ENDPOINT)
63 } else if endpoint == &Endpoint::devnet() {
64 Some(NOTE_TRANSPORT_DEVNET_ENDPOINT)
65 } else {
66 None
67 }
68}
69
70fn resolved_note_transport_endpoint(
74 endpoint: &Endpoint,
75 note_transport_endpoint: Option<&str>,
76 selection: &RpcSelection,
77) -> Option<String> {
78 if let Some(url) = note_transport_endpoint {
79 return Some(url.to_string());
80 }
81 match selection {
82 RpcSelection::Passthrough => None,
83 RpcSelection::Configured { .. } => {
84 preset_note_transport_endpoint(endpoint).map(str::to_string)
85 }
86 }
87}
88
89fn configured_client_builder(
90 endpoint: &Endpoint,
91 note_transport_endpoint: Option<&str>,
92 prover_config: &ProverConfig,
93 rpc_config: &RpcConfig,
94) -> ClientBuilder<FilesystemKeyStore> {
95 let base = if endpoint == &Endpoint::devnet() {
96 ClientBuilder::<FilesystemKeyStore>::for_devnet()
97 } else if endpoint == &Endpoint::testnet() {
98 ClientBuilder::<FilesystemKeyStore>::for_testnet()
99 } else if endpoint == &Endpoint::localhost() {
100 ClientBuilder::<FilesystemKeyStore>::for_localhost()
101 } else {
102 ClientBuilder::<FilesystemKeyStore>::new()
103 };
104
105 let builder = base.rpc(configured_node_rpc_client(endpoint, rpc_config));
106
107 let selection = rpc_config.resolve(DEFAULT_GRPC_TIMEOUT_MS);
108 let builder =
109 match resolved_note_transport_endpoint(endpoint, note_transport_endpoint, &selection) {
110 Some(transport_endpoint) => {
111 let timeout_ms = match &selection {
112 RpcSelection::Passthrough => DEFAULT_GRPC_TIMEOUT_MS,
113 RpcSelection::Configured { timeout_ms, .. } => *timeout_ms,
114 };
115 let transport: Arc<dyn NoteTransportClient> =
116 Arc::new(GrpcNoteTransportClient::new(transport_endpoint, timeout_ms));
117 builder.note_transport(configured_note_transport_client(transport, rpc_config))
118 }
119 None => builder,
120 };
121
122 let default_remote = if endpoint == &Endpoint::devnet() {
123 Some(DEVNET_PROVER_ENDPOINT)
124 } else if endpoint == &Endpoint::testnet() {
125 Some(TESTNET_PROVER_ENDPOINT)
126 } else {
127 None
128 };
129
130 match prover_config.resolve(default_remote) {
131 ProverSelection::Local => builder,
132 ProverSelection::Remote {
133 endpoint,
134 custom: _,
135 retry_policy,
136 } => builder.prover(Arc::new(RetryingTransactionProver::remote(
137 endpoint,
138 retry_policy,
139 ))),
140 }
141}
142
143pub struct MultisigClientBuilder {
165 miden_endpoint: Option<Endpoint>,
166 note_transport_endpoint: Option<String>,
167 guardian_endpoint: Option<String>,
168 account_dir: Option<PathBuf>,
169 key_manager: Option<Arc<dyn KeyManager>>,
170 prover_config: ProverConfig,
171 rpc_config: RpcConfig,
172}
173
174impl Default for MultisigClientBuilder {
175 fn default() -> Self {
176 Self::new()
177 }
178}
179
180impl MultisigClientBuilder {
181 pub fn new() -> Self {
183 Self {
184 miden_endpoint: None,
185 note_transport_endpoint: None,
186 guardian_endpoint: None,
187 account_dir: None,
188 key_manager: None,
189 prover_config: ProverConfig::new(),
190 rpc_config: RpcConfig::new(),
191 }
192 }
193
194 pub fn miden_endpoint(mut self, endpoint: Endpoint) -> Self {
196 self.miden_endpoint = Some(endpoint);
197 self
198 }
199
200 pub fn note_transport_endpoint(mut self, endpoint: impl Into<String>) -> Self {
207 self.note_transport_endpoint = Some(endpoint.into());
208 self
209 }
210
211 pub fn guardian_endpoint(mut self, endpoint: impl Into<String>) -> Self {
213 self.guardian_endpoint = Some(endpoint.into());
214 self
215 }
216
217 pub fn prover_config(mut self, prover_config: ProverConfig) -> Self {
219 self.prover_config = prover_config;
220 self
221 }
222
223 pub fn rpc_config(mut self, rpc_config: RpcConfig) -> Self {
225 self.rpc_config = rpc_config;
226 self
227 }
228
229 pub fn account_dir(mut self, path: impl Into<PathBuf>) -> Self {
233 self.account_dir = Some(path.into());
234 self
235 }
236
237 pub fn key_manager(mut self, key_manager: Box<dyn KeyManager>) -> Self {
239 self.key_manager = Some(key_manager.into());
240 self
241 }
242
243 pub fn with_secret_key(mut self, secret_key: SecretKey) -> Self {
245 self.key_manager = Some(Arc::new(GuardianKeyStore::new(secret_key)));
246 self
247 }
248
249 pub fn with_ecdsa_secret_key(mut self, secret_key: EcdsaSecretKey) -> Self {
251 self.key_manager = Some(Arc::new(EcdsaGuardianKeyStore::new(secret_key)));
252 self
253 }
254
255 pub fn generate_key(mut self) -> Self {
257 self.key_manager = Some(Arc::new(GuardianKeyStore::generate()));
258 self
259 }
260
261 pub fn generate_ecdsa_key(mut self) -> Self {
263 self.key_manager = Some(Arc::new(EcdsaGuardianKeyStore::generate()));
264 self
265 }
266
267 pub async fn build(self) -> Result<MultisigClient> {
269 let miden_endpoint = self
270 .miden_endpoint
271 .ok_or_else(|| MultisigError::MissingConfig("miden_endpoint".to_string()))?;
272
273 let note_transport_endpoint = match self.note_transport_endpoint {
274 Some(endpoint) => {
275 let trimmed = endpoint.trim();
276 if trimmed.is_empty() {
277 return Err(MultisigError::InvalidConfig(
278 "note_transport_endpoint must not be empty".to_string(),
279 ));
280 }
281 Some(trimmed.to_string())
282 }
283 None => None,
284 };
285
286 let guardian_endpoint = self
287 .guardian_endpoint
288 .ok_or_else(|| MultisigError::MissingConfig("guardian_endpoint".to_string()))?;
289
290 let account_dir = self
291 .account_dir
292 .ok_or_else(|| MultisigError::MissingConfig("account_dir".to_string()))?;
293
294 let key_manager = self.key_manager.ok_or(MultisigError::NoSigner)?;
295
296 std::fs::create_dir_all(&account_dir).map_err(|e| {
298 MultisigError::MidenClient(format!("failed to create account dir: {}", e))
299 })?;
300
301 let miden_client = create_miden_client(
302 &account_dir,
303 &miden_endpoint,
304 note_transport_endpoint.as_deref(),
305 &self.prover_config,
306 &self.rpc_config,
307 )
308 .await?;
309
310 Ok(MultisigClient::new(
311 miden_client,
312 key_manager,
313 guardian_endpoint,
314 account_dir,
315 miden_endpoint,
316 note_transport_endpoint,
317 self.prover_config,
318 self.rpc_config,
319 ))
320 }
321}
322
323pub(crate) async fn create_miden_client(
328 account_dir: &std::path::Path,
329 endpoint: &Endpoint,
330 note_transport_endpoint: Option<&str>,
331 prover_config: &ProverConfig,
332 rpc_config: &RpcConfig,
333) -> Result<MidenSdkClient> {
334 let timestamp = std::time::SystemTime::now()
335 .duration_since(std::time::UNIX_EPOCH)
336 .unwrap_or_default()
337 .as_millis();
338 let random_suffix: u32 = rand::random();
339 let store_path = account_dir.join(format!(
340 "miden-client-{}-{}.sqlite",
341 timestamp, random_suffix
342 ));
343 let store = SqliteStore::new(store_path)
344 .await
345 .map_err(|e| MultisigError::MidenClient(format!("failed to open SQLite store: {}", e)))?;
346 let store = Arc::new(store);
347
348 let rng_seed: [u32; 4] = rand::random();
349 let rng = Box::new(RandomCoin::new(rng_seed.into()));
350
351 configured_client_builder(endpoint, note_transport_endpoint, prover_config, rpc_config)
352 .store(store)
353 .rng(rng)
354 .in_debug_mode(DebugMode::Enabled)
355 .tx_discard_delta(Some(20))
356 .max_block_number_delta(256)
357 .build()
358 .await
359 .map_err(|e| MultisigError::MidenClient(format!("failed to create miden client: {}", e)))
360}
361
362#[cfg(test)]
363mod tests {
364 use super::*;
365 use crate::rpc::RpcRetryPolicy;
366
367 fn custom_endpoint() -> Endpoint {
368 Endpoint::new("http".to_string(), "node".to_string(), Some(57291))
369 }
370
371 fn single_attempt_config() -> RpcConfig {
372 RpcConfig::new().with_retry_policy(RpcRetryPolicy::new(1))
373 }
374
375 fn selection(rpc_config: &RpcConfig) -> RpcSelection {
376 rpc_config.resolve(DEFAULT_GRPC_TIMEOUT_MS)
377 }
378
379 #[test]
380 fn note_transport_endpoints_exist_only_for_public_presets() {
381 assert_eq!(
382 preset_note_transport_endpoint(&Endpoint::testnet()),
383 Some(NOTE_TRANSPORT_TESTNET_ENDPOINT)
384 );
385 assert_eq!(
386 preset_note_transport_endpoint(&Endpoint::devnet()),
387 Some(NOTE_TRANSPORT_DEVNET_ENDPOINT)
388 );
389 assert_eq!(preset_note_transport_endpoint(&Endpoint::localhost()), None);
390 assert_eq!(preset_note_transport_endpoint(&custom_endpoint()), None);
391 }
392
393 #[test]
394 fn explicit_note_transport_endpoint_wires_regardless_of_rpc_config() {
395 assert_eq!(
396 resolved_note_transport_endpoint(
397 &custom_endpoint(),
398 Some("https://transport.internal"),
399 &selection(&single_attempt_config()),
400 ),
401 Some("https://transport.internal".to_string())
402 );
403 assert_eq!(
404 resolved_note_transport_endpoint(
405 &Endpoint::testnet(),
406 Some("https://transport.internal"),
407 &selection(&RpcConfig::new()),
408 ),
409 Some("https://transport.internal".to_string())
410 );
411 }
412
413 #[test]
414 fn preset_note_transport_wires_unless_rpc_config_is_passthrough() {
415 assert_eq!(
416 resolved_note_transport_endpoint(
417 &Endpoint::testnet(),
418 None,
419 &selection(&RpcConfig::new())
420 ),
421 Some(NOTE_TRANSPORT_TESTNET_ENDPOINT.to_string())
422 );
423 assert_eq!(
424 resolved_note_transport_endpoint(
425 &Endpoint::testnet(),
426 None,
427 &selection(&single_attempt_config())
428 ),
429 None
430 );
431 let timeout_only = single_attempt_config().with_timeout_ms(5_000).unwrap();
432 assert_eq!(
433 resolved_note_transport_endpoint(&Endpoint::testnet(), None, &selection(&timeout_only)),
434 Some(NOTE_TRANSPORT_TESTNET_ENDPOINT.to_string())
435 );
436 }
437
438 #[test]
439 fn custom_endpoint_without_override_has_no_note_transport() {
440 assert_eq!(
441 resolved_note_transport_endpoint(
442 &custom_endpoint(),
443 None,
444 &selection(&RpcConfig::new())
445 ),
446 None
447 );
448 }
449
450 #[tokio::test]
456 async fn the_inner_miden_client_retry_loop_stays_disabled_over_a_real_wire() {
457 use std::sync::atomic::{AtomicU32, Ordering};
458
459 let calls = Arc::new(AtomicU32::new(0));
460 let node = miden_rpc_client::test_node::ScriptedNode::failing(
461 u32::MAX,
462 || tonic::Status::resource_exhausted("Too Many Requests!"),
463 calls.clone(),
464 );
465 let url = miden_rpc_client::test_node::serve(node).await;
466 let address = url
467 .strip_prefix("http://")
468 .expect("scripted node is plain http");
469 let (host, port) = address.rsplit_once(':').expect("endpoint carries a port");
470 let endpoint = Endpoint::new(
471 "http".to_string(),
472 host.to_string(),
473 Some(port.parse().expect("ephemeral port parses")),
474 );
475
476 let rpc_config = RpcConfig::new().with_retry_policy(RpcRetryPolicy::new(2));
477 let client = configured_node_rpc_client(&endpoint, &rpc_config);
478
479 client
480 .get_rpc_limits()
481 .await
482 .expect_err("the scripted node always rate-limits");
483
484 assert_eq!(calls.load(Ordering::SeqCst), 2);
485 }
486}