Skip to content

Commit e61cba6

Browse files
committed
Initial implementation of wallet functionality
1 parent 053a5ed commit e61cba6

File tree

1 file changed

+197
-0
lines changed

1 file changed

+197
-0
lines changed

src/wallet.rs

+197
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
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+
inner: Mutex<bdk::Wallet<D>>,
28+
// A cache storing the most recently retrieved fee rate estimations.
29+
fee_rate_cache: RwLock<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 inner = Mutex::new(wallet);
42+
let fee_rate_cache = RwLock::new(HashMap::new());
43+
let tokio_runtime = RwLock::new(None);
44+
Self { blockchain, inner, fee_rate_cache, tokio_runtime, logger }
45+
}
46+
47+
pub(crate) async fn sync(&self) -> Result<(), Error> {
48+
match self.update_fee_estimates().await {
49+
Ok(()) => (),
50+
Err(e) => {
51+
log_error!(self.logger, "Fee estimation error: {}", e);
52+
return Err(e);
53+
}
54+
}
55+
56+
let sync_options = SyncOptions { progress: None };
57+
match self.inner.lock().unwrap().sync(&self.blockchain, sync_options).await {
58+
Ok(()) => Ok(()),
59+
Err(e) => {
60+
log_error!(self.logger, "Wallet sync error: {}", e);
61+
Err(From::from(e))
62+
}
63+
}
64+
}
65+
66+
pub(crate) fn set_runtime(&self, tokio_runtime: Arc<tokio::runtime::Runtime>) {
67+
*self.tokio_runtime.write().unwrap() = Some(tokio_runtime);
68+
}
69+
70+
pub(crate) fn drop_runtime(&self) {
71+
*self.tokio_runtime.write().unwrap() = None;
72+
}
73+
74+
pub(crate) async fn update_fee_estimates(&self) -> Result<(), Error> {
75+
let mut locked_fee_rate_cache = self.fee_rate_cache.write().unwrap();
76+
77+
let confirmation_targets = vec![
78+
ConfirmationTarget::Background,
79+
ConfirmationTarget::Normal,
80+
ConfirmationTarget::HighPriority,
81+
];
82+
for target in confirmation_targets {
83+
let num_blocks = match target {
84+
ConfirmationTarget::Background => 12,
85+
ConfirmationTarget::Normal => 6,
86+
ConfirmationTarget::HighPriority => 3,
87+
};
88+
89+
let est_fee_rate = self.blockchain.estimate_fee(num_blocks).await;
90+
91+
match est_fee_rate {
92+
Ok(rate) => {
93+
locked_fee_rate_cache.insert(target, rate);
94+
log_trace!(
95+
self.logger,
96+
"Fee rate estimation updated: {} sats/kwu",
97+
rate.fee_wu(1000)
98+
);
99+
}
100+
Err(e) => {
101+
log_error!(self.logger, "Failed to update fee rate estimation: {}", e);
102+
}
103+
}
104+
}
105+
Ok(())
106+
}
107+
108+
pub(crate) fn create_funding_transaction(
109+
&self, output_script: Script, value_sats: u64, confirmation_target: ConfirmationTarget,
110+
) -> Result<Transaction, Error> {
111+
let fee_rate = self.estimate_fee_rate(confirmation_target);
112+
113+
let locked_wallet = self.inner.lock().unwrap();
114+
let mut tx_builder = locked_wallet.build_tx();
115+
116+
tx_builder.add_recipient(output_script, value_sats).fee_rate(fee_rate).enable_rbf();
117+
118+
let mut psbt = match tx_builder.finish() {
119+
Ok((psbt, _)) => {
120+
log_trace!(self.logger, "Created funding PSBT: {:?}", psbt);
121+
psbt
122+
}
123+
Err(err) => {
124+
log_error!(self.logger, "Failed to create funding transaction: {}", err);
125+
Err(err)?
126+
}
127+
};
128+
129+
if !locked_wallet.sign(&mut psbt, SignOptions::default())? {
130+
return Err(Error::FundingTxCreationFailed);
131+
}
132+
133+
Ok(psbt.extract_tx())
134+
}
135+
136+
pub(crate) fn get_new_address(&self) -> Result<bitcoin::Address, Error> {
137+
let address_info = self.inner.lock().unwrap().get_address(AddressIndex::New)?;
138+
Ok(address_info.address)
139+
}
140+
141+
#[cfg(any(test))]
142+
pub(crate) fn get_balance(&self) -> Result<bdk::Balance, Error> {
143+
Ok(self.inner.lock().unwrap().get_balance()?)
144+
}
145+
146+
fn estimate_fee_rate(&self, confirmation_target: ConfirmationTarget) -> FeeRate {
147+
let locked_fee_rate_cache = self.fee_rate_cache.read().unwrap();
148+
149+
let fallback_sats_kwu = match confirmation_target {
150+
ConfirmationTarget::Background => FEERATE_FLOOR_SATS_PER_KW,
151+
ConfirmationTarget::Normal => 2000,
152+
ConfirmationTarget::HighPriority => 5000,
153+
};
154+
155+
// We'll fall back on this, if we really don't have any other information.
156+
let fallback_rate = FeeRate::from_sat_per_kwu(fallback_sats_kwu as f32);
157+
158+
*locked_fee_rate_cache.get(&confirmation_target).unwrap_or(&fallback_rate)
159+
}
160+
}
161+
162+
impl<D> FeeEstimator for Wallet<D>
163+
where
164+
D: BatchDatabase,
165+
{
166+
fn get_est_sat_per_1000_weight(&self, confirmation_target: ConfirmationTarget) -> u32 {
167+
(self.estimate_fee_rate(confirmation_target).fee_wu(1000) as u32)
168+
.max(FEERATE_FLOOR_SATS_PER_KW)
169+
}
170+
}
171+
172+
impl<D> BroadcasterInterface for Wallet<D>
173+
where
174+
D: BatchDatabase,
175+
{
176+
fn broadcast_transaction(&self, tx: &Transaction) {
177+
let locked_runtime = self.tokio_runtime.read().unwrap();
178+
if locked_runtime.as_ref().is_none() {
179+
log_error!(self.logger, "Failed to broadcast transaction: No runtime.");
180+
unreachable!("Failed to broadcast transaction: No runtime.");
181+
}
182+
183+
let res = tokio::task::block_in_place(move || {
184+
locked_runtime
185+
.as_ref()
186+
.unwrap()
187+
.block_on(async move { self.blockchain.broadcast(tx).await })
188+
});
189+
190+
match res {
191+
Ok(_) => {}
192+
Err(err) => {
193+
log_error!(self.logger, "Failed to broadcast transaction: {}", err);
194+
}
195+
}
196+
}
197+
}

0 commit comments

Comments
 (0)