Skip to content

Commit cca2687

Browse files
committed
Initial implementation of wallet functionality
1 parent b646a57 commit cca2687

File tree

1 file changed

+201
-0
lines changed

1 file changed

+201
-0
lines changed

src/wallet.rs

+201
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
use crate::logger::{
2+
log_error, log_given_level, log_internal, log_trace, FilesystemLogger, Logger,
3+
};
4+
use crate::Error;
5+
6+
use lightning::chain::chaininterface::{
7+
BroadcasterInterface, ConfirmationTarget, FeeEstimator, FEERATE_FLOOR_SATS_PER_KW,
8+
};
9+
10+
use bdk::blockchain::{Blockchain, EsploraBlockchain};
11+
use bdk::database::BatchDatabase;
12+
use bdk::wallet::AddressIndex;
13+
use bdk::{FeeRate, SignOptions, SyncOptions};
14+
15+
use bitcoin::{Script, Transaction};
16+
17+
use std::collections::HashMap;
18+
use std::sync::{Arc, Mutex, RwLock};
19+
20+
pub struct Wallet<D>
21+
where
22+
D: BatchDatabase,
23+
{
24+
// A BDK blockchain used for wallet sync.
25+
blockchain: EsploraBlockchain,
26+
// A BDK on-chain wallet.
27+
wallet: Mutex<bdk::Wallet<D>>,
28+
// A cache storing the most recently retrieved fee rate estimations.
29+
fee_rate_cache: Mutex<HashMap<ConfirmationTarget, FeeRate>>,
30+
tokio_runtime: RwLock<Option<Arc<tokio::runtime::Runtime>>>,
31+
logger: Arc<FilesystemLogger>,
32+
}
33+
34+
impl<D> Wallet<D>
35+
where
36+
D: BatchDatabase,
37+
{
38+
pub(crate) fn new(
39+
blockchain: EsploraBlockchain, wallet: bdk::Wallet<D>, logger: Arc<FilesystemLogger>,
40+
) -> Self {
41+
let wallet = Mutex::new(wallet);
42+
let fee_rate_cache = Mutex::new(HashMap::new());
43+
let tokio_runtime = RwLock::new(None);
44+
Self { blockchain, wallet, fee_rate_cache, tokio_runtime, logger }
45+
}
46+
47+
pub(crate) async fn sync(&self) -> Result<(), Error> {
48+
let sync_options = SyncOptions { progress: None };
49+
match self.wallet.lock().unwrap().sync(&self.blockchain, sync_options).await {
50+
Ok(()) => Ok(()),
51+
Err(e) => {
52+
log_error!(self.logger, "Wallet sync error: {}", e);
53+
Err(e)?
54+
}
55+
}
56+
}
57+
58+
pub(crate) fn set_runtime(&self, tokio_runtime: Arc<tokio::runtime::Runtime>) {
59+
*self.tokio_runtime.write().unwrap() = Some(tokio_runtime);
60+
}
61+
62+
pub(crate) fn drop_runtime(&self) {
63+
*self.tokio_runtime.write().unwrap() = None;
64+
}
65+
66+
pub(crate) fn create_funding_transaction(
67+
&self, output_script: &Script, value_sats: u64, confirmation_target: ConfirmationTarget,
68+
) -> Result<Transaction, Error> {
69+
let fee_rate = self.estimate_fee_rate(confirmation_target);
70+
71+
let locked_wallet = self.wallet.lock().unwrap();
72+
let mut tx_builder = locked_wallet.build_tx();
73+
74+
tx_builder.add_recipient(output_script.clone(), value_sats).fee_rate(fee_rate).enable_rbf();
75+
76+
let mut psbt = match tx_builder.finish() {
77+
Ok((psbt, _)) => {
78+
log_trace!(self.logger, "Created funding PSBT: {:?}", psbt);
79+
psbt
80+
}
81+
Err(err) => {
82+
log_error!(self.logger, "Failed to create funding transaction: {}", err);
83+
Err(err)?
84+
}
85+
};
86+
87+
// We double-check that no inputs try to spend non-witness outputs. As we use a SegWit
88+
// wallet descriptor this technically shouldn't ever happen, but better safe than sorry.
89+
for input in &psbt.inputs {
90+
if input.witness_utxo.is_none() {
91+
log_error!(self.logger, "Tried to spend a non-witness funding output. This must not ever happen. Panicking!");
92+
panic!("Tried to spend a non-witness funding output. This must not ever happen.");
93+
}
94+
}
95+
96+
if !locked_wallet.sign(&mut psbt, SignOptions::default())? {
97+
return Err(Error::FundingTxCreationFailed);
98+
}
99+
100+
Ok(psbt.extract_tx())
101+
}
102+
103+
pub(crate) fn get_new_address(&self) -> Result<bitcoin::Address, Error> {
104+
let address_info = self.wallet.lock().unwrap().get_address(AddressIndex::New)?;
105+
Ok(address_info.address)
106+
}
107+
108+
fn estimate_fee_rate(&self, confirmation_target: ConfirmationTarget) -> FeeRate {
109+
let mut locked_fee_rate_cache = self.fee_rate_cache.lock().unwrap();
110+
let num_blocks = num_blocks_from_conf_target(confirmation_target);
111+
112+
// We'll fall back on this, if we really don't have any other information.
113+
let fallback_rate = fallback_fee_from_conf_target(confirmation_target);
114+
115+
let locked_runtime = self.tokio_runtime.read().unwrap();
116+
if locked_runtime.as_ref().is_none() {
117+
log_error!(self.logger, "Failed to update fee rate estimation: No runtime.");
118+
return *locked_fee_rate_cache.get(&confirmation_target).unwrap_or(&fallback_rate);
119+
}
120+
121+
let est_fee_rate = tokio::task::block_in_place(move || {
122+
locked_runtime
123+
.as_ref()
124+
.unwrap()
125+
.handle()
126+
.block_on(async move { self.blockchain.estimate_fee(num_blocks).await })
127+
});
128+
129+
match est_fee_rate {
130+
Ok(rate) => {
131+
locked_fee_rate_cache.insert(confirmation_target, rate);
132+
log_trace!(
133+
self.logger,
134+
"Fee rate estimation updated: {} sats/kwu",
135+
rate.fee_wu(1000)
136+
);
137+
rate
138+
}
139+
Err(e) => {
140+
log_error!(self.logger, "Failed to update fee rate estimation: {}", e);
141+
*locked_fee_rate_cache.get(&confirmation_target).unwrap_or(&fallback_rate)
142+
}
143+
}
144+
}
145+
}
146+
147+
impl<D> FeeEstimator for Wallet<D>
148+
where
149+
D: BatchDatabase,
150+
{
151+
fn get_est_sat_per_1000_weight(&self, confirmation_target: ConfirmationTarget) -> u32 {
152+
(self.estimate_fee_rate(confirmation_target).fee_wu(1000) as u32)
153+
.max(FEERATE_FLOOR_SATS_PER_KW)
154+
}
155+
}
156+
157+
impl<D> BroadcasterInterface for Wallet<D>
158+
where
159+
D: BatchDatabase,
160+
{
161+
fn broadcast_transaction(&self, tx: &Transaction) {
162+
let locked_runtime = self.tokio_runtime.read().unwrap();
163+
if locked_runtime.as_ref().is_none() {
164+
log_error!(self.logger, "Failed to broadcast transaction: No runtime.");
165+
panic!("Failed to broadcast transaction: No runtime.");
166+
}
167+
168+
let res = tokio::task::block_in_place(move || {
169+
locked_runtime
170+
.as_ref()
171+
.unwrap()
172+
.block_on(async move { self.blockchain.broadcast(tx).await })
173+
});
174+
175+
match res {
176+
Ok(_) => {}
177+
Err(err) => {
178+
log_error!(self.logger, "Failed to broadcast transaction: {}", err);
179+
panic!("Failed to broadcast transaction: {}", err);
180+
}
181+
}
182+
}
183+
}
184+
185+
fn num_blocks_from_conf_target(confirmation_target: ConfirmationTarget) -> usize {
186+
match confirmation_target {
187+
ConfirmationTarget::Background => 12,
188+
ConfirmationTarget::Normal => 6,
189+
ConfirmationTarget::HighPriority => 3,
190+
}
191+
}
192+
193+
fn fallback_fee_from_conf_target(confirmation_target: ConfirmationTarget) -> FeeRate {
194+
let sats_kwu = match confirmation_target {
195+
ConfirmationTarget::Background => FEERATE_FLOOR_SATS_PER_KW,
196+
ConfirmationTarget::Normal => 2000,
197+
ConfirmationTarget::HighPriority => 5000,
198+
};
199+
200+
FeeRate::from_sat_per_kwu(sats_kwu as f32)
201+
}

0 commit comments

Comments
 (0)