OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-wallet / wallet-server.cjs

2072 lines · javascript ref:
#!/usr/bin/env bare

// wallet-server.cjs - HTTP Server for Hyper Wallet Web UI
// Runs in Bare 1.26.0 (no Pear version conflict!)

require('bare-node-runtime/global')
// WORKAROUND: Pear doesn't support import attributes with ESM yet
// Manually load imports.json into bundle for Bare 1.24/1.26 compatibility
require('bare-node-runtime/imports')

const http = require('bare-http1')
const fs = require('bare-fs')
const path = require('bare-path')
const os = require('bare-os')

// Check if running under Pear (legacy path)
const isPear = typeof Pear !== 'undefined'

const {
  initializeStorage,
  getTransactions,
  getTransaction,
  storeTransaction,
  closeStorage,
  createInvite,
  joinWallet,
  listDevices,
  removeDevice,
  deduplicateDevices,
  waitForNewDevice,
  enableReplication,
  // Contact management
  addContact,
  getContacts,
  findContact,
  getContactById,
  updateContact,
  deleteContact,
  findContactByAddress,
  addAddressToContact,
  removeAddressFromContact,
  // Transaction syncing
  syncTransactionsFromWDK
} = require('./lib/hyper-wallet-lib/wallet-storage.cjs')

const IdentityKey = require('keet-identity-key')
const b4a = require('b4a')

const {
  loadWalletMnemonic,
  generateWalletMnemonic,
  saveWalletMnemonic,
  initializeWDK
} = require('./lib/hyper-wallet-lib/wdk-integration.cjs')

const { loadConfig } = require('./lib/hyper-wallet-lib/config-loader.cjs')
const { getConfig, updateConfig, saveConfig, DEFAULT_CONFIG } = require('./lib/hyper-wallet-lib/config.cjs')
const { getAvailableProviders, getProviderInfo } = require('./lib/hyper-wallet-lib/blockchain-providers.cjs')
const {
  generateToken,
  saveToken,
  loadToken,
  verifyToken,
  tokenExists
} = require('./lib/hyper-wallet-lib/auth.cjs')

const {
  encrypt,
  decrypt,
  isValidFormat
} = require('./lib/hyper-wallet-lib/encryption.cjs')

const {
  checkRateLimit,
  getRateLimitStatus
} = require('./lib/hyper-wallet-lib/rate-limiter.cjs')

const {
  validateAddress
} = require('./lib/hyper-wallet-lib/address-validator.cjs')

const {
  validateAmount,
  validateSufficientBalance,
  formatAmount
} = require('./lib/hyper-wallet-lib/amount-validator.cjs')

const {
  sanitizeContactName,
  sanitizeDeviceName,
  sanitizeNotes,
  validateChain,
  validateNetwork,
  validateInviteCode
} = require('./lib/hyper-wallet-lib/input-sanitizer.cjs')

// Load configuration from file
const config = loadConfig()

// Polyfill TextEncoder for QR code generation
if (typeof global.TextEncoder === 'undefined') {
  const { TextEncoder, TextDecoder } = require('bare-encoding')
  global.TextEncoder = TextEncoder
  global.TextDecoder = TextDecoder
}

const QRCodeCore = require('qrcode/lib/core/qrcode')
const SvgRenderer = require('qrcode/lib/renderer/svg')

// Storage configuration
const STORAGE_DIR = path.join(os.homedir(), '.hyper-wallet')
const MNEMONIC_FILE = path.join(STORAGE_DIR, '.mnemonic')

// Password caching (in-memory only, cleared on server restart)
// Security: Passwords never persisted to disk
let cachedPassword = null

function setCachedPassword(password) {
  cachedPassword = password
  console.log('✅ Password cached in memory (cleared on server restart)')
}

function getCachedPassword() {
  return cachedPassword
}

function clearCachedPassword() {
  cachedPassword = null
  console.log('🔒 Password cleared from memory')
}

// Helper: Load identity mnemonic using cached password (for internal operations)
function loadIdentityMnemonicCached() {
  const password = getCachedPassword()
  if (!password) {
    // Check if file exists and is plaintext (legacy) - can load without password
    if (fs.existsSync(MNEMONIC_FILE)) {
      const content = fs.readFileSync(MNEMONIC_FILE, 'utf8').trim()
      if (content.includes(' ') && content.split(' ').length === 24) {
        console.warn('⚠️  Loading plaintext identity mnemonic (legacy format)')
        return content
      }
    }
    throw new Error('Password not cached. Please restart wallet or re-initialize.')
  }
  return loadIdentityMnemonic(password)
}

// Helper: Load wallet mnemonic using cached password (for internal operations)
function loadWalletMnemonicCached() {
  const password = getCachedPassword()
  if (!password) {
    // Check if file exists and is plaintext (legacy) - can load without password
    const WALLET_FILE = path.join(STORAGE_DIR, '.wallet-mnemonic')
    if (fs.existsSync(WALLET_FILE)) {
      const content = fs.readFileSync(WALLET_FILE, 'utf8').trim()
      if (content.includes(' ') && content.split(' ').length === 12) {
        console.warn('⚠️  Loading plaintext wallet mnemonic (legacy format)')
        return content
      }
    }
    throw new Error('Password not cached. Please restart wallet or re-initialize.')
  }
  return loadWalletMnemonic(password)
}

// Load identity mnemonic (with password-based decryption)
function loadIdentityMnemonic(password) {
  if (!fs.existsSync(MNEMONIC_FILE)) {
    return null
  }

  const encrypted = fs.readFileSync(MNEMONIC_FILE, 'utf8').trim()

  // Check if this is plaintext (legacy format) - contains spaces and 24 words
  if (encrypted.includes(' ') && encrypted.split(' ').length === 24) {
    console.warn('⚠️  WARNING: Identity mnemonic is stored in plaintext (legacy format)')
    console.warn('⚠️  Please set a password to encrypt your mnemonics')
    return encrypted
  }

  // Decrypt with password
  if (!password) {
    throw new Error('Password required to decrypt identity mnemonic')
  }

  try {
    return decrypt(encrypted, password)
  } catch (err) {
    throw new Error('Invalid password or corrupted mnemonic data')
  }
}

// Save identity mnemonic (with password-based encryption)
function saveIdentityMnemonic(mnemonic, password) {
  if (!fs.existsSync(STORAGE_DIR)) {
    fs.mkdirSync(STORAGE_DIR, { recursive: true, mode: 0o700 })
  }

  // Require password for new mnemonics
  if (!password || password.length < 8) {
    throw new Error('Password must be at least 8 characters')
  }

  // Encrypt mnemonic before saving
  const encrypted = encrypt(mnemonic, password)
  fs.writeFileSync(MNEMONIC_FILE, encrypted, { mode: 0o600 })
}

// Generate identity mnemonic (24 words)
function generateIdentityMnemonic() {
  return IdentityKey.generateMnemonic()
}

// Load identity from mnemonic
async function loadIdentity() {
  const mnemonic = loadIdentityMnemonicCached()
  if (!mnemonic) {
    throw new Error('Identity not initialized')
  }
  return await IdentityKey.from({ mnemonic })
}

const PORT = parseInt(process.env.PORT || config.server?.port || 4004)
const HOST = process.env.WALLET_HOST || config.server?.host || 'localhost'
// Use process.cwd() instead of __dirname for Pear compatibility
// Pear sets cwd to the app bundle directory, but __dirname is empty
const UI_DIR = path.join(__dirname, 'ui')

// Track pending device add sessions
// Map<sessionId, { inviteCode, status, deviceName, error }>
const pendingDeviceAdds = new Map()

// Helper to parse JSON body
async function parseBody(req) {
  return new Promise((resolve, reject) => {
    let body = ''
    req.on('data', chunk => { body += chunk })
    req.on('end', () => {
      try {
        resolve(body ? JSON.parse(body) : {})
      } catch (err) {
        reject(err)
      }
    })
    req.on('error', reject)
  })
}

// Helper to send JSON response
function sendJSON(res, data, status = 200) {
  res.statusCode = status
  res.setHeader('Content-Type', 'application/json')
  res.end(JSON.stringify(data))
}

// Helper to send error
function sendError(res, message, status = 500) {
  sendJSON(res, { error: message }, status)
}

/**
 * Authentication middleware
 * Wraps route handlers to require Bearer token authentication
 *
 * @param {function} handler - Route handler function
 * @returns {function} - Wrapped handler with auth check
 */
function requireAuth(handler) {
  return async (req, res, ...params) => {
    // Verify Bearer token
    if (!verifyToken(req)) {
      console.warn(`⚠️  Unauthorized access attempt: ${req.method} ${req.url}`)
      return sendError(res, 'Unauthorized - Valid Bearer token required', 401)
    }

    // Token valid - proceed to handler
    return handler(req, res, ...params)
  }
}

/**
 * Rate limiting middleware
 * Wraps route handlers to enforce rate limits per IP address
 *
 * @param {function} handler - Route handler function
 * @returns {function} - Wrapped handler with rate limit check
 */
function withRateLimit(handler) {
  return async (req, res, ...params) => {
    const result = checkRateLimit(req)

    // Set rate limit headers
    res.setHeader('X-RateLimit-Limit', String(result.limit))
    res.setHeader('X-RateLimit-Remaining', String(result.remaining))
    res.setHeader('X-RateLimit-Reset', String(result.resetIn))

    if (!result.allowed) {
      console.warn(`⚠️  Rate limit exceeded: ${req.method} ${req.url} (IP: ${req.socket.remoteAddress})`)
      return sendError(res, `Rate limit exceeded. Try again in ${result.resetIn} seconds.`, 429)
    }

    // Rate limit OK - proceed to handler
    return handler(req, res, ...params)
  }
}

// CORS Configuration
// Define allowed origins (whitelist)
const ALLOWED_ORIGINS = [
  'http://localhost:4004',
  'http://127.0.0.1:4004',
  'http://0.0.0.0:4004',
  'http://ashatron.local:4004',      // Network testing
  'http://tabertron.local:4004'      // Network testing
  // Add production origins here when deploying:
  // 'https://your-production-domain.com'
]

// pear:// CORS origin no longer needed — apps run locally

/**
 * Set CORS headers with origin whitelist
 * @param {ServerResponse} res - HTTP response
 * @param {IncomingMessage} req - HTTP request
 */
function setCORS(res, req) {
  const origin = req.headers.origin || req.headers.referer

  // Check if origin is in whitelist
  if (origin && ALLOWED_ORIGINS.some(allowed => origin.startsWith(allowed))) {
    res.setHeader('Access-Control-Allow-Origin', origin)
    res.setHeader('Access-Control-Allow-Credentials', 'true')
  } else if (!origin) {
    // No origin header (e.g., same-origin request or API client)
    // Allow localhost by default for development
    res.setHeader('Access-Control-Allow-Origin', ALLOWED_ORIGINS[0])
  } else {
    // Origin not in whitelist - log warning but still set a default
    console.warn(`⚠️  CORS: Blocked origin: ${origin}`)
    res.setHeader('Access-Control-Allow-Origin', ALLOWED_ORIGINS[0])
  }

  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, DELETE, PUT')
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization')
}

// API Routes
const routes = {
  // GET /api/status (public - health check)
  'GET /api/status': async (req, res) => {
    // Check if wallet files exist (don't try to decrypt - no password needed for status check)
    const identityFileExists = fs.existsSync(MNEMONIC_FILE)
    const walletFileExists = fs.existsSync(path.join(STORAGE_DIR, '.wallet-mnemonic'))
    const walletInitialized = identityFileExists && walletFileExists
    const tokenConfigured = tokenExists()

    sendJSON(res, {
      status: 'ok',
      version: '1.0.0-beta',
      initialized: walletInitialized,
      authEnabled: tokenConfigured,
      passwordCached: getCachedPassword() !== null
    })
  },

  // GET /api/balance/:chain (protected)
  'GET /api/balance/': requireAuth(async (req, res, chain) => {
    try {
      const walletMnemonic = loadWalletMnemonicCached()
      if (!walletMnemonic) {
        return sendError(res, 'Wallet not initialized', 400)
      }

      const network = chain.startsWith('testnet') ? 'testnet' : 'mainnet'
      const chainName = chain.replace(/^(testnet|mainnet)-/, '')

      const { wdk } = await initializeWDK(walletMnemonic, { testnet: network === 'testnet' })
      const account = await wdk.getAccount(chainName, 0)
      const balance = await account.getBalance()

      sendJSON(res, {
        chain: chainName,
        network,
        balance: balance.toString()
      })
    } catch (err) {
      console.error('Balance error:', err)
      sendError(res, err.message)
    }
  }),

  // GET /api/address/:chain
  'GET /api/address/': requireAuth(async (req, res, chain) => {
    try {
      const walletMnemonic = loadWalletMnemonicCached()
      if (!walletMnemonic) {
        return sendError(res, 'Wallet not initialized', 400)
      }

      const network = chain.startsWith('testnet') ? 'testnet' : 'mainnet'
      const chainName = chain.replace(/^(testnet|mainnet)-/, '')

      const { wdk } = await initializeWDK(walletMnemonic, { testnet: network === 'testnet' })
      const account = await wdk.getAccount(chainName, 0)
      const address = await account.getAddress()

      sendJSON(res, {
        chain: chainName,
        network,
        address
      })
    } catch (err) {
      console.error('Address error:', err)
      sendError(res, err.message)
    }
  }),

  // GET /api/qrcode/:chain
  'GET /api/qrcode/': requireAuth(async (req, res, chain) => {
    try {
      const walletMnemonic = loadWalletMnemonicCached()
      if (!walletMnemonic) {
        return sendError(res, 'Wallet not initialized', 400)
      }

      const network = chain.startsWith('testnet') ? 'testnet' : 'mainnet'
      const chainName = chain.replace(/^(testnet|mainnet)-/, '')

      const { wdk } = await initializeWDK(walletMnemonic, { testnet: network === 'testnet' })
      const account = await wdk.getAccount(chainName, 0)
      const address = await account.getAddress()

      // Generate QR code as SVG data URL (no fs dependency)
      const qrData = QRCodeCore.create(address, { errorCorrectionLevel: 'M' })
      const svg = await SvgRenderer.render(qrData, { width: 256, margin: 2 })
      const qrcode = `data:image/svg+xml;base64,${Buffer.from(svg).toString('base64')}`

      sendJSON(res, {
        chain: chainName,
        network,
        address,
        qrcode
      })
    } catch (err) {
      console.error('QR code error:', err)
      sendError(res, err.message)
    }
  }),

  // GET /api/transactions/:chain
  'GET /api/transactions/': requireAuth(async (req, res, chain) => {
    try {
      const identity = await loadIdentity()
      await initializeStorage(identity)

      const chainFilter = chain === 'all' ? 'all' : chain.replace(/^(testnet|mainnet)-/, '')
      const transactions = await getTransactions(chainFilter)

      // Note: Storage remains open (persistent daemon mode)

      sendJSON(res, {
        chain: chainFilter,
        count: transactions.length,
        transactions: transactions.map(tx => ({
          hash: tx.hash,
          chain: tx.chain,
          network: tx.network || 'testnet', // Add network field
          direction: tx.direction,
          from: tx.from,
          to: tx.to,
          amount: tx.amount.toString(),
          fee: tx.fee.toString(),
          status: tx.status,
          timestamp: tx.timestamp,
          blockHeight: tx.blockHeight
        }))
      })
    } catch (err) {
      console.error('Transactions error:', err)
      sendError(res, err.message)
    }
  }),

  // POST /api/transactions/sync - Sync transactions from blockchain
  'POST /api/transactions/sync': withRateLimit(requireAuth(async (req, res) => {
    try {
      const body = await parseBody(req)
      const { chain } = body

      if (!chain) {
        return sendError(res, 'Missing required field: chain', 400)
      }

      const identity = await loadIdentity()
      await initializeStorage(identity)

      // Initialize WDK
      const walletMnemonic = loadWalletMnemonicCached()
      if (!walletMnemonic) {
        return sendError(res, 'Wallet not initialized', 400)
      }

      const { initializeWDK } = require('./lib/hyper-wallet-lib/wdk-integration.cjs')
      const { wdk } = await initializeWDK(walletMnemonic, { testnet: true })

      // Sync transactions
      const chainName = chain.replace(/^(testnet|mainnet)-/, '')
      const result = await syncTransactionsFromWDK(wdk, {
        chains: [chainName],
        network: 'testnet'
      })

      sendJSON(res, {
        success: true,
        message: `Synced ${result.synced} new transactions`,
        result
      })
    } catch (err) {
      console.error('Sync transactions error:', err)
      sendError(res, err.message)
    }
  })),

  // GET /api/status
  // GET /api/whoami
  'GET /api/whoami': requireAuth(async (req, res) => {
    try {
      const identityMnemonic = loadIdentityMnemonicCached()
      if (!identityMnemonic) {
        return sendError(res, 'Identity not initialized', 400)
      }

      const identity = await loadIdentity()
      const publicKey = b4a.toString(identity.identityKeyPair.publicKey, 'hex')

      sendJSON(res, {
        publicKey,
        hostname: os.hostname(),
        platform: os.platform()
      })
    } catch (err) {
      console.error('WhoAmI error:', err)
      sendError(res, err.message)
    }
  }),

  // GET /api/sync-status
  'GET /api/sync-status': requireAuth(async (req, res) => {
    try {
      // For now, return mock data
      // TODO: Integrate with actual Autobase sync status
      sendJSON(res, {
        syncing: false,
        deviceCount: 1,
        lastSync: Date.now()
      })
    } catch (err) {
      sendError(res, err.message)
    }
  }),

  // POST /api/export-identity (changed to POST to accept password in body)
  'POST /api/export-identity': withRateLimit(requireAuth(async (req, res) => {
    try {
      const body = await parseBody(req)
      const { password } = body

      if (!password) {
        return sendError(res, 'Password required to decrypt identity mnemonic', 400)
      }

      const mnemonic = loadIdentityMnemonic(password)
      if (!mnemonic) {
        return sendError(res, 'Identity not initialized', 400)
      }

      sendJSON(res, {
        mnemonic,
        warning: 'CRITICAL: Keep this safe! This is for device attestation.'
      })
    } catch (err) {
      console.error('Export identity error:', err)
      // Provide user-friendly error for wrong password
      if (err.message.includes('Invalid password')) {
        return sendError(res, 'Invalid password', 401)
      }
      sendError(res, err.message)
    }
  })),

  // POST /api/export-wallet (changed to POST to accept password in body)
  'POST /api/export-wallet': withRateLimit(requireAuth(async (req, res) => {
    try {
      const body = await parseBody(req)
      const { password } = body

      if (!password) {
        return sendError(res, 'Password required to decrypt wallet mnemonic', 400)
      }

      const mnemonic = loadWalletMnemonic(password)
      if (!mnemonic) {
        return sendError(res, 'Wallet not initialized', 400)
      }

      sendJSON(res, {
        mnemonic,
        warning: 'CRITICAL: Keep this safe! This controls your funds.'
      })
    } catch (err) {
      console.error('Export wallet error:', err)
      // Provide user-friendly error for wrong password
      if (err.message.includes('Invalid password')) {
        return sendError(res, 'Invalid password', 401)
      }
      sendError(res, err.message)
    }
  })),

  // POST /api/reset
  'POST /api/reset': requireAuth(async (req, res) => {
    try {
      // Delete identity mnemonic
      if (fs.existsSync(MNEMONIC_FILE)) {
        fs.unlinkSync(MNEMONIC_FILE)
      }

      // Delete wallet mnemonic
      const walletMnemonicFile = path.join(STORAGE_DIR, '.wallet-mnemonic')
      if (fs.existsSync(walletMnemonicFile)) {
        fs.unlinkSync(walletMnemonicFile)
      }

      // Note: This doesn't delete Autobase/Hyperbee data
      // Users would need to manually delete ~/.hyper-wallet for full reset

      sendJSON(res, {
        success: true,
        message: 'Wallet reset complete. Please refresh the page.'
      })
    } catch (err) {
      console.error('Reset error:', err)
      sendError(res, err.message)
    }
  }),

  // POST /api/device/add - Create invite and start pairing
  'POST /api/device/add': withRateLimit(requireAuth(async (req, res) => {
    try {
      const identity = await loadIdentity()
      const identityMnemonic = loadIdentityMnemonicCached()
      const walletMnemonic = loadWalletMnemonicCached()

      if (!walletMnemonic) {
        return sendError(res, 'Wallet not initialized', 400)
      }

      await initializeStorage(identity)

      // Create invite code
      const inviteCode = await createInvite(identity)

      // Generate session ID
      const sessionId = b4a.toString(b4a.from(Math.random().toString()), 'hex').slice(0, 16)

      // Store pending session
      pendingDeviceAdds.set(sessionId, {
        inviteCode,
        status: 'waiting',
        deviceName: null,
        error: null
      })

      // Start waiting for device in background
      ;(async () => {
        try {
          const { writerKey, deviceName } = await waitForNewDevice(0, identityMnemonic, walletMnemonic)

          pendingDeviceAdds.set(sessionId, {
            inviteCode,
            status: 'completed',
            deviceName,
            writerKey: b4a.toString(writerKey, 'hex'),
            error: null
          })

          console.log(`✅ Device added: ${deviceName}`)
        } catch (err) {
          pendingDeviceAdds.set(sessionId, {
            inviteCode,
            status: 'error',
            deviceName: null,
            error: err.message
          })

          console.error('Device add error:', err)
        }
      })()

      // Return immediately with invite and session ID
      sendJSON(res, {
        sessionId,
        inviteCode,
        status: 'waiting',
        message: 'Share this invite code with your other device'
      })
    } catch (err) {
      console.error('Create invite error:', err)
      sendError(res, err.message)
    }
  })),

  // GET /api/device/add/:sessionId - Check pairing status
  'GET /api/device/add/': requireAuth(async (req, res, sessionId) => {
    const session = pendingDeviceAdds.get(sessionId)

    if (!session) {
      return sendError(res, 'Session not found', 404)
    }

    sendJSON(res, session)
  }),

  // POST /api/device/join - Join wallet using invite code
  'POST /api/device/join': withRateLimit(requireAuth(async (req, res) => {
    try {
      const body = await parseBody(req)
      let { inviteCode, deviceName } = body

      if (!inviteCode) {
        return sendError(res, 'Invite code required', 400)
      }

      if (!deviceName) {
        return sendError(res, 'Device name required', 400)
      }

      // Validate and sanitize inputs
      try {
        inviteCode = validateInviteCode(inviteCode)
        deviceName = sanitizeDeviceName(deviceName)
      } catch (err) {
        return sendError(res, err.message, 400)
      }

      // Join wallet (receives BOTH mnemonics from primary device)
      // This will also verify invite signature and expiration
      const { device, identityMnemonic, walletMnemonic } = await joinWallet(inviteCode, deviceName)

      // Save both mnemonics
      saveIdentityMnemonic(identityMnemonic)
      saveWalletMnemonic(walletMnemonic)

      sendJSON(res, {
        success: true,
        message: 'Joined wallet successfully',
        device: {
          name: device.name,
          writerKey: b4a.toString(device.key, 'hex').slice(0, 16) + '...'
        }
      })
    } catch (err) {
      console.error('Join wallet error:', err)
      sendError(res, err.message)
    }
  })),

  // GET /api/device/list - List all devices
  'GET /api/device/list': requireAuth(async (req, res) => {
    try {
      const identity = await loadIdentity()
      await initializeStorage(identity)

      const devices = await listDevices()

      sendJSON(res, {
        devices: devices.map(d => {
          const writerKey = b4a.toString(d.key, 'hex')
          const identityKey = d.identityKey ? b4a.toString(d.identityKey, 'hex') : null

          return {
            name: d.name,
            writerKey: writerKey,
            writerKeyShort: writerKey.slice(0, 16) + '...',
            identityKey: identityKey,
            identityKeyShort: identityKey ? identityKey.slice(0, 16) + '...' : null,
            addedAt: d.addedAt,
            isCurrentDevice: d.isCurrentDevice || d.isLocal || false
          }
        })
      })

      // Note: Storage remains open (persistent daemon mode)
    } catch (err) {
      console.error('List devices error:', err)
      sendError(res, err.message)
    }
  }),

  // POST /api/device/remove - Remove device
  'POST /api/device/remove': requireAuth(async (req, res) => {
    try {
      const body = await parseBody(req)
      const { deviceKey } = body

      if (!deviceKey) {
        return sendError(res, 'Device key required', 400)
      }

      const identity = await loadIdentity()
      await initializeStorage(identity)

      const keyBuffer = b4a.from(deviceKey, 'hex')
      await removeDevice(keyBuffer)

      sendJSON(res, {
        success: true,
        message: 'Device removed successfully'
      })

      // Note: Storage remains open (persistent daemon mode)
    } catch (err) {
      console.error('Remove device error:', err)
      sendError(res, err.message)
    }
  }),

  // POST /api/device/deduplicate - Remove duplicate device entries
  'POST /api/device/deduplicate': requireAuth(async (req, res) => {
    try {
      const result = deduplicateDevices()

      sendJSON(res, {
        success: true,
        removed: result.removed,
        total: result.total,
        message: `Removed ${result.removed} duplicate(s), ${result.total} device(s) remaining`
      })
    } catch (err) {
      console.error('Deduplicate error:', err)
      sendError(res, err.message)
    }
  }),

  // GET /api/contacts - Get all contacts
  'GET /api/contacts': requireAuth(async (req, res) => {
    try {
      const identity = await loadIdentity()
      await initializeStorage(identity)

      const contacts = await getContacts()

      sendJSON(res, {
        success: true,
        count: contacts.length,
        contacts
      })
    } catch (err) {
      console.error('Get contacts error:', err)
      sendError(res, err.message)
    }
  }),

  // GET /api/contacts/:chain - Get contacts for specific chain
  'GET /api/contacts/': requireAuth(async (req, res, chain) => {
    try {
      const identity = await loadIdentity()
      await initializeStorage(identity)

      const contacts = await getContacts(chain)

      sendJSON(res, {
        success: true,
        count: contacts.length,
        chain,
        contacts
      })
    } catch (err) {
      console.error('Get contacts by chain error:', err)
      sendError(res, err.message)
    }
  }),

  // POST /api/contacts - Add new contact
  'POST /api/contacts': requireAuth(async (req, res) => {
    try {
      const body = await parseBody(req)
      let { address, chain, name, notes } = body

      if (!address || !chain || !name) {
        return sendError(res, 'Missing required fields: address, chain, name', 400)
      }

      // Validate and sanitize chain
      try {
        chain = validateChain(chain)
      } catch (err) {
        return sendError(res, err.message, 400)
      }

      // Validate address with checksum
      const config = getConfig()
      const network = config.network || 'testnet'
      const addressValidation = validateAddress(address, chain, network)
      if (!addressValidation.valid) {
        return sendError(res, `Invalid address: ${addressValidation.reason}`, 400)
      }
      if (addressValidation.warning) {
        console.warn(`⚠️  Address validation warning: ${addressValidation.warning}`)
      }

      // Sanitize name and notes
      try {
        name = sanitizeContactName(name)
        notes = notes ? sanitizeNotes(notes) : ''
      } catch (err) {
        return sendError(res, err.message, 400)
      }

      const identity = await loadIdentity()
      await initializeStorage(identity)

      // Check if contact already exists for this address
      const existing = await findContact(address, chain)
      if (existing) {
        return sendError(res, `Contact already exists: ${existing.name}`, 400)
      }

      const contact = await addContact({ address, chain, name, notes })

      sendJSON(res, {
        success: true,
        message: 'Contact added successfully',
        contact
      })
    } catch (err) {
      console.error('Add contact error:', err)
      sendError(res, err.message)
    }
  }),

  // PUT /api/contacts/:id - Update contact
  'PUT /api/contacts/': requireAuth(async (req, res, id) => {
    try {
      const body = await parseBody(req)
      let { name, notes, metadata } = body

      if (!id) {
        return sendError(res, 'Contact ID required', 400)
      }

      // Sanitize name and notes if provided
      try {
        if (name !== undefined) {
          name = sanitizeContactName(name)
        }
        if (notes !== undefined) {
          notes = sanitizeNotes(notes)
        }
      } catch (err) {
        return sendError(res, err.message, 400)
      }

      const identity = await loadIdentity()
      await initializeStorage(identity)

      const updates = {}
      if (name !== undefined) updates.name = name
      if (notes !== undefined) updates.notes = notes
      if (metadata !== undefined) updates.metadata = metadata

      const contact = await updateContact(id, updates)

      sendJSON(res, {
        success: true,
        message: 'Contact updated successfully',
        contact
      })
    } catch (err) {
      console.error('Update contact error:', err)
      sendError(res, err.message)
    }
  }),

  // DELETE /api/contacts/:id - Delete contact
  'DELETE /api/contacts/': requireAuth(async (req, res, id) => {
    try {
      if (!id) {
        return sendError(res, 'Contact ID required', 400)
      }

      const identity = await loadIdentity()
      await initializeStorage(identity)

      await deleteContact(id)

      sendJSON(res, {
        success: true,
        message: 'Contact deleted successfully'
      })
    } catch (err) {
      console.error('Delete contact error:', err)
      sendError(res, err.message)
    }
  }),

  // POST /api/contacts/:id/addresses - Add address to contact
  'POST /api/contacts/': requireAuth(async (req, res, id, ...rest) => {
    console.log('📝 POST /api/contacts/ route hit:', { id, rest })

    // Check if this is the /addresses sub-route
    if (rest[0] === 'addresses') {
      console.log('✅ /addresses sub-route detected')
      try {
        if (!id) {
          console.log('❌ No contact ID provided')
          return sendError(res, 'Contact ID required', 400)
        }

        const body = await parseBody(req)
        console.log('📦 Request body:', { chain: body.chain, address: body.address, network: body.network })

        let { chain, address, network } = body

        if (!chain || !address) {
          console.log('❌ Missing required fields:', { chain, address })
          return sendError(res, 'Missing required fields: chain, address', 400)
        }

        // Validate chain
        try {
          chain = validateChain(chain)
          console.log('✅ Chain validated:', chain)
        } catch (err) {
          console.log('❌ Chain validation error:', err.message)
          return sendError(res, err.message, 400)
        }

        // Validate network
        if (!network) {
          const config = getConfig()
          network = config.network || 'testnet'
        }
        console.log('🌐 Network:', network)

        // Validate address
        console.log('🔍 Validating address:', { address, chain, network })
        const addressValidation = validateAddress(address, chain, network)
        console.log('📋 Address validation result:', addressValidation)

        if (!addressValidation.valid) {
          console.log('❌ Address validation failed:', addressValidation.reason)
          return sendError(res, `Invalid address: ${addressValidation.reason}`, 400)
        }
        if (addressValidation.warning) {
          console.warn(`⚠️  Address validation warning: ${addressValidation.warning}`)
        }

        console.log('💾 Loading identity and initializing storage...')
        const identity = await loadIdentity()
        await initializeStorage(identity)

        console.log('➕ Adding address to contact...')
        const contact = await addAddressToContact(id, { chain, address, network })
        console.log('✅ Address added successfully:', contact.id)

        sendJSON(res, {
          success: true,
          message: 'Address added to contact successfully',
          contact
        })
      } catch (err) {
        console.error('❌ Add address to contact error:', err)
        console.error('Stack trace:', err.stack)
        sendError(res, err.message)
      }
      return
    }

    // If not /addresses, return 404
    console.log('❌ Not /addresses sub-route, returning 404')
    sendError(res, 'Not found', 404)
  }),

  // DELETE /api/contacts/:id/addresses/:chain - Remove address from contact
  'DELETE /api/contacts/': requireAuth(async (req, res, id, ...rest) => {
    // Check if this is the /addresses/:chain sub-route
    if (rest[0] === 'addresses' && rest[1]) {
      try {
        if (!id) {
          return sendError(res, 'Contact ID required', 400)
        }

        const chain = rest[1]

        const identity = await loadIdentity()
        await initializeStorage(identity)

        const contact = await removeAddressFromContact(id, chain)

        sendJSON(res, {
          success: true,
          message: 'Address removed from contact successfully',
          contact
        })
      } catch (err) {
        console.error('Remove address from contact error:', err)
        sendError(res, err.message)
      }
      return
    }

    // If not matching, return 404
    sendError(res, 'Not found', 404)
  }),

  // GET /api/contacts/search?q=name - Search contacts
  'GET /api/contacts/search': requireAuth(async (req, res) => {
    try {
      const url = new URL(req.url, `http://${req.headers.host}`)
      const query = url.searchParams.get('q')

      if (!query) {
        return sendError(res, 'Search query required', 400)
      }

      const identity = await loadIdentity()
      await initializeStorage(identity)

      const contact = await findContact(query)

      if (!contact) {
        sendJSON(res, {
          success: true,
          found: false,
          contact: null
        })
      } else {
        sendJSON(res, {
          success: true,
          found: true,
          contact
        })
      }
    } catch (err) {
      console.error('Search contact error:', err)
      sendError(res, err.message)
    }
  }),

  // GET /api/settings - Get current settings
  'GET /api/settings': requireAuth(async (req, res) => {
    try {
      const settings = getConfig()

      sendJSON(res, {
        success: true,
        settings
      })
    } catch (err) {
      console.error('Get settings error:', err)
      sendError(res, err.message)
    }
  }),

  // PUT /api/settings - Update a setting
  'PUT /api/settings': requireAuth(async (req, res) => {
    try {
      const body = await parseBody(req)
      const { key, value } = body

      if (!key || value === undefined) {
        return sendError(res, 'Missing required fields: key, value', 400)
      }

      updateConfig(key, value)

      sendJSON(res, {
        success: true,
        message: 'Setting updated',
        key,
        value
      })
    } catch (err) {
      console.error('Update setting error:', err)
      sendError(res, err.message)
    }
  }),

  // GET /api/settings/providers/:chain - Get available providers for chain
  'GET /api/settings/providers/': requireAuth(async (req, res, chain) => {
    try {
      if (!chain || (chain !== 'bitcoin' && chain !== 'ethereum')) {
        return sendError(res, 'Invalid chain. Must be bitcoin or ethereum', 400)
      }

      const providers = getAvailableProviders(chain)
      const providerDetails = providers.map(name => {
        const info = getProviderInfo(chain, name)
        return {
          name,
          displayName: info.name,
          needsApiKey: info.needsApiKey,
          usesNativeWDK: info.usesNativeWDK || false
        }
      })

      sendJSON(res, {
        success: true,
        chain,
        providers: providerDetails
      })
    } catch (err) {
      console.error('Get providers error:', err)
      sendError(res, err.message)
    }
  }),

  // POST /api/settings/reset - Reset settings to defaults
  'POST /api/settings/reset': requireAuth(async (req, res) => {
    try {
      saveConfig(DEFAULT_CONFIG)

      sendJSON(res, {
        success: true,
        message: 'Settings reset to defaults'
      })
    } catch (err) {
      console.error('Reset settings error:', err)
      sendError(res, err.message)
    }
  }),

  // POST /api/estimate-fee
  'POST /api/estimate-fee': withRateLimit(requireAuth(async (req, res) => {
    try {
      const body = await parseBody(req)
      let { chain, network, to, amount } = body

      if (!chain || !network || !to || !amount) {
        return sendError(res, 'Missing required fields', 400)
      }

      // Validate and sanitize chain and network
      try {
        chain = validateChain(chain)
        network = validateNetwork(network)
      } catch (err) {
        return sendError(res, err.message, 400)
      }

      // Validate recipient address with checksum
      const addressValidation = validateAddress(to, chain, network)
      if (!addressValidation.valid) {
        return sendError(res, `Invalid recipient address: ${addressValidation.reason}`, 400)
      }

      // Validate amount format and range
      const amountValidation = validateAmount(amount, chain)
      if (!amountValidation.valid) {
        return sendError(res, `Invalid amount: ${amountValidation.reason}`, 400)
      }
      const amountBigInt = amountValidation.amount

      const walletMnemonic = loadWalletMnemonicCached()
      if (!walletMnemonic) {
        return sendError(res, 'Wallet not initialized', 400)
      }

      const { wdk } = await initializeWDK(walletMnemonic, { testnet: network === 'testnet' })
      const account = await wdk.getAccount(chain, 0)

      // Quote the transaction to get fee estimate
      const quote = await account.quoteSendTransaction({
        to,
        value: amountBigInt
      })

      sendJSON(res, {
        fee: quote.fee.toString(),
        total: (amountBigInt + quote.fee).toString(),
        formatted: {
          fee: formatAmount(quote.fee, chain),
          total: formatAmount(amountBigInt + quote.fee, chain)
        }
      })
    } catch (err) {
      console.error('Fee estimation error:', err)
      sendError(res, err.message)
    }
  })),

  // POST /api/send
  'POST /api/send': withRateLimit(requireAuth(async (req, res) => {
    try {
      const body = await parseBody(req)
      let { chain, network, to, amount } = body

      if (!chain || !network || !to || !amount) {
        return sendError(res, 'Missing required fields', 400)
      }

      // Validate and sanitize chain and network
      try {
        chain = validateChain(chain)
        network = validateNetwork(network)
      } catch (err) {
        return sendError(res, err.message, 400)
      }

      // Validate recipient address with checksum
      const addressValidation = validateAddress(to, chain, network)
      if (!addressValidation.valid) {
        return sendError(res, `Invalid recipient address: ${addressValidation.reason}`, 400)
      }
      if (addressValidation.warning) {
        console.warn(`⚠️  Address validation warning: ${addressValidation.warning}`)
      }

      // Validate amount format and range
      const amountValidation = validateAmount(amount, chain)
      if (!amountValidation.valid) {
        return sendError(res, `Invalid amount: ${amountValidation.reason}`, 400)
      }
      const amountBigInt = amountValidation.amount

      const walletMnemonic = loadWalletMnemonicCached()
      if (!walletMnemonic) {
        return sendError(res, 'Wallet not initialized', 400)
      }

      const { wdk } = await initializeWDK(walletMnemonic, { testnet: network === 'testnet' })
      const account = await wdk.getAccount(chain, 0)

      // Check balance
      const balance = await account.getBalance()

      // Get fee estimate
      let fee
      try {
        const quote = await account.quoteSendTransaction({
          to,
          value: amountBigInt
        })
        fee = quote.fee
      } catch (err) {
        console.error('Fee estimation failed:', err)
        return sendError(res, `Cannot estimate fee: ${err.message}`, 400)
      }

      // Check sufficient balance (amount + fee)
      const balanceCheck = validateSufficientBalance(amountBigInt, balance, fee, chain)
      if (!balanceCheck.valid) {
        return sendError(res, balanceCheck.reason, 400)
      }

      // Send the transaction
      const result = await account.sendTransaction({
        to,
        value: amountBigInt
      })

      console.log(`✅ Transaction sent: ${result.hash}`)
      console.log(`   Amount: ${formatAmount(amountBigInt, chain)}`)
      console.log(`   Fee: ${formatAmount(fee, chain)}`)
      console.log(`   To: ${to}`)

      sendJSON(res, {
        hash: result.hash,
        fee: result.fee.toString(),
        amount: amountBigInt.toString()
      })
    } catch (err) {
      console.error('Send transaction error:', err)
      sendError(res, err.message)
    }
  })),

  // POST /api/sync-transactions - Manual transaction sync from blockchain
  'POST /api/sync-transactions': requireAuth(async (req, res) => {
    try {
      const walletMnemonic = loadWalletMnemonicCached()
      if (!walletMnemonic) {
        return sendError(res, 'Wallet not initialized', 400)
      }

      const identity = await loadIdentity()
      await initializeStorage(identity)

      const { wdk } = await initializeWDK(walletMnemonic, { testnet: true })

      const results = {
        synced: 0,
        errors: [],
        chains: {}
      }

      // Sync Bitcoin transactions using Blockstream API
      try {
        const btcAccount = await wdk.getAccount('bitcoin', 0)
        const btcAddress = await btcAccount.getAddress()

        const response = await fetch(`https://blockstream.info/testnet/api/address/${btcAddress}/txs`)
        const txs = await response.json()

        let btcSynced = 0
        let btcSkipped = 0

        for (const tx of txs) {
          const existing = await getTransaction('bitcoin', tx.txid)
          if (existing) {
            btcSkipped++
            continue
          }

          // Determine direction and amount
          let direction = 'sent'
          let amount = 0n
          let to = '(unknown)'
          let from = btcAddress

          // Check outputs to see if we received
          const receivedOutput = tx.vout.find(out => out.scriptpubkey_address === btcAddress)
          if (receivedOutput) {
            direction = 'received'
            amount = BigInt(receivedOutput.value)
            to = btcAddress
            from = '(external)'
          } else {
            // We sent - find the output that's NOT our change address
            const sentOutput = tx.vout.find(out => out.scriptpubkey_address !== btcAddress)
            if (sentOutput) {
              amount = BigInt(sentOutput.value)
              to = sentOutput.scriptpubkey_address || '(unknown)'
            }
          }

          await storeTransaction({
            hash: tx.txid,
            chain: 'bitcoin',
            network: 'testnet',
            direction,
            from,
            to,
            amount: amount.toString(),
            fee: tx.fee ? tx.fee.toString() : '0',
            status: tx.status.confirmed ? 'confirmed' : 'pending',
            timestamp: tx.status.block_time ? tx.status.block_time * 1000 : Date.now(),
            blockHeight: tx.status.block_height || null
          })

          btcSynced++
          results.synced++
        }

        results.chains.bitcoin = {
          synced: btcSynced,
          skipped: btcSkipped,
          total: txs.length
        }

      } catch (err) {
        console.error('Bitcoin sync error:', err.message)
        results.errors.push(`bitcoin: ${err.message}`)
      }

      // Ethereum - skip for now (would need Etherscan API)
      results.chains.ethereum = {
        synced: 0,
        skipped: 0,
        note: 'Ethereum sync requires Etherscan API - not implemented yet'
      }

      sendJSON(res, results)

    } catch (err) {
      console.error('Sync transactions error:', err)
      sendError(res, err.message)
    }
  }),

  // GET /api/wallet/encryption-status - Check if mnemonics are encrypted or plaintext
  'GET /api/wallet/encryption-status': async (req, res) => {
    try {
      // Check if wallet files exist
      if (!fs.existsSync(MNEMONIC_FILE)) {
        return sendJSON(res, { encrypted: null, initialized: false })
      }

      // Read identity mnemonic file
      const identityContent = fs.readFileSync(MNEMONIC_FILE, 'utf8').trim()

      // Check if plaintext (24 words separated by spaces)
      const isPlaintext = identityContent.includes(' ') && identityContent.split(' ').length === 24

      sendJSON(res, {
        encrypted: !isPlaintext,
        initialized: true,
        needsMigration: isPlaintext
      })
    } catch (err) {
      console.error('Encryption status check error:', err)
      sendError(res, err.message)
    }
  },

  // POST /api/change-password - Change wallet encryption password
  'POST /api/change-password': requireAuth(async (req, res) => {
    try {
      const body = await parseBody(req)
      const { currentPassword, newPassword } = body

      // Validate inputs
      if (!currentPassword || !newPassword) {
        return sendError(res, 'Both current and new passwords required', 400)
      }

      if (newPassword.length < 8) {
        return sendError(res, 'New password must be at least 8 characters', 400)
      }

      if (currentPassword === newPassword) {
        return sendError(res, 'New password must be different from current password', 400)
      }

      // Verify current password by trying to decrypt
      let identityMnemonic, walletMnemonic
      try {
        identityMnemonic = loadIdentityMnemonic(currentPassword)
        walletMnemonic = loadWalletMnemonic(currentPassword)
      } catch (err) {
        return sendError(res, 'Current password is incorrect', 401)
      }

      // Re-encrypt with new password
      saveIdentityMnemonic(identityMnemonic, newPassword)
      saveWalletMnemonic(walletMnemonic, newPassword)

      // Update cached password
      setCachedPassword(newPassword)

      console.log('🔑 Password changed successfully')

      sendJSON(res, {
        success: true,
        message: 'Password changed successfully. Mnemonics re-encrypted.'
      })
    } catch (err) {
      console.error('Change password error:', err)
      sendError(res, err.message)
    }
  }),

  // POST /api/unlock - Unlock existing wallet with password (caches password in memory)
  'POST /api/unlock': async (req, res) => {
    try {
      const body = await parseBody(req)
      const { password } = body

      // Validate password provided
      if (!password || password.length < 8) {
        return sendError(res, 'Password must be at least 8 characters', 400)
      }

      // Check if wallet exists
      if (!fs.existsSync(MNEMONIC_FILE)) {
        return sendError(res, 'Wallet not initialized. Use /api/init or /api/restore first.', 400)
      }

      // Try to load mnemonics with provided password
      let identityMnemonic, walletMnemonic
      try {
        identityMnemonic = loadIdentityMnemonic(password)
        walletMnemonic = loadWalletMnemonic(password)
      } catch (err) {
        // If decryption fails, check if it's plaintext (legacy)
        const identityContent = fs.readFileSync(MNEMONIC_FILE, 'utf8').trim()
        const walletContent = fs.readFileSync(path.join(STORAGE_DIR, '.wallet-mnemonic'), 'utf8').trim()

        if (identityContent.includes(' ') && identityContent.split(' ').length === 24 &&
            walletContent.includes(' ') && walletContent.split(' ').length === 12) {
          // Legacy plaintext mnemonics - migrate to encrypted format
          console.log('🔄 Migrating plaintext mnemonics to encrypted format...')
          identityMnemonic = identityContent
          walletMnemonic = walletContent

          // Save encrypted versions
          saveIdentityMnemonic(identityMnemonic, password)
          saveWalletMnemonic(walletMnemonic, password)
          console.log('✅ Mnemonics encrypted successfully')
        } else {
          return sendError(res, 'Invalid password', 401)
        }
      }

      // Cache password in memory
      setCachedPassword(password)

      // Load or generate API token
      let apiToken = loadToken()
      if (!apiToken) {
        apiToken = generateToken()
        saveToken(apiToken)
      }

      // Get public key for confirmation
      const identity = await IdentityKey.from({ mnemonic: identityMnemonic })
      const publicKey = b4a.toString(identity.identityKeyPair.publicKey, 'hex').slice(0, 16) + '...'

      sendJSON(res, {
        success: true,
        message: 'Wallet unlocked successfully',
        publicKey,
        token: apiToken,
        migrated: identityMnemonic !== loadIdentityMnemonic(password) // True if we migrated from plaintext
      })
    } catch (err) {
      console.error('Unlock error:', err)
      sendError(res, err.message)
    }
  },

  // POST /api/init (full initialization: identity + wallet)
  'POST /api/init': async (req, res) => {
    try {
      const body = await readBody(req)
      const { password } = body

      // Validate password
      if (!password || password.length < 8) {
        return sendError(res, 'Password must be at least 8 characters', 400)
      }

      // Check if already initialized (use try/catch since loadIdentityMnemonic now requires password)
      let existingIdentity = null
      let existingWallet = null
      try {
        existingIdentity = loadIdentityMnemonic(password)
        existingWallet = loadWalletMnemonic(password)
      } catch (err) {
        // Ignore errors - if decrypt fails, it means no existing wallet or wrong password
        // We'll catch the "already initialized" case by checking file existence below
      }

      if (fs.existsSync(MNEMONIC_FILE) || fs.existsSync(path.join(STORAGE_DIR, '.wallet-mnemonic'))) {
        return sendError(res, 'System already initialized', 400)
      }

      // Generate identity mnemonic (24 words for device attestation)
      const identityMnemonic = generateIdentityMnemonic()

      // Generate wallet mnemonic (12 words for crypto keys)
      const walletMnemonic = generateWalletMnemonic()

      // Save both (encrypted with password)
      saveIdentityMnemonic(identityMnemonic, password)
      saveWalletMnemonic(walletMnemonic, password)

      // Cache password in memory for subsequent operations
      setCachedPassword(password)

      // Generate and save API token
      const apiToken = generateToken()
      saveToken(apiToken)

      // Get public key for display
      const identity = await IdentityKey.from({ mnemonic: identityMnemonic })
      const publicKey = b4a.toString(identity.identityKeyPair.publicKey, 'hex').slice(0, 16) + '...'

      // Return both mnemonics (PLAINTEXT for user backup) AND API token
      sendJSON(res, {
        identityMnemonic,
        walletMnemonic,
        publicKey,
        token: apiToken,
        warning: 'CRITICAL: Write down BOTH sets of words and store them safely!',
        passwordWarning: 'IMPORTANT: Remember your password - you will need it to access your wallet. If you lose it, you can only restore using your mnemonics.',
        tokenWarning: 'IMPORTANT: Save this API token - it\'s required for all API requests. Store it securely (e.g., in localStorage for web UI).'
      })
    } catch (err) {
      console.error('Init error:', err)
      sendError(res, err.message)
    }
  },

  // POST /api/wallet/init (wallet only - for backward compatibility)
  'POST /api/wallet/init': async (req, res) => {
    try {
      // Check if wallet already exists
      const existing = loadWalletMnemonicCached()
      if (existing) {
        return sendError(res, 'Wallet already initialized', 400)
      }

      // Generate new mnemonic
      const mnemonic = generateWalletMnemonic()

      // Save to file
      saveWalletMnemonic(mnemonic)

      // Return mnemonic (user MUST backup!)
      sendJSON(res, {
        mnemonic,
        warning: 'CRITICAL: Write down these 12 words and store them safely. This is the ONLY way to recover your wallet!'
      })
    } catch (err) {
      console.error('Wallet init error:', err)
      sendError(res, err.message)
    }
  },

  // POST /api/restore (full restore: identity + wallet)
  'POST /api/restore': async (req, res) => {
    try {
      const body = await parseBody(req)
      const { identityMnemonic, walletMnemonic, password } = body

      if (!identityMnemonic || !walletMnemonic) {
        return sendError(res, 'Both identity and wallet mnemonics are required', 400)
      }

      // Validate password
      if (!password || password.length < 8) {
        return sendError(res, 'Password must be at least 8 characters', 400)
      }

      // Validate identity mnemonic format (24 words)
      const identityWords = identityMnemonic.trim().split(/\s+/)
      if (identityWords.length !== 24) {
        return sendError(res, 'Invalid identity mnemonic: must be 24 words', 400)
      }

      // Validate wallet mnemonic format (12 words)
      const walletWords = walletMnemonic.trim().split(/\s+/)
      if (walletWords.length !== 12) {
        return sendError(res, 'Invalid wallet mnemonic: must be 12 words', 400)
      }

      // Check if already initialized (use file existence check)
      if (fs.existsSync(MNEMONIC_FILE) || fs.existsSync(path.join(STORAGE_DIR, '.wallet-mnemonic'))) {
        return sendError(res, 'System already initialized. Delete ~/.hyper-wallet files to restore.', 400)
      }

      // Test identity mnemonic
      try {
        await IdentityKey.from({ mnemonic: identityMnemonic })
      } catch (err) {
        return sendError(res, 'Invalid identity mnemonic: ' + err.message, 400)
      }

      // Test wallet mnemonic
      try {
        await initializeWDK(walletMnemonic, { testnet: true })
      } catch (err) {
        return sendError(res, 'Invalid wallet mnemonic: ' + err.message, 400)
      }

      // Save both mnemonics (encrypted with password)
      saveIdentityMnemonic(identityMnemonic, password)
      saveWalletMnemonic(walletMnemonic, password)

      // Cache password in memory for subsequent operations
      setCachedPassword(password)

      // Generate and save new API token (security: don't reuse old token)
      const apiToken = generateToken()
      saveToken(apiToken)

      sendJSON(res, {
        success: true,
        message: 'System restored successfully',
        token: apiToken,
        tokenWarning: 'IMPORTANT: Save this API token - it\'s required for all API requests.',
        passwordWarning: 'IMPORTANT: Remember your password - you will need it to access your wallet.'
      })
    } catch (err) {
      console.error('Restore error:', err)
      sendError(res, err.message)
    }
  },


  // POST /api/wallet/restore (wallet only - for backward compatibility)
  'POST /api/wallet/restore': async (req, res) => {
    try {
      const body = await parseBody(req)
      const { mnemonic } = body

      if (!mnemonic) {
        return sendError(res, 'Mnemonic is required', 400)
      }

      // Validate mnemonic format (12 words)
      const words = mnemonic.trim().split(/\s+/)
      if (words.length !== 12) {
        return sendError(res, 'Invalid mnemonic: must be 12 words', 400)
      }

      // Check if wallet already exists
      const existing = loadWalletMnemonicCached()
      if (existing) {
        return sendError(res, 'Wallet already initialized. Delete ~/.hyper-wallet/.wallet-mnemonic to restore.', 400)
      }

      // Test mnemonic by trying to initialize WDK
      try {
        await initializeWDK(mnemonic, { testnet: true })
      } catch (err) {
        return sendError(res, 'Invalid mnemonic: ' + err.message, 400)
      }

      // Save mnemonic
      saveWalletMnemonic(mnemonic)

      sendJSON(res, {
        success: true,
        message: 'Wallet restored successfully'
      })
    } catch (err) {
      console.error('Wallet restore error:', err)
      sendError(res, err.message)
    }
  }
}

// Static file MIME types
const mimeTypes = {
  '.html': 'text/html',
  '.css': 'text/css',
  '.js': 'application/javascript',
  '.json': 'application/json',
  '.png': 'image/png',
  '.jpg': 'image/jpeg',
  '.svg': 'image/svg+xml'
}

// Serve static files from local filesystem
async function serveStatic(req, res) {
  const fileName = req.url === '/' ? 'index.html' : req.url.substring(1)

  try {
    let content

    const filePath = path.join(UI_DIR, fileName)

    // Security: prevent directory traversal
    if (!filePath.startsWith(UI_DIR)) {
      res.statusCode = 403
      res.end('Forbidden')
      return
    }

    if (!fs.existsSync(filePath)) {

        res.statusCode = 404
        res.end('Not found')
        return
      }

      content = fs.readFileSync(filePath)
    }

    // Serve the file
    const ext = path.extname(fileName)
    const contentType = mimeTypes[ext] || 'application/octet-stream'

    res.statusCode = 200
    res.setHeader('Content-Type', contentType)
    res.end(content)
  } catch (err) {
    console.error(`[STATIC] Error serving ${fileName}:`, err.message)
    res.statusCode = 500
    res.end('Internal server error')
  }
}

// Server instance (created in startServer function)
let server = null

// Start server function
async function startServer() {

  // Main HTTP server
  server = http.createServer(async (req, res) => {
    setCORS(res, req)

    // Handle OPTIONS (CORS preflight)
    if (req.method === 'OPTIONS') {
      res.statusCode = 204
      res.end()
      return
    }

    console.log(`${req.method} ${req.url}`)

    // API routes
    if (req.url.startsWith('/api/')) {
      // Find matching route
      let handled = false

      // Sort routes by path length (longest first) so specific routes match before generic
      const sortedRoutes = Object.entries(routes).sort((a, b) => {
        const pathA = a[0].split(' ')[1]
        const pathB = b[0].split(' ')[1]
        return pathB.length - pathA.length
      })

      for (const [routeKey, handler] of sortedRoutes) {
        const [method, routePath] = routeKey.split(' ')

        // Use startsWith for all routes to support path parameters
        const isMatch = req.method === method && req.url.startsWith(routePath)

        if (isMatch) {
          // Extract parameters (e.g., :id/addresses from /api/contacts/:id/addresses)
          const param = req.url.substring(routePath.length)
          const params = param ? param.split('/').filter(p => p) : []
          await handler(req, res, ...params)
          handled = true
          break
        }
      }

      if (!handled) {
        res.statusCode = 404
        sendJSON(res, { error: 'Route not found' }, 404)
      }

      return
    }

    // Serve static files
    await serveStatic(req, res)
  })

  // Add error handler BEFORE calling listen
  server.on('error', (err) => {
    console.error('')
    console.error('❌ SERVER ERROR:', err.message)
    console.error('   Code:', err.code)
    console.error('   Stack:', err.stack)
    console.error('')
    process.exit(1)
  })

  // Start server with persistent storage
  server.listen(PORT, HOST, async () => {
    // Check for hidden error (bare-tcp bug - swallows errors)
    if (server._error) {
      console.error('')
      console.error('❌ HIDDEN SERVER ERROR DETECTED!')
      console.error('   This is a bare-tcp bug - error was swallowed')
      console.error('   Error:', server._error.message)
      console.error('   Code:', server._error.code)
      console.error('')
      process.exit(1)
    }

    console.log('')
    console.log('🌐 Hyper Wallet Server (Daemon Mode)')
    console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
    console.log('')

    // Initialize persistent storage if wallet exists and password is cached
    try {
      const identityFileExists = fs.existsSync(MNEMONIC_FILE)
      const walletFileExists = fs.existsSync(path.join(STORAGE_DIR, '.wallet-mnemonic'))

      if (identityFileExists && walletFileExists) {
        if (getCachedPassword()) {
          // Password cached - can initialize storage
          const identityMnemonic = loadIdentityMnemonicCached()
          const walletMnemonic = loadWalletMnemonicCached()

          const identity = await loadIdentity()
          await initializeStorage(identity)
          enableReplication()

          console.log('✅ Persistent storage initialized')
          console.log('✅ P2P sync enabled')
          console.log('💡 Use POST /api/sync-transactions to sync transaction history')
          console.log('')
        } else {
          console.log('⚠️  Wallet encrypted - password not cached')
          console.log('   Storage will initialize after first authenticated request')
          console.log('   Password cached in memory during this server session only')
          console.log('')
        }
      } else {
        console.log('⚠️  Wallet not initialized - storage will open per-request')
        console.log('   Use web UI or CLI to initialize wallet')
        console.log('')
      }
    } catch (err) {
      console.error('⚠️  Warning: Could not initialize persistent storage:', err.message)
      console.error('   Storage will open per-request')
      console.log('')
    }

    // Get actual bound address from server
    const actualAddress = server.address()
    const actualPort = actualAddress ? actualAddress.port : PORT
    const actualHost = actualAddress ? actualAddress.address : HOST

    console.log(`   Requested: ${HOST}:${PORT}`)
    console.log(`   Actually:  ${actualHost}:${actualPort}`)

    if (actualPort !== parseInt(PORT)) {
      console.log(`   ⚠️  PORT MISMATCH - Pear remapped ${PORT} → ${actualPort}`)
    }

    if (HOST === 'localhost' || HOST === '127.0.0.1') {
      console.log(`   Access:    http://localhost:${actualPort} (local only)`)
      console.log(`   Security:  ✅ Locked to localhost`)
    } else {
      console.log(`   Access:    http://localhost:${actualPort}`)
      console.log(`   Network:   http://<your-ip>:${actualPort}`)
      console.log(`   Security:  ⚠️  Accessible from network`)
    }
    console.log(`   API:       http://localhost:${actualPort}/api/status`)
    console.log(`   UI Dir:    ${UI_DIR}`)
    console.log('')
    console.log('📡 API Endpoints:')
    console.log('   GET  /api/status')
    console.log('   GET  /api/balance/:chain')
    console.log('   GET  /api/address/:chain')
    console.log('   GET  /api/transactions/:chain')
    console.log('   POST /api/device/add')
    console.log('   POST /api/device/join')
    console.log('   GET  /api/device/list')
    console.log('')
    console.log('💡 Use hyper-wallet-client.cjs to access via CLI')
    console.log('')
    console.log('Press Ctrl+C to stop')
    console.log('')
  })
}

// Export startServer for hyper-wallet.cjs to call
module.exports = { startServer }

// Only start if run directly (not when required as module)
if (require.main === module) {
  startServer().catch(err => {
    console.error('Failed to start server:', err)
    process.exit(1)
  })
}

// Graceful shutdown
process.on('SIGINT', async () => {
  console.log('')
  console.log('🛑 Shutting down server...')

  try {
    await closeStorage()
    console.log('✅ Storage closed')
  } catch (err) {
    // Storage might not be open
  }

  if (server) {
    server.close(() => {
      console.log('✅ Server stopped')
      process.exit(0)
    })
  } else {
    process.exit(0)
  }
})