A starter template for building browser extension wallets with the Tether Wallet Development Kit (WDK). Clone, customize, and ship your own self-custodial wallet.
This project is a ready-to-use starting point for building a non-custodial browser extension wallet on top of WDK - Tether's modular, stateless, self-custodial SDK for multi-chain wallet development. Private keys never leave the user's device and no data is stored by WDK.
The starter comes pre-wired with EVM support (via @tetherto/wdk-wallet-evm), password-encrypted vault storage, EIP-1193/EIP-6963 dApp integration, and a popup UI built with React 19 and shadcn/ui. Extend it with additional WDK wallet modules (Bitcoin, TON, TRON, Solana, etc.) or protocol modules (swaps, bridges, lending) as needed.
- WDK (Wallet Development Kit) - Tether's self-custodial, stateless wallet SDK
@tetherto/wdk-wallet-evm- EVM wallet module with BIP-39/BIP-44 support
- ethers.js 6 - Ethereum library for blockchain interactions
- bip39 - Mnemonic seed phrase generation and validation
- WXT - Modern web extension framework with hot-reload, TypeScript support, and cross-browser compatibility (Chrome, Firefox)
- React 19 - Latest React with concurrent features and improved hooks
- TypeScript 5.9 - Type-safe development with strict mode enabled
- Tailwind CSS v4 - Utility-first CSS framework with custom design system
- shadcn/ui - High-quality, accessible component library
- class-variance-authority - Type-safe variant management for components
- Vite - Lightning-fast bundler with HMR
- Vitest - Unit testing framework
The wallet uses military-grade encryption to protect your secret recovery phrase:
Encryption Algorithm: AES-256-GCM (Galois/Counter Mode)
- Key Length: 256 bits (strongest AES variant)
- Authentication: Built-in authentication tag prevents tampering
- IV (Initialization Vector): 12 bytes, randomly generated per encryption
Key Derivation: PBKDF2 (Password-Based Key Derivation Function 2)
- Hash Algorithm: SHA-256
- Iterations: 600,000 (OWASP recommended as of 2023)
- Salt: 16 bytes, cryptographically random per password
Implementation (lib/crypto/vault.ts):
// User's password → PBKDF2 (600k iterations) → 256-bit key
// Mnemonic → AES-256-GCM encryption → Ciphertext + IV + Salt
// Stored: { ciphertext, iv, salt, kdf, encryption, createdAt }The encrypted vault is never decrypted except when:
- User unlocks wallet with password
- Mnemonic is needed to sign transactions
- Immediately cleared from memory after use via
dispose()methods
WXT Storage is used instead of raw chrome.storage for several critical reasons:
- Type-Safe API: TypeScript-first with automatic serialization
- Cross-Browser Compatibility: Abstracts browser differences (Chrome vs Firefox)
- Reactive Updates: Built-in watchers for storage changes across extension contexts
- Migration Support: Versioned storage with automatic migration paths
- Prefix Isolation: Automatic key prefixing prevents collisions
Usage (lib/storage/vault-storage.ts):
import { storage } from 'wxt/utils/storage'
// Stores in chrome.storage.local with 'local:' prefix
await storage.setItem('local:vault', encryptedVault)Storage Location: Browser's encrypted storage API (chrome.storage.local)
- Encrypted at rest by the browser
- Isolated per extension
- Survives browser restarts
- NOT synced across devices (sensitive data stays local)
EIP-6963 is a standard for multi-wallet discovery in web3.
The Problem It Solves:
- Old approach: Every wallet fights for
window.ethereum - Result: Last-loaded wallet wins, others get overwritten
- Users couldn't choose their preferred wallet
How EIP-6963 Works:
- Wallet Announces: Extension dispatches
eip6963:announceProviderevent with metadata - dApp Requests: dApp dispatches
eip6963:requestProviderto discover all wallets - User Chooses: dApp shows wallet selector, user picks WDK Browser Extension Wallet
- Clean Coexistence: All wallets available without conflicts
Implementation (entrypoints/inpage.ts):
// Announce Wallet to dApps
const providerInfo = {
uuid: crypto.randomUUID(), // Unique session ID
name: 'WDK Browser Extension Wallet', // Display name
icon: 'data:image/svg+xml,...', // Base64 SVG logo
rdns: 'com.wdk.wallet', // Reverse DNS identifier
}
window.dispatchEvent(
new CustomEvent('eip6963:announceProvider', {
detail: { info: providerInfo, provider: inpageProvider },
}),
)Result: WDK Browser Extension Wallet appears in dApp wallet selectors alongside MetaMask, Coinbase Wallet, etc.
Why We Still Inject window.ethereum:
Object.defineProperty(window, 'ethereum', {
value: provider,
writable: false,
configurable: false,
})Reasons:
- Backward Compatibility: Many dApps built before EIP-6963 only check
window.ethereum - Default Wallet UX: Users who only have WDK Browser Extension Wallet installed get instant access
- Standard Compliance: EIP-1193 (Ethereum Provider API) requires this pattern
Trade-off: If multiple wallets are installed, the last-loaded wallet "wins" window.ethereum. This is why EIP-6963 exists—it lets users choose explicitly.
Three-Layer Isolation:
-
Background Script (
entrypoints/background.ts)- Runs in isolated extension context
- Manages wallet session and encrypted vault
- Has access to
chrome.storageand extension APIs - Never exposed to web pages
-
Content Script (
entrypoints/content.ts)- Runs in page context but isolated from page JS
- Acts as message bridge between inpage ↔ background
- Prevents page scripts from accessing extension APIs
-
Inpage Script (
entrypoints/inpage.ts)- Runs in page's JavaScript context (same as dApp code)
- Exposes
window.ethereumprovider - Sends requests via
postMessageto content script - No direct access to wallet keys or extension APIs
Security Model:
dApp JS → window.ethereum.request()
↓ (postMessage)
Content Script → browser.runtime.sendMessage()
↓ (chrome messaging)
Background Script → WalletSession → Decrypt Vault → Sign TX
↓ (response)
Content Script → window.postMessage()
↓ (response)
dApp JS ← transaction hash
Key Security Properties:
- dApp never sees your private keys or mnemonic
- All signatures happen in background script
- User must explicitly unlock wallet with password
- Session expires after inactivity
- Content Security Policy prevents inline scripts
wdk-starter-browser-extension/
├── entrypoints/ # Extension entry points
│ ├── popup/ # Extension popup UI (React)
│ ├── background.ts
│ ├── content.ts
│ └── inpage.ts
├── lib/
│ ├── crypto/ # Vault encryption (AES-256-GCM)
│ ├── storage/ # WXT storage abstractions
│ ├── session/ # Wallet session management
│ ├── providers/ # EIP-1193 & EIP-6963 providers
│ ├── balance/ # Token balance queries
│ ├── network/ # Network/chain configuration
│ └── transport/ # Extension messaging layer
├── components/
│ └── ui/ # shadcn/ui components
├── assets/ # Static assets (CSS, SVGs)
├── public/ # Extension icons
├── wxt.config.ts
├── vitest.config.ts
└── tsconfig.json
# Install dependencies
npm install
# Start extension in dev mode (Chrome)
npm run dev
# Start extension in dev mode (Firefox)
npm run dev:firefox
# Run tests
npm test
# Lint and format
npm run lint# Build for Chrome
npm run build
# Build for Firefox
npm run build:firefox
# Create distribution zips
npm run zipChrome:
- Run
npm run build - Open
chrome://extensions/ - Enable Developer mode (top-right toggle)
- Click Load unpacked
- Select the
.output/chrome-mv3/directory
Firefox:
- Run
npm run build:firefox - Open
about:debugging#/runtime/this-firefox - Click Load Temporary Add-on
- Select any file inside
.output/firefox-mv3/
- Click the extension icon in the toolbar and create a wallet
- Open any dApp (e.g. app.uniswap.org) and click Connect Wallet — the extension should appear via EIP-6963
A bundled test dApp is included at test-dapp.html for verifying provider detection, wallet connection, and network switching without a real dApp:
# serve it locally
npx serve . -l 8080
# then open http://localhost:8080/test-dapp.htmlTo debug, inspect the background service worker from chrome://extensions/ → service worker link.
npm test- EIP-1193: Ethereum Provider JavaScript API
- EIP-6963: Multi-Wallet Discovery Standard
- BIP-39: Mnemonic Seed Phrases
- BIP-44: Multi-Account Hierarchy (m/44'/60'/0'/0/n)
Apache-2.0 - See LICENSE for details.
Contributions are welcome! Please feel free to submit a Pull Request.
For support, please open an issue on the GitHub repository.
- WDK Documentation - Wallet Development Kit guides
- WXT Documentation - Web Extension Framework
- WDK Wallet Cosmos - A simple and secure package to manage BIP-44 wallets for Cosmos-compatible blockchains.




