Skip to main content

casper_rust_wasm_sdk/sdk/rpcs/
get_balance.rs

1use crate::{
2    types::{
3        digest::{Digest, ToDigest},
4        sdk_error::SdkError,
5        uref::URef,
6        verbosity::Verbosity,
7    },
8    SDK,
9};
10use casper_client::{
11    cli::get_balance as get_balance_cli, get_balance as get_balance_lib,
12    rpcs::results::GetBalanceResult as _GetBalanceResult, JsonRpcId, SuccessResponse,
13};
14#[cfg(all(feature = "js", target_arch = "wasm32"))]
15use gloo_utils::format::JsValueSerdeExt;
16use rand::RngExt;
17#[cfg(all(feature = "js", target_arch = "wasm32"))]
18use serde::{Deserialize, Serialize};
19#[cfg(all(feature = "js", target_arch = "wasm32"))]
20use wasm_bindgen::prelude::*;
21
22// Define a struct to wrap the GetBalanceResult
23#[cfg(all(feature = "js", target_arch = "wasm32"))]
24#[derive(Debug, Deserialize, Clone, Serialize)]
25#[cfg_attr(feature = "js", wasm_bindgen)]
26pub struct GetBalanceResult(_GetBalanceResult);
27
28#[cfg(all(feature = "js", target_arch = "wasm32"))]
29impl From<GetBalanceResult> for _GetBalanceResult {
30    fn from(result: GetBalanceResult) -> Self {
31        result.0
32    }
33}
34
35#[cfg(all(feature = "js", target_arch = "wasm32"))]
36impl From<_GetBalanceResult> for GetBalanceResult {
37    fn from(result: _GetBalanceResult) -> Self {
38        GetBalanceResult(result)
39    }
40}
41
42#[cfg(all(feature = "js", target_arch = "wasm32"))]
43#[cfg_attr(feature = "js", wasm_bindgen)]
44impl GetBalanceResult {
45    /// Gets the API version as a JsValue.
46    #[cfg_attr(feature = "js", wasm_bindgen(getter))]
47    #[cfg(feature = "js")]
48    pub fn api_version(&self) -> JsValue {
49        JsValue::from_serde(&self.0.api_version).unwrap()
50    }
51
52    /// Gets the balance value as a JsValue.
53    #[cfg_attr(feature = "js", wasm_bindgen(getter))]
54    #[cfg(feature = "js")]
55    pub fn balance_value(&self) -> JsValue {
56        JsValue::from_serde(&self.0.balance_value).unwrap()
57    }
58
59    /// Gets the Merkle proof as a string.
60    #[cfg_attr(feature = "js", wasm_bindgen(getter))]
61    pub fn merkle_proof(&self) -> String {
62        self.0.merkle_proof.clone()
63    }
64
65    /// Converts the GetBalanceResult to a JsValue.
66    #[cfg_attr(feature = "js", wasm_bindgen(js_name = "toJson"))]
67    #[cfg(feature = "js")]
68    pub fn to_json(&self) -> JsValue {
69        JsValue::from_serde(&self.0).unwrap_or(JsValue::null())
70    }
71}
72
73/// Options for the `get_balance` method.
74#[derive(Default, Debug, Deserialize, Clone, Serialize)]
75#[cfg(all(feature = "js", target_arch = "wasm32"))]
76#[cfg_attr(
77    feature = "js",
78    wasm_bindgen(js_name = "getBalanceOptions", getter_with_clone)
79)]
80pub struct GetBalanceOptions {
81    pub state_root_hash_as_string: Option<String>,
82    pub state_root_hash: Option<Digest>,
83    pub purse_uref_as_string: Option<String>,
84    pub purse_uref: Option<URef>,
85    pub rpc_address: Option<String>,
86    pub verbosity: Option<Verbosity>,
87}
88
89#[cfg(all(feature = "js", target_arch = "wasm32"))]
90#[cfg_attr(feature = "js", wasm_bindgen)]
91impl SDK {
92    /// Parses balance options from a JsValue.
93    ///
94    /// # Arguments
95    ///
96    /// * `options` - A JsValue containing balance options to be parsed.
97    ///
98    /// # Returns
99    ///
100    /// Parsed balance options as a `GetBalanceOptions` struct.
101    #[cfg(feature = "js")]
102    pub fn get_balance_options(&self, options: JsValue) -> Result<GetBalanceOptions, JsError> {
103        options
104            .into_serde::<GetBalanceOptions>()
105            .map_err(|err| JsError::new(&format!("Error deserializing options: {err:?}")))
106    }
107
108    /// Retrieves balance information using the provided options.
109    ///
110    /// # Arguments
111    ///
112    /// * `options` - An optional `GetBalanceOptions` struct containing retrieval options.
113    ///
114    /// # Returns
115    ///
116    /// A `Result` containing either a `GetBalanceResult` or a `JsError` in case of an error.
117    ///
118    /// # Errors
119    ///
120    /// Returns a `JsError` if there is an error during the retrieval process.
121    #[cfg_attr(feature = "js", wasm_bindgen(js_name = "get_balance"))]
122    #[cfg(feature = "js")]
123    pub async fn get_balance_js_alias(
124        &self,
125        options: Option<GetBalanceOptions>,
126    ) -> Result<GetBalanceResult, JsError> {
127        let GetBalanceOptions {
128            state_root_hash_as_string,
129            state_root_hash,
130            purse_uref_as_string,
131            purse_uref,
132            verbosity,
133            rpc_address,
134        } = options.unwrap_or_default();
135
136        let purse_uref = if let Some(purse_uref) = purse_uref {
137            GetBalanceInput::PurseUref(purse_uref)
138        } else if let Some(purse_uref_as_string) = purse_uref_as_string {
139            GetBalanceInput::PurseUrefAsString(purse_uref_as_string)
140        } else {
141            let err = "Error: Missing purse uref as string or purse uref";
142            return Err(JsError::new(err));
143        };
144
145        let result = if let Some(hash) = state_root_hash {
146            self.get_balance(purse_uref, Some(hash.to_digest()), verbosity, rpc_address)
147                .await
148        } else if let Some(hash) = state_root_hash_as_string.clone() {
149            let hash = if !hash.is_empty() {
150                match Digest::new(&hash) {
151                    Ok(digest) => digest.to_string(),
152                    _ => "".to_string(),
153                }
154            } else {
155                "".to_string()
156            };
157            self.get_balance(purse_uref, Some(hash.as_str()), verbosity, rpc_address)
158                .await
159        } else {
160            self.get_balance(purse_uref, None::<&str>, verbosity, rpc_address)
161                .await
162        };
163
164        match result {
165            Ok(data) => Ok(data.result.into()),
166            Err(err) => {
167                let err = &format!("Error occurred with {err:?}");
168                Err(JsError::new(err))
169            }
170        }
171    }
172
173    /// JavaScript Alias for `get_balance`.
174    ///
175    /// # Arguments
176    ///
177    /// * `options` - An optional `GetBalanceOptions` struct containing retrieval options.
178    ///
179    /// # Returns
180    ///
181    /// A `Result` containing either a `GetBalanceResult` or a `JsError` in case of an error.
182    #[cfg_attr(feature = "js", wasm_bindgen(js_name = "state_get_balance"))]
183    #[deprecated(note = "This function is an alias. Please use `get_balance` instead.")]
184    #[allow(deprecated)]
185    #[cfg(feature = "js")]
186    pub async fn state_get_balance(
187        &self,
188        options: Option<GetBalanceOptions>,
189    ) -> Result<GetBalanceResult, JsError> {
190        self.get_balance_js_alias(options).await
191    }
192}
193
194/// Enum representing different ways to specify the purse uref.
195#[derive(Debug, Clone)]
196pub enum GetBalanceInput {
197    PurseUref(URef),
198    PurseUrefAsString(String),
199}
200
201impl SDK {
202    /// Retrieves balance information based on the provided options.
203    ///
204    /// # Arguments
205    ///
206    /// * `state_root_hash` - The state root hash to query for balance information.
207    /// * `purse_uref` - The purse uref specifying the purse for which to retrieve the balance.
208    /// * `verbosity` - An optional `Verbosity` level for controlling the output verbosity.
209    /// * `rpc_address` - An optional string specifying the rpc address to use for the request.
210    ///
211    /// # Returns
212    ///
213    /// A `Result` containing either a `GetBalanceResult` or a `SdkError` in case of an error.
214    ///
215    /// # Errors
216    ///
217    /// Returns a `SdkError` if there is an error during the retrieval process.
218    pub async fn get_balance(
219        &self,
220        purse_uref: GetBalanceInput,
221        state_root_hash: Option<impl ToDigest>,
222        verbosity: Option<Verbosity>,
223        rpc_address: Option<String>,
224    ) -> Result<SuccessResponse<_GetBalanceResult>, SdkError> {
225        //log("get_balance!");
226        let state_root_hash = if let Some(state_root_hash) = state_root_hash {
227            if state_root_hash.is_empty() {
228                let state_root_hash = self
229                    .get_state_root_hash(
230                        None,
231                        None,
232                        Some(self.get_rpc_address(rpc_address.clone())),
233                    )
234                    .await;
235
236                match state_root_hash {
237                    Ok(state_root_hash) => {
238                        let state_root_hash: Digest =
239                            state_root_hash.result.state_root_hash.unwrap().into();
240                        state_root_hash
241                    }
242                    Err(_) => "".to_digest(),
243                }
244            } else {
245                state_root_hash.to_digest()
246            }
247        } else {
248            let state_root_hash = self
249                .get_state_root_hash(None, None, Some(self.get_rpc_address(rpc_address.clone())))
250                .await;
251
252            match state_root_hash {
253                Ok(state_root_hash) => {
254                    let state_root_hash: Digest =
255                        state_root_hash.result.state_root_hash.unwrap().into();
256                    state_root_hash
257                }
258                Err(_) => "".to_digest(),
259            }
260        };
261        let random_id = rand::rng().random::<u64>().to_string();
262        match purse_uref {
263            GetBalanceInput::PurseUref(purse_uref) => get_balance_lib(
264                JsonRpcId::from(random_id),
265                &self.get_rpc_address(rpc_address),
266                self.get_verbosity(verbosity).into(),
267                state_root_hash.into(),
268                purse_uref.into(),
269            )
270            .await
271            .map_err(SdkError::from),
272            GetBalanceInput::PurseUrefAsString(purse_uref) => get_balance_cli(
273                &random_id,
274                &self.get_rpc_address(rpc_address),
275                self.get_verbosity(verbosity).into(),
276                &state_root_hash.to_string(),
277                &purse_uref,
278            )
279            .await
280            .map_err(SdkError::from),
281        }
282    }
283}
284
285#[cfg(test)]
286mod tests {
287    use super::*;
288    use crate::helpers::public_key_from_secret_key;
289    use sdk_tests::tests::helpers::{
290        get_enable_addressable_entity, get_network_constants, get_user_secret_key,
291    };
292
293    async fn get_main_purse() -> URef {
294        let sdk = SDK::new(None, None, None);
295        let (rpc_address, _, _, _, _) = get_network_constants();
296        let secret_key = get_user_secret_key(None).unwrap();
297        let account = public_key_from_secret_key(&secret_key).unwrap();
298
299        let main_purse = if get_enable_addressable_entity() {
300            sdk.get_entity(None, Some(account), None, None, Some(rpc_address))
301                .await
302                .unwrap()
303                .result
304                .entity_result
305                .addressable_entity()
306                .unwrap()
307                .entity
308                .main_purse()
309        } else {
310            #[allow(deprecated)]
311            sdk.get_account(None, Some(account), None, None, Some(rpc_address))
312                .await
313                .unwrap()
314                .result
315                .account
316                .main_purse()
317        };
318
319        main_purse.into()
320    }
321
322    #[tokio::test]
323    async fn test_get_balance_with_none_values() {
324        // Arrange
325        let sdk = SDK::new(None, None, None);
326        let purse_uref = GetBalanceInput::PurseUref(get_main_purse().await);
327        let error_message = "failed to parse node address as valid URL";
328
329        // Act
330        let result = sdk
331            .get_balance(
332                purse_uref,
333                Some("7d3dc9c74fe93e83fe6cc7a9830ba223035ad4fd4fd464489640742069ca31ed"), // get_balance does not support empty string as state_root_hash
334                None,
335                None,
336            )
337            .await;
338
339        // Assert
340        assert!(result.is_err());
341        let err_string = result.err().unwrap().to_string();
342        assert!(err_string.contains(error_message));
343    }
344
345    #[tokio::test]
346    async fn test_get_balance_with_purse_uref() {
347        // Arrange
348        let sdk = SDK::new(None, None, None);
349        let (rpc_address, _, _, _, _) = get_network_constants();
350        let purse_uref = GetBalanceInput::PurseUref(get_main_purse().await);
351
352        // Act
353        let result = sdk
354            .get_balance(purse_uref, None::<&str>, None, Some(rpc_address))
355            .await;
356
357        // Assert
358        assert!(result.is_ok());
359    }
360
361    #[tokio::test]
362    async fn test_get_balance_with_purse_uref_as_string() {
363        // Arrange
364        let sdk = SDK::new(None, None, None);
365        let (rpc_address, _, _, _, _) = get_network_constants();
366        let purse_uref =
367            GetBalanceInput::PurseUrefAsString(get_main_purse().await.to_formatted_string());
368
369        // Act
370        let result = sdk
371            .get_balance(purse_uref, None::<&str>, None, Some(rpc_address))
372            .await;
373
374        // Assert
375        assert!(result.is_ok());
376    }
377
378    #[tokio::test]
379    async fn test_get_balance_with_state_root_hash() {
380        // Arrange
381        let sdk = SDK::new(None, None, None);
382        let (rpc_address, _, _, _, _) = get_network_constants();
383
384        let state_root_hash: Digest = sdk
385            .get_state_root_hash(None, Some(Verbosity::High), Some(rpc_address.clone()))
386            .await
387            .unwrap()
388            .result
389            .state_root_hash
390            .unwrap()
391            .into();
392        let purse_uref = GetBalanceInput::PurseUref(get_main_purse().await);
393
394        // Act
395        let result = sdk
396            .get_balance(purse_uref, Some(state_root_hash), None, Some(rpc_address))
397            .await;
398
399        // Assert
400        assert!(result.is_ok());
401    }
402
403    #[tokio::test]
404    async fn test_get_balance_with_error() {
405        // Arrange
406        let sdk = SDK::new(Some("http://localhost".to_string()), None, None);
407        let error_message = "error sending request";
408        let purse_uref = GetBalanceInput::PurseUref(get_main_purse().await);
409        // Act
410        let result = sdk
411            .get_balance(
412                purse_uref,
413                Some("7d3dc9c74fe93e83fe6cc7a9830ba223035ad4fd4fd464489640742069ca31ed"), // get_balance does not support empty string as state_root_hash
414                None,
415                None,
416            )
417            .await;
418
419        // Assert
420        assert!(result.is_err());
421        let err_string = result.err().unwrap().to_string();
422        assert!(err_string.contains(error_message));
423    }
424}