lexe/
config.rs

1use std::{
2    borrow::Cow,
3    fmt,
4    path::{Path, PathBuf},
5    sync::LazyLock,
6};
7
8use anyhow::Context;
9use common::{
10    api::user::UserPk, env::DeployEnv, ln::network::LxNetwork,
11    root_seed::RootSeed,
12};
13use node_client::credentials::CredentialsRef;
14
15use crate::unstable::provision;
16
17/// The user agent string used for SDK requests to Lexe infrastructure.
18///
19/// Format: `lexe/<sdk_version> node/<latest_node_version>`
20///
21/// Example: `lexe/0.1.0 node/0.8.11`
22pub static SDK_USER_AGENT: LazyLock<&'static str> = LazyLock::new(|| {
23    // Get the latest node version.
24    let releases = provision::releases_json();
25    let node_releases =
26        releases.0.get("node").expect("No 'node' in releases.json");
27    let (latest_node_version, _release) =
28        node_releases.last_key_value().expect("No node releases");
29
30    let sdk_with_version = lexe_api_core::user_agent_to_lexe!();
31    let user_agent = format!("{sdk_with_version} node/{latest_node_version}");
32
33    Box::leak(user_agent.into_boxed_str())
34});
35
36// --- Structs --- //
37//
38// - WalletEnv (`wallet_env`)
39// - WalletEnvConfig (`env_config`)
40// - WalletUserConfig (`user_config`)
41// - WalletEnvDbConfig (`env_db_config`)
42// - WalletUserDbConfig (`user_db_config`)
43
44/// A wallet environment, e.g. "prod-mainnet-true".
45///
46/// We use this to disambiguate persisted state and secrets so we don't
47/// accidentally clobber state when testing across e.g. testnet vs regtest.
48#[derive(Copy, Clone, Debug, Eq, PartialEq)]
49pub struct WalletEnv {
50    /// Prod, staging, or dev.
51    pub deploy_env: DeployEnv,
52    /// The Bitcoin network: mainnet, testnet, or regtest.
53    pub network: LxNetwork,
54    /// Whether our node should be running in a real SGX enclave.
55    /// Set to [`true`] for prod and staging.
56    pub use_sgx: bool,
57}
58
59/// A configuration for a wallet environment.
60#[derive(Clone, Debug, Eq, PartialEq)]
61pub struct WalletEnvConfig {
62    /// The wallet environment.
63    pub wallet_env: WalletEnv,
64    // NOTE(unstable): Fields should stay pub(crate) until API is more mature
65    pub(crate) gateway_url: Cow<'static, str>,
66    pub(crate) user_agent: Cow<'static, str>,
67}
68
69/// A wallet configuration for a specific user and wallet environment.
70#[derive(Clone)]
71pub struct WalletUserConfig {
72    /// The user public key.
73    pub user_pk: UserPk,
74    /// The configuration for the wallet environment.
75    pub env_config: WalletEnvConfig,
76}
77
78/// Database directory configuration for a specific wallet environment.
79#[derive(Clone)]
80pub struct WalletEnvDbConfig {
81    // NOTE(unstable): Fields should stay pub(crate) until API is more mature
82    /// The base data directory for all Lexe-related data.
83    /// Holds data for different app environments and users.
84    pub(crate) lexe_data_dir: PathBuf,
85    /// Database directory for a specific wallet environment.
86    /// Holds data for all users within that environment.
87    ///
88    /// `<lexe_data_dir>/<deploy_env>-<network>-<use_sgx>`
89    pub(crate) env_db_dir: PathBuf,
90}
91
92/// Database directory configuration for a specific user and wallet environment.
93#[derive(Clone)]
94pub struct WalletUserDbConfig {
95    // NOTE(unstable): Fields should stay pub(crate) until API is more mature
96    /// Environment-level database configuration.
97    pub(crate) env_db_config: WalletEnvDbConfig,
98    /// The user public key.
99    pub(crate) user_pk: UserPk,
100    /// Database directory for a specific user.
101    /// Contains user-specific data like payments, settings, etc.
102    ///
103    /// `<lexe_data_dir>/<deploy_env>-<network>-<use_sgx>/<user_pk>`
104    pub(crate) user_db_dir: PathBuf,
105}
106
107// --- impl WalletEnv --- //
108
109impl WalletEnv {
110    /// Bitcoin mainnet environment (prod, SGX).
111    pub fn mainnet() -> Self {
112        Self {
113            deploy_env: DeployEnv::Prod,
114            network: LxNetwork::Mainnet,
115            use_sgx: true,
116        }
117    }
118
119    /// Bitcoin testnet3 environment (staging, SGX).
120    pub fn testnet3() -> Self {
121        Self {
122            deploy_env: DeployEnv::Staging,
123            network: LxNetwork::Testnet3,
124            use_sgx: true,
125        }
126    }
127
128    /// Regtest environment (dev, configurable SGX).
129    pub fn regtest(use_sgx: bool) -> Self {
130        Self {
131            deploy_env: DeployEnv::Dev,
132            network: LxNetwork::Regtest,
133            use_sgx,
134        }
135    }
136
137    // --- Seedphrase file I/O --- //
138
139    /// Returns the path to the seedphrase file for this environment.
140    ///
141    /// - Mainnet (prod): `<data_dir>/seedphrase.txt`
142    /// - Other environments: `<data_dir>/seedphrase.<wallet_env>.txt`
143    pub fn seedphrase_path(&self, data_dir: &Path) -> PathBuf {
144        let filename = if self.deploy_env == DeployEnv::Prod {
145            Cow::Borrowed("seedphrase.txt")
146        } else {
147            Cow::Owned(format!("seedphrase.{self}.txt"))
148        };
149        data_dir.join(filename.as_ref())
150    }
151
152    /// Reads a [`RootSeed`] from `~/.lexe/seedphrase[.env].txt`.
153    ///
154    /// Returns `Ok(None)` if the file doesn't exist.
155    pub fn read_seed(&self) -> anyhow::Result<Option<RootSeed>> {
156        let lexe_data_dir = common::default_lexe_data_dir()
157            .context("Could not get default lexe data dir")?;
158        let path = self.seedphrase_path(&lexe_data_dir);
159        RootSeed::read_from_path(&path)
160    }
161
162    /// Writes a [`RootSeed`]'s mnemonic to `~/.lexe/seedphrase[.env].txt`.
163    ///
164    /// Creates parent directories if needed. Fails if the file already exists.
165    pub fn write_seed(&self, root_seed: &RootSeed) -> anyhow::Result<()> {
166        let lexe_data_dir = common::default_lexe_data_dir()
167            .context("Could not get default lexe data dir")?;
168        let path = self.seedphrase_path(&lexe_data_dir);
169        root_seed.write_to_path(&path)
170    }
171}
172
173impl fmt::Display for WalletEnv {
174    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
175        let deploy_env = self.deploy_env.as_str();
176        let network = self.network.as_str();
177        let sgx = if self.use_sgx { "sgx" } else { "dbg" };
178        write!(f, "{deploy_env}-{network}-{sgx}")
179    }
180}
181
182// --- impl WalletEnvConfig --- //
183
184impl WalletEnvConfig {
185    /// Standard wallet environment configuration for Bitcoin mainnet.
186    pub fn mainnet() -> Self {
187        let wallet_env = WalletEnv::mainnet();
188        Self {
189            gateway_url: wallet_env.deploy_env.gateway_url(None),
190            user_agent: Cow::Borrowed(*SDK_USER_AGENT),
191            wallet_env,
192        }
193    }
194
195    /// Standard wallet environment configuration for Bitcoin testnet3.
196    pub fn testnet3() -> Self {
197        let wallet_env = WalletEnv::testnet3();
198        Self {
199            gateway_url: wallet_env.deploy_env.gateway_url(None),
200            user_agent: Cow::Borrowed(*SDK_USER_AGENT),
201            wallet_env,
202        }
203    }
204
205    /// Regtest configuration for local development and testing.
206    ///
207    /// - `use_sgx`: Whether to use SGX enclaves.
208    /// - `gateway_url`: Custom gateway URL. If `None`, uses the default dev
209    ///   URL.
210    pub fn regtest(
211        use_sgx: bool,
212        gateway_url: Option<impl Into<Cow<'static, str>>>,
213    ) -> Self {
214        let wallet_env = WalletEnv::regtest(use_sgx);
215        let gateway_url = gateway_url.map(Into::into);
216        Self {
217            gateway_url: wallet_env.deploy_env.gateway_url(gateway_url),
218            user_agent: Cow::Borrowed(*SDK_USER_AGENT),
219            wallet_env,
220        }
221    }
222
223    /// Construct a [`WalletEnvConfig`].
224    #[cfg(feature = "unstable")]
225    pub fn new(
226        wallet_env: WalletEnv,
227        gateway_url: Cow<'static, str>,
228        user_agent: Cow<'static, str>,
229    ) -> Self {
230        Self {
231            wallet_env,
232            gateway_url,
233            user_agent,
234        }
235    }
236
237    /// Get a [`WalletEnvConfig`] for production.
238    //
239    // This is unstable because "Deploy environment" is an internal concept and
240    // should not be exposed to SDK users.
241    #[cfg(feature = "unstable")]
242    pub fn prod() -> Self {
243        Self::mainnet()
244    }
245
246    /// Get a [`WalletEnvConfig`] for staging.
247    //
248    // This is unstable because "Deploy environment" is an internal concept and
249    // should not be exposed to SDK users.
250    #[cfg(feature = "unstable")]
251    pub fn staging() -> Self {
252        Self::testnet3()
253    }
254
255    /// Get a [`WalletEnvConfig`] for dev/testing.
256    //
257    // This is unstable because "Deploy environment" is an internal concept and
258    // should not be exposed to SDK users.
259    #[cfg(feature = "unstable")]
260    pub fn dev(
261        use_sgx: bool,
262        gateway_url: Option<impl Into<Cow<'static, str>>>,
263    ) -> Self {
264        Self::regtest(use_sgx, gateway_url)
265    }
266
267    /// The gateway URL.
268    #[cfg(feature = "unstable")]
269    pub fn gateway_url(&self) -> &str {
270        &self.gateway_url
271    }
272
273    /// The user agent string.
274    #[cfg(feature = "unstable")]
275    pub fn user_agent(&self) -> &str {
276        &self.user_agent
277    }
278
279    /// Returns the path to the seedphrase file for this environment.
280    pub fn seedphrase_path(&self, data_dir: &Path) -> PathBuf {
281        self.wallet_env.seedphrase_path(data_dir)
282    }
283
284    /// Reads a [`RootSeed`] from `~/.lexe/seedphrase[.env].txt`.
285    ///
286    /// Returns `Ok(None)` if the file doesn't exist.
287    pub fn read_seed(&self) -> anyhow::Result<Option<RootSeed>> {
288        self.wallet_env.read_seed()
289    }
290
291    /// Writes a [`RootSeed`]'s mnemonic to `~/.lexe/seedphrase[.env].txt`.
292    ///
293    /// Creates parent directories if needed. Fails if the file already exists.
294    pub fn write_seed(&self, root_seed: &RootSeed) -> anyhow::Result<()> {
295        self.wallet_env.write_seed(root_seed)
296    }
297}
298
299// --- impl WalletEnvDbConfig --- //
300
301impl WalletEnvDbConfig {
302    /// Construct a new [`WalletEnvDbConfig`] from the wallet environment and
303    /// base data directory.
304    pub fn new(wallet_env: WalletEnv, lexe_data_dir: PathBuf) -> Self {
305        let env_db_dir = lexe_data_dir.join(wallet_env.to_string());
306        Self {
307            lexe_data_dir,
308            env_db_dir,
309        }
310    }
311
312    /// The top-level, root, base data directory for Lexe-related data.
313    pub fn lexe_data_dir(&self) -> &PathBuf {
314        &self.lexe_data_dir
315    }
316
317    /// The database directory for this wallet environment.
318    pub fn env_db_dir(&self) -> &PathBuf {
319        &self.env_db_dir
320    }
321}
322
323// --- impl WalletUserDbConfig --- //
324
325impl WalletUserDbConfig {
326    /// Construct a new [`WalletUserDbConfig`] from the environment database
327    /// config and user public key.
328    pub fn new(env_db_config: WalletEnvDbConfig, user_pk: UserPk) -> Self {
329        let user_db_dir = env_db_config.env_db_dir.join(user_pk.to_string());
330        Self {
331            env_db_config,
332            user_pk,
333            user_db_dir,
334        }
335    }
336
337    /// Construct a new [`WalletUserDbConfig`] from credentials and the
338    /// environment database config.
339    pub fn from_credentials(
340        credentials: CredentialsRef<'_>,
341        env_db_config: WalletEnvDbConfig,
342    ) -> anyhow::Result<Self> {
343        // Is `Some(_)` if the credentials were created by `node-v0.8.11+`.
344        let user_pk = credentials.user_pk().context(
345            "Client credentials are out of date. \
346             Please create a new one from within the Lexe wallet app.",
347        )?;
348        Ok(Self::new(env_db_config, user_pk))
349    }
350
351    /// The environment-level database configuration.
352    pub fn env_db_config(&self) -> &WalletEnvDbConfig {
353        &self.env_db_config
354    }
355
356    /// The user public key.
357    pub fn user_pk(&self) -> UserPk {
358        self.user_pk
359    }
360
361    /// The top-level, root, base data directory for Lexe-related data.
362    ///
363    /// `<lexe_data_dir>`
364    pub fn lexe_data_dir(&self) -> &PathBuf {
365        self.env_db_config.lexe_data_dir()
366    }
367
368    /// The database directory for this wallet environment.
369    ///
370    /// `<lexe_data_dir>/<deploy_env>-<network>-<use_sgx>`
371    pub fn env_db_dir(&self) -> &PathBuf {
372        self.env_db_config.env_db_dir()
373    }
374
375    /// The user-specific database directory.
376    ///
377    /// `<lexe_data_dir>/<deploy_env>-<network>-<use_sgx>/<user_pk>`
378    pub fn user_db_dir(&self) -> &PathBuf {
379        &self.user_db_dir
380    }
381
382    /// Payment records and history.
383    ///
384    /// `<lexe_data_dir>/<deploy_env>-<network>-<use_sgx>/<user_pk>/payments_db`
385    // Unstable
386    pub(crate) fn payments_db_dir(&self) -> PathBuf {
387        self.user_db_dir.join("payments_db")
388    }
389
390    // --- Old dirs --- //
391
392    /// Old payment database directories that may need cleanup after migration.
393    pub(crate) fn old_payment_db_dirs(&self) -> [PathBuf; 1] {
394        [
395            // BasicPaymentV1
396            self.user_db_dir.join("payment_db"),
397            // Add more here as needed
398        ]
399    }
400
401    /// Old provision database directory that may need cleanup.
402    pub(crate) fn old_provision_db_dir(&self) -> PathBuf {
403        self.user_db_dir.join("provision_db")
404    }
405}
406
407#[cfg(test)]
408mod test {
409    use std::str::FromStr;
410
411    use super::*;
412
413    /// Ensure SDK_USER_AGENT parses correctly and has the expected format.
414    #[test]
415    fn test_sdk_user_agent() {
416        let user_agent: &str = &SDK_USER_AGENT;
417
418        // Should match: "lexe/<semver> node/<semver>"
419        let (sdk_part, node_part) = user_agent
420            .split_once(" node/")
421            .expect("Missing ' node/' separator");
422
423        // Validate sdk part: "lexe/<version>"
424        let sdk_version_str = sdk_part
425            .strip_prefix("lexe/")
426            .expect("Missing 'lexe/' prefix");
427        let _sdk_version = semver::Version::from_str(sdk_version_str)
428            .expect("Invalid SDK semver version");
429
430        // Validate node version
431        let _node_version = semver::Version::from_str(node_part)
432            .expect("Invalid node semver version");
433    }
434}