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