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

231 lines
7.0 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 } from '@nestjs/common';
import { JsonRpcProvider, Contract } from 'ethers';
import { RpcProviderManager } from '@/domain/services/rpc-provider-manager.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 链的交互。通过 RpcProviderManager 获取 provider
* 自动上报 RPC 调用的成功/失败状态,实现故障转移。
*/
@Injectable()
export class EvmProviderAdapter {
private readonly logger = new Logger(EvmProviderAdapter.name);
constructor(private readonly rpcProviderManager: RpcProviderManager) {}
/**
* 获取某条链当前活跃的 provider由 RpcProviderManager 统一管理)
*/
private getProvider(chainType: ChainType): JsonRpcProvider {
return this.rpcProviderManager.getProvider(chainType.value);
}
/**
* 执行 RPC 调用并自动上报成功/失败状态
*
* 所有公开方法通过此辅助方法包裹 RPC 调用:
* - 成功时调用 reportSuccess() 重置失败计时
* - 失败时调用 reportFailure() 记录失败(超过 3 分钟自动切换端点)
* - 错误会 re-throw不影响调用方的错误处理逻辑
*/
private async executeWithFailover<T>(
chainType: ChainType,
operation: () => Promise<T>,
): Promise<T> {
try {
const result = await operation();
this.rpcProviderManager.reportSuccess(chainType.value);
return result;
} catch (error) {
this.rpcProviderManager.reportFailure(
chainType.value,
error instanceof Error ? error : new Error(String(error)),
);
throw error;
}
}
/**
* 获取当前区块号
*/
async getCurrentBlockNumber(chainType: ChainType): Promise<BlockNumber> {
return this.executeWithFailover(chainType, async () => {
const provider = this.getProvider(chainType);
const blockNumber = await provider.getBlockNumber();
return BlockNumber.create(blockNumber);
});
}
/**
* 获取区块时间戳
*/
async getBlockTimestamp(chainType: ChainType, blockNumber: BlockNumber): Promise<Date> {
return this.executeWithFailover(chainType, async () => {
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[]> {
return this.executeWithFailover(chainType, async () => {
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> {
return this.executeWithFailover(chainType, async () => {
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> {
return this.executeWithFailover(chainType, async () => {
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> {
return this.executeWithFailover(chainType, async () => {
const provider = this.getProvider(chainType);
const balance = await provider.getBalance(address);
return TokenAmount.fromRaw(balance, 18);
});
}
/**
* 广播签名交易
*/
async broadcastTransaction(chainType: ChainType, signedTx: string): Promise<string> {
return this.executeWithFailover(chainType, async () => {
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> {
return this.executeWithFailover(chainType, async () => {
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> {
return this.executeWithFailover(chainType, async () => {
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;
});
}
}