Skip to main content

xrpl_wasm_stdlib/core/ledger_objects/
account_root.rs

1use crate::core::keylets::account_keylet;
2use crate::core::ledger_objects::traits::{AccountFields, LedgerObjectCommonFields};
3use crate::core::types::account_id::AccountID;
4use crate::core::types::amount::Amount;
5use crate::host;
6use host::Error;
7
8#[derive(Debug, Clone, Copy, Eq, PartialEq)]
9#[repr(C)]
10pub struct AccountRoot {
11    pub slot_num: i32,
12}
13
14impl LedgerObjectCommonFields for AccountRoot {
15    fn get_slot_num(&self) -> i32 {
16        self.slot_num
17    }
18}
19
20impl AccountFields for AccountRoot {}
21
22pub fn get_account_balance(account_id: &AccountID) -> host::Result<Option<Amount>> {
23    // Construct the account keylet. This calls a host function, so propagate the error via `?`
24    let account_keylet = match account_keylet(account_id) {
25        host::Result::Ok(keylet) => keylet,
26        host::Result::Err(e) => return host::Result::Err(e),
27    };
28
29    // Try to cache the ledger object inside rippled
30    let slot = unsafe { host::cache_ledger_obj(account_keylet.as_ptr(), account_keylet.len(), 0) };
31    if slot < 0 {
32        return host::Result::Err(Error::from_code(slot));
33    }
34
35    // Get the balance.
36    // We use the trait-bound implementation so as not to duplicate accessor logic.
37    let account = AccountRoot { slot_num: slot };
38    account.balance()
39}
40
41#[cfg(test)]
42mod tests {
43    use super::*;
44    use crate::core::keylets::XRPL_KEYLET_SIZE;
45    use crate::core::types::amount::AMOUNT_SIZE;
46    use crate::host::error_codes::INTERNAL_ERROR;
47    use crate::host::host_bindings_trait::MockHostBindings;
48    use crate::host::setup_mock;
49    use crate::sfield;
50    use mockall::predicate::{always, eq};
51
52    /// Mock account_keylet to write 0xCC bytes and return success.
53    /// The byte value is arbitrary — `cache_ledger_obj` is itself mocked and
54    /// never reads the buffer; what matters is that the keylet's `MaybeUninit`
55    /// storage is initialized before downstream code calls `assume_init`.
56    fn mock_account_keylet_success(mock: &mut MockHostBindings) {
57        mock.expect_account_keylet()
58            .times(1)
59            .returning(|_, _, out_buff_ptr, out_buff_len| {
60                assert_eq!(out_buff_len, XRPL_KEYLET_SIZE);
61                unsafe {
62                    for i in 0..XRPL_KEYLET_SIZE {
63                        *out_buff_ptr.add(i) = 0xCC;
64                    }
65                }
66                XRPL_KEYLET_SIZE as i32
67            });
68    }
69
70    #[test]
71    fn test_get_account_balance_success() {
72        let mut mock = MockHostBindings::new();
73        let slot = 5;
74        let balance_field_code: i32 = sfield::Balance.into();
75
76        mock_account_keylet_success(&mut mock);
77
78        // Mock cache_ledger_obj to return a valid slot
79        mock.expect_cache_ledger_obj()
80            .times(1)
81            .returning(move |_, _, _| slot);
82
83        // Mock get_ledger_obj_field for Balance. Zero-fill the buffer: the
84        // Amount getter allocates via `MaybeUninit` and calls `assume_init`,
85        // so leaving it uninitialized would be UB. Zero bytes route through
86        // the XRP variant of `Amount::from_bytes`.
87        mock.expect_get_ledger_obj_field()
88            .with(eq(slot), eq(balance_field_code), always(), eq(AMOUNT_SIZE))
89            .times(1)
90            .returning(move |_, _, buf, buf_size| {
91                unsafe { core::ptr::write_bytes(buf, 0, buf_size) };
92                AMOUNT_SIZE as i32
93            });
94
95        let _guard = setup_mock(mock);
96
97        let account_id = AccountID::from([0xBB; 20]);
98        let result = get_account_balance(&account_id);
99        assert!(result.is_ok());
100        assert!(result.unwrap().is_some());
101    }
102
103    #[test]
104    fn test_get_account_balance_keylet_error() {
105        let mut mock = MockHostBindings::new();
106
107        // Mock account_keylet to fail
108        mock.expect_account_keylet()
109            .times(1)
110            .returning(|_, _, _, _| INTERNAL_ERROR);
111
112        let _guard = setup_mock(mock);
113
114        let account_id = AccountID::from([0xBB; 20]);
115        let result = get_account_balance(&account_id);
116        assert!(result.is_err());
117        assert_eq!(result.err().unwrap().code(), INTERNAL_ERROR);
118    }
119
120    #[test]
121    fn test_get_account_balance_cache_error() {
122        let mut mock = MockHostBindings::new();
123
124        mock_account_keylet_success(&mut mock);
125
126        // Mock cache_ledger_obj to return error
127        mock.expect_cache_ledger_obj()
128            .times(1)
129            .returning(|_, _, _| INTERNAL_ERROR);
130
131        let _guard = setup_mock(mock);
132
133        let account_id = AccountID::from([0xBB; 20]);
134        let result = get_account_balance(&account_id);
135        assert!(result.is_err());
136        assert_eq!(result.err().unwrap().code(), INTERNAL_ERROR);
137    }
138}