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}