import { ethers } from 'ethers'; import { Connection, PublicKey, Keypair, Transaction, SystemProgram, clusterApiUrl, LAMPORTS_PER_SOL } from '@solana/web3.js'; import { getAssociatedTokenAddress, getAccount } from '@solana/spl-token'; import bs58 from 'bs58'; import cryptoConfig from './crypto-config.json'; const { TronWeb } = require('tronweb'); // ERC20 ABI for checking USDT/USDC balances const ERC20_ABI = [ "function balanceOf(address owner) view returns (uint256)", "function decimals() view returns (uint8)", "function symbol() view returns (string)", "function transfer(address to, uint256 value) public returns (bool)" ]; export class CryptoEngine { private provider!: ethers.JsonRpcProvider; private solConnection!: Connection; private tronWeb: any; private network: string; private config: any; constructor(networkId: string = 'POLYGON') { this.network = networkId; this.config = cryptoConfig.networks.find(n => n.id === networkId); if (!this.config) throw new Error(`Network ${networkId} not found in config.`); if (this.network === 'SOLANA') { this.solConnection = new Connection(this.config.rpc, 'confirmed'); } else if (this.network === 'TRON') { this.tronWeb = new TronWeb({ fullHost: this.config.rpc, headers: { "TRON-PRO-API-KEY": process.env.TRON_GRID_API_KEY || "" } }); } else if (this.network === 'BITCOIN') { // Bitcoin usually handled via Electrum OR simple Public API } else { const options = this.config.chainId ? { chainId: this.config.chainId, name: this.config.name.toLowerCase() } : undefined; this.provider = new ethers.JsonRpcProvider(this.config.rpc, options, { staticNetwork: !!this.config.chainId }); } } /** * Helper to get token config from JSON */ private getTokenConfig(symbol: string) { return this.config.tokens.find((t: any) => t.symbol === symbol); } /** * Generates a temporary wallet for a transaction. */ static async createTemporaryWallet(network: string = 'POLYGON'): Promise<{ address: string, privateKey: string }> { if (network === 'SOLANA') { const keypair = Keypair.generate(); return { address: keypair.publicKey.toBase58(), privateKey: bs58.encode(keypair.secretKey) }; } else if (network === 'TRON') { const tempTronWeb = new TronWeb({ fullHost: 'https://api.trongrid.io' }); const account = await tempTronWeb.createAccount(); return { address: account.address.base58, privateKey: account.privateKey }; } else if (network === 'BITCOIN') { // Mock SegWit address for logic return { address: `bc1q${Math.random().toString(36).substring(2, 12)}...MOCK`, privateKey: 'MOCK_BTC_PRIVATE_KEY' }; } else { const wallet = ethers.Wallet.createRandom(); return { address: wallet.address, privateKey: wallet.privateKey }; } } /** * Sweeps funds from temporary wallet to Platform and Merchant */ async sweepFunds( tempWalletPrivateKey: string, platformAddress: string, tokenSymbol: string = 'USDT' ) { if (this.network === 'SOLANA') { return this.sweepSolana(tempWalletPrivateKey, platformAddress, tokenSymbol); } else if (this.network === 'TRON') { return this.sweepTron(tempWalletPrivateKey, platformAddress, tokenSymbol); } else if (this.network === 'BITCOIN') { return this.sweepBitcoin(tempWalletPrivateKey, platformAddress, tokenSymbol); } else { return this.sweepEVM(tempWalletPrivateKey, platformAddress, tokenSymbol); } } /** * Send a specific amount from treasury to a destination address (for payouts) */ async sendPayout( senderPrivateKey: string, destinationAddress: string, amount: string, tokenSymbol: string = 'SOL' ): Promise<{ success: boolean; txHash: string | null; error?: string }> { console.log(`[Payout] Sending ${amount} ${tokenSymbol} on ${this.network} to ${destinationAddress}`); if (this.network === 'SOLANA') { return this.sendSolanaPayout(senderPrivateKey, destinationAddress, amount, tokenSymbol); } else { // EVM / TRON / BTC - placeholder for now console.warn(`[Payout] Real transfer not yet implemented for ${this.network}. Using mock.`); return { success: true, txHash: `mock_${this.network}_${Date.now()}` }; } } private async sendSolanaPayout( senderPrivateKey: string, destinationAddress: string, amount: string, tokenSymbol: string ): Promise<{ success: boolean; txHash: string | null; error?: string }> { try { // Decode private key (support both base64 and base58) let secretKey: Uint8Array; try { secretKey = Uint8Array.from(Buffer.from(senderPrivateKey, 'base64')); if (secretKey.length !== 64) throw new Error('not base64'); } catch { secretKey = bs58.decode(senderPrivateKey); } const senderKeypair = Keypair.fromSecretKey(secretKey); const destPubKey = new PublicKey(destinationAddress); const parsedAmount = parseFloat(amount); console.log(`[Payout SOL] From: ${senderKeypair.publicKey.toBase58()} -> To: ${destinationAddress} | ${parsedAmount} ${tokenSymbol}`); if (tokenSymbol === 'SOL' || tokenSymbol === 'NATIVE') { const lamports = Math.floor(parsedAmount * LAMPORTS_PER_SOL); const balance = await this.solConnection.getBalance(senderKeypair.publicKey); if (balance < lamports + 5000) { return { success: false, txHash: null, error: `Insufficient SOL. Have: ${balance / LAMPORTS_PER_SOL}, Need: ${parsedAmount}` }; } const transaction = new Transaction().add( SystemProgram.transfer({ fromPubkey: senderKeypair.publicKey, toPubkey: destPubKey, lamports }) ); const { blockhash } = await this.solConnection.getLatestBlockhash(); transaction.recentBlockhash = blockhash; transaction.feePayer = senderKeypair.publicKey; transaction.sign(senderKeypair); const txHash = await this.solConnection.sendRawTransaction(transaction.serialize()); await this.solConnection.confirmTransaction(txHash); console.log(`[Payout SOL] ✅ Sent ${parsedAmount} SOL | TX: ${txHash}`); return { success: true, txHash }; } else { // SPL token payout const tokenConfig = this.getTokenConfig(tokenSymbol); if (!tokenConfig) return { success: false, txHash: null, error: `Token ${tokenSymbol} not found` }; const { createTransferInstruction, getAssociatedTokenAddress: getATA } = require('@solana/spl-token'); const mintPubKey = new PublicKey(tokenConfig.address); const senderATA = await getATA(mintPubKey, senderKeypair.publicKey); const destATA = await getATA(mintPubKey, destPubKey); const tokenAmount = Math.floor(parsedAmount * Math.pow(10, tokenConfig.decimals)); const transaction = new Transaction().add( createTransferInstruction(senderATA, destATA, senderKeypair.publicKey, tokenAmount) ); const { blockhash } = await this.solConnection.getLatestBlockhash(); transaction.recentBlockhash = blockhash; transaction.feePayer = senderKeypair.publicKey; transaction.sign(senderKeypair); const txHash = await this.solConnection.sendRawTransaction(transaction.serialize()); await this.solConnection.confirmTransaction(txHash); console.log(`[Payout SOL] ✅ Sent ${parsedAmount} ${tokenSymbol} | TX: ${txHash}`); return { success: true, txHash }; } } catch (e: any) { console.error(`[Payout SOL] ❌ Error:`, e.message); return { success: false, txHash: null, error: e.message }; } } private async sweepEVM( tempWalletPrivateKey: string, platformAddress: string, tokenSymbol: string ) { const tempWallet = new ethers.Wallet(tempWalletPrivateKey, this.provider); const tokenConfig = this.getTokenConfig(tokenSymbol); if (!tokenConfig) throw new Error(`Unsupported token ${tokenSymbol} on ${this.network}`); console.log(`[Sweep EVM] Sweeping 100% to Platform Treasury: ${platformAddress}`); // Mocking the real transfer for demo return { success: true, txHash: '0x' + Math.random().toString(16).slice(2, 66) }; } async fuelWallet(targetAddress: string, amount: string) { const gasTankKey = process.env.CRYPTO_GAS_TANK_KEY; if (!gasTankKey) { console.warn("[CryptoEngine] No CRYPTO_GAS_TANK_KEY provided. Fueling skipped (Demo mode)."); return; } try { const gasTank = new ethers.Wallet(gasTankKey, this.provider); const tx = await gasTank.sendTransaction({ to: targetAddress, value: ethers.parseEther(amount) }); await tx.wait(); console.log(`[CryptoEngine] Fueled ${targetAddress} with ${amount} native currency. Hash: ${tx.hash}`); } catch (error) { console.error("[CryptoEngine] Fueling failed:", error); } } private async sweepSolana( senderPrivateKey: string, destinationAddress: string, tokenSymbol: string ) { console.log(`[Sweep SOLANA] Sending ${tokenSymbol} to ${destinationAddress}`); try { // Decode private key (support both base64 and base58) let secretKey: Uint8Array; try { secretKey = Uint8Array.from(Buffer.from(senderPrivateKey, 'base64')); if (secretKey.length !== 64) throw new Error('not base64'); } catch { secretKey = bs58.decode(senderPrivateKey); } const senderKeypair = Keypair.fromSecretKey(secretKey); const destPubKey = new PublicKey(destinationAddress); if (tokenSymbol === 'SOL' || tokenSymbol === 'NATIVE') { // Get balance and send almost all (leave exactly 0 to avoid rent-exempt errors) // A standard Solana signature costs exactly 5000 lamports const balance = await this.solConnection.getBalance(senderKeypair.publicKey); const fee = 5000; const sendAmount = balance - fee; if (sendAmount <= 0) { return { success: false, txHash: null, error: 'Insufficient SOL balance' }; } const transaction = new Transaction().add( SystemProgram.transfer({ fromPubkey: senderKeypair.publicKey, toPubkey: destPubKey, lamports: sendAmount }) ); const { blockhash } = await this.solConnection.getLatestBlockhash(); transaction.recentBlockhash = blockhash; transaction.feePayer = senderKeypair.publicKey; transaction.sign(senderKeypair); const txHash = await this.solConnection.sendRawTransaction(transaction.serialize()); await this.solConnection.confirmTransaction(txHash); console.log(`[Sweep SOLANA] ✅ Sent ${sendAmount / LAMPORTS_PER_SOL} SOL | TX: ${txHash}`); return { success: true, txHash }; } else { // SPL Token transfer (USDT, USDC) - requires ATA const tokenConfig = this.getTokenConfig(tokenSymbol); if (!tokenConfig) throw new Error(`Token ${tokenSymbol} not found`); const { createTransferInstruction, getAssociatedTokenAddress: getATA, getOrCreateAssociatedTokenAccount } = require('@solana/spl-token'); const mintPubKey = new PublicKey(tokenConfig.address); const senderATA = await getATA(mintPubKey, senderKeypair.publicKey); const destATA = await getATA(mintPubKey, destPubKey); // Check sender token balance try { const accountInfo = await getAccount(this.solConnection, senderATA); const tokenBalance = Number(accountInfo.amount); if (tokenBalance <= 0) { return { success: false, txHash: null, error: `No ${tokenSymbol} balance` }; } const transaction = new Transaction().add( createTransferInstruction(senderATA, destATA, senderKeypair.publicKey, tokenBalance) ); const { blockhash } = await this.solConnection.getLatestBlockhash(); transaction.recentBlockhash = blockhash; transaction.feePayer = senderKeypair.publicKey; transaction.sign(senderKeypair); const txHash = await this.solConnection.sendRawTransaction(transaction.serialize()); await this.solConnection.confirmTransaction(txHash); console.log(`[Sweep SOLANA] ✅ Sent ${tokenBalance} ${tokenSymbol} | TX: ${txHash}`); return { success: true, txHash }; } catch (e: any) { console.error(`[Sweep SOLANA] Token transfer error:`, e.message); return { success: false, txHash: null, error: e.message }; } } } catch (e: any) { console.error(`[Sweep SOLANA] Error:`, e.message); return { success: false, txHash: null, error: e.message }; } } private async sweepTron( tempWalletPrivateKey: string, platformAddress: string, tokenSymbol: string ) { console.log(`[Sweep TRON] Sweeping 100% to Platform Treasury: ${platformAddress}`); return { success: true, txHash: 'tron_mock_tx_' + Math.random().toString(36).substring(7) }; } private async sweepBitcoin( tempWalletPrivateKey: string, platformAddress: string, tokenSymbol: string ) { console.log(`[Sweep BTC] Sweeping 100% to Platform Treasury: ${platformAddress}`); return { success: true, txHash: 'btc_mock_tx_' + Math.random().toString(36).substring(7) }; } async getBalance(address: string, tokenSymbol: string = 'NATIVE'): Promise { try { const tokenConfig = this.getTokenConfig(tokenSymbol); if (!tokenConfig) return "0.00"; if (this.network === 'SOLANA') { const pubKey = new PublicKey(address); if (tokenConfig.address === 'NATIVE') { const balance = await this.solConnection.getBalance(pubKey); return (balance / LAMPORTS_PER_SOL).toFixed(4); } else { const tokenMint = new PublicKey(tokenConfig.address); const ata = await getAssociatedTokenAddress(tokenMint, pubKey); try { const accountInfo = await getAccount(this.solConnection, ata); return (Number(accountInfo.amount) / Math.pow(10, tokenConfig.decimals)).toFixed(4); } catch (e) { return "0.00"; } } } else if (this.network === 'TRON') { try { if (tokenConfig.address === 'NATIVE') { const balance = await this.tronWeb.trx.getBalance(address); return (balance / 1000000).toFixed(2); } else { const contract = await this.tronWeb.contract().at(tokenConfig.address); const balance = await contract.balanceOf(address).call(); return (Number(balance) / Math.pow(10, tokenConfig.decimals)).toFixed(2); } } catch (e: any) { console.warn(`[CryptoEngine] TRON balance check failed (likely API key or address issue):`, e.message); return "0.00"; } } else if (this.network === 'BITCOIN') { try { const btcRes = await fetch(`https://blockchain.info/q/addressbalance/${address}`).then(r => r.text()); return (parseInt(btcRes) / 1e8).toFixed(8); } catch (e) { return "0.00"; } } else { // EVM try { const safeAddress = ethers.getAddress(address); if (tokenConfig.address === 'NATIVE') { const balance = await this.provider.getBalance(safeAddress); return ethers.formatEther(balance); } else { const safeTokenAddr = ethers.getAddress(tokenConfig.address); const contract = new ethers.Contract(safeTokenAddr, ERC20_ABI, this.provider); const balance = await contract.balanceOf(safeAddress); return ethers.formatUnits(balance, tokenConfig.decimals); } } catch (e: any) { console.warn(`[CryptoEngine] EVM balance check failed for ${tokenSymbol}:`, e.message); return "0.00"; } } } catch (e: any) { console.error(`[CryptoEngine] getBalance error for ${tokenSymbol}:`, e); return "0.00"; } } /** * Verifies if a specific amount has arrived at the address. */ async verifyPayment(address: string, expectedAmount: string, tokenSymbol?: string): Promise<{ success: boolean; txHash?: string; error?: string; }> { try { const tokenConfig = this.getTokenConfig(tokenSymbol || 'USDT'); if (!tokenConfig) return { success: false, error: "Token not supported in config" }; if (this.network === 'SOLANA') { const pubKey = new PublicKey(address); if (tokenConfig.address === 'NATIVE') { const balance = await this.solConnection.getBalance(pubKey); const balanceInSol = balance / LAMPORTS_PER_SOL; if (balanceInSol >= parseFloat(expectedAmount)) return { success: true }; } else { const tokenMint = new PublicKey(tokenConfig.address); const ata = await getAssociatedTokenAddress(tokenMint, pubKey); try { const accountInfo = await getAccount(this.solConnection, ata); const balance = Number(accountInfo.amount) / Math.pow(10, tokenConfig.decimals); if (balance >= parseFloat(expectedAmount)) return { success: true }; } catch (e) {} } } else if (this.network === 'TRON') { if (tokenConfig.address === 'NATIVE') { const balance = await this.tronWeb.trx.getBalance(address); const balanceInTrx = balance / 1000000; if (balanceInTrx >= parseFloat(expectedAmount)) return { success: true }; } else { const contract = await this.tronWeb.contract().at(tokenConfig.address); const balance = await contract.balanceOf(address).call(); const formattedBalance = Number(balance) / Math.pow(10, tokenConfig.decimals); if (formattedBalance >= parseFloat(expectedAmount)) return { success: true }; } } else if (this.network === 'BITCOIN') { try { const res = await fetch(`https://blockchain.info/rawaddr/${address}`); if (res.ok) { const data = await res.json(); const balanceInBtc = data.total_received / 100000000; if (balanceInBtc >= parseFloat(expectedAmount)) return { success: true }; } } catch (e) { console.warn("[BTC Verify] Public API failed, using mock success for demo if address is not empty"); if (address && address.length > 20) return { success: true }; } } else { if (tokenConfig.address === 'NATIVE') { const balance = await this.provider.getBalance(address); const balanceInEth = ethers.formatEther(balance); if (parseFloat(balanceInEth) >= parseFloat(expectedAmount)) return { success: true }; } else { const contract = new ethers.Contract(tokenConfig.address, ERC20_ABI, this.provider); const balance = await contract.balanceOf(address); const formattedBalance = ethers.formatUnits(balance, tokenConfig.decimals); if (parseFloat(formattedBalance) >= parseFloat(expectedAmount)) return { success: true }; } } return { success: false }; } catch (error: any) { return { success: false, error: error.message }; } } }