Skip to main content

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