Skip to main content

casper_rust_wasm_sdk/sdk/rpcs/
query_global_state.rs

1use crate::{
2    types::{
3        digest::Digest, identifier::global_state_identifier::GlobalStateIdentifier, key::Key,
4        path::Path, sdk_error::SdkError, stored_value::StoredValue, verbosity::Verbosity,
5    },
6    SDK,
7};
8use casper_client::{
9    cli::query_global_state as query_global_state_cli,
10    query_global_state as query_global_state_lib,
11    rpcs::results::QueryGlobalStateResult as _QueryGlobalStateResult, JsonRpcId, SuccessResponse,
12};
13#[cfg(all(feature = "js", target_arch = "wasm32"))]
14use gloo_utils::format::JsValueSerdeExt;
15use rand::RngExt;
16use serde::{Deserialize, Serialize};
17#[cfg(feature = "js")]
18use wasm_bindgen::prelude::*;
19
20// Define a struct to wrap the QueryGlobalStateResult
21#[derive(Debug, Deserialize, Clone, Serialize)]
22#[cfg_attr(feature = "js", wasm_bindgen)]
23pub struct QueryGlobalStateResult(_QueryGlobalStateResult);
24
25impl From<QueryGlobalStateResult> for _QueryGlobalStateResult {
26    fn from(result: QueryGlobalStateResult) -> Self {
27        result.0
28    }
29}
30
31impl From<_QueryGlobalStateResult> for QueryGlobalStateResult {
32    fn from(result: _QueryGlobalStateResult) -> Self {
33        QueryGlobalStateResult(result)
34    }
35}
36
37impl QueryGlobalStateResult {
38    /// Gets the typed stored value wrapper.
39    pub fn stored_value_typed(&self) -> StoredValue {
40        self.0.stored_value.clone().into()
41    }
42}
43
44#[cfg(all(feature = "js", target_arch = "wasm32"))]
45#[cfg_attr(feature = "js", wasm_bindgen)]
46impl QueryGlobalStateResult {
47    /// Gets the API version as a JsValue.
48    #[cfg_attr(feature = "js", wasm_bindgen(getter))]
49    #[cfg(feature = "js")]
50    pub fn api_version(&self) -> JsValue {
51        JsValue::from_serde(&self.0.api_version).unwrap()
52    }
53
54    /// Gets the block header as a JsValue.
55    #[cfg_attr(feature = "js", wasm_bindgen(getter))]
56    #[cfg(feature = "js")]
57    pub fn block_header(&self) -> JsValue {
58        JsValue::from_serde(&self.0.block_header).unwrap()
59    }
60
61    /// Gets the stored value as a JsValue.
62    #[cfg_attr(feature = "js", wasm_bindgen(getter))]
63    #[cfg(feature = "js")]
64    pub fn stored_value(&self) -> JsValue {
65        JsValue::from_serde(&self.0.stored_value).unwrap()
66    }
67
68    /// Gets the typed stored value wrapper.
69    #[cfg_attr(feature = "js", wasm_bindgen(js_name = "storedValueTyped"))]
70    pub fn stored_value_typed_js(&self) -> StoredValue {
71        self.stored_value_typed()
72    }
73
74    /// Gets the Merkle proof as a string.
75    #[cfg_attr(feature = "js", wasm_bindgen(getter))]
76    pub fn merkle_proof(&self) -> String {
77        self.0.merkle_proof.clone()
78    }
79
80    /// Converts the QueryGlobalStateResult to a JsValue.
81    #[cfg_attr(feature = "js", wasm_bindgen(js_name = "toJson"))]
82    #[cfg(feature = "js")]
83    pub fn to_json(&self) -> JsValue {
84        JsValue::from_serde(&self.0).unwrap_or(JsValue::null())
85    }
86}
87
88/// Options for the `query_global_state` method.
89#[derive(Debug, Deserialize, Clone, Default, Serialize)]
90#[cfg_attr(
91    feature = "js",
92    wasm_bindgen(js_name = "queryGlobalStateOptions", getter_with_clone)
93)]
94pub struct QueryGlobalStateOptions {
95    pub global_state_identifier: Option<GlobalStateIdentifier>,
96    pub state_root_hash_as_string: Option<String>,
97    pub state_root_hash: Option<Digest>,
98    pub maybe_block_id_as_string: Option<String>,
99    pub key_as_string: Option<String>,
100    pub key: Option<Key>,
101    pub path_as_string: Option<String>,
102    pub path: Option<Path>,
103    pub rpc_address: Option<String>,
104    pub verbosity: Option<Verbosity>,
105}
106
107#[cfg(all(feature = "js", target_arch = "wasm32"))]
108#[cfg_attr(feature = "js", wasm_bindgen)]
109impl SDK {
110    /// Parses query global state options from a JsValue.
111    ///
112    /// # Arguments
113    ///
114    /// * `options` - A JsValue containing query global state options to be parsed.
115    ///
116    /// # Returns
117    ///
118    /// Parsed query global state options as a `QueryGlobalStateOptions` struct.
119    #[cfg(feature = "js")]
120    pub fn query_global_state_options(
121        &self,
122        options: JsValue,
123    ) -> Result<QueryGlobalStateOptions, JsError> {
124        options
125            .into_serde::<QueryGlobalStateOptions>()
126            .map_err(|err| JsError::new(&format!("Error deserializing options: {err:?}")))
127    }
128
129    /// Retrieves global state information using the provided options.
130    ///
131    /// # Arguments
132    ///
133    /// * `options` - An optional `QueryGlobalStateOptions` struct containing retrieval options.
134    ///
135    /// # Returns
136    ///
137    /// A `Result` containing either a `QueryGlobalStateResult` or a `JsError` in case of an error.
138    ///
139    /// # Errors
140    ///
141    /// Returns a `JsError` if there is an error during the retrieval process.
142    #[cfg_attr(feature = "js", wasm_bindgen(js_name = "query_global_state"))]
143    #[cfg(feature = "js")]
144    pub async fn query_global_state_js_alias(
145        &self,
146        options: Option<QueryGlobalStateOptions>,
147    ) -> Result<QueryGlobalStateResult, JsError> {
148        match self.query_global_state_js_alias_params(options) {
149            Ok(params) => {
150                let result = self.query_global_state(params).await;
151                match result {
152                    Ok(data) => Ok(data.result.into()),
153                    Err(err) => {
154                        let err = &format!("Error occurred with {err:?}");
155                        Err(JsError::new(err))
156                    }
157                }
158            }
159            Err(err) => {
160                let err = &format!("Error building parameters: {err:?}");
161                Err(JsError::new(err))
162            }
163        }
164    }
165}
166
167#[cfg(all(feature = "js", target_arch = "wasm32"))]
168impl SDK {
169    /// Builds parameters for querying global state based on the provided options.
170    ///
171    /// # Arguments
172    ///
173    /// * `options` - An optional `QueryGlobalStateOptions` struct containing retrieval options.
174    ///
175    /// # Returns
176    ///
177    /// A `Result` containing either a `QueryGlobalStateParams` struct or a `SdkError` in case of an error.
178    pub fn query_global_state_js_alias_params(
179        &self,
180        options: Option<QueryGlobalStateOptions>,
181    ) -> Result<QueryGlobalStateParams, SdkError> {
182        let QueryGlobalStateOptions {
183            global_state_identifier,
184            state_root_hash_as_string,
185            state_root_hash,
186            maybe_block_id_as_string,
187            key_as_string,
188            key,
189            path_as_string,
190            path,
191            verbosity,
192            rpc_address,
193        } = options.unwrap_or_default();
194
195        let key = if let Some(key) = key {
196            Some(KeyIdentifierInput::Key(key))
197        } else if let Some(key_as_string) = key_as_string {
198            Some(KeyIdentifierInput::String(key_as_string))
199        } else {
200            let err_msg = "Error: Missing Key as string or Key".to_string();
201            return Err(SdkError::InvalidArgument {
202                context: "query_global_state",
203                error: err_msg,
204            });
205        };
206
207        let maybe_path = if let Some(path) = path {
208            Some(PathIdentifierInput::Path(path))
209        } else if let Some(path_str) = path_as_string {
210            if path_str.is_empty() {
211                None
212            } else {
213                Some(PathIdentifierInput::String(path_str))
214            }
215        } else {
216            None
217        };
218
219        let query_params = if let Some(hash) = state_root_hash {
220            let state_root_hash_str = hash.to_string();
221            QueryGlobalStateParams {
222                key: key.unwrap(),
223                path: maybe_path.clone(),
224                maybe_global_state_identifier: global_state_identifier.clone(),
225                state_root_hash: if state_root_hash_str.is_empty() {
226                    None
227                } else {
228                    Some(state_root_hash_str)
229                },
230                maybe_block_id: None,
231                verbosity,
232                rpc_address,
233            }
234        } else if let Some(hash) = state_root_hash_as_string {
235            let state_root_hash_str = hash.to_string();
236            QueryGlobalStateParams {
237                key: key.unwrap(),
238                path: maybe_path.clone(),
239                maybe_global_state_identifier: global_state_identifier.clone(),
240                state_root_hash: if state_root_hash_str.is_empty() {
241                    None
242                } else {
243                    Some(state_root_hash_str)
244                },
245                maybe_block_id: None,
246                verbosity,
247                rpc_address,
248            }
249        } else if let Some(maybe_block_id_as_string) = maybe_block_id_as_string {
250            QueryGlobalStateParams {
251                key: key.unwrap(),
252                path: maybe_path.clone(),
253                maybe_global_state_identifier: global_state_identifier.clone(),
254                state_root_hash: None,
255                maybe_block_id: Some(maybe_block_id_as_string),
256                verbosity,
257                rpc_address,
258            }
259        } else {
260            QueryGlobalStateParams {
261                key: key.unwrap(),
262                path: maybe_path.clone(),
263                maybe_global_state_identifier: global_state_identifier.clone(),
264                state_root_hash: None,
265                maybe_block_id: None,
266                verbosity,
267                rpc_address,
268            }
269        };
270        Ok(query_params)
271    }
272}
273
274/// Enum to represent input for KeyIdentifier.
275#[derive(Debug, Clone)]
276pub enum KeyIdentifierInput {
277    Key(Key),
278    String(String),
279}
280
281/// Enum to represent input for PathIdentifier.
282#[derive(Debug, Clone)]
283pub enum PathIdentifierInput {
284    Path(Path),
285    String(String),
286}
287
288/// Struct to store parameters for querying global state.
289#[derive(Debug)]
290pub struct QueryGlobalStateParams {
291    pub key: KeyIdentifierInput,
292    pub path: Option<PathIdentifierInput>,
293    pub maybe_global_state_identifier: Option<GlobalStateIdentifier>,
294    pub state_root_hash: Option<String>,
295    pub maybe_block_id: Option<String>,
296    pub rpc_address: Option<String>,
297    pub verbosity: Option<Verbosity>,
298}
299
300impl SDK {
301    /// Retrieves global state information based on the provided parameters.
302    ///
303    /// # Arguments
304    ///
305    /// * `query_params` - A `QueryGlobalStateParams` struct containing query parameters.
306    ///
307    /// # Returns
308    ///
309    /// A `Result` containing either a `SuccessResponse<_QueryGlobalStateResult>` or a `SdkError` in case of an error.
310    pub async fn query_global_state(
311        &self,
312        query_params: QueryGlobalStateParams,
313    ) -> Result<SuccessResponse<_QueryGlobalStateResult>, SdkError> {
314        //log("query_global_state!");
315
316        let QueryGlobalStateParams {
317            key,
318            path,
319            maybe_global_state_identifier,
320            state_root_hash,
321            maybe_block_id,
322            verbosity,
323            rpc_address,
324        } = query_params;
325
326        let key = match key {
327            KeyIdentifierInput::Key(key) => Some(key),
328            KeyIdentifierInput::String(key_string) => Key::from_formatted_str(&key_string).ok(),
329        };
330
331        if key.is_none() {
332            let err = "Error: Missing key from formatted string".to_string();
333            return Err(SdkError::InvalidArgument {
334                context: "query_global_state",
335                error: err,
336            });
337        }
338
339        let path = if let Some(path) = path {
340            let path = match path {
341                PathIdentifierInput::Path(path) => path,
342                PathIdentifierInput::String(path_string) => Path::from(path_string),
343            };
344            Some(path)
345        } else {
346            None
347        };
348
349        let path_str: String = match path.clone() {
350            Some(p) => p.to_string(),
351            None => String::new(),
352        };
353        let random_id = rand::rng().random::<u64>().to_string();
354        if let Some(maybe_global_state_identifier) = maybe_global_state_identifier {
355            let path = match path {
356                Some(path) if path.is_empty() => Vec::new(),
357                Some(path) => path.into(),
358                None => Vec::new(),
359            };
360            let random_id = JsonRpcId::from(random_id);
361            query_global_state_lib(
362                random_id,
363                &self.get_rpc_address(rpc_address),
364                self.get_verbosity(verbosity).into(),
365                Some(maybe_global_state_identifier.into()),
366                key.unwrap().into(),
367                path,
368            )
369            .await
370            .map_err(SdkError::from)
371        } else if let Some(state_root_hash) = state_root_hash {
372            let random_id = rand::rng().random::<u64>().to_string();
373            query_global_state_cli(
374                &random_id,
375                &self.get_rpc_address(rpc_address),
376                self.get_verbosity(verbosity).into(),
377                "",
378                &state_root_hash,
379                &key.unwrap().to_formatted_string(),
380                &path_str,
381            )
382            .await
383            .map_err(SdkError::from)
384        } else if let Some(maybe_block_id) = maybe_block_id {
385            let random_id = rand::rng().random::<u64>().to_string();
386            query_global_state_cli(
387                &random_id,
388                &self.get_rpc_address(rpc_address),
389                self.get_verbosity(verbosity).into(),
390                &maybe_block_id,
391                "",
392                &key.unwrap().to_formatted_string(),
393                &path_str,
394            )
395            .await
396            .map_err(SdkError::from)
397        } else {
398            let state_root_hash = self
399                .get_state_root_hash(None, None, Some(self.get_rpc_address(rpc_address.clone())))
400                .await;
401
402            let state_root_hash_as_string: String = match state_root_hash {
403                Ok(state_root_hash) => {
404                    let state_root_hash: Digest =
405                        state_root_hash.result.state_root_hash.unwrap().into();
406                    state_root_hash.to_string()
407                }
408                Err(_) => "".to_string(),
409            };
410            let random_id = rand::rng().random::<u64>().to_string();
411            query_global_state_cli(
412                &random_id,
413                &self.get_rpc_address(rpc_address),
414                self.get_verbosity(verbosity).into(),
415                "",
416                &state_root_hash_as_string,
417                &key.unwrap().to_formatted_string(),
418                &path_str,
419            )
420            .await
421            .map_err(SdkError::from)
422        }
423    }
424}
425
426#[cfg(test)]
427mod tests {
428    use super::*;
429    use crate::{helpers::public_key_from_secret_key, types::public_key::PublicKey};
430    use sdk_tests::tests::helpers::{get_network_constants, get_user_secret_key};
431
432    fn get_key_input() -> KeyIdentifierInput {
433        let secret_key = get_user_secret_key(None).unwrap();
434        let account = public_key_from_secret_key(&secret_key).unwrap();
435        let public_key = PublicKey::new(&account).unwrap();
436        KeyIdentifierInput::String(public_key.to_account_hash().to_formatted_string())
437    }
438
439    #[tokio::test]
440    async fn test_query_global_state_with_none_values() {
441        // Arrange
442        let sdk = SDK::new(None, None, None);
443        let error_message = "failed to parse";
444
445        // Act
446        let result = sdk
447            .query_global_state(QueryGlobalStateParams {
448                key: get_key_input(),
449                path: None,
450                maybe_global_state_identifier: None,
451                state_root_hash: None,
452                maybe_block_id: None,
453                verbosity: None,
454                rpc_address: None,
455            })
456            .await;
457
458        // Assert
459        assert!(result.is_err());
460        let err_string = result.err().unwrap().to_string();
461        assert!(err_string.contains(error_message));
462    }
463
464    #[tokio::test]
465    async fn test_query_global_state_with_missing_key() {
466        // Arrange
467        let sdk = SDK::new(None, None, None);
468        let error_message =
469            "Invalid argument 'query_global_state': Error: Missing key from formatted string";
470
471        // Act
472        let result = sdk
473            .query_global_state(QueryGlobalStateParams {
474                key: KeyIdentifierInput::String(String::new()),
475                path: None,
476                maybe_global_state_identifier: None,
477                state_root_hash: None,
478                maybe_block_id: None,
479                verbosity: None,
480                rpc_address: None,
481            })
482            .await;
483
484        // Assert
485        assert!(result.is_err());
486        let err_string = result.err().unwrap().to_string();
487        assert!(err_string.contains(error_message));
488    }
489
490    #[tokio::test]
491    async fn test_query_global_state_with_global_state_identifier() {
492        // Arrange
493        let sdk = SDK::new(None, None, None);
494        let global_state_identifier = GlobalStateIdentifier::from_block_height(1);
495        let verbosity = Some(Verbosity::High);
496        let (rpc_address, _, _, _, _) = get_network_constants();
497
498        // Act
499        let result = sdk
500            .query_global_state(QueryGlobalStateParams {
501                key: get_key_input(),
502                path: None,
503                maybe_global_state_identifier: Some(global_state_identifier.clone()),
504                state_root_hash: None,
505                maybe_block_id: None,
506                verbosity,
507                rpc_address: Some(rpc_address),
508            })
509            .await;
510
511        // Assert
512        assert!(result.is_ok());
513    }
514
515    #[tokio::test]
516    async fn test_query_global_state_with_state_root_hash() {
517        // Arrange
518        let sdk = SDK::new(None, None, None);
519        let verbosity = Some(Verbosity::High);
520        let (rpc_address, _, _, _, _) = get_network_constants();
521        let state_root_hash: Digest = sdk
522            .get_state_root_hash(None, verbosity, Some(rpc_address.clone()))
523            .await
524            .unwrap()
525            .result
526            .state_root_hash
527            .unwrap()
528            .into();
529        // Act
530        let result = sdk
531            .query_global_state(QueryGlobalStateParams {
532                key: get_key_input(),
533                path: None,
534                maybe_global_state_identifier: None,
535                state_root_hash: Some(state_root_hash.to_string()),
536                maybe_block_id: None,
537                verbosity,
538                rpc_address: Some(rpc_address),
539            })
540            .await;
541
542        // Assert
543        assert!(result.is_ok());
544    }
545
546    #[tokio::test]
547    async fn test_query_global_state_with_block_id() {
548        // Arrange
549        let sdk = SDK::new(None, None, None);
550        let verbosity = Some(Verbosity::High);
551        let (rpc_address, _, _, _, _) = get_network_constants();
552
553        // Act
554        let result = sdk
555            .query_global_state(QueryGlobalStateParams {
556                key: get_key_input(),
557                path: None,
558                maybe_global_state_identifier: None,
559                state_root_hash: None,
560                maybe_block_id: Some("1".to_string()),
561                verbosity,
562                rpc_address: Some(rpc_address),
563            })
564            .await;
565
566        // Assert
567        assert!(result.is_ok());
568    }
569
570    #[tokio::test]
571    async fn test_query_global_state_typed_account_access() {
572        let sdk = SDK::new(None, None, None);
573        let verbosity = Some(Verbosity::High);
574        let (rpc_address, _, _, _, _) = get_network_constants();
575        let global_state_identifier = GlobalStateIdentifier::from_block_height(1);
576
577        let result = sdk
578            .query_global_state(QueryGlobalStateParams {
579                key: get_key_input(),
580                path: None,
581                maybe_global_state_identifier: Some(global_state_identifier),
582                state_root_hash: None,
583                maybe_block_id: None,
584                verbosity,
585                rpc_address: Some(rpc_address),
586            })
587            .await
588            .expect("query_global_state");
589
590        let stored = StoredValue::from(result.result.stored_value.clone());
591        assert_eq!(stored.variant(), "Account");
592        let account = stored.as_account().expect("typed Account");
593        assert!(!account.account_hash().to_formatted_string().is_empty());
594        assert!(!account.main_purse().to_formatted_string().is_empty());
595        // JSON escape hatch remains available on the parent result path via serde.
596        let json = serde_json::to_value(&result.result.stored_value).expect("json");
597        assert!(json.get("Account").is_some());
598    }
599}