/** * Base chain adapter: RPC abstraction, receipt/log fetch, reorg detection, fallback RPC. */ import { JsonRpcProvider, TransactionReceipt, Log } from 'ethers'; import type { ChainAdapterConfig, NormalizedReceipt, NormalizedLog, IChainAdapter } from './types.js'; import { getChainConfig } from './config.js'; function toHex(n: bigint): string { return '0x' + n.toString(16); } export abstract class BaseChainAdapter implements IChainAdapter { protected provider: JsonRpcProvider; protected config: ChainAdapterConfig; private rpcIndex = 0; constructor(chainId: number, rpcUrls?: string[]) { const cfg = getChainConfig(chainId); if (!cfg) throw new Error(`Unknown chainId: ${chainId}`); this.config = rpcUrls?.length ? { ...cfg, rpcUrls } : cfg; this.provider = new JsonRpcProvider(this.config.rpcUrls[0]); } getChainId(): number { return this.config.chainId; } getConfig(): ChainAdapterConfig { return this.config; } protected getRpcUrl(): string { return this.config.rpcUrls[this.rpcIndex % this.config.rpcUrls.length]; } protected async switchRpc(): Promise { if (this.config.rpcUrls.length <= 1) return; this.rpcIndex++; this.provider = new JsonRpcProvider(this.getRpcUrl()); } async getBlockNumber(): Promise { const n = await this.provider.getBlockNumber(); return n; } async getBlock(blockNumber: number): Promise<{ number: number; hash: string; parentHash: string; timestamp: number } | null> { try { const block = await this.provider.getBlock(blockNumber); if (!block) return null; return { number: block.number, hash: block.hash ?? '', parentHash: block.parentHash ?? '', timestamp: block.timestamp, }; } catch { return null; } } async sendTransaction(signedTxHex: string): Promise<{ hash: string; from: string; nonce: number }> { const tx = await this.provider.broadcastTransaction(signedTxHex); return { hash: tx.hash, from: tx.from ?? '', nonce: tx.nonce, }; } async getTransactionReceipt(txHash: string): Promise { try { const receipt = await this.provider.getTransactionReceipt(txHash); if (!receipt) return null; return this.normalizeReceipt(receipt); } catch { return null; } } protected normalizeReceipt(receipt: TransactionReceipt): NormalizedReceipt { return { chainId: this.config.chainId, transactionHash: receipt.hash, blockNumber: BigInt(receipt.blockNumber), blockHash: receipt.blockHash ?? '', transactionIndex: receipt.index, from: receipt.from, to: receipt.to ?? null, gasUsed: BigInt(receipt.gasUsed.toString()), cumulativeGasUsed: BigInt(receipt.cumulativeGasUsed.toString()), contractAddress: receipt.contractAddress ?? null, logsBloom: receipt.logsBloom ?? '', status: receipt.status === 1 ? 1 : 0, root: receipt.root ?? null, }; } async getLogs( fromBlock: number, toBlock: number, address?: string, topics?: string[] ): Promise { const filter: { fromBlock: number; toBlock: number; address?: string; topics?: string[] } = { fromBlock, toBlock, }; if (address) filter.address = address; if (topics?.length) filter.topics = topics as `0x${string}`[]; const logs = await this.provider.getLogs(filter); return logs.map((log) => this.normalizeLog(log)); } protected normalizeLog(log: Log): NormalizedLog { const topics = log.topics as string[]; return { chainId: this.config.chainId, transactionHash: log.transactionHash, blockNumber: BigInt(log.blockNumber), blockHash: log.blockHash ?? '', logIndex: log.index, address: log.address, topic0: topics[0] ?? null, topic1: topics[1] ?? null, topic2: topics[2] ?? null, topic3: topics[3] ?? null, data: log.data, }; } async detectReorg(blockNumber: number, expectedBlockHash: string): Promise { const block = await this.getBlock(blockNumber); if (!block) return true; return block.hash.toLowerCase() !== expectedBlockHash.toLowerCase(); } async healthCheck(): Promise { try { await this.provider.getBlockNumber(); return true; } catch { if (this.config.rpcUrls.length > 1) { await this.switchRpc(); try { await this.provider.getBlockNumber(); return true; } catch { return false; } } return false; } } }