Skip to main content

cat_signer_api/
client.rs

1use crate::{GetPubkeyResponse, SignTxInputRequest, SignTxInputResponse};
2use bitcoin::consensus::{Decodable, Encodable};
3use bitcoin::hex::{DisplayHex, FromHex};
4use bitcoin::{Transaction, XOnlyPublicKey};
5use reqwest::Client;
6use std::fmt::{Display, Formatter};
7
8#[derive(Debug, Clone)]
9pub struct CatSignerClient {
10    pub base_url: String,
11    pub client: Client,
12}
13
14impl CatSignerClient {
15    /// Default reqwest client
16    pub fn new(base_url: String) -> Result<Self, Error> {
17        let client_builder = Client::builder();
18
19        Ok(Self::from_client(client_builder.build()?, base_url))
20    }
21
22    /// Build a [`CatSignerClient`] from a custom reqwest client
23    pub fn from_client(client: Client, base_url: String) -> Self {
24        Self { client, base_url }
25    }
26
27    /// Gets the signer's public key
28    pub async fn get_pubkey(&self) -> Result<XOnlyPublicKey, Error> {
29        let url = format!("{}/pubkey", self.base_url);
30        let resp = self.client.get(&url).send().await?;
31
32        let resp: GetPubkeyResponse = resp.error_for_status()?.json::<GetPubkeyResponse>().await?;
33        Ok(resp.pubkey)
34    }
35
36    /// Sends the transaction to the server to be signed, if it has a valid witness for the given input
37    /// the server will sign the input and return the signed transaction. For the input to be valid it must
38    /// be a tapscript spend with a valid control block, the control block must be valid for the *wrapped*
39    /// script, not the unwrapped script.
40    pub async fn sign_tx_input(
41        &self,
42        tx: &Transaction,
43        input_index: usize,
44    ) -> Result<Transaction, Error> {
45        let url = format!("{}/sign-tx-input", self.base_url);
46
47        let mut tx_bytes: Vec<u8> = Vec::with_capacity(tx.total_size());
48        tx.consensus_encode(&mut tx_bytes)
49            .expect("consensus encode");
50
51        let payload = SignTxInputRequest {
52            tx: tx_bytes.to_lower_hex_string(),
53            index: input_index,
54        };
55
56        let resp = self
57            .client
58            .post(&url)
59            .json(&payload)
60            .send()
61            .await?
62            .error_for_status()?
63            .json::<SignTxInputResponse>()
64            .await?;
65
66        let hex_bytes = Vec::from_hex(&resp.tx)
67            .map_err(|_| Error::Other("Could not decode hex".to_string()))?;
68
69        let signed_tx = Transaction::consensus_decode(&mut hex_bytes.as_slice())
70            .map_err(|_| Error::Other("Could not decode tx".to_string()))?;
71
72        Ok(signed_tx)
73    }
74}
75
76#[derive(Debug)]
77pub enum Error {
78    Reqwest(reqwest::Error),
79    /// HTTP response error
80    HttpResponse(u16),
81    /// Error decoding JSON
82    Json(serde_json::Error),
83    /// Invalid Response
84    InvalidResponse,
85    /// Other error
86    Other(String),
87}
88
89impl Display for Error {
90    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
91        write!(f, "{:?}", self)
92    }
93}
94
95impl std::error::Error for Error {}
96
97macro_rules! impl_error {
98    ( $from:ty, $to:ident ) => {
99        impl_error!($from, $to, Error);
100    };
101    ( $from:ty, $to:ident, $impl_for:ty ) => {
102        impl std::convert::From<$from> for $impl_for {
103            fn from(err: $from) -> Self {
104                <$impl_for>::$to(err)
105            }
106        }
107    };
108}
109
110impl_error!(reqwest::Error, Reqwest, Error);
111impl_error!(serde_json::Error, Json, Error);