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 99672a038..c3acd6b58 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -129,7 +129,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; @@ -832,7 +832,7 @@ 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() } @@ -1213,6 +1213,23 @@ impl Node { } } + /// Send an on-chain payment to the given address. + pub fn send_onchain_payment( + &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, amount_sats) + } + /// Returns a payable invoice that can be used to request and receive a payment of the amount /// given. pub fn receive_payment( diff --git a/src/test/functional_tests.rs b/src/test/functional_tests.rs index e2074af57..ce969a341 100644 --- a/src/test/functional_tests.rs +++ b/src/test/functional_tests.rs @@ -29,8 +29,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(), 100000); - assert_eq!(node_b.on_chain_balance().unwrap().get_spendable(), 100000); + assert_eq!(node_a.onchain_balance().unwrap().get_spendable(), 100000); + assert_eq!(node_b.onchain_balance().unwrap().get_spendable(), 100000); println!("\nA -- connect_open_channel -> B"); let node_b_addr = format!("{}@{}", node_b.node_id(), node_b.listening_address().unwrap()); @@ -53,10 +53,10 @@ fn channel_full_cycle() { node_a.sync_wallets().unwrap(); node_b.sync_wallets().unwrap(); - let node_a_balance = node_a.on_chain_balance().unwrap(); + let node_a_balance = node_a.onchain_balance().unwrap(); assert!(node_a_balance.get_spendable() < 50000); assert!(node_a_balance.get_spendable() > 40000); - assert_eq!(node_b.on_chain_balance().unwrap().get_spendable(), 100000); + assert_eq!(node_b.onchain_balance().unwrap().get_spendable(), 100000); expect_event!(node_a, ChannelReady); @@ -174,8 +174,8 @@ fn channel_full_cycle() { node_a.sync_wallets().unwrap(); node_b.sync_wallets().unwrap(); - assert!(node_a.on_chain_balance().unwrap().get_spendable() > 90000); - assert_eq!(node_b.on_chain_balance().unwrap().get_spendable(), 103234); + assert!(node_a.onchain_balance().unwrap().get_spendable() > 90000); + assert_eq!(node_b.onchain_balance().unwrap().get_spendable(), 103234); node_a.stop().unwrap(); println!("\nA stopped"); @@ -207,8 +207,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(), 100000); - assert_eq!(node_b.on_chain_balance().unwrap().get_spendable(), 100000); + assert_eq!(node_a.onchain_balance().unwrap().get_spendable(), 100000); + assert_eq!(node_b.onchain_balance().unwrap().get_spendable(), 100000); println!("\nA -- connect_open_channel -> B"); let node_b_addr = format!("{}@{}", node_b.node_id(), node_b.listening_address().unwrap()); @@ -243,13 +243,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)); @@ -267,15 +267,55 @@ 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_onchain_payment(&addr_b, 1000)); + + let txid = node_b.send_onchain_payment(&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); +} diff --git a/src/wallet.rs b/src/wallet.rs index beb8861c6..dc83c1b48 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,60 @@ where Ok(self.inner.lock().unwrap().get_balance()?) } + pub(crate) fn send_to_address( + &self, address: &bitcoin::Address, amount_sats: u64, + ) -> 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(); + + tx_builder + .add_recipient(address.script_pubkey(), amount_sats) + .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(); + log_info!( + self.logger, + "Created new transaction {} sending {}sats on-chain to address {}", + txid, + amount_sats, + address + ); + + Ok(txid) + } + fn estimate_fee_rate(&self, confirmation_target: ConfirmationTarget) -> FeeRate { let locked_fee_rate_cache = self.fee_rate_cache.read().unwrap();