231 lines
7.0 KiB
TypeScript
231 lines
7.0 KiB
TypeScript
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;
|
||
});
|
||
}
|
||
}
|