breez_sdk_liquid/chain/liquid/
electrum.rs1#![cfg(not(all(target_family = "wasm", target_os = "unknown")))]
2
3use std::sync::OnceLock;
4
5use anyhow::{anyhow, bail, Context as _, Result};
6use tokio::sync::RwLock;
7
8use crate::{
9 chain::{with_empty_retry, with_error_retry},
10 elements::{Address, OutPoint, Script, Transaction, Txid},
11 model::{BlockchainExplorer, Config, Utxo},
12 utils,
13};
14
15use log::info;
16use lwk_wollet::{
17 clients::blocking::BlockchainBackend as _, elements::hex::FromHex as _, ElectrumClient,
18};
19use sdk_common::bitcoin::hashes::hex::ToHex as _;
20
21use super::{History, LiquidChainService};
22
23pub(crate) struct ElectrumLiquidChainService {
24 config: Config,
25 client: OnceLock<RwLock<ElectrumClient>>,
26}
27
28impl ElectrumLiquidChainService {
29 pub(crate) fn new(config: Config) -> Self {
30 Self {
31 config,
32 client: OnceLock::new(),
33 }
34 }
35
36 fn get_client(&self) -> Result<&RwLock<ElectrumClient>> {
37 if let Some(c) = self.client.get() {
38 return Ok(c);
39 }
40
41 let client = match &self.config.liquid_explorer {
42 BlockchainExplorer::Electrum { url } => self.config.electrum_client(url)?,
43 _ => bail!("Cannot start Liquid Electrum chain service without an Electrum url"),
44 };
45 let client = self.client.get_or_init(|| RwLock::new(client));
46 Ok(client)
47 }
48
49 async fn get_scripts_history(&self, scripts: &[Script]) -> Result<Vec<Vec<History>>> {
50 let scripts: Vec<&Script> = scripts.iter().collect();
51 Ok(self
52 .get_client()?
53 .read()
54 .await
55 .get_scripts_history(&scripts)?
56 .into_iter()
57 .map(|h| h.into_iter().map(Into::into).collect())
58 .collect())
59 }
60}
61
62#[sdk_macros::async_trait]
63impl LiquidChainService for ElectrumLiquidChainService {
64 async fn tip(&self) -> Result<u32> {
65 Ok(self
66 .get_client()?
67 .write()
68 .await
69 .tip()
70 .map(|header| header.height)?)
71 }
72
73 async fn broadcast(&self, tx: &Transaction) -> Result<Txid> {
74 Ok(self.get_client()?.read().await.broadcast(tx)?)
75 }
76
77 async fn get_transaction_hex(&self, txid: &Txid) -> Result<Option<Transaction>> {
78 Ok(self.get_transactions(&[*txid]).await?.first().cloned())
79 }
80
81 async fn get_transactions(&self, txids: &[Txid]) -> Result<Vec<Transaction>> {
82 Ok(self.get_client()?.read().await.get_transactions(txids)?)
83 }
84
85 async fn get_script_history(&self, script: &Script) -> Result<Vec<History>> {
86 self.get_scripts_history(std::slice::from_ref(script))
87 .await?
88 .into_iter()
89 .nth(0)
90 .context("History not found")
91 }
92
93 async fn get_script_history_with_retry(
94 &self,
95 script: &Script,
96 retries: u64,
97 ) -> Result<Vec<History>> {
98 info!("Fetching script history for {script:x}");
99 with_empty_retry(|| self.get_script_history(script), retries).await
100 }
101
102 async fn get_scripts_history_with_retry(
103 &self,
104 scripts: &[Script],
105 retries: u64,
106 ) -> Result<Vec<Vec<History>>> {
107 info!("Fetching scripts history for {} scripts", scripts.len());
108 with_error_retry(|| self.get_scripts_history(scripts), retries).await
109 }
110
111 async fn get_script_utxos(&self, script: &Script) -> Result<Vec<Utxo>> {
112 let history = self.get_script_history_with_retry(script, 10).await?;
113
114 let mut utxos: Vec<Utxo> = vec![];
115 for history_item in history {
116 match self.get_transaction_hex(&history_item.txid).await {
117 Ok(Some(tx)) => {
118 let mut new_utxos = tx
119 .output
120 .iter()
121 .enumerate()
122 .map(|(vout, output)| {
123 Utxo::Liquid(Box::new((
124 OutPoint::new(history_item.txid, vout as u32),
125 output.clone(),
126 )))
127 })
128 .collect();
129 utxos.append(&mut new_utxos);
130 }
131 _ => {
132 log::warn!("Could not retrieve transaction from history item");
133 continue;
134 }
135 }
136 }
137
138 Ok(utxos)
139 }
140
141 async fn verify_tx(
142 &self,
143 address: &Address,
144 tx_id: &str,
145 tx_hex: &str,
146 verify_confirmation: bool,
147 ) -> Result<Transaction> {
148 let script = Script::from_hex(
149 hex::encode(address.to_unconfidential().script_pubkey().as_bytes()).as_str(),
150 )
151 .map_err(|e| anyhow!("Failed to get script from address {e:?}"))?;
152
153 let script_history = self.get_script_history_with_retry(&script, 30).await?;
154 let lockup_tx_history = script_history.iter().find(|h| h.txid.to_hex().eq(tx_id));
155
156 match lockup_tx_history {
157 Some(history) => {
158 info!("Liquid transaction found, verifying transaction content...");
159 let tx: Transaction = utils::deserialize_tx_hex(tx_hex)?;
160 if !tx.txid().to_hex().eq(&history.txid.to_hex()) {
161 return Err(anyhow!(
162 "Liquid transaction id and hex do not match: {} vs {}",
163 tx_id,
164 tx.txid().to_hex()
165 ));
166 }
167
168 if verify_confirmation && history.height <= 0 {
169 return Err(anyhow!(
170 "Liquid transaction was not confirmed, txid={} waiting for confirmation",
171 tx_id,
172 ));
173 }
174 Ok(tx)
175 }
176 None => Err(anyhow!(
177 "Liquid transaction was not found, txid={} waiting for broadcast",
178 tx_id,
179 )),
180 }
181 }
182}