From 57a22a50667a822f18eb92ad5a13f65d1386753d Mon Sep 17 00:00:00 2001 From: Andrei Date: Fri, 13 Feb 2026 00:00:00 +0000 Subject: [PATCH] [skip ci] Add swapd to itest --- libs/sdk-itest/src/environment/cln.rs | 6 +- libs/sdk-itest/src/environment/lnd.rs | 29 ++++++++++ libs/sdk-itest/src/environment/mod.rs | 21 ++++++- libs/sdk-itest/src/environment/swapd.rs | 73 +++++++++++++++++++++++++ libs/sdk-itest/tests/node.rs | 64 +++++++++++++++++++++- 5 files changed, 188 insertions(+), 5 deletions(-) create mode 100644 libs/sdk-itest/src/environment/swapd.rs diff --git a/libs/sdk-itest/src/environment/cln.rs b/libs/sdk-itest/src/environment/cln.rs index 68e03f97a..36c3ac799 100644 --- a/libs/sdk-itest/src/environment/cln.rs +++ b/libs/sdk-itest/src/environment/cln.rs @@ -12,9 +12,9 @@ use crate::environment::container::ContainerExt; use crate::environment::log::LogConsumer; use crate::environment::{ApiCredentials, Cert, EnvironmentId}; -const CA_PEM_FILE: &str = "/data/.lightning/regtest/ca.pem"; -const CLIENT_CERT_FILE: &str = "/data/.lightning/regtest/client.pem"; -const CLIENT_KEY_FILE: &str = "/data/.lightning/regtest/client-key.pem"; +const CA_PEM_FILE: &str = "/root/.lightning/regtest/ca.pem"; +const CLIENT_CERT_FILE: &str = "/root/.lightning/regtest/client.pem"; +const CLIENT_KEY_FILE: &str = "/root/.lightning/regtest/client-key.pem"; const CLN_HOSTNAME: &str = "cln"; const GRPC_PORT: u16 = 8888; const IMAGE_NAME: &str = "elementsproject/lightningd"; diff --git a/libs/sdk-itest/src/environment/lnd.rs b/libs/sdk-itest/src/environment/lnd.rs index ba4d9b35a..b3d6a642a 100644 --- a/libs/sdk-itest/src/environment/lnd.rs +++ b/libs/sdk-itest/src/environment/lnd.rs @@ -19,13 +19,18 @@ use crate::environment::{ApiCredentials, EnvironmentId}; const IMAGE_NAME: &str = "lightninglabs/lnd"; const IMAGE_TAG: &str = "v0.19.3-beta"; +const SOCAT_IMAGE: &str = "alpine/socat"; +const SOCAT_IMAGE_TAG: &str = "latest"; +const LND_HOSTNAME: &str = "lnd"; const LIGHTNING_PORT: u16 = 9735; const RPC_PORT: u16 = 10009; pub struct Lnd { pub lightning_api: ApiCredentials, + pub grpc_api: ApiCredentials, client: Mutex, _container: ContainerAsync, + _socat: ContainerAsync, } impl Lnd { @@ -40,6 +45,7 @@ impl Lnd { .with_exposed_port(RPC_PORT.into()) .with_wait_for(WaitFor::message_on_stdout("Server listening on")) .with_network(environment_id.network_name()) + .with_hostname(LND_HOSTNAME) .with_log_consumer(LogConsumer::new("lnd")) .with_cmd([ "--bitcoin.regtest", @@ -72,6 +78,7 @@ impl Lnd { container .copy_file("/root/.lnd/tls.cert", &cert_path) .await?; + let tls_cert = container.read_file("/root/.lnd/tls.cert").await?; let macaroon_path = working_dir.join("admin.macaroon"); container .copy_file( @@ -79,15 +86,37 @@ impl Lnd { &macaroon_path, ) .await?; + let macaroon = container + .read_file("/root/.lnd/data/chain/bitcoin/regtest/admin.macaroon") + .await?; let lightning_api = ApiCredentials::from_container(&container, LIGHTNING_PORT).await?; let port = container.get_host_port_ipv4(RPC_PORT).await?; let endpoint = format!("https://localhost:{port}"); let client = tonic_lnd::connect(endpoint, &cert_path, &macaroon_path).await?; + let socat = GenericImage::new(SOCAT_IMAGE, SOCAT_IMAGE_TAG) + .with_exposed_port(RPC_PORT.into()) + .with_wait_for(WaitFor::message_on_stderr("listening on")) + .with_network(environment_id.network_name()) + .with_log_consumer(LogConsumer::new("lnd-socat")) + .with_copy_to("/root/.lnd/tls.cert", tls_cert.clone()) + .with_cmd([ + "-dd", + format!("TCP-LISTEN:{RPC_PORT},fork").as_str(), + format!("OPENSSL:{LND_HOSTNAME}:{RPC_PORT},cafile=/root/.lnd/tls.cert").as_str(), + ]) + .start() + .await?; + + let mut grpc_api = ApiCredentials::from_container(&socat, RPC_PORT).await?; + grpc_api.macaroon = macaroon; + Ok(Self { lightning_api, + grpc_api, client: Mutex::new(client), + _socat: socat, _container: container, }) } diff --git a/libs/sdk-itest/src/environment/mod.rs b/libs/sdk-itest/src/environment/mod.rs index 5c40006c2..b57dad3d0 100644 --- a/libs/sdk-itest/src/environment/mod.rs +++ b/libs/sdk-itest/src/environment/mod.rs @@ -7,6 +7,7 @@ mod log; mod lsp; mod mempool; mod rgs; +mod swapd; mod vss; use std::path::PathBuf; @@ -22,6 +23,7 @@ use lsp::Lsp; use mempool::Mempool; use rand::Rng; use rgs::Rgs; +use swapd::Swapd; use testcontainers::{ContainerAsync, Image}; use testdir::testdir; use tokio::sync::OnceCell; @@ -83,6 +85,7 @@ pub struct ApiCredentials { pub username: String, pub password: String, pub cert: Cert, + pub macaroon: Vec, } impl ApiCredentials { @@ -128,6 +131,7 @@ pub struct Environment { lsp: OnceCell, lnd: OnceCell, cln: OnceCell, + swapd: OnceCell, channel: OnceCell<()>, cln_channel: OnceCell<()>, rgs: OnceCell, @@ -229,6 +233,21 @@ impl Environment { self.cln().await } + #[instrument(skip(self))] + pub async fn swapd(&self) -> Result<&ApiCredentials> { + let swapd = self + .swapd + .get_or_try_init(|| async { + info!("Initializing swapd"); + let bitcoind_api = self.bitcoind_api(); + let lnd_grpc_api = async { Ok(&self.lnd().await?.grpc_api) }; + let result = Swapd::new(&self.environmnet_id, bitcoind_api, lnd_grpc_api).await; + log_result(result, "swapd") + }) + .await?; + Ok(&swapd.api) + } + #[instrument(skip(self))] pub async fn rgs(&self) -> Result<&ApiCredentials> { let rgs = self @@ -339,7 +358,7 @@ impl Environment { } #[instrument(skip(self))] - async fn bitcoind(&self) -> Result<&Bitcoind> { + pub async fn bitcoind(&self) -> Result<&Bitcoind> { let bitcoind = self .bitcoind .get_or_try_init(|| async { diff --git a/libs/sdk-itest/src/environment/swapd.rs b/libs/sdk-itest/src/environment/swapd.rs new file mode 100644 index 000000000..2d27344f6 --- /dev/null +++ b/libs/sdk-itest/src/environment/swapd.rs @@ -0,0 +1,73 @@ +use anyhow::{Error, Result}; +use futures::TryFutureExt; +use testcontainers::core::WaitFor; +use testcontainers::core::wait::LogWaitStrategy; +use testcontainers::runners::AsyncRunner; +use testcontainers::{ContainerAsync, GenericImage, ImageExt}; +use testcontainers_modules::postgres::Postgres; +use tokio::try_join; + +use crate::environment::log::LogConsumer; +use crate::environment::{ApiCredentials, EnvironmentId}; + +const IMAGE_NAME: &str = "swapd"; +const IMAGE_TAG: &str = "latest"; +const RPC_PORT: u16 = 8011; + +pub struct Swapd { + pub api: ApiCredentials, + _container: ContainerAsync, + _postgres: ContainerAsync, +} + +impl Swapd { + pub async fn new( + environment_id: &EnvironmentId, + bitcoind_api: impl Future>, + lnd_grpc_api: impl Future>, + ) -> Result { + let postgres = Postgres::default() + .with_tag("16") + .with_network(environment_id.network_name()) + .with_log_consumer(LogConsumer::new("swapd-postgres")) + .start() + .map_err(Error::msg); + let (postgres, bitcoind_api, lnd_grpc_api) = + try_join!(postgres, bitcoind_api, lnd_grpc_api)?; + let postgres_host = postgres.get_bridge_ip_address().await?.to_string(); + + let container = GenericImage::new(IMAGE_NAME, IMAGE_TAG) + .with_exposed_port(RPC_PORT.into()) + .with_wait_for(WaitFor::Log(LogWaitStrategy::stdout("swapd started"))) + .with_network(environment_id.network_name()) + .with_log_consumer(LogConsumer::new("swapd")) + .with_copy_to( + "/data/lnd/admin.macaroon", + hex::encode(&lnd_grpc_api.macaroon).into_bytes(), + ) + .with_env_var("NO_COLOR", "1") + .with_cmd([ + "--auto-migrate", + "--chain-poll-interval-seconds=5", + format!("--lnd-grpc-address={}", lnd_grpc_api.endpoint()).as_str(), + "--lnd-grpc-macaroon=/data/lnd/admin.macaroon", + "--log-level=swapd=debug,info", + "--network=regtest", + format!("--address=0.0.0.0:{RPC_PORT}").as_str(), + format!("--db-url=postgresql://postgres:postgres@{postgres_host}/postgres?sslmode=disable").as_str(), + format!("--bitcoind-rpc-address={}", bitcoind_api.endpoint()).as_str(), + format!("--bitcoind-rpc-user={}", bitcoind_api.username).as_str(), + format!("--bitcoind-rpc-password={}", bitcoind_api.password).as_str(), + ]) + .start() + .await?; + + let api = ApiCredentials::from_container(&container, RPC_PORT).await?; + + Ok(Self { + api, + _container: container, + _postgres: postgres, + }) + } +} diff --git a/libs/sdk-itest/tests/node.rs b/libs/sdk-itest/tests/node.rs index b425cb537..c3c5cfd12 100644 --- a/libs/sdk-itest/tests/node.rs +++ b/libs/sdk-itest/tests/node.rs @@ -1,8 +1,9 @@ mod event_listener; +use std::str::FromStr; use std::time::Duration; -use bitcoin::Amount; +use bitcoin::{Address, Amount}; use breez_sdk_core::error::{ConnectError, SendPaymentError}; use breez_sdk_core::{ BreezEvent, BreezServices, Config, ConnectRequest, ListPaymentsRequest, LnPaymentDetails, @@ -32,6 +33,67 @@ async fn test_environment() { Environment::default().cln_with_channel().await.unwrap(); } +#[rstest] +#[tokio::test(flavor = "multi_thread")] +#[test_log::test] +async fn test_node_swap() { + let env = Environment::default(); + let (bitcoind, esplora, vss, lsp, _lnd, swapd) = try_join!( + env.bitcoind(), + env.esplora_api(), + env.vss_api(), + env.lsp_external_address(), + env.lnd_with_channel(), + env.swapd(), + ) + .unwrap(); + info!("Esplora is running: {}", esplora.external_endpoint()); + info!(" VSS is running: {}", vss.external_endpoint()); + info!(" LND is running"); + + let seed = rand::rng().random::<[u8; 64]>().to_vec(); + + let mut config = Config::regtest(String::new()); + config.working_dir = testdir!().to_string_lossy().to_string(); + config.mempoolspace_url = None; + config.esplora_url = esplora.external_endpoint(); + config.vss_url = vss.external_endpoint(); + config.rgs_url = "localhost:9".to_string(); + config.lsps2_address = lsp; + config.breezserver = swapd.endpoint(); + + info!("Starting a fresh node with restore_only=None"); + let req = ConnectRequest { + config: config.clone(), + seed: seed.clone(), + restore_only: None, + }; + + let (tx, mut events) = mpsc::channel(100); + let services = BreezServices::connect(req, Box::new(EventListenerImpl::new(tx))) + .await + .unwrap(); + + let res = services.receive_onchain(Default::default()).await.unwrap(); + assert!(res.bitcoin_address.starts_with("bcrt1")); + let address = Address::from_str(&res.bitcoin_address) + .unwrap() + .assume_checked(); + let amount = Amount::from_sat(50_000); + bitcoind.fund_address(&address, amount).await.unwrap(); + bitcoind.generate_blocks(6).await.unwrap(); + + info!("Waiting for BreezEvent::InvoicePaid..."); + wait_for!(matches!( + events.recv().await, + Some(BreezEvent::InvoicePaid { .. }) + )); + + services.disconnect().await.unwrap(); + drop(services); + assert!(events.is_closed()); +} + #[rstest] #[tokio::test(flavor = "multi_thread")] #[test_log::test]