1use crate::error::{Result, TokenError};
6use dashmap::DashMap;
7use serde::{Deserialize, Serialize};
8use tenzro_types::primitives::Address;
9use tenzro_types::asset::AssetId;
10use tenzro_storage::kv::{KvStore, CF_ACCOUNTS};
11use std::sync::Arc;
12use tracing::{debug, info, warn};
13
14pub const TNZO_DECIMALS: u8 = 18;
16
17const CIRCUIT_BREAKER_MAX_OUTFLOW_PERCENT: u128 = 10;
19const CIRCUIT_BREAKER_WINDOW_SECS: u64 = 3600;
21const CIRCUIT_BREAKER_COOLDOWN_SECS: u64 = 1800;
23
24pub const ONE_TNZO: u128 = 1_000_000_000_000_000_000;
26
27pub const MAX_SUPPLY: u128 = 1_000_000_000 * ONE_TNZO;
29
30pub struct CircuitBreaker {
41 max_outflow_per_window: u128,
43 window_seconds: u64,
45 current_outflow: parking_lot::RwLock<u128>,
47 window_start: parking_lot::RwLock<u64>,
49 tripped: parking_lot::RwLock<bool>,
51 cooldown_seconds: u64,
53 tripped_at: parking_lot::RwLock<Option<u64>>,
55}
56
57impl CircuitBreaker {
58 pub fn new(max_outflow_per_window: u128, window_seconds: u64, cooldown_seconds: u64) -> Self {
65 let now = std::time::SystemTime::now()
66 .duration_since(std::time::UNIX_EPOCH)
67 .unwrap_or_default()
68 .as_secs();
69
70 Self {
71 max_outflow_per_window,
72 window_seconds,
73 current_outflow: parking_lot::RwLock::new(0),
74 window_start: parking_lot::RwLock::new(now),
75 tripped: parking_lot::RwLock::new(false),
76 cooldown_seconds,
77 tripped_at: parking_lot::RwLock::new(None),
78 }
79 }
80
81 pub fn default_for_tnzo() -> Self {
84 let max_outflow = MAX_SUPPLY / 100 * CIRCUIT_BREAKER_MAX_OUTFLOW_PERCENT;
85 Self::new(max_outflow, CIRCUIT_BREAKER_WINDOW_SECS, CIRCUIT_BREAKER_COOLDOWN_SECS)
86 }
87
88 fn now_secs() -> u64 {
90 std::time::SystemTime::now()
91 .duration_since(std::time::UNIX_EPOCH)
92 .unwrap_or_default()
93 .as_secs()
94 }
95
96 pub fn is_tripped(&self) -> bool {
99 let tripped = *self.tripped.read();
100 if !tripped {
101 return false;
102 }
103
104 if let Some(trip_time) = *self.tripped_at.read() {
106 let now = Self::now_secs();
107 if now >= trip_time + self.cooldown_seconds {
108 self.reset_internal();
110 return false;
111 }
112 }
113
114 true
115 }
116
117 pub fn check_outflow(&self, amount: u128) -> Result<()> {
124 if self.is_tripped() {
125 let remaining = self.tripped_at.read()
126 .map(|t| {
127 let elapsed = Self::now_secs().saturating_sub(t);
128 self.cooldown_seconds.saturating_sub(elapsed)
129 })
130 .unwrap_or(self.cooldown_seconds);
131
132 return Err(TokenError::Unauthorized {
133 reason: format!(
134 "Circuit breaker tripped: outflow limit exceeded. \
135 Auto-reset in {} seconds.",
136 remaining
137 ),
138 });
139 }
140
141 let now = Self::now_secs();
142 let mut window_start = self.window_start.write();
143 let mut current_outflow = self.current_outflow.write();
144
145 if now >= *window_start + self.window_seconds {
147 *window_start = now;
148 *current_outflow = 0;
149 }
150
151 let new_outflow = current_outflow
153 .checked_add(amount)
154 .ok_or_else(|| TokenError::ArithmeticOverflow {
155 operation: "circuit breaker outflow check".to_string(),
156 })?;
157
158 if new_outflow > self.max_outflow_per_window {
159 drop(window_start);
161 drop(current_outflow);
162 self.trip();
163 return Err(TokenError::Unauthorized {
164 reason: format!(
165 "Circuit breaker tripped: transfer of {} would exceed \
166 window limit of {} ({} already used). \
167 Cooldown: {} seconds.",
168 amount,
169 self.max_outflow_per_window,
170 new_outflow - amount,
171 self.cooldown_seconds,
172 ),
173 });
174 }
175
176 Ok(())
177 }
178
179 pub fn record_outflow(&self, amount: u128) {
181 let mut current_outflow = self.current_outflow.write();
182 *current_outflow = current_outflow.saturating_add(amount);
183 }
184
185 pub fn reset(&self) {
187 self.reset_internal();
188 info!("Circuit breaker manually reset");
189 }
190
191 fn reset_internal(&self) {
193 *self.tripped.write() = false;
194 *self.tripped_at.write() = None;
195 *self.current_outflow.write() = 0;
196 *self.window_start.write() = Self::now_secs();
197 }
198
199 fn trip(&self) {
201 *self.tripped.write() = true;
202 *self.tripped_at.write() = Some(Self::now_secs());
203 warn!(
204 "Circuit breaker TRIPPED: outflow limit of {} exceeded in {}-second window. \
205 Transfers paused for {} seconds.",
206 self.max_outflow_per_window,
207 self.window_seconds,
208 self.cooldown_seconds,
209 );
210 }
211
212 pub fn current_outflow(&self) -> u128 {
214 *self.current_outflow.read()
215 }
216
217 pub fn max_outflow(&self) -> u128 {
219 self.max_outflow_per_window
220 }
221}
222
223impl std::fmt::Debug for CircuitBreaker {
224 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
225 f.debug_struct("CircuitBreaker")
226 .field("max_outflow_per_window", &self.max_outflow_per_window)
227 .field("window_seconds", &self.window_seconds)
228 .field("cooldown_seconds", &self.cooldown_seconds)
229 .field("tripped", &*self.tripped.read())
230 .field("current_outflow", &*self.current_outflow.read())
231 .finish()
232 }
233}
234
235pub trait StorageBackend: Send + Sync {
237 fn get_balance(&self, address: &Address) -> Result<Option<u128>>;
239
240 fn set_balance(&self, address: &Address, balance: u128) -> Result<()>;
242
243 fn get_total_supply(&self) -> Result<u128>;
245
246 fn set_total_supply(&self, supply: u128) -> Result<()>;
248}
249
250pub struct RocksDbBackend {
252 store: Arc<dyn KvStore>,
253}
254
255impl RocksDbBackend {
256 pub fn new(store: Arc<dyn KvStore>) -> Self {
258 Self { store }
259 }
260
261 fn balance_key(address: &Address) -> Vec<u8> {
263 let mut key = b"balance:".to_vec();
264 key.extend_from_slice(address.as_bytes());
265 key
266 }
267
268 fn supply_key() -> Vec<u8> {
270 b"total_supply".to_vec()
271 }
272}
273
274impl StorageBackend for RocksDbBackend {
275 fn get_balance(&self, address: &Address) -> Result<Option<u128>> {
276 let key = Self::balance_key(address);
277 match self.store.get(CF_ACCOUNTS, &key)? {
278 Some(bytes) => {
279 if bytes.len() == 16 {
280 let array: [u8; 16] = bytes.try_into().unwrap();
281 Ok(Some(u128::from_le_bytes(array)))
282 } else {
283 warn!("Invalid balance bytes length for {}: {}", address, bytes.len());
284 Ok(None)
285 }
286 }
287 None => Ok(None),
288 }
289 }
290
291 fn set_balance(&self, address: &Address, balance: u128) -> Result<()> {
292 let key = Self::balance_key(address);
293 let value = balance.to_le_bytes();
294 self.store.put(CF_ACCOUNTS, &key, &value)?;
295 Ok(())
296 }
297
298 fn get_total_supply(&self) -> Result<u128> {
299 let key = Self::supply_key();
300 match self.store.get(CF_ACCOUNTS, &key)? {
301 Some(bytes) => {
302 if bytes.len() == 16 {
303 let array: [u8; 16] = bytes.try_into().unwrap();
304 Ok(u128::from_le_bytes(array))
305 } else {
306 warn!("Invalid supply bytes length: {}", bytes.len());
307 Ok(0)
308 }
309 }
310 None => Ok(0),
311 }
312 }
313
314 fn set_total_supply(&self, supply: u128) -> Result<()> {
315 let key = Self::supply_key();
316 let value = supply.to_le_bytes();
317 self.store.put(CF_ACCOUNTS, &key, &value)?;
318 Ok(())
319 }
320}
321
322pub struct TnzoToken {
328 pub asset_id: AssetId,
330 balances: DashMap<Address, u128>,
332 total_supply: parking_lot::RwLock<u128>,
334 total_burned: parking_lot::RwLock<u128>,
336 treasury_address: parking_lot::RwLock<Option<Address>>,
338 storage: Option<Arc<dyn StorageBackend>>,
340 circuit_breaker: CircuitBreaker,
342}
343
344impl std::fmt::Debug for TnzoToken {
345 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
346 f.debug_struct("TnzoToken")
347 .field("asset_id", &self.asset_id)
348 .field("total_supply", &self.total_supply)
349 .field("total_burned", &self.total_burned)
350 .field("treasury_address", &self.treasury_address)
351 .field("storage", &self.storage.as_ref().map(|_| "Some(...)"))
352 .field("circuit_breaker", &self.circuit_breaker)
353 .finish()
354 }
355}
356
357impl TnzoToken {
358 pub fn new() -> Self {
360 Self {
361 asset_id: AssetId::tnzo(),
362 balances: DashMap::new(),
363 total_supply: parking_lot::RwLock::new(0),
364 total_burned: parking_lot::RwLock::new(0),
365 treasury_address: parking_lot::RwLock::new(None),
366 storage: None,
367 circuit_breaker: CircuitBreaker::default_for_tnzo(),
368 }
369 }
370
371 pub fn with_storage(storage: Arc<dyn StorageBackend>) -> Result<Self> {
373 let total_supply = storage.get_total_supply()?;
375
376 Ok(Self {
377 asset_id: AssetId::tnzo(),
378 balances: DashMap::new(),
379 total_supply: parking_lot::RwLock::new(total_supply),
380 total_burned: parking_lot::RwLock::new(0),
381 treasury_address: parking_lot::RwLock::new(None),
382 storage: Some(storage),
383 circuit_breaker: CircuitBreaker::default_for_tnzo(),
384 })
385 }
386
387 pub fn set_treasury_address(&self, address: Address) {
389 *self.treasury_address.write() = Some(address);
390 info!("Treasury address set to: {}", address);
391 }
392
393 pub fn treasury_address_ref(&self) -> Option<Address> {
395 *self.treasury_address.read()
396 }
397
398 pub fn balance_of(&self, address: &Address) -> u128 {
414 if let Some(storage) = &self.storage {
416 return match storage.get_balance(address) {
417 Ok(Some(balance)) => balance,
418 Ok(None) => 0,
419 Err(e) => {
420 warn!("Failed to load balance from storage: {}", e);
421 0
422 }
423 };
424 }
425
426 self.balances.get(address).map(|v| *v).unwrap_or(0)
428 }
429
430 fn persist_balance(&self, address: &Address, balance: u128) -> Result<()> {
432 if let Some(storage) = &self.storage {
433 storage.set_balance(address, balance)?;
434 }
435 Ok(())
436 }
437
438 fn persist_supply(&self, supply: u128) -> Result<()> {
440 if let Some(storage) = &self.storage {
441 storage.set_total_supply(supply)?;
442 }
443 Ok(())
444 }
445
446 pub fn transfer(&self, from: &Address, to: &Address, amount: u128) -> Result<()> {
458 if amount == 0 {
459 return Err(TokenError::InvalidAmount("Amount must be greater than zero".to_string()));
460 }
461
462 self.circuit_breaker.check_outflow(amount)?;
464
465 let from_balance = self.balance_of(from);
466 if from_balance < amount {
467 return Err(TokenError::InsufficientBalance {
468 required: amount,
469 available: from_balance,
470 });
471 }
472
473 let new_from_balance = from_balance.checked_sub(amount)
475 .ok_or_else(|| TokenError::ArithmeticOverflow {
476 operation: "transfer subtraction".to_string(),
477 })?;
478
479 let to_balance = self.balance_of(to);
481 let new_to_balance = to_balance.checked_add(amount)
482 .ok_or_else(|| TokenError::ArithmeticOverflow {
483 operation: "transfer addition".to_string(),
484 })?;
485
486 self.balances.insert(*from, new_from_balance);
488 self.balances.insert(*to, new_to_balance);
489
490 self.persist_balance(from, new_from_balance)?;
492 self.persist_balance(to, new_to_balance)?;
493
494 self.circuit_breaker.record_outflow(amount);
496
497 debug!("Transferred {} TNZO from {} to {}", amount, from, to);
498 Ok(())
499 }
500
501 pub fn mint(&self, to: &Address, amount: u128, caller: &Address) -> Result<()> {
509 let treasury = self.treasury_address.read();
511 if treasury.as_ref() != Some(caller) {
512 return Err(TokenError::Unauthorized {
513 reason: "Only treasury can mint tokens".to_string(),
514 });
515 }
516
517 if amount == 0 {
518 return Err(TokenError::InvalidAmount("Amount must be greater than zero".to_string()));
519 }
520
521 let current_supply = *self.total_supply.read();
523 let new_supply = current_supply.checked_add(amount)
524 .ok_or_else(|| TokenError::ArithmeticOverflow {
525 operation: "mint supply increase".to_string(),
526 })?;
527
528 if new_supply > MAX_SUPPLY {
529 return Err(TokenError::InvalidAmount(
530 format!("Minting would exceed max supply: {} > {}", new_supply, MAX_SUPPLY)
531 ));
532 }
533
534 let current_balance = self.balance_of(to);
536 let new_balance = current_balance.checked_add(amount)
537 .ok_or_else(|| TokenError::ArithmeticOverflow {
538 operation: "mint balance increase".to_string(),
539 })?;
540 self.balances.insert(*to, new_balance);
541
542 *self.total_supply.write() = new_supply;
544
545 self.persist_balance(to, new_balance)?;
547 self.persist_supply(new_supply)?;
548
549 info!("Minted {} TNZO to {}", amount, to);
550 Ok(())
551 }
552
553 pub fn burn(&self, from: &Address, amount: u128) -> Result<()> {
560 if amount == 0 {
561 return Err(TokenError::InvalidAmount("Amount must be greater than zero".to_string()));
562 }
563
564 let balance = self.balance_of(from);
565 if balance < amount {
566 return Err(TokenError::InsufficientBalance {
567 required: amount,
568 available: balance,
569 });
570 }
571
572 let new_balance = balance.checked_sub(amount)
574 .ok_or_else(|| TokenError::ArithmeticOverflow {
575 operation: "burn balance decrease".to_string(),
576 })?;
577 self.balances.insert(*from, new_balance);
578
579 let mut supply = self.total_supply.write();
581 *supply = supply.checked_sub(amount)
582 .ok_or_else(|| TokenError::ArithmeticOverflow {
583 operation: "burn supply decrease".to_string(),
584 })?;
585 let new_supply = *supply;
586 drop(supply);
587
588 let mut burned = self.total_burned.write();
590 *burned = burned.checked_add(amount)
591 .ok_or_else(|| TokenError::ArithmeticOverflow {
592 operation: "burn total increase".to_string(),
593 })?;
594
595 self.persist_balance(from, new_balance)?;
597 self.persist_supply(new_supply)?;
598
599 info!("Burned {} TNZO from {}", amount, from);
600 Ok(())
601 }
602
603 pub fn total_supply(&self) -> u128 {
605 *self.total_supply.read()
606 }
607
608 pub fn circulating_supply(&self) -> u128 {
610 self.total_supply()
611 }
612
613 pub fn total_burned(&self) -> u128 {
615 *self.total_burned.read()
616 }
617
618 pub fn stats(&self) -> TokenStats {
620 TokenStats {
621 total_supply: self.total_supply(),
622 circulating_supply: self.circulating_supply(),
623 total_burned: self.total_burned(),
624 total_accounts: self.balances.len() as u64,
625 }
626 }
627
628 pub fn circuit_breaker(&self) -> &CircuitBreaker {
630 &self.circuit_breaker
631 }
632
633 pub fn get_all_balances(&self) -> Vec<(Address, u128)> {
635 self.balances
636 .iter()
637 .map(|entry| (*entry.key(), *entry.value()))
638 .collect()
639 }
640}
641
642impl Default for TnzoToken {
643 fn default() -> Self {
644 Self::new()
645 }
646}
647
648#[derive(Debug, Clone, Serialize, Deserialize)]
650pub struct TokenStats {
651 pub total_supply: u128,
653 pub circulating_supply: u128,
655 pub total_burned: u128,
657 pub total_accounts: u64,
659}
660
661#[cfg(test)]
662mod tests {
663 use super::*;
664
665 #[test]
666 fn test_token_creation() {
667 let token = TnzoToken::new();
668 assert_eq!(token.total_supply(), 0);
669 assert_eq!(token.total_burned(), 0);
670 }
671
672 #[test]
673 fn test_transfer() {
674 let token = TnzoToken::new();
675 let from = Address::new([1u8; 32]);
676 let to = Address::new([2u8; 32]);
677
678 token.balances.insert(from, 1000 * ONE_TNZO);
680
681 token.transfer(&from, &to, 100 * ONE_TNZO).unwrap();
683
684 assert_eq!(token.balance_of(&from), 900 * ONE_TNZO);
685 assert_eq!(token.balance_of(&to), 100 * ONE_TNZO);
686 }
687
688 #[test]
689 fn test_mint() {
690 let token = TnzoToken::new();
691 let treasury = Address::new([1u8; 32]);
692 let recipient = Address::new([2u8; 32]);
693
694 token.set_treasury_address(treasury);
695 token.mint(&recipient, 1000 * ONE_TNZO, &treasury).unwrap();
696
697 assert_eq!(token.balance_of(&recipient), 1000 * ONE_TNZO);
698 assert_eq!(token.total_supply(), 1000 * ONE_TNZO);
699 }
700
701 #[test]
702 fn test_burn() {
703 let token = TnzoToken::new();
704 let address = Address::new([1u8; 32]);
705
706 token.balances.insert(address, 1000 * ONE_TNZO);
708 *token.total_supply.write() = 1000 * ONE_TNZO;
709
710 token.burn(&address, 100 * ONE_TNZO).unwrap();
712
713 assert_eq!(token.balance_of(&address), 900 * ONE_TNZO);
714 assert_eq!(token.total_supply(), 900 * ONE_TNZO);
715 assert_eq!(token.total_burned(), 100 * ONE_TNZO);
716 }
717
718 #[test]
723 fn test_circuit_breaker_allows_normal_transfer() {
724 let cb = CircuitBreaker::new(1000 * ONE_TNZO, 3600, 1800);
726 assert!(!cb.is_tripped());
727 assert!(cb.check_outflow(500 * ONE_TNZO).is_ok());
728 cb.record_outflow(500 * ONE_TNZO);
729 assert_eq!(cb.current_outflow(), 500 * ONE_TNZO);
730 }
731
732 #[test]
733 fn test_circuit_breaker_trips_on_excess() {
734 let cb = CircuitBreaker::new(1000 * ONE_TNZO, 3600, 1800);
735 cb.record_outflow(900 * ONE_TNZO);
736 let result = cb.check_outflow(200 * ONE_TNZO);
738 assert!(result.is_err());
739 assert!(cb.is_tripped());
740 }
741
742 #[test]
743 fn test_circuit_breaker_blocks_after_trip() {
744 let cb = CircuitBreaker::new(1000 * ONE_TNZO, 3600, 1800);
745 cb.record_outflow(900 * ONE_TNZO);
746 let _ = cb.check_outflow(200 * ONE_TNZO); assert!(cb.is_tripped());
748
749 let result = cb.check_outflow(1);
751 assert!(result.is_err());
752 }
753
754 #[test]
755 fn test_circuit_breaker_manual_reset() {
756 let cb = CircuitBreaker::new(1000 * ONE_TNZO, 3600, 1800);
757 cb.record_outflow(900 * ONE_TNZO);
758 let _ = cb.check_outflow(200 * ONE_TNZO); assert!(cb.is_tripped());
760
761 cb.reset();
762 assert!(!cb.is_tripped());
763 assert_eq!(cb.current_outflow(), 0);
764 assert!(cb.check_outflow(500 * ONE_TNZO).is_ok());
765 }
766
767 #[test]
768 fn test_circuit_breaker_wired_into_transfer() {
769 let token = TnzoToken::new();
770 let from = Address::new([1u8; 32]);
771 let to = Address::new([2u8; 32]);
772
773 token.balances.insert(from, MAX_SUPPLY);
775 *token.total_supply.write() = MAX_SUPPLY;
776
777 let limit = MAX_SUPPLY / 100 * CIRCUIT_BREAKER_MAX_OUTFLOW_PERCENT;
779
780 let small_amount = limit / 2;
782 assert!(token.transfer(&from, &to, small_amount).is_ok());
783
784 let over_amount = limit;
786 let result = token.transfer(&from, &to, over_amount);
787 assert!(result.is_err());
788 assert!(token.circuit_breaker().is_tripped());
789
790 token.circuit_breaker().reset();
792 assert!(token.transfer(&from, &to, small_amount).is_ok());
793 }
794
795 #[test]
796 fn test_treasury_address_ref() {
797 let token = TnzoToken::new();
798 assert!(token.treasury_address_ref().is_none());
799
800 let treasury = Address::new([0xFF; 32]);
801 token.set_treasury_address(treasury);
802 assert_eq!(token.treasury_address_ref(), Some(treasury));
803 }
804
805 #[test]
814 fn test_balance_of_reflects_external_storage_writes() {
815 use std::sync::Mutex as StdMutex;
816
817 struct MockBackend {
818 balances: StdMutex<std::collections::HashMap<Address, u128>>,
819 supply: StdMutex<u128>,
820 }
821 impl StorageBackend for MockBackend {
822 fn get_balance(&self, address: &Address) -> Result<Option<u128>> {
823 Ok(self.balances.lock().unwrap().get(address).copied())
824 }
825 fn set_balance(&self, address: &Address, balance: u128) -> Result<()> {
826 self.balances.lock().unwrap().insert(*address, balance);
827 Ok(())
828 }
829 fn get_total_supply(&self) -> Result<u128> {
830 Ok(*self.supply.lock().unwrap())
831 }
832 fn set_total_supply(&self, s: u128) -> Result<()> {
833 *self.supply.lock().unwrap() = s;
834 Ok(())
835 }
836 }
837
838 let backend = Arc::new(MockBackend {
839 balances: StdMutex::new(Default::default()),
840 supply: StdMutex::new(0),
841 });
842
843 let token = TnzoToken::with_storage(backend.clone() as Arc<dyn StorageBackend>).unwrap();
844 let recipient = Address::new([0xAB; 32]);
845
846 assert_eq!(token.balance_of(&recipient), 0);
848
849 backend.balances.lock().unwrap().insert(recipient, 5_000 * ONE_TNZO);
852
853 assert_eq!(token.balance_of(&recipient), 5_000 * ONE_TNZO);
855
856 backend.balances.lock().unwrap().insert(recipient, 1_000 * ONE_TNZO);
858 assert_eq!(token.balance_of(&recipient), 1_000 * ONE_TNZO);
859 }
860}