fedimint_gateway_server/
client.rs

1use std::collections::BTreeSet;
2use std::fmt::Debug;
3use std::path::PathBuf;
4use std::sync::Arc;
5
6use fedimint_bip39::{Bip39RootSecretStrategy, Mnemonic};
7use fedimint_client::db::ClientConfigKey;
8use fedimint_client::module_init::ClientModuleInitRegistry;
9use fedimint_client::{Client, ClientBuilder, RootSecret};
10use fedimint_client_module::secret::{PlainRootSecretStrategy, RootSecretStrategy};
11use fedimint_core::config::FederationId;
12use fedimint_core::db::{Database, IDatabaseTransactionOpsCoreTyped};
13use fedimint_core::module::registry::ModuleDecoderRegistry;
14use fedimint_derive_secret::DerivableSecret;
15use fedimint_gateway_common::FederationConfig;
16use fedimint_gateway_server_db::GatewayDbExt as _;
17use fedimint_gw_client::GatewayClientInit;
18use fedimint_gwv2_client::GatewayClientInitV2;
19
20use crate::config::DatabaseBackend;
21use crate::error::AdminGatewayError;
22use crate::{AdminResult, Gateway};
23
24#[derive(Debug, Clone)]
25pub struct GatewayClientBuilder {
26    work_dir: PathBuf,
27    registry: ClientModuleInitRegistry,
28    db_backend: DatabaseBackend,
29}
30
31impl GatewayClientBuilder {
32    pub fn new(
33        work_dir: PathBuf,
34        registry: ClientModuleInitRegistry,
35        db_backend: DatabaseBackend,
36    ) -> Self {
37        Self {
38            work_dir,
39            registry,
40            db_backend,
41        }
42    }
43
44    pub fn data_dir(&self) -> PathBuf {
45        self.work_dir.clone()
46    }
47
48    /// Reads a plain root secret from a database to construct a database.
49    /// Only used for "legacy" federations before v0.5.0
50    async fn client_plainrootsecret(&self, db: &Database) -> AdminResult<DerivableSecret> {
51        let client_secret = Client::load_decodable_client_secret::<[u8; 64]>(db)
52            .await
53            .map_err(AdminGatewayError::ClientCreationError)?;
54        Ok(PlainRootSecretStrategy::to_root_secret(&client_secret))
55    }
56
57    /// Constructs the client builder with the modules, database, and connector
58    /// used to create clients for connected federations.
59    async fn create_client_builder(
60        &self,
61        federation_config: &FederationConfig,
62        gateway: Arc<Gateway>,
63    ) -> AdminResult<ClientBuilder> {
64        let FederationConfig {
65            federation_index,
66            connector,
67            ..
68        } = federation_config.to_owned();
69
70        let mut registry = self.registry.clone();
71
72        registry.attach(GatewayClientInit {
73            federation_index,
74            lightning_manager: gateway.clone(),
75        });
76
77        registry.attach(GatewayClientInitV2 {
78            gateway: gateway.clone(),
79        });
80
81        let mut client_builder = Client::builder()
82            .await
83            .map_err(AdminGatewayError::ClientCreationError)?
84            .with_iroh_enable_dht(true)
85            .with_iroh_enable_next(true);
86        client_builder.with_module_inits(registry);
87        client_builder.with_connector(connector);
88        Ok(client_builder)
89    }
90
91    /// Recovers a client with the provided mnemonic. This function will wait
92    /// for the recoveries to finish, but a new client must be created
93    /// afterwards and waited on until the state machines have finished
94    /// for a balance to be present.
95    pub async fn recover(
96        &self,
97        config: FederationConfig,
98        gateway: Arc<Gateway>,
99        mnemonic: &Mnemonic,
100    ) -> AdminResult<()> {
101        let federation_id = config.invite_code.federation_id();
102        let db = gateway.gateway_db.get_client_database(&federation_id);
103        let client_builder = self.create_client_builder(&config, gateway.clone()).await?;
104        let root_secret = RootSecret::StandardDoubleDerive(
105            Bip39RootSecretStrategy::<12>::to_root_secret(mnemonic),
106        );
107        let client = client_builder
108            .preview(&config.invite_code)
109            .await?
110            .recover(db, root_secret, None)
111            .await
112            .map(Arc::new)
113            .map_err(AdminGatewayError::ClientCreationError)?;
114        client
115            .wait_for_all_recoveries()
116            .await
117            .map_err(AdminGatewayError::ClientCreationError)?;
118        Ok(())
119    }
120
121    /// Builds a new client with the provided `FederationConfig` and `Mnemonic`.
122    /// Only used for newly joined federations.
123    pub async fn build(
124        &self,
125        config: FederationConfig,
126        gateway: Arc<Gateway>,
127        mnemonic: &Mnemonic,
128    ) -> AdminResult<fedimint_client::ClientHandleArc> {
129        let invite_code = config.invite_code.clone();
130        let federation_id = invite_code.federation_id();
131        let db_path = self.work_dir.join(format!("{federation_id}.db"));
132
133        let (db, root_secret) = if db_path.exists() {
134            let db = match self.db_backend {
135                DatabaseBackend::RocksDb => {
136                    let rocksdb = fedimint_rocksdb::RocksDb::open(db_path.clone())
137                        .await
138                        .map_err(AdminGatewayError::ClientCreationError)?;
139                    Database::new(rocksdb, ModuleDecoderRegistry::default())
140                }
141                DatabaseBackend::CursedRedb => {
142                    let cursed_redb = fedimint_cursed_redb::MemAndRedb::new(db_path.clone())
143                        .await
144                        .map_err(AdminGatewayError::ClientCreationError)?;
145                    Database::new(cursed_redb, ModuleDecoderRegistry::default())
146                }
147            };
148            let root_secret = RootSecret::Custom(self.client_plainrootsecret(&db).await?);
149            (db, root_secret)
150        } else {
151            let db = gateway.gateway_db.get_client_database(&federation_id);
152
153            let root_secret = RootSecret::StandardDoubleDerive(
154                Bip39RootSecretStrategy::<12>::to_root_secret(mnemonic),
155            );
156            (db, root_secret)
157        };
158
159        Self::verify_client_config(&db, federation_id).await?;
160
161        let client_builder = self.create_client_builder(&config, gateway).await?;
162
163        if Client::is_initialized(&db).await {
164            client_builder.open(db, root_secret).await
165        } else {
166            client_builder
167                .preview(&invite_code)
168                .await?
169                .join(db, root_secret)
170                .await
171        }
172        .map(Arc::new)
173        .map_err(AdminGatewayError::ClientCreationError)
174    }
175
176    /// Verifies that the saved `ClientConfig` contains the expected
177    /// federation's config.
178    async fn verify_client_config(db: &Database, federation_id: FederationId) -> AdminResult<()> {
179        let mut dbtx = db.begin_transaction_nc().await;
180        if let Some(config) = dbtx.get_value(&ClientConfigKey).await
181            && config.calculate_federation_id() != federation_id
182        {
183            return Err(AdminGatewayError::ClientCreationError(anyhow::anyhow!(
184                "Federation Id did not match saved federation ID".to_string()
185            )));
186        }
187        Ok(())
188    }
189
190    /// Returns a vector of "legacy" federations which did not derive their
191    /// client secret's from the gateway's mnemonic.
192    pub fn legacy_federations(&self, all_federations: BTreeSet<FederationId>) -> Vec<FederationId> {
193        all_federations
194            .into_iter()
195            .filter(|federation_id| {
196                let db_path = self.work_dir.join(format!("{federation_id}.db"));
197                db_path.exists()
198            })
199            .collect::<Vec<FederationId>>()
200    }
201}