diff --git a/Cargo.lock b/Cargo.lock index 267b5ab81..ddf58318b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -74,6 +74,12 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" +[[package]] +name = "assertables" +version = "7.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c24e9d990669fbd16806bff449e4ac644fd9b1fca014760087732fe4102f131" + [[package]] name = "async-stream" version = "0.3.4" @@ -98,13 +104,13 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.68" +version = "0.1.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842" +checksum = "a564d521dd56509c4c47480d00b80ee55f7e385ae48db5744c67ad50c92d2ebf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.23", ] [[package]] @@ -1693,6 +1699,7 @@ name = "ln-dlc-node" version = "0.1.0" dependencies = [ "anyhow", + "async-trait", "autometrics", "bdk", "bip39", @@ -1884,7 +1891,7 @@ checksum = "ddece26afd34c31585c74a4db0630c376df271c285d682d1e55012197830b6df" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.23", ] [[package]] @@ -2459,9 +2466,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.56" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435" +checksum = "7b368fba921b0dce7e60f5e04ec15e565b3303972b42bcfde1d0713b881959eb" dependencies = [ "unicode-ident", ] @@ -2580,9 +2587,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.28" +version = "1.0.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488" +checksum = "573015e8ab27661678357f27dc26460738fd2b6c86e46f386fde94cb5d913105" dependencies = [ "proc-macro2", ] @@ -3179,7 +3186,7 @@ checksum = "c87e960f4dca2788eeb86bbdde8dd246be8948790b7618d656e68f9b720a86e8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.23", ] [[package]] @@ -3222,9 +3229,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.15" +version = "2.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a34fcf3e8b60f57e6a14301a2e916d323af98b0ea63c599441eec8558660c822" +checksum = "59fb7d6d8281a51045d62b8eb3a7d1ce347b76f312af50cd3dc0af39c87c1737" dependencies = [ "proc-macro2", "quote", @@ -3282,6 +3289,7 @@ name = "tests-e2e" version = "0.1.0" dependencies = [ "anyhow", + "assertables", "bitcoin", "clap", "coordinator", @@ -3442,7 +3450,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.23", ] [[package]] diff --git a/crates/ln-dlc-node/Cargo.toml b/crates/ln-dlc-node/Cargo.toml index 1680cce46..3c25c0e4f 100644 --- a/crates/ln-dlc-node/Cargo.toml +++ b/crates/ln-dlc-node/Cargo.toml @@ -8,6 +8,7 @@ description = "A common interface for using Lightning and DLC channels side-by-s [dependencies] anyhow = { version = "1", features = ["backtrace"] } +async-trait = "0.1.71" autometrics = "0.5" bdk = { version = "0.27.0", default-features = false, features = ["key-value-db", "use-esplora-blocking"] } bip39 = { version = "2", features = ["rand_core"] } diff --git a/crates/ln-dlc-node/src/fee_rate_estimator.rs b/crates/ln-dlc-node/src/fee_rate_estimator.rs index e5ee54869..ffc413134 100644 --- a/crates/ln-dlc-node/src/fee_rate_estimator.rs +++ b/crates/ln-dlc-node/src/fee_rate_estimator.rs @@ -32,6 +32,16 @@ pub struct FeeRateEstimator { fee_rate_cache: RwLock>, } +pub trait EstimateFeeRate { + fn estimate(&self, target: ConfirmationTarget) -> FeeRate; +} + +impl EstimateFeeRate for FeeRateEstimator { + fn estimate(&self, target: ConfirmationTarget) -> FeeRate { + self.get(target) + } +} + impl FeeRateEstimator { /// Constructor for the [`FeeRateEstimator`]. pub fn new(esplora_url: String) -> Self { @@ -66,7 +76,7 @@ impl FeeRateEstimator { } } - pub(crate) fn get(&self, target: ConfirmationTarget) -> FeeRate { + fn get(&self, target: ConfirmationTarget) -> FeeRate { self.cache_read_lock() .get(&target) .copied() @@ -110,6 +120,6 @@ impl FeeRateEstimator { impl FeeEstimator for FeeRateEstimator { #[autometrics] fn get_est_sat_per_1000_weight(&self, confirmation_target: ConfirmationTarget) -> u32 { - (self.get(confirmation_target).fee_wu(1000) as u32).max(FEERATE_FLOOR_SATS_PER_KW) + (self.estimate(confirmation_target).fee_wu(1000) as u32).max(FEERATE_FLOOR_SATS_PER_KW) } } diff --git a/crates/ln-dlc-node/src/ldk_node_wallet.rs b/crates/ln-dlc-node/src/ldk_node_wallet.rs index 8ef467449..50d6246b0 100644 --- a/crates/ln-dlc-node/src/ldk_node_wallet.rs +++ b/crates/ln-dlc-node/src/ldk_node_wallet.rs @@ -1,11 +1,11 @@ -use crate::fee_rate_estimator::FeeRateEstimator; +use crate::fee_rate_estimator::EstimateFeeRate; use anyhow::bail; use anyhow::Context; use anyhow::Error; use anyhow::Result; use autometrics::autometrics; use bdk::blockchain::Blockchain; -use bdk::blockchain::EsploraBlockchain; +use bdk::blockchain::GetBlockHash; use bdk::blockchain::GetHeight; use bdk::database::BatchDatabase; use bdk::wallet::AddressIndex; @@ -14,6 +14,7 @@ use bdk::SyncOptions; use bdk::TransactionDetails; use bitcoin::consensus::encode::serialize_hex; use bitcoin::BlockHash; +use bitcoin::OutPoint; use bitcoin::Script; use bitcoin::Transaction; use bitcoin::Txid; @@ -25,16 +26,19 @@ use std::sync::Arc; use std::time::Instant; use tokio::sync::RwLock; -pub struct Wallet +pub struct Wallet where D: BatchDatabase, + B: Blockchain, + F: EstimateFeeRate, { // A BDK blockchain used for wallet sync. - pub(crate) blockchain: Arc, + pub(crate) blockchain: Arc, // A BDK on-chain wallet. inner: Mutex>, settings: RwLock, - fee_rate_estimator: Arc, + fee_rate_estimator: Arc, + locked_outpoints: Mutex>, } #[derive(Clone, Debug, Default)] @@ -42,15 +46,13 @@ pub struct WalletSettings { pub max_allowed_tx_fee_rate_when_opening_channel: Option, } -impl Wallet +impl Wallet where D: BatchDatabase, + B: Blockchain, + F: EstimateFeeRate, { - pub(crate) fn new( - blockchain: EsploraBlockchain, - wallet: bdk::Wallet, - fee_rate_estimator: Arc, - ) -> Self { + pub(crate) fn new(blockchain: B, wallet: bdk::Wallet, fee_rate_estimator: Arc) -> Self { let inner = Mutex::new(wallet); let settings = RwLock::new(WalletSettings::default()); @@ -59,6 +61,7 @@ where inner, settings, fee_rate_estimator, + locked_outpoints: Mutex::new(vec![]), } } @@ -90,6 +93,8 @@ where "Finished on-chain sync", ); + self.locked_outpoints.lock().clear(); + Ok(()) } @@ -103,12 +108,17 @@ where let locked_wallet = self.bdk_lock(); let mut tx_builder = locked_wallet.build_tx(); - let fee_rate = self.fee_rate_estimator.get(confirmation_target); + let fee_rate = self.fee_rate_estimator.estimate(confirmation_target); tx_builder .add_recipient(output_script, value_sats) .fee_rate(fee_rate) .enable_rbf(); + let mut locked_outpoints = self.locked_outpoints.lock(); + for outpoint in locked_outpoints.iter() { + tx_builder.add_unspendable(*outpoint); + } + let mut psbt = match tx_builder.finish() { Ok((psbt, _)) => { tracing::trace!("Created funding PSBT: {:?}", psbt); @@ -132,7 +142,17 @@ where } } - Ok(psbt.extract_tx()) + let transaction = psbt.extract_tx(); + + let prev_outpoints = transaction + .input + .iter() + .map(|input| input.previous_output) + .collect::>(); + + locked_outpoints.extend(prev_outpoints); + + Ok(transaction) } #[autometrics] @@ -159,7 +179,7 @@ where address: &bitcoin::Address, amount_msat_or_drain: Option, ) -> Result { - let fee_rate = self.fee_rate_estimator.get(ConfirmationTarget::Normal); + let fee_rate = self.fee_rate_estimator.estimate(ConfirmationTarget::Normal); let tx = { let locked_wallet = self.bdk_lock(); @@ -226,7 +246,7 @@ where #[autometrics] pub fn tip(&self) -> Result<(u32, BlockHash)> { let height = self.blockchain.get_height()?; - let hash = self.blockchain.get_tip_hash()?; + let hash = self.blockchain.get_block_hash(height as u64)?; Ok((height, hash)) } @@ -240,9 +260,11 @@ where } } -impl BroadcasterInterface for Wallet +impl BroadcasterInterface for Wallet where D: BatchDatabase, + B: Blockchain, + F: EstimateFeeRate, { fn broadcast_transaction(&self, tx: &Transaction) { let txid = tx.txid(); @@ -254,3 +276,160 @@ where } } } + +#[cfg(test)] +pub mod tests { + use crate::fee_rate_estimator::EstimateFeeRate; + use crate::ldk_node_wallet::Wallet; + use anyhow::Result; + use bdk::blockchain::Blockchain; + use bdk::blockchain::Capability; + use bdk::blockchain::GetBlockHash; + use bdk::blockchain::GetHeight; + use bdk::blockchain::GetTx; + use bdk::blockchain::Progress; + use bdk::blockchain::WalletSync; + use bdk::database::BatchDatabase; + use bdk::populate_test_db; + use bdk::testutils; + use bdk::BlockTime; + use bdk::Error; + use bdk::FeeRate; + use bitcoin::util::bip32::ExtendedPrivKey; + use bitcoin::Amount; + use bitcoin::BlockHash; + use bitcoin::Network; + use bitcoin::Script; + use bitcoin::Transaction; + use bitcoin::Txid; + use lightning::chain::chaininterface::ConfirmationTarget; + use rand::thread_rng; + use rand::CryptoRng; + use rand::RngCore; + use std::cell::RefCell; + use std::collections::HashSet; + use std::sync::Arc; + + #[tokio::test] + async fn wallet_with_two_utxo_should_be_able_to_fund_twice_but_not_three_times() { + let mut rng = thread_rng(); + let test_wallet = new_test_wallet(&mut rng, Amount::from_btc(1.0).unwrap(), 2).unwrap(); + let wallet = Wallet::new(DummyEsplora, test_wallet, Arc::new(DummyFeeRateEstimator)); + + let _ = wallet + .create_funding_transaction( + Script::new(), + Amount::from_btc(0.5).unwrap().to_sat(), + ConfirmationTarget::Background, + ) + .await + .unwrap(); + let _ = wallet + .create_funding_transaction( + Script::new(), + Amount::from_btc(0.5).unwrap().to_sat(), + ConfirmationTarget::Background, + ) + .await + .unwrap(); + assert!(wallet + .create_funding_transaction( + Script::new(), + Amount::from_btc(0.5).unwrap().to_sat(), + ConfirmationTarget::Background, + ) + .await + .is_err()); + } + + fn new_test_wallet( + rng: &mut (impl RngCore + CryptoRng), + utxo_amount: Amount, + num_utxos: u8, + ) -> Result> { + new_test_wallet_from_database( + rng, + utxo_amount, + num_utxos, + bdk::database::MemoryDatabase::new(), + ) + } + + fn new_test_wallet_from_database( + rng: &mut (impl RngCore + CryptoRng), + utxo_amount: Amount, + num_utxos: u8, + mut database: DB, + ) -> Result> { + let mut seed = [0u8; 32]; + rng.fill_bytes(&mut seed); + + let key = ExtendedPrivKey::new_master(Network::Regtest, &seed)?; + let descriptors = testutils!(@descriptors (&format!("wpkh({key}/*)"))); + + for index in 0..num_utxos { + populate_test_db!( + &mut database, + testutils! { + @tx ( (@external descriptors, index as u32) => utxo_amount.to_sat() ) (@confirmations 1) + }, + Some(100) + ); + } + + let wallet = bdk::Wallet::new(&descriptors.0, None, Network::Regtest, database)?; + + Ok(wallet) + } + + struct DummyEsplora; + struct DummyFeeRateEstimator; + + impl EstimateFeeRate for DummyFeeRateEstimator { + fn estimate(&self, _: ConfirmationTarget) -> FeeRate { + FeeRate::from_sat_per_vb(1.0) + } + } + + impl WalletSync for DummyEsplora { + fn wallet_setup( + &self, + _: &RefCell, + _: Box, + ) -> std::result::Result<(), Error> { + unimplemented!() + } + } + + impl GetHeight for DummyEsplora { + fn get_height(&self) -> std::result::Result { + unimplemented!() + } + } + + impl GetTx for DummyEsplora { + fn get_tx(&self, _: &Txid) -> std::result::Result, Error> { + unimplemented!() + } + } + + impl GetBlockHash for DummyEsplora { + fn get_block_hash(&self, _: u64) -> std::result::Result { + unimplemented!() + } + } + + impl Blockchain for DummyEsplora { + fn get_capabilities(&self) -> HashSet { + unimplemented!() + } + + fn broadcast(&self, _: &Transaction) -> std::result::Result<(), Error> { + unimplemented!() + } + + fn estimate_fee(&self, _: usize) -> std::result::Result { + unimplemented!() + } + } +} diff --git a/crates/ln-dlc-node/src/ln_dlc_wallet.rs b/crates/ln-dlc-node/src/ln_dlc_wallet.rs index eba935af3..71f183f38 100644 --- a/crates/ln-dlc-node/src/ln_dlc_wallet.rs +++ b/crates/ln-dlc-node/src/ln_dlc_wallet.rs @@ -36,7 +36,7 @@ use std::sync::Arc; /// This is a wrapper type introduced to be able to implement traits from `rust-dlc` on the /// `ldk_node::LightningWallet`. pub struct LnDlcWallet { - ln_wallet: Arc>, + ln_wallet: Arc>, storage: Arc, secp: Secp256k1, seed: Bip39Seed, @@ -91,7 +91,9 @@ impl LnDlcWallet { } // TODO: Better to keep this private and expose the necessary APIs instead. - pub fn inner(&self) -> Arc> { + pub fn inner( + &self, + ) -> Arc> { self.ln_wallet.clone() } diff --git a/crates/ln-dlc-node/src/node/wallet.rs b/crates/ln-dlc-node/src/node/wallet.rs index 117ce8b21..86cadfe3a 100644 --- a/crates/ln-dlc-node/src/node/wallet.rs +++ b/crates/ln-dlc-node/src/node/wallet.rs @@ -1,3 +1,4 @@ +use crate::fee_rate_estimator::FeeRateEstimator; use crate::ldk_node_wallet; use crate::node::HTLCStatus; use crate::node::Node; @@ -5,6 +6,7 @@ use crate::node::Storage; use crate::PaymentFlow; use anyhow::Context; use anyhow::Result; +use bdk::blockchain::EsploraBlockchain; use bdk::sled; use bitcoin::secp256k1::SecretKey; use bitcoin::Address; @@ -26,7 +28,9 @@ where self.wallet.get_seed_phrase() } - pub fn wallet(&self) -> Arc> { + pub fn wallet( + &self, + ) -> Arc> { self.wallet.inner() } diff --git a/crates/ln-dlc-node/src/tests/bitcoind.rs b/crates/ln-dlc-node/src/tests/bitcoind.rs index f6c1edcc9..c51c110f9 100644 --- a/crates/ln-dlc-node/src/tests/bitcoind.rs +++ b/crates/ln-dlc-node/src/tests/bitcoind.rs @@ -13,9 +13,9 @@ struct BitcoindResponse { pub async fn fund(address: String, amount: bitcoin::Amount) -> Result { query(format!( - r#"{{"jsonrpc": "1.0", "method": "sendtoaddress", "params": ["{}", "{}"]}}"#, + r#"{{"jsonrpc": "1.0", "method": "sendtoaddress", "params": ["{}", "{}", "", "", false, false, null, null, false, 1.0]}}"#, address, - amount.to_btc() + amount.to_btc(), )) .await } diff --git a/crates/ln-dlc-node/src/tests/just_in_time_channel/create.rs b/crates/ln-dlc-node/src/tests/just_in_time_channel/create.rs index 2ff24b217..e12ada8e7 100644 --- a/crates/ln-dlc-node/src/tests/just_in_time_channel/create.rs +++ b/crates/ln-dlc-node/src/tests/just_in_time_channel/create.rs @@ -1,3 +1,4 @@ +use crate::fee_rate_estimator::EstimateFeeRate; use crate::ln::JUST_IN_TIME_CHANNEL_OUTBOUND_LIQUIDITY_SAT_MAX; use crate::ln::LIQUIDITY_MULTIPLIER; use crate::node::InMemoryStore; @@ -74,7 +75,7 @@ async fn fail_to_open_jit_channel_with_fee_rate_over_max() { let background_fee_rate = coordinator .fee_rate_estimator - .get(ConfirmationTarget::Background) + .estimate(ConfirmationTarget::Background) .fee_wu(1000) as u32; // Set max allowed TX fee rate when opening channel to a value below the current background fee diff --git a/crates/ln-dlc-node/src/tests/mod.rs b/crates/ln-dlc-node/src/tests/mod.rs index 991cedf62..07314423f 100644 --- a/crates/ln-dlc-node/src/tests/mod.rs +++ b/crates/ln-dlc-node/src/tests/mod.rs @@ -10,7 +10,6 @@ use crate::seed::Bip39Seed; use crate::util; use anyhow::Result; use bitcoin::secp256k1::PublicKey; -use bitcoin::Address; use bitcoin::Amount; use bitcoin::Network; use bitcoin::XOnlyPublicKey; @@ -159,21 +158,29 @@ impl Node { let starting_balance = self.get_confirmed_balance().await?; let expected_balance = starting_balance + amount.to_sat(); - let address = self.wallet.unused_address(); - - fund_and_mine(address, amount).await?; + // we mine blocks so that the internal wallet in bitcoind has enough utxos to fund the + // wallet + bitcoind::mine(11).await?; + for _ in 0..10 { + let address = self.wallet.unused_address(); + bitcoind::fund(address.to_string(), Amount::from_sat(amount.to_sat() / 10)).await?; + } + bitcoind::mine(1).await?; - while self.get_confirmed_balance().await? < expected_balance { - let interval = Duration::from_millis(200); + tokio::time::timeout(Duration::from_secs(30), async { + while self.get_confirmed_balance().await.unwrap() < expected_balance { + let interval = Duration::from_millis(200); - self.sync_on_chain().await.unwrap(); + self.sync_on_chain().await.unwrap(); - tokio::time::sleep(interval).await; - tracing::debug!( - ?interval, - "Checking if wallet has been funded after interval" - ) - } + tokio::time::sleep(interval).await; + tracing::debug!( + ?interval, + "Checking if wallet has been funded after interval" + ); + } + }) + .await?; Ok(()) } @@ -253,12 +260,6 @@ impl Node { } } -async fn fund_and_mine(address: Address, amount: Amount) -> Result<()> { - bitcoind::fund(address.to_string(), amount).await?; - bitcoind::mine(1).await?; - Ok(()) -} - async fn setup_coordinator_payer_channel( payer_to_payee_invoice_amount: u64, coordinator: &Node, diff --git a/crates/tests-e2e/Cargo.toml b/crates/tests-e2e/Cargo.toml index eac27849e..19f8fdb73 100644 --- a/crates/tests-e2e/Cargo.toml +++ b/crates/tests-e2e/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" [dependencies] anyhow = "1" +bitcoin = "0.29" coordinator = { path = "../../coordinator" } coordinator-commons = { path = "../coordinator-commons" } flutter_rust_bridge = "1.78.0" @@ -23,6 +24,7 @@ tracing = "0.1.37" tracing-subscriber = { version = "0.3", features = ["env-filter"] } [dev-dependencies] +assertables = "7.0.1" bitcoin = "0.29" clap = { version = "4", features = ["derive"] } local-ip-address = "0.5.1" diff --git a/crates/tests-e2e/src/bitcoind.rs b/crates/tests-e2e/src/bitcoind.rs index 69b311773..9cbfe728c 100644 --- a/crates/tests-e2e/src/bitcoind.rs +++ b/crates/tests-e2e/src/bitcoind.rs @@ -1,5 +1,7 @@ use anyhow::bail; use anyhow::Result; +use bitcoin::Address; +use bitcoin::Amount; use reqwest::Client; use reqwest::Response; use serde::Deserialize; @@ -50,6 +52,20 @@ impl Bitcoind { Ok(()) } + pub async fn send_to_address(&self, address: Address, amount: Amount) -> Result { + let response = self + .client + .post(&self.host) + .body(format!( + r#"{{"jsonrpc": "1.0", "method": "sendtoaddress", "params": ["{}", "{}", "", "", false, false, null, null, false, 1.0]}}"#, + address, + amount.to_btc(), + )) + .send() + .await?; + Ok(response) + } + pub async fn post(&self, endpoint: &str, body: Option) -> Result { let mut builder = self.client.post(endpoint.to_string()); if let Some(body) = body { diff --git a/crates/tests-e2e/src/fund.rs b/crates/tests-e2e/src/fund.rs index 3c9f0e2a9..a81a50e43 100644 --- a/crates/tests-e2e/src/fund.rs +++ b/crates/tests-e2e/src/fund.rs @@ -1,31 +1,17 @@ +use anyhow::bail; use anyhow::Result; use native::api; use reqwest::Client; -use reqwest::Response; +use serde::Deserialize; use tokio::task::spawn_blocking; -use crate::bitcoind::Bitcoind; -use crate::coordinator::Coordinator; - // TODO: Fetch these from the app pub const FUNDING_TRANSACTION_FEES: u64 = 153; /// Pay a lightning invoice using an LND faucet /// /// Returns the funded amount (in satoshis) -pub async fn fund_app_with_faucet( - coordinator: &Coordinator, - client: &Client, - funding_amount: u64, -) -> Result { - let bitcoind = Bitcoind::new(client.clone()); - - // FIXME: We mine a block before funding the app to ensure that all - // outputs are spendable. This is necessary as the test might otherwise fail due to missing - // or unspendable output when broadcasting the funding transaction. - bitcoind.mine(1).await?; - coordinator.sync_wallet().await?; - +pub async fn fund_app_with_faucet(client: &Client, funding_amount: u64) -> Result { let invoice = spawn_blocking(move || { api::create_invoice_with_amount(funding_amount).expect("to succeed") }) @@ -37,29 +23,52 @@ pub async fn fund_app_with_faucet( // Ensure we sync the wallet info after funding spawn_blocking(move || api::refresh_wallet_info().expect("to succeed")).await?; - // FIXME: We mine a block before funding the app to ensure that all - // outputs are spendable. This is necessary as the test might otherwise fail due to missing - // or unspendable output when broadcasting the funding transaction. - bitcoind.mine(1).await?; - coordinator.sync_wallet().await?; - Ok(funding_amount - FUNDING_TRANSACTION_FEES) } -async fn pay_with_faucet(client: &Client, invoice: String) -> Result { +async fn pay_with_faucet(client: &Client, invoice: String) -> Result<()> { #[derive(serde::Serialize)] struct PayInvoice { payment_request: String, } + #[derive(Deserialize, Debug)] + struct FaucetResponse { + payment_error: Option, + } + #[derive(Deserialize, Debug)] + enum PaymentError { + #[serde(rename = "insufficient_balance")] + InsufficientBalance, + #[serde(rename = "no_route")] + NoRoute, + #[serde(rename = "")] + NoError, + } let faucet = "http://localhost:8080"; - let response = client + let body = serde_json::to_string(&PayInvoice { + payment_request: invoice, + })?; + let response: FaucetResponse = client .post(format!("{faucet}/lnd/v1/channels/transactions")) - .body(serde_json::to_string(&PayInvoice { - payment_request: invoice, - })?) + .body(body) .send() .await? - .error_for_status()?; - Ok(response) + .error_for_status()? + .json() + .await?; + if let Some(payment_error) = response.payment_error { + match payment_error { + PaymentError::InsufficientBalance => { + bail!("Could not fund wallet due to insufficient balance in faucet"); + } + PaymentError::NoRoute => { + bail!("Could not fund wallet due to no route found from faucet to app"); + } + PaymentError::NoError => { + tracing::info!("Payment succeeded 🚀") + } + } + } + Ok(()) } diff --git a/crates/tests-e2e/src/setup.rs b/crates/tests-e2e/src/setup.rs index 8f58e641a..22529d406 100644 --- a/crates/tests-e2e/src/setup.rs +++ b/crates/tests-e2e/src/setup.rs @@ -1,12 +1,16 @@ +use bitcoin::Address; +use bitcoin::Amount; use native::api; use native::api::ContractSymbol; use native::trade::order::api::NewOrder; use native::trade::order::api::OrderType; use native::trade::position::PositionState; +use std::str::FromStr; use tokio::task::spawn_blocking; use crate::app::run_app; use crate::app::AppHandle; +use crate::bitcoind::Bitcoind; use crate::coordinator::Coordinator; use crate::fund::fund_app_with_faucet; use crate::http::init_reqwest; @@ -25,9 +29,28 @@ impl TestSetup { let client = init_reqwest(); let coordinator = Coordinator::new_local(client.clone()); assert!(coordinator.is_running().await); + // ensure coordinator has a free UTXO available + let address = coordinator + .get_new_address() + .await + .expect("To be able to get a new address from coordinator"); + let bitcoind = Bitcoind::new(client.clone()); + bitcoind + .send_to_address( + Address::from_str(address.as_str()) + .expect("To be able to parse address string to address"), + Amount::ONE_BTC, + ) + .await + .expect("To be able to send to address"); + bitcoind.mine(1).await.expect("To be able to mine a block"); + coordinator + .sync_wallet() + .await + .expect("To be able to sync coordinator wallet"); let app = run_app().await; - let funded_amount = fund_app_with_faucet(&coordinator, &client, 50_000) + let funded_amount = fund_app_with_faucet(&client, 50_000) .await .expect("to be able to fund"); diff --git a/crates/tests-e2e/tests/basic.rs b/crates/tests-e2e/tests/basic.rs index f639c9e74..ba7813f85 100644 --- a/crates/tests-e2e/tests/basic.rs +++ b/crates/tests-e2e/tests/basic.rs @@ -1,5 +1,11 @@ use anyhow::Result; +use assertables::assert_ge; +use assertables::assert_ge_as_result; +use bitcoin::Address; +use bitcoin::Amount; +use std::str::FromStr; use tests_e2e::app::run_app; +use tests_e2e::bitcoind::Bitcoind; use tests_e2e::coordinator::Coordinator; use tests_e2e::fund::fund_app_with_faucet; use tests_e2e::http::init_reqwest; @@ -14,13 +20,26 @@ async fn app_can_be_funded_with_lnd_faucet() -> Result<()> { let coordinator = Coordinator::new_local(client.clone()); assert!(coordinator.is_running().await); + // ensure coordinator has a free UTXO available + let address = coordinator.get_new_address().await.unwrap(); + let bitcoind = Bitcoind::new(client.clone()); + bitcoind + .send_to_address( + Address::from_str(address.as_str()).unwrap(), + Amount::ONE_BTC, + ) + .await + .unwrap(); + bitcoind.mine(1).await.unwrap(); + coordinator.sync_wallet().await.unwrap(); + let app = run_app().await; // Unfunded wallet should be empty assert_eq!(app.rx.wallet_info().unwrap().balances.on_chain, 0); assert_eq!(app.rx.wallet_info().unwrap().balances.lightning, 0); - let funded_amount = fund_app_with_faucet(&coordinator, &client, 50_000).await?; + let funded_amount = fund_app_with_faucet(&client, 50_000).await?; assert_eq!(app.rx.wallet_info().unwrap().balances.on_chain, 0); @@ -29,6 +48,6 @@ async fn app_can_be_funded_with_lnd_faucet() -> Result<()> { // See: https://github.com/get10101/10101/issues/883 let ln_balance = app.rx.wallet_info().unwrap().balances.lightning; tracing::info!(%funded_amount, %ln_balance, "Successfully funded app with faucet"); - assert!(ln_balance >= funded_amount); + assert_ge!(ln_balance, funded_amount); Ok(()) } diff --git a/mobile/ios/Podfile.lock b/mobile/ios/Podfile.lock index b91baa4e8..3148e8322 100644 --- a/mobile/ios/Podfile.lock +++ b/mobile/ios/Podfile.lock @@ -41,7 +41,7 @@ SPEC CHECKSUMS: package_info_plus: fd030dabf36271f146f1f3beacd48f564b0f17f7 path_provider_foundation: eaf5b3e458fc0e5fbb9940fb09980e853fe058b8 share_plus: 599aa54e4ea31d4b4c0e9c911bcc26c55e791028 - shared_preferences_foundation: e2dae3258e06f44cc55f49d42024fd8dd03c590c + shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126 PODFILE CHECKSUM: cc1f88378b4bfcf93a6ce00d2c587857c6008d3b