1pub mod persister;
2
3use std::collections::HashMap;
4use std::io::Write;
5use std::str::FromStr;
6use std::sync::Arc;
7
8use anyhow::{anyhow, bail, Result};
9use boltz_client::ElementsAddress;
10use log::{debug, error, info, warn};
11use lwk_common::Signer as LwkSigner;
12use lwk_common::{singlesig_desc, Singlesig};
13use lwk_wollet::asyncr::{EsploraClient, EsploraClientBuilder};
14use lwk_wollet::elements::hex::ToHex;
15use lwk_wollet::elements::pset::PartiallySignedTransaction;
16use lwk_wollet::elements::{Address, AssetId, OutPoint, Transaction, TxOut, Txid};
17use lwk_wollet::secp256k1::Message;
18use lwk_wollet::{ElementsNetwork, WalletTx, WalletTxOut, Wollet, WolletDescriptor};
19use persister::SqliteWalletCachePersister;
20use sdk_common::bitcoin::hashes::{sha256, Hash};
21use sdk_common::bitcoin::secp256k1::PublicKey;
22use sdk_common::lightning::util::message_signing::verify;
23use tokio::sync::Mutex;
24use web_time::Instant;
25
26use crate::model::{BlockchainExplorer, Signer, BREEZ_LIQUID_ESPLORA_URL};
27use crate::persist::Persister;
28use crate::signer::SdkLwkSigner;
29use crate::{ensure_sdk, error::PaymentError, model::Config};
30
31use crate::wallet::persister::WalletCachePersister;
32#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
33use lwk_wollet::blocking::BlockchainBackend;
34
35static LN_MESSAGE_PREFIX: &[u8] = b"Lightning Signed Message:";
36
37#[sdk_macros::async_trait]
38pub trait OnchainWallet: Send + Sync {
39 async fn transactions(&self) -> Result<Vec<WalletTx>, PaymentError>;
41
42 async fn transactions_by_tx_id(&self) -> Result<HashMap<Txid, WalletTx>, PaymentError>;
44
45 async fn asset_utxos(&self, asset: &AssetId) -> Result<Vec<WalletTxOut>, PaymentError>;
47
48 async fn build_tx(
50 &self,
51 fee_rate_sats_per_kvb: Option<f32>,
52 recipient_address: &str,
53 asset_id: &str,
54 amount_sat: u64,
55 ) -> Result<Transaction, PaymentError>;
56
57 async fn build_drain_tx(
65 &self,
66 fee_rate_sats_per_kvb: Option<f32>,
67 recipient_address: &str,
68 enforce_amount_sat: Option<u64>,
69 ) -> Result<Transaction, PaymentError>;
70
71 async fn build_tx_or_drain_tx(
75 &self,
76 fee_rate_sats_per_kvb: Option<f32>,
77 recipient_address: &str,
78 asset_id: &str,
79 amount_sat: u64,
80 ) -> Result<Transaction, PaymentError>;
81
82 async fn sign_pset(&self, pset: &mut PartiallySignedTransaction) -> Result<(), PaymentError>;
84
85 async fn next_unused_address(&self) -> Result<Address, PaymentError>;
87
88 async fn next_unused_change_address(&self) -> Result<Address, PaymentError>;
90
91 async fn tip(&self) -> u32;
93
94 fn pubkey(&self) -> Result<String>;
96
97 fn fingerprint(&self) -> Result<String>;
99
100 fn sign_message(&self, msg: &str) -> Result<String>;
103
104 fn check_message(&self, message: &str, pubkey: &str, signature: &str) -> Result<bool>;
107
108 async fn full_scan(&self) -> Result<(), PaymentError>;
110}
111
112pub enum WalletClient {
113 #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
114 Electrum(Box<lwk_wollet::ElectrumClient>),
115 Esplora(Box<EsploraClient>),
116}
117
118impl WalletClient {
119 pub(crate) fn from_config(config: &Config) -> Result<Self> {
120 match &config.liquid_explorer {
121 #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
122 BlockchainExplorer::Electrum { url } => {
123 let client = Box::new(config.electrum_client(url)?);
124 Ok(Self::Electrum(client))
125 }
126 BlockchainExplorer::Esplora {
127 url,
128 use_waterfalls,
129 } => {
130 let waterfalls = *use_waterfalls;
131 let mut builder = EsploraClientBuilder::new(url, config.network.into());
132 if url == BREEZ_LIQUID_ESPLORA_URL {
133 match &config.breez_api_key {
134 Some(api_key) => {
135 builder = builder
136 .header("authorization".to_string(), format!("Bearer {api_key}"));
137 }
138 None => {
139 let err = "Cannot start Breez Esplora client: Breez API key is not set";
140 error!("{err}");
141 bail!(err)
142 }
143 };
144 }
145 let client = Box::new(builder.timeout(3).waterfalls(waterfalls).build());
146 Ok(Self::Esplora(client))
147 }
148 }
149 }
150
151 pub(crate) async fn full_scan_to_index(
152 &mut self,
153 wallet: &mut Wollet,
154 index: u32,
155 ) -> Result<(), lwk_wollet::Error> {
156 let maybe_update = match self {
157 #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
158 WalletClient::Electrum(electrum_client) => {
159 electrum_client.full_scan_to_index(&wallet.state(), index)?
160 }
161 WalletClient::Esplora(esplora_client) => {
162 esplora_client.full_scan_to_index(wallet, index).await?
163 }
164 };
165
166 if let Some(update) = maybe_update {
167 debug!(
168 "WalletClient::full_scan_to_index: applying update {}",
169 update.version
170 );
171 wallet.apply_update(update)?;
172 }
173
174 Ok(())
175 }
176}
177
178pub struct LiquidOnchainWallet {
179 config: Config,
180 persister: std::sync::Arc<Persister>,
181 wallet: Arc<Mutex<Wollet>>,
182 client: Mutex<Option<WalletClient>>,
183 pub(crate) signer: SdkLwkSigner,
184 wallet_cache_persister: Arc<dyn WalletCachePersister>,
185}
186
187impl LiquidOnchainWallet {
188 pub(crate) async fn new(
190 config: Config,
191 persister: std::sync::Arc<Persister>,
192 user_signer: Arc<Box<dyn Signer>>,
193 ) -> Result<Self> {
194 let signer = SdkLwkSigner::new(user_signer.clone())?;
195
196 let wallet_cache_persister: Arc<dyn WalletCachePersister> =
197 Arc::new(SqliteWalletCachePersister::new(
198 std::sync::Arc::clone(&persister),
199 get_descriptor(&signer)?,
200 )?);
201
202 let wollet = Self::create_wallet(&config, &signer, wallet_cache_persister.clone()).await?;
203
204 Ok(Self {
205 config,
206 persister,
207 wallet: Arc::new(Mutex::new(wollet)),
208 client: Mutex::new(None),
209 signer,
210 wallet_cache_persister,
211 })
212 }
213
214 async fn create_wallet(
215 config: &Config,
216 signer: &SdkLwkSigner,
217 wallet_cache_persister: Arc<dyn WalletCachePersister>,
218 ) -> Result<Wollet> {
219 let elements_network: ElementsNetwork = config.network.into();
220 let descriptor = get_descriptor(signer)?;
221 let wollet_res = Wollet::new(
222 elements_network,
223 wallet_cache_persister.get_lwk_persister()?,
224 descriptor.clone(),
225 );
226 match wollet_res {
227 Ok(wollet) => Ok(wollet),
228 res @ Err(
229 lwk_wollet::Error::PersistError(_)
230 | lwk_wollet::Error::UpdateHeightTooOld { .. }
231 | lwk_wollet::Error::UpdateOnDifferentStatus { .. },
232 ) => {
233 warn!("Update error initialising wollet, wiping cache and retrying: {res:?}");
234 wallet_cache_persister.clear_cache().await?;
235 Ok(Wollet::new(
236 elements_network,
237 wallet_cache_persister.get_lwk_persister()?,
238 descriptor.clone(),
239 )?)
240 }
241 Err(e) => Err(e.into()),
242 }
243 }
244
245 async fn get_txout(&self, wallet: &Wollet, outpoint: &OutPoint) -> Result<TxOut> {
246 let wallet_tx = wallet
247 .transaction(&outpoint.txid)?
248 .ok_or(anyhow!("Transaction not found"))?;
249 let tx_out = wallet_tx
250 .tx
251 .output
252 .get(outpoint.vout as usize)
253 .ok_or(anyhow!("Output not found"))?;
254 Ok(tx_out.clone())
255 }
256}
257
258pub fn get_descriptor(signer: &SdkLwkSigner) -> Result<WolletDescriptor, PaymentError> {
259 let descriptor_str = singlesig_desc(
260 signer,
261 Singlesig::Wpkh,
262 lwk_common::DescriptorBlindingKey::Slip77,
263 )
264 .map_err(|e| anyhow!("Invalid descriptor: {e}"))?;
265 Ok(descriptor_str.parse()?)
266}
267
268#[sdk_macros::async_trait]
269impl OnchainWallet for LiquidOnchainWallet {
270 async fn transactions(&self) -> Result<Vec<WalletTx>, PaymentError> {
272 let wallet = self.wallet.lock().await;
273 wallet.transactions().map_err(|e| PaymentError::Generic {
274 err: format!("Failed to fetch wallet transactions: {e:?}"),
275 })
276 }
277
278 async fn transactions_by_tx_id(&self) -> Result<HashMap<Txid, WalletTx>, PaymentError> {
280 let tx_map: HashMap<Txid, WalletTx> = self
281 .transactions()
282 .await?
283 .iter()
284 .map(|tx| (tx.txid, tx.clone()))
285 .collect();
286 Ok(tx_map)
287 }
288
289 async fn asset_utxos(&self, asset: &AssetId) -> Result<Vec<WalletTxOut>, PaymentError> {
290 Ok(self
291 .wallet
292 .lock()
293 .await
294 .utxos()?
295 .into_iter()
296 .filter(|utxo| &utxo.unblinded.asset == asset)
297 .collect())
298 }
299
300 async fn build_tx(
302 &self,
303 fee_rate_sats_per_kvb: Option<f32>,
304 recipient_address: &str,
305 asset_id: &str,
306 amount_sat: u64,
307 ) -> Result<Transaction, PaymentError> {
308 let lwk_wollet = self.wallet.lock().await;
309 let address =
310 ElementsAddress::from_str(recipient_address).map_err(|e| PaymentError::Generic {
311 err: format!(
312 "Recipient address {recipient_address} is not a valid ElementsAddress: {e:?}"
313 ),
314 })?;
315 let mut tx_builder = lwk_wollet::TxBuilder::new(self.config.network.into())
316 .fee_rate(fee_rate_sats_per_kvb)
317 .enable_ct_discount();
318 if asset_id.eq(&self.config.lbtc_asset_id()) {
319 tx_builder = tx_builder.add_lbtc_recipient(&address, amount_sat)?;
320 } else {
321 let asset = AssetId::from_str(asset_id)?;
322 tx_builder = tx_builder.add_recipient(&address, amount_sat, asset)?;
323 }
324 let mut pset = tx_builder.finish(&lwk_wollet)?;
325 self.signer
326 .sign(&mut pset)
327 .map_err(|e| PaymentError::Generic {
328 err: format!("Failed to sign transaction: {e:?}"),
329 })?;
330 Ok(lwk_wollet.finalize(&mut pset)?)
331 }
332
333 async fn build_drain_tx(
334 &self,
335 fee_rate_sats_per_kvb: Option<f32>,
336 recipient_address: &str,
337 enforce_amount_sat: Option<u64>,
338 ) -> Result<Transaction, PaymentError> {
339 let lwk_wollet = self.wallet.lock().await;
340
341 let address =
342 ElementsAddress::from_str(recipient_address).map_err(|e| PaymentError::Generic {
343 err: format!(
344 "Recipient address {recipient_address} is not a valid ElementsAddress: {e:?}"
345 ),
346 })?;
347 let mut pset = lwk_wollet
348 .tx_builder()
349 .drain_lbtc_wallet()
350 .drain_lbtc_to(address)
351 .fee_rate(fee_rate_sats_per_kvb)
352 .enable_ct_discount()
353 .finish()?;
354
355 if let Some(enforce_amount_sat) = enforce_amount_sat {
356 let pset_details = lwk_wollet.get_details(&pset)?;
357 let pset_balance_sat = pset_details
358 .balance
359 .balances
360 .get(&lwk_wollet.policy_asset())
361 .unwrap_or(&0);
362 let pset_fees = pset_details.balance.fee;
363
364 ensure_sdk!(
365 (*pset_balance_sat * -1) as u64 - pset_fees == enforce_amount_sat,
366 PaymentError::Generic {
367 err: format!("Drain tx amount {pset_balance_sat} sat doesn't match enforce_amount_sat {enforce_amount_sat} sat")
368 }
369 );
370 }
371
372 self.signer
373 .sign(&mut pset)
374 .map_err(|e| PaymentError::Generic {
375 err: format!("Failed to sign transaction: {e:?}"),
376 })?;
377 Ok(lwk_wollet.finalize(&mut pset)?)
378 }
379
380 async fn build_tx_or_drain_tx(
381 &self,
382 fee_rate_sats_per_kvb: Option<f32>,
383 recipient_address: &str,
384 asset_id: &str,
385 amount_sat: u64,
386 ) -> Result<Transaction, PaymentError> {
387 match self
388 .build_tx(
389 fee_rate_sats_per_kvb,
390 recipient_address,
391 asset_id,
392 amount_sat,
393 )
394 .await
395 {
396 Ok(tx) => Ok(tx),
397 Err(PaymentError::InsufficientFunds) if asset_id.eq(&self.config.lbtc_asset_id()) => {
398 warn!("Cannot build tx due to insufficient funds, attempting to build drain tx");
399 self.build_drain_tx(fee_rate_sats_per_kvb, recipient_address, Some(amount_sat))
400 .await
401 }
402 Err(e) => Err(e),
403 }
404 }
405
406 async fn sign_pset(&self, pset: &mut PartiallySignedTransaction) -> Result<(), PaymentError> {
407 let lwk_wollet = self.wallet.lock().await;
408
409 for input in pset.inputs_mut().iter_mut() {
411 let tx_out_res = self
412 .get_txout(
413 &lwk_wollet,
414 &OutPoint {
415 txid: input.previous_txid,
416 vout: input.previous_output_index,
417 },
418 )
419 .await;
420 if let Ok(mut tx_out) = tx_out_res {
421 input.in_utxo_rangeproof = tx_out.witness.rangeproof.take();
422 input.witness_utxo = Some(tx_out);
423 }
424 }
425
426 lwk_wollet.add_details(pset)?;
427
428 self.signer.sign(pset).map_err(|e| PaymentError::Generic {
429 err: format!("Failed to sign transaction: {e:?}"),
430 })?;
431
432 for input in pset.inputs_mut() {
434 if let Some((public_key, input_sign)) = input.partial_sigs.iter().next() {
435 input.final_script_witness = Some(vec![input_sign.clone(), public_key.to_bytes()]);
436 }
437 }
438
439 Ok(())
440 }
441
442 async fn next_unused_address(&self) -> Result<Address, PaymentError> {
444 let tip = self.tip().await;
445 let address = match self.persister.next_expired_reserved_address(tip)? {
446 Some(reserved_address) => {
447 debug!(
448 "Got reserved address {} that expired on block height {}",
449 reserved_address.address, reserved_address.expiry_block_height
450 );
451 ElementsAddress::from_str(&reserved_address.address)
452 .map_err(|e| PaymentError::Generic { err: e.to_string() })?
453 }
454 None => {
455 let next_index = self.persister.next_derivation_index()?;
456 let address_result = self.wallet.lock().await.address(next_index)?;
457 let address = address_result.address().clone();
458 let index = address_result.index();
459 debug!("Got unused address {address} with derivation index {index}");
460 if next_index.is_none() {
461 self.persister.set_last_derivation_index(index)?;
462 }
463 address
464 }
465 };
466
467 Ok(address)
468 }
469
470 async fn next_unused_change_address(&self) -> Result<Address, PaymentError> {
472 let address = self.wallet.lock().await.change(None)?.address().clone();
473
474 Ok(address)
475 }
476
477 async fn tip(&self) -> u32 {
479 self.wallet.lock().await.tip().height()
480 }
481
482 fn pubkey(&self) -> Result<String> {
484 Ok(self.signer.xpub()?.public_key.to_string())
485 }
486
487 fn fingerprint(&self) -> Result<String> {
489 Ok(self.signer.fingerprint()?.to_hex())
490 }
491
492 async fn full_scan(&self) -> Result<(), PaymentError> {
494 debug!("LiquidOnchainWallet::full_scan: start");
495 let full_scan_started = Instant::now();
496
497 let mut client = self.client.lock().await;
499 if client.is_none() {
500 *client = Some(WalletClient::from_config(&self.config)?);
501 }
502 let client = client.as_mut().ok_or_else(|| PaymentError::Generic {
503 err: "Wallet client not initialized".to_string(),
504 })?;
505
506 let last_derivation_index = self
508 .persister
509 .get_last_derivation_index()?
510 .unwrap_or_default();
511 let index_with_buffer = last_derivation_index + 5;
512 let mut wallet = self.wallet.lock().await;
513
514 if self
517 .persister
518 .get_last_scanned_derivation_index()?
519 .is_some_and(|index| index != last_derivation_index)
520 {
521 debug!("LiquidOnchainWallet::full_scan: reunblinding all transactions");
522 wallet.reunblind()?;
523 }
524
525 let res = match client
526 .full_scan_to_index(&mut wallet, index_with_buffer)
527 .await
528 {
529 Ok(()) => Ok(()),
530 Err(e)
531 if matches!(
532 e,
533 lwk_wollet::Error::UpdateHeightTooOld { .. }
534 | lwk_wollet::Error::PersistError(_)
535 ) =>
536 {
537 warn!("Full scan failed due to {e}, reloading wallet and retrying");
538 let mut new_wallet = Self::create_wallet(
539 &self.config,
540 &self.signer,
541 self.wallet_cache_persister.clone(),
542 )
543 .await?;
544 client
545 .full_scan_to_index(&mut new_wallet, index_with_buffer)
546 .await?;
547 *wallet = new_wallet;
548 Ok(())
549 }
550 Err(e) => Err(e.into()),
551 };
552
553 self.persister
554 .set_last_scanned_derivation_index(last_derivation_index)?;
555
556 let duration_ms = Instant::now().duration_since(full_scan_started).as_millis();
557 info!("lwk wallet full_scan duration: ({duration_ms} ms)");
558 debug!("LiquidOnchainWallet::full_scan: end");
559 res
560 }
561
562 fn sign_message(&self, message: &str) -> Result<String> {
563 let mut engine = sha256::HashEngine::default();
565 engine.write_all(LN_MESSAGE_PREFIX)?;
566 engine.write_all(message.as_bytes())?;
567 let hashed_msg = sha256::Hash::from_engine(engine);
568 let double_hashed_msg = Message::from_digest(sha256::Hash::hash(&hashed_msg).into_inner());
569 let recoverable_sig = self.signer.sign_ecdsa_recoverable(&double_hashed_msg)?;
571 Ok(zbase32::encode_full_bytes(recoverable_sig.as_slice()))
572 }
573
574 fn check_message(&self, message: &str, pubkey: &str, signature: &str) -> Result<bool> {
575 let pk = PublicKey::from_str(pubkey)?;
576 Ok(verify(message.as_bytes(), signature, &pk))
577 }
578}
579
580#[cfg(test)]
581mod tests {
582 use super::*;
583 use crate::model::Config;
584 use crate::signer::SdkSigner;
585 use crate::test_utils::persist::create_persister;
586 use crate::wallet::LiquidOnchainWallet;
587 use anyhow::Result;
588
589 #[cfg(feature = "browser-tests")]
590 wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
591
592 #[sdk_macros::async_test_all]
593 async fn test_sign_and_check_message() -> Result<()> {
594 let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
595 let sdk_signer: Box<dyn Signer> = Box::new(SdkSigner::new(mnemonic, "", false).unwrap());
596 let sdk_signer = Arc::new(sdk_signer);
597
598 let config = Config::testnet_esplora(None);
599
600 create_persister!(storage);
601
602 let wallet: Arc<dyn OnchainWallet> = Arc::new(
603 LiquidOnchainWallet::new(config, storage, sdk_signer.clone())
604 .await
605 .unwrap(),
606 );
607
608 let message = "Hello, Liquid!";
610
611 let signature = wallet.sign_message(message).unwrap();
613
614 let pubkey = wallet.pubkey().unwrap();
616
617 let is_valid = wallet.check_message(message, &pubkey, &signature).unwrap();
619 assert!(is_valid, "Message signature should be valid");
620
621 let incorrect_message = "Wrong message";
623 let is_invalid = wallet
624 .check_message(incorrect_message, &pubkey, &signature)
625 .unwrap();
626 assert!(
627 !is_invalid,
628 "Message signature should be invalid for incorrect message"
629 );
630
631 let incorrect_pubkey = "02a1633cafcc01ebfb6d78e39f687a1f0995c62fc95f51ead10a02ee0be551b5dc";
633 let is_invalid = wallet
634 .check_message(message, incorrect_pubkey, &signature)
635 .unwrap();
636 assert!(
637 !is_invalid,
638 "Message signature should be invalid for incorrect public key"
639 );
640
641 let incorrect_signature = zbase32::encode_full_bytes(&[0; 65]);
643 let is_invalid = wallet
644 .check_message(message, &pubkey, &incorrect_signature)
645 .unwrap();
646 assert!(
647 !is_invalid,
648 "Message signature should be invalid for incorrect signature"
649 );
650
651 Ok(())
653 }
654}