Skip to main content

tenzro_token/
cross_vm.rs

1//! Cross-VM token types and decimal conversion utilities
2//!
3//! Provides types for representing tokens across EVM, SVM, and DAML VMs,
4//! including safe decimal conversion between native 18-decimal and SPL 9-decimal.
5
6use serde::{Deserialize, Serialize};
7use std::fmt;
8
9/// VM types for token addressing
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
11pub enum TokenVmType {
12    /// Native Tenzro L1 token (18 decimals)
13    Native,
14    /// EVM ERC-20 representation (configurable decimals, typically 18)
15    Evm,
16    /// SVM SPL token representation (9 decimals max due to u64 constraint)
17    Svm,
18    /// DAML CIP-56 token representation (arbitrary precision Decimal)
19    Daml,
20    /// Tempo L1 TIP-20 stablecoin representation (Stripe + Paradigm chain, EIP-155 EVM)
21    TempoTip20,
22}
23
24impl fmt::Display for TokenVmType {
25    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
26        match self {
27            TokenVmType::Native => write!(f, "native"),
28            TokenVmType::Evm => write!(f, "evm"),
29            TokenVmType::Svm => write!(f, "svm"),
30            TokenVmType::Daml => write!(f, "daml"),
31            TokenVmType::TempoTip20 => write!(f, "tempo-tip20"),
32        }
33    }
34}
35
36/// Cross-VM address mapping for a token
37#[derive(Debug, Clone, Default, Serialize, Deserialize)]
38pub struct VmAddresses {
39    /// ERC-20 contract address (20 bytes) on EVM
40    pub evm: Option<[u8; 20]>,
41    /// SPL mint address (32 bytes) on SVM
42    pub svm: Option<[u8; 32]>,
43    /// DAML template identifier on Canton
44    pub daml_template_id: Option<String>,
45    /// Native token address (for TNZO only)
46    pub native: Option<[u8; 32]>,
47    /// Tempo L1 TIP-20 contract address (20 bytes, EIP-155 EVM at chain_id 42431)
48    pub tempo: Option<[u8; 20]>,
49}
50
51impl VmAddresses {
52    /// Returns true if the token has an address on the given VM
53    pub fn has_vm(&self, vm: TokenVmType) -> bool {
54        match vm {
55            TokenVmType::Native => self.native.is_some(),
56            TokenVmType::Evm => self.evm.is_some(),
57            TokenVmType::Svm => self.svm.is_some(),
58            TokenVmType::Daml => self.daml_template_id.is_some(),
59            TokenVmType::TempoTip20 => self.tempo.is_some(),
60        }
61    }
62
63    /// Returns the Tempo TIP-20 address as a hex string (0x-prefixed)
64    pub fn tempo_hex(&self) -> Option<String> {
65        self.tempo.map(|addr| format!("0x{}", hex::encode(addr)))
66    }
67
68    /// Returns the EVM address as a hex string (0x-prefixed)
69    pub fn evm_hex(&self) -> Option<String> {
70        self.evm.map(|addr| format!("0x{}", hex::encode(addr)))
71    }
72
73    /// Returns the SVM mint as a hex string
74    pub fn svm_hex(&self) -> Option<String> {
75        self.svm.map(hex::encode)
76    }
77}
78
79/// Token permissions flags
80#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct TokenPermissions {
82    /// Whether new tokens can be minted after creation
83    pub mintable: bool,
84    /// Whether tokens can be burned by holders
85    pub burnable: bool,
86    /// Whether transfers can be paused by the creator
87    pub pausable: bool,
88    /// Whether individual accounts can be frozen
89    pub freezable: bool,
90    /// Whether the token is currently paused
91    pub paused: bool,
92}
93
94impl Default for TokenPermissions {
95    fn default() -> Self {
96        Self {
97            mintable: false,
98            burnable: true,
99            pausable: false,
100            freezable: false,
101            paused: false,
102        }
103    }
104}
105
106/// Token metadata
107#[derive(Debug, Clone, Default, Serialize, Deserialize)]
108pub struct TokenMetadata {
109    /// Token description
110    pub description: Option<String>,
111    /// Token icon URI
112    pub icon_uri: Option<String>,
113    /// Token website
114    pub website: Option<String>,
115    /// Additional key-value metadata
116    pub extra: std::collections::HashMap<String, String>,
117}
118
119/// Unique token identifier (32 bytes, SHA-256 of creator + nonce)
120#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
121pub struct TokenId(pub [u8; 32]);
122
123impl TokenId {
124    /// Creates a new TokenId from a byte array
125    pub fn new(bytes: [u8; 32]) -> Self {
126        Self(bytes)
127    }
128
129    /// Computes a TokenId from creator address and nonce
130    pub fn compute(creator: &[u8], nonce: u64) -> Self {
131        use sha2::{Sha256, Digest};
132        let mut hasher = Sha256::new();
133        hasher.update(b"tenzro-token-id:");
134        hasher.update(creator);
135        hasher.update(nonce.to_le_bytes());
136        let result = hasher.finalize();
137        let mut bytes = [0u8; 32];
138        bytes.copy_from_slice(&result);
139        Self(bytes)
140    }
141
142    /// Returns the token ID as hex string
143    pub fn to_hex(&self) -> String {
144        hex::encode(self.0)
145    }
146
147    /// The well-known token ID for TNZO (all zeros except first byte = 0x01)
148    pub fn tnzo() -> Self {
149        let mut bytes = [0u8; 32];
150        bytes[0] = 0x01;
151        Self(bytes)
152    }
153}
154
155impl fmt::Display for TokenId {
156    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
157        write!(f, "{}", self.to_hex())
158    }
159}
160
161/// Token type classification
162#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
163pub enum TokenType {
164    /// Native L1 token (TNZO)
165    Native,
166    /// User-created ERC-20 on EVM
167    Erc20,
168    /// User-created SPL token on SVM
169    Spl,
170    /// Enterprise CIP-56 token on Canton/DAML
171    Cip56,
172    /// Cross-VM token (exists on multiple VMs via pointer contracts)
173    CrossVm,
174}
175
176/// Full token definition stored in the registry
177#[derive(Debug, Clone, Serialize, Deserialize)]
178pub struct TokenDefinition {
179    /// Unique token identifier
180    pub token_id: TokenId,
181    /// Human-readable name
182    pub name: String,
183    /// Trading symbol
184    pub symbol: String,
185    /// Decimal places (18 for EVM, 9 for SPL)
186    pub decimals: u8,
187    /// Total supply in native (18-decimal) units
188    pub total_supply: u128,
189    /// Maximum supply (None = unlimited if mintable)
190    pub max_supply: Option<u128>,
191    /// Creator address (32 bytes)
192    pub creator: [u8; 32],
193    /// Token type
194    pub token_type: TokenType,
195    /// Cross-VM address mapping
196    pub vm_addresses: VmAddresses,
197    /// Permission flags
198    pub permissions: TokenPermissions,
199    /// Creation timestamp (Unix seconds)
200    pub created_at: u64,
201    /// Token metadata
202    pub metadata: TokenMetadata,
203}
204
205/// Decimal conversion constants
206pub const NATIVE_DECIMALS: u8 = 18;
207pub const SPL_DECIMALS: u8 = 9;
208pub const NATIVE_UNIT: u128 = 1_000_000_000_000_000_000; // 10^18
209pub const SPL_UNIT: u64 = 1_000_000_000; // 10^9
210pub const DECIMAL_SHIFT: u128 = 1_000_000_000; // 10^9 (difference between 18 and 9)
211
212/// Converts a native 18-decimal amount to SPL 9-decimal amount.
213/// Truncates (does not round) to prevent inflation from rounding errors.
214///
215/// # Returns
216/// `None` if the result exceeds u64::MAX (overflow)
217pub fn native_to_spl(native_amount: u128) -> Option<u64> {
218    let spl_amount = native_amount / DECIMAL_SHIFT;
219    if spl_amount > u64::MAX as u128 {
220        None
221    } else {
222        Some(spl_amount as u64)
223    }
224}
225
226/// Converts an SPL 9-decimal amount to native 18-decimal amount.
227/// This is lossless since we're scaling up.
228pub fn spl_to_native(spl_amount: u64) -> u128 {
229    (spl_amount as u128) * DECIMAL_SHIFT
230}
231
232/// Computes the truncation dust lost when converting native -> SPL -> native
233pub fn truncation_dust(native_amount: u128) -> u128 {
234    native_amount % DECIMAL_SHIFT
235}
236
237/// Cross-VM transfer request
238#[derive(Debug, Clone, Serialize, Deserialize)]
239pub struct CrossVmTransfer {
240    /// Token being transferred
241    pub token_id: TokenId,
242    /// Source VM
243    pub from_vm: TokenVmType,
244    /// Destination VM
245    pub to_vm: TokenVmType,
246    /// Sender address (variable length)
247    pub from_address: Vec<u8>,
248    /// Recipient address (variable length)
249    pub to_address: Vec<u8>,
250    /// Amount in native (18-decimal) units
251    pub amount: u128,
252    /// Nonce for replay protection
253    pub nonce: u64,
254}
255
256#[cfg(test)]
257mod tests {
258    use super::*;
259
260    #[test]
261    fn test_native_to_spl_conversion() {
262        // 1 TNZO = 10^18 native = 10^9 SPL
263        assert_eq!(native_to_spl(NATIVE_UNIT), Some(SPL_UNIT));
264
265        // 0.5 TNZO
266        let half_tnzo = NATIVE_UNIT / 2;
267        assert_eq!(native_to_spl(half_tnzo), Some(SPL_UNIT / 2));
268
269        // Very small amount (less than 1 SPL unit)
270        assert_eq!(native_to_spl(999_999_999), Some(0)); // truncated to 0
271
272        // Exact boundary: 1 SPL lamport = 10^9 native
273        assert_eq!(native_to_spl(DECIMAL_SHIFT), Some(1));
274    }
275
276    #[test]
277    fn test_spl_to_native_conversion() {
278        assert_eq!(spl_to_native(SPL_UNIT), NATIVE_UNIT);
279        assert_eq!(spl_to_native(1), DECIMAL_SHIFT);
280        assert_eq!(spl_to_native(0), 0);
281    }
282
283    #[test]
284    fn test_roundtrip_conversion() {
285        let original = 12_345_678_901_234_567_890u128; // ~12.3 TNZO
286        let spl = native_to_spl(original).unwrap();
287        let back = spl_to_native(spl);
288        let dust = truncation_dust(original);
289
290        // Roundtrip loses the dust
291        assert_eq!(back + dust, original);
292        // Dust is always less than DECIMAL_SHIFT
293        assert!(dust < DECIMAL_SHIFT);
294    }
295
296    #[test]
297    fn test_spl_overflow_protection() {
298        // u64::MAX * DECIMAL_SHIFT would exceed u64 capacity
299        let huge = u128::MAX;
300        assert_eq!(native_to_spl(huge), None);
301    }
302
303    #[test]
304    fn test_token_id_computation() {
305        let creator = [42u8; 32];
306        let id1 = TokenId::compute(&creator, 0);
307        let id2 = TokenId::compute(&creator, 1);
308        assert_ne!(id1, id2); // Different nonces produce different IDs
309    }
310
311    #[test]
312    fn test_token_id_tnzo() {
313        let tnzo = TokenId::tnzo();
314        assert_eq!(tnzo.0[0], 0x01);
315        assert!(tnzo.0[1..].iter().all(|&b| b == 0));
316    }
317
318    #[test]
319    fn test_vm_addresses_has_vm() {
320        let mut addrs = VmAddresses::default();
321        assert!(!addrs.has_vm(TokenVmType::Evm));
322        addrs.evm = Some([0u8; 20]);
323        assert!(addrs.has_vm(TokenVmType::Evm));
324    }
325
326    #[test]
327    fn test_token_permissions_default() {
328        let perms = TokenPermissions::default();
329        assert!(!perms.mintable);
330        assert!(perms.burnable);
331        assert!(!perms.pausable);
332        assert!(!perms.freezable);
333        assert!(!perms.paused);
334    }
335}