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