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()
82 && *scheme != uri::Scheme::HTTP
83 && *scheme != uri::Scheme::HTTPS
84 {
85 bail!("Invalid scheme in URL: {scheme}");
86 }
87
88 if let Some(s) = base_url.host()
89 && s.is_empty()
90 {
91 bail!("Invalid URL for REST endpoint. Empty hostname given.");
92 } else if base_url.host().is_none() {
93 bail!("Invalid URL for REST endpoint. No hostname given.");
94 }
95
96 if base_url.query().is_some() {
97 bail!("Base URL for REST endpoints cannot contain a query");
98 }
99
100 Ok(Self { base_url, _marker: Default::default() })
101 }
102}
103
104#[cfg_attr(feature = "async", async_trait::async_trait(?Send))]
105impl<N: Network> QueryTrait<N> for RestQuery<N> {
106 fn current_state_root(&self) -> Result<N::StateRoot> {
108 self.get_request("stateRoot/latest")
109 }
110
111 #[cfg(feature = "async")]
113 async fn current_state_root_async(&self) -> Result<N::StateRoot> {
114 self.get_request_async("stateRoot/latest").await
115 }
116
117 fn get_state_path_for_commitment(&self, commitment: &Field<N>) -> Result<StatePath<N>> {
119 self.get_request(&format!("statePath/{commitment}"))
120 }
121
122 #[cfg(feature = "async")]
124 async fn get_state_path_for_commitment_async(&self, commitment: &Field<N>) -> Result<StatePath<N>> {
125 self.get_request_async(&format!("statePath/{commitment}")).await
126 }
127
128 fn get_state_paths_for_commitments(&self, commitments: &[Field<N>]) -> Result<Vec<StatePath<N>>> {
130 let commitments_string = commitments.iter().map(|cm| cm.to_string()).collect::<Vec<_>>().join(",");
132 self.get_request(&format!("statePaths?commitments={commitments_string}"))
133 }
134
135 #[cfg(feature = "async")]
137 async fn get_state_paths_for_commitments_async(&self, commitments: &[Field<N>]) -> Result<Vec<StatePath<N>>> {
138 let commitments_string = commitments.iter().map(|cm| cm.to_string()).collect::<Vec<_>>().join(",");
140 self.get_request_async(&format!("statePaths?commitments={commitments_string}")).await
141 }
142
143 fn current_block_height(&self) -> Result<u32> {
145 self.get_request("block/height/latest")
146 }
147
148 #[cfg(feature = "async")]
150 async fn current_block_height_async(&self) -> Result<u32> {
151 self.get_request_async("block/height/latest").await
152 }
153}
154
155impl<N: Network> RestQuery<N> {
156 const API_VERSION: &str = "v2";
158
159 pub fn get_transaction(&self, transaction_id: &N::TransactionID) -> Result<Transaction<N>> {
161 self.get_request(&format!("transaction/{transaction_id}"))
162 }
163
164 #[cfg(feature = "async")]
166 pub async fn get_transaction_async(&self, transaction_id: &N::TransactionID) -> Result<Transaction<N>> {
167 self.get_request_async(&format!("transaction/{transaction_id}")).await
168 }
169
170 pub fn get_program(&self, program_id: &ProgramID<N>) -> Result<Program<N>> {
172 self.get_request(&format!("program/{program_id}"))
173 }
174
175 #[cfg(feature = "async")]
177 pub async fn get_program_async(&self, program_id: &ProgramID<N>) -> Result<Program<N>> {
178 self.get_request_async(&format!("program/{program_id}")).await
179 }
180
181 fn build_endpoint(&self, route: &str) -> Result<String> {
187 ensure!(!route.starts_with('/'), "path cannot start with a slash");
189
190 let path = if self.base_url.path().ends_with('/') {
193 format!(
194 "{base_url}{api_version}/{network}/{route}",
195 base_url = self.base_url,
196 api_version = Self::API_VERSION,
197 network = N::SHORT_NAME
198 )
199 } else {
200 format!(
201 "{base_url}/{api_version}/{network}/{route}",
202 base_url = self.base_url,
203 api_version = Self::API_VERSION,
204 network = N::SHORT_NAME
205 )
206 };
207
208 Ok(path)
209 }
210
211 fn get_request<T: DeserializeOwned>(&self, route: &str) -> Result<T> {
216 let endpoint = self.build_endpoint(route)?;
217 let mut response = ureq::get(&endpoint)
218 .config()
219 .http_status_as_error(false)
220 .build()
221 .call()
222 .with_context(|| format!("Failed to fetch from {endpoint}"))?;
224
225 if response.status().is_success() {
226 response.body_mut().read_json().with_context(|| "Failed to parse JSON response")
227 } else {
228 let error: RestError =
230 response.body_mut().read_json().with_context(|| "Failed to parse JSON error response")?;
231 Err(error.parse().context(format!("Failed to fetch from {endpoint}")))
232 }
233 }
234
235 #[cfg(feature = "async")]
240 async fn get_request_async<T: DeserializeOwned>(&self, route: &str) -> Result<T> {
241 let endpoint = self.build_endpoint(route)?;
242 let response = reqwest::get(&endpoint).await.with_context(|| format!("Failed to fetch from {endpoint}"))?;
243
244 if response.status().is_success() {
245 response.json().await.with_context(|| "Failed to parse JSON response")
246 } else {
247 let error: RestError = response.json().await.with_context(|| "Failed to parse JSON error response")?;
249 Err(error.parse().context(format!("Failed to fetch from {endpoint}")))
250 }
251 }
252}
253
254#[cfg(test)]
255mod tests {
256 use crate::Query;
257
258 use snarkvm_console::network::TestnetV0;
259 use snarkvm_ledger_store::helpers::memory::BlockMemory;
260
261 use anyhow::Result;
262
263 type CurrentNetwork = TestnetV0;
264 type CurrentQuery = Query<CurrentNetwork, BlockMemory<CurrentNetwork>>;
265
266 #[test]
272 fn test_rest_url_parse() -> Result<()> {
273 let noslash = "http://localhost:3030";
274 let withslash = format!("{noslash}/");
275 let route = "some/route";
276
277 let query = noslash.parse::<CurrentQuery>().unwrap();
278 let Query::REST(rest) = query else { panic!() };
279 assert_eq!(rest.base_url.path_and_query().unwrap().to_string(), "/");
280 assert_eq!(rest.base_url.to_string(), withslash);
281 assert_eq!(rest.build_endpoint(route)?, format!("{noslash}/v2/testnet/{route}"));
282
283 let query = withslash.parse::<CurrentQuery>().unwrap();
284 let Query::REST(rest) = query else { panic!() };
285 assert_eq!(rest.base_url.path_and_query().unwrap().to_string(), "/");
286 assert_eq!(rest.base_url.to_string(), withslash);
287 assert_eq!(rest.build_endpoint(route)?, format!("{noslash}/v2/testnet/{route}"));
288
289 Ok(())
290 }
291
292 #[test]
293 fn test_rest_url_with_colon_parse() {
294 let str = "http://myendpoint.addr/:var/foo/bar";
295 let query = str.parse::<CurrentQuery>().unwrap();
296
297 let Query::REST(rest) = query else { panic!() };
298 assert_eq!(rest.base_url.to_string(), format!("{str}"));
299 assert_eq!(rest.base_url.path_and_query().unwrap().to_string(), "/:var/foo/bar");
300 }
301
302 #[test]
303 fn test_rest_url_parse_with_suffix() -> Result<()> {
304 let base = "http://localhost:3030/a/prefix";
305 let route = "a/route";
306 let query = base.parse::<CurrentQuery>().unwrap();
307
308 let Query::REST(rest) = query else { panic!() };
310 assert_eq!(rest.build_endpoint(route)?, format!("{base}/v2/testnet/{route}"));
311
312 let query = format!("{base}/").parse::<CurrentQuery>().unwrap();
314 let Query::REST(rest) = query else { panic!() };
315 assert_eq!(rest.build_endpoint(route)?, format!("{base}/v2/testnet/{route}"));
316
317 Ok(())
318 }
319}