ledger.rs
1 //! Ledger trait and implementations for balance storage 2 //! 3 //! Designed for ecosystem-wide ingestion: 4 //! - Trait-based abstraction for multiple backends 5 //! - MemoryLedger for testing/embedded use 6 //! - DhtLedger (future) for distributed mesh storage 7 //! - Thread-safe with interior mutability 8 9 use crate::balance::Balance; 10 use crate::error::TokenError; 11 use std::collections::HashMap; 12 use std::sync::RwLock; 13 14 /// Address type — 32-byte Ed25519 public key as hex 15 pub type Address = String; 16 17 /// Ledger trait for balance storage backends 18 /// 19 /// Implementors must be thread-safe. 20 pub trait Ledger: Send + Sync { 21 /// Get balance for an address 22 fn balance(&self, address: &Address) -> Balance; 23 24 /// Transfer tokens between addresses 25 fn transfer( 26 &self, 27 from: &Address, 28 to: &Address, 29 amount: Balance, 30 ) -> Result<(), TokenError>; 31 32 /// Credit tokens to an address (genesis/curve minting only) 33 fn credit(&self, address: &Address, amount: Balance) -> Result<(), TokenError>; 34 35 /// Debit tokens from an address (slashing only) 36 fn debit(&self, address: &Address, amount: Balance) -> Result<(), TokenError>; 37 38 /// Lock tokens for a bond 39 fn lock(&self, address: &Address, amount: Balance) -> Result<(), TokenError>; 40 41 /// Unlock tokens from a bond 42 fn unlock(&self, address: &Address, amount: Balance) -> Result<(), TokenError>; 43 44 /// Get locked balance for an address 45 fn locked_balance(&self, address: &Address) -> Balance; 46 47 /// Get available (unlocked) balance for an address 48 fn available_balance(&self, address: &Address) -> Balance { 49 let total = self.balance(address); 50 let locked = self.locked_balance(address); 51 total.saturating_sub(locked) 52 } 53 } 54 55 /// In-memory ledger for testing and embedded use 56 pub struct MemoryLedger { 57 balances: RwLock<HashMap<Address, Balance>>, 58 locked: RwLock<HashMap<Address, Balance>>, 59 } 60 61 impl MemoryLedger { 62 /// Create a new empty ledger 63 pub fn new() -> Self { 64 Self { 65 balances: RwLock::new(HashMap::new()), 66 locked: RwLock::new(HashMap::new()), 67 } 68 } 69 70 /// Create a ledger with initial balances (for testing/genesis) 71 pub fn with_balances(initial: Vec<(Address, Balance)>) -> Self { 72 let ledger = Self::new(); 73 { 74 let mut balances = ledger.balances.write().unwrap(); 75 for (addr, bal) in initial { 76 balances.insert(addr, bal); 77 } 78 } 79 ledger 80 } 81 } 82 83 impl Default for MemoryLedger { 84 fn default() -> Self { 85 Self::new() 86 } 87 } 88 89 impl Ledger for MemoryLedger { 90 fn balance(&self, address: &Address) -> Balance { 91 self.balances 92 .read() 93 .unwrap() 94 .get(address) 95 .copied() 96 .unwrap_or(Balance::ZERO) 97 } 98 99 fn transfer( 100 &self, 101 from: &Address, 102 to: &Address, 103 amount: Balance, 104 ) -> Result<(), TokenError> { 105 if amount.is_zero() { 106 return Ok(()); 107 } 108 109 let mut balances = self.balances.write().unwrap(); 110 let locked = self.locked.read().unwrap(); 111 112 // Check available balance (total - locked) 113 let from_balance = balances.get(from).copied().unwrap_or(Balance::ZERO); 114 let from_locked = locked.get(from).copied().unwrap_or(Balance::ZERO); 115 let available = from_balance.saturating_sub(from_locked); 116 117 if available.drops() < amount.drops() { 118 return Err(TokenError::InsufficientBalance { 119 have: available.drops(), 120 need: amount.drops(), 121 }); 122 } 123 124 // Debit from 125 let new_from = from_balance.checked_sub(amount)?; 126 if new_from.is_zero() { 127 balances.remove(from); 128 } else { 129 balances.insert(from.clone(), new_from); 130 } 131 132 // Credit to 133 let to_balance = balances.get(to).copied().unwrap_or(Balance::ZERO); 134 let new_to = to_balance.checked_add(amount)?; 135 balances.insert(to.clone(), new_to); 136 137 Ok(()) 138 } 139 140 fn credit(&self, address: &Address, amount: Balance) -> Result<(), TokenError> { 141 let mut balances = self.balances.write().unwrap(); 142 let current = balances.get(address).copied().unwrap_or(Balance::ZERO); 143 let new_balance = current.checked_add(amount)?; 144 balances.insert(address.clone(), new_balance); 145 Ok(()) 146 } 147 148 fn debit(&self, address: &Address, amount: Balance) -> Result<(), TokenError> { 149 let mut balances = self.balances.write().unwrap(); 150 let current = balances.get(address).copied().unwrap_or(Balance::ZERO); 151 let new_balance = current.checked_sub(amount)?; 152 if new_balance.is_zero() { 153 balances.remove(address); 154 } else { 155 balances.insert(address.clone(), new_balance); 156 } 157 Ok(()) 158 } 159 160 fn lock(&self, address: &Address, amount: Balance) -> Result<(), TokenError> { 161 // Verify address has enough available balance 162 let available = self.available_balance(address); 163 if available.drops() < amount.drops() { 164 return Err(TokenError::InsufficientBalance { 165 have: available.drops(), 166 need: amount.drops(), 167 }); 168 } 169 170 let mut locked = self.locked.write().unwrap(); 171 let current_locked = locked.get(address).copied().unwrap_or(Balance::ZERO); 172 let new_locked = current_locked.checked_add(amount)?; 173 locked.insert(address.clone(), new_locked); 174 Ok(()) 175 } 176 177 fn unlock(&self, address: &Address, amount: Balance) -> Result<(), TokenError> { 178 let mut locked = self.locked.write().unwrap(); 179 let current_locked = locked.get(address).copied().unwrap_or(Balance::ZERO); 180 let new_locked = current_locked.checked_sub(amount)?; 181 if new_locked.is_zero() { 182 locked.remove(address); 183 } else { 184 locked.insert(address.clone(), new_locked); 185 } 186 Ok(()) 187 } 188 189 fn locked_balance(&self, address: &Address) -> Balance { 190 self.locked 191 .read() 192 .unwrap() 193 .get(address) 194 .copied() 195 .unwrap_or(Balance::ZERO) 196 } 197 } 198 199 #[cfg(test)] 200 mod tests { 201 use super::*; 202 203 #[test] 204 fn test_transfer() { 205 let ledger = MemoryLedger::with_balances(vec![ 206 ("alice".into(), Balance::from_abzu(100)), 207 ]); 208 209 ledger.transfer(&"alice".into(), &"bob".into(), Balance::from_abzu(30)).unwrap(); 210 211 assert_eq!(ledger.balance(&"alice".into()).abzu_whole(), 70); 212 assert_eq!(ledger.balance(&"bob".into()).abzu_whole(), 30); 213 } 214 215 #[test] 216 fn test_transfer_insufficient() { 217 let ledger = MemoryLedger::with_balances(vec![ 218 ("alice".into(), Balance::from_abzu(10)), 219 ]); 220 221 let result = ledger.transfer(&"alice".into(), &"bob".into(), Balance::from_abzu(50)); 222 assert!(matches!(result, Err(TokenError::InsufficientBalance { .. }))); 223 } 224 225 #[test] 226 fn test_lock_unlock() { 227 let ledger = MemoryLedger::with_balances(vec![ 228 ("alice".into(), Balance::from_abzu(100)), 229 ]); 230 231 // Lock 30 232 ledger.lock(&"alice".into(), Balance::from_abzu(30)).unwrap(); 233 assert_eq!(ledger.locked_balance(&"alice".into()).abzu_whole(), 30); 234 assert_eq!(ledger.available_balance(&"alice".into()).abzu_whole(), 70); 235 236 // Can't transfer locked funds 237 let result = ledger.transfer(&"alice".into(), &"bob".into(), Balance::from_abzu(80)); 238 assert!(matches!(result, Err(TokenError::InsufficientBalance { .. }))); 239 240 // Can transfer available 241 ledger.transfer(&"alice".into(), &"bob".into(), Balance::from_abzu(50)).unwrap(); 242 243 // Unlock 244 ledger.unlock(&"alice".into(), Balance::from_abzu(30)).unwrap(); 245 assert_eq!(ledger.locked_balance(&"alice".into()).abzu_whole(), 0); 246 } 247 }