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 }