Files
CurrenciCombo/src/services/chain138.ts
Devin AI 007c79d7a9 feat(portal): wire DashboardPage to live Chain-138 RPC + SolaceScan Explorer
- Add services/{http,chain138,explorer,proxmox,dbisCore} + hooks/{useLiveChain,useOnChainBalances}
- Add BackendStatusBar + LiveNetworkPanel components on DashboardPage
- Overlay on-chain META balance on account rows carrying a walletAddress
- Normalize EIP-55 checksum in chain138.getNativeBalance so hand-typed
  sample custody addresses (e.g. 0x742d35Cc...bD38) don't silently drop
  out of the balance map
- Default RPC: https://rpc.d-bis.org (user-preferred gateway)
- proxmox.ts stays mocked (CF-Access, needs BFF); dbisCore.ts stays
  mocked (no public deployment yet)

Co-Authored-By: Nakamoto, S <defi@defi-oracle.io>
2026-04-19 00:33:46 +00:00

134 lines
3.9 KiB
TypeScript

/**
* 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<ChainHealth> {
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<OnChainBalance> {
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<Record<string, OnChainBalance>> {
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<string, OnChainBalance> = {};
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<LatestBlock | null> {
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 ?? '',
};
}