Skip to content

Commit 7519ca1

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 2ceb197 commit 7519ca1

File tree

4 files changed

+132
-24
lines changed

4 files changed

+132
-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: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ use bdk::template::Bip84;
129129
use bitcoin::hashes::sha256::Hash as Sha256;
130130
use bitcoin::hashes::Hash;
131131
use bitcoin::secp256k1::PublicKey;
132-
use bitcoin::BlockHash;
132+
use bitcoin::{BlockHash, Txid};
133133

134134
use rand::Rng;
135135

@@ -832,7 +832,7 @@ impl Node {
832832
}
833833

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

@@ -1218,6 +1218,23 @@ impl Node {
12181218
}
12191219
}
12201220

1221+
/// Send an on-chain payment to the given address.
1222+
pub fn send_onchain_payment(
1223+
&self, address: &bitcoin::Address, amount_sats: u64,
1224+
) -> Result<Txid, Error> {
1225+
let runtime_lock = self.running.read().unwrap();
1226+
if runtime_lock.is_none() {
1227+
return Err(Error::NotRunning);
1228+
}
1229+
1230+
let cur_balance = self.wallet.get_balance()?;
1231+
if cur_balance.get_spendable() < amount_sats {
1232+
log_error!(self.logger, "Unable to send payment due to insufficient funds.");
1233+
return Err(Error::InsufficientFunds);
1234+
}
1235+
self.wallet.send_to_address(address, amount_sats)
1236+
}
1237+
12211238
/// Returns a payable invoice that can be used to request and receive a payment of the amount
12221239
/// given.
12231240
pub fn receive_payment(

src/test/functional_tests.rs

Lines changed: 53 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;
@@ -60,12 +60,12 @@ fn channel_full_cycle() {
6060
node_b.sync_wallets().unwrap();
6161

6262
let onchain_fee_buffer_sat = 1500;
63-
let node_a_balance = node_a.on_chain_balance().unwrap();
63+
let node_a_balance = node_a.onchain_balance().unwrap();
6464
let node_a_upper_bound_sat = premine_amount_sat - funding_amount_sat;
6565
let node_a_lower_bound_sat = premine_amount_sat - funding_amount_sat - onchain_fee_buffer_sat;
6666
assert!(node_a_balance.get_spendable() < node_a_upper_bound_sat);
6767
assert!(node_a_balance.get_spendable() > node_a_lower_bound_sat);
68-
assert_eq!(node_b.on_chain_balance().unwrap().get_spendable(), premine_amount_sat);
68+
assert_eq!(node_b.onchain_balance().unwrap().get_spendable(), premine_amount_sat);
6969

7070
expect_event!(node_a, ChannelReady);
7171

@@ -188,13 +188,10 @@ fn channel_full_cycle() {
188188
let node_a_upper_bound_sat =
189189
(premine_amount_sat - funding_amount_sat) + (funding_amount_sat - sum_of_all_payments_sat);
190190
let node_a_lower_bound_sat = node_a_upper_bound_sat - onchain_fee_buffer_sat;
191-
assert!(node_a.on_chain_balance().unwrap().get_spendable() > node_a_lower_bound_sat);
192-
assert!(node_a.on_chain_balance().unwrap().get_spendable() < node_a_upper_bound_sat);
191+
assert!(node_a.onchain_balance().unwrap().get_spendable() > node_a_lower_bound_sat);
192+
assert!(node_a.onchain_balance().unwrap().get_spendable() < node_a_upper_bound_sat);
193193
let expected_final_amount_node_b_sat = premine_amount_sat + sum_of_all_payments_sat;
194-
assert_eq!(
195-
node_b.on_chain_balance().unwrap().get_spendable(),
196-
expected_final_amount_node_b_sat
197-
);
194+
assert_eq!(node_b.onchain_balance().unwrap().get_spendable(), expected_final_amount_node_b_sat);
198195

199196
node_a.stop().unwrap();
200197
println!("\nA stopped");
@@ -228,8 +225,8 @@ fn channel_open_fails_when_funds_insufficient() {
228225
);
229226
node_a.sync_wallets().unwrap();
230227
node_b.sync_wallets().unwrap();
231-
assert_eq!(node_a.on_chain_balance().unwrap().get_spendable(), premine_amount_sat);
232-
assert_eq!(node_b.on_chain_balance().unwrap().get_spendable(), premine_amount_sat);
228+
assert_eq!(node_a.onchain_balance().unwrap().get_spendable(), premine_amount_sat);
229+
assert_eq!(node_b.onchain_balance().unwrap().get_spendable(), premine_amount_sat);
233230

234231
println!("\nA -- connect_open_channel -> B");
235232
let node_b_addr = format!("{}@{}", node_b.node_id(), node_b.listening_address().unwrap());
@@ -264,13 +261,13 @@ fn start_stop_reinit() {
264261
let expected_amount = Amount::from_sat(100000);
265262

266263
premine_and_distribute_funds(&bitcoind, &electrsd, vec![funding_address], expected_amount);
267-
assert_eq!(node.on_chain_balance().unwrap().get_total(), 0);
264+
assert_eq!(node.onchain_balance().unwrap().get_total(), 0);
268265

269266
node.start().unwrap();
270267
assert_eq!(node.start(), Err(Error::AlreadyRunning));
271268

272269
node.sync_wallets().unwrap();
273-
assert_eq!(node.on_chain_balance().unwrap().get_spendable(), expected_amount.to_sat());
270+
assert_eq!(node.onchain_balance().unwrap().get_spendable(), expected_amount.to_sat());
274271

275272
node.stop().unwrap();
276273
assert_eq!(node.stop(), Err(Error::NotRunning));
@@ -288,15 +285,55 @@ fn start_stop_reinit() {
288285
reinitialized_node.start().unwrap();
289286

290287
assert_eq!(
291-
reinitialized_node.on_chain_balance().unwrap().get_spendable(),
288+
reinitialized_node.onchain_balance().unwrap().get_spendable(),
292289
expected_amount.to_sat()
293290
);
294291

295292
reinitialized_node.sync_wallets().unwrap();
296293
assert_eq!(
297-
reinitialized_node.on_chain_balance().unwrap().get_spendable(),
294+
reinitialized_node.onchain_balance().unwrap().get_spendable(),
298295
expected_amount.to_sat()
299296
);
300297

301298
reinitialized_node.stop().unwrap();
302299
}
300+
301+
#[test]
302+
fn onchain_spend_receive() {
303+
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
304+
let esplora_url = electrsd.esplora_url.as_ref().unwrap();
305+
306+
let config_a = random_config(esplora_url);
307+
let node_a = Builder::from_config(config_a).build();
308+
node_a.start().unwrap();
309+
let addr_a = node_a.new_funding_address().unwrap();
310+
311+
let config_b = random_config(esplora_url);
312+
let node_b = Builder::from_config(config_b).build();
313+
node_b.start().unwrap();
314+
let addr_b = node_b.new_funding_address().unwrap();
315+
316+
premine_and_distribute_funds(
317+
&bitcoind,
318+
&electrsd,
319+
vec![addr_b.clone()],
320+
Amount::from_sat(100000),
321+
);
322+
323+
node_a.sync_wallets().unwrap();
324+
node_b.sync_wallets().unwrap();
325+
assert_eq!(node_b.onchain_balance().unwrap().get_spendable(), 100000);
326+
327+
assert_eq!(Err(Error::InsufficientFunds), node_a.send_onchain_payment(&addr_b, 1000));
328+
329+
let txid = node_b.send_onchain_payment(&addr_a, 1000).unwrap();
330+
generate_blocks_and_wait(&bitcoind, &electrsd, 6);
331+
wait_for_tx(&electrsd, txid);
332+
333+
node_a.sync_wallets().unwrap();
334+
node_b.sync_wallets().unwrap();
335+
336+
assert_eq!(node_a.onchain_balance().unwrap().get_spendable(), 1000);
337+
assert!(node_b.onchain_balance().unwrap().get_spendable() > 98000);
338+
assert!(node_b.onchain_balance().unwrap().get_spendable() < 100000);
339+
}

src/wallet.rs

Lines changed: 56 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,60 @@ where
208208
Ok(self.inner.lock().unwrap().get_balance()?)
209209
}
210210

211+
pub(crate) fn send_to_address(
212+
&self, address: &bitcoin::Address, amount_sats: u64,
213+
) -> Result<Txid, Error> {
214+
let confirmation_target = ConfirmationTarget::Normal;
215+
let fee_rate = self.estimate_fee_rate(confirmation_target);
216+
217+
let tx = {
218+
let locked_wallet = self.inner.lock().unwrap();
219+
let mut tx_builder = locked_wallet.build_tx();
220+
221+
tx_builder
222+
.add_recipient(address.script_pubkey(), amount_sats)
223+
.fee_rate(fee_rate)
224+
.enable_rbf();
225+
226+
let mut psbt = match tx_builder.finish() {
227+
Ok((psbt, _)) => {
228+
log_trace!(self.logger, "Created PSBT: {:?}", psbt);
229+
psbt
230+
}
231+
Err(err) => {
232+
log_error!(self.logger, "Failed to create transaction: {}", err);
233+
return Err(err.into());
234+
}
235+
};
236+
237+
match locked_wallet.sign(&mut psbt, SignOptions::default()) {
238+
Ok(finalized) => {
239+
if !finalized {
240+
return Err(Error::OnchainTxCreationFailed);
241+
}
242+
}
243+
Err(err) => {
244+
log_error!(self.logger, "Failed to create transaction: {}", err);
245+
return Err(err.into());
246+
}
247+
}
248+
psbt.extract_tx()
249+
};
250+
251+
self.broadcast_transaction(&tx);
252+
253+
let txid = tx.txid();
254+
log_info!(
255+
self.logger,
256+
"Created new transaction {} sending {}sats on-chain to address {}",
257+
txid,
258+
amount_sats,
259+
address
260+
);
261+
262+
Ok(txid)
263+
}
264+
211265
fn estimate_fee_rate(&self, confirmation_target: ConfirmationTarget) -> FeeRate {
212266
let locked_fee_rate_cache = self.fee_rate_cache.read().unwrap();
213267

0 commit comments

Comments
 (0)