snarkvm_ledger_query/query/
rest.rs1use crate::QueryTrait;
17
18use snarkvm_console::{
19 network::Network,
20 program::{ProgramID, StatePath},
21 types::Field,
22};
23use snarkvm_ledger_block::Transaction;
24use snarkvm_synthesizer_program::Program;
25
26use anyhow::{Context, Result, anyhow, bail, ensure};
27use serde::{Deserialize, de::DeserializeOwned};
28use ureq::http::{self, uri};
29
30use std::str::FromStr;
31
32#[derive(Clone)]
34pub struct RestQuery<N: Network> {
35 base_url: http::Uri,
36 _marker: std::marker::PhantomData<N>,
37}
38
39impl<N: Network> From<http::Uri> for RestQuery<N> {
40 fn from(base_url: http::Uri) -> Self {
41 Self { base_url, _marker: Default::default() }
42 }
43}
44
45#[derive(Debug, Deserialize)]
47pub struct RestError {
48 error_type: String,
50 message: String,
52 #[serde(skip_serializing_if = "Vec::is_empty")]
54 chain: Vec<String>,
55}
56
57impl RestError {
58 pub fn parse(self) -> anyhow::Error {
60 let mut error: Option<anyhow::Error> = None;
61 for next in self.chain.into_iter() {
62 if let Some(previous) = error {
63 error = Some(previous.context(next));
64 } else {
65 error = Some(anyhow!(next));
66 }
67 }
68
69 let toplevel = format!("{}: {}", self.error_type, self.message);
70 if let Some(error) = error { error.context(toplevel) } else { anyhow!(toplevel) }
71 }
72}
73
74impl<N: Network> FromStr for RestQuery<N> {
76 type Err = anyhow::Error;
77
78 fn from_str(str_representation: &str) -> Result<Self> {
79 let base_url = str_representation.parse::<http::Uri>().with_context(|| "Failed to parse URL")?;
80
81 if let Some(scheme) = base_url.scheme()
83 && *scheme != uri::Scheme::HTTP
84 && *scheme != uri::Scheme::HTTPS
85 {
86 bail!("Invalid scheme in URL: {scheme}");
87 }
88
89 if let Some(s) = base_url.host()
90 && s.is_empty()
91 {
92 bail!("Invalid URL for REST endpoint. Empty hostname given.");
93 } else if base_url.host().is_none() {
94 bail!("Invalid URL for REST endpoint. No hostname given.");
95 }
96
97 if base_url.query().is_some() {
98 bail!("Base URL for REST endpoints cannot contain a query");
99 }
100
101 Ok(Self::from(base_url))
102 }
103}
104
105#[cfg_attr(feature = "async", async_trait::async_trait(?Send))]
106impl<N: Network> QueryTrait<N> for RestQuery<N> {
107 fn current_state_root(&self) -> Result<N::StateRoot> {
109 self.get_request("stateRoot/latest")
110 }
111
112 #[cfg(feature = "async")]
114 async fn current_state_root_async(&self) -> Result<N::StateRoot> {
115 self.get_request_async("stateRoot/latest").await
116 }
117
118 fn get_state_path_for_commitment(&self, commitment: &Field<N>) -> Result<StatePath<N>> {
120 self.get_request(&format!("statePath/{commitment}"))
121 }
122
123 #[cfg(feature = "async")]
125 async fn get_state_path_for_commitment_async(&self, commitment: &Field<N>) -> Result<StatePath<N>> {
126 self.get_request_async(&format!("statePath/{commitment}")).await
127 }
128
129 fn get_state_paths_for_commitments(&self, commitments: &[Field<N>]) -> Result<Vec<StatePath<N>>> {
131 let commitments_string = commitments.iter().map(|cm| cm.to_string()).collect::<Vec<_>>().join(",");
133 self.get_request(&format!("statePaths?commitments={commitments_string}"))
134 }
135
136 #[cfg(feature = "async")]
138 async fn get_state_paths_for_commitments_async(&self, commitments: &[Field<N>]) -> Result<Vec<StatePath<N>>> {
139 let commitments_string = commitments.iter().map(|cm| cm.to_string()).collect::<Vec<_>>().join(",");
141 self.get_request_async(&format!("statePaths?commitments={commitments_string}")).await
142 }
143
144 fn current_block_height(&self) -> Result<u32> {
146 self.get_request("block/height/latest")
147 }
148
149 #[cfg(feature = "async")]
151 async fn current_block_height_async(&self) -> Result<u32> {
152 self.get_request_async("block/height/latest").await
153 }
154}
155
156impl<N: Network> RestQuery<N> {
157 pub fn get_transaction(&self, transaction_id: &N::TransactionID) -> Result<Transaction<N>> {
159 self.get_request(&format!("transaction/{transaction_id}"))
160 }
161
162 #[cfg(feature = "async")]
164 pub async fn get_transaction_async(&self, transaction_id: &N::TransactionID) -> Result<Transaction<N>> {
165 self.get_request_async(&format!("transaction/{transaction_id}")).await
166 }
167
168 pub fn get_program(&self, program_id: &ProgramID<N>) -> Result<Program<N>> {
170 self.get_request(&format!("program/{program_id}"))
171 }
172
173 #[cfg(feature = "async")]
175 pub async fn get_program_async(&self, program_id: &ProgramID<N>) -> Result<Program<N>> {
176 self.get_request_async(&format!("program/{program_id}")).await
177 }
178
179 fn build_endpoint(&self, route: &str) -> Result<String> {
185 ensure!(!route.starts_with('/'), "path cannot start with a slash");
187
188 let path = if self.base_url.path().ends_with('/') {
191 format!("{base_url}{network}/{route}", base_url = self.base_url, network = N::SHORT_NAME)
192 } else {
193 format!("{base_url}/{network}/{route}", base_url = self.base_url, network = N::SHORT_NAME)
194 };
195
196 Ok(path)
197 }
198
199 fn get_request<T: DeserializeOwned>(&self, route: &str) -> Result<T> {
204 let endpoint = self.build_endpoint(route)?;
205 let mut response = ureq::get(&endpoint)
206 .config()
207 .http_status_as_error(false)
208 .build()
209 .call()
210 .with_context(|| format!("Failed to fetch from {endpoint}"))?;
212
213 if response.status().is_success() {
214 response.body_mut().read_json().with_context(|| format!("Failed to parse JSON response from {endpoint}"))
215 } else {
216 let is_json = response
218 .headers()
219 .get(http::header::CONTENT_TYPE)
220 .and_then(|ct| ct.to_str().ok())
221 .map(|ct| ct.contains("json"))
222 .unwrap_or(false);
223
224 if is_json {
227 let error: RestError = response
228 .body_mut()
229 .read_json()
230 .with_context(|| format!("Failed to parse JSON error response from {endpoint}"))?;
231 Err(error.parse().context(format!("Failed to fetch from {endpoint}")))
232 } else {
233 let error = response
234 .body_mut()
235 .read_to_string()
236 .with_context(|| format!("Failed to read error message {endpoint}"))?;
237 Err(anyhow!(error).context(format!("Failed to fetch from {endpoint}")))
238 }
239 }
240 }
241
242 #[cfg(feature = "async")]
247 async fn get_request_async<T: DeserializeOwned>(&self, route: &str) -> Result<T> {
248 let endpoint = self.build_endpoint(route)?;
249 let response = reqwest::get(&endpoint).await.with_context(|| format!("Failed to fetch from {endpoint}"))?;
250
251 if response.status().is_success() {
252 response.json().await.with_context(|| format!("Failed to parse JSON response from {endpoint}"))
253 } else {
254 let is_json = response
256 .headers()
257 .get(http::header::CONTENT_TYPE)
258 .and_then(|ct| ct.to_str().ok())
259 .map(|ct| ct.contains("json"))
260 .unwrap_or(false);
261
262 if is_json {
263 let error: RestError = response
265 .json()
266 .await
267 .with_context(|| format!("Failed to parse JSON error response from {endpoint}"))?;
268 Err(error.parse().context(format!("Failed to fetch from {endpoint}")))
269 } else {
270 let error =
271 response.text().await.with_context(|| format!("Failed to read error message {endpoint}"))?;
272 Err(anyhow!(error).context(format!("Failed to fetch from {endpoint}")))
273 }
274 }
275 }
276}
277
278#[cfg(test)]
279mod tests {
280 use crate::Query;
281
282 use snarkvm_console::network::TestnetV0;
283 use snarkvm_ledger_store::helpers::memory::BlockMemory;
284
285 use anyhow::Result;
286
287 type CurrentNetwork = TestnetV0;
288 type CurrentQuery = Query<CurrentNetwork, BlockMemory<CurrentNetwork>>;
289
290 #[test]
296 fn test_rest_url_parse() -> Result<()> {
297 let noslash = "http://localhost:3030";
298 let withslash = format!("{noslash}/");
299 let route = "some/route";
300
301 let query = noslash.parse::<CurrentQuery>().unwrap();
302 let Query::REST(rest) = query else { panic!() };
303 assert_eq!(rest.base_url.path_and_query().unwrap().to_string(), "/");
304 assert_eq!(rest.base_url.to_string(), withslash);
305 assert_eq!(rest.build_endpoint(route)?, format!("{noslash}/testnet/{route}"));
306
307 let query = withslash.parse::<CurrentQuery>().unwrap();
308 let Query::REST(rest) = query else { panic!() };
309 assert_eq!(rest.base_url.path_and_query().unwrap().to_string(), "/");
310 assert_eq!(rest.base_url.to_string(), withslash);
311 assert_eq!(rest.build_endpoint(route)?, format!("{noslash}/testnet/{route}"));
312
313 Ok(())
314 }
315
316 #[test]
317 fn test_rest_url_with_colon_parse() {
318 let str = "http://myendpoint.addr/:var/foo/bar";
319 let query = str.parse::<CurrentQuery>().unwrap();
320
321 let Query::REST(rest) = query else { panic!() };
322 assert_eq!(rest.base_url.to_string(), format!("{str}"));
323 assert_eq!(rest.base_url.path_and_query().unwrap().to_string(), "/:var/foo/bar");
324 }
325
326 #[test]
327 fn test_rest_url_parse_with_suffix() -> Result<()> {
328 let base = "http://localhost:3030/a/prefix/v2";
329 let route = "a/route";
330
331 let query = base.parse::<CurrentQuery>().unwrap();
333 let Query::REST(rest) = query else { panic!() };
334 assert_eq!(rest.build_endpoint(route)?, format!("{base}/testnet/{route}"));
335
336 let query = format!("{base}/").parse::<CurrentQuery>().unwrap();
338 let Query::REST(rest) = query else { panic!() };
339 assert_eq!(rest.build_endpoint(route)?, format!("{base}/testnet/{route}"));
340
341 Ok(())
342 }
343}