blockfrost_http_client/
lib.rs

1#![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, // A.K.A. `project_id`
37}
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            // TODO: Paginate response for more than 100
95            vec![("order".to_string(), "desc".to_string())]
96        };
97        self.get_endpoint_with_params(&ext, &params).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()) // For some dumb-ass reason this is binary
142            .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 json: serde_json::Value = serde_json::from_slice(&full).unwrap();
185    // println!("json: {:?}", json);
186    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}