================================================================================ CARBONCORE DEVELOPER GUIDE #07 UNIFIED TOKEN MANAGER ================================================================================ PRINCIPLE: Single solution for ALL token operations with automatic decimals ================================================================================ ARCHITECTURE OVERVIEW ================================================================================ File: js/service/unified-token-manager.js Global Instance: window.tokenManager Purpose: - Handle tokens with different decimals (6, 18) - Provide universal parsing and formatting - Manage approvals and balances - Eliminate decimal-related bugs Core Challenge: Stablecoins (USDT, USDC): 6 decimals Carbon Tokens (CCT): 18 decimals ETH: 18 decimals Solution: Automatic decimal detection and conversion ================================================================================ CLASS STRUCTURE ================================================================================ class UnifiedTokenManager { constructor() { this.provider = null; this.signer = null; this.tokenCache = new Map(); this.cacheTimeout = 3600000; // 1 hour // Predefined tokens for quick access this.knownTokens = { '0x2e6Df83CC364317a06AeD2369d5a2761d5025a19': { symbol: 'Mock USD', decimals: 6 }, '0xdAC17F958D2ee523a2206206994597C13D831ec7': { symbol: 'USDT', decimals: 6 }, '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48': { symbol: 'USDC', decimals: 6 }, '0x0000000000000000000000000000000000000000': { symbol: 'ETH', decimals: 18 } }; } async init() { // Initialize provider and signer } } ================================================================================ CORE METHODS ================================================================================ Method: getTokenInfo(tokenAddress) Purpose: Retrieve token metadata with automatic decimal detection Process: 1. Check cache 2. Handle special case: ETH (0x0 address) 3. Check known tokens list 4. Query blockchain for unknown tokens 5. Cache result Code: async getTokenInfo(tokenAddress) { // Normalize address const normalizedAddress = tokenAddress.toLowerCase(); // Check cache const cached = this.tokenCache.get(normalizedAddress); if (cached && (Date.now() - cached.timestamp < this.cacheTimeout)) { return cached.data; } // Special case: ETH if (normalizedAddress === '0x0000000000000000000000000000000000000000') { return { address: normalizedAddress, symbol: 'ETH', decimals: 18, name: 'Ether' }; } // Check known tokens if (this.knownTokens[normalizedAddress]) { const info = { address: normalizedAddress, ...this.knownTokens[normalizedAddress] }; this.cacheTokenInfo(normalizedAddress, info); return info; } // Query blockchain try { const tokenABI = [ "function symbol() view returns (string)", "function decimals() view returns (uint8)", "function name() view returns (string)" ]; const token = new ethers.Contract( tokenAddress, tokenABI, this.provider ); const [symbol, decimals, name] = await Promise.all([ token.symbol(), token.decimals(), token.name() ]); const info = { address: normalizedAddress, symbol: symbol, decimals: decimals, name: name }; this.cacheTokenInfo(normalizedAddress, info); return info; } catch (error) { console.error('Error getting token info:', error); throw new Error(`Failed to get token info: ${error.message}`); } } Return: { address: "0x...", symbol: "USDT", decimals: 6, name: "Tether USD" } ================================================================================ AMOUNT PARSING ================================================================================ Method: parseAmount(amount, tokenAddress) Purpose: Convert human-readable amount to wei with correct decimals Code: async parseAmount(amount, tokenAddress) { const tokenInfo = await this.getTokenInfo(tokenAddress); // Convert to string and handle scientific notation let amountStr = amount.toString(); if (amountStr.includes('e')) { amountStr = parseFloat(amount).toFixed(tokenInfo.decimals); } // Parse with correct decimals return ethers.utils.parseUnits(amountStr, tokenInfo.decimals); } Examples: // USDT (6 decimals) await tokenManager.parseAmount(100, usdtAddress); // Returns: BigNumber(100000000) = 100 * 10^6 // Carbon Token (18 decimals) await tokenManager.parseAmount(100, cctAddress); // Returns: BigNumber(100000000000000000000) = 100 * 10^18 // ETH await tokenManager.parseAmount(1, ethers.constants.AddressZero); // Returns: BigNumber(1000000000000000000) = 1 * 10^18 ================================================================================ AMOUNT FORMATTING ================================================================================ Method: formatAmount(amount, tokenAddress) Purpose: Convert wei to human-readable amount with correct decimals Code: async formatAmount(amount, tokenAddress) { const tokenInfo = await this.getTokenInfo(tokenAddress); return ethers.utils.formatUnits(amount, tokenInfo.decimals); } Examples: // USDT (6 decimals) await tokenManager.formatAmount(BigNumber(100000000), usdtAddress); // Returns: "100.0" (100 USDT) // Carbon Token (18 decimals) await tokenManager.formatAmount( BigNumber("100000000000000000000"), cctAddress ); // Returns: "100.0" (100 CCT) Display Formatting: const formatted = await tokenManager.formatAmount(wei, tokenAddress); const display = parseFloat(formatted).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }); // Returns: "100.00" or "1,234.56" ================================================================================ BALANCE MANAGEMENT ================================================================================ Method: getBalance(tokenAddress, walletAddress = null) Purpose: Get token balance with automatic formatting Code: async getBalance(tokenAddress, walletAddress = null) { const wallet = walletAddress || (await this.signer.getAddress()); const tokenInfo = await this.getTokenInfo(tokenAddress); // Special case: ETH if (tokenAddress === ethers.constants.AddressZero) { const balance = await this.provider.getBalance(wallet); return { raw: balance.toString(), formatted: ethers.utils.formatEther(balance), symbol: 'ETH', decimals: 18 }; } // ERC20 tokens const tokenABI = ["function balanceOf(address) view returns (uint256)"]; const token = new ethers.Contract( tokenAddress, tokenABI, this.provider ); const balance = await token.balanceOf(wallet); const formatted = ethers.utils.formatUnits( balance, tokenInfo.decimals ); return { raw: balance.toString(), formatted: formatted, symbol: tokenInfo.symbol, decimals: tokenInfo.decimals }; } Usage: const balance = await tokenManager.getBalance(usdtAddress, userWallet); console.log(`Balance: ${balance.formatted} ${balance.symbol}`); // Output: "Balance: 1234.56 USDT" ================================================================================ TOKEN APPROVAL ================================================================================ Method: approve(tokenAddress, spenderAddress, amount) Purpose: Approve token spending with automatic decimal handling Code: async approve(tokenAddress, spenderAddress, amount) { // ETH doesn't require approval if (tokenAddress === ethers.constants.AddressZero) { return { success: true, message: 'ETH does not require approval' }; } // Get token info const tokenInfo = await this.getTokenInfo(tokenAddress); // Parse amount with correct decimals const amountWei = await this.parseAmount(amount, tokenAddress); // Create contract instance const tokenABI = [ "function approve(address spender, uint256 amount) returns (bool)" ]; const token = new ethers.Contract( tokenAddress, tokenABI, this.signer ); // Execute approval const tx = await token.approve(spenderAddress, amountWei); const receipt = await tx.wait(); return { success: true, transactionHash: receipt.transactionHash, message: `Approved ${amount} ${tokenInfo.symbol}` }; } Usage: const result = await tokenManager.approve( usdtAddress, marketplaceAddress, 1000 // Amount in human-readable format ); ================================================================================ BALANCE SUFFICIENCY CHECK ================================================================================ Method: checkSufficientBalance(tokenAddress, requiredAmount, walletAddress) Purpose: Verify user has enough balance for transaction Code: async checkSufficientBalance( tokenAddress, requiredAmount, walletAddress = null ) { const balance = await this.getBalance(tokenAddress, walletAddress); const required = parseFloat(requiredAmount); const available = parseFloat(balance.formatted); const sufficient = available >= required; const deficit = sufficient ? 0 : (required - available); return { sufficient: sufficient, available: balance.formatted, required: requiredAmount.toString(), symbol: balance.symbol, deficit: deficit.toFixed(balance.decimals) }; } Usage: const check = await tokenManager.checkSufficientBalance( usdtAddress, 1000, userWallet ); if (!check.sufficient) { alert(`Insufficient balance: need ${check.deficit} ${check.symbol} more`); } ================================================================================ UNIVERSAL TRANSFER ================================================================================ Method: transfer(tokenAddress, toAddress, amount) Purpose: Send tokens with automatic decimal handling Code: async transfer(tokenAddress, toAddress, amount) { const tokenInfo = await this.getTokenInfo(tokenAddress); const amountWei = await this.parseAmount(amount, tokenAddress); // ETH transfer if (tokenAddress === ethers.constants.AddressZero) { const tx = await this.signer.sendTransaction({ to: toAddress, value: amountWei }); const receipt = await tx.wait(); return { success: true, transactionHash: receipt.transactionHash }; } // ERC20 transfer const tokenABI = [ "function transfer(address to, uint256 amount) returns (bool)" ]; const token = new ethers.Contract( tokenAddress, tokenABI, this.signer ); const tx = await token.transfer(toAddress, amountWei); const receipt = await tx.wait(); return { success: true, transactionHash: receipt.transactionHash }; } ================================================================================ CACHING SYSTEM ================================================================================ Cache Structure: tokenCache: Map { "0x..." => { timestamp: 1234567890, data: { address: "0x...", symbol: "USDT", decimals: 6, name: "Tether USD" } } } Cache Methods: cacheTokenInfo(address, info) { this.tokenCache.set(address.toLowerCase(), { timestamp: Date.now(), data: info }); } clearExpiredCache() { const now = Date.now(); for (const [address, cached] of this.tokenCache.entries()) { if (now - cached.timestamp > this.cacheTimeout) { this.tokenCache.delete(address); } } } Cache Benefits: - Reduce RPC calls - Faster repeat operations - Lower gas costs (fewer calls) - Improved UX (instant results) ================================================================================ ERROR HANDLING ================================================================================ Common Errors: 1. Invalid token address 2. Token doesn't implement ERC20 standard 3. RPC timeout 4. User rejects transaction 5. Insufficient balance 6. Insufficient allowance Error Handler: try { const result = await tokenManager.approve(...); } catch (error) { if (error.code === 4001) { // User rejected alert('Transaction rejected by user'); } else if (error.message.includes('insufficient funds')) { // Insufficient balance alert('Insufficient balance for gas fees'); } else if (error.message.includes('execution reverted')) { // Contract error alert('Transaction failed: ' + error.reason); } else { // Generic error console.error('Error:', error); alert('Transaction failed: ' + error.message); } } ================================================================================ INTEGRATION EXAMPLES ================================================================================ Example 1: OTC Marketplace Payment // Parse buyer's payment amount const paymentWei = await tokenManager.parseAmount( pricePerToken * quantity, paymentTokenAddress ); // Check balance const balanceCheck = await tokenManager.checkSufficientBalance( paymentTokenAddress, pricePerToken * quantity ); if (!balanceCheck.sufficient) { throw new Error('Insufficient balance'); } // Approve marketplace contract await tokenManager.approve( paymentTokenAddress, marketplaceAddress, pricePerToken * quantity ); // Execute purchase await marketplace.buy(...); Example 2: Display User Portfolio const tokens = [usdtAddress, usdcAddress, cct24Address]; for (const tokenAddress of tokens) { const balance = await tokenManager.getBalance(tokenAddress); const tokenInfo = await tokenManager.getTokenInfo(tokenAddress); console.log( `${tokenInfo.name}: ${balance.formatted} ${balance.symbol}` ); } // Output: // Tether USD: 1,234.56 USDT // USD Coin: 567.89 USDC // CarbonCore Territory 24: 10,000.00 CCT24-2025 Example 3: Convert Between Display and Wei // User enters: "100.50" const userInput = "100.50"; // Convert to wei for contract call const wei = await tokenManager.parseAmount(userInput, usdtAddress); // Result: BigNumber(100500000) for USDT (6 decimals) // Convert back to display after transaction const display = await tokenManager.formatAmount(wei, usdtAddress); // Result: "100.5" ================================================================================ GLOBAL INITIALIZATION ================================================================================ Setup in main.js or index.html: // Create global instance window.tokenManager = new UnifiedTokenManager(); // Initialize on page load document.addEventListener('DOMContentLoaded', async () => { try { await window.tokenManager.init(); console.log('✅ Global UnifiedTokenManager ready'); } catch (error) { console.error('❌ Failed to initialize tokenManager:', error); } }); Usage Everywhere: // In any module const balance = await window.tokenManager.getBalance(tokenAddress); const parsed = await window.tokenManager.parseAmount(100, tokenAddress); const formatted = await window.tokenManager.formatAmount(wei, tokenAddress); ================================================================================ BEST PRACTICES ================================================================================ 1. Always use tokenManager for amount conversions ❌ DON'T: ethers.utils.parseUnits(amount, 6) ✅ DO: tokenManager.parseAmount(amount, tokenAddress) 2. Never hardcode decimals ❌ DON'T: amount * 1000000 // Assumes 6 decimals ✅ DO: await tokenManager.parseAmount(amount, tokenAddress) 3. Check balance before operations ✅ const check = await tokenManager.checkSufficientBalance(...); if (!check.sufficient) return; 4. Handle all token types uniformly ✅ Same code for USDT (6), CCT (18), and ETH (18) 5. Cache token info when possible ✅ tokenManager handles caching automatically 6. Always format for display ❌ DON'T: Show raw wei values ✅ DO: const display = await tokenManager.formatAmount(wei, address); ================================================================================