Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow for withdrawing on-chain funds #61

Merged
merged 1 commit into from
Apr 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
31 changes: 29 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -850,10 +850,37 @@ 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()
}

/// Send an on-chain payment to the given address.
pub fn send_to_onchain_address(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason why we don't expose the wallet? Something to do with the runtime?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there are a number of reasons. For example I'd really like to keep the main API centered around a single Node object, at least as long as that is still feasible. We hopefully never get to a place where the API surface is getting unfeasibly large for a single object. If it does this might rather act as a canary that we should refactor it into something more minimal rather than breaking out individual objects.

Additionally, it's tbd how and what additional wallet behavior we will/should expose to give users more flexibility. Post-0.1 we might think about offering a wallet interface to allow users to bring their own implementation, which I'm however still not convinced of, as this might as well be quite a footgun. However, if we were to go this route, the exposed API also needs to be separate from the concrete wallet implementation. We'll see..

&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, 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<Txid, Error> {
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<ChannelDetails> {
self.channel_manager.list_channels()
Expand Down
81 changes: 65 additions & 16 deletions src/test/functional_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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!(
Expand Down Expand Up @@ -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));
Expand All @@ -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);
}
80 changes: 78 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,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(
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps it is better to allow pass an optional fee rate?
Also a lightning node user would probably want to send all the onchain funds (empty the wallet) so perhaps this use case should be taken into account when exposing this ability at the Node level.

Copy link
Collaborator Author

@tnull tnull Apr 4, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps it is better to allow pass an optional fee rate?

We likely don't want to expose all the complexity of on-chain fee estimation to the user, but provide sane defaults, otherwise we'll end up replicating more and more of BDK/LDK APIs. We could consider letting them set a specific ConfirmationTarget, but I'm not fully convinced that we want to complicate the API that far here. That said, we probably should provide a config knob allowing to bound the fee rate (for all on-chain operations) eventually, which is tracked here: #55

Also a lightning node user would probably want to send all the onchain funds (empty the wallet) so perhaps this use case should be taken into account when exposing this ability at the Node level.

Ah, good idea. I now added a withdraw_all method (and renamed send_onchain_payment to withdraw for consistency).

&self, address: &bitcoin::Address, amount_msat_or_drain: Option<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();

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();

Expand Down