Wallets
Wallets are key-management UIs. Understand the difference between EOA and smart-wallet, custodial vs non-custodial, and the UX rules that keep users safe.
Web3 — wallets
EXAMPLE
// ===== Wallet types =====
//
// EOA (Externally Owned Account)
// - Pair of (private key, public address)
// - Sign transactions with the private key
// - MetaMask, Rabby, Phantom, Coinbase Wallet, Frame
//
// Smart wallet (smart contract wallet / account abstraction, ERC-4337)
// - On-chain contract is the account
// - Owner(s) signal intent; bundlers post the tx
// - Per-tx gas sponsorship, social recovery, batched ops, multisig
// - Safe (Gnosis), Argent, Coinbase Smart Wallet
//
// Custody axis
// - Custodial: exchange holds keys (Coinbase exchange, Binance accounts)
// - Non-custodial: user holds keys (the default Web3 stance)
// - Hybrid / MPC: shares of the key spread across parties (Web3Auth, Magic, Privy)
// ===== Connect via EIP-1193 (the standard JS provider interface) =====
async function connect() {
if (!window.ethereum) throw new Error('install a wallet');
const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
return accounts[0]; // the active address
}
// Listen for account / chain changes:
window.ethereum.on('accountsChanged', (accs) => {/* refresh UI */});
window.ethereum.on('chainChanged', (cid) => {/* refresh UI; will hard-reload by spec */});
// ===== Multi-wallet via EIP-6963 (the modern discovery standard) =====
window.addEventListener('eip6963:announceProvider', (e) => {
const { info, provider } = e.detail;
// info.name, info.icon, info.uuid; provider is an EIP-1193 instance
});
window.dispatchEvent(new Event('eip6963:requestProvider'));
// Multiple wallets installed? Show them; let the user pick.
// ===== Sign-In with Ethereum (EIP-4361) =====
// Signed message proves the user controls the address.
const message = [
'app.example.com wants you to sign in with your Ethereum account:',
address,
'',
'Sign in to load your profile.',
'',
'URI: https://app.example.com',
'Version: 1',
'Chain ID: 1',
'Nonce: ' + nonceFromServer,
'Issued At: ' + new Date().toISOString(),
].join('\n');
const sig = await window.ethereum.request({
method: 'personal_sign', params: [message, address],
});
// Server verifies signature; issues a session.
// ===== Sending transactions (read with viem / ethers; write with wallet) =====
import { createWalletClient, custom, parseEther } from 'viem';
import { mainnet } from 'viem/chains';
const wallet = createWalletClient({ chain: mainnet, transport: custom(window.ethereum) });
const hash = await wallet.sendTransaction({
account: address,
to: '0xRecipient',
value: parseEther('0.01'),
});
// hash is the broadcast tx hash; the wallet shows a confirmation UI.
// ===== Hardware wallets =====
// Ledger / Trezor connect via USB / WebUSB / WebHID.
// They expose addresses via a derivation path; private keys never leave the device.
// Signing happens on-device with a physical confirm.
// ===== UX rules that keep users safe =====
// 1. Never ask for a seed phrase or private key in your UI. Period.
// 2. Show the exact tx the user will sign: function name, decoded params, recipient, value.
// 3. Default to mainnet only when the user opts in; testnets must be explicit.
// 4. Warn on:
// - Unlimited approvals (approve(spender, 2**256-1))
// - setApprovalForAll(true)
// - Cross-domain signatures (EIP-712 with domain != your dApp)
// 5. Pin the WalletConnect / connect modal to a single trusted lib; phishing modals exist.
// ===== Patterns to internalise =====
// - EIP-1193 for the connection; EIP-6963 for multi-wallet discovery; EIP-4361 for sign-in
// - viem / wagmi for the typed read path; let the wallet handle the write path
// - Smart wallets unlock UX wins (no gas, recovery) but cost a contract deploy
// - Hardware wallets for high-value accounts; require physical confirm
// - Per-app SIWE nonce; rotate frequently
// ===== Pitfalls =====
// - One window.ethereum injected by multiple wallets -> ambiguity; use EIP-6963
// - Asking users for unlimited token approvals 'for convenience' -> classic drain vector
// - Storing the seed phrase 'just in case' anywhere outside the wallet -> game over if leaked
// - Treating MPC / social recovery as identical to seed-phrase non-custodial; threat models differ
// - Trusting the wallet's currency formatting -> always show value + token + USD estimate yourself
Why it matters
A wallet is a key manager dressed up as a UI. EIP-1193 + EIP-6963 + SIWE is the modern connect-and-sign stack; smart wallets buy UX wins; hardware wallets protect the high-value accounts. The most important UX work is making the tx the user is about to sign legible, and refusing to ever ask for the seed phrase.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
// Wallets hold a private key. The signature proves ownership. // MetaMask, Rabby, Frame, Coinbase Wallet, Ledger / Trezor (hardware). // Never paste a seed phrase into a website. Ever.Try it Yourself »
Discussion
Loading…