/** * Chain helpers for mavi — balance, send, optional test fund. * RPC comes from the network switcher (local private or public anvil). */ import { createPublicClient, createWalletClient, formatEther, http, parseEther, type Hex, } from "viem"; import { privateKeyToAccount } from "viem/accounts"; import { getActiveNetwork, type MaviNetwork } from "./network"; /** Anvil account #0 — local dev only, never production */ const ANVIL_0_KEY = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" as Hex; function chainFromNetwork(net: MaviNetwork) { return { id: net.chainId, name: net.label, nativeCurrency: { name: "ETH", symbol: "ETH", decimals: 18 }, rpcUrls: { default: { http: [net.rpcUrl] }, }, } as const; } function publicClient(net = getActiveNetwork()) { return createPublicClient({ chain: chainFromNetwork(net), transport: http(net.rpcUrl), }); } export type ChainBalance = { ok: boolean; eth: string; wei: string; chainId?: number; networkId?: string; networkLabel?: string; error?: string; }; export async function fetchEthBalance( address: `0x${string}`, ): Promise { const net = getActiveNetwork(); try { const client = publicClient(net); const [wei, chainId] = await Promise.all([ client.getBalance({ address }), client.getChainId(), ]); return { ok: true, eth: formatEther(wei), wei: wei.toString(), chainId, networkId: net.id, networkLabel: net.shortLabel, }; } catch (e) { return { ok: false, eth: "0", wei: "0", networkId: net.id, networkLabel: net.shortLabel, error: e instanceof Error ? e.message : `Chain unreachable (${net.shortLabel}). Start: bun run chains:up`, }; } } export async function sendEth(opts: { privateKey: Hex; to: `0x${string}`; amountEth: string; }): Promise<{ hash: `0x${string}`; networkId: string }> { const net = getActiveNetwork(); const chain = chainFromNetwork(net); const account = privateKeyToAccount(opts.privateKey); const client = createWalletClient({ account, chain, transport: http(net.rpcUrl), }); const publicC = publicClient(net); const hash = await client.sendTransaction({ account, to: opts.to, value: parseEther(opts.amountEth), chain, }); await publicC.waitForTransactionReceipt({ hash, timeout: 20_000 }); return { hash, networkId: net.id }; } /** * Dev-only: send test ETH from anvil account #0 to a mavi address. */ export async function fundFromAnvil( to: `0x${string}`, amountEth = "1", ): Promise<{ hash: `0x${string}`; networkId: string }> { const net = getActiveNetwork(); if (!net.canFund) { throw new Error("Test fund is only available on local anvil networks."); } return sendEth({ privateKey: ANVIL_0_KEY, to, amountEth, }); } export function isValidEthAddress(value: string): value is `0x${string}` { return /^0x[a-fA-F0-9]{40}$/.test(value.trim()); }