anyswap-sdk 1.8.0

Rust client SDK for AnySwap: swaps between Lightning and on-chain Bitcoin or Liquid, with a driver that runs them to completion
Documentation
use async_trait::async_trait;
use thiserror::Error;

use super::{
    record::Height,
    rt::{MaybeSend, MaybeSync},
};

#[derive(Clone, Debug, Error)]
#[error("chain: {0}")]
pub struct ChainError(pub String);

/// One transaction of an address's history, mempool included.
#[derive(Clone, Debug)]
pub struct HistoryTx {
    pub hex: String,
    /// The confirming block, `None` while in the mempool.
    pub height: Option<Height>,
}

/// Query and broadcast on one chain. The driver reads a script's whole history
/// and interprets it itself, so an implementation fetches bytes by a fixed
/// recipe and never decides anything. It also bounds each of its own requests:
/// the driver waits on a call for as long as it takes, since a history read
/// may need many requests.
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait ChainClient: MaybeSend + MaybeSync {
    async fn height(&self) -> Result<Height, ChainError>;

    /// Estimate for confirmation within `target` blocks, in sat/vB.
    async fn fee_rate(&self, target: u32) -> Result<f64, ChainError>;

    /// Every transaction paying or spending `address`.
    async fn address_history(&self, address: &str) -> Result<Vec<HistoryTx>, ChainError>;

    /// The transaction spending output `vout` of `txid`, mempool included, or
    /// `None` while it is unspent: Esplora's outspend with the spending
    /// transaction fetched. Read from the output itself, so it is found
    /// however many other transactions crowd the address's history.
    async fn outspend(&self, txid: &str, vout: u32) -> Result<Option<HistoryTx>, ChainError>;

    /// Broadcasts a signed transaction and returns its txid.
    async fn broadcast(&self, tx_hex: &str) -> Result<String, ChainError>;
}

#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl<C: ChainClient + ?Sized> ChainClient for std::sync::Arc<C> {
    async fn height(&self) -> Result<Height, ChainError> {
        (**self).height().await
    }

    async fn fee_rate(&self, target: u32) -> Result<f64, ChainError> {
        (**self).fee_rate(target).await
    }

    async fn address_history(&self, address: &str) -> Result<Vec<HistoryTx>, ChainError> {
        (**self).address_history(address).await
    }

    async fn outspend(&self, txid: &str, vout: u32) -> Result<Option<HistoryTx>, ChainError> {
        (**self).outspend(txid, vout).await
    }

    async fn broadcast(&self, tx_hex: &str) -> Result<String, ChainError> {
        (**self).broadcast(tx_hex).await
    }
}

#[cfg(feature = "esplora")]
mod esplora {
    use std::str::FromStr;

    use anyswap_core::{
        esplora::{BitcoinEsplora, EsploraError, LiquidEsplora},
        transaction::bitcoin::{decode_tx_hex, encode_tx_hex},
    };
    use async_trait::async_trait;

    use super::{ChainClient, ChainError, HistoryTx};
    use crate::driver::record::Height;

    impl From<EsploraError> for ChainError {
        fn from(e: EsploraError) -> Self {
            Self(e.to_string())
        }
    }

    fn invalid(what: &str, e: impl std::fmt::Display) -> ChainError {
        ChainError(format!("invalid {what}: {e}"))
    }

    #[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
    #[cfg_attr(not(target_arch = "wasm32"), async_trait)]
    impl ChainClient for BitcoinEsplora {
        async fn height(&self) -> Result<Height, ChainError> {
            Ok(BitcoinEsplora::get_height(self).await?)
        }

        async fn fee_rate(&self, target: u32) -> Result<f64, ChainError> {
            Ok(BitcoinEsplora::get_fee_rate(self, target as usize).await?)
        }

        async fn address_history(&self, address: &str) -> Result<Vec<HistoryTx>, ChainError> {
            let address = bitcoin::Address::from_str(address)
                .map_err(|e| invalid("address", e))?
                .assume_checked();
            Ok(self
                .address_txs(&address)
                .await?
                .into_iter()
                .map(|(tx, height)| HistoryTx {
                    hex: encode_tx_hex(&tx),
                    height,
                })
                .collect())
        }

        async fn outspend(&self, txid: &str, vout: u32) -> Result<Option<HistoryTx>, ChainError> {
            let txid = bitcoin::Txid::from_str(txid).map_err(|e| invalid("txid", e))?;
            let Some(spend) = self.get_outspend(&txid, vout).await? else {
                return Ok(None);
            };
            let Some(tx) = self.get_tx(&spend.txid).await? else {
                return Ok(None);
            };
            Ok(Some(HistoryTx {
                hex: encode_tx_hex(&tx),
                height: self.get_confirmation_height(&spend.txid).await?,
            }))
        }

        async fn broadcast(&self, tx_hex: &str) -> Result<String, ChainError> {
            let tx = decode_tx_hex(tx_hex).map_err(|e| invalid("transaction", e))?;
            Ok(BitcoinEsplora::broadcast(self, &tx).await?.to_string())
        }
    }

    #[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
    #[cfg_attr(not(target_arch = "wasm32"), async_trait)]
    impl ChainClient for LiquidEsplora {
        async fn height(&self) -> Result<Height, ChainError> {
            Ok(LiquidEsplora::get_height(self).await?)
        }

        async fn fee_rate(&self, target: u32) -> Result<f64, ChainError> {
            Ok(LiquidEsplora::get_fee_rate(self, target as usize).await?)
        }

        async fn address_history(&self, address: &str) -> Result<Vec<HistoryTx>, ChainError> {
            let address =
                elements::Address::from_str(address).map_err(|e| invalid("address", e))?;
            Ok(self
                .address_txs(&address)
                .await?
                .into_iter()
                .map(|(tx, height)| HistoryTx {
                    hex: hex::encode(elements::encode::serialize(&tx)),
                    height,
                })
                .collect())
        }

        async fn outspend(&self, txid: &str, vout: u32) -> Result<Option<HistoryTx>, ChainError> {
            let txid = elements::Txid::from_str(txid).map_err(|e| invalid("txid", e))?;
            let Some(spend) = self.get_outspend(&txid, vout).await? else {
                return Ok(None);
            };
            let Some(tx) = self.get_tx(&spend.txid).await? else {
                return Ok(None);
            };
            Ok(Some(HistoryTx {
                hex: hex::encode(elements::encode::serialize(&tx)),
                height: self.get_confirmation_height(&spend.txid).await?,
            }))
        }

        async fn broadcast(&self, tx_hex: &str) -> Result<String, ChainError> {
            let tx = anyswap_core::transaction::liquid::decode_tx_hex(tx_hex)
                .map_err(|e| invalid("transaction", e))?;
            Ok(LiquidEsplora::broadcast(self, &tx).await?.to_string())
        }
    }
}