rwadurian/backend/services/blockchain-service/src/infrastructure/blockchain/evm-provider.adapter.ts

199 lines
5.9 KiB
TypeScript

import { Injectable, Logger } from '@nestjs/common';
import { JsonRpcProvider, Contract } from 'ethers';
import { ChainConfigService } from '@/domain/services/chain-config.service';
import { ChainType, BlockNumber, TokenAmount } from '@/domain/value-objects';
import { ChainTypeEnum } from '@/domain/enums';
// ERC20 Transfer 事件 ABI
const ERC20_TRANSFER_EVENT_ABI = [
'event Transfer(address indexed from, address indexed to, uint256 value)',
];
// ERC20 balanceOf ABI
const ERC20_BALANCE_ABI = [
'function balanceOf(address owner) view returns (uint256)',
'function decimals() view returns (uint8)',
];
export interface TransferEvent {
txHash: string;
logIndex: number;
blockNumber: bigint;
blockTimestamp: Date;
from: string;
to: string;
value: bigint;
tokenContract: string;
}
/**
* EVM 区块链提供者适配器
* 封装与 EVM 链的交互
*/
@Injectable()
export class EvmProviderAdapter {
private readonly logger = new Logger(EvmProviderAdapter.name);
private readonly providers: Map<ChainTypeEnum, JsonRpcProvider> = new Map();
constructor(private readonly chainConfig: ChainConfigService) {
this.initializeProviders();
}
private initializeProviders(): void {
for (const chainType of this.chainConfig.getSupportedChains()) {
const config = this.chainConfig.getConfig(ChainType.fromEnum(chainType));
const provider = new JsonRpcProvider(config.rpcUrl, config.chainId);
this.providers.set(chainType, provider);
this.logger.log(`Initialized provider for ${chainType}: ${config.rpcUrl}`);
}
}
private getProvider(chainType: ChainType): JsonRpcProvider {
const provider = this.providers.get(chainType.value);
if (!provider) {
throw new Error(`No provider for chain: ${chainType.toString()}`);
}
return provider;
}
/**
* 获取当前区块号
*/
async getCurrentBlockNumber(chainType: ChainType): Promise<BlockNumber> {
const provider = this.getProvider(chainType);
const blockNumber = await provider.getBlockNumber();
return BlockNumber.create(blockNumber);
}
/**
* 获取区块时间戳
*/
async getBlockTimestamp(chainType: ChainType, blockNumber: BlockNumber): Promise<Date> {
const provider = this.getProvider(chainType);
const block = await provider.getBlock(blockNumber.asNumber);
if (!block) {
throw new Error(`Block not found: ${blockNumber.toString()}`);
}
return new Date(block.timestamp * 1000);
}
/**
* 扫描指定区块范围内的 ERC20 Transfer 事件
*/
async scanTransferEvents(
chainType: ChainType,
fromBlock: BlockNumber,
toBlock: BlockNumber,
tokenContract: string,
): Promise<TransferEvent[]> {
const provider = this.getProvider(chainType);
const contract = new Contract(tokenContract, ERC20_TRANSFER_EVENT_ABI, provider);
const filter = contract.filters.Transfer();
const logs = await contract.queryFilter(filter, fromBlock.asNumber, toBlock.asNumber);
const events: TransferEvent[] = [];
for (const log of logs) {
const block = await provider.getBlock(log.blockNumber);
if (!block) continue;
const parsedLog = contract.interface.parseLog({
topics: log.topics as string[],
data: log.data,
});
if (parsedLog) {
events.push({
txHash: log.transactionHash,
logIndex: log.index,
blockNumber: BigInt(log.blockNumber),
blockTimestamp: new Date(block.timestamp * 1000),
from: parsedLog.args[0],
to: parsedLog.args[1],
value: parsedLog.args[2],
tokenContract,
});
}
}
return events;
}
/**
* 查询 ERC20 代币余额
*/
async getTokenBalance(
chainType: ChainType,
tokenContract: string,
address: string,
): Promise<TokenAmount> {
const provider = this.getProvider(chainType);
const contract = new Contract(tokenContract, ERC20_BALANCE_ABI, provider);
const [balance, decimals] = await Promise.all([
contract.balanceOf(address),
contract.decimals(),
]);
return TokenAmount.fromRaw(balance, Number(decimals));
}
/**
* 查询 ERC20 代币的 decimals
*/
async getTokenDecimals(chainType: ChainType, tokenContract: string): Promise<number> {
const provider = this.getProvider(chainType);
const contract = new Contract(tokenContract, ERC20_BALANCE_ABI, provider);
const decimals = await contract.decimals();
return Number(decimals);
}
/**
* 查询原生代币余额
*/
async getNativeBalance(chainType: ChainType, address: string): Promise<TokenAmount> {
const provider = this.getProvider(chainType);
const balance = await provider.getBalance(address);
return TokenAmount.fromRaw(balance, 18);
}
/**
* 广播签名交易
*/
async broadcastTransaction(chainType: ChainType, signedTx: string): Promise<string> {
const provider = this.getProvider(chainType);
const txResponse = await provider.broadcastTransaction(signedTx);
this.logger.log(`Transaction broadcasted: ${txResponse.hash}`);
return txResponse.hash;
}
/**
* 等待交易确认
*/
async waitForTransaction(
chainType: ChainType,
txHash: string,
confirmations: number = 1,
): Promise<boolean> {
const provider = this.getProvider(chainType);
const receipt = await provider.waitForTransaction(txHash, confirmations);
return receipt !== null && receipt.status === 1;
}
/**
* 检查交易是否确认
*/
async isTransactionConfirmed(
chainType: ChainType,
txHash: string,
requiredConfirmations: number,
): Promise<boolean> {
const provider = this.getProvider(chainType);
const receipt = await provider.getTransactionReceipt(txHash);
if (!receipt) return false;
const currentBlock = await provider.getBlockNumber();
const confirmations = currentBlock - receipt.blockNumber;
return confirmations >= requiredConfirmations;
}
}