272 lines
6.8 KiB
TypeScript
272 lines
6.8 KiB
TypeScript
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();
|
||
}
|
||
}
|