rwadurian/backend/services/mining-wallet-service/src/infrastructure/blockchain/kava-blockchain.service.ts

272 lines
6.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<string>(
'KAVA_BLACK_HOLE_ADDRESS',
'0x000000000000000000000000000000000000dEaD',
);
}
async onModuleInit() {
await this.connect();
}
private async connect(): Promise<void> {
try {
const rpcUrl = this.configService.get<string>('KAVA_RPC_URL', 'https://evm.kava.io');
const chainId = this.configService.get<number>('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<string>('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<Decimal> {
const balance = await this.provider.getBalance(address);
return new Decimal(ethers.formatEther(balance));
}
/**
* 获取当前区块号
*/
async getCurrentBlockNumber(): Promise<number> {
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<TransactionResult> {
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<TransactionResult> {
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<TransactionResult> {
return this.sendToken(tokenAddress, this.blackHoleAddress, amount, decimals);
}
/**
* 获取 ERC20 代币余额
*/
async getTokenBalance(
tokenAddress: string,
walletAddress: string,
): Promise<TokenBalance> {
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();
}
}