199 lines
5.9 KiB
TypeScript
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;
|
|
}
|
|
}
|