Skip to content

Commit

Permalink
Allow for spending on-chain funds
Browse files Browse the repository at this point in the history
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`.
  • Loading branch information
tnull committed Apr 7, 2023
1 parent 40f82b8 commit 167583e
Show file tree
Hide file tree
Showing 4 changed files with 131 additions and 20 deletions.
8 changes: 4 additions & 4 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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."),
Expand Down
21 changes: 19 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -832,7 +832,7 @@ impl Node {
}

/// Retrieve the current on-chain balance.
pub fn on_chain_balance(&self) -> Result<bdk::Balance, Error> {
pub fn onchain_balance(&self) -> Result<bdk::Balance, Error> {
self.wallet.get_balance()
}

Expand Down Expand Up @@ -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<Txid, Error> {
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(
Expand Down
64 changes: 52 additions & 12 deletions src/test/functional_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand All @@ -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);

Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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));
Expand All @@ -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);
}
58 changes: 56 additions & 2 deletions src/wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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) => {
Expand All @@ -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<Txid, Error> {
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();

Expand Down

0 comments on commit 167583e

Please sign in to comment.