1use std::collections::HashMap;
2use std::sync::Arc;
3
4use pot_o_core::TokenType;
5use serde::{Deserialize, Serialize};
6use sha2::{Digest, Sha256};
7use tokio::sync::RwLock;
8use tracing;
9
10use crate::tx::{CoinbaseTransaction, TransferTransaction};
11use hexchain_p2p::block::HexBlock;
12
13pub const TRIBE_HARD_CAP: u64 = 1_000_000_000_000_000;
14pub const TRIBE_BLOCK_REWARD_INITIAL: u64 = 1_000_000_000; pub const TRIBE_HALVING_INTERVAL: u64 = 210_000; pub const TRIBE_PROOF_REWARD: u64 = 10_000_000; pub const COINBASE_MATURITY_DEPTH: u64 = 100;
18
19pub fn block_reward_at_height(height: u64) -> u64 {
20 let halvings = height / TRIBE_HALVING_INTERVAL;
21 if halvings >= 64 {
22 return 0;
23 }
24 TRIBE_BLOCK_REWARD_INITIAL >> halvings
25}
26
27#[derive(Debug, Clone)]
28pub struct CoinbaseEntry {
29 pub amount: u64,
30 pub mature_at_height: u64,
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct LedgerEntry {
35 pub address: String,
36 pub token: TokenType,
37 pub balance: u64,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct LedgerSnapshot {
42 pub entries: Vec<LedgerEntry>,
43 pub tx_history: Vec<TxReceipt>,
44 pub nonces: Vec<(String, u64)>,
45 pub block_height: u64,
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct TxReceipt {
50 pub tx_hash: String,
51 pub from: String,
52 pub to: String,
53 pub token: TokenType,
54 pub amount: u64,
55 pub fee: u64,
56 pub block_height: u64,
57 pub timestamp: u64,
58}
59
60pub struct Ledger {
61 balances: HashMap<(String, TokenType), u64>,
62 tx_history: Vec<TxReceipt>,
63 block_height: u64,
64 protocol_fee_address: String,
65 modified: bool,
66 nonces: HashMap<String, u64>,
67 coinbase_maturity: HashMap<String, Vec<CoinbaseEntry>>,
68 total_supply_map: HashMap<TokenType, u64>,
69 last_interaction: HashMap<(String, TokenType), u64>, minter_allowlist: HashMap<TokenType, Vec<String>>, }
72
73impl Ledger {
74 pub fn new(protocol_fee_address: String) -> Self {
75 Self {
76 balances: HashMap::new(),
77 tx_history: Vec::new(),
78 block_height: 0,
79 protocol_fee_address,
80 modified: false,
81 nonces: HashMap::new(),
82 coinbase_maturity: HashMap::new(),
83 total_supply_map: HashMap::new(),
84 last_interaction: HashMap::new(),
85 minter_allowlist: HashMap::new(),
86 }
87 }
88
89 pub fn balance_of(&self, address: &str, token: &TokenType) -> u64 {
90 self.balances
91 .get(&(address.to_string(), token.clone()))
92 .copied()
93 .unwrap_or(0)
94 }
95
96 pub fn balances(&self) -> &std::collections::HashMap<(String, TokenType), u64> {
97 &self.balances
98 }
99
100 pub fn issue(&mut self, to: &str, token: &TokenType, amount: u64) {
101 let key = (to.to_string(), token.clone());
102 let entry = self.balances.entry(key).or_insert(0);
103 *entry = entry.saturating_add(amount);
104
105 let total = self.total_supply_map.entry(token.clone()).or_insert(0);
107 *total = total.saturating_add(amount);
108
109 self.block_height = self.block_height.saturating_add(1);
110 self.modified = true;
111
112 let mut hasher = Sha256::new();
113 hasher.update(b"MINTER");
114 hasher.update(to.as_bytes());
115 hasher.update(amount.to_le_bytes());
116 hasher.update(self.block_height.to_le_bytes());
117 let tx_hash = hex::encode(hasher.finalize());
118
119 let receipt = TxReceipt {
120 tx_hash,
121 from: "MINTER".to_string(),
122 to: to.to_string(),
123 token: token.clone(),
124 amount,
125 fee: 0,
126 block_height: self.block_height,
127 timestamp: chrono::Utc::now().timestamp() as u64,
128 };
129 self.tx_history.push(receipt);
130 }
131
132 pub fn try_issue(&mut self, to: &str, token: &TokenType, amount: u64) -> Result<(), String> {
134 use pot_o_core::token_config::token_config;
135
136 let config_set = token_config();
138
139 if let Some(config) = config_set.get(token) {
141 let current_supply = self.total_supply_of(token);
143
144 if current_supply.saturating_add(amount) > config.supply_cap {
146 return Err(format!(
147 "Supply cap exceeded for {:?}: current={}, requested={}, cap={}",
148 token, current_supply, amount, config.supply_cap
149 ));
150 }
151 }
152
153 let key = (to.to_string(), token.clone());
155 let entry = self.balances.entry(key).or_insert(0);
156 *entry = entry.saturating_add(amount);
157
158 let total = self.total_supply_map.entry(token.clone()).or_insert(0);
160 *total = total.saturating_add(amount);
161
162 self.block_height = self.block_height.saturating_add(1);
163 self.modified = true;
164
165 let mut hasher = Sha256::new();
166 hasher.update(b"MINTER");
167 hasher.update(to.as_bytes());
168 hasher.update(amount.to_le_bytes());
169 hasher.update(self.block_height.to_le_bytes());
170 let tx_hash = hex::encode(hasher.finalize());
171
172 let receipt = TxReceipt {
173 tx_hash,
174 from: "MINTER".to_string(),
175 to: to.to_string(),
176 token: token.clone(),
177 amount,
178 fee: 0,
179 block_height: self.block_height,
180 timestamp: chrono::Utc::now().timestamp() as u64,
181 };
182 self.tx_history.push(receipt);
183
184 Ok(())
185 }
186
187 pub fn transfer(
188 &mut self,
189 from: &str,
190 to: &str,
191 token: &TokenType,
192 amount: u64,
193 fee: u64,
194 ) -> Result<TxReceipt, String> {
195 use pot_o_core::token_config::token_config;
196
197 if amount == 0 {
198 return Err("Transfer amount must be positive".into());
199 }
200
201 let config_set = token_config();
203 let burn = if let Some(config) = config_set.get(token) {
204 config.calculate_burn(amount)
205 } else {
206 0
207 };
208
209 let total = amount.checked_add(fee).ok_or("Overflow in amount + fee")?;
210 let from_key = (from.to_string(), token.clone());
211 let from_bal = self.balances.get(&from_key).copied().unwrap_or(0);
212
213 let total_needed = total.checked_add(burn).ok_or("Overflow in total + burn")?;
215 if from_bal < total_needed {
216 return Err(format!(
217 "Insufficient balance: have {}, need {} (amount {} + fee {} + burn {})",
218 from_bal, total_needed, amount, fee, burn
219 ));
220 }
221
222 self.balances
223 .insert(from_key.clone(), from_bal - total_needed);
224
225 let to_key = (to.to_string(), token.clone());
226 let to_bal = self.balances.entry(to_key).or_insert(0);
227
228 if fee > 0 && !self.protocol_fee_address.is_empty() && self.protocol_fee_address == to {
229 *to_bal = to_bal.saturating_add(amount + fee);
230 } else {
231 *to_bal = to_bal.saturating_add(amount);
232 if fee > 0 && !self.protocol_fee_address.is_empty() {
233 let fee_key = (self.protocol_fee_address.clone(), token.clone());
234 let fee_bal = self.balances.entry(fee_key).or_insert(0);
235 *fee_bal = fee_bal.saturating_add(fee);
236 }
237 }
238
239 if burn > 0 {
241 let total = self.total_supply_map.entry(token.clone()).or_insert(0);
242 *total = total.saturating_sub(burn);
243 }
244
245 self.block_height = self.block_height.saturating_add(1);
246 self.modified = true;
247
248 let mut hasher = Sha256::new();
249 hasher.update(from.as_bytes());
250 hasher.update(to.as_bytes());
251 hasher.update(amount.to_le_bytes());
252 hasher.update(fee.to_le_bytes());
253 hasher.update(self.block_height.to_le_bytes());
254 let tx_hash = hex::encode(hasher.finalize());
255
256 let receipt = TxReceipt {
257 tx_hash,
258 from: from.to_string(),
259 to: to.to_string(),
260 token: token.clone(),
261 amount,
262 fee,
263 block_height: self.block_height,
264 timestamp: chrono::Utc::now().timestamp() as u64,
265 };
266 self.tx_history.push(receipt.clone());
267
268 Ok(receipt)
269 }
270
271 pub fn tx_history(&self) -> &[TxReceipt] {
272 &self.tx_history
273 }
274
275 pub fn tx_history_for(&self, address: &str) -> Vec<TxReceipt> {
276 self.tx_history
277 .iter()
278 .filter(|t| t.from == address || t.to == address)
279 .cloned()
280 .collect()
281 }
282
283 pub fn block_height(&self) -> u64 {
284 self.block_height
285 }
286
287 pub fn nonces(&self) -> &HashMap<String, u64> {
288 &self.nonces
289 }
290
291 pub fn is_modified(&self) -> bool {
292 self.modified
293 }
294
295 pub fn clear_modified(&mut self) {
296 self.modified = false;
297 }
298
299 pub fn protocol_fee_address(&self) -> &str {
300 &self.protocol_fee_address
301 }
302
303 pub fn total_supply(&self, token: &TokenType) -> u64 {
304 self.balances
305 .iter()
306 .filter(|((_, t), _)| t == token)
307 .map(|(_, bal)| *bal)
308 .sum()
309 }
310
311 pub fn current_nonce(&self, address: &str) -> u64 {
312 self.nonces.get(address).copied().unwrap_or(0)
313 }
314
315 pub fn total_supply_of(&self, token: &TokenType) -> u64 {
316 self.total_supply_map.get(token).copied().unwrap_or(0)
317 }
318
319 pub fn update_interaction(&mut self, address: &str, token: &TokenType) {
321 let key = (address.to_string(), token.clone());
322 self.last_interaction.insert(key, self.block_height);
323 }
324
325 pub fn last_interaction(&self, address: &str, token: &TokenType) -> Option<u64> {
327 self.last_interaction
328 .get(&(address.to_string(), token.clone()))
329 .copied()
330 }
331
332 pub fn apply_decay(&self, address: &str, token: &TokenType, blocks_elapsed: u64) -> u64 {
335 use pot_o_core::token_config::token_config;
336
337 let balance = self.balance_of(address, token);
338
339 let config_set = token_config();
341
342 if let Some(config) = config_set.get(token) {
343 config.apply_decay(balance, blocks_elapsed)
344 } else {
345 balance
347 }
348 }
349
350 pub fn aum_block_reward(&self, height: u64) -> u64 {
353 const AUM_INITIAL_REWARD: u64 = 1_000_000_000; const AUM_HALVING_INTERVAL: u64 = 1_000_000; let halvings = height / AUM_HALVING_INTERVAL;
357 if halvings >= 64 {
358 return 0; }
360 AUM_INITIAL_REWARD >> halvings
361 }
362
363 pub fn add_minter(&mut self, token: &TokenType, minter: &str) {
365 self.minter_allowlist
366 .entry(token.clone())
367 .or_default()
368 .push(minter.to_string());
369 }
370
371 pub fn is_authorized_minter(&self, token: &TokenType, minter: &str) -> bool {
373 self.minter_allowlist
374 .get(token)
375 .map(|minters| minters.contains(&minter.to_string()))
376 .unwrap_or(false)
377 }
378
379 pub fn try_issue_with_minter(
381 &mut self,
382 minter: &str,
383 token: &TokenType,
384 to: &str,
385 amount: u64,
386 ) -> Result<(), String> {
387 if !self.is_authorized_minter(token, minter) {
389 return Err(format!(
390 "Minter {} is not authorized to mint {:?}",
391 minter, token
392 ));
393 }
394
395 self.try_issue(to, token, amount)
397 }
398
399 pub fn is_coinbase_mature(&self, _address: &str, height: u64, current_height: u64) -> bool {
400 current_height >= height + COINBASE_MATURITY_DEPTH
401 }
402
403 pub fn apply_transfer(
404 &mut self,
405 tx: &TransferTransaction,
406 miner: &str,
407 ) -> Result<TxReceipt, String> {
408 if tx.amount == 0 {
409 return Err("Transfer amount must be positive".into());
410 }
411 let total = tx
412 .amount
413 .checked_add(tx.fee)
414 .ok_or("Overflow in amount + fee")?;
415 let from_key = (tx.from.clone(), tx.token.clone());
416 let from_bal = self.balances.get(&from_key).copied().unwrap_or(0);
417 if from_bal < total {
418 return Err(format!(
419 "Insufficient balance: have {}, need {} (amount {} + fee {})",
420 from_bal, total, tx.amount, tx.fee
421 ));
422 }
423
424 self.balances.insert(from_key, from_bal - total);
425
426 let to_key = (tx.to.clone(), tx.token.clone());
427 let to_bal = self.balances.entry(to_key).or_insert(0);
428 *to_bal = to_bal.saturating_add(tx.amount);
429
430 if tx.fee > 0 {
431 let miner_key = (miner.to_string(), tx.token.clone());
432 let miner_bal = self.balances.entry(miner_key).or_insert(0);
433 *miner_bal = miner_bal.saturating_add(tx.fee);
434 }
435
436 self.nonces
437 .entry(tx.from.clone())
438 .and_modify(|n| *n += 1)
439 .or_insert(1);
440
441 self.modified = true;
442
443 let receipt = TxReceipt {
444 tx_hash: hex::encode(tx.tx_hash),
445 from: tx.from.clone(),
446 to: tx.to.clone(),
447 token: tx.token.clone(),
448 amount: tx.amount,
449 fee: tx.fee,
450 block_height: self.block_height,
451 timestamp: tx.timestamp,
452 };
453 self.tx_history.push(receipt.clone());
454
455 Ok(receipt)
456 }
457
458 pub fn rollback_transfer(
459 &mut self,
460 tx: &TransferTransaction,
461 miner: &str,
462 ) -> Result<(), String> {
463 let to_key = (tx.to.clone(), tx.token.clone());
464 let to_bal = self.balances.get(&to_key).copied().unwrap_or(0);
465 if to_bal < tx.amount {
466 return Err(format!(
467 "Insufficient balance to rollback recipient: have {}, need {}",
468 to_bal, tx.amount
469 ));
470 }
471 self.balances.insert(to_key, to_bal - tx.amount);
472
473 if tx.fee > 0 {
474 let miner_key = (miner.to_string(), tx.token.clone());
475 let miner_bal = self.balances.get(&miner_key).copied().unwrap_or(0);
476 if miner_bal < tx.fee {
477 return Err(format!(
478 "Insufficient balance to rollback miner fee: have {}, need {}",
479 miner_bal, tx.fee
480 ));
481 }
482 self.balances.insert(miner_key, miner_bal - tx.fee);
483 }
484
485 let from_key = (tx.from.clone(), tx.token.clone());
486 let from_bal = self.balances.entry(from_key).or_insert(0);
487 *from_bal = from_bal.saturating_add(tx.amount + tx.fee);
488
489 self.nonces.entry(tx.from.clone()).and_modify(|n| {
490 if *n > 0 {
491 *n -= 1
492 }
493 });
494
495 self.modified = true;
496 Ok(())
497 }
498
499 pub fn apply_coinbase(&mut self, cb: &CoinbaseTransaction) -> Result<(), String> {
500 let total_proof_rewards: u64 = cb.proof_rewards.iter().map(|pr| pr.reward_amount).sum();
501 let total_mint = cb.block_reward + total_proof_rewards;
502
503 let current_supply = self.total_supply_of(&TokenType::TribeChain);
504 if current_supply
505 .checked_add(total_mint)
506 .is_none_or(|s| s > TRIBE_HARD_CAP)
507 {
508 return Err("Supply cap exceeded".into());
509 }
510
511 let miner_key = (cb.miner_address.clone(), TokenType::TribeChain);
512 let miner_bal = self.balances.entry(miner_key).or_insert(0);
513 *miner_bal = miner_bal.saturating_add(cb.block_reward);
514
515 for pr in &cb.proof_rewards {
516 let pr_key = (pr.miner_pubkey.clone(), TokenType::TribeChain);
517 let pr_bal = self.balances.entry(pr_key).or_insert(0);
518 *pr_bal = pr_bal.saturating_add(pr.reward_amount);
519 }
520
521 self.coinbase_maturity
522 .entry(cb.miner_address.clone())
523 .or_default()
524 .push(CoinbaseEntry {
525 amount: cb.block_reward,
526 mature_at_height: cb.height + COINBASE_MATURITY_DEPTH,
527 });
528
529 *self
530 .total_supply_map
531 .entry(TokenType::TribeChain)
532 .or_insert(0) += total_mint;
533
534 self.modified = true;
535 Ok(())
536 }
537
538 pub fn rollback_coinbase(&mut self, cb: &CoinbaseTransaction) -> Result<(), String> {
539 let miner_key = (cb.miner_address.clone(), TokenType::TribeChain);
540 let miner_bal = self.balances.entry(miner_key).or_insert(0);
541 *miner_bal = miner_bal.saturating_sub(cb.block_reward);
542
543 for pr in &cb.proof_rewards {
544 let pr_key = (pr.miner_pubkey.clone(), TokenType::TribeChain);
545 let pr_bal = self.balances.entry(pr_key).or_insert(0);
546 *pr_bal = pr_bal.saturating_sub(pr.reward_amount);
547 }
548
549 if let Some(entries) = self.coinbase_maturity.get_mut(&cb.miner_address) {
550 entries.pop();
551 }
552
553 let total_proof_rewards: u64 = cb.proof_rewards.iter().map(|pr| pr.reward_amount).sum();
554 *self
555 .total_supply_map
556 .entry(TokenType::TribeChain)
557 .or_insert(0) -= cb.block_reward + total_proof_rewards;
558
559 self.modified = true;
560 Ok(())
561 }
562
563 pub fn apply_block(&mut self, block: &HexBlock) -> Result<Vec<TxReceipt>, String> {
564 let txs = block
565 .transactions
566 .as_ref()
567 .ok_or_else(|| "Block has no transactions".to_string())?;
568
569 if txs.is_empty() {
570 return Err("Block has no transactions".into());
571 }
572
573 self.block_height = block.height;
574
575 let cb: CoinbaseTransaction = serde_json::from_value(txs[0].clone())
576 .map_err(|e| format!("Failed to deserialize coinbase: {}", e))?;
577 let miner = cb.miner_address.clone();
578
579 self.apply_coinbase(&cb)?;
580
581 let mut receipts = Vec::new();
582 for tx_val in txs[1..].iter() {
583 let tx: TransferTransaction = serde_json::from_value(tx_val.clone())
584 .map_err(|e| format!("Failed to deserialize transfer: {}", e))?;
585 let receipt = self.apply_transfer(&tx, &miner)?;
586 receipts.push(receipt);
587 }
588
589 Ok(receipts)
590 }
591
592 pub fn rollback_block(&mut self, block: &HexBlock) -> Result<(), String> {
593 let txs = block
594 .transactions
595 .as_ref()
596 .ok_or_else(|| "Block has no transactions".to_string())?;
597
598 if txs.is_empty() {
599 return Err("Block has no transactions".into());
600 }
601
602 let cb: CoinbaseTransaction = serde_json::from_value(txs[0].clone())
603 .map_err(|e| format!("Failed to deserialize coinbase: {}", e))?;
604 let miner = cb.miner_address.clone();
605
606 for tx_val in txs[1..].iter().rev() {
607 let tx: TransferTransaction = serde_json::from_value(tx_val.clone())
608 .map_err(|e| format!("Failed to deserialize transfer: {}", e))?;
609 self.rollback_transfer(&tx, &miner)?;
610 }
611
612 self.rollback_coinbase(&cb)?;
613
614 Ok(())
615 }
616
617 pub fn compute_state_root(&self) -> [u8; 32] {
618 use crate::state_root::compute_state_root;
619 use std::collections::BTreeMap;
620
621 let mut balances = BTreeMap::new();
622 for ((addr, token), balance) in &self.balances {
623 let key = format!("{:?}:{:?}", addr, token);
624 balances.insert(key, *balance);
625 }
626
627 let mut nonces = BTreeMap::new();
628 for (addr, nonce) in &self.nonces {
629 nonces.insert(addr.clone(), *nonce);
630 }
631
632 compute_state_root(&balances, &nonces)
633 }
634
635 pub fn mint_tokens(&mut self, to: &str, token: &TokenType, amount: u64) -> Result<(), String> {
636 let current = self.total_supply_of(token);
637 if current
638 .checked_add(amount)
639 .is_none_or(|s| *token == TokenType::TribeChain && s > TRIBE_HARD_CAP)
640 {
641 return Err("Supply cap exceeded".into());
642 }
643 self.issue(to, token, amount);
644 Ok(())
645 }
646}
647
648pub fn load_ledger(path: &str, protocol_fee_address: &str) -> Ledger {
649 let content = std::fs::read_to_string(path).ok();
650 let mut ledger = Ledger::new(protocol_fee_address.to_string());
651 if let Some(ref s) = content {
652 if let Ok(snapshot) = serde_json::from_str::<LedgerSnapshot>(s) {
654 for entry in snapshot.entries {
655 let key = (entry.address, entry.token);
656 ledger.balances.insert(key, entry.balance);
657 }
658 ledger.tx_history = snapshot.tx_history;
659 for (addr, nonce) in snapshot.nonces {
660 ledger.nonces.insert(addr, nonce);
661 }
662 ledger.block_height = snapshot.block_height;
663 } else if let Ok(entries) = serde_json::from_str::<Vec<LedgerEntry>>(s) {
664 for entry in entries {
665 let key = (entry.address, entry.token);
666 ledger.balances.insert(key, entry.balance);
667 }
668 }
669 }
670 ledger.modified = false;
671 ledger
672}
673
674pub fn spawn_persist_ledger(ledger: Arc<RwLock<Ledger>>, path: String) {
675 tokio::spawn(async move {
676 let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(5));
677 loop {
678 interval.tick().await;
679 let snapshot: LedgerSnapshot = {
680 let l = ledger.read().await;
681 if !l.is_modified() {
682 continue;
683 }
684 let entries: Vec<LedgerEntry> = l
685 .balances
686 .iter()
687 .map(|((addr, token), bal)| LedgerEntry {
688 address: addr.clone(),
689 token: token.clone(),
690 balance: *bal,
691 })
692 .collect();
693 let nonces: Vec<(String, u64)> =
694 l.nonces.iter().map(|(k, v)| (k.clone(), *v)).collect();
695 LedgerSnapshot {
696 entries,
697 tx_history: l.tx_history.clone(),
698 nonces,
699 block_height: l.block_height,
700 }
701 };
702 let path_clone = path.clone();
703 let json = serde_json::to_string_pretty(&snapshot).unwrap();
704 let write_result =
705 tokio::task::spawn_blocking(move || std::fs::write(&path_clone, json)).await;
706 match write_result {
707 Ok(Ok(())) => {
708 let mut l = ledger.write().await;
709 l.clear_modified();
710 }
711 Ok(Err(e)) => {
712 tracing::warn!(error = %e, "Failed to persist ledger");
713 }
714 Err(e) => {
715 tracing::warn!(error = %e, "spawn_blocking join error for ledger persist");
716 }
717 }
718 }
719 });
720}
721
722pub const DEFAULT_LEDGER_PATH: &str = "ledger.json";
723
724#[cfg(test)]
725mod tests {
726 use super::*;
727 use crate::tx::ProofRewardEntry;
728
729 #[test]
730 fn test_issue_and_balance() {
731 let mut ledger = Ledger::new("protocol".into());
732 ledger.issue("alice", &TokenType::TribeChain, 1000);
733 assert_eq!(ledger.balance_of("alice", &TokenType::TribeChain), 1000);
734 assert_eq!(ledger.balance_of("bob", &TokenType::TribeChain), 0);
735 assert_eq!(ledger.block_height(), 1);
736 }
737
738 #[test]
739 fn test_transfer_basic() {
740 let mut ledger = Ledger::new("protocol".into());
741 ledger.issue("alice", &TokenType::TribeChain, 1000);
742 let receipt = ledger
743 .transfer("alice", "bob", &TokenType::TribeChain, 300, 10)
744 .unwrap();
745 assert_eq!(receipt.amount, 300);
746 assert_eq!(receipt.fee, 10);
747 assert_eq!(ledger.balance_of("alice", &TokenType::TribeChain), 690);
748 assert_eq!(ledger.balance_of("bob", &TokenType::TribeChain), 300);
749 assert_eq!(ledger.balance_of("protocol", &TokenType::TribeChain), 10);
750 }
751
752 #[test]
753 fn test_transfer_insufficient_balance() {
754 let mut ledger = Ledger::new("protocol".into());
755 ledger.issue("alice", &TokenType::TribeChain, 100);
756 let err = ledger
757 .transfer("alice", "bob", &TokenType::TribeChain, 200, 0)
758 .unwrap_err();
759 assert!(err.contains("Insufficient balance"));
760 }
761
762 #[test]
763 fn test_transfer_zero_amount() {
764 let mut ledger = Ledger::new("protocol".into());
765 let err = ledger
766 .transfer("alice", "bob", &TokenType::TribeChain, 0, 0)
767 .unwrap_err();
768 assert!(err.contains("positive"));
769 }
770
771 #[test]
772 fn test_tx_history() {
773 let mut ledger = Ledger::new("protocol".into());
774 ledger.issue("alice", &TokenType::TribeChain, 1000);
775 ledger
776 .transfer("alice", "bob", &TokenType::TribeChain, 200, 5)
777 .unwrap();
778 assert_eq!(ledger.tx_history().len(), 2);
779 let alice_txs = ledger.tx_history_for("alice");
780 assert_eq!(alice_txs.len(), 2);
781 let bob_txs = ledger.tx_history_for("bob");
782 assert_eq!(bob_txs.len(), 1);
783 let charlie_txs = ledger.tx_history_for("charlie");
784 assert_eq!(charlie_txs.len(), 0);
785 }
786
787 #[test]
788 fn test_load_ledger_persistence_roundtrip() {
789 let dir = tempfile::tempdir().unwrap();
790 let path = dir.path().join("test_ledger.json");
791 let path_str = path.to_str().unwrap().to_string();
792
793 {
794 let mut ledger = Ledger::new("fee_addr".into());
795 ledger.issue("alice", &TokenType::PTtC, 500);
796 let entries: Vec<LedgerEntry> = ledger
797 .balances
798 .iter()
799 .map(|((addr, token), bal)| LedgerEntry {
800 address: addr.clone(),
801 token: token.clone(),
802 balance: *bal,
803 })
804 .collect();
805 std::fs::write(&path, serde_json::to_string_pretty(&entries).unwrap()).unwrap();
806 }
807
808 let loaded = load_ledger(&path_str, "fee_addr");
809 assert_eq!(loaded.balance_of("alice", &TokenType::PTtC), 500);
810 assert_eq!(loaded.balance_of("bob", &TokenType::PTtC), 0);
811 assert!(!loaded.is_modified());
812 }
813
814 #[test]
815 fn test_no_fee_when_protocol_is_recipient() {
816 let mut ledger = Ledger::new("bob".into());
817 ledger.issue("alice", &TokenType::TribeChain, 1000);
818 ledger
819 .transfer("alice", "bob", &TokenType::TribeChain, 300, 10)
820 .unwrap();
821 assert_eq!(ledger.balance_of("alice", &TokenType::TribeChain), 690);
822 assert_eq!(ledger.balance_of("bob", &TokenType::TribeChain), 310);
823 }
824
825 #[test]
826 fn test_multiple_token_types_independent() {
827 let mut ledger = Ledger::new("protocol".into());
828 ledger.issue("alice", &TokenType::TribeChain, 100);
829 ledger.issue("alice", &TokenType::PTtC, 200);
830 assert_eq!(ledger.balance_of("alice", &TokenType::TribeChain), 100);
831 assert_eq!(ledger.balance_of("alice", &TokenType::PTtC), 200);
832 ledger
833 .transfer("alice", "bob", &TokenType::TribeChain, 50, 0)
834 .unwrap();
835 assert_eq!(ledger.balance_of("alice", &TokenType::TribeChain), 50);
836 assert_eq!(ledger.balance_of("alice", &TokenType::PTtC), 200);
837 }
838
839 #[test]
840 fn test_block_reward_halving() {
841 assert_eq!(block_reward_at_height(0), TRIBE_BLOCK_REWARD_INITIAL);
842 assert_eq!(
843 block_reward_at_height(TRIBE_HALVING_INTERVAL),
844 TRIBE_BLOCK_REWARD_INITIAL / 2
845 );
846 assert_eq!(block_reward_at_height(TRIBE_HALVING_INTERVAL * 64), 0);
847 }
848
849 #[test]
850 fn test_apply_transfer_normal() {
851 let mut ledger = Ledger::new("protocol".to_string());
852 ledger
853 .mint_tokens("alice", &TokenType::TribeChain, 1000)
854 .unwrap();
855 let tx = TransferTransaction {
856 tx_hash: [0u8; 32],
857 nonce: 0,
858 from: "alice".to_string(),
859 to: "bob".to_string(),
860 token: TokenType::TribeChain,
861 amount: 100,
862 fee: 1,
863 signature: vec![],
864 timestamp: 0,
865 };
866 let result = ledger.apply_transfer(&tx, "miner").unwrap();
867 assert_eq!(ledger.balance_of("alice", &TokenType::TribeChain), 899);
868 assert_eq!(ledger.balance_of("bob", &TokenType::TribeChain), 100);
869 assert_eq!(ledger.balance_of("miner", &TokenType::TribeChain), 1);
870 assert_eq!(ledger.current_nonce("alice"), 1);
871 }
872
873 #[test]
874 fn test_apply_transfer_insufficient() {
875 let mut ledger = Ledger::new("protocol".to_string());
876 ledger
877 .mint_tokens("alice", &TokenType::TribeChain, 50)
878 .unwrap();
879 let tx = TransferTransaction {
880 tx_hash: [0u8; 32],
881 nonce: 0,
882 from: "alice".to_string(),
883 to: "bob".to_string(),
884 token: TokenType::TribeChain,
885 amount: 100,
886 fee: 1,
887 signature: vec![],
888 timestamp: 0,
889 };
890 assert!(ledger.apply_transfer(&tx, "miner").is_err());
891 }
892
893 #[test]
894 fn test_supply_cap_enforcement() {
895 let mut ledger = Ledger::new("protocol".to_string());
896 ledger
897 .mint_tokens("alice", &TokenType::TribeChain, TRIBE_HARD_CAP)
898 .unwrap();
899 let result = ledger.mint_tokens("bob", &TokenType::TribeChain, 1);
900 assert!(result.is_err(), "Should reject minting beyond hard cap");
901 }
902
903 #[test]
904 fn test_coinbase_maturity() {
905 let mut ledger = Ledger::new("protocol".to_string());
906 assert!(!ledger.is_coinbase_mature("miner", 0, 50));
907 assert!(ledger.is_coinbase_mature("miner", 0, 100));
908 assert!(ledger.is_coinbase_mature("miner", 0, 200));
909 }
910
911 #[test]
912 fn test_rollback_transfer() {
913 let mut ledger = Ledger::new("protocol".to_string());
914 ledger
915 .mint_tokens("alice", &TokenType::TribeChain, 1000)
916 .unwrap();
917 let alice_before = ledger.balance_of("alice", &TokenType::TribeChain);
918 let tx = TransferTransaction {
919 tx_hash: [0u8; 32],
920 nonce: 0,
921 from: "alice".to_string(),
922 to: "bob".to_string(),
923 token: TokenType::TribeChain,
924 amount: 200,
925 fee: 1,
926 signature: vec![],
927 timestamp: 0,
928 };
929 ledger.apply_transfer(&tx, "miner").unwrap();
930 ledger.rollback_transfer(&tx, "miner").unwrap();
931 assert_eq!(
932 ledger.balance_of("alice", &TokenType::TribeChain),
933 alice_before
934 );
935 assert_eq!(ledger.balance_of("bob", &TokenType::TribeChain), 0);
936 assert_eq!(ledger.balance_of("miner", &TokenType::TribeChain), 0);
937 }
938
939 #[test]
940 fn test_current_nonce() {
941 let mut ledger = Ledger::new("protocol".to_string());
942 assert_eq!(ledger.current_nonce("alice"), 0);
943 ledger
944 .mint_tokens("alice", &TokenType::TribeChain, 1000)
945 .unwrap();
946 let tx = TransferTransaction {
947 tx_hash: [0u8; 32],
948 nonce: 0,
949 from: "alice".to_string(),
950 to: "bob".to_string(),
951 token: TokenType::TribeChain,
952 amount: 100,
953 fee: 0,
954 signature: vec![],
955 timestamp: 0,
956 };
957 ledger.apply_transfer(&tx, "miner").unwrap();
958 assert_eq!(ledger.current_nonce("alice"), 1);
959 }
960
961 #[test]
962 fn test_total_supply_of() {
963 let mut ledger = Ledger::new("protocol".to_string());
964 assert_eq!(ledger.total_supply_of(&TokenType::TribeChain), 0);
965 ledger
966 .mint_tokens("alice", &TokenType::TribeChain, 500)
967 .unwrap();
968 assert_eq!(ledger.total_supply_of(&TokenType::TribeChain), 500);
969 }
970
971 #[test]
972 fn test_apply_coinbase_supply_cap() {
973 let mut ledger = Ledger::new("protocol".to_string());
974 let cb = CoinbaseTransaction {
975 tx_hash: [0u8; 32],
976 height: 1,
977 miner_address: "miner".to_string(),
978 block_reward: TRIBE_HARD_CAP,
979 proof_rewards: vec![],
980 signature: vec![],
981 };
982 ledger.apply_coinbase(&cb).unwrap();
983 let cb2 = CoinbaseTransaction {
984 tx_hash: [1u8; 32],
985 height: 2,
986 miner_address: "miner".to_string(),
987 block_reward: 1,
988 proof_rewards: vec![],
989 signature: vec![],
990 };
991 assert!(ledger.apply_coinbase(&cb2).is_err());
992 }
993
994 #[test]
995 fn test_apply_rollback_block() {
996 let mut ledger = Ledger::new("protocol".to_string());
997 ledger
998 .mint_tokens("alice", &TokenType::TribeChain, 1000)
999 .unwrap();
1000
1001 let tx1 = serde_json::to_value(TransferTransaction {
1002 tx_hash: [1u8; 32],
1003 nonce: 0,
1004 from: "alice".to_string(),
1005 to: "bob".to_string(),
1006 token: TokenType::TribeChain,
1007 amount: 100,
1008 fee: 5,
1009 signature: vec![],
1010 timestamp: 0,
1011 })
1012 .unwrap();
1013
1014 let cb = serde_json::to_value(CoinbaseTransaction {
1015 tx_hash: [0u8; 32],
1016 height: 1,
1017 miner_address: "miner".to_string(),
1018 block_reward: 100,
1019 proof_rewards: vec![ProofRewardEntry {
1020 miner_pubkey: "prover1".to_string(),
1021 reward_amount: 10,
1022 proof_hash: "proof1".to_string(),
1023 }],
1024 signature: vec![],
1025 })
1026 .unwrap();
1027
1028 let block = HexBlock {
1029 parent_hash: [0u8; 32],
1030 height: 1,
1031 tx_merkle_root: [0u8; 32],
1032 transactions: Some(vec![cb, tx1]),
1033 miner_address: Some("miner".to_string()),
1034 timestamp: 100,
1035 nonce: 0,
1036 coord: hexchain_p2p::lattice_geometry::HCPCoord { q: 0, r: 0, s: 0 },
1037 neighbor_hashes: [hexchain_p2p::types::NEIGHBOR_SLOT_EMPTY; 12],
1038 tensor: hexchain_p2p::types::TensorMeta {
1039 expected_capacity: 1000,
1040 actual_capacity: 1000,
1041 compression_num: 1,
1042 compression_den: 1,
1043 },
1044 };
1045
1046 let receipts = ledger.apply_block(&block).unwrap();
1047 assert_eq!(receipts.len(), 1);
1048 assert_eq!(ledger.balance_of("alice", &TokenType::TribeChain), 895);
1049 assert_eq!(ledger.balance_of("bob", &TokenType::TribeChain), 100);
1050 assert!(ledger.balance_of("miner", &TokenType::TribeChain) >= 100);
1051 assert_eq!(ledger.block_height(), 1);
1052
1053 ledger.rollback_block(&block).unwrap();
1054 assert_eq!(ledger.balance_of("alice", &TokenType::TribeChain), 1000);
1055 assert_eq!(ledger.balance_of("bob", &TokenType::TribeChain), 0);
1056 }
1057
1058 #[test]
1059 fn test_try_issue_respects_pttc_cap() {
1060 let mut ledger = Ledger::new("protocol".to_string());
1061 let cap = 21_000_000_000_000u64;
1062 ledger.try_issue("alice", &TokenType::PTtC, cap).unwrap();
1063 assert!(ledger.try_issue("bob", &TokenType::PTtC, 1).is_err());
1064 }
1065}