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