/** * Chain 138 (DeFi Oracle Meta Mainnet) read-only client. * * Uses `ethers` v6 JsonRpcProvider against `rpc-core.d-bis.org`. * All calls are read-only — no signing, no tx submission from here. * Wallet signing happens through the MetaMask BrowserProvider in AuthContext. */ import { JsonRpcProvider, formatEther, formatUnits, getAddress } from 'ethers'; import { endpoints } from '../config/endpoints'; /** * Normalize an EVM address before handing it to the provider. Ethers v6 * enforces EIP-55 checksum for mixed-case addresses and throws * `bad address checksum` otherwise — which silently loses balances for any * hand-typed sample address whose casing doesn't match the canonical * checksum. Lowercasing sidesteps that validation while remaining a * perfectly valid on-chain reference. If the string isn't a well-formed * address at all we still let `getAddress` surface the error. */ function normalizeAddress(address: string): string { try { return getAddress(address); } catch { return getAddress(address.toLowerCase()); } } let _provider: JsonRpcProvider | null = null; export function getChain138Provider(): JsonRpcProvider { if (_provider) return _provider; _provider = new JsonRpcProvider(endpoints.chain138.rpcUrl, { chainId: endpoints.chain138.chainId, name: endpoints.chain138.name, }); return _provider; } export interface ChainHealth { chainId: number; blockNumber: number; gasPriceGwei: number; latencyMs: number; rpcUrl: string; } export async function getChainHealth(): Promise { const provider = getChain138Provider(); const t0 = performance.now(); const [network, blockNumber, feeData] = await Promise.all([ provider.getNetwork(), provider.getBlockNumber(), provider.getFeeData(), ]); const latencyMs = Math.round(performance.now() - t0); const gasPriceWei = feeData.gasPrice ?? feeData.maxFeePerGas ?? 0n; const gasPriceGwei = Number(formatUnits(gasPriceWei, 'gwei')); return { chainId: Number(network.chainId), blockNumber, gasPriceGwei, latencyMs, rpcUrl: endpoints.chain138.rpcUrl, }; } export interface OnChainBalance { address: string; balanceEth: string; balanceWei: string; blockNumber: number; } export async function getNativeBalance(address: string): Promise { const provider = getChain138Provider(); const normalized = normalizeAddress(address); const [balanceWei, blockNumber] = await Promise.all([ provider.getBalance(normalized), provider.getBlockNumber(), ]); return { address, balanceWei: balanceWei.toString(), balanceEth: formatEther(balanceWei), blockNumber, }; } export async function getNativeBalances(addresses: string[]): Promise> { const results = await Promise.all( addresses.map(a => getNativeBalance(a).catch(err => { // Surface in dev — otherwise a single bad address silently disappears // from the balances map and the UI shows "off-chain" forever. // eslint-disable-next-line no-console console.warn(`[chain138] getNativeBalance(${a}) failed:`, err); return { address: a, error: err }; }), ), ); const out: Record = {}; for (const r of results) { if ('error' in r) continue; out[r.address] = r; } return out; } export interface LatestBlock { number: number; hash: string; timestamp: number; txCount: number; gasUsed: string; gasLimit: string; miner: string; } export async function getLatestBlock(): Promise { const provider = getChain138Provider(); const block = await provider.getBlock('latest'); if (!block) return null; return { number: block.number, hash: block.hash ?? '', timestamp: block.timestamp, txCount: block.transactions.length, gasUsed: block.gasUsed?.toString() ?? '0', gasLimit: block.gasLimit?.toString() ?? '0', miner: block.miner ?? '', }; }