use crate::cross_vm::TokenId;
use crate::error::{Result, TokenError};
use crate::registry::TokenRegistry;
use crate::tnzo::TnzoToken;
use dashmap::DashMap;
use serde::{Deserialize, Serialize};
use std::sync::atomic::{AtomicI64, AtomicU64, Ordering};
use std::sync::Arc;
use tenzro_types::primitives::Address;
use tracing::{debug, info};
struct AtomicU128(parking_lot::Mutex<u128>);
impl AtomicU128 {
fn new(v: u128) -> Self {
Self(parking_lot::Mutex::new(v))
}
fn load(&self) -> u128 {
*self.0.lock()
}
fn store(&self, v: u128) {
*self.0.lock() = v;
}
fn fetch_add(&self, v: u128) -> u128 {
let mut guard = self.0.lock();
let old = *guard;
*guard = old.saturating_add(v);
old
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CrosschainMintEvent {
pub nonce: u64,
pub to: Address,
pub amount: u128,
pub sender: Vec<u8>,
pub bridge: [u8; 20],
pub timestamp: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CrosschainBurnEvent {
pub nonce: u64,
pub from: Address,
pub amount: u128,
pub sender: Vec<u8>,
pub bridge: [u8; 20],
pub timestamp: i64,
}
pub struct BridgeAuthorization {
pub bridge_address: [u8; 20],
pub name: String,
pub authorized_tokens: Vec<TokenId>,
pub max_mint_per_tx: Option<u128>,
pub max_burn_per_tx: Option<u128>,
pub daily_mint_limit: Option<u128>,
pub daily_burn_limit: Option<u128>,
minted_today: AtomicU128,
burned_today: AtomicU128,
last_reset: AtomicI64,
pub enabled: bool,
}
impl BridgeAuthorization {
fn maybe_reset_daily(&self, now: i64) {
let last = self.last_reset.load(Ordering::Relaxed);
if now - last >= 86_400 {
self.minted_today.store(0);
self.burned_today.store(0);
self.last_reset.store(now, Ordering::Relaxed);
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BridgeInfo {
pub bridge_address: String,
pub name: String,
pub authorized_tokens: Vec<String>,
pub max_mint_per_tx: Option<u128>,
pub max_burn_per_tx: Option<u128>,
pub daily_mint_limit: Option<u128>,
pub daily_burn_limit: Option<u128>,
pub minted_today: u128,
pub burned_today: u128,
pub enabled: bool,
}
pub struct CrosschainTokenManager {
authorized_bridges: DashMap<[u8; 20], BridgeAuthorization>,
tnzo_token: Arc<TnzoToken>,
registry: Arc<TokenRegistry>,
mint_events: DashMap<u64, CrosschainMintEvent>,
burn_events: DashMap<u64, CrosschainBurnEvent>,
nonce: AtomicU64,
}
impl CrosschainTokenManager {
pub fn new(tnzo_token: Arc<TnzoToken>, registry: Arc<TokenRegistry>) -> Self {
Self {
authorized_bridges: DashMap::new(),
tnzo_token,
registry,
mint_events: DashMap::new(),
burn_events: DashMap::new(),
nonce: AtomicU64::new(1),
}
}
pub fn authorize_bridge(
&self,
bridge_address: [u8; 20],
name: String,
authorized_tokens: Vec<TokenId>,
max_mint_per_tx: Option<u128>,
max_burn_per_tx: Option<u128>,
daily_mint_limit: Option<u128>,
daily_burn_limit: Option<u128>,
) -> Result<()> {
let now = chrono::Utc::now().timestamp();
let auth = BridgeAuthorization {
bridge_address,
name: name.clone(),
authorized_tokens,
max_mint_per_tx,
max_burn_per_tx,
daily_mint_limit,
daily_burn_limit,
minted_today: AtomicU128::new(0),
burned_today: AtomicU128::new(0),
last_reset: AtomicI64::new(now),
enabled: true,
};
info!(
"Authorized bridge {} (0x{})",
name,
hex::encode(bridge_address)
);
self.authorized_bridges.insert(bridge_address, auth);
Ok(())
}
pub fn revoke_bridge(&self, bridge_address: &[u8; 20]) -> Result<()> {
match self.authorized_bridges.remove(bridge_address) {
Some((_, auth)) => {
info!(
"Revoked bridge {} (0x{})",
auth.name,
hex::encode(bridge_address)
);
Ok(())
}
None => Err(TokenError::Unauthorized {
reason: format!(
"Bridge 0x{} is not authorized",
hex::encode(bridge_address)
),
}),
}
}
pub fn update_bridge_limits(
&self,
bridge_address: &[u8; 20],
max_mint_per_tx: Option<u128>,
max_burn_per_tx: Option<u128>,
daily_mint_limit: Option<u128>,
daily_burn_limit: Option<u128>,
) -> Result<()> {
let mut entry = self
.authorized_bridges
.get_mut(bridge_address)
.ok_or_else(|| TokenError::Unauthorized {
reason: format!(
"Bridge 0x{} is not authorized",
hex::encode(bridge_address)
),
})?;
let auth = entry.value_mut();
auth.max_mint_per_tx = max_mint_per_tx;
auth.max_burn_per_tx = max_burn_per_tx;
auth.daily_mint_limit = daily_mint_limit;
auth.daily_burn_limit = daily_burn_limit;
debug!(
"Updated limits for bridge 0x{}",
hex::encode(bridge_address)
);
Ok(())
}
pub fn is_bridge_authorized(&self, bridge_address: &[u8; 20]) -> bool {
self.authorized_bridges
.get(bridge_address)
.map(|e| e.value().enabled)
.unwrap_or(false)
}
pub fn get_bridge_info(&self, bridge_address: &[u8; 20]) -> Option<BridgeInfo> {
self.authorized_bridges.get(bridge_address).map(|entry| {
let auth = entry.value();
BridgeInfo {
bridge_address: format!("0x{}", hex::encode(auth.bridge_address)),
name: auth.name.clone(),
authorized_tokens: auth
.authorized_tokens
.iter()
.map(|t| t.to_hex())
.collect(),
max_mint_per_tx: auth.max_mint_per_tx,
max_burn_per_tx: auth.max_burn_per_tx,
daily_mint_limit: auth.daily_mint_limit,
daily_burn_limit: auth.daily_burn_limit,
minted_today: auth.minted_today.load(),
burned_today: auth.burned_today.load(),
enabled: auth.enabled,
}
})
}
pub fn resolve_authorized_tokens(
&self,
bridge_address: &[u8; 20],
) -> Vec<crate::TokenDefinition> {
let entry = match self.authorized_bridges.get(bridge_address) {
Some(e) => e,
None => return Vec::new(),
};
entry
.value()
.authorized_tokens
.iter()
.filter_map(|tid| self.registry.get(tid))
.collect()
}
pub fn list_authorized_bridges(&self) -> Vec<BridgeInfo> {
self.authorized_bridges
.iter()
.map(|entry| {
let auth = entry.value();
BridgeInfo {
bridge_address: format!("0x{}", hex::encode(auth.bridge_address)),
name: auth.name.clone(),
authorized_tokens: auth
.authorized_tokens
.iter()
.map(|t| t.to_hex())
.collect(),
max_mint_per_tx: auth.max_mint_per_tx,
max_burn_per_tx: auth.max_burn_per_tx,
daily_mint_limit: auth.daily_mint_limit,
daily_burn_limit: auth.daily_burn_limit,
minted_today: auth.minted_today.load(),
burned_today: auth.burned_today.load(),
enabled: auth.enabled,
}
})
.collect()
}
fn validate_bridge(
&self,
bridge_address: &[u8; 20],
amount: u128,
is_mint: bool,
) -> Result<()> {
let entry =
self.authorized_bridges
.get(bridge_address)
.ok_or_else(|| TokenError::Unauthorized {
reason: format!(
"Bridge 0x{} is not authorized",
hex::encode(bridge_address)
),
})?;
let auth = entry.value();
if !auth.enabled {
return Err(TokenError::Unauthorized {
reason: format!(
"Bridge 0x{} is disabled",
hex::encode(bridge_address)
),
});
}
if amount == 0 {
return Err(TokenError::InvalidAmount(
"Cross-chain amount must be greater than zero".to_string(),
));
}
let now = chrono::Utc::now().timestamp();
auth.maybe_reset_daily(now);
if is_mint {
if let Some(max) = auth.max_mint_per_tx
&& amount > max
{
return Err(TokenError::InvalidAmount(format!(
"Mint amount {} exceeds per-tx limit {}",
amount, max
)));
}
if let Some(daily) = auth.daily_mint_limit {
let already = auth.minted_today.load();
if already.saturating_add(amount) > daily {
return Err(TokenError::InvalidAmount(format!(
"Mint amount {} would exceed daily limit {} (already minted {})",
amount, daily, already
)));
}
}
} else {
if let Some(max) = auth.max_burn_per_tx
&& amount > max
{
return Err(TokenError::InvalidAmount(format!(
"Burn amount {} exceeds per-tx limit {}",
amount, max
)));
}
if let Some(daily) = auth.daily_burn_limit {
let already = auth.burned_today.load();
if already.saturating_add(amount) > daily {
return Err(TokenError::InvalidAmount(format!(
"Burn amount {} would exceed daily limit {} (already burned {})",
amount, daily, already
)));
}
}
}
Ok(())
}
fn now_secs() -> i64 {
chrono::Utc::now().timestamp()
}
pub fn crosschain_mint(
&self,
bridge: [u8; 20],
to: Address,
amount: u128,
sender: Vec<u8>,
) -> Result<CrosschainMintEvent> {
self.validate_bridge(&bridge, amount, true)?;
let treasury = self
.tnzo_token
.treasury_address_ref()
.ok_or_else(|| TokenError::Unauthorized {
reason: "Treasury address not set; cannot mint cross-chain tokens".to_string(),
})?;
self.tnzo_token.mint(&to, amount, &treasury)?;
{
let entry = self.authorized_bridges.get(&bridge).unwrap();
entry.value().minted_today.fetch_add(amount);
}
let nonce = self.nonce.fetch_add(1, Ordering::Relaxed);
let event = CrosschainMintEvent {
nonce,
to,
amount,
sender,
bridge,
timestamp: Self::now_secs(),
};
info!(
"crosschainMint: bridge=0x{} to={} amount={} nonce={}",
hex::encode(bridge),
to,
amount,
nonce
);
self.mint_events.insert(nonce, event.clone());
Ok(event)
}
pub fn crosschain_burn(
&self,
bridge: [u8; 20],
from: Address,
amount: u128,
sender: Vec<u8>,
) -> Result<CrosschainBurnEvent> {
self.validate_bridge(&bridge, amount, false)?;
self.tnzo_token.burn(&from, amount)?;
{
let entry = self.authorized_bridges.get(&bridge).unwrap();
entry.value().burned_today.fetch_add(amount);
}
let nonce = self.nonce.fetch_add(1, Ordering::Relaxed);
let event = CrosschainBurnEvent {
nonce,
from,
amount,
sender,
bridge,
timestamp: Self::now_secs(),
};
info!(
"crosschainBurn: bridge=0x{} from={} amount={} nonce={}",
hex::encode(bridge),
from,
amount,
nonce
);
self.burn_events.insert(nonce, event.clone());
Ok(event)
}
pub fn get_mint_events(&self) -> Vec<CrosschainMintEvent> {
let mut events: Vec<CrosschainMintEvent> = self
.mint_events
.iter()
.map(|e| e.value().clone())
.collect();
events.sort_by(|a, b| b.nonce.cmp(&a.nonce));
events
}
pub fn get_burn_events(&self) -> Vec<CrosschainBurnEvent> {
let mut events: Vec<CrosschainBurnEvent> = self
.burn_events
.iter()
.map(|e| e.value().clone())
.collect();
events.sort_by(|a, b| b.nonce.cmp(&a.nonce));
events
}
pub fn get_mint_event(&self, nonce: u64) -> Option<CrosschainMintEvent> {
self.mint_events.get(&nonce).map(|e| e.value().clone())
}
pub fn get_burn_event(&self, nonce: u64) -> Option<CrosschainBurnEvent> {
self.burn_events.get(&nonce).map(|e| e.value().clone())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tnzo::ONE_TNZO;
fn setup() -> (CrosschainTokenManager, [u8; 20], Address) {
let token = Arc::new(TnzoToken::new());
let registry = Arc::new(TokenRegistry::new());
let treasury = Address::new([0xAA; 32]);
token.set_treasury_address(treasury);
token
.mint(&Address::new([0xBB; 32]), 1_000_000 * ONE_TNZO, &treasury)
.unwrap();
let mgr = CrosschainTokenManager::new(token, registry);
let bridge: [u8; 20] = [0x01; 20];
mgr.authorize_bridge(
bridge,
"TestBridge".to_string(),
vec![],
Some(10_000 * ONE_TNZO),
Some(10_000 * ONE_TNZO),
Some(100_000 * ONE_TNZO),
Some(100_000 * ONE_TNZO),
)
.unwrap();
(mgr, bridge, treasury)
}
#[test]
fn test_authorize_bridge() {
let token = Arc::new(TnzoToken::new());
let registry = Arc::new(TokenRegistry::new());
let mgr = CrosschainTokenManager::new(token, registry);
let bridge: [u8; 20] = [0x42; 20];
mgr.authorize_bridge(
bridge,
"MyBridge".to_string(),
vec![],
None,
None,
None,
None,
)
.unwrap();
assert!(mgr.is_bridge_authorized(&bridge));
let info = mgr.get_bridge_info(&bridge).unwrap();
assert_eq!(info.name, "MyBridge");
assert!(info.enabled);
}
#[test]
fn test_crosschain_mint_succeeds() {
let (mgr, bridge, _treasury) = setup();
let to = Address::new([0xCC; 32]);
let event = mgr
.crosschain_mint(bridge, to, 500 * ONE_TNZO, vec![1, 2, 3])
.unwrap();
assert_eq!(event.amount, 500 * ONE_TNZO);
assert_eq!(event.to, to);
assert_eq!(event.bridge, bridge);
assert_eq!(mgr.tnzo_token.balance_of(&to), 500 * ONE_TNZO);
}
#[test]
fn test_crosschain_mint_fails_unauthorized() {
let (mgr, _bridge, _treasury) = setup();
let rogue: [u8; 20] = [0xFF; 20];
let to = Address::new([0xCC; 32]);
let result = mgr.crosschain_mint(rogue, to, 100 * ONE_TNZO, vec![]);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("not authorized"));
}
#[test]
fn test_crosschain_mint_per_tx_limit() {
let (mgr, bridge, _treasury) = setup();
let to = Address::new([0xCC; 32]);
let result = mgr.crosschain_mint(bridge, to, 20_000 * ONE_TNZO, vec![]);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("exceeds per-tx limit"));
}
#[test]
fn test_crosschain_mint_daily_limit() {
let (mgr, bridge, _treasury) = setup();
let to = Address::new([0xCC; 32]);
for _ in 0..10 {
mgr.crosschain_mint(bridge, to, 10_000 * ONE_TNZO, vec![])
.unwrap();
}
let result = mgr.crosschain_mint(bridge, to, ONE_TNZO, vec![]);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("daily limit"));
}
#[test]
fn test_crosschain_burn_succeeds() {
let (mgr, bridge, _treasury) = setup();
let from = Address::new([0xBB; 32]);
let event = mgr
.crosschain_burn(bridge, from, 500 * ONE_TNZO, vec![4, 5, 6])
.unwrap();
assert_eq!(event.amount, 500 * ONE_TNZO);
assert_eq!(event.from, from);
assert_eq!(
mgr.tnzo_token.balance_of(&from),
999_500 * ONE_TNZO
);
}
#[test]
fn test_crosschain_burn_insufficient_balance() {
let (mgr, bridge, _treasury) = setup();
let from = Address::new([0xDD; 32]);
let result = mgr.crosschain_burn(bridge, from, 100 * ONE_TNZO, vec![]);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("Insufficient balance"));
}
#[test]
fn test_crosschain_burn_per_tx_limit() {
let (mgr, bridge, _treasury) = setup();
let from = Address::new([0xBB; 32]);
let result = mgr.crosschain_burn(bridge, from, 20_000 * ONE_TNZO, vec![]);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("exceeds per-tx limit"));
}
#[test]
fn test_revoke_bridge() {
let (mgr, bridge, _treasury) = setup();
mgr.revoke_bridge(&bridge).unwrap();
assert!(!mgr.is_bridge_authorized(&bridge));
let to = Address::new([0xCC; 32]);
let result = mgr.crosschain_mint(bridge, to, ONE_TNZO, vec![]);
assert!(result.is_err());
}
#[test]
fn test_event_tracking() {
let (mgr, bridge, _treasury) = setup();
let to = Address::new([0xCC; 32]);
let from = Address::new([0xBB; 32]);
let mint_evt = mgr
.crosschain_mint(bridge, to, 100 * ONE_TNZO, vec![10])
.unwrap();
let burn_evt = mgr
.crosschain_burn(bridge, from, 50 * ONE_TNZO, vec![20])
.unwrap();
let mints = mgr.get_mint_events();
assert_eq!(mints.len(), 1);
assert_eq!(mints[0].nonce, mint_evt.nonce);
let burns = mgr.get_burn_events();
assert_eq!(burns.len(), 1);
assert_eq!(burns[0].nonce, burn_evt.nonce);
assert!(mgr.get_mint_event(mint_evt.nonce).is_some());
assert!(mgr.get_burn_event(burn_evt.nonce).is_some());
assert!(mgr.get_mint_event(999).is_none());
}
#[test]
fn test_multiple_bridges() {
let (mgr, bridge_a, _treasury) = setup();
let bridge_b: [u8; 20] = [0x02; 20];
mgr.authorize_bridge(
bridge_b,
"BridgeB".to_string(),
vec![],
Some(5_000 * ONE_TNZO),
Some(5_000 * ONE_TNZO),
None,
None,
)
.unwrap();
let to = Address::new([0xCC; 32]);
mgr.crosschain_mint(bridge_a, to, 1_000 * ONE_TNZO, vec![])
.unwrap();
mgr.crosschain_mint(bridge_b, to, 2_000 * ONE_TNZO, vec![])
.unwrap();
assert_eq!(mgr.tnzo_token.balance_of(&to), 3_000 * ONE_TNZO);
let bridges = mgr.list_authorized_bridges();
assert_eq!(bridges.len(), 2);
}
#[test]
fn test_crosschain_mint_zero_amount_rejected() {
let (mgr, bridge, _treasury) = setup();
let to = Address::new([0xCC; 32]);
let result = mgr.crosschain_mint(bridge, to, 0, vec![]);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("greater than zero"));
}
#[test]
fn test_update_bridge_limits() {
let (mgr, bridge, _treasury) = setup();
mgr.update_bridge_limits(&bridge, Some(50_000 * ONE_TNZO), None, None, None)
.unwrap();
let info = mgr.get_bridge_info(&bridge).unwrap();
assert_eq!(info.max_mint_per_tx, Some(50_000 * ONE_TNZO));
assert_eq!(info.max_burn_per_tx, None);
}
#[test]
fn test_revoke_nonexistent_bridge_fails() {
let (mgr, _bridge, _treasury) = setup();
let fake: [u8; 20] = [0xFE; 20];
let result = mgr.revoke_bridge(&fake);
assert!(result.is_err());
}
#[test]
fn test_daily_burn_limit() {
let token = Arc::new(TnzoToken::new());
let registry = Arc::new(TokenRegistry::new());
let treasury = Address::new([0xAA; 32]);
token.set_treasury_address(treasury);
let from = Address::new([0xBB; 32]);
token.mint(&from, 10_000_000 * ONE_TNZO, &treasury).unwrap();
let mgr = CrosschainTokenManager::new(token, registry);
let bridge: [u8; 20] = [0x05; 20];
mgr.authorize_bridge(
bridge,
"LimitedBridge".to_string(),
vec![],
None,
Some(5_000 * ONE_TNZO),
None,
Some(10_000 * ONE_TNZO), )
.unwrap();
mgr.crosschain_burn(bridge, from, 5_000 * ONE_TNZO, vec![])
.unwrap();
mgr.crosschain_burn(bridge, from, 5_000 * ONE_TNZO, vec![])
.unwrap();
let result = mgr.crosschain_burn(bridge, from, ONE_TNZO, vec![]);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("daily limit"));
}
}