blockfrost_http_client/
lib.rs1#![deny(unused_crate_dependencies)]
2use crate::models::{
3 Address, AddressInfo, BlockInfo, EvaluateTxResult, Genesis, HTTPResponse, HttpErrorInner,
4 ProtocolParams, TxSubmitResult, UTxO,
5};
6use async_trait::async_trait;
7use reqwest::Response;
8use serde::de::DeserializeOwned;
9use std::{fs, path::Path};
10use url::Url;
11
12use crate::error::{Error, Result};
13
14pub mod error;
15pub mod models;
16#[cfg(test)]
17pub mod tests;
18
19pub const MAINNET_URL: &str = "https://cardano-mainnet.blockfrost.io/api/v0";
20pub const PREPROD_NETWORK_URL: &str = "https://cardano-preprod.blockfrost.io/api/v0/";
21
22pub fn load_key_from_file(key_path: &str) -> Result<String> {
23 let path = Path::new(key_path);
24 let text = fs::read_to_string(&path).map_err(Error::FileRead)?;
25 let config: toml::Value = toml::from_str(&text).map_err(Error::Toml)?;
26 let field = "project_id";
27 let project_id = config[field]
28 .as_str()
29 .ok_or_else(|| Error::Config(field.to_string()))?
30 .to_string();
31 Ok(project_id)
32}
33
34pub struct BlockFrostHttp {
35 parent_url: String,
36 api_key: String, }
38
39#[async_trait]
40pub trait BlockFrostHttpTrait {
41 async fn genesis(&self) -> Result<Genesis>;
42
43 async fn latest_block_info(&self) -> Result<BlockInfo>;
44
45 async fn protocol_params(&self, epoch: u32) -> Result<ProtocolParams>;
46
47 async fn address_info(&self, address: &str) -> Result<AddressInfo>;
48
49 async fn utxos(&self, address: &str, maybe_count: Option<usize>) -> Result<Vec<UTxO>>;
50
51 async fn datum(&self, datum_hash: &str) -> Result<serde_json::Value>;
52
53 async fn assoc_addresses(&self, stake_address: &str) -> Result<Vec<Address>>;
54
55 async fn account_associated_addresses_total(&self, base_addr: &str) -> Result<Vec<Address>>;
56
57 async fn execution_units(&self, bytes: &[u8]) -> Result<EvaluateTxResult>;
58
59 async fn submit_tx(&self, bytes: &[u8]) -> Result<TxSubmitResult>;
60}
61
62#[async_trait]
63impl BlockFrostHttpTrait for BlockFrostHttp {
64 async fn genesis(&self) -> Result<Genesis> {
65 let ext = "./genesis";
66 self.get_endpoint(ext).await
67 }
68
69 async fn latest_block_info(&self) -> Result<BlockInfo> {
70 let ext = "./blocks/latest";
71 self.get_endpoint(&ext).await
72 }
73
74 async fn protocol_params(&self, epoch: u32) -> Result<ProtocolParams> {
75 let ext = format!("./epochs/{}/parameters", epoch);
76 self.get_endpoint(&ext).await
77 }
78
79 async fn address_info(&self, address: &str) -> Result<AddressInfo> {
80 let ext = format!("./addresses/{}", address);
81 self.get_endpoint(&ext).await
82 }
83
84 async fn utxos(&self, address: &str, maybe_count: Option<usize>) -> Result<Vec<UTxO>> {
85 let ext = format!("./addresses/{}/utxos", address);
86
87 let params = if let Some(count) = maybe_count {
88 let count_str = count.to_string();
89 vec![
90 ("order".to_string(), "desc".to_string()),
91 ("count".to_string(), count_str),
92 ]
93 } else {
94 vec![("order".to_string(), "desc".to_string())]
96 };
97 self.get_endpoint_with_params(&ext, ¶ms).await
98 }
99
100 async fn datum(&self, datum_hash: &str) -> Result<serde_json::Value> {
101 let ext = format!("./scripts/datum/{}", datum_hash);
102 self.get_endpoint(&ext).await
103 }
104
105 async fn assoc_addresses(&self, stake_address: &str) -> Result<Vec<Address>> {
106 let ext = format!("./accounts/{}/addresses", stake_address);
107 self.get_endpoint(&ext).await
108 }
109
110 async fn account_associated_addresses_total(&self, base_addr: &str) -> Result<Vec<Address>> {
111 let ext = format!("./accounts/{}/addresses/total", base_addr);
112 self.get_endpoint(&ext).await
113 }
114
115 async fn execution_units(&self, bytes: &[u8]) -> Result<EvaluateTxResult> {
116 let ext = "./utils/txs/evaluate".to_string();
117 let url = Url::parse(&self.parent_url)?.join(&ext)?;
118 let client = reqwest::Client::new();
119 let project_id = &self.api_key;
120 let encoded = hex::encode(bytes);
121 let res = client
122 .post(url)
123 .header("Content-Type", "application/cbor")
124 .header("project_id", project_id)
125 .body(encoded)
126 .send()
127 .await
128 .unwrap();
129 try_deserializing(res).await
130 }
131
132 async fn submit_tx(&self, bytes: &[u8]) -> Result<TxSubmitResult> {
133 let ext = "./tx/submit".to_string();
134 let url = Url::parse(&self.parent_url)?.join(&ext)?;
135 let client = reqwest::Client::new();
136 let project_id = &self.api_key;
137 let res = client
138 .post(url)
139 .header("Content-Type", "application/cbor")
140 .header("project_id", project_id)
141 .body(bytes.to_owned()) .send()
143 .await
144 .unwrap();
145 try_deserializing(res).await
146 }
147}
148
149impl BlockFrostHttp {
150 pub fn new(url: &str, key: &str) -> Self {
151 let parent_url = url.to_string();
152 let api_key = key.to_string();
153 BlockFrostHttp {
154 parent_url,
155 api_key,
156 }
157 }
158
159 async fn get_endpoint<T: DeserializeOwned + std::fmt::Debug>(&self, ext: &str) -> Result<T> {
160 self.get_endpoint_with_params(ext, &[]).await
161 }
162
163 async fn get_endpoint_with_params<T: DeserializeOwned + std::fmt::Debug>(
164 &self,
165 ext: &str,
166 params: &[(String, String)],
167 ) -> Result<T> {
168 let mut url = Url::parse(&self.parent_url)?.join(ext)?;
169 url.query_pairs_mut().extend_pairs(params);
170 let client = reqwest::Client::new();
171 let project_id = &self.api_key;
172 let res = client
173 .get(url)
174 .header("project_id", project_id)
175 .send()
176 .await?;
177
178 try_deserializing(res).await
179 }
180}
181
182async fn try_deserializing<T: DeserializeOwned + std::fmt::Debug>(res: Response) -> Result<T> {
183 let full = res.bytes().await.map_err(|e| Error::Reqwest(e))?;
184 let response = if let Ok(inner) = serde_json::from_slice(&full) {
187 HTTPResponse::HttpOk(inner)
188 } else if let Ok(err) = serde_json::from_slice(&full) {
189 HTTPResponse::HttpError(err)
190 } else {
191 let err = serde_json::from_slice::<T>(&full)
192 .map_err(|e| Error::SerdeJson(e))
193 .unwrap_err();
194 return Err(err);
195 };
196 match response {
197 HTTPResponse::HttpOk(inner) => Ok(inner),
198 HTTPResponse::HttpError(HttpErrorInner {
199 status_code,
200 error,
201 message,
202 }) => Err(Error::HttpError {
203 status_code,
204 error,
205 message,
206 }),
207 }
208}