taproot_assets_rpc/
client.rs1use 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
15pub type RpcClient = TaprootAssetsClient<InterceptedService<Channel, MacaroonInterceptor>>;
17
18#[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 pub fn new(inner: RpcClient) -> Self {
44 Self { inner }
45 }
46
47 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 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 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 let domain_response = crate::taprpc::types::ListAssetsResponse::try_from(rpc_response)?;
110 Ok(domain_response)
111 }
112
113 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 let domain_response = crate::taprpc::types::VerifyProofResponse::try_from(rpc_response)?;
163
164 Ok(domain_response)
165 }
166}
167
168async 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 let pem = fs::read_to_string(server_ca_cert_path)?;
188 let ca = Certificate::from_pem(pem);
189
190 let tls_config = ClientTlsConfig::new()
192 .ca_certificate(ca)
193 .domain_name(expected_server_domain);
194
195 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
209fn 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#[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}