Skip to main content

taproot_assets_rpc/
client.rs

1use crate::taprpc::taproot_assets_client::TaprootAssetsClient;
2use std::{fs, path::Path};
3use thiserror::Error;
4use tonic::codegen::InterceptedService;
5use tonic::metadata::{MetadataKey, MetadataValue};
6use tonic::service::Interceptor;
7use tonic::transport::{Certificate, Channel, ClientTlsConfig, Endpoint};
8use tonic::{Request, Status};
9
10use taproot_assets_types as types;
11
12#[cfg(test)]
13mod tests;
14
15/// A type alias for a TaprootAssetsClient with macaroon authentication.
16pub type RpcClient = TaprootAssetsClient<InterceptedService<Channel, MacaroonInterceptor>>;
17
18/// Public error type for this crate.
19#[derive(Debug, Error)]
20#[non_exhaustive]
21pub enum ClientError {
22    #[error(transparent)]
23    Io(#[from] std::io::Error),
24
25    #[error(transparent)]
26    Transport(#[from] tonic::transport::Error),
27
28    #[error("gRPC call failed: {0}")]
29    GrpcStatus(#[from] tonic::Status),
30
31    #[error("Conversion error: {0}")]
32    Conversion(#[from] crate::convert::ConversionError),
33}
34
35pub type Result<T> = std::result::Result<T, ClientError>;
36
37pub struct Client {
38    inner: RpcClient,
39}
40
41impl Client {
42    /// Creates a new client from an existing RpcClient.
43    pub fn new(inner: RpcClient) -> Self {
44        Self { inner }
45    }
46
47    /// Connects to a tapd instance.
48    ///
49    /// # Arguments
50    /// * `dst_uri` - The destination URI (e.g., "https://localhost:port").
51    /// * `macaroon_path` - Path to the macaroon file.
52    /// * `server_ca_cert_path` - Path to the server's root CA certificate file.
53    /// * `expected_server_domain` - The domain name to verify against the server's certificate.
54    ///
55    /// # Returns
56    /// A new Client instance.
57    pub async fn connect(
58        dst_uri: String,
59        macaroon_path: impl AsRef<Path>,
60        server_ca_cert_path: impl AsRef<Path>,
61        expected_server_domain: String,
62    ) -> Result<Self> {
63        let rpc_client = connect_rpc(
64            dst_uri,
65            macaroon_path,
66            server_ca_cert_path,
67            expected_server_domain,
68        )
69        .await?;
70        Ok(Self::new(rpc_client))
71    }
72
73    /// Retrieves information about the tapd instance.
74    pub async fn get_info(&mut self) -> Result<crate::taprpc::GetInfoResponse> {
75        let request = crate::taprpc::GetInfoRequest {};
76        let response = self.inner.get_info(request).await?;
77        Ok(response.into_inner())
78    }
79
80    /// Lists assets.
81    pub async fn list_assets(
82        &mut self,
83        with_witness: bool,
84        include_spent: bool,
85        include_leased: bool,
86        include_unconfirmed_mints: bool,
87        min_amount: u64,
88        max_amount: u64,
89        group_key: Vec<u8>,
90        script_key: Option<crate::taprpc::ScriptKey>,
91        anchor_outpoint: Option<crate::taprpc::OutPoint>,
92        script_key_type: Option<crate::taprpc::ScriptKeyTypeQuery>,
93    ) -> Result<crate::taprpc::types::ListAssetsResponse> {
94        let request = crate::taprpc::ListAssetRequest {
95            with_witness,
96            include_spent,
97            include_leased,
98            include_unconfirmed_mints,
99            min_amount,
100            max_amount,
101            group_key,
102            script_key,
103            anchor_outpoint,
104            script_key_type,
105        };
106        let rpc_response = self.inner.list_assets(request).await?.into_inner();
107
108        // Perform the conversion using TryFrom.
109        let domain_response = crate::taprpc::types::ListAssetsResponse::try_from(rpc_response)?;
110        Ok(domain_response)
111    }
112
113    /// ExportProof exports the latest raw proof file anchored at the specified script_key.
114    pub async fn export_proof(
115        &mut self,
116        asset_id: types::asset::AssetID,
117        script_key: Vec<u8>,
118        outpoint: Option<bitcoin::OutPoint>,
119    ) -> Result<crate::taprpc::types::ExportProofResponse> {
120        let rpc_outpoint = outpoint.map(|op| crate::taprpc::OutPoint {
121            txid: <bitcoin::Txid as AsRef<[u8; 32]>>::as_ref(&op.txid).to_vec(),
122            output_index: op.vout,
123        });
124
125        let request = crate::taprpc::ExportProofRequest {
126            asset_id: <bitcoin::hashes::sha256::Hash as AsRef<[u8; 32]>>::as_ref(&asset_id)
127                .to_vec(),
128            script_key,
129            outpoint: rpc_outpoint,
130        };
131
132        log::debug!("Calling export_proof RPC endpoint");
133        let rpc_response = self.inner.export_proof(request).await?.into_inner();
134        log::debug!("Got export_proof RPC endpoint response");
135
136        log::debug!("Converting export_proof RPC endpoint response to domain response");
137        let domain_response = crate::taprpc::types::ExportProofResponse::try_from(rpc_response)?;
138        log::debug!("Successfully converted export_proof RPC endpoint response to domain response");
139
140        Ok(domain_response)
141    }
142
143    pub async fn verify_proof(
144        &mut self,
145        raw_proof_file: Vec<u8>,
146        genesis_point: Option<bitcoin::OutPoint>,
147    ) -> Result<crate::taprpc::types::VerifyProofResponse> {
148        let genesis_point_str = if let Some(genesis_point) = genesis_point {
149            genesis_point.to_string()
150        } else {
151            "".to_string()
152        };
153
154        let request = crate::taprpc::ProofFile {
155            raw_proof_file,
156            genesis_point: genesis_point_str,
157        };
158
159        let rpc_response = self.inner.verify_proof(request).await?.into_inner();
160
161        // Parse response.
162        let domain_response = crate::taprpc::types::VerifyProofResponse::try_from(rpc_response)?;
163
164        Ok(domain_response)
165    }
166}
167
168/// Connects to a tapd instance for testing, using a specific server CA certificate.
169///
170/// # Arguments
171/// * `dst_uri` - The destination URI (e.g., "https://localhost:port").
172/// * `macaroon_path` - Path to the macaroon file.
173/// * `server_ca_cert_path` - Path to the server's root CA certificate file.
174/// * `expected_server_domain` - The domain name to verify against the server's certificate.
175///
176/// # Returns
177/// A TaprootAssetsClient configured for the test connection.
178async fn connect_rpc(
179    dst_uri: String,
180    macaroon_path: impl AsRef<Path>,
181    server_ca_cert_path: impl AsRef<Path>,
182    expected_server_domain: String,
183) -> Result<RpcClient> {
184    ensure_crypto_default_provider();
185
186    // Load the CA certificate
187    let pem = fs::read_to_string(server_ca_cert_path)?;
188    let ca = Certificate::from_pem(pem);
189
190    // Create TLS configuration
191    let tls_config = ClientTlsConfig::new()
192        .ca_certificate(ca)
193        .domain_name(expected_server_domain);
194
195    // Read the macaroon file
196    let macaroon = fs::read(macaroon_path)?;
197    let interceptor = MacaroonInterceptor::new(macaroon);
198
199    let mut endpoint: Endpoint = dst_uri.try_into()?;
200    endpoint = endpoint.tls_config(tls_config)?;
201
202    let channel = endpoint.connect().await?;
203
204    Ok(TaprootAssetsClient::with_interceptor(channel, interceptor))
205}
206
207static INIT_CRYPTO: std::sync::Once = std::sync::Once::new();
208
209// Ensures the default crypto provider is installed for TLS.
210fn ensure_crypto_default_provider() {
211    INIT_CRYPTO.call_once(|| {
212        rustls::crypto::ring::default_provider()
213            .install_default()
214            .expect("install provider");
215    });
216}
217
218/// Interceptor to add macaroon to each gRPC request.
219#[derive(Clone)]
220pub struct MacaroonInterceptor {
221    macaroon_hex: MetadataValue<tonic::metadata::Ascii>,
222}
223
224impl MacaroonInterceptor {
225    fn new(bytes: Vec<u8>) -> Self {
226        let macaroon_hex = MetadataValue::try_from(hex::encode(bytes)).expect("hex is valid ASCII");
227        Self { macaroon_hex }
228    }
229}
230
231impl Interceptor for MacaroonInterceptor {
232    fn call(&mut self, mut req: Request<()>) -> std::result::Result<Request<()>, Status> {
233        req.metadata_mut().insert(
234            MetadataKey::from_static("macaroon"),
235            self.macaroon_hex.clone(),
236        );
237
238        Ok(req)
239    }
240}