Skip to content

Commit 5dd3c80

Browse files
authored
Merge pull request theblockcade#457 from chiscookeke11/issue-355
Contract balance-management: add account balance summary and last-update accessor
2 parents 985ed47 + d158fe3 commit 5dd3c80

2 files changed

Lines changed: 208 additions & 14 deletions

File tree

contracts/balance-management/src/lib.rs

Lines changed: 156 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,172 @@
22
#![no_std]
33
#![allow(unexpected_cfgs)]
44

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+
}
633

734
#[contract]
835
pub struct BalanceManager;
936

1037
#[contractimpl]
1138
impl BalanceManager {
1239
/// 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) {
1441
// 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);
1660
}
1761

1862
/// 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);
22172
}
23173
}

docs/contracts/balance-management.md

Lines changed: 52 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,33 +6,77 @@
66
Update user balance (Internal use by other contracts).
77

88
```rust
9-
pub fn update_balance(_env: Env, _user: Address, _amount: i128, _is_add: bool)
9+
pub fn update_balance(env: Env, user: Address, amount: i128, is_add: bool)
1010
```
1111

1212
#### Parameters
1313

1414
| Name | Type |
1515
|------|------|
16-
| `_env` | `Env` |
17-
| `_user` | `Address` |
18-
| `_amount` | `i128` |
19-
| `_is_add` | `bool` |
16+
| `env` | `Env` |
17+
| `user` | `Address` |
18+
| `amount` | `i128` |
19+
| `is_add` | `bool` |
20+
21+
#### Behavior
22+
23+
- Read-only callers are not affected; this method mutates account state.
24+
- `amount` must be non-negative.
25+
- When `is_add = true`, `amount` is added to the current balance.
26+
- When `is_add = false`, `amount` is subtracted from the current balance.
27+
- `last_update` is written as the current ledger sequence whenever a mutation succeeds.
2028

2129
### `get_balance`
2230
View user balance.
2331

2432
```rust
25-
pub fn get_balance(_env: Env, _user: Address) -> i128
33+
pub fn get_balance(env: Env, user: Address) -> i128
2634
```
2735

2836
#### Parameters
2937

3038
| Name | Type |
3139
|------|------|
32-
| `_env` | `Env` |
33-
| `_user` | `Address` |
40+
| `env` | `Env` |
41+
| `user` | `Address` |
3442

3543
#### Return Type
3644

3745
`i128`
3846

47+
#### Behavior
48+
49+
- Returns `0` - when no account state exists yet for `user`.
50+
51+
### `get_account_summary`
52+
Return a compact, deterministic account snapshot for backend consumers.
53+
54+
```rust
55+
pub fn get_account_summary(env: Env, user: Address) -> AccountSummary
56+
```
57+
58+
#### Parameters
59+
60+
| Name | Type |
61+
|------|------|
62+
| `env` | `Env` |
63+
| `user` | `Address` |
64+
65+
#### Return Type
66+
67+
```rust
68+
pub struct AccountSummary {
69+
pub exists: bool,
70+
pub balance: i128,
71+
pub reserved: i128,
72+
pub last_update: u32,
73+
}
74+
```
75+
76+
#### Summary Contract (backend-facing)
77+
78+
- **Read-only and deterministic:** no side effects and output depends only on stored state.
79+
- **Unknown account distinction:**
80+
- Unknown account → `exists = false`, `balance = 0`, `reserved = 0`, `last_update = 0`.
81+
- Known zeroed account → `exists = true` (e.g. account mutated but ended at zero).
82+
- **Typed-client stability:** the response shape is a fixed struct with concrete scalar fields (`bool`, `i128`, `u32`).

0 commit comments

Comments
 (0)