snarkvm_ledger_query/
query.rs

1// Copyright (c) 2019-2025 Provable Inc.
2// This file is part of the snarkVM 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
16use crate::QueryTrait;
17
18use snarkvm_console::{
19    network::prelude::*,
20    program::{ProgramID, StatePath},
21    types::Field,
22};
23use snarkvm_ledger_block::Transaction;
24use snarkvm_ledger_store::{BlockStorage, BlockStore};
25use snarkvm_synthesizer_program::Program;
26
27use anyhow::{Context, Result};
28// ureq re-exports the `http` crate.
29use ureq::http;
30
31mod static_;
32pub use static_::StaticQuery;
33
34mod rest;
35pub use rest::RestQuery;
36
37/// Make the REST error type available public as it can be used for any API endpoint.
38pub use rest::RestError;
39
40/// Allows inspecting the state of the blockstain using either local state or a remote endpoint.
41#[derive(Clone)]
42pub 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`.
52impl<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`.
59impl<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.
66impl<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.
73impl<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.
82impl<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.
91impl<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.
100impl<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))]
117impl<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
200impl<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)]
251mod tests {
252    use super::*;
253
254    use snarkvm_console::network::TestnetV0;
255    use snarkvm_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}