Skip to content

Commit 1651ed9

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

File tree

1 file changed

+205
-0
lines changed

1 file changed

+205
-0
lines changed

src/wallet.rs

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

0 commit comments

Comments
 (0)