Networks
networks is the wallet’s list of networks and the endpoints to reach them:
- the catalog mainnets (30 EVM networks, Solana, TON, Tron, Bitcoin, NEAR, Zcash), each with a shipped list of public RPCs to fail over through;
- default testnets, shipped switched off;
- the RPCs and custom networks the user adds, and the ones sites add through
wallet_addEthereumChain; - a verifier that proves an endpoint serves the network it claims before the wallet trusts it.
It is plain TypeScript with no runtime dependencies beyond the asset catalog, and runs under
Hermes. Snippets come from examples/networks, which our CI typechecks and tests.
npm install @anywalletkit/networksimport { NetworksTable, SOLANA_CHAIN, checkRpcUrl, isAddChainError, parseAddChainParams, verifyNetwork, type FetchLike, type JsonStorage, type Network, type NetworksState,} from '@anywalletkit/networks'import { Wallet } from '@anywalletkit/kit'import { WalletManagerEvm } from '@anywalletkit/wallet-evm'import { WalletManagerSolana } from '@anywalletkit/wallet-solana'The table
Section titled “The table”/** Load once at cold start. Storage holds only the user's changes; the shipped lists stay current. */export async function openNetworks(storage: JsonStorage<NetworksState>): Promise<NetworksTable> { const table = new NetworksTable(storage) // AsyncStorage, MMKV, chrome.storage… await table.load() return table}The table stores only what the user changed. The shipped lists live in the package, so an app update with new endpoints reaches every install, and the user’s own RPCs stay on top.
| Rule | Why |
|---|---|
| Built-in networks can’t be removed; the user can add RPCs before the shipped list, or switch a network off | The fallback is always there |
“Only my RPC” (setOnlyOwnRpc) drops the shipped fallback |
For users who don’t want any public provider to see their addresses |
| A custom network needs at least one RPC and gets no fallback | There is nothing to fall back to |
| A stored custom entry for a built-in chain id is ignored | A site must not replace Ethereum’s endpoints |
A failing user RPC is parked with the reason (markRpc) until it passes again |
Failover skips it; the settings screen shows why |
Wire it into the wallet
Section titled “Wire it into the wallet”export function walletOnNetworks(seedPhrase: string, table: NetworksTable, evmChainIds: number[]): Wallet { const wallet = new Wallet(seedPhrase) for (const id of evmChainIds) { // One registration per EVM network: its own failover list, user RPCs first. wallet.registerWallet(`eip155:${id}`, WalletManagerEvm, { provider: table.rpcUrlsForSync(`eip155:${id}`), chainId: id }) } wallet.registerWallet('solana', WalletManagerSolana, { provider: table.rpcUrlsForSync(SOLANA_CHAIN) }) return wallet}rpcUrlsForSync answers from memory, so client factories can call it synchronously after
load(). Register each EVM network on its own chain id to give it its own list.
Two names for the same chain: the table names Solana solana:mainnet, while accounts report the
genesis-hash form (solana:5eykt4…). Use the table’s id when you read endpoints, and the
account’s chain everywhere else.
The user’s own RPC
Section titled “The user’s own RPC”/** "Add your own RPC": check the URL, prove the endpoint serves this chain, then store it. */export async function addUserRpc(table: NetworksTable, caip2: string, url: string, fetch?: FetchLike) { const policy = checkRpcUrl(url, { allowHttpLocal: table.allowHttpLocalSync() }) if (!policy.ok) return { ok: false as const, error: policy.message } // http, credentials in the URL…
const network = (await table.get(caip2))! const result = await verifyNetwork(network, { rpc: url, ...(fetch ? { fetch } : {}) }) if (!result.ok) return { ok: false as const, error: result.error! } // e.g. "chain-id-mismatch: …"
await table.addRpc(caip2, url) // goes before the shipped list await table.markRpc(caip2, url, result) return { ok: true as const, latencyMs: result.latencyMs }}checkRpcUrl allows https:// anywhere, and http:// only for private addresses (localhost,
192.168.x.x, *.local…) with the “allow local HTTP” setting on. Credentials in the URL are
refused: use a keyed path or a header.
verifyNetwork asks the endpoint who it is and compares it with the network:
| Family | Identity checked | Tip |
|---|---|---|
| EVM | eth_chainId |
eth_blockNumber |
| Solana | genesis hash | slot |
| TON | global id from config param 19 | masterchain seqno |
| Tron | genesis block id | latest block |
| Bitcoin, Zcash | genesis block | tip height |
| NEAR | chain_id |
latest block height |
It never throws. A failure comes back with a coded reason: chain-id-mismatch, url-policy,
redirected, http-<status>, timeout, unreachable… An endpoint that changes its chain id later is treated
as an attack, not an outage. Redirects are refused: the URL policy judged the address that was
entered, so an endpoint that answers through a redirect fails with redirected until the user enters
its final URL. Show the reason to the user; a site that supplied the endpoint gets a generic error.
/** Label screens that ran through an endpoint you didn't ship. */export function reviewLabel(table: NetworksTable, caip2: string): string | null { return table.usesUserRpcSync(caip2) ? 'Custom RPC: the simulation comes from a node you added' : null}A simulation or a balance from a node you didn’t ship is only as honest as that node. Say so on the review screen.
Testnets
Section titled “Testnets”Testnets ship switched off: Sepolia, Base Sepolia, Arbitrum Sepolia, OP Sepolia, Polygon Amoy,
BSC testnet, Monad testnet, Solana devnet, Bitcoin testnet4, TON testnet, Tron Nile, NEAR
testnet. A Zcash testnet entry exists, but no public API answers for it, so the user brings a
Blockbook. Turn one on with table.enable(caip2, true), and show a clear “testnet” badge
wherever its balances appear.
Sites that add a network
Section titled “Sites that add a network”/** A site calls wallet_addEthereumChain. Never add silently: ask, verify, then store. */export async function onAddEthereumChain( table: NetworksTable, origin: string, params: unknown, ask: (network: Network) => Promise<boolean>, fetch?: FetchLike,): Promise<null | { code: number; message: string }> { // `known` decides which chains a site may add: by default only the catalog (already built in), // here any chain the user approves. const parsed = parseAddChainParams(params, { origin, known: () => true }) if (isAddChainError(parsed)) return parsed // the EIP-1193 error to answer with if (await table.get(parsed.caip2)) return null // known already: nothing to add if (!(await ask(parsed))) return { code: 4001, message: 'User rejected the request.' } const result = await verifyNetwork(parsed, fetch ? { fetch } : {}) // The site gets a generic answer: what the endpoint said (or where it redirected) is for the user's // own screen, not for the page that supplied the endpoint. if (!result.ok) return { code: -32603, message: "The network's RPC failed the check." } await table.add(parsed) return null}parseAddChainParams turns a wallet_addEthereumChain request into a network, or into the
EIP-1193 error to answer with: -32602 for bad params, 4902 for a chain the wallet refuses.
By default only catalog chains are accepted, and those are built in already. Pass known to
let users add others. The new network records the site’s origin.
Never add a network silently. Show the chain id, the name, the RPC and the requesting site, verify the endpoint, and only then store it.