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