// Generic single-address read-only imported adapter for account-model // chains. One config-driven runtime handles ETH-family, Tron, and Solana // balance polling — every chain differs only in the RPC verb and the // JSON path to the balance number. // // The adapter mirrors the public shape every Aegis chain runtime exposes // (snapshot, refresh, plan, signAndBroadcast, dispose) so mountWallet // stays chain-agnostic. planSend/send throw a "read-only" error until // M.1b delivers the sign path per chain. module.exports = function makeGenericImportedAdapter() { const CHAIN_CFGS = { eth: { ticker: "ETH", decimals: 18, networks: { mainnet: { id: "mainnet", label: "Mainnet", rpc: "https://eth.llamarpc.com", explorerAddr: "https://etherscan.io/address/", explorerTx: "https://etherscan.io/tx/" }, sepolia: { id: "sepolia", label: "Sepolia", rpc: "https://ethereum-sepolia-rpc.publicnode.com", explorerAddr: "https://sepolia.etherscan.io/address/", explorerTx: "https://sepolia.etherscan.io/tx/", testnet: true, faucet: "https://sepoliafaucet.com/" }, }, // JSON-RPC eth_getBalance → hex-string wei. async fetchBalance({ rpc, address }) { const r = await fetch(rpc, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "eth_getBalance", params: [address, "latest"] }) }); const j = await r.json(); const hex = String(j?.result || "0x0").replace(/^0x/, ""); return BigInt("0x" + hex).toString(); }, }, trx: { ticker: "TRX", decimals: 6, networks: { mainnet: { id: "mainnet", label: "Mainnet", rpc: "https://api.trongrid.io", explorerAddr: "https://tronscan.org/#/address/", explorerTx: "https://tronscan.org/#/transaction/" }, nile: { id: "nile", label: "Nile testnet", rpc: "https://api.nileex.io", explorerAddr: "https://nile.tronscan.org/#/address/", explorerTx: "https://nile.tronscan.org/#/transaction/", testnet: true, faucet: "https://nileex.io/join/getJoinPage" }, }, // Tron HTTP API returns account.balance in SUN (10^-6 TRX). async fetchBalance({ rpc, address }) { const r = await fetch(rpc.replace(/\/+$/, "") + "/wallet/getaccount", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ address, visible: true }) }); const j = await r.json(); return String(j?.balance || 0); }, }, sol: { ticker: "SOL", decimals: 9, networks: { mainnet: { id: "mainnet", label: "Mainnet-beta", rpc: "https://api.mainnet-beta.solana.com", explorerAddr: "https://explorer.solana.com/address/", explorerTx: "https://explorer.solana.com/tx/" }, devnet: { id: "devnet", label: "Devnet", rpc: "https://api.devnet.solana.com", explorerAddr: "https://explorer.solana.com/address/", explorerTx: "https://explorer.solana.com/tx/", explorerSuffix: "?cluster=devnet", testnet: true, faucet: "https://faucet.solana.com/" }, }, // Solana JSON-RPC getBalance returns lamports as a number. async fetchBalance({ rpc, address }) { const r = await fetch(rpc, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "getBalance", params: [address] }) }); const j = await r.json(); return String(j?.result?.value || 0); }, }, }; class GenericImportedWallet { constructor({ chain, network, address, log = () => {}, onChange = () => {}, rpcUrl } = {}) { const cfg = CHAIN_CFGS[chain]; if (!cfg) throw new Error(`chain-generic-imported: unknown chain ${chain}`); const net = cfg.networks[network]; if (!net) throw new Error(`chain-generic-imported: ${chain} has no network ${network}`); if (!address) throw new Error("address required"); this.chain = chain; this.network = network; this._cfg = cfg; this._net = { ...net, rpc: rpcUrl || net.rpc }; this.log = log; this.onChange = onChange; this._address = address; this._state = { balance: { confirmed: "0", unconfirmed: "0" }, history: [], scanning: false, error: null, }; this._pollTimer = null; } setServers() { /* no-op: this adapter uses HTTP RPC, not electrum */ } schedulePoll(ms) { clearTimeout(this._pollTimer); this._pollTimer = setTimeout(() => { this.refresh(false).catch(() => {}); this.schedulePoll(ms); }, ms); } _emit() { try { this.onChange(); } catch {} } snapshot() { return { chain: this.chain, network: this.network, ticker: this._cfg.ticker, decimals: this._cfg.decimals, address: this._address, addressIndex: 0, addressPath: null, balance: this._state.balance, history: this._state.history, scanning: this._state.scanning, error: this._state.error, server: this._net.rpc, rpcUrl: this._net.rpc, imported: true, explorerAddr: this._net.explorerAddr, explorerTx: this._net.explorerTx, explorerSuffix: this._net.explorerSuffix || "", faucet: this._net.faucet || null, }; } async refresh() { this._state.scanning = true; this._emit(); try { const confirmed = await this._cfg.fetchBalance({ rpc: this._net.rpc, address: this._address }); this._state.balance = { confirmed: String(confirmed || 0), unconfirmed: "0" }; this._state.error = null; } catch (e) { this._state.error = e?.message || String(e); } finally { this._state.scanning = false; this._emit(); } } nextAddress() { return { address: this._address, index: 0 }; } current() { return { address: this._address, index: 0, branch: 0, path: null }; } plan() { throw new Error(`Imported ${this.chain.toUpperCase()} wallets are read-only in this build. Spending support ships in the next Aegis update.`); } signAndBroadcast() { throw new Error("read-only"); } signMessage() { throw new Error("read-only"); } recovery() { return { accountPath: null, xpub: null, xprv: null, note: "Recovery lives in the source of the import." }; } dispose() { clearTimeout(this._pollTimer); } } return { GenericImportedWallet, CHAIN_CFGS }; };