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