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