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