breez_sdk_liquid/chain/liquid/
electrum.rs

1#![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}