From 1f3dfa819bc236636f0d2251c517f6fcc37a7f42 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Tue, 4 Apr 2023 09:40:24 +0200 Subject: [PATCH] Allow for spending on-chain funds Previously we only exposed methods to generate new addresses, retrieve the balance, and fund channels. Here, we fix the oversight and allow users to actually withdraw their funds again. We also rename some API methods for consistency of the term `onchain`. --- src/error.rs | 8 ++-- src/lib.rs | 31 +++++++++++++- src/test/functional_tests.rs | 81 +++++++++++++++++++++++++++++------- src/wallet.rs | 80 ++++++++++++++++++++++++++++++++++- 4 files changed, 176 insertions(+), 24 deletions(-) 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();