Skip to content

Commit 43f1d2d

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

File tree

1 file changed

+202
-0
lines changed

1 file changed

+202
-0
lines changed

src/wallet.rs

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
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!(
102+
self.logger,
103+
"Failed to update fee rate estimation for {:?}: {}",
104+
target,
105+
e
106+
);
107+
}
108+
}
109+
}
110+
Ok(())
111+
}
112+
113+
pub(crate) fn create_funding_transaction(
114+
&self, output_script: Script, value_sats: u64, confirmation_target: ConfirmationTarget,
115+
) -> Result<Transaction, Error> {
116+
let fee_rate = self.estimate_fee_rate(confirmation_target);
117+
118+
let locked_wallet = self.inner.lock().unwrap();
119+
let mut tx_builder = locked_wallet.build_tx();
120+
121+
tx_builder.add_recipient(output_script, value_sats).fee_rate(fee_rate).enable_rbf();
122+
123+
let mut psbt = match tx_builder.finish() {
124+
Ok((psbt, _)) => {
125+
log_trace!(self.logger, "Created funding PSBT: {:?}", psbt);
126+
psbt
127+
}
128+
Err(err) => {
129+
log_error!(self.logger, "Failed to create funding transaction: {}", err);
130+
Err(err)?
131+
}
132+
};
133+
134+
if !locked_wallet.sign(&mut psbt, SignOptions::default())? {
135+
return Err(Error::FundingTxCreationFailed);
136+
}
137+
138+
Ok(psbt.extract_tx())
139+
}
140+
141+
pub(crate) fn get_new_address(&self) -> Result<bitcoin::Address, Error> {
142+
let address_info = self.inner.lock().unwrap().get_address(AddressIndex::New)?;
143+
Ok(address_info.address)
144+
}
145+
146+
#[cfg(any(test))]
147+
pub(crate) fn get_balance(&self) -> Result<bdk::Balance, Error> {
148+
Ok(self.inner.lock().unwrap().get_balance()?)
149+
}
150+
151+
fn estimate_fee_rate(&self, confirmation_target: ConfirmationTarget) -> FeeRate {
152+
let locked_fee_rate_cache = self.fee_rate_cache.read().unwrap();
153+
154+
let fallback_sats_kwu = match confirmation_target {
155+
ConfirmationTarget::Background => FEERATE_FLOOR_SATS_PER_KW,
156+
ConfirmationTarget::Normal => 2000,
157+
ConfirmationTarget::HighPriority => 5000,
158+
};
159+
160+
// We'll fall back on this, if we really don't have any other information.
161+
let fallback_rate = FeeRate::from_sat_per_kwu(fallback_sats_kwu as f32);
162+
163+
*locked_fee_rate_cache.get(&confirmation_target).unwrap_or(&fallback_rate)
164+
}
165+
}
166+
167+
impl<D> FeeEstimator for Wallet<D>
168+
where
169+
D: BatchDatabase,
170+
{
171+
fn get_est_sat_per_1000_weight(&self, confirmation_target: ConfirmationTarget) -> u32 {
172+
(self.estimate_fee_rate(confirmation_target).fee_wu(1000) as u32)
173+
.max(FEERATE_FLOOR_SATS_PER_KW)
174+
}
175+
}
176+
177+
impl<D> BroadcasterInterface for Wallet<D>
178+
where
179+
D: BatchDatabase,
180+
{
181+
fn broadcast_transaction(&self, tx: &Transaction) {
182+
let locked_runtime = self.tokio_runtime.read().unwrap();
183+
if locked_runtime.as_ref().is_none() {
184+
log_error!(self.logger, "Failed to broadcast transaction: No runtime.");
185+
unreachable!("Failed to broadcast transaction: No runtime.");
186+
}
187+
188+
let res = tokio::task::block_in_place(move || {
189+
locked_runtime
190+
.as_ref()
191+
.unwrap()
192+
.block_on(async move { self.blockchain.broadcast(tx).await })
193+
});
194+
195+
match res {
196+
Ok(_) => {}
197+
Err(err) => {
198+
log_error!(self.logger, "Failed to broadcast transaction: {}", err);
199+
}
200+
}
201+
}
202+
}

0 commit comments

Comments
 (0)