cat-signer-api 0.1.0

An api client for working with the OP_CAT signer
Documentation
use crate::{GetPubkeyResponse, SignTxInputRequest, SignTxInputResponse};
use bitcoin::consensus::{Decodable, Encodable};
use bitcoin::hex::{DisplayHex, FromHex};
use bitcoin::{Transaction, XOnlyPublicKey};
use reqwest::Client;
use std::fmt::{Display, Formatter};

#[derive(Debug, Clone)]
pub struct CatSignerClient {
    pub base_url: String,
    pub client: Client,
}

impl CatSignerClient {
    /// Default reqwest client
    pub fn new(base_url: String) -> Result<Self, Error> {
        let client_builder = Client::builder();

        Ok(Self::from_client(client_builder.build()?, base_url))
    }

    /// Build a [`CatSignerClient`] from a custom reqwest client
    pub fn from_client(client: Client, base_url: String) -> Self {
        Self { client, base_url }
    }

    /// Gets the signer's public key
    pub async fn get_pubkey(&self) -> Result<XOnlyPublicKey, Error> {
        let url = format!("{}/pubkey", self.base_url);
        let resp = self.client.get(&url).send().await?;

        let resp: GetPubkeyResponse = resp.error_for_status()?.json::<GetPubkeyResponse>().await?;
        Ok(resp.pubkey)
    }

    /// Sends the transaction to the server to be signed, if it has a valid witness for the given input
    /// the server will sign the input and return the signed transaction. For the input to be valid it must
    /// be a tapscript spend with a valid control block, the control block must be valid for the *wrapped*
    /// script, not the unwrapped script.
    pub async fn sign_tx_input(
        &self,
        tx: &Transaction,
        input_index: usize,
    ) -> Result<Transaction, Error> {
        let url = format!("{}/sign-tx-input", self.base_url);

        let mut tx_bytes: Vec<u8> = Vec::with_capacity(tx.total_size());
        tx.consensus_encode(&mut tx_bytes)
            .expect("consensus encode");

        let payload = SignTxInputRequest {
            tx: tx_bytes.to_lower_hex_string(),
            index: input_index,
        };

        let resp = self
            .client
            .post(&url)
            .json(&payload)
            .send()
            .await?
            .error_for_status()?
            .json::<SignTxInputResponse>()
            .await?;

        let hex_bytes = Vec::from_hex(&resp.tx)
            .map_err(|_| Error::Other("Could not decode hex".to_string()))?;

        let signed_tx = Transaction::consensus_decode(&mut hex_bytes.as_slice())
            .map_err(|_| Error::Other("Could not decode tx".to_string()))?;

        Ok(signed_tx)
    }
}

#[derive(Debug)]
pub enum Error {
    Reqwest(reqwest::Error),
    /// HTTP response error
    HttpResponse(u16),
    /// Error decoding JSON
    Json(serde_json::Error),
    /// Invalid Response
    InvalidResponse,
    /// Other error
    Other(String),
}

impl Display for Error {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        write!(f, "{:?}", self)
    }
}

impl std::error::Error for Error {}

macro_rules! impl_error {
    ( $from:ty, $to:ident ) => {
        impl_error!($from, $to, Error);
    };
    ( $from:ty, $to:ident, $impl_for:ty ) => {
        impl std::convert::From<$from> for $impl_for {
            fn from(err: $from) -> Self {
                <$impl_for>::$to(err)
            }
        }
    };
}

impl_error!(reqwest::Error, Reqwest, Error);
impl_error!(serde_json::Error, Json, Error);