- 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>
134 lines
3.9 KiB
TypeScript
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 ?? '',
|
|
};
|
|
}
|