diff --git a/src/error.rs b/src/error.rs index cefa57538..eb0028006 100644 --- a/src/error.rs +++ b/src/error.rs @@ -7,8 +7,8 @@ pub enum Error { AlreadyRunning, /// Returned when trying to stop [`crate::Node`] while it is not running. NotRunning, - /// The funding transaction could not be created. - FundingTxCreationFailed, + /// An on-chain transaction could not be created. + OnchainTxCreationFailed, /// A network connection has been closed. ConnectionFailed, /// Payment of the given invoice has already been intiated. @@ -44,8 +44,8 @@ impl fmt::Display for Error { match *self { Self::AlreadyRunning => write!(f, "Node is already running."), Self::NotRunning => write!(f, "Node is not running."), - Self::FundingTxCreationFailed => { - write!(f, "Funding transaction could not be created.") + Self::OnchainTxCreationFailed => { + write!(f, "On-chain transaction could not be created.") } Self::ConnectionFailed => write!(f, "Network connection closed."), Self::NonUniquePaymentHash => write!(f, "An invoice must not get payed twice."), diff --git a/src/lib.rs b/src/lib.rs index 8b2d1333b..57f4973a8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -133,7 +133,7 @@ use bdk::template::Bip84; use bitcoin::hashes::sha256::Hash as Sha256; use bitcoin::hashes::Hash; use bitcoin::secp256k1::PublicKey; -use bitcoin::BlockHash; +use bitcoin::{BlockHash, Txid}; use rand::Rng; @@ -850,10 +850,37 @@ impl Node { } /// Retrieve the current on-chain balance. - pub fn on_chain_balance(&self) -> Result { + pub fn onchain_balance(&self) -> Result { self.wallet.get_balance() } + /// Send an on-chain payment to the given address. + pub fn send_to_onchain_address( + &self, address: &bitcoin::Address, amount_sats: u64, + ) -> Result { + let runtime_lock = self.running.read().unwrap(); + if runtime_lock.is_none() { + return Err(Error::NotRunning); + } + + let cur_balance = self.wallet.get_balance()?; + if cur_balance.get_spendable() < amount_sats { + log_error!(self.logger, "Unable to send payment due to insufficient funds."); + return Err(Error::InsufficientFunds); + } + self.wallet.send_to_address(address, Some(amount_sats)) + } + + /// Send an on-chain payment to the given address, draining all the available funds. + pub fn send_all_to_onchain_address(&self, address: &bitcoin::Address) -> Result { + let runtime_lock = self.running.read().unwrap(); + if runtime_lock.is_none() { + return Err(Error::NotRunning); + } + + self.wallet.send_to_address(address, None) + } + /// Retrieve a list of known channels. pub fn list_channels(&self) -> Vec { self.channel_manager.list_channels() diff --git a/src/test/functional_tests.rs b/src/test/functional_tests.rs index 04078c2d2..c4b1d1662 100644 --- a/src/test/functional_tests.rs +++ b/src/test/functional_tests.rs @@ -30,8 +30,8 @@ fn channel_full_cycle() { ); node_a.sync_wallets().unwrap(); node_b.sync_wallets().unwrap(); - assert_eq!(node_a.on_chain_balance().unwrap().get_spendable(), premine_amount_sat); - assert_eq!(node_b.on_chain_balance().unwrap().get_spendable(), premine_amount_sat); + assert_eq!(node_a.onchain_balance().unwrap().get_spendable(), premine_amount_sat); + assert_eq!(node_b.onchain_balance().unwrap().get_spendable(), premine_amount_sat); println!("\nA -- connect_open_channel -> B"); let funding_amount_sat = 80_000; @@ -67,12 +67,12 @@ fn channel_full_cycle() { node_b.sync_wallets().unwrap(); let onchain_fee_buffer_sat = 1500; - let node_a_balance = node_a.on_chain_balance().unwrap(); + let node_a_balance = node_a.onchain_balance().unwrap(); let node_a_upper_bound_sat = premine_amount_sat - funding_amount_sat; let node_a_lower_bound_sat = premine_amount_sat - funding_amount_sat - onchain_fee_buffer_sat; assert!(node_a_balance.get_spendable() < node_a_upper_bound_sat); assert!(node_a_balance.get_spendable() > node_a_lower_bound_sat); - assert_eq!(node_b.on_chain_balance().unwrap().get_spendable(), premine_amount_sat); + assert_eq!(node_b.onchain_balance().unwrap().get_spendable(), premine_amount_sat); expect_event!(node_a, ChannelReady); @@ -195,13 +195,10 @@ fn channel_full_cycle() { let node_a_upper_bound_sat = (premine_amount_sat - funding_amount_sat) + (funding_amount_sat - sum_of_all_payments_sat); let node_a_lower_bound_sat = node_a_upper_bound_sat - onchain_fee_buffer_sat; - assert!(node_a.on_chain_balance().unwrap().get_spendable() > node_a_lower_bound_sat); - assert!(node_a.on_chain_balance().unwrap().get_spendable() < node_a_upper_bound_sat); + assert!(node_a.onchain_balance().unwrap().get_spendable() > node_a_lower_bound_sat); + assert!(node_a.onchain_balance().unwrap().get_spendable() < node_a_upper_bound_sat); let expected_final_amount_node_b_sat = premine_amount_sat + sum_of_all_payments_sat; - assert_eq!( - node_b.on_chain_balance().unwrap().get_spendable(), - expected_final_amount_node_b_sat - ); + assert_eq!(node_b.onchain_balance().unwrap().get_spendable(), expected_final_amount_node_b_sat); node_a.stop().unwrap(); println!("\nA stopped"); @@ -235,8 +232,8 @@ fn channel_open_fails_when_funds_insufficient() { ); node_a.sync_wallets().unwrap(); node_b.sync_wallets().unwrap(); - assert_eq!(node_a.on_chain_balance().unwrap().get_spendable(), premine_amount_sat); - assert_eq!(node_b.on_chain_balance().unwrap().get_spendable(), premine_amount_sat); + assert_eq!(node_a.onchain_balance().unwrap().get_spendable(), premine_amount_sat); + assert_eq!(node_b.onchain_balance().unwrap().get_spendable(), premine_amount_sat); println!("\nA -- connect_open_channel -> B"); assert_eq!( @@ -276,13 +273,13 @@ fn start_stop_reinit() { let expected_amount = Amount::from_sat(100000); premine_and_distribute_funds(&bitcoind, &electrsd, vec![funding_address], expected_amount); - assert_eq!(node.on_chain_balance().unwrap().get_total(), 0); + assert_eq!(node.onchain_balance().unwrap().get_total(), 0); node.start().unwrap(); assert_eq!(node.start(), Err(Error::AlreadyRunning)); node.sync_wallets().unwrap(); - assert_eq!(node.on_chain_balance().unwrap().get_spendable(), expected_amount.to_sat()); + assert_eq!(node.onchain_balance().unwrap().get_spendable(), expected_amount.to_sat()); node.stop().unwrap(); assert_eq!(node.stop(), Err(Error::NotRunning)); @@ -300,15 +297,67 @@ fn start_stop_reinit() { reinitialized_node.start().unwrap(); assert_eq!( - reinitialized_node.on_chain_balance().unwrap().get_spendable(), + reinitialized_node.onchain_balance().unwrap().get_spendable(), expected_amount.to_sat() ); reinitialized_node.sync_wallets().unwrap(); assert_eq!( - reinitialized_node.on_chain_balance().unwrap().get_spendable(), + reinitialized_node.onchain_balance().unwrap().get_spendable(), expected_amount.to_sat() ); reinitialized_node.stop().unwrap(); } + +#[test] +fn onchain_spend_receive() { + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let esplora_url = electrsd.esplora_url.as_ref().unwrap(); + + let config_a = random_config(esplora_url); + let node_a = Builder::from_config(config_a).build(); + node_a.start().unwrap(); + let addr_a = node_a.new_funding_address().unwrap(); + + let config_b = random_config(esplora_url); + let node_b = Builder::from_config(config_b).build(); + node_b.start().unwrap(); + let addr_b = node_b.new_funding_address().unwrap(); + + premine_and_distribute_funds( + &bitcoind, + &electrsd, + vec![addr_b.clone()], + Amount::from_sat(100000), + ); + + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + assert_eq!(node_b.onchain_balance().unwrap().get_spendable(), 100000); + + assert_eq!(Err(Error::InsufficientFunds), node_a.send_to_onchain_address(&addr_b, 1000)); + + let txid = node_b.send_to_onchain_address(&addr_a, 1000).unwrap(); + generate_blocks_and_wait(&bitcoind, &electrsd, 6); + wait_for_tx(&electrsd, txid); + + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + + assert_eq!(node_a.onchain_balance().unwrap().get_spendable(), 1000); + assert!(node_b.onchain_balance().unwrap().get_spendable() > 98000); + assert!(node_b.onchain_balance().unwrap().get_spendable() < 100000); + + let addr_b = node_b.new_funding_address().unwrap(); + let txid = node_a.send_all_to_onchain_address(&addr_b).unwrap(); + generate_blocks_and_wait(&bitcoind, &electrsd, 6); + wait_for_tx(&electrsd, txid); + + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + + assert_eq!(node_a.onchain_balance().unwrap().get_total(), 0); + assert!(node_b.onchain_balance().unwrap().get_spendable() > 99000); + assert!(node_b.onchain_balance().unwrap().get_spendable() < 100000); +} diff --git a/src/wallet.rs b/src/wallet.rs index beb8861c6..ee4d6122a 100644 --- a/src/wallet.rs +++ b/src/wallet.rs @@ -22,7 +22,7 @@ use bitcoin::bech32::u5; use bitcoin::secp256k1::ecdh::SharedSecret; use bitcoin::secp256k1::ecdsa::{RecoverableSignature, Signature}; use bitcoin::secp256k1::{PublicKey, Scalar, Secp256k1, Signing}; -use bitcoin::{Script, Transaction, TxOut}; +use bitcoin::{Script, Transaction, TxOut, Txid}; use std::collections::HashMap; use std::sync::{Arc, Condvar, Mutex, RwLock}; @@ -187,7 +187,7 @@ where match locked_wallet.sign(&mut psbt, SignOptions::default()) { Ok(finalized) => { if !finalized { - return Err(Error::FundingTxCreationFailed); + return Err(Error::OnchainTxCreationFailed); } } Err(err) => { @@ -208,6 +208,82 @@ where Ok(self.inner.lock().unwrap().get_balance()?) } + /// Send funds to the given address. + /// + /// If `amount_msat_or_drain` is `None` the wallet will be drained, i.e., all available funds will be + /// spent. + pub(crate) fn send_to_address( + &self, address: &bitcoin::Address, amount_msat_or_drain: Option, + ) -> Result { + let confirmation_target = ConfirmationTarget::Normal; + let fee_rate = self.estimate_fee_rate(confirmation_target); + + let tx = { + let locked_wallet = self.inner.lock().unwrap(); + let mut tx_builder = locked_wallet.build_tx(); + + if let Some(amount_sats) = amount_msat_or_drain { + tx_builder + .add_recipient(address.script_pubkey(), amount_sats) + .fee_rate(fee_rate) + .enable_rbf(); + } else { + tx_builder + .drain_wallet() + .drain_to(address.script_pubkey()) + .fee_rate(fee_rate) + .enable_rbf(); + } + + let mut psbt = match tx_builder.finish() { + Ok((psbt, _)) => { + log_trace!(self.logger, "Created PSBT: {:?}", psbt); + psbt + } + Err(err) => { + log_error!(self.logger, "Failed to create transaction: {}", err); + return Err(err.into()); + } + }; + + match locked_wallet.sign(&mut psbt, SignOptions::default()) { + Ok(finalized) => { + if !finalized { + return Err(Error::OnchainTxCreationFailed); + } + } + Err(err) => { + log_error!(self.logger, "Failed to create transaction: {}", err); + return Err(err.into()); + } + } + psbt.extract_tx() + }; + + self.broadcast_transaction(&tx); + + let txid = tx.txid(); + + if let Some(amount_sats) = amount_msat_or_drain { + log_info!( + self.logger, + "Created new transaction {} sending {}sats on-chain to address {}", + txid, + amount_sats, + address + ); + } else { + log_info!( + self.logger, + "Created new transaction {} sending all available on-chain funds to address {}", + txid, + address + ); + } + + Ok(txid) + } + fn estimate_fee_rate(&self, confirmation_target: ConfirmationTarget) -> FeeRate { let locked_fee_rate_cache = self.fee_rate_cache.read().unwrap();