fedimint_testing/
fixtures.rs

1use std::env;
2use std::net::SocketAddr;
3use std::str::FromStr;
4use std::sync::Arc;
5use std::time::Duration;
6
7use fedimint_bitcoind::{DynBitcoindRpc, IBitcoindRpc, create_esplora_rpc};
8use fedimint_client::module_init::{
9    ClientModuleInitRegistry, DynClientModuleInit, IClientModuleInit,
10};
11use fedimint_core::core::{ModuleInstanceId, ModuleKind};
12use fedimint_core::db::Database;
13use fedimint_core::db::mem_impl::MemDatabase;
14use fedimint_core::envs::BitcoinRpcConfig;
15use fedimint_core::task::{MaybeSend, MaybeSync};
16use fedimint_core::util::SafeUrl;
17use fedimint_gateway_common::{ChainSource, LightningInfo, LightningMode};
18use fedimint_gateway_server::Gateway;
19use fedimint_gateway_server::client::GatewayClientBuilder;
20use fedimint_gateway_server::config::DatabaseBackend;
21use fedimint_lightning::{ILnRpcClient, LightningContext};
22use fedimint_logging::TracingSetup;
23use fedimint_server::core::{DynServerModuleInit, IServerModuleInit, ServerModuleInitRegistry};
24use fedimint_server_bitcoin_rpc::bitcoind::BitcoindClient;
25use fedimint_server_bitcoin_rpc::esplora::EsploraClient;
26use fedimint_server_core::bitcoin_rpc::{DynServerBitcoinRpc, IServerBitcoinRpc};
27use fedimint_testing_core::test_dir;
28
29use crate::btc::BitcoinTest;
30use crate::btc::mock::FakeBitcoinTest;
31use crate::btc::real::RealBitcoinTest;
32use crate::envs::{
33    FM_PORT_ESPLORA_ENV, FM_TEST_BACKEND_BITCOIN_RPC_KIND_ENV, FM_TEST_BACKEND_BITCOIN_RPC_URL_ENV,
34    FM_TEST_BITCOIND_RPC_ENV, FM_TEST_USE_REAL_DAEMONS_ENV,
35};
36use crate::federation::{FederationTest, FederationTestBuilder};
37use crate::ln::FakeLightningTest;
38
39/// A default timeout for things happening in tests
40pub const TIMEOUT: Duration = Duration::from_secs(10);
41
42pub const DEFAULT_GATEWAY_PASSWORD: &str = "thereisnosecondbest";
43
44/// A tool for easily writing fedimint integration tests
45pub struct Fixtures {
46    clients: ClientModuleInitRegistry,
47    servers: ServerModuleInitRegistry,
48    bitcoin_rpc: BitcoinRpcConfig,
49    bitcoin: Arc<dyn BitcoinTest>,
50    fake_bitcoin_rpc: Option<DynBitcoindRpc>,
51    server_bitcoin_rpc: DynServerBitcoinRpc,
52    primary_module_kind: ModuleKind,
53}
54
55impl Fixtures {
56    pub fn new_primary(
57        client: impl IClientModuleInit + 'static,
58        server: impl IServerModuleInit + MaybeSend + MaybeSync + 'static,
59    ) -> Self {
60        // Ensure tracing has been set once
61        let _ = TracingSetup::default().init();
62        let real_testing = Fixtures::is_real_test();
63        let (bitcoin, config, bitcoin_rpc_connection, fake_bitcoin_rpc): (
64            Arc<dyn BitcoinTest>,
65            BitcoinRpcConfig,
66            DynServerBitcoinRpc,
67            Option<DynBitcoindRpc>,
68        ) = if real_testing {
69            // `backend-test.sh` overrides which Bitcoin RPC to use for esplora
70            // backend tests
71            let override_bitcoin_rpc_kind = env::var(FM_TEST_BACKEND_BITCOIN_RPC_KIND_ENV);
72            let override_bitcoin_rpc_url = env::var(FM_TEST_BACKEND_BITCOIN_RPC_URL_ENV);
73
74            let rpc_config = match (override_bitcoin_rpc_kind, override_bitcoin_rpc_url) {
75                (Ok(kind), Ok(url)) => BitcoinRpcConfig {
76                    kind: kind.parse().expect("must provide valid kind"),
77                    url: url.parse().expect("must provide valid url"),
78                },
79                _ => BitcoinRpcConfig::get_defaults_from_env_vars()
80                    .expect("must provide valid default env vars"),
81            };
82
83            let server_bitcoin_rpc = match rpc_config.kind.as_ref() {
84                "bitcoind" => {
85                    // Directly extract the authentication details from the url.
86                    // Since this is just testing we can be careful to not use characters that need
87                    // to be URL-encoded
88                    let bitcoind_username = rpc_config.url.username();
89                    let bitcoind_password = rpc_config
90                        .url
91                        .password()
92                        .expect("bitcoind password was not set");
93                    BitcoindClient::new(
94                        bitcoind_username.to_string(),
95                        bitcoind_password.to_string(),
96                        &rpc_config.url,
97                    )
98                    .unwrap()
99                    .into_dyn()
100                }
101                "esplora" => EsploraClient::new(&rpc_config.url).unwrap().into_dyn(),
102                kind => panic!("Unknown bitcoin rpc kind {kind}"),
103            };
104
105            let bitcoincore_url = env::var(FM_TEST_BITCOIND_RPC_ENV)
106                .expect("Must have bitcoind RPC defined for real tests")
107                .parse()
108                .expect("Invalid bitcoind RPC URL");
109            let bitcoin = RealBitcoinTest::new(&bitcoincore_url, server_bitcoin_rpc.clone());
110
111            (Arc::new(bitcoin), rpc_config, server_bitcoin_rpc, None)
112        } else {
113            let bitcoin = FakeBitcoinTest::new();
114
115            let config = BitcoinRpcConfig {
116                kind: format!("test_btc-{}", rand::random::<u64>()),
117                url: "http://ignored".parse().unwrap(),
118            };
119
120            let dyn_bitcoin_rpc = IBitcoindRpc::into_dyn(bitcoin.clone());
121
122            let server_bitcoin_rpc = IServerBitcoinRpc::into_dyn(bitcoin.clone());
123
124            let bitcoin = Arc::new(bitcoin);
125
126            (
127                bitcoin.clone(),
128                config,
129                server_bitcoin_rpc,
130                Some(dyn_bitcoin_rpc),
131            )
132        };
133
134        Self {
135            clients: ClientModuleInitRegistry::default(),
136            servers: ServerModuleInitRegistry::default(),
137            bitcoin_rpc: config,
138            fake_bitcoin_rpc,
139            bitcoin,
140            server_bitcoin_rpc: bitcoin_rpc_connection,
141            primary_module_kind: IClientModuleInit::module_kind(&client),
142        }
143        .with_module(client, server)
144    }
145
146    pub fn is_real_test() -> bool {
147        env::var(FM_TEST_USE_REAL_DAEMONS_ENV) == Ok("1".to_string())
148    }
149
150    /// Add a module to the fed
151    pub fn with_module(
152        mut self,
153        client: impl IClientModuleInit + 'static,
154        server: impl IServerModuleInit + MaybeSend + MaybeSync + 'static,
155    ) -> Self {
156        self.clients.attach(DynClientModuleInit::from(client));
157        self.servers.attach(DynServerModuleInit::from(server));
158        self
159    }
160
161    pub fn with_server_only_module(
162        mut self,
163        server: impl IServerModuleInit + MaybeSend + MaybeSync + 'static,
164    ) -> Self {
165        self.servers.attach(DynServerModuleInit::from(server));
166        self
167    }
168
169    /// Starts a new federation with 3/4 peers online
170    pub async fn new_fed_degraded(&self) -> FederationTest {
171        self.new_fed_builder(1).build().await
172    }
173
174    /// Starts a new federation with 4/4 peers online
175    pub async fn new_fed_not_degraded(&self) -> FederationTest {
176        self.new_fed_builder(0).build().await
177    }
178
179    /// Creates a new `FederationTestBuilder` that can be used to build up a
180    /// `FederationTest` for module tests.
181    pub fn new_fed_builder(&self, num_offline: u16) -> FederationTestBuilder {
182        FederationTestBuilder::new(
183            self.servers.clone(),
184            self.clients.clone(),
185            self.primary_module_kind.clone(),
186            num_offline,
187            self.server_bitcoin_rpc(),
188        )
189    }
190
191    /// Creates a new Gateway that can be used for module tests.
192    pub async fn new_gateway(&self) -> Gateway {
193        // Use server_gens.iter() to match the alphabetical order used by the server
194        // when assigning module instance IDs (BTreeMap iteration order)
195        let module_kinds: Vec<_> = self
196            .servers
197            .iter()
198            .enumerate()
199            .map(|(id, (kind, _))| (id as ModuleInstanceId, kind.clone()))
200            .collect();
201        let decoders = self
202            .servers
203            .available_decoders(module_kinds.iter().map(|(id, kind)| (*id, kind)))
204            .unwrap();
205        let gateway_db = Database::new(MemDatabase::new(), decoders.clone());
206
207        let registry = self
208            .clients
209            .iter()
210            .filter(|(kind, _)| {
211                // Remove LN module because the gateway adds one
212                **kind != ModuleKind::from_static_str("ln")
213            })
214            .filter(|(kind, _)| {
215                // Remove LN NG module because the gateway adds one
216                **kind != ModuleKind::from_static_str("lnv2")
217            })
218            .map(|(_, client)| client.clone())
219            .collect();
220
221        let (path, _config_dir) = test_dir(&format!("gateway-{}", rand::random::<u64>()));
222
223        // Create federation client builder for the gateway
224        let client_builder: GatewayClientBuilder =
225            GatewayClientBuilder::new(path.clone(), registry, DatabaseBackend::RocksDb)
226                .await
227                .expect("Failed to initialize gateway");
228
229        let ln_client: Arc<dyn ILnRpcClient> = Arc::new(FakeLightningTest::new());
230
231        let LightningInfo::Connected {
232            public_key: lightning_public_key,
233            alias: lightning_alias,
234            network: lightning_network,
235            block_height: _,
236            synced_to_chain: _,
237        } = ln_client.parsed_node_info().await
238        else {
239            panic!("Could not connect to Lightning node")
240        };
241        let lightning_context = LightningContext {
242            lnrpc: ln_client.clone(),
243            lightning_public_key,
244            lightning_alias,
245            lightning_network,
246        };
247
248        // Module tests do not use the webserver, so any port is ok
249        let listen: SocketAddr = "127.0.0.1:9000".parse().unwrap();
250        let address: SafeUrl = format!("http://{listen}").parse().unwrap();
251
252        let esplora_server_url = SafeUrl::parse(&format!(
253            "http://127.0.0.1:{}",
254            env::var(FM_PORT_ESPLORA_ENV).unwrap_or(String::from("50002"))
255        ))
256        .expect("Failed to parse default esplora server");
257        let bitcoin_rpc = fedimint_bitcoind::EsploraClient::new(&esplora_server_url)
258            .expect("Could not create EsploraClient")
259            .into_dyn();
260        let esplora_chain_source = ChainSource::Esplora {
261            server_url: esplora_server_url,
262        };
263
264        Gateway::new_with_custom_registry(
265            // Fixtures does not use real lightning connection, so just fake the connection
266            // parameters
267            LightningMode::Lnd {
268                lnd_rpc_addr: "FakeRpcAddr".to_string(),
269                lnd_tls_cert: "FakeTlsCert".to_string(),
270                lnd_macaroon: "FakeMacaroon".to_string(),
271            },
272            client_builder,
273            listen,
274            address.clone(),
275            bcrypt::HashParts::from_str(
276                &bcrypt::hash(DEFAULT_GATEWAY_PASSWORD, bcrypt::DEFAULT_COST).unwrap(),
277            )
278            .unwrap(),
279            bitcoin::Network::Regtest,
280            0,
281            gateway_db,
282            // Manually set the gateway's state to `Running`. In tests, we do don't run the
283            // webserver or intercept HTLCs, so this is necessary for instructing the
284            // gateway that it is connected to the mock Lightning node.
285            fedimint_gateway_server::GatewayState::Running { lightning_context },
286            esplora_chain_source,
287            None,
288            bitcoin_rpc,
289        )
290        .await
291        .expect("Failed to create gateway")
292    }
293
294    /// Get a server bitcoin RPC config
295    pub fn bitcoin_server(&self) -> BitcoinRpcConfig {
296        self.bitcoin_rpc.clone()
297    }
298
299    pub fn client_esplora_rpc(&self) -> DynBitcoindRpc {
300        if Fixtures::is_real_test() {
301            create_esplora_rpc(
302                &SafeUrl::parse(&format!(
303                    "http://127.0.0.1:{}/",
304                    env::var(FM_PORT_ESPLORA_ENV).unwrap_or(String::from("50002"))
305                ))
306                .expect("Failed to parse default esplora server"),
307            )
308            .unwrap()
309        } else {
310            self.fake_bitcoin_rpc.clone().unwrap()
311        }
312    }
313
314    /// Get a test bitcoin fixture
315    pub fn bitcoin(&self) -> Arc<dyn BitcoinTest> {
316        self.bitcoin.clone()
317    }
318
319    pub fn server_bitcoin_rpc(&self) -> DynServerBitcoinRpc {
320        self.server_bitcoin_rpc.clone()
321    }
322}