/ abzu-token / src / ledger.rs
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  }