Skip to content
This repository was archived by the owner on Aug 28, 2026. It is now read-only.

Latest commit

 

History

History
289 lines (220 loc) · 17 KB

File metadata and controls

289 lines (220 loc) · 17 KB

Contributing to stupid wallet

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.

Architecture Overview

  • App (SwiftUI)

    • UI: ios-wallet/ContentView.swift implements 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 UserDefaults for 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 optional rpcUrls)
      • connectedSites (dictionary keyed by domain hostname; values contain { address?, connectedAt } used to persist per‑domain dApp connection state and enable auto‑connect)
  • Safari Web Extension (safari/)

    • Injected provider (main world): safari/Resources/inject.js
      • EIP-1193 provider with supported methods:
        • eth_requestAccounts, eth_accounts
        • eth_chainId, eth_blockNumber
        • eth_getTransactionByHash, eth_getTransactionReceipt, eth_getBlockByNumber
        • wallet_addEthereumChain, wallet_switchEthereumChain
        • personal_sign, eth_signTypedData_v4
        • eth_sendTransaction
      • Emits accountsChanged and chainChanged where applicable.
      • EIP-6963 provider discovery: announces via eip6963:announceProvider and responds to eip6963:requestProvider.
      • Communicates with the extension via window.postMessage to avoid restricted APIs in the main world.
    • Content script (isolated world): built bundle at safari/Resources/dist/content.iife.js
      • Source lives in web-ui/src/main.tsx and is bundled via Vite.
      • Bridges between the injected provider and the background service worker using web-ui/src/bridge.ts for fast methods and web-ui/src/App.tsx for 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)
    • 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 under customChains), wallet_switchEthereumChain (updates chainId).
        • 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.

Data Flow

  • 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:
    1. Injected provider posts a message to the window (stupid-wallet-inject).
    2. Content script relays to background via browser.runtime.sendMessage.
    3. Background queries native/handler (or shared storage) and returns the result; for consented flows it first replies { pending: true }.
    4. On { pending: true }, the content script displays a modal and then sends a WALLET_CONFIRM to background; background finalizes by calling native and returns { result } or { error }.
    5. Content script posts the final response back to the injected provider, which resolves the original request.

Connected State and Auto‑connect

  • Storage key and semantics

    • connectedSites: App Group UserDefaults dictionary 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 in connectedSites, short‑circuit (no modal) and return the account(s).
    • wallet_connect: if params[0].capabilities is absent or an empty object and the domain exists in connectedSites, 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.
  • Disconnect and clearing

    • wallet_disconnect removes the domain from connectedSites.
    • Clearing the wallet in the app also removes connectedSites, revoking auto‑connect for all domains.

Code Layout

  • 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.css and web-ui/src/shadow.css: Tailwind v4 styles and design tokens (inlined into shadow root).
    • Output directory is safari/Resources/dist/ with file content.iife.js.

Activity Log

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).

Prerequisites

  • Xcode 15+
  • iOS 17+ SDK
  • Swift Package dependencies (resolved by Xcode):
    • Web3.swift (Web3, Web3PromiseKit)
    • PromiseKit
    • Dawn Key Management
  • 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)

Local Setup

  • Clone the repo and open ios-wallet.xcodeproj in 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.
  • 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 both ios-wallet/ios-wallet.entitlements and safari/safari.entitlements under Keychain Sharing.
  • 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.html for 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 build automatically
    • Files under safari/Resources/** are bundled into the Safari extension
    • UI components follow shadcn/ui conventions (configured in components.json with "new-york" style and CSS variables)

Build and Run

  • 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-wallet scheme.
    • 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 build manually.

Adding Features

  • EIP‑1193 methods

    For Fast Methods (no user confirmation required):

    • Add method name to FAST_METHODS in web-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_METHODS in web-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 handle WALLET_CONFIRM after user approval
    • All methods support site metadata extraction for proper domain/URI handling
    • Use requestId for tracking pending requests across the confirmation flow
    • Native handlers should return { result } or { error } responses
    • Request Tracking: Background script maintains a pendingRequests Map 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 in shared/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 returns Swift.Result<T, Error> for proper error handling and uses synchronous bridging via awaitPromise() internally for PromiseKit compatibility.

  • Balances / Networks

    • Add network RPC URL and call web3.eth.getBalance in ContentView.swift.
    • Keep UI responsive; prefer Task and async/await bridging for PromiseKit results.
  • 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 keep onOpenChange rejecting 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

Security Considerations

  • 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).

Style Guidelines

  • 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 Logger with subsystem and category for structured logging
      • let logger = Logger(subsystem: "co.za.stephancill.stupid-wallet", category: "SafariWebExtensionHandler")
      • Use privacy: .public for 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
  • JavaScript

    • Keep provider implementation minimal and standards-compliant.
    • Avoid global pollution; encapsulate in IIFE.
    • Use strict mode and avoid deprecated APIs (prefer request over send).

Submitting Changes

  • 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.

Troubleshooting

  • 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-ui and that the Shadow DOM variables are injected (see shadowHost.ts).
  • If you see ReferenceError: process from third‑party code in the content script, the Vite config defines process.env/global shims; ensure you’re using the repo’s web-ui/vite.config.ts.
  • If Xcode shows script sandbox denials when building the web‑ui: either disable ENABLE_USER_SCRIPT_SANDBOXING for the safari target, or add proper Input/Output file lists to the Run Script phase.

Thanks again for contributing!