From 167583eccf0d8f9c7053a9a04b698212acd5bd36 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 | 21 ++++++++++-- src/test/functional_tests.rs | 64 +++++++++++++++++++++++++++++------- src/wallet.rs | 58 ++++++++++++++++++++++++++++++-- 4 files changed, 131 insertions(+), 20 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 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();