Skip to content

Commit adfdc24

Browse files
authored
Merge pull request #33 from tnull/2022-11-initial-wallet
2 parents 053a5ed + 97716a4 commit adfdc24

File tree

1 file changed

+339
-0
lines changed

1 file changed

+339
-0
lines changed

src/wallet.rs

+339
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,339 @@
1+
use crate::logger::{
2+
log_error, log_given_level, log_internal, log_trace, FilesystemLogger, Logger,
3+
};
4+
5+
use crate::Error;
6+
7+
use lightning::chain::chaininterface::{
8+
BroadcasterInterface, ConfirmationTarget, FeeEstimator, FEERATE_FLOOR_SATS_PER_KW,
9+
};
10+
11+
use lightning::chain::keysinterface::{
12+
EntropySource, InMemorySigner, KeyMaterial, KeysManager, NodeSigner, Recipient, SignerProvider,
13+
SpendableOutputDescriptor,
14+
};
15+
use lightning::ln::msgs::DecodeError;
16+
use lightning::ln::script::ShutdownScript;
17+
18+
use bdk::blockchain::{Blockchain, EsploraBlockchain};
19+
use bdk::database::BatchDatabase;
20+
use bdk::wallet::AddressIndex;
21+
use bdk::{FeeRate, SignOptions, SyncOptions};
22+
23+
use bitcoin::bech32::u5;
24+
use bitcoin::secp256k1::ecdh::SharedSecret;
25+
use bitcoin::secp256k1::ecdsa::RecoverableSignature;
26+
use bitcoin::secp256k1::{PublicKey, Scalar, Secp256k1, SecretKey, Signing};
27+
use bitcoin::{Script, Transaction, TxOut};
28+
29+
use std::collections::HashMap;
30+
use std::sync::{Arc, Mutex, RwLock};
31+
32+
pub struct Wallet<D>
33+
where
34+
D: BatchDatabase,
35+
{
36+
// A BDK blockchain used for wallet sync.
37+
blockchain: EsploraBlockchain,
38+
// A BDK on-chain wallet.
39+
inner: Mutex<bdk::Wallet<D>>,
40+
// A cache storing the most recently retrieved fee rate estimations.
41+
fee_rate_cache: RwLock<HashMap<ConfirmationTarget, FeeRate>>,
42+
tokio_runtime: RwLock<Option<Arc<tokio::runtime::Runtime>>>,
43+
logger: Arc<FilesystemLogger>,
44+
}
45+
46+
impl<D> Wallet<D>
47+
where
48+
D: BatchDatabase,
49+
{
50+
pub(crate) fn new(
51+
blockchain: EsploraBlockchain, wallet: bdk::Wallet<D>, logger: Arc<FilesystemLogger>,
52+
) -> Self {
53+
let inner = Mutex::new(wallet);
54+
let fee_rate_cache = RwLock::new(HashMap::new());
55+
let tokio_runtime = RwLock::new(None);
56+
Self { blockchain, inner, fee_rate_cache, tokio_runtime, logger }
57+
}
58+
59+
pub(crate) async fn sync(&self) -> Result<(), Error> {
60+
match self.update_fee_estimates().await {
61+
Ok(()) => (),
62+
Err(e) => {
63+
log_error!(self.logger, "Fee estimation error: {}", e);
64+
return Err(e);
65+
}
66+
}
67+
68+
let sync_options = SyncOptions { progress: None };
69+
match self.inner.lock().unwrap().sync(&self.blockchain, sync_options).await {
70+
Ok(()) => Ok(()),
71+
Err(e) => {
72+
log_error!(self.logger, "Wallet sync error: {}", e);
73+
Err(From::from(e))
74+
}
75+
}
76+
}
77+
78+
pub(crate) fn set_runtime(&self, tokio_runtime: Arc<tokio::runtime::Runtime>) {
79+
*self.tokio_runtime.write().unwrap() = Some(tokio_runtime);
80+
}
81+
82+
pub(crate) fn drop_runtime(&self) {
83+
*self.tokio_runtime.write().unwrap() = None;
84+
}
85+
86+
pub(crate) async fn update_fee_estimates(&self) -> Result<(), Error> {
87+
let mut locked_fee_rate_cache = self.fee_rate_cache.write().unwrap();
88+
89+
let confirmation_targets = vec![
90+
ConfirmationTarget::Background,
91+
ConfirmationTarget::Normal,
92+
ConfirmationTarget::HighPriority,
93+
];
94+
for target in confirmation_targets {
95+
let num_blocks = match target {
96+
ConfirmationTarget::Background => 12,
97+
ConfirmationTarget::Normal => 6,
98+
ConfirmationTarget::HighPriority => 3,
99+
};
100+
101+
let est_fee_rate = self.blockchain.estimate_fee(num_blocks).await;
102+
103+
match est_fee_rate {
104+
Ok(rate) => {
105+
locked_fee_rate_cache.insert(target, rate);
106+
log_trace!(
107+
self.logger,
108+
"Fee rate estimation updated: {} sats/kwu",
109+
rate.fee_wu(1000)
110+
);
111+
}
112+
Err(e) => {
113+
log_error!(
114+
self.logger,
115+
"Failed to update fee rate estimation for {:?}: {}",
116+
target,
117+
e
118+
);
119+
}
120+
}
121+
}
122+
Ok(())
123+
}
124+
125+
pub(crate) fn create_funding_transaction(
126+
&self, output_script: Script, value_sats: u64, confirmation_target: ConfirmationTarget,
127+
) -> Result<Transaction, Error> {
128+
let fee_rate = self.estimate_fee_rate(confirmation_target);
129+
130+
let locked_wallet = self.inner.lock().unwrap();
131+
let mut tx_builder = locked_wallet.build_tx();
132+
133+
tx_builder.add_recipient(output_script, value_sats).fee_rate(fee_rate).enable_rbf();
134+
135+
let mut psbt = match tx_builder.finish() {
136+
Ok((psbt, _)) => {
137+
log_trace!(self.logger, "Created funding PSBT: {:?}", psbt);
138+
psbt
139+
}
140+
Err(err) => {
141+
log_error!(self.logger, "Failed to create funding transaction: {}", err);
142+
Err(err)?
143+
}
144+
};
145+
146+
if !locked_wallet.sign(&mut psbt, SignOptions::default())? {
147+
return Err(Error::FundingTxCreationFailed);
148+
}
149+
150+
Ok(psbt.extract_tx())
151+
}
152+
153+
pub(crate) fn get_new_address(&self) -> Result<bitcoin::Address, Error> {
154+
let address_info = self.inner.lock().unwrap().get_address(AddressIndex::New)?;
155+
Ok(address_info.address)
156+
}
157+
158+
#[cfg(any(test))]
159+
pub(crate) fn get_balance(&self) -> Result<bdk::Balance, Error> {
160+
Ok(self.inner.lock().unwrap().get_balance()?)
161+
}
162+
163+
fn estimate_fee_rate(&self, confirmation_target: ConfirmationTarget) -> FeeRate {
164+
let locked_fee_rate_cache = self.fee_rate_cache.read().unwrap();
165+
166+
let fallback_sats_kwu = match confirmation_target {
167+
ConfirmationTarget::Background => FEERATE_FLOOR_SATS_PER_KW,
168+
ConfirmationTarget::Normal => 2000,
169+
ConfirmationTarget::HighPriority => 5000,
170+
};
171+
172+
// We'll fall back on this, if we really don't have any other information.
173+
let fallback_rate = FeeRate::from_sat_per_kwu(fallback_sats_kwu as f32);
174+
175+
*locked_fee_rate_cache.get(&confirmation_target).unwrap_or(&fallback_rate)
176+
}
177+
}
178+
179+
impl<D> FeeEstimator for Wallet<D>
180+
where
181+
D: BatchDatabase,
182+
{
183+
fn get_est_sat_per_1000_weight(&self, confirmation_target: ConfirmationTarget) -> u32 {
184+
(self.estimate_fee_rate(confirmation_target).fee_wu(1000) as u32)
185+
.max(FEERATE_FLOOR_SATS_PER_KW)
186+
}
187+
}
188+
189+
impl<D> BroadcasterInterface for Wallet<D>
190+
where
191+
D: BatchDatabase,
192+
{
193+
fn broadcast_transaction(&self, tx: &Transaction) {
194+
let locked_runtime = self.tokio_runtime.read().unwrap();
195+
if locked_runtime.as_ref().is_none() {
196+
log_error!(self.logger, "Failed to broadcast transaction: No runtime.");
197+
unreachable!("Failed to broadcast transaction: No runtime.");
198+
}
199+
200+
let res = tokio::task::block_in_place(move || {
201+
locked_runtime
202+
.as_ref()
203+
.unwrap()
204+
.block_on(async move { self.blockchain.broadcast(tx).await })
205+
});
206+
207+
match res {
208+
Ok(_) => {}
209+
Err(err) => {
210+
log_error!(self.logger, "Failed to broadcast transaction: {}", err);
211+
}
212+
}
213+
}
214+
}
215+
216+
/// Similar to [`KeysManager`], but overrides the destination and shutdown scripts so they are
217+
/// directly spendable by the BDK wallet.
218+
pub struct WalletKeysManager<D>
219+
where
220+
D: BatchDatabase,
221+
{
222+
inner: KeysManager,
223+
wallet: Arc<Wallet<D>>,
224+
}
225+
226+
impl<D> WalletKeysManager<D>
227+
where
228+
D: BatchDatabase,
229+
{
230+
/// Constructs a `WalletKeysManager` that overrides the destination and shutdown scripts.
231+
///
232+
/// See [`KeysManager::new`] for more information on `seed`, `starting_time_secs`, and
233+
/// `starting_time_nanos`.
234+
pub fn new(
235+
seed: &[u8; 32], starting_time_secs: u64, starting_time_nanos: u32, wallet: Arc<Wallet<D>>,
236+
) -> Self {
237+
let inner = KeysManager::new(seed, starting_time_secs, starting_time_nanos);
238+
Self { inner, wallet }
239+
}
240+
241+
/// See [`KeysManager::spend_spendable_outputs`] for documentation on this method.
242+
pub fn spend_spendable_outputs<C: Signing>(
243+
&self, descriptors: &[&SpendableOutputDescriptor], outputs: Vec<TxOut>,
244+
change_destination_script: Script, feerate_sat_per_1000_weight: u32,
245+
secp_ctx: &Secp256k1<C>,
246+
) -> Result<Transaction, ()> {
247+
let only_non_static = &descriptors
248+
.iter()
249+
.filter(|desc| !matches!(desc, SpendableOutputDescriptor::StaticOutput { .. }))
250+
.copied()
251+
.collect::<Vec<_>>();
252+
self.inner.spend_spendable_outputs(
253+
only_non_static,
254+
outputs,
255+
change_destination_script,
256+
feerate_sat_per_1000_weight,
257+
secp_ctx,
258+
)
259+
}
260+
}
261+
262+
impl<D> NodeSigner for WalletKeysManager<D>
263+
where
264+
D: BatchDatabase,
265+
{
266+
fn get_node_secret(&self, recipient: Recipient) -> Result<SecretKey, ()> {
267+
self.inner.get_node_secret(recipient)
268+
}
269+
270+
fn get_node_id(&self, recipient: Recipient) -> Result<PublicKey, ()> {
271+
self.inner.get_node_id(recipient)
272+
}
273+
274+
fn ecdh(
275+
&self, recipient: Recipient, other_key: &PublicKey, tweak: Option<&Scalar>,
276+
) -> Result<SharedSecret, ()> {
277+
self.inner.ecdh(recipient, other_key, tweak)
278+
}
279+
280+
fn get_inbound_payment_key_material(&self) -> KeyMaterial {
281+
self.inner.get_inbound_payment_key_material()
282+
}
283+
284+
fn sign_invoice(
285+
&self, hrp_bytes: &[u8], invoice_data: &[u5], recipient: Recipient,
286+
) -> Result<RecoverableSignature, ()> {
287+
self.inner.sign_invoice(hrp_bytes, invoice_data, recipient)
288+
}
289+
}
290+
291+
impl<D> EntropySource for WalletKeysManager<D>
292+
where
293+
D: BatchDatabase,
294+
{
295+
fn get_secure_random_bytes(&self) -> [u8; 32] {
296+
self.inner.get_secure_random_bytes()
297+
}
298+
}
299+
300+
impl<D> SignerProvider for WalletKeysManager<D>
301+
where
302+
D: BatchDatabase,
303+
{
304+
type Signer = InMemorySigner;
305+
306+
fn generate_channel_keys_id(
307+
&self, inbound: bool, channel_value_satoshis: u64, user_channel_id: u128,
308+
) -> [u8; 32] {
309+
self.inner.generate_channel_keys_id(inbound, channel_value_satoshis, user_channel_id)
310+
}
311+
312+
fn derive_channel_signer(
313+
&self, channel_value_satoshis: u64, channel_keys_id: [u8; 32],
314+
) -> Self::Signer {
315+
self.inner.derive_channel_signer(channel_value_satoshis, channel_keys_id)
316+
}
317+
318+
fn read_chan_signer(&self, reader: &[u8]) -> Result<Self::Signer, DecodeError> {
319+
self.inner.read_chan_signer(reader)
320+
}
321+
322+
fn get_destination_script(&self) -> Script {
323+
let address =
324+
self.wallet.get_new_address().expect("Failed to retrieve new address from wallet.");
325+
address.script_pubkey()
326+
}
327+
328+
fn get_shutdown_scriptpubkey(&self) -> ShutdownScript {
329+
let address =
330+
self.wallet.get_new_address().expect("Failed to retrieve new address from wallet.");
331+
match address.payload {
332+
bitcoin::util::address::Payload::WitnessProgram { version, program } => {
333+
return ShutdownScript::new_witness_program(version, &program)
334+
.expect("Invalid shutdown script.");
335+
}
336+
_ => panic!("Tried to use a non-witness address. This must not ever happen."),
337+
}
338+
}
339+
}

0 commit comments

Comments
 (0)