|
2 | 2 | #![no_std] |
3 | 3 | #![allow(unexpected_cfgs)] |
4 | 4 |
|
5 | | -use soroban_sdk::{contract, contractimpl, Address, Env}; |
| 5 | +use soroban_sdk::{contract, contractimpl, contracttype, Address, Env}; |
| 6 | + |
| 7 | +#[contracttype] |
| 8 | +#[derive(Clone, Debug, Eq, PartialEq)] |
| 9 | +pub enum DataKey { |
| 10 | + Account(Address), |
| 11 | +} |
| 12 | + |
| 13 | +#[contracttype] |
| 14 | +#[derive(Clone, Debug, Eq, PartialEq)] |
| 15 | +pub struct AccountState { |
| 16 | + pub balance: i128, |
| 17 | + pub reserved: i128, |
| 18 | + pub last_update: u32, |
| 19 | +} |
| 20 | + |
| 21 | +#[contracttype] |
| 22 | +#[derive(Clone, Debug, Eq, PartialEq)] |
| 23 | +pub struct AccountSummary { |
| 24 | + /// True if the account has state persisted in this contract. |
| 25 | + pub exists: bool, |
| 26 | + /// Spendable balance. |
| 27 | + pub balance: i128, |
| 28 | + /// Amount currently reserved/locked. |
| 29 | + pub reserved: i128, |
| 30 | + /// Ledger sequence of the latest state mutation affecting the account. |
| 31 | + pub last_update: u32, |
| 32 | +} |
6 | 33 |
|
7 | 34 | #[contract] |
8 | 35 | pub struct BalanceManager; |
9 | 36 |
|
10 | 37 | #[contractimpl] |
11 | 38 | impl BalanceManager { |
12 | 39 | /// Update user balance (Internal use by other contracts). |
13 | | - pub fn update_balance(_env: Env, _user: Address, _amount: i128, _is_add: bool) { |
| 40 | + pub fn update_balance(env: Env, user: Address, amount: i128, is_add: bool) { |
14 | 41 | // TODO: Require authorization from authorized game contracts |
15 | | - // TODO: Update storage |
| 42 | + assert!(amount >= 0, "amount must be non-negative"); |
| 43 | + |
| 44 | + let mut state = Self::read_state_or_default(&env, user.clone()); |
| 45 | + |
| 46 | + if is_add { |
| 47 | + state.balance = state |
| 48 | + .balance |
| 49 | + .checked_add(amount) |
| 50 | + .expect("balance overflow on add"); |
| 51 | + } else { |
| 52 | + state.balance = state |
| 53 | + .balance |
| 54 | + .checked_sub(amount) |
| 55 | + .expect("balance underflow on subtract"); |
| 56 | + } |
| 57 | + |
| 58 | + state.last_update = env.ledger().sequence(); |
| 59 | + env.storage().persistent().set(&DataKey::Account(user), &state); |
16 | 60 | } |
17 | 61 |
|
18 | 62 | /// View user balance. |
19 | | - pub fn get_balance(_env: Env, _user: Address) -> i128 { |
20 | | - // TODO: Read from storage |
21 | | - 0 |
| 63 | + pub fn get_balance(env: Env, user: Address) -> i128 { |
| 64 | + Self::read_state_or_default(&env, user).balance |
| 65 | + } |
| 66 | + |
| 67 | + /// Returns a stable account snapshot for backend consumers. |
| 68 | + /// |
| 69 | + /// If an account has never been written, `exists` is false and numeric |
| 70 | + /// fields are zeroed so unknown and zero-balance-known accounts are |
| 71 | + /// distinguishable. |
| 72 | + pub fn get_account_summary(env: Env, user: Address) -> AccountSummary { |
| 73 | + match env |
| 74 | + .storage() |
| 75 | + .persistent() |
| 76 | + .get::<DataKey, AccountState>(&DataKey::Account(user)) |
| 77 | + { |
| 78 | + Some(state) => AccountSummary { |
| 79 | + exists: true, |
| 80 | + balance: state.balance, |
| 81 | + reserved: state.reserved, |
| 82 | + last_update: state.last_update, |
| 83 | + }, |
| 84 | + None => AccountSummary { |
| 85 | + exists: false, |
| 86 | + balance: 0, |
| 87 | + reserved: 0, |
| 88 | + last_update: 0, |
| 89 | + }, |
| 90 | + } |
| 91 | + } |
| 92 | + |
| 93 | + fn read_state_or_default(env: &Env, user: Address) -> AccountState { |
| 94 | + env.storage() |
| 95 | + .persistent() |
| 96 | + .get::<DataKey, AccountState>(&DataKey::Account(user)) |
| 97 | + .unwrap_or(AccountState { |
| 98 | + balance: 0, |
| 99 | + reserved: 0, |
| 100 | + last_update: 0, |
| 101 | + }) |
| 102 | + } |
| 103 | +} |
| 104 | + |
| 105 | +#[cfg(test)] |
| 106 | +mod tests { |
| 107 | + extern crate std; |
| 108 | + |
| 109 | + use super::{AccountSummary, BalanceManager, BalanceManagerClient}; |
| 110 | + use soroban_sdk::{testutils::{Address as _, Ledger as _}, Address, Env}; |
| 111 | + |
| 112 | + #[test] |
| 113 | + fn empty_account_summary_is_explicitly_unknown() { |
| 114 | + let env = Env::default(); |
| 115 | + let contract_id = env.register(BalanceManager, ()); |
| 116 | + let client = BalanceManagerClient::new(&env, &contract_id); |
| 117 | + let user = Address::generate(&env); |
| 118 | + |
| 119 | + let summary = client.get_account_summary(&user); |
| 120 | + assert_eq!( |
| 121 | + summary, |
| 122 | + AccountSummary { |
| 123 | + exists: false, |
| 124 | + balance: 0, |
| 125 | + reserved: 0, |
| 126 | + last_update: 0, |
| 127 | + } |
| 128 | + ); |
| 129 | + assert_eq!(client.get_balance(&user), 0); |
| 130 | + } |
| 131 | + |
| 132 | + #[test] |
| 133 | + fn funded_account_summary_reflects_balance() { |
| 134 | + let env = Env::default(); |
| 135 | + let contract_id = env.register(BalanceManager, ()); |
| 136 | + let client = BalanceManagerClient::new(&env, &contract_id); |
| 137 | + let user = Address::generate(&env); |
| 138 | + |
| 139 | + env.ledger().with_mut(|li| li.sequence_number = 11); |
| 140 | + client.update_balance(&user, &250, &true); |
| 141 | + |
| 142 | + let summary = client.get_account_summary(&user); |
| 143 | + assert_eq!(summary.exists, true); |
| 144 | + assert_eq!(summary.balance, 250); |
| 145 | + assert_eq!(summary.reserved, 0); |
| 146 | + assert_eq!(summary.last_update, 11); |
| 147 | + assert_eq!(client.get_balance(&user), 250); |
| 148 | + } |
| 149 | + |
| 150 | + #[test] |
| 151 | + fn summary_last_update_tracks_balance_mutations() { |
| 152 | + let env = Env::default(); |
| 153 | + let contract_id = env.register(BalanceManager, ()); |
| 154 | + let client = BalanceManagerClient::new(&env, &contract_id); |
| 155 | + let user = Address::generate(&env); |
| 156 | + |
| 157 | + env.ledger().with_mut(|li| li.sequence_number = 5); |
| 158 | + client.update_balance(&user, &100, &true); |
| 159 | + |
| 160 | + let after_fund = client.get_account_summary(&user); |
| 161 | + assert_eq!(after_fund.balance, 100); |
| 162 | + assert_eq!(after_fund.last_update, 5); |
| 163 | + |
| 164 | + env.ledger().with_mut(|li| li.sequence_number = 9); |
| 165 | + client.update_balance(&user, &40, &false); |
| 166 | + |
| 167 | + let after_spend = client.get_account_summary(&user); |
| 168 | + assert_eq!(after_spend.exists, true); |
| 169 | + assert_eq!(after_spend.balance, 60); |
| 170 | + assert_eq!(after_spend.reserved, 0); |
| 171 | + assert_eq!(after_spend.last_update, 9); |
22 | 172 | } |
23 | 173 | } |
0 commit comments