Skip to content

Commit 1f3dfa8

Browse files
committed
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`.
1 parent 434db9f commit 1f3dfa8

File tree

4 files changed

+176
-24
lines changed

4 files changed

+176
-24
lines changed

src/error.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ pub enum Error {
77
AlreadyRunning,
88
/// Returned when trying to stop [`crate::Node`] while it is not running.
99
NotRunning,
10-
/// The funding transaction could not be created.
11-
FundingTxCreationFailed,
10+
/// An on-chain transaction could not be created.
11+
OnchainTxCreationFailed,
1212
/// A network connection has been closed.
1313
ConnectionFailed,
1414
/// Payment of the given invoice has already been intiated.
@@ -44,8 +44,8 @@ impl fmt::Display for Error {
4444
match *self {
4545
Self::AlreadyRunning => write!(f, "Node is already running."),
4646
Self::NotRunning => write!(f, "Node is not running."),
47-
Self::FundingTxCreationFailed => {
48-
write!(f, "Funding transaction could not be created.")
47+
Self::OnchainTxCreationFailed => {
48+
write!(f, "On-chain transaction could not be created.")
4949
}
5050
Self::ConnectionFailed => write!(f, "Network connection closed."),
5151
Self::NonUniquePaymentHash => write!(f, "An invoice must not get payed twice."),

src/lib.rs

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ use bdk::template::Bip84;
133133
use bitcoin::hashes::sha256::Hash as Sha256;
134134
use bitcoin::hashes::Hash;
135135
use bitcoin::secp256k1::PublicKey;
136-
use bitcoin::BlockHash;
136+
use bitcoin::{BlockHash, Txid};
137137

138138
use rand::Rng;
139139

@@ -850,10 +850,37 @@ impl Node {
850850
}
851851

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

857+
/// Send an on-chain payment to the given address.
858+
pub fn send_to_onchain_address(
859+
&self, address: &bitcoin::Address, amount_sats: u64,
860+
) -> Result<Txid, Error> {
861+
let runtime_lock = self.running.read().unwrap();
862+
if runtime_lock.is_none() {
863+
return Err(Error::NotRunning);
864+
}
865+
866+
let cur_balance = self.wallet.get_balance()?;
867+
if cur_balance.get_spendable() < amount_sats {
868+
log_error!(self.logger, "Unable to send payment due to insufficient funds.");
869+
return Err(Error::InsufficientFunds);
870+
}
871+
self.wallet.send_to_address(address, Some(amount_sats))
872+
}
873+
874+
/// Send an on-chain payment to the given address, draining all the available funds.
875+
pub fn send_all_to_onchain_address(&self, address: &bitcoin::Address) -> Result<Txid, Error> {
876+
let runtime_lock = self.running.read().unwrap();
877+
if runtime_lock.is_none() {
878+
return Err(Error::NotRunning);
879+
}
880+
881+
self.wallet.send_to_address(address, None)
882+
}
883+
857884
/// Retrieve a list of known channels.
858885
pub fn list_channels(&self) -> Vec<ChannelDetails> {
859886
self.channel_manager.list_channels()

src/test/functional_tests.rs

Lines changed: 65 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ fn channel_full_cycle() {
3030
);
3131
node_a.sync_wallets().unwrap();
3232
node_b.sync_wallets().unwrap();
33-
assert_eq!(node_a.on_chain_balance().unwrap().get_spendable(), premine_amount_sat);
34-
assert_eq!(node_b.on_chain_balance().unwrap().get_spendable(), premine_amount_sat);
33+
assert_eq!(node_a.onchain_balance().unwrap().get_spendable(), premine_amount_sat);
34+
assert_eq!(node_b.onchain_balance().unwrap().get_spendable(), premine_amount_sat);
3535

3636
println!("\nA -- connect_open_channel -> B");
3737
let funding_amount_sat = 80_000;
@@ -67,12 +67,12 @@ fn channel_full_cycle() {
6767
node_b.sync_wallets().unwrap();
6868

6969
let onchain_fee_buffer_sat = 1500;
70-
let node_a_balance = node_a.on_chain_balance().unwrap();
70+
let node_a_balance = node_a.onchain_balance().unwrap();
7171
let node_a_upper_bound_sat = premine_amount_sat - funding_amount_sat;
7272
let node_a_lower_bound_sat = premine_amount_sat - funding_amount_sat - onchain_fee_buffer_sat;
7373
assert!(node_a_balance.get_spendable() < node_a_upper_bound_sat);
7474
assert!(node_a_balance.get_spendable() > node_a_lower_bound_sat);
75-
assert_eq!(node_b.on_chain_balance().unwrap().get_spendable(), premine_amount_sat);
75+
assert_eq!(node_b.onchain_balance().unwrap().get_spendable(), premine_amount_sat);
7676

7777
expect_event!(node_a, ChannelReady);
7878

@@ -195,13 +195,10 @@ fn channel_full_cycle() {
195195
let node_a_upper_bound_sat =
196196
(premine_amount_sat - funding_amount_sat) + (funding_amount_sat - sum_of_all_payments_sat);
197197
let node_a_lower_bound_sat = node_a_upper_bound_sat - onchain_fee_buffer_sat;
198-
assert!(node_a.on_chain_balance().unwrap().get_spendable() > node_a_lower_bound_sat);
199-
assert!(node_a.on_chain_balance().unwrap().get_spendable() < node_a_upper_bound_sat);
198+
assert!(node_a.onchain_balance().unwrap().get_spendable() > node_a_lower_bound_sat);
199+
assert!(node_a.onchain_balance().unwrap().get_spendable() < node_a_upper_bound_sat);
200200
let expected_final_amount_node_b_sat = premine_amount_sat + sum_of_all_payments_sat;
201-
assert_eq!(
202-
node_b.on_chain_balance().unwrap().get_spendable(),
203-
expected_final_amount_node_b_sat
204-
);
201+
assert_eq!(node_b.onchain_balance().unwrap().get_spendable(), expected_final_amount_node_b_sat);
205202

206203
node_a.stop().unwrap();
207204
println!("\nA stopped");
@@ -235,8 +232,8 @@ fn channel_open_fails_when_funds_insufficient() {
235232
);
236233
node_a.sync_wallets().unwrap();
237234
node_b.sync_wallets().unwrap();
238-
assert_eq!(node_a.on_chain_balance().unwrap().get_spendable(), premine_amount_sat);
239-
assert_eq!(node_b.on_chain_balance().unwrap().get_spendable(), premine_amount_sat);
235+
assert_eq!(node_a.onchain_balance().unwrap().get_spendable(), premine_amount_sat);
236+
assert_eq!(node_b.onchain_balance().unwrap().get_spendable(), premine_amount_sat);
240237

241238
println!("\nA -- connect_open_channel -> B");
242239
assert_eq!(
@@ -276,13 +273,13 @@ fn start_stop_reinit() {
276273
let expected_amount = Amount::from_sat(100000);
277274

278275
premine_and_distribute_funds(&bitcoind, &electrsd, vec![funding_address], expected_amount);
279-
assert_eq!(node.on_chain_balance().unwrap().get_total(), 0);
276+
assert_eq!(node.onchain_balance().unwrap().get_total(), 0);
280277

281278
node.start().unwrap();
282279
assert_eq!(node.start(), Err(Error::AlreadyRunning));
283280

284281
node.sync_wallets().unwrap();
285-
assert_eq!(node.on_chain_balance().unwrap().get_spendable(), expected_amount.to_sat());
282+
assert_eq!(node.onchain_balance().unwrap().get_spendable(), expected_amount.to_sat());
286283

287284
node.stop().unwrap();
288285
assert_eq!(node.stop(), Err(Error::NotRunning));
@@ -300,15 +297,67 @@ fn start_stop_reinit() {
300297
reinitialized_node.start().unwrap();
301298

302299
assert_eq!(
303-
reinitialized_node.on_chain_balance().unwrap().get_spendable(),
300+
reinitialized_node.onchain_balance().unwrap().get_spendable(),
304301
expected_amount.to_sat()
305302
);
306303

307304
reinitialized_node.sync_wallets().unwrap();
308305
assert_eq!(
309-
reinitialized_node.on_chain_balance().unwrap().get_spendable(),
306+
reinitialized_node.onchain_balance().unwrap().get_spendable(),
310307
expected_amount.to_sat()
311308
);
312309

313310
reinitialized_node.stop().unwrap();
314311
}
312+
313+
#[test]
314+
fn onchain_spend_receive() {
315+
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
316+
let esplora_url = electrsd.esplora_url.as_ref().unwrap();
317+
318+
let config_a = random_config(esplora_url);
319+
let node_a = Builder::from_config(config_a).build();
320+
node_a.start().unwrap();
321+
let addr_a = node_a.new_funding_address().unwrap();
322+
323+
let config_b = random_config(esplora_url);
324+
let node_b = Builder::from_config(config_b).build();
325+
node_b.start().unwrap();
326+
let addr_b = node_b.new_funding_address().unwrap();
327+
328+
premine_and_distribute_funds(
329+
&bitcoind,
330+
&electrsd,
331+
vec![addr_b.clone()],
332+
Amount::from_sat(100000),
333+
);
334+
335+
node_a.sync_wallets().unwrap();
336+
node_b.sync_wallets().unwrap();
337+
assert_eq!(node_b.onchain_balance().unwrap().get_spendable(), 100000);
338+
339+
assert_eq!(Err(Error::InsufficientFunds), node_a.send_to_onchain_address(&addr_b, 1000));
340+
341+
let txid = node_b.send_to_onchain_address(&addr_a, 1000).unwrap();
342+
generate_blocks_and_wait(&bitcoind, &electrsd, 6);
343+
wait_for_tx(&electrsd, txid);
344+
345+
node_a.sync_wallets().unwrap();
346+
node_b.sync_wallets().unwrap();
347+
348+
assert_eq!(node_a.onchain_balance().unwrap().get_spendable(), 1000);
349+
assert!(node_b.onchain_balance().unwrap().get_spendable() > 98000);
350+
assert!(node_b.onchain_balance().unwrap().get_spendable() < 100000);
351+
352+
let addr_b = node_b.new_funding_address().unwrap();
353+
let txid = node_a.send_all_to_onchain_address(&addr_b).unwrap();
354+
generate_blocks_and_wait(&bitcoind, &electrsd, 6);
355+
wait_for_tx(&electrsd, txid);
356+
357+
node_a.sync_wallets().unwrap();
358+
node_b.sync_wallets().unwrap();
359+
360+
assert_eq!(node_a.onchain_balance().unwrap().get_total(), 0);
361+
assert!(node_b.onchain_balance().unwrap().get_spendable() > 99000);
362+
assert!(node_b.onchain_balance().unwrap().get_spendable() < 100000);
363+
}

src/wallet.rs

Lines changed: 78 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ use bitcoin::bech32::u5;
2222
use bitcoin::secp256k1::ecdh::SharedSecret;
2323
use bitcoin::secp256k1::ecdsa::{RecoverableSignature, Signature};
2424
use bitcoin::secp256k1::{PublicKey, Scalar, Secp256k1, Signing};
25-
use bitcoin::{Script, Transaction, TxOut};
25+
use bitcoin::{Script, Transaction, TxOut, Txid};
2626

2727
use std::collections::HashMap;
2828
use std::sync::{Arc, Condvar, Mutex, RwLock};
@@ -187,7 +187,7 @@ where
187187
match locked_wallet.sign(&mut psbt, SignOptions::default()) {
188188
Ok(finalized) => {
189189
if !finalized {
190-
return Err(Error::FundingTxCreationFailed);
190+
return Err(Error::OnchainTxCreationFailed);
191191
}
192192
}
193193
Err(err) => {
@@ -208,6 +208,82 @@ where
208208
Ok(self.inner.lock().unwrap().get_balance()?)
209209
}
210210

211+
/// Send funds to the given address.
212+
///
213+
/// If `amount_msat_or_drain` is `None` the wallet will be drained, i.e., all available funds will be
214+
/// spent.
215+
pub(crate) fn send_to_address(
216+
&self, address: &bitcoin::Address, amount_msat_or_drain: Option<u64>,
217+
) -> Result<Txid, Error> {
218+
let confirmation_target = ConfirmationTarget::Normal;
219+
let fee_rate = self.estimate_fee_rate(confirmation_target);
220+
221+
let tx = {
222+
let locked_wallet = self.inner.lock().unwrap();
223+
let mut tx_builder = locked_wallet.build_tx();
224+
225+
if let Some(amount_sats) = amount_msat_or_drain {
226+
tx_builder
227+
.add_recipient(address.script_pubkey(), amount_sats)
228+
.fee_rate(fee_rate)
229+
.enable_rbf();
230+
} else {
231+
tx_builder
232+
.drain_wallet()
233+
.drain_to(address.script_pubkey())
234+
.fee_rate(fee_rate)
235+
.enable_rbf();
236+
}
237+
238+
let mut psbt = match tx_builder.finish() {
239+
Ok((psbt, _)) => {
240+
log_trace!(self.logger, "Created PSBT: {:?}", psbt);
241+
psbt
242+
}
243+
Err(err) => {
244+
log_error!(self.logger, "Failed to create transaction: {}", err);
245+
return Err(err.into());
246+
}
247+
};
248+
249+
match locked_wallet.sign(&mut psbt, SignOptions::default()) {
250+
Ok(finalized) => {
251+
if !finalized {
252+
return Err(Error::OnchainTxCreationFailed);
253+
}
254+
}
255+
Err(err) => {
256+
log_error!(self.logger, "Failed to create transaction: {}", err);
257+
return Err(err.into());
258+
}
259+
}
260+
psbt.extract_tx()
261+
};
262+
263+
self.broadcast_transaction(&tx);
264+
265+
let txid = tx.txid();
266+
267+
if let Some(amount_sats) = amount_msat_or_drain {
268+
log_info!(
269+
self.logger,
270+
"Created new transaction {} sending {}sats on-chain to address {}",
271+
txid,
272+
amount_sats,
273+
address
274+
);
275+
} else {
276+
log_info!(
277+
self.logger,
278+
"Created new transaction {} sending all available on-chain funds to address {}",
279+
txid,
280+
address
281+
);
282+
}
283+
284+
Ok(txid)
285+
}
286+
211287
fn estimate_fee_rate(&self, confirmation_target: ConfirmationTarget) -> FeeRate {
212288
let locked_fee_rate_cache = self.fee_rate_cache.read().unwrap();
213289

0 commit comments

Comments
 (0)