Skip to main content

miden_client_integration_tests/
config.rs

1use std::env::temp_dir;
2use std::fmt;
3use std::path::{Path, PathBuf};
4use std::str::FromStr;
5use std::sync::Arc;
6
7use anyhow::{Context, Result};
8use miden_client::RemoteTransactionProver;
9use miden_client::builder::ClientBuilder;
10use miden_client::grpc_support::{DEVNET_PROVER_ENDPOINT, TESTNET_PROVER_ENDPOINT};
11use miden_client::note_transport::grpc::GrpcNoteTransportClient;
12use miden_client::note_transport::{
13    NOTE_TRANSPORT_DEVNET_ENDPOINT,
14    NOTE_TRANSPORT_TESTNET_ENDPOINT,
15};
16use miden_client::rpc::{Endpoint, GrpcClient, VerifyingRpcClient};
17use miden_client::testing::common::{FilesystemKeyStore, TestClient, create_test_store_path};
18use miden_client::testing::fee::FeeFunder;
19use miden_client_sqlite_store::ClientBuilderSqliteExt;
20use uuid::Uuid;
21
22use crate::fee_funding;
23
24const NETWORK_DEVNET: &str = "devnet";
25const NETWORK_TESTNET: &str = "testnet";
26const NETWORK_LOCALHOST: &str = "localhost";
27
28/// Identifies the note transport service to connect to.
29#[derive(Clone, Debug)]
30pub enum NoteTransportEndpoint {
31    Devnet,
32    Testnet,
33    Custom(String),
34}
35
36impl NoteTransportEndpoint {
37    /// Returns the gRPC URL for this endpoint.
38    pub fn to_url(&self) -> String {
39        match self {
40            Self::Devnet => NOTE_TRANSPORT_DEVNET_ENDPOINT.to_string(),
41            Self::Testnet => NOTE_TRANSPORT_TESTNET_ENDPOINT.to_string(),
42            Self::Custom(url) => url.clone(),
43        }
44    }
45}
46
47impl FromStr for NoteTransportEndpoint {
48    type Err = std::convert::Infallible;
49
50    fn from_str(s: &str) -> Result<Self, Self::Err> {
51        Ok(match s.to_lowercase().as_str() {
52            "devnet" => Self::Devnet,
53            "testnet" => Self::Testnet,
54            _ => Self::Custom(s.to_string()),
55        })
56    }
57}
58
59impl fmt::Display for NoteTransportEndpoint {
60    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61        match self {
62            Self::Devnet => write!(f, "devnet ({NOTE_TRANSPORT_DEVNET_ENDPOINT})"),
63            Self::Testnet => write!(f, "testnet ({NOTE_TRANSPORT_TESTNET_ENDPOINT})"),
64            Self::Custom(url) => write!(f, "{url}"),
65        }
66    }
67}
68
69#[derive(Clone, Debug)]
70pub struct ClientConfig {
71    pub rpc_endpoint: Endpoint,
72    pub rpc_timeout_ms: u64,
73    /// Optional remote prover endpoint. If set, the client will use a remote prover instead of the
74    /// default local prover.
75    pub prover_endpoint: Option<String>,
76    /// Optional note transport endpoint. If set, the client will connect to a note transport
77    /// service.
78    pub note_transport_endpoint: Option<NoteTransportEndpoint>,
79    /// Funder the account-creating test helpers draw the native fee asset from. Shared by every
80    /// client built from this config and its clones, so consecutive payments from one wallet chain
81    /// off each other's nonce.
82    pub fee_funder: Option<Arc<dyn FeeFunder>>,
83}
84
85impl ClientConfig {
86    pub fn new(rpc_endpoint: Endpoint, rpc_timeout_ms: u64) -> Self {
87        Self {
88            rpc_endpoint,
89            rpc_timeout_ms,
90            prover_endpoint: None,
91            note_transport_endpoint: None,
92            fee_funder: None,
93        }
94    }
95
96    #[allow(clippy::return_self_not_must_use)]
97    pub fn with_prover_endpoint(mut self, prover_endpoint: Option<String>) -> Self {
98        self.prover_endpoint = prover_endpoint;
99        self
100    }
101
102    #[allow(clippy::return_self_not_must_use)]
103    pub fn with_note_transport_endpoint(
104        mut self,
105        note_transport_endpoint: Option<NoteTransportEndpoint>,
106    ) -> Self {
107        self.note_transport_endpoint = note_transport_endpoint;
108        self
109    }
110
111    /// Sets the funder the account-creating test helpers draw the native fee asset from.
112    #[allow(clippy::return_self_not_must_use)]
113    pub fn with_fee_funder(mut self, fee_funder: Option<Arc<dyn FeeFunder>>) -> Self {
114        self.fee_funder = fee_funder;
115        self
116    }
117
118    /// Loads the pre-funded wallets at `funders`, one `.mac` account file or a directory of them,
119    /// as the fee funder. A path naming no funder file leaves the config without one, which is all
120    /// a fee-free chain needs.
121    pub fn with_funders(self, funders: Option<&Path>) -> Result<Self> {
122        let fee_funder = fee_funding::load(&self, funders)?;
123        Ok(self.with_fee_funder(fee_funder))
124    }
125
126    /// Waits until a block carries every payment the fee funder has submitted.
127    pub async fn flush_funder(&self) -> Result<()> {
128        match &self.fee_funder {
129            Some(funder) => funder.flush().await,
130            None => Ok(()),
131        }
132    }
133
134    /// Creates a `TestClient` without syncing it, for tests that have to wait for the node first.
135    ///
136    /// The client gets its own store and keystore, the latter reachable through
137    /// `TestClient::keystore`. The store is a `SQLite` database at a temporary location, and the
138    /// keystore a temporary directory, both created here rather than held on the config, so every
139    /// client this is called on gets its own.
140    pub async fn into_unsynced_client(self) -> Result<TestClient> {
141        let fee_funder = self.fee_funder.clone();
142
143        let store_config = create_test_store_path();
144        let auth_path = create_test_auth_path();
145
146        let keystore = FilesystemKeyStore::new(auth_path.clone()).with_context(|| {
147            format!("failed to create keystore at path: {}", auth_path.to_string_lossy())
148        })?;
149
150        let rpc_client = Arc::new(VerifyingRpcClient::new(GrpcClient::new(
151            &self.rpc_endpoint,
152            self.rpc_timeout_ms,
153        )));
154
155        let mut builder = ClientBuilder::new()
156            .rpc(rpc_client)
157            .sqlite_store(store_config)
158            .authenticator(Arc::new(keystore))
159            .tx_discard_delta(None);
160
161        if let Some(prover_url) = &self.prover_endpoint {
162            builder = builder.prover(Arc::new(RemoteTransactionProver::new(prover_url)));
163        }
164
165        if let Some(transport) = &self.note_transport_endpoint {
166            let transport_url = transport.to_url();
167            let transport_timeout = std::env::var("MIDEN_TEST_TIMEOUT")
168                .ok()
169                .and_then(|s| s.parse::<u64>().ok())
170                .unwrap_or(10_000);
171            let nt_client =
172                Arc::new(GrpcNoteTransportClient::new(transport_url.clone(), transport_timeout));
173            builder = builder.note_transport(nt_client);
174        }
175
176        let client = builder.build().await.with_context(|| "failed to build test client")?;
177
178        Ok(TestClient::from(client).with_fee_funder(fee_funder))
179    }
180
181    /// Creates a `TestClient`.
182    ///
183    /// The client gets its own store and keystore, and is synced to the current state before being
184    /// returned.
185    pub async fn into_client(self) -> Result<TestClient> {
186        let mut client = self.into_unsynced_client().await?;
187
188        client.sync_state().await.with_context(|| "failed to sync client state")?;
189
190        Ok(client)
191    }
192}
193
194impl Default for ClientConfig {
195    /// Creates a default client config.
196    ///
197    /// `TEST_MIDEN_NETWORK` sets the top-level preset (defaults for all components):
198    /// - `testnet`: RPC testnet, remote prover testnet, note transport testnet
199    /// - `devnet`: RPC devnet, remote prover devnet, note transport devnet
200    ///
201    /// When unset, only RPC defaults to localhost (local prover, no note transport).
202    ///
203    /// Individual env vars override specific components:
204    /// - `TEST_MIDEN_RPC_URL`: overrides the RPC endpoint
205    /// - `TEST_MIDEN_PROVER_URL`: overrides the prover (`local` forces local prover)
206    /// - `TEST_MIDEN_NOTE_TRANSPORT_URL`: overrides the note transport endpoint
207    fn default() -> Self {
208        let network = std::env::var("TEST_MIDEN_NETWORK").ok();
209        let network_lower = network.map(|n| n.to_lowercase());
210
211        // Resolve RPC endpoint: TEST_MIDEN_RPC_URL overrides network preset. When no network is
212        // set, defaults to localhost.
213        let endpoint = if let Ok(rpc_url) = std::env::var("TEST_MIDEN_RPC_URL") {
214            Endpoint::try_from(rpc_url.as_str()).unwrap()
215        } else {
216            match network_lower.as_deref() {
217                Some(NETWORK_DEVNET) => Endpoint::devnet(),
218                Some(NETWORK_TESTNET) => Endpoint::testnet(),
219                Some(NETWORK_LOCALHOST) | None => Endpoint::localhost(),
220                Some(custom) => Endpoint::try_from(custom).unwrap(),
221            }
222        };
223
224        // Resolve prover: TEST_MIDEN_PROVER_URL overrides network preset. "localhost" forces local
225        // prover. Named values resolve to their URLs.
226        let prover_endpoint = if let Ok(url) = std::env::var("TEST_MIDEN_PROVER_URL") {
227            match url.to_lowercase().as_str() {
228                NETWORK_LOCALHOST => None,
229                NETWORK_DEVNET => Some(DEVNET_PROVER_ENDPOINT.to_string()),
230                NETWORK_TESTNET => Some(TESTNET_PROVER_ENDPOINT.to_string()),
231                _ => Some(url),
232            }
233        } else {
234            // Network preset defaults
235            match network_lower.as_deref() {
236                Some(NETWORK_TESTNET) => Some(TESTNET_PROVER_ENDPOINT.to_string()),
237                Some(NETWORK_DEVNET) => Some(DEVNET_PROVER_ENDPOINT.to_string()),
238                _ => None,
239            }
240        };
241
242        // Resolve note transport: TEST_MIDEN_NOTE_TRANSPORT_URL overrides network preset.
243        let note_transport_endpoint =
244            if let Ok(url) = std::env::var("TEST_MIDEN_NOTE_TRANSPORT_URL") {
245                Some(url.parse::<NoteTransportEndpoint>().unwrap())
246            } else {
247                // Network preset defaults
248                match network_lower.as_deref() {
249                    Some(NETWORK_TESTNET) => Some(NoteTransportEndpoint::Testnet),
250                    Some(NETWORK_DEVNET) => Some(NoteTransportEndpoint::Devnet),
251                    _ => None,
252                }
253            };
254
255        let timeout_ms = std::env::var("MIDEN_TEST_TIMEOUT")
256            .ok()
257            .and_then(|s| s.parse::<u64>().ok())
258            .unwrap_or(10_000);
259
260        Self::new(endpoint, timeout_ms)
261            .with_prover_endpoint(prover_endpoint)
262            .with_note_transport_endpoint(note_transport_endpoint)
263    }
264}
265
266/// Creates a fresh keystore directory, for a test building a client without a [`ClientConfig`].
267pub fn create_test_auth_path() -> PathBuf {
268    let auth_path = temp_dir().join(format!("keystore-{}", Uuid::new_v4()));
269    std::fs::create_dir_all(&auth_path).unwrap();
270    auth_path
271}