Some checks failed
Deploy to Phoenix / deploy (push) Has been cancelled
- ADD_CHAIN138_TO_LEDGER_LIVE: Ledger form done; public code review repo bis-innovations/LedgerLive; init/push commands - CONTRACT_DEPLOYMENT_RUNBOOK: Chain 138 gas price 1 gwei, 36-addr check, TransactionMirror workaround - CONTRACT_*: AddressMapper, MirrorManager deployed 2026-02-12; 36-address on-chain check - NEXT_STEPS_FOR_YOU: Ledger done; steps completable now (no LAN); run-completable-tasks-from-anywhere - MASTER_INDEX, OPERATOR_OPTIONAL, SMART_CONTRACTS_INVENTORY_SIMPLE: updates - LEDGER_BLOCKCHAIN_INTEGRATION_COMPLETE: bis-innovations/LedgerLive reference Co-authored-by: Cursor <cursoragent@cursor.com>
157 lines
4.6 KiB
TypeScript
157 lines
4.6 KiB
TypeScript
/**
|
|
* 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<void> {
|
|
if (this.config.rpcUrls.length <= 1) return;
|
|
this.rpcIndex++;
|
|
this.provider = new JsonRpcProvider(this.getRpcUrl());
|
|
}
|
|
|
|
async getBlockNumber(): Promise<number> {
|
|
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<NormalizedReceipt | null> {
|
|
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<NormalizedLog[]> {
|
|
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<boolean> {
|
|
const block = await this.getBlock(blockNumber);
|
|
if (!block) return true;
|
|
return block.hash.toLowerCase() !== expectedBlockHash.toLowerCase();
|
|
}
|
|
|
|
async healthCheck(): Promise<boolean> {
|
|
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;
|
|
}
|
|
}
|
|
}
|