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