Wallet integration
OpenPons is non-custodial. It never asks for, receives, stores or transmits a private key, seed phrase or raw signing key. Every write is signed in the user's own wallet.
Source: src/lib/wallet/eip6963.ts, src/lib/wallet/WalletProvider.tsx, src/components/WalletUI.tsx.
Discovery — EIP-6963
On page load OpenPons:
- Adds a listener for
eip6963:announceProvider. - Dispatches
eip6963:requestProvider. - Stores every announcement as
{ key, info: { uuid, name, icon, rdns }, provider }. The key is the wallet'srdns(e.g.io.metamask,io.rabby,com.okex.wallet), so the list is not hard-coded — any EIP-6963 wallet appears automatically. - ~350 ms later, checks
window.ethereum. If it exists and is not the same object as any announced provider, it is added as a separate Legacy injected entry.
The modal re-dispatches eip6963:requestProvider every time it opens, so wallets that load late
still show up.
Wallet icons are only rendered if they are data:image/* URIs (as EIP-6963 requires); anything
else is dropped to prevent remote tracking pixels or javascript: URIs.
Provider isolation
- Each discovered wallet keeps a reference to its own EIP-1193 provider object.
- After the user picks a wallet, every request (
eth_requestAccounts,eth_chainId,wallet_switchEthereumChain,eth_sendTransaction, …) is sent only to that object. window.ethereumis never used as an implicit default. It is only reachable by explicitly selecting the "Legacy injected" entry.accountsChanged/chainChanged/disconnectlisteners are attached to the selected provider only and removed when the selection changes.
Persistence
Only the wallet key (rdns) is stored in localStorage (openpons.wallet.v1). After reload,
providers are rediscovered and — if the same rdns reappears — OpenPons calls eth_accounts
(which never prompts) to restore the session. If the wallet is gone or locked, the user
simply sees "Connect wallet".
Robinhood Chain
| Field | Value |
|---|---|
| chainId | 4663 (0x1237) |
| Native gas | ETH |
| RPC | https://rpc.mainnet.chain.robinhood.com |
| Explorer | https://robinhoodchain.blockscout.com |
- Detection:
eth_chainIdon connect and on everychainChangedevent. - Wrong network UI: a banner under the header plus a "Wrong network" wallet button.
- Switch / add:
wallet_switchEthereumChain; on error4902(unknown chain) OpenPons callswallet_addEthereumChainwith the parameters above. - Reads never go through the wallet. They use OpenPons' own RPC client (official RPC first,
then optional
OPENPONS_RPC_URLS/NEXT_PUBLIC_OPENPONS_RPC_URLS) with retries and fallback. A wallet on the wrong network can therefore never corrupt displayed data.
The single write path
sendPrepared(tx, onUpdate) is the only function that asks a wallet to sign. It:
- Refuses if another transaction is in flight (double-click / duplicate protection).
- Refuses if
tx.chainId !== 4663, iftx.tois not an address, or iftx.fromis set and differs from the connected account. - Immediately before signing, re-reads
eth_chainIdandeth_accountsfrom the selected provider and aborts if either changed. - Sends
eth_sendTransactionwith an explicitchainId: 0x1237to the selected provider. - Tracks the receipt through OpenPons' RPC (not the wallet), with retries, and reports
confirmedorrevertedbased onreceipt.status.
Callers (Trade, Launch, Claim) must additionally re-quote and simulate right before calling
sendPrepared — see SECURITY.md.
Manual test matrix
| Scenario | Expected |
|---|---|
| Two EIP-6963 wallets installed | Both listed; picking one only prompts that one |
| Only a legacy wallet | Listed under "Legacy injected" |
| No wallet | Empty state with install hint |
| Wallet on another chain | Banner + "Wrong network"; trade buttons disabled |
| Switch account in wallet | Address updates; stale prepared txs are rejected (from mismatch) |
| Reload | Same wallet silently restored via eth_accounts |
| Double-click Buy | Second click ignored while the first is in flight |