Skip to content

Commit 167583e

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 40f82b8 commit 167583e

File tree

4 files changed

+131
-20
lines changed

4 files changed

+131
-20
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

@@ -1213,6 +1213,23 @@ impl Node {
12131213
}
12141214
}
12151215

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

src/test/functional_tests.rs

Lines changed: 52 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ fn channel_full_cycle() {
2929
);
3030
node_a.sync_wallets().unwrap();
3131
node_b.sync_wallets().unwrap();
32-
assert_eq!(node_a.on_chain_balance().unwrap().get_spendable(), 100000);
33-
assert_eq!(node_b.on_chain_balance().unwrap().get_spendable(), 100000);
32+
assert_eq!(node_a.onchain_balance().unwrap().get_spendable(), 100000);
33+
assert_eq!(node_b.onchain_balance().unwrap().get_spendable(), 100000);
3434

3535
println!("\nA -- connect_open_channel -> B");
3636
let node_b_addr = format!("{}@{}", node_b.node_id(), node_b.listening_address().unwrap());
@@ -53,10 +53,10 @@ fn channel_full_cycle() {
5353
node_a.sync_wallets().unwrap();
5454
node_b.sync_wallets().unwrap();
5555

56-
let node_a_balance = node_a.on_chain_balance().unwrap();
56+
let node_a_balance = node_a.onchain_balance().unwrap();
5757
assert!(node_a_balance.get_spendable() < 50000);
5858
assert!(node_a_balance.get_spendable() > 40000);
59-
assert_eq!(node_b.on_chain_balance().unwrap().get_spendable(), 100000);
59+
assert_eq!(node_b.onchain_balance().unwrap().get_spendable(), 100000);
6060

6161
expect_event!(node_a, ChannelReady);
6262

@@ -174,8 +174,8 @@ fn channel_full_cycle() {
174174
node_a.sync_wallets().unwrap();
175175
node_b.sync_wallets().unwrap();
176176

177-
assert!(node_a.on_chain_balance().unwrap().get_spendable() > 90000);
178-
assert_eq!(node_b.on_chain_balance().unwrap().get_spendable(), 103234);
177+
assert!(node_a.onchain_balance().unwrap().get_spendable() > 90000);
178+
assert_eq!(node_b.onchain_balance().unwrap().get_spendable(), 103234);
179179

180180
node_a.stop().unwrap();
181181
println!("\nA stopped");
@@ -207,8 +207,8 @@ fn channel_open_fails_when_funds_insufficient() {
207207
);
208208
node_a.sync_wallets().unwrap();
209209
node_b.sync_wallets().unwrap();
210-
assert_eq!(node_a.on_chain_balance().unwrap().get_spendable(), 100000);
211-
assert_eq!(node_b.on_chain_balance().unwrap().get_spendable(), 100000);
210+
assert_eq!(node_a.onchain_balance().unwrap().get_spendable(), 100000);
211+
assert_eq!(node_b.onchain_balance().unwrap().get_spendable(), 100000);
212212

213213
println!("\nA -- connect_open_channel -> B");
214214
let node_b_addr = format!("{}@{}", node_b.node_id(), node_b.listening_address().unwrap());
@@ -243,13 +243,13 @@ fn start_stop_reinit() {
243243
let expected_amount = Amount::from_sat(100000);
244244

245245
premine_and_distribute_funds(&bitcoind, &electrsd, vec![funding_address], expected_amount);
246-
assert_eq!(node.on_chain_balance().unwrap().get_total(), 0);
246+
assert_eq!(node.onchain_balance().unwrap().get_total(), 0);
247247

248248
node.start().unwrap();
249249
assert_eq!(node.start(), Err(Error::AlreadyRunning));
250250

251251
node.sync_wallets().unwrap();
252-
assert_eq!(node.on_chain_balance().unwrap().get_spendable(), expected_amount.to_sat());
252+
assert_eq!(node.onchain_balance().unwrap().get_spendable(), expected_amount.to_sat());
253253

254254
node.stop().unwrap();
255255
assert_eq!(node.stop(), Err(Error::NotRunning));
@@ -267,15 +267,55 @@ fn start_stop_reinit() {
267267
reinitialized_node.start().unwrap();
268268

269269
assert_eq!(
270-
reinitialized_node.on_chain_balance().unwrap().get_spendable(),
270+
reinitialized_node.onchain_balance().unwrap().get_spendable(),
271271
expected_amount.to_sat()
272272
);
273273

274274
reinitialized_node.sync_wallets().unwrap();
275275
assert_eq!(
276-
reinitialized_node.on_chain_balance().unwrap().get_spendable(),
276+
reinitialized_node.onchain_balance().unwrap().get_spendable(),
277277
expected_amount.to_sat()
278278
);
279279

280280
reinitialized_node.stop().unwrap();
281281
}
282+
283+
#[test]
284+
fn onchain_spend_receive() {
285+
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
286+
let esplora_url = electrsd.esplora_url.as_ref().unwrap();
287+
288+
let config_a = random_config(esplora_url);
289+
let node_a = Builder::from_config(config_a).build();
290+
node_a.start().unwrap();
291+
let addr_a = node_a.new_funding_address().unwrap();
292+
293+
let config_b = random_config(esplora_url);
294+
let node_b = Builder::from_config(config_b).build();
295+
node_b.start().unwrap();
296+
let addr_b = node_b.new_funding_address().unwrap();
297+
298+
premine_and_distribute_funds(
299+
&bitcoind,
300+
&electrsd,
301+
vec![addr_b.clone()],
302+
Amount::from_sat(100000),
303+
);
304+
305+
node_a.sync_wallets().unwrap();
306+
node_b.sync_wallets().unwrap();
307+
assert_eq!(node_b.onchain_balance().unwrap().get_spendable(), 100000);
308+
309+
assert_eq!(Err(Error::InsufficientFunds), node_a.send_onchain_payment(&addr_b, 1000));
310+
311+
let txid = node_b.send_onchain_payment(&addr_a, 1000).unwrap();
312+
generate_blocks_and_wait(&bitcoind, &electrsd, 6);
313+
wait_for_tx(&electrsd, txid);
314+
315+
node_a.sync_wallets().unwrap();
316+
node_b.sync_wallets().unwrap();
317+
318+
assert_eq!(node_a.onchain_balance().unwrap().get_spendable(), 1000);
319+
assert!(node_b.onchain_balance().unwrap().get_spendable() > 98000);
320+
assert!(node_b.onchain_balance().unwrap().get_spendable() < 100000);
321+
}

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)