Skip to main content

pot_o_extensions/
ledger.rs

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; // 1,000 TRIBE per block
15pub const TRIBE_HALVING_INTERVAL: u64 = 210_000; // ~73 days at 30s blocks
16pub const TRIBE_PROOF_REWARD: u64 = 10_000_000; // 10 TRIBE per proof
17pub 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>, // block height of last interaction
70    minter_allowlist: HashMap<TokenType, Vec<String>>,   // authorized minters per token
71}
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        // Update total supply map
106        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    /// Try to issue tokens, respecting supply caps from token_config
133    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        // Get token configuration
137        let config_set = token_config();
138
139        // Check if token has a supply cap configured
140        if let Some(config) = config_set.get(token) {
141            // Calculate current total supply
142            let current_supply = self.total_supply_of(token);
143
144            // Check if adding this amount would exceed the cap
145            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        // If all checks pass, issue the tokens
154        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        // Update total supply map
159        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        // Calculate burn amount based on token configuration
202        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        // Sender must have: amount + fee + burn
214        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        // Update total supply to reflect burn
240        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    /// Record an interaction (transfer/issue) for an address-token pair
320    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    /// Get the block height of the last interaction for an address-token pair
326    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    /// Apply age-based decay to a balance based on blocks elapsed
333    /// Returns the decayed amount using exponential decay formula
334    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        // Get token configuration
340        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            // No decay configured for this token
346            balance
347        }
348    }
349
350    /// Calculate AUM block reward with halving schedule
351    /// Halving occurs every 1M blocks
352    pub fn aum_block_reward(&self, height: u64) -> u64 {
353        const AUM_INITIAL_REWARD: u64 = 1_000_000_000; // 1 billion
354        const AUM_HALVING_INTERVAL: u64 = 1_000_000; // 1 million blocks
355
356        let halvings = height / AUM_HALVING_INTERVAL;
357        if halvings >= 64 {
358            return 0; // After 64 halvings, reward becomes 0
359        }
360        AUM_INITIAL_REWARD >> halvings
361    }
362
363    /// Add an address to the minter allowlist for a specific token
364    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    /// Check if an address is an authorized minter for a token
372    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    /// Try to issue tokens with minter authorization check
380    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        // Check minter authorization
388        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        // Use try_issue to respect supply caps
396        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        // Try full snapshot first, fall back to old entry list for backward compat
653        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}