/ ledger / query / src / query.rs
query.rs
  1  // Copyright (c) 2019-2025 Alpha-Delta Network Inc.
  2  // This file is part of the deltavm library.
  3  
  4  // Licensed under the Apache License, Version 2.0 (the "License");
  5  // you may not use this file except in compliance with the License.
  6  // You may obtain a copy of the License at:
  7  
  8  // http://www.apache.org/licenses/LICENSE-2.0
  9  
 10  // Unless required by applicable law or agreed to in writing, software
 11  // distributed under the License is distributed on an "AS IS" BASIS,
 12  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 13  // See the License for the specific language governing permissions and
 14  // limitations under the License.
 15  
 16  use crate::QueryTrait;
 17  
 18  use deltavm_console::{
 19      network::prelude::*,
 20      program::{ProgramID, StatePath},
 21      types::Field,
 22  };
 23  use deltavm_ledger_block::Transaction;
 24  use deltavm_ledger_store::{BlockStorage, BlockStore};
 25  use deltavm_synthesizer_program::Program;
 26  
 27  use anyhow::{Context, Result};
 28  // ureq re-exports the `http` crate.
 29  use ureq::http;
 30  
 31  mod static_;
 32  pub use static_::StaticQuery;
 33  
 34  mod rest;
 35  pub use rest::RestQuery;
 36  
 37  /// Make the REST error type available public as it can be used for any API endpoint.
 38  pub use rest::RestError;
 39  
 40  /// Allows inspecting the state of the blockstain using either local state or a remote endpoint.
 41  #[derive(Clone)]
 42  pub enum Query<N: Network, B: BlockStorage<N>> {
 43      /// Query state in a local block store.
 44      VM(BlockStore<N, B>),
 45      /// Query state using a node's REST API.
 46      REST(RestQuery<N>),
 47      // Return static state for testing and performance.
 48      STATIC(StaticQuery<N>),
 49  }
 50  
 51  /// Initialize the `Query` object from a local `BlockStore`.
 52  impl<N: Network, B: BlockStorage<N>> From<BlockStore<N, B>> for Query<N, B> {
 53      fn from(block_store: BlockStore<N, B>) -> Self {
 54          Self::VM(block_store)
 55      }
 56  }
 57  
 58  /// Initialize the `Query` object from a local `BlockStore`.
 59  impl<N: Network, B: BlockStorage<N>> From<&BlockStore<N, B>> for Query<N, B> {
 60      fn from(block_store: &BlockStore<N, B>) -> Self {
 61          Self::VM(block_store.clone())
 62      }
 63  }
 64  
 65  /// Initialize the `Query` object from an endpoint URL. The URI should point to a snarkOS node's REST API.
 66  impl<N: Network, B: BlockStorage<N>> From<http::Uri> for Query<N, B> {
 67      fn from(uri: http::Uri) -> Self {
 68          Self::REST(RestQuery::from(uri))
 69      }
 70  }
 71  
 72  /// Initialize the `Query` object from an endpoint URL (passed as a string). The URI should point to a snarkOS node's REST API.
 73  impl<N: Network, B: BlockStorage<N>> TryFrom<String> for Query<N, B> {
 74      type Error = anyhow::Error;
 75  
 76      fn try_from(string_representation: String) -> Result<Self> {
 77          Self::try_from(string_representation.as_str())
 78      }
 79  }
 80  
 81  /// Initialize the `Query` object from an endpoint URL (passed as a string). The URI should point to a snarkOS node's REST API.
 82  impl<N: Network, B: BlockStorage<N>> TryFrom<&String> for Query<N, B> {
 83      type Error = anyhow::Error;
 84  
 85      fn try_from(string_representation: &String) -> Result<Self> {
 86          Self::try_from(string_representation.as_str())
 87      }
 88  }
 89  
 90  /// Initialize the `Query` object from an endpoint URL (passed as a string). The URI should point to a snarkOS node's REST API.
 91  impl<N: Network, B: BlockStorage<N>> TryFrom<&str> for Query<N, B> {
 92      type Error = anyhow::Error;
 93  
 94      fn try_from(str_representation: &str) -> Result<Self> {
 95          str_representation.parse::<Self>()
 96      }
 97  }
 98  
 99  /// Initialize the `Query` object from an endpoint URL (passed as a string). The URI should point to a snarkOS node's REST API.
100  impl<N: Network, B: BlockStorage<N>> FromStr for Query<N, B> {
101      type Err = anyhow::Error;
102  
103      fn from_str(str_representation: &str) -> Result<Self> {
104          // A static query is represented as JSON and a valid URI does not start with `}`.
105          if str_representation.trim().starts_with('{') {
106              let static_query =
107                  str_representation.parse::<StaticQuery<N>>().with_context(|| "Failed to parse static query")?;
108              Ok(Self::STATIC(static_query))
109          } else {
110              let rest_query = RestQuery::from_str(str_representation).with_context(|| "Failed to parse query")?;
111              Ok(Self::REST(rest_query))
112          }
113      }
114  }
115  
116  #[cfg_attr(feature = "async", async_trait::async_trait(?Send))]
117  impl<N: Network, B: BlockStorage<N>> QueryTrait<N> for Query<N, B> {
118      /// Returns the current state root.
119      fn current_state_root(&self) -> Result<N::StateRoot> {
120          match self {
121              Self::VM(block_store) => Ok(block_store.current_state_root()),
122              Self::REST(query) => query.current_state_root(),
123              Self::STATIC(query) => query.current_state_root(),
124          }
125      }
126  
127      /// Returns the current state root.
128      #[cfg(feature = "async")]
129      async fn current_state_root_async(&self) -> Result<N::StateRoot> {
130          match self {
131              Self::VM(block_store) => Ok(block_store.current_state_root()),
132              Self::REST(query) => query.current_state_root_async().await,
133              Self::STATIC(query) => query.current_state_root_async().await,
134          }
135      }
136  
137      /// Returns a state path for the given `commitment`.
138      fn get_state_path_for_commitment(&self, commitment: &Field<N>) -> Result<StatePath<N>> {
139          match self {
140              Self::VM(block_store) => block_store.get_state_path_for_commitment(commitment),
141              Self::REST(query) => query.get_state_path_for_commitment(commitment),
142              Self::STATIC(query) => query.get_state_path_for_commitment(commitment),
143          }
144      }
145  
146      /// Returns a state path for the given `commitment`.
147      #[cfg(feature = "async")]
148      async fn get_state_path_for_commitment_async(&self, commitment: &Field<N>) -> Result<StatePath<N>> {
149          match self {
150              Self::VM(block_store) => block_store.get_state_path_for_commitment(commitment),
151              Self::REST(query) => query.get_state_path_for_commitment_async(commitment).await,
152              Self::STATIC(query) => query.get_state_path_for_commitment_async(commitment).await,
153          }
154      }
155  
156      /// Returns a list of state paths for the given list of `commitment`s.
157      fn get_state_paths_for_commitments(&self, commitments: &[Field<N>]) -> Result<Vec<StatePath<N>>> {
158          // Return an empty vector if there are no commitments.
159          if commitments.is_empty() {
160              return Ok(vec![]);
161          }
162  
163          match self {
164              Self::VM(block_store) => block_store.get_state_paths_for_commitments(commitments),
165              Self::REST(query) => query.get_state_paths_for_commitments(commitments),
166              Self::STATIC(query) => query.get_state_paths_for_commitments(commitments),
167          }
168      }
169  
170      /// Returns a list of state paths for the given list of `commitment`s.
171      #[cfg(feature = "async")]
172      async fn get_state_paths_for_commitments_async(&self, commitments: &[Field<N>]) -> Result<Vec<StatePath<N>>> {
173          match self {
174              Self::VM(block_store) => block_store.get_state_paths_for_commitments(commitments),
175              Self::REST(query) => query.get_state_paths_for_commitments_async(commitments).await,
176              Self::STATIC(query) => query.get_state_paths_for_commitments(commitments),
177          }
178      }
179  
180      /// Returns a state path for the given `commitment`.
181      fn current_block_height(&self) -> Result<u32> {
182          match self {
183              Self::VM(block_store) => Ok(block_store.max_height().unwrap_or_default()),
184              Self::REST(query) => query.current_block_height(),
185              Self::STATIC(query) => query.current_block_height(),
186          }
187      }
188  
189      /// Returns a state path for the given `commitment`.
190      #[cfg(feature = "async")]
191      async fn current_block_height_async(&self) -> Result<u32> {
192          match self {
193              Self::VM(block_store) => Ok(block_store.max_height().unwrap_or_default()),
194              Self::REST(query) => query.current_block_height_async().await,
195              Self::STATIC(query) => query.current_block_height_async().await,
196          }
197      }
198  }
199  
200  impl<N: Network, B: BlockStorage<N>> Query<N, B> {
201      /// Returns the transaction for the given transaction ID.
202      pub fn get_transaction(&self, transaction_id: &N::TransactionID) -> Result<Transaction<N>> {
203          match self {
204              Self::VM(block_store) => {
205                  let txn = block_store.get_transaction(transaction_id)?;
206                  txn.ok_or_else(|| anyhow!("Transaction {transaction_id} not in local storage"))
207              }
208              Self::REST(query) => query.get_transaction(transaction_id),
209              Self::STATIC(_query) => bail!("get_transaction is not supported by StaticQuery"),
210          }
211      }
212  
213      /// Returns the transaction for the given transaction ID.
214      #[cfg(feature = "async")]
215      pub async fn get_transaction_async(&self, transaction_id: &N::TransactionID) -> Result<Transaction<N>> {
216          match self {
217              Self::VM(block_store) => {
218                  let txn = block_store.get_transaction(transaction_id)?;
219                  txn.ok_or_else(|| anyhow!("Transaction {transaction_id} not in local storage"))
220              }
221              Self::REST(query) => query.get_transaction_async(transaction_id).await,
222              Self::STATIC(_query) => bail!("get_transaction is not supported by StaticQuery"),
223          }
224      }
225  
226      /// Returns the program for the given program ID.
227      pub fn get_program(&self, program_id: &ProgramID<N>) -> Result<Program<N>> {
228          match self {
229              Self::VM(block_store) => block_store
230                  .get_latest_program(program_id)?
231                  .ok_or_else(|| anyhow!("Program {program_id} not found in storage")),
232              Self::REST(query) => query.get_program(program_id),
233              Self::STATIC(_query) => bail!("get_program is not supported by StaticQuery"),
234          }
235      }
236  
237      /// Returns the program for the given program ID.
238      #[cfg(feature = "async")]
239      pub async fn get_program_async(&self, program_id: &ProgramID<N>) -> Result<Program<N>> {
240          match self {
241              Self::VM(block_store) => block_store
242                  .get_latest_program(program_id)?
243                  .with_context(|| format!("Program {program_id} not found in storage")),
244              Self::REST(query) => query.get_program_async(program_id).await,
245              Self::STATIC(_query) => bail!("get_program_async is not supported by StaticQuery"),
246          }
247      }
248  }
249  
250  #[cfg(test)]
251  mod tests {
252      use super::*;
253  
254      use deltavm_console::network::TestnetV0;
255      use deltavm_ledger_store::helpers::memory::BlockMemory;
256  
257      type CurrentNetwork = TestnetV0;
258      type CurrentQuery = Query<CurrentNetwork, BlockMemory<CurrentNetwork>>;
259  
260      #[test]
261      fn test_static_query_parse() {
262          let json = r#"{"state_root": "sr1dz06ur5spdgzkguh4pr42mvft6u3nwsg5drh9rdja9v8jpcz3czsls9geg", "height": 14}"#
263              .to_string();
264          let query = CurrentQuery::try_from(json).unwrap();
265  
266          assert!(matches!(query, Query::STATIC(_)));
267      }
268  
269      #[test]
270      fn test_static_query_parse_invalid() {
271          let json = r#"{"invalid_key": "sr1dz06ur5spdgzkguh4pr42mvft6u3nwsg5drh9rdja9v8jpcz3czsls9geg", "height": 14}"#
272              .to_string();
273          let result = json.parse::<CurrentQuery>();
274  
275          assert!(result.is_err());
276      }
277  
278      #[test]
279      fn test_rest_url_parse_invalid_scheme() {
280          let str = "ftp://localhost:3030";
281          let result = CurrentQuery::try_from(str);
282  
283          assert!(result.is_err());
284      }
285  
286      #[test]
287      fn test_rest_url_parse_invalid_host() {
288          let str = "http://:3030";
289          let result = CurrentQuery::try_from(str);
290  
291          assert!(result.is_err());
292      }
293  }