Thank you for your interest in contributing! This project is a stupid wallet app with a Safari Web Extension that injects an EIP-1193 provider and supports multi-provider discovery via EIP-6963. This document explains the architecture, how to set up your environment, and how to submit contributions.
-
App (SwiftUI)
- UI:
ios-wallet/ContentView.swiftimplements a minimal wallet UI. - Key management: Uses Dawn Key Management to encrypt and store the private key using Secure Enclave + Keychain.
- RPC + balances: Uses Web3.swift (+ PromiseKit) to query balances on multiple networks.
- Shared storage: Persists values in an App Group
UserDefaultsfor the Safari extension to read:walletAddress(checksummed address)chainId(current chain as hex, e.g.0x1)customChains(dictionary keyed by hex chainId containing chain metadata and optionalrpcUrls)connectedSites(dictionary keyed by domain hostname; values contain{ address?, connectedAt }used to persist per‑domain dApp connection state and enable auto‑connect)
- UI:
-
Safari Web Extension (
safari/)- Injected provider (main world):
safari/Resources/inject.js- EIP-1193 provider with supported methods:
eth_requestAccounts,eth_accountseth_chainId,eth_blockNumbereth_getTransactionByHash,eth_getTransactionReceipt,eth_getBlockByNumberwallet_addEthereumChain,wallet_switchEthereumChainpersonal_sign,eth_signTypedData_v4eth_sendTransaction
- Emits
accountsChangedandchainChangedwhere applicable. - EIP-6963 provider discovery: announces via
eip6963:announceProviderand responds toeip6963:requestProvider. - Communicates with the extension via
window.postMessageto avoid restricted APIs in the main world.
- EIP-1193 provider with supported methods:
- Content script (isolated world): built bundle at
safari/Resources/dist/content.iife.js- Source lives in
web-ui/src/main.tsxand is bundled via Vite. - Bridges between the injected provider and the background service worker using
web-ui/src/bridge.tsfor fast methods andweb-ui/src/App.tsxfor UI flows. - Presents in-page modals using React + shadcn/ui (Credenza) mounted within a Shadow DOM for consented flows:
- Connect (
eth_requestAccounts) - Message signing (
personal_sign) - Typed data signing (
eth_signTypedData_v4) - Transaction sending (
eth_sendTransaction)
- Connect (
- Source lives in
- Background (service worker):
safari/Resources/background.js- Receives wallet requests, routes to native handler, or responds immediately when trivial.
- Implements a pending → confirm handshake for consented flows (connect, sign, typed data, send tx).
- Supports routing for the methods listed above and falls back to safe defaults when native is unavailable.
- Native handler (Swift):
safari/SafariWebExtensionHandler.swift- Implements:
- Accounts and network:
eth_requestAccounts,eth_accounts,eth_chainId,eth_blockNumber. - Transaction and block queries:
eth_getTransactionByHash,eth_getTransactionReceipt,eth_getBlockByNumber— direct RPC passthrough that preserves null responses. - Chains:
wallet_addEthereumChain(persists metadata undercustomChains),wallet_switchEthereumChain(updateschainId). - Signing:
personal_sign(EIP-191),eth_signTypedData_v4(EIP-712) — uses Dawn Key Management to sign digests without exporting keys. - Transactions:
eth_sendTransaction— builds legacy or EIP-1559 transactions, signs, and broadcasts via Web3.swift.
- Accounts and network:
- Implements:
- Injected provider (main world):
- DApp → Provider: DApp calls
window.ethereum.request({ method }). - EIP‑6963: Provider announces over window events; DApps can discover this provider without clobbering
window.ethereum. - Request path:
- Injected provider posts a message to the window (
stupid-wallet-inject). - Content script relays to background via
browser.runtime.sendMessage. - Background queries native/handler (or shared storage) and returns the result; for consented flows it first replies
{ pending: true }. - On
{ pending: true }, the content script displays a modal and then sends aWALLET_CONFIRMto background; background finalizes by calling native and returns{ result }or{ error }. - Content script posts the final response back to the injected provider, which resolves the original request.
- Injected provider posts a message to the window (
-
Storage key and semantics
connectedSites: App GroupUserDefaultsdictionary mapping domain hostname →{ address?: string, connectedAt: ISO‑8601 string }.- Source of truth shared by the app and extension; domains are normalized to lowercase hostnames.
-
Auto‑connect rules
eth_requestAccounts: if the domain exists inconnectedSites, short‑circuit (no modal) and return the account(s).wallet_connect: ifparams[0].capabilitiesis absent or an empty object and the domain exists inconnectedSites, short‑circuit (no modal). If capabilities are present and non‑empty (e.g., SIWE), show the modal and run the full flow.
-
Gating
eth_accounts- When the domain is not in
connectedSites, return an EIP‑1193 RPC error{ code: 4100, message: "Unauthorized" }. - When connected, return the account list from the native handler.
- When the domain is not in
-
Disconnect and clearing
wallet_disconnectremoves the domain fromconnectedSites.- Clearing the wallet in the app also removes
connectedSites, revoking auto‑connect for all domains.
-
iOS App
ios-wallet/ContentView.swift: UI, persistence, and balance fetching.ios-wallet/ios_walletApp.swift: App entry point.
-
Safari Extension
safari/SafariWebExtensionHandler.swift: Native handler for web extension requests.safari/Resources/inject.js: EIP-1193 provider + EIP-6963 discovery.safari/Resources/dist/content.iife.js: Built content script bundle (do not edit).safari/Resources/background.js: Service worker handling wallet requests.safari/Resources/manifest.json: MV3 manifest.
-
Web UI (React/Vite)
web-ui/src/main.tsx: TypeScript content script entry point and Shadow DOM initialization.web-ui/src/bridge.ts: Lightweight bridge for fast EIP-1193 methods (accounts, chainId, blockNumber, chain switching).web-ui/src/App.tsx: React app component orchestrating modal flows and pending → confirm handshakes.web-ui/src/shadowHost.ts: Creates Shadow DOM host, injects Tailwind CSS, and manages portal routing.web-ui/src/components/RequestModal.tsx: Shared modal wrapper using shadcn/ui Credenza (responsive Dialog/Drawer).web-ui/src/components/Providers.tsx: React context providers (React Query client).web-ui/src/components/*Modal.tsx: Individual modal components for each wallet flow (Connect, SignMessage, SignTypedData, SendTx).web-ui/src/components/ui/*: Generated shadcn/ui components (button, dialog, drawer, credenza, skeleton, scroll-box).web-ui/src/index.cssandweb-ui/src/shadow.css: Tailwind v4 styles and design tokens (inlined into shadow root).- Output directory is
safari/Resources/dist/with filecontent.iife.js.
The Activity View and SQLite-backed Activity Log (including schema, storage, and extension integration points) are specified in docs/ActivityLog.md.
Signature Logging: The Activity Log also captures message signatures (personal_sign, eth_signTypedData_v4, and SIWE via wallet_connect) alongside transactions. See docs/SignatureLogging.md for the signature logging implementation specification (Phases 1-4 complete).
- Xcode 15+
- iOS 17+ SDK
- Swift Package dependencies (resolved by Xcode):
- Web3.swift (
Web3,Web3PromiseKit) - PromiseKit
- Dawn Key Management
- Web3.swift (
- Bun (for web-ui tooling) —
curl -fsSL https://bun.sh/install | bash - Node.js 18+ (Bun provides faster builds and better TypeScript support for the web-ui)
-
Clone the repo and open
ios-wallet.xcodeprojin Xcode. -
Enable capabilities (both app and extension targets):
- App Groups: create/use an App Group and set it in code (default:
group.co.za.stephancill.stupid-wallet). - Keychain Sharing: required by Dawn Key Management.
- App Groups: create/use an App Group and set it in code (default:
-
Update code constants if you use a different App Group:
- In
ContentView.swift:appGroupId. - In
SafariWebExtensionHandler.swift:appGroupId. - In
shared/Constants.swift:Constants.accessGroup— set to your Keychain Access Group and make sure the same group is present in bothios-wallet/ios-wallet.entitlementsandsafari/safari.entitlementsunder Keychain Sharing.
- In
-
Web UI setup (Vite + Tailwind v4 + shadcn/ui):
cd web-ui bun install # dev playground for testing modal components (optional) bun run dev # build the content script bundle to safari/Resources/dist/content.iife.js bun run build
Development workflow:
- The dev server runs on port 5173 and provides a playground at
index.htmlfor testing modal components independently - Build output is automatically placed in
safari/Resources/dist/content.iife.js - Xcode build process includes a Run Script phase that runs
bun run buildautomatically - Files under
safari/Resources/**are bundled into the Safari extension - UI components follow shadcn/ui conventions (configured in
components.jsonwith "new-york" style and CSS variables)
- The dev server runs on port 5173 and provides a playground at
- From Terminal (simulator build):
cd ios-wallet
set -o pipefail
xcodebuild -scheme ios-wallet -configuration Debug -destination 'generic/platform=iOS Simulator' build | xcpretty- From Xcode:
- Select the
ios-walletscheme. - Choose an iOS Simulator and Run.
- To run the Safari extension, enable Safari Web Extensions in Settings (iOS Simulator) and activate the extension in Safari.
- The web UI is built automatically by an Xcode Run Script phase. If needed, you can still run
bun run buildmanually.
- Select the
-
EIP‑1193 methods
For Fast Methods (no user confirmation required):
- Add method name to
FAST_METHODSinweb-ui/src/lib/constants.ts - Add method case to fast methods switch in
safari/Resources/background.js - Implement handler in
SafariWebExtensionHandler.swift - Add method case to request switch in
safari/Resources/inject.js
For Confirmation-Required Methods (need user approval):
- Add method name to
UI_METHODSinweb-ui/src/lib/constants.ts - Add method case to confirmation-required switch in
safari/Resources/background.js - Create modal component in
web-ui/src/components/(e.g.,NewMethodModal.tsx) - Add method case to request switch in
safari/Resources/inject.js - Add method case to pending→confirm flow in
web-ui/src/App.tsx - Implement handler in
SafariWebExtensionHandler.swift - Update supported methods documentation
Implementation Notes:
- Fast methods return results immediately via native handler
- Confirmation methods first return
{ pending: true }, then handleWALLET_CONFIRMafter user approval - All methods support site metadata extraction for proper domain/URI handling
- Use
requestIdfor tracking pending requests across the confirmation flow - Native handlers should return
{ result }or{ error }responses - Request Tracking: Background script maintains a
pendingRequestsMap to store site metadata for confirmation-required methods, with automatic cleanup (5-minute timeout)
For Gas Estimation:
All gas estimation should use the centralized
GasEstimationUtil(located inshared/GasEstimationUtil.swift):estimateGasLimit()- Get gas limit with standard 20% buffer (or 1,500 gas minimum)fetchGasPrices()/getGasPrices()- Get current network gas prices (EIP-1559 style with fallback to legacy)applyEIP7702Overhead()- Add overhead for EIP-7702 authorization transactions (25k per auth + 21k base + 20k safety margin)calculateTotalCost()- Calculate total transaction cost (gas + value) with formatted ETH strings
Never duplicate gas estimation logic. All transaction flows (
eth_sendTransaction,wallet_sendCalls, EIP-7702 authorizations) use these utilities to ensure consistency. The utility returnsSwift.Result<T, Error>for proper error handling and uses synchronous bridging viaawaitPromise()internally for PromiseKit compatibility. - Add method name to
-
Balances / Networks
- Add network RPC URL and call
web3.eth.getBalanceinContentView.swift. - Keep UI responsive; prefer
Taskand async/await bridging for PromiseKit results.
- Add network RPC URL and call
-
Key Management
- Use Dawn Wallet Key Management for any operations involving private key access.
- Ensure any new signing flows request consent and never expose the raw private key to the page.
-
Web UI / Modals
- Modals are React components using shadcn/ui Credenza (responsive Dialog/Drawer) and render inside a Shadow DOM.
- Edit
web-ui/src/components/RequestModal.tsx(shared wrapper) and specific modal components; ensure to keeponOpenChangerejecting on dismiss. - Shadow DOM styling is isolated; Tailwind v4 tokens are provided via CSS variables injected in
shadowHost.ts.
-
Site Metadata Extraction
- Background script automatically extracts site metadata (domain, URI, scheme) from sender information
- Metadata is passed through the request chain for SIWE message generation and security validation
- Supports various sender types: tabs, frames, direct URLs with fallback handling
- Metadata extraction prioritizes: message data → request attachments → userInfo → fallback
- Do not inject privileged APIs into the page; use postMessage bridges.
- Freeze provider detail objects when announcing via EIP-6963.
- Request Flow Patterns:
- Fast Methods: Direct native handler execution (accounts, chain info, chain switching, disconnect)
- Confirmation Methods: Pending → user approval → native handler execution (connect, signing, transactions)
- Supported today:
eth_requestAccounts,eth_accounts,eth_chainId,eth_blockNumber,eth_getTransactionByHash,eth_getTransactionReceipt,eth_getBlockByNumber,wallet_addEthereumChain,wallet_switchEthereumChain,wallet_connect,wallet_disconnect,personal_sign,eth_signTypedData_v4,eth_sendTransaction. - All methods support automatic site metadata extraction (domain, URI, scheme) for proper SIWE message generation.
- Never log sensitive data (private keys, seeds, decrypted material).
-
Swift
- Prefer clear naming and explicit types on public APIs.
- Use guard/early returns and avoid deep nesting.
- Keep UI code simple and state-driven with
@StateObject/@Published.
-
Logging
- Safari Extension Logging: Use
Loggerwith subsystem and category for structured logginglet logger = Logger(subsystem: "co.za.stephancill.stupid-wallet", category: "SafariWebExtensionHandler")- Use
privacy: .publicfor values that are safe to log:logger.info("Transaction hash: \(txHash, privacy: .public)") - Never log sensitive data: private keys, seeds, decrypted material, or personal information
- Viewing Logs: To see Safari extension logs in Xcode:
- Run the app in Simulator
- In Xcode: Debug > Attach to Process > Safari
- Open Safari in the Simulator and navigate to a site that uses the extension
- Console logs will appear in Xcode's debug console
- Safari Extension Logging: Use
-
JavaScript
- Keep provider implementation minimal and standards-compliant.
- Avoid global pollution; encapsulate in IIFE.
- Use strict mode and avoid deprecated APIs (prefer
requestoversend).
- Issues: Open an issue describing the problem or proposal before large changes.
- Branches: Use feature branches (e.g.,
feat/eip1193-sign,fix/accounts-timeout). - Commits: Keep commits small and descriptive. Reference issues if applicable.
- PRs: Provide a concise description, screenshots/logs if UI/behavior changes. Note any security implications.
- Checks: Ensure the app builds for iOS Simulator and the extension loads without console errors.
- If simulator build fails due to provisioning: ensure you selected an iOS Simulator destination and not a device.
- If balances don’t load: verify RPC endpoints and network connectivity.
- If the provider doesn’t appear in a DApp: check the console logs in the page, content script, and background.
- If modals render unstyled: make sure you rebuilt
web-uiand that the Shadow DOM variables are injected (seeshadowHost.ts). - If you see
ReferenceError: processfrom third‑party code in the content script, the Vite config definesprocess.env/globalshims; ensure you’re using the repo’sweb-ui/vite.config.ts. - If Xcode shows script sandbox denials when building the web‑ui: either disable
ENABLE_USER_SCRIPT_SANDBOXINGfor thesafaritarget, or add proper Input/Output file lists to the Run Script phase.
Thanks again for contributing!