Skip to content
docs

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.

Terminal window
npm install @anywalletkit/networks
import {
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'
/** 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
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.

/** "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 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.

/** 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.