import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { ethers } from 'ethers'; import Decimal from 'decimal.js'; export interface TransactionResult { txHash: string; blockNumber: number; gasUsed: bigint; status: 'success' | 'failed'; } export interface TokenBalance { balance: Decimal; decimals: number; } @Injectable() export class KavaBlockchainService implements OnModuleInit { private readonly logger = new Logger(KavaBlockchainService.name); private provider: ethers.JsonRpcProvider; private hotWallet: ethers.Wallet | null = null; private blackHoleAddress: string; private isConnected = false; constructor(private readonly configService: ConfigService) { this.blackHoleAddress = this.configService.get( 'KAVA_BLACK_HOLE_ADDRESS', '0x000000000000000000000000000000000000dEaD', ); } async onModuleInit() { await this.connect(); } private async connect(): Promise { try { const rpcUrl = this.configService.get('KAVA_RPC_URL', 'https://evm.kava.io'); const chainId = this.configService.get('KAVA_CHAIN_ID', 2222); this.provider = new ethers.JsonRpcProvider(rpcUrl, chainId); // Test connection const network = await this.provider.getNetwork(); this.logger.log(`Connected to KAVA network: ${network.chainId}`); // Initialize hot wallet if private key is provided const privateKey = this.configService.get('KAVA_HOT_WALLET_PRIVATE_KEY'); if (privateKey) { this.hotWallet = new ethers.Wallet(privateKey, this.provider); this.logger.log(`Hot wallet initialized: ${this.hotWallet.address}`); } else { this.logger.warn('No hot wallet private key provided - blockchain operations limited'); } this.isConnected = true; } catch (error) { this.logger.error('Failed to connect to KAVA blockchain', error); this.isConnected = false; } } isReady(): boolean { return this.isConnected && this.hotWallet !== null; } getHotWalletAddress(): string | null { return this.hotWallet?.address || null; } getBlackHoleAddress(): string { return this.blackHoleAddress; } /** * 获取账户的原生币余额 */ async getBalance(address: string): Promise { const balance = await this.provider.getBalance(address); return new Decimal(ethers.formatEther(balance)); } /** * 获取当前区块号 */ async getCurrentBlockNumber(): Promise { return this.provider.getBlockNumber(); } /** * 获取交易状态 */ async getTransactionStatus(txHash: string): Promise<{ confirmed: boolean; blockNumber: number | null; confirmations: number; status: 'success' | 'failed' | 'pending'; }> { const receipt = await this.provider.getTransactionReceipt(txHash); if (!receipt) { return { confirmed: false, blockNumber: null, confirmations: 0, status: 'pending', }; } const currentBlock = await this.getCurrentBlockNumber(); const confirmations = currentBlock - receipt.blockNumber; return { confirmed: confirmations >= 1, blockNumber: receipt.blockNumber, confirmations, status: receipt.status === 1 ? 'success' : 'failed', }; } /** * 发送原生币(KAVA) */ async sendNative( toAddress: string, amount: Decimal, ): Promise { if (!this.hotWallet) { throw new Error('Hot wallet not initialized'); } const tx = await this.hotWallet.sendTransaction({ to: toAddress, value: ethers.parseEther(amount.toString()), }); const receipt = await tx.wait(); if (!receipt) { throw new Error('Transaction failed - no receipt'); } return { txHash: receipt.hash, blockNumber: receipt.blockNumber, gasUsed: receipt.gasUsed, status: receipt.status === 1 ? 'success' : 'failed', }; } /** * 发送 ERC20 代币 */ async sendToken( tokenAddress: string, toAddress: string, amount: Decimal, decimals: number = 18, ): Promise { if (!this.hotWallet) { throw new Error('Hot wallet not initialized'); } const erc20Abi = [ 'function transfer(address to, uint256 amount) returns (bool)', 'function balanceOf(address account) view returns (uint256)', 'function decimals() view returns (uint8)', ]; const tokenContract = new ethers.Contract(tokenAddress, erc20Abi, this.hotWallet); const amountWei = ethers.parseUnits(amount.toString(), decimals); const tx = await tokenContract.transfer(toAddress, amountWei); const receipt = await tx.wait(); if (!receipt) { throw new Error('Transaction failed - no receipt'); } return { txHash: receipt.hash, blockNumber: receipt.blockNumber, gasUsed: receipt.gasUsed, status: receipt.status === 1 ? 'success' : 'failed', }; } /** * 销毁到黑洞地址 */ async burnToBlackHole( tokenAddress: string, amount: Decimal, decimals: number = 18, ): Promise { return this.sendToken(tokenAddress, this.blackHoleAddress, amount, decimals); } /** * 获取 ERC20 代币余额 */ async getTokenBalance( tokenAddress: string, walletAddress: string, ): Promise { const erc20Abi = [ 'function balanceOf(address account) view returns (uint256)', 'function decimals() view returns (uint8)', ]; const tokenContract = new ethers.Contract(tokenAddress, erc20Abi, this.provider); const [balance, decimals] = await Promise.all([ tokenContract.balanceOf(walletAddress), tokenContract.decimals(), ]); return { balance: new Decimal(ethers.formatUnits(balance, decimals)), decimals, }; } /** * 估算 Gas 费用 */ async estimateGas( toAddress: string, value: Decimal, data?: string, ): Promise<{ gasLimit: bigint; gasPrice: bigint; estimatedFee: Decimal }> { const gasPrice = (await this.provider.getFeeData()).gasPrice || 0n; const gasLimit = await this.provider.estimateGas({ to: toAddress, value: ethers.parseEther(value.toString()), data: data || '0x', }); const estimatedFee = new Decimal(ethers.formatEther(gasLimit * gasPrice)); return { gasLimit, gasPrice, estimatedFee, }; } /** * 验证地址格式 */ isValidAddress(address: string): boolean { return ethers.isAddress(address); } /** * 监听新区块(用于检测充值) */ onNewBlock(callback: (blockNumber: number) => void): void { this.provider.on('block', callback); } /** * 停止监听 */ removeAllListeners(): void { this.provider.removeAllListeners(); } }