From 2783def1391bc0f03e162fddcf7f1f7e2ed4a852 Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Fri, 2 Jan 2026 17:07:16 +0100 Subject: [PATCH] backlog: Add document doc-001 --- ... - Web3-Wallet-Integration-Architecture.md | 665 ++++++++++++++++++ backlog/tasks/task-007 - web3-integration.md | 21 - 2 files changed, 665 insertions(+), 21 deletions(-) create mode 100644 backlog/docs/doc-001 - Web3-Wallet-Integration-Architecture.md delete mode 100644 backlog/tasks/task-007 - web3-integration.md diff --git a/backlog/docs/doc-001 - Web3-Wallet-Integration-Architecture.md b/backlog/docs/doc-001 - Web3-Wallet-Integration-Architecture.md new file mode 100644 index 0000000..700dbd4 --- /dev/null +++ b/backlog/docs/doc-001 - Web3-Wallet-Integration-Architecture.md @@ -0,0 +1,665 @@ +--- +id: doc-001 +title: Web3 Wallet Integration Architecture +type: other +created_date: '2026-01-02 16:07' +--- +# Web3 Wallet Integration Architecture + +**Status:** Planning +**Created:** 2026-01-02 +**Related Task:** task-007 + +--- + +## 1. Overview + +This document outlines the architecture for integrating Web3 wallet capabilities into the canvas-website, enabling CryptID users to link Ethereum wallets for on-chain transactions, voting, and token-gated features. + +### Key Constraint: Cryptographic Curve Mismatch + +| System | Curve | Usage | +|--------|-------|-------| +| **CryptID (WebCrypto)** | ECDSA P-256 (NIST) | Authentication, passwordless login | +| **Ethereum** | ECDSA secp256k1 | Transactions, message signing | + +These curves are **incompatible**. A CryptID key cannot sign Ethereum transactions. Therefore, we use a **wallet linking** approach where: +1. CryptID handles authentication (who you are) +2. Linked wallet handles on-chain actions (what you can do) + +--- + +## 2. Database Schema + +### Migration: `002_linked_wallets.sql` + +```sql +-- Migration: Add Linked Wallets for Web3 Integration +-- Date: 2026-01-02 +-- Description: Enables CryptID users to link Ethereum wallets for +-- on-chain transactions, voting, and token-gated features. + +-- ============================================================================= +-- LINKED WALLETS TABLE +-- ============================================================================= +-- Each CryptID user can link multiple Ethereum wallets (EOA, Safe, hardware) +-- Linking requires signature verification to prove wallet ownership + +CREATE TABLE IF NOT EXISTS linked_wallets ( + id TEXT PRIMARY KEY, -- UUID for the link record + user_id TEXT NOT NULL, -- References users.id (CryptID account) + wallet_address TEXT NOT NULL, -- Ethereum address (checksummed, 0x-prefixed) + + -- Wallet metadata + wallet_type TEXT DEFAULT 'eoa' CHECK (wallet_type IN ('eoa', 'safe', 'hardware', 'contract')), + chain_id INTEGER DEFAULT 1, -- Primary chain (1 = Ethereum mainnet) + label TEXT, -- User-provided label (e.g., "Main Wallet") + + -- Verification proof + signature_message TEXT NOT NULL, -- The message that was signed + signature TEXT NOT NULL, -- EIP-191 personal_sign signature + verified_at TEXT NOT NULL, -- When signature was verified + + -- ENS integration + ens_name TEXT, -- Resolved ENS name (if any) + ens_avatar TEXT, -- ENS avatar URL (if any) + ens_resolved_at TEXT, -- When ENS was last resolved + + -- Flags + is_primary INTEGER DEFAULT 0, -- 1 = primary wallet for this user + is_active INTEGER DEFAULT 1, -- 0 = soft-deleted + + -- Timestamps + created_at TEXT DEFAULT (datetime('now')), + updated_at TEXT DEFAULT (datetime('now')), + last_used_at TEXT, -- Last time wallet was used for action + + -- Constraints + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + UNIQUE(user_id, wallet_address) -- Can't link same wallet twice +); + +-- Indexes for efficient lookups +CREATE INDEX IF NOT EXISTS idx_linked_wallets_user ON linked_wallets(user_id); +CREATE INDEX IF NOT EXISTS idx_linked_wallets_address ON linked_wallets(wallet_address); +CREATE INDEX IF NOT EXISTS idx_linked_wallets_active ON linked_wallets(is_active); +CREATE INDEX IF NOT EXISTS idx_linked_wallets_primary ON linked_wallets(user_id, is_primary); + +-- ============================================================================= +-- WALLET LINKING TOKENS TABLE (for Safe/multisig delayed verification) +-- ============================================================================= +-- For contract wallets that require on-chain signature verification + +CREATE TABLE IF NOT EXISTS wallet_link_tokens ( + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL, + wallet_address TEXT NOT NULL, + nonce TEXT NOT NULL, -- Random nonce for signature message + token TEXT NOT NULL UNIQUE, -- Secret token for verification callback + expires_at TEXT NOT NULL, + used INTEGER DEFAULT 0, + created_at TEXT DEFAULT (datetime('now')), + + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE +); + +CREATE INDEX IF NOT EXISTS idx_wallet_link_tokens_token ON wallet_link_tokens(token); + +-- ============================================================================= +-- TOKEN BALANCES CACHE (optional, for token-gating) +-- ============================================================================= +-- Cache of token balances for faster permission checks + +CREATE TABLE IF NOT EXISTS wallet_token_balances ( + id TEXT PRIMARY KEY, + wallet_address TEXT NOT NULL, + token_address TEXT NOT NULL, -- ERC-20/721/1155 contract address + token_type TEXT CHECK (token_type IN ('erc20', 'erc721', 'erc1155')), + chain_id INTEGER NOT NULL, + balance TEXT NOT NULL, -- String to handle big numbers + last_updated TEXT DEFAULT (datetime('now')), + + UNIQUE(wallet_address, token_address, chain_id) +); + +CREATE INDEX IF NOT EXISTS idx_token_balances_wallet ON wallet_token_balances(wallet_address); +CREATE INDEX IF NOT EXISTS idx_token_balances_token ON wallet_token_balances(token_address); +``` + +### TypeScript Types + +Add to `worker/types.ts`: + +```typescript +// ============================================================================= +// Linked Wallet Types +// ============================================================================= + +export type WalletType = 'eoa' | 'safe' | 'hardware' | 'contract'; + +export interface LinkedWallet { + id: string; + user_id: string; + wallet_address: string; + wallet_type: WalletType; + chain_id: number; + label: string | null; + signature_message: string; + signature: string; + verified_at: string; + ens_name: string | null; + ens_avatar: string | null; + ens_resolved_at: string | null; + is_primary: number; // SQLite boolean + is_active: number; // SQLite boolean + created_at: string; + updated_at: string; + last_used_at: string | null; +} + +export interface WalletLinkToken { + id: string; + user_id: string; + wallet_address: string; + nonce: string; + token: string; + expires_at: string; + used: number; + created_at: string; +} + +export interface WalletTokenBalance { + id: string; + wallet_address: string; + token_address: string; + token_type: 'erc20' | 'erc721' | 'erc1155'; + chain_id: number; + balance: string; + last_updated: string; +} + +// API Response types +export interface LinkedWalletResponse { + id: string; + address: string; + type: WalletType; + chainId: number; + label: string | null; + ensName: string | null; + ensAvatar: string | null; + isPrimary: boolean; + linkedAt: string; + lastUsedAt: string | null; +} + +export interface WalletLinkRequest { + walletAddress: string; + signature: string; + message: string; + walletType?: WalletType; + chainId?: number; + label?: string; +} +``` + +--- + +## 3. API Endpoints + +### Base Path: `/api/wallet` + +All endpoints require CryptID authentication via `X-CryptID-PublicKey` header. + +--- + +### `POST /api/wallet/link` + +Link a new wallet to the authenticated CryptID account. + +**Request:** +```typescript +{ + walletAddress: string; // 0x-prefixed Ethereum address + signature: string; // EIP-191 signature of the message + message: string; // Must match server-generated format + walletType?: 'eoa' | 'safe' | 'hardware' | 'contract'; + chainId?: number; // Default: 1 (mainnet) + label?: string; // Optional user label +} +``` + +**Message Format (must be signed):** +``` +Link wallet to CryptID + +Account: ${cryptidUsername} +Wallet: ${walletAddress} +Timestamp: ${isoTimestamp} +Nonce: ${randomNonce} + +This signature proves you own this wallet. +``` + +**Response (201 Created):** +```typescript +{ + success: true; + wallet: LinkedWalletResponse; +} +``` + +**Errors:** +- `400` - Invalid request body or signature +- `401` - Not authenticated +- `409` - Wallet already linked to this account +- `422` - Signature verification failed + +--- + +### `GET /api/wallet/list` + +Get all wallets linked to the authenticated user. + +**Response:** +```typescript +{ + wallets: LinkedWalletResponse[]; + count: number; +} +``` + +--- + +### `GET /api/wallet/:address` + +Get details for a specific linked wallet. + +**Response:** +```typescript +{ + wallet: LinkedWalletResponse; +} +``` + +--- + +### `PATCH /api/wallet/:address` + +Update a linked wallet (label, primary status). + +**Request:** +```typescript +{ + label?: string; + isPrimary?: boolean; +} +``` + +**Response:** +```typescript +{ + success: true; + wallet: LinkedWalletResponse; +} +``` + +--- + +### `DELETE /api/wallet/:address` + +Unlink a wallet from the account. + +**Response:** +```typescript +{ + success: true; + message: 'Wallet unlinked'; +} +``` + +--- + +### `GET /api/wallet/verify/:address` + +Check if a wallet address is linked to any CryptID account. +(Public endpoint - no auth required) + +**Response:** +```typescript +{ + linked: boolean; + cryptidUsername?: string; // Only if user allows public display +} +``` + +--- + +### `POST /api/wallet/refresh-ens` + +Refresh ENS name resolution for a linked wallet. + +**Request:** +```typescript +{ + walletAddress: string; +} +``` + +**Response:** +```typescript +{ + ensName: string | null; + ensAvatar: string | null; + resolvedAt: string; +} +``` + +--- + +## 4. Signature Verification Implementation + +```typescript +// worker/walletAuth.ts + +import { verifyMessage, getAddress } from 'viem'; + +export function generateLinkMessage( + username: string, + address: string, + timestamp: string, + nonce: string +): string { + return `Link wallet to CryptID + +Account: ${username} +Wallet: ${address} +Timestamp: ${timestamp} +Nonce: ${nonce} + +This signature proves you own this wallet.`; +} + +export async function verifyWalletSignature( + address: string, + message: string, + signature: `0x${string}` +): Promise { + try { + // Normalize address + const checksumAddress = getAddress(address); + + // Verify EIP-191 personal_sign signature + const valid = await verifyMessage({ + address: checksumAddress, + message, + signature, + }); + + return valid; + } catch (error) { + console.error('Signature verification error:', error); + return false; + } +} + +// For ERC-1271 contract wallet verification (Safe, etc.) +export async function verifyContractSignature( + address: string, + message: string, + signature: string, + rpcUrl: string +): Promise { + // ERC-1271 magic value: 0x1626ba7e + // Implementation needed for Safe/contract wallet support + // Uses eth_call to isValidSignature(bytes32,bytes) + throw new Error('Contract signature verification not yet implemented'); +} +``` + +--- + +## 5. Library Comparison + +### Recommendation: **wagmi v2 + viem** + +| Library | Bundle Size | Type Safety | React Hooks | Maintenance | Recommendation | +|---------|-------------|-------------|-------------|-------------|----------------| +| **wagmi v2** | ~40KB | Excellent | Native | Active (wevm team) | ✅ **Best for React** | +| **viem** | ~25KB | Excellent | N/A | Active (wevm team) | ✅ **Best for worker** | +| **ethers v6** | ~120KB | Good | None | Active | ⚠️ Larger bundle | +| **web3.js** | ~400KB | Poor | None | Declining | ❌ Avoid | + +### Why wagmi + viem? + +1. **Same team** - wagmi and viem are both from wevm, designed to work together +2. **Tree-shakeable** - Only import what you use +3. **TypeScript-first** - Excellent type inference and autocomplete +4. **Modern React** - Hooks-based, works with React 18+ and Suspense +5. **WalletConnect v2** - Built-in support via Web3Modal +6. **No ethers dependency** - Pure viem underneath + +### Package Configuration + +```json +{ + "dependencies": { + "wagmi": "^2.12.0", + "viem": "^2.19.0", + "@tanstack/react-query": "^5.45.0", + "@web3modal/wagmi": "^5.0.0" + } +} +``` + +### Supported Wallets (via Web3Modal) + +- MetaMask (injected) +- WalletConnect v2 (mobile wallets) +- Coinbase Wallet +- Rainbow +- Safe (via WalletConnect) +- Hardware wallets (via MetaMask bridge) + +--- + +## 6. Frontend Architecture + +### Provider Setup (`src/providers/Web3Provider.tsx`) + +```typescript +import { WagmiProvider, createConfig, http } from 'wagmi'; +import { mainnet, optimism, arbitrum, base } from 'wagmi/chains'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { createWeb3Modal } from '@web3modal/wagmi/react'; + +// Configure chains +const chains = [mainnet, optimism, arbitrum, base] as const; + +// Create wagmi config +const config = createConfig({ + chains, + transports: { + [mainnet.id]: http(), + [optimism.id]: http(), + [arbitrum.id]: http(), + [base.id]: http(), + }, +}); + +// Create Web3Modal +const projectId = process.env.WALLETCONNECT_PROJECT_ID!; + +createWeb3Modal({ + wagmiConfig: config, + projectId, + chains, + themeMode: 'dark', +}); + +const queryClient = new QueryClient(); + +export function Web3Provider({ children }: { children: React.ReactNode }) { + return ( + + + {children} + + + ); +} +``` + +### Wallet Link Hook (`src/hooks/useWalletLink.ts`) + +```typescript +import { useAccount, useSignMessage, useDisconnect } from 'wagmi'; +import { useAuth } from '../context/AuthContext'; +import { useState } from 'react'; + +export function useWalletLink() { + const { address, isConnected } = useAccount(); + const { signMessageAsync } = useSignMessage(); + const { disconnect } = useDisconnect(); + const { session } = useAuth(); + const [isLinking, setIsLinking] = useState(false); + + const linkWallet = async (label?: string) => { + if (!address || !session.username) return; + + setIsLinking(true); + try { + // Generate link message + const timestamp = new Date().toISOString(); + const nonce = crypto.randomUUID(); + const message = generateLinkMessage( + session.username, + address, + timestamp, + nonce + ); + + // Request signature from wallet + const signature = await signMessageAsync({ message }); + + // Send to backend for verification + const response = await fetch('/api/wallet/link', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CryptID-PublicKey': session.publicKey, + }, + body: JSON.stringify({ + walletAddress: address, + signature, + message, + label, + }), + }); + + if (!response.ok) { + throw new Error('Failed to link wallet'); + } + + return await response.json(); + } finally { + setIsLinking(false); + } + }; + + return { + address, + isConnected, + isLinking, + linkWallet, + disconnect, + }; +} +``` + +--- + +## 7. Integration Points + +### A. AuthContext Extension + +Add to `Session` type: +```typescript +interface Session { + // ... existing fields + linkedWallets?: LinkedWalletResponse[]; + primaryWallet?: LinkedWalletResponse; +} +``` + +### B. Token-Gated Features + +```typescript +// Check if user holds specific tokens +async function checkTokenGate( + walletAddress: string, + requirement: { + tokenAddress: string; + minBalance: string; + chainId: number; + } +): Promise { + // Query on-chain balance or use cached value +} +``` + +### C. Snapshot Voting (Future) + +```typescript +// Vote on Snapshot proposal +async function voteOnProposal( + space: string, + proposal: string, + choice: number, + walletAddress: string +): Promise { + // Use Snapshot.js SDK with linked wallet +} +``` + +--- + +## 8. Security Considerations + +1. **Signature Replay Prevention** + - Include timestamp and nonce in message + - Server validates timestamp is recent (within 5 minutes) + - Nonces are single-use + +2. **Address Validation** + - Always checksum addresses before storing/comparing + - Validate address format (0x + 40 hex chars) + +3. **Rate Limiting** + - Limit link attempts per user (e.g., 5/hour) + - Limit total wallets per user (e.g., 10) + +4. **Wallet Verification** + - EOA: EIP-191 personal_sign + - Safe: ERC-1271 isValidSignature + - Hardware: Same as EOA (via MetaMask bridge) + +--- + +## 9. Next Steps + +1. **Phase 1 (This Sprint)** + - [ ] Add migration file + - [ ] Install wagmi/viem dependencies + - [ ] Implement link/list/unlink endpoints + - [ ] Create WalletLinkPanel UI + - [ ] Add wallet section to settings + +2. **Phase 2 (Next Sprint)** + - [ ] Snapshot.js integration + - [ ] VotingShape for canvas + - [ ] Token balance caching + +3. **Phase 3 (Future)** + - [ ] Safe SDK integration + - [ ] TransactionBuilderShape + - [ ] Account Abstraction exploration diff --git a/backlog/tasks/task-007 - web3-integration.md b/backlog/tasks/task-007 - web3-integration.md deleted file mode 100644 index 0df128e..0000000 --- a/backlog/tasks/task-007 - web3-integration.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -id: task-007 -title: Web3 Integration -status: To Do -assignee: [] -created_date: '2025-12-03' -labels: [feature, web3, blockchain] -priority: low -branch: web3-integration ---- - -## Description -Integrate Web3 capabilities for blockchain-based features (wallet connect, NFT canvas elements, etc.). - -## Branch Info -- **Branch**: `web3-integration` - -## Acceptance Criteria -- [ ] Add wallet connection -- [ ] Enable NFT minting of canvas elements -- [ ] Blockchain-based ownership verification