OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-wallet-lib / wallet-storage.cjs

2271 lines · javascript ref:
// lib/wallet-context-autobase.cjs - Multi-device storage with Autobase

require('bare-node-runtime/global')
const path = require('bare-path')
const os = require('bare-os')
const fs = require('bare-fs')
const Corestore = require('corestore')
const Hyperbee = require('hyperbee')
const Autobase = require('autobase')
const Hyperswarm = require('hyperswarm')
const crypto = require('hypercore-crypto')
const b4a = require('b4a')
const IdentityKey = require('keet-identity-key')
const { createPairingServer, createPairingClient } = require('./pairing-protocol.cjs')
const { loadWalletMnemonic, initializeWDK } = require('./wdk-integration.cjs')
const { fetchTransactions, fetchTokenTransactions } = require('./blockchain-providers.cjs')
const { getConfig } = require('./config.cjs')

// Storage configuration
const STORAGE_DIR = path.join(os.homedir(), '.hyper-wallet', 'storage')
const DEVICES_FILE = path.join(os.homedir(), '.hyper-wallet', '.devices')
const PENDING_FILE = path.join(os.homedir(), '.hyper-wallet', '.pending')

// Singleton instances
let store = null
let autobase = null
let txHistoryDb = null
let swarm = null
let identityKeyPair = null
let connections = new Set() // Track active connections

// Initialization lock to prevent concurrent initialization
let initializationPromise = null

// WDK singleton instances (Phase 6.1)
let wdkInstance = null
let walletManagers = {} // { bitcoin, ethereum }

/**
 * Apply function for Autobase
 * Processes operations from all input cores and builds Hyperbee view
 *
 * Pattern based on holepunchto/autobase examples:
 * - nodes: array of node objects from all input cores
 * - view: already a Hyperbee instance (created by open())
 * - base: the Autobase instance (for operations like addWriter)
 * - IMPORTANT: Must parse JSON manually (no valueEncoding in Autobase)
 */
async function apply(nodes, view, base) {
  // Use batch for efficiency when processing multiple operations
  const b = view.batch({ update: false })

  for (const node of nodes) {
    // Parse JSON manually (autobase doesn't use valueEncoding)
    const value = JSON.parse(node.value)

    // Handle writer management operations
    if (value.addWriter) {
      // Add writer to Autobase inputs using the host.addWriter() API
      const writerKey = b4a.from(value.addWriter, 'hex')
      await base.addWriter(writerKey, { indexer: true })
      continue
    }

    if (value.removeWriter) {
      // Writer removal marker
      // Note: Autobase doesn't actually support removing inputs
      // This is just a marker that the device has been revoked
      // The device list manages the actual removal
      continue
    }

    // Handle transaction operations
    if (value.type === 'put-tx') {
      // Store value as JSON string
      await b.put(value.key, JSON.stringify(value.value))
    } else if (value.type === 'del-tx') {
      await b.del(value.key)
    }

    // Handle address operations (Phase 6.2)
    if (value.type === 'put-address') {
      await b.put(value.key, JSON.stringify(value.value))
    } else if (value.type === 'del-address') {
      await b.del(value.key)
    }

    // Handle UTXO operations (Phase 6.3)
    if (value.type === 'put-utxo') {
      await b.put(value.key, JSON.stringify(value.value))
    } else if (value.type === 'del-utxo') {
      await b.del(value.key)
    }

    // Handle transaction storage (Phase 6.6)
    if (value.type === 'store-transaction') {
      await b.put(value.key, JSON.stringify(value.value))
    } else if (value.type === 'delete-transaction') {
      await b.del(value.key)
    }

    // Handle contact storage
    if (value.type === 'put-contact') {
      await b.put(value.key, JSON.stringify(value.value))
    } else if (value.type === 'del-contact') {
      await b.del(value.key)
    }
  }

  await b.flush()
}

/**
 * Initialize storage with Autobase multi-writer support
 * @param {Object} identity - keet-identity-key instance
 */
async function initializeStorage(identity) {
  // Return existing instance if already initialized
  if (store && autobase && txHistoryDb) {
    return { store, autobase, txHistoryDb, swarm }
  }

  // If initialization is in progress, wait for it
  if (initializationPromise) {
    console.log('⏳ Storage initialization in progress, waiting...')
    return await initializationPromise
  }

  // Start new initialization
  console.log('🔧 Initializing storage (singleton)')
  initializationPromise = _initializeStorage(identity)

  try {
    const result = await initializationPromise
    return result
  } finally {
    // Clear the promise after initialization completes (success or failure)
    initializationPromise = null
  }
}

/**
 * Internal storage initialization (wrapped by singleton lock)
 */
async function _initializeStorage(identity) {
  // Store identity keypair for attestation
  identityKeyPair = identity.identityKeyPair

  // Initialize Corestore
  store = new Corestore(STORAGE_DIR)
  await store.ready()

  // View class for Autobase
  // Pattern from autobase-example
  class WalletView {
    open(store) {
      const core = store.get('view')
      // Use binary encoding - we'll handle JSON manually
      return new Hyperbee(core, {
        keyEncoding: 'utf-8',
        valueEncoding: 'binary'
      })
    }

    async apply(nodes, view, base) {
      return apply(nodes, view, base)
    }

    close(view) {
      return view.close()
    }
  }

  // Get or create namespace for this autobase
  const namespace = await Autobase.getLocalKey(store)

  // Initialize Autobase with Corestore
  // First param: Namespaced corestore
  // Second param: bootstrap key (null for new, or existing autobase key)
  // Third param: View instance with open/apply/close methods
  // Note: No valueEncoding - handle JSON manually
  autobase = new Autobase(store.namespace(namespace), null, new WalletView())

  await autobase.ready()

  // Add existing devices as writers by appending operations
  // Autobase will process these operations and add the writers automatically
  const devices = loadDevices()
  for (const device of devices) {
    // Skip if this is the local writer (already added automatically)
    if (!b4a.equals(device.key, autobase.local.key)) {
      // Append addWriter operation - Autobase processes this internally
      const op = { addWriter: b4a.toString(device.key, 'hex') }
      await autobase.append(JSON.stringify(op))
    }
  }

  // Wait for operations to be processed
  if (devices.length > 1) {
    await autobase.update()
  }

  // Get view as Hyperbee (already created by open())
  txHistoryDb = autobase.view

  // Initialize Hyperswarm for P2P
  swarm = new Hyperswarm()

  // Track connections (Phase 6.7: Auto-replication enabled)
  swarm.on('connection', (conn, info) => {
    connections.add(conn)
    console.log(`📡 Connected to peer (${connections.size} connection${connections.size !== 1 ? 's' : ''})`)

    // Auto-replicate on all connections (Phase 6.7)
    // This enables real-time multi-device sync
    store.replicate(conn)

    // Handle disconnection
    conn.on('close', () => {
      connections.delete(conn)
      console.log(`📡 Peer disconnected (${connections.size} connection${connections.size !== 1 ? 's' : ''})`)
    })
  })

  // Join discovery key for this wallet
  const discoveryKey = crypto.discoveryKey(identityKeyPair.publicKey)
  const discovery = swarm.join(discoveryKey, { server: true, client: true })

  // Flush in background - don't block initialization on DHT announcement
  discovery.flushed().then(() => {
    console.log('📡 DHT discovery announced')
  }).catch(err => {
    console.warn('⚠️  DHT discovery flush failed:', err.message)
  })

  console.log('✅ Storage initialized successfully')
  return { store, autobase, txHistoryDb, swarm }
}

/**
 * Store a transaction in Autobase
 * Operations are appended to local input core and merged by Autobase
 */
async function storeTx(tx) {
  if (!autobase) {
    throw new Error('Storage not initialized')
  }

  // Append operation to Autobase
  // Note: No valueEncoding in Autobase - must JSON.stringify manually
  const op = {
    type: 'put-tx',
    key: `tx:${tx.timestamp}:${tx.hash}`,
    value: {
      hash: tx.hash,
      chain: tx.chain,
      to: tx.to,
      amount: tx.amount,
      fee: tx.fee,
      timestamp: tx.timestamp,
      status: tx.status || 'pending'
    }
  }

  await autobase.append(JSON.stringify(op))

  // Wait for view to update
  await new Promise(resolve => setTimeout(resolve, 100))
}

/**
 * Get transaction history from Hyperbee view
 */
async function getTxHistory(chain = null) {
  if (!txHistoryDb) {
    throw new Error('Storage not initialized')
  }

  const transactions = []

  for await (const entry of txHistoryDb.createReadStream()) {
    // Parse JSON manually (binary encoding)
    const tx = JSON.parse(entry.value)

    if (chain && tx.chain !== chain) {
      continue
    }

    transactions.push(tx)
  }

  // Sort by timestamp (newest first)
  transactions.sort((a, b) => b.timestamp - a.timestamp)

  return transactions
}

/**
 * Get specific transaction by hash
 */
async function getTx(hash) {
  if (!txHistoryDb) {
    throw new Error('Storage not initialized')
  }

  for await (const entry of txHistoryDb.createReadStream()) {
    // Parse JSON manually (binary encoding)
    const tx = JSON.parse(entry.value)
    if (tx.hash === hash) {
      return tx
    }
  }

  return null
}

/**
 * Update transaction status
 */
async function updateTxStatus(hash, status) {
  const tx = await getTx(hash)
  if (!tx) return

  tx.status = status
  await storeTx(tx)
}

// ============================================================================
// WDK WALLET MANAGEMENT (Phase 6.2)
// ============================================================================

/**
 * Initialize WDK wallets singleton
 * @param {Object} config - Configuration { testnet: boolean }
 * @returns {Object} - { wdk } - WDK instance with registered wallets
 */
async function initializeWallets(config = { testnet: true }) {
  if (wdkInstance) {
    return { wdk: wdkInstance }
  }

  const walletMnemonic = loadWalletMnemonic()
  if (!walletMnemonic) {
    throw new Error('Wallet mnemonic not found. Run: bare hyper-wallet-autobase.cjs init')
  }

  const { wdk } = await initializeWDK(walletMnemonic, config)

  wdkInstance = wdk
  return { wdk: wdkInstance }
}

/**
 * Store address in Autobase
 * @param {Object} addressData - { chain, network, address, path, index }
 */
async function storeAddress(addressData) {
  if (!autobase) {
    throw new Error('Storage not initialized')
  }

  const op = {
    type: 'put-address',
    key: `address:${addressData.chain}:${addressData.index}`,
    value: {
      chain: addressData.chain,
      network: addressData.network,
      address: addressData.address,
      path: addressData.path,
      index: addressData.index,
      label: addressData.label || '',  // Pet-name/label for address
      createdAt: addressData.createdAt || Date.now(),
      device: getCurrentDevice()?.name || 'unknown',
      isArchived: addressData.isArchived || false,  // For hiding old addresses
      isCurrent: addressData.isCurrent || false  // Mark current receiving address
    }
  }

  await autobase.append(JSON.stringify(op))

  // Wait for view to update
  await new Promise(resolve => setTimeout(resolve, 100))
}

/**
 * Get all addresses for a chain
 * @param {string} chain - 'bitcoin' or 'ethereum' or 'all'
 * @returns {Array} - Array of address objects sorted by index
 */
async function getAddresses(chain) {
  if (!txHistoryDb) {
    throw new Error('Storage not initialized')
  }

  const addresses = []
  const prefix = chain === 'all' ? 'address:' : `address:${chain}:`

  for await (const entry of txHistoryDb.createReadStream()) {
    if (entry.key.startsWith(prefix)) {
      addresses.push(JSON.parse(entry.value))
    }
  }

  // For Ethereum, also include ERC-20 token addresses (they share the same address space)
  // When querying 'testnet-ethereum', also return 'testnet-ethereum-usdt' addresses
  if (!chain.includes('all') && (chain.includes('ethereum') && !chain.includes('-usdt'))) {
    const tokenPrefix = `${prefix}usdt:`
    for await (const entry of txHistoryDb.createReadStream()) {
      if (entry.key.startsWith(tokenPrefix)) {
        const addr = JSON.parse(entry.value)
        // Check if we already have this index (don't duplicate)
        if (!addresses.find(a => a.index === addr.index)) {
          addresses.push(addr)
        }
      }
    }
  }

  return addresses.sort((a, b) => a.index - b.index)
}

/**
 * Get next address index for a chain
 * @param {string} chain - 'bitcoin' or 'ethereum'
 * @returns {number} - Next index to use (0 if no addresses exist)
 */
async function getNextAddressIndex(chain) {
  const addresses = await getAddresses(chain)
  if (addresses.length === 0) return 0
  return Math.max(...addresses.map(a => a.index)) + 1
}

/**
 * Get current (latest) address for a chain
 * @param {string} chain - 'bitcoin' or 'ethereum'
 * @returns {Object|null} - Latest address or null if none exist
 */
async function getCurrentAddress(chain) {
  // Strip network prefix (testnet-ethereum-usdt -> ethereum-usdt)
  // Addresses are stored without network prefix
  const chainWithoutNetwork = chain.replace(/^(testnet|mainnet)-/, '')

  let addresses = await getAddresses(chainWithoutNetwork)

  // For ERC-20 tokens (like USDT), fall back to base Ethereum addresses
  // since they share the same address space
  if (addresses.length === 0 && chainWithoutNetwork.includes('-usdt')) {
    const baseChain = chainWithoutNetwork.replace('-usdt', '')
    addresses = await getAddresses(baseChain)
  }

  if (addresses.length === 0) return null

  // Find address marked as current, or return latest
  const current = addresses.find(a => a.isCurrent)
  return current || addresses[addresses.length - 1]
}

/**
 * Update address label
 * @param {string} chain - 'bitcoin' or 'ethereum'
 * @param {number} index - Address index
 * @param {string} label - New label/pet-name
 */
async function updateAddressLabel(chain, index, label) {
  if (!autobase) {
    throw new Error('Storage not initialized')
  }

  // Get existing address
  const addresses = await getAddresses(chain)
  const existing = addresses.find(a => a.index === index)
  if (!existing) {
    throw new Error(`Address not found: ${chain}:${index}`)
  }

  // Update with new label
  await storeAddress({
    ...existing,
    label
  })

  return { ...existing, label }
}

/**
 * Set address as current receiving address
 * @param {string} chain - 'bitcoin' or 'ethereum'
 * @param {number} index - Address index to set as current
 */
async function setCurrentAddress(chain, index) {
  if (!autobase) {
    throw new Error('Storage not initialized')
  }

  let addresses = await getAddresses(chain)

  // For ERC-20 tokens (like USDT), fall back to base Ethereum addresses
  // since they share the same address space
  let actualChain = chain
  if (addresses.length === 0 && chain.includes('-usdt')) {
    const baseChain = chain.replace('-usdt', '')
    addresses = await getAddresses(baseChain)
    actualChain = baseChain
  }

  // Find the address to set as current
  const targetAddr = addresses.find(a => a.index === index)
  if (!targetAddr) {
    throw new Error(`Address not found: ${actualChain}:${index}`)
  }

  // Update all addresses: unset current on others, set on target
  for (const addr of addresses) {
    if (addr.index === index) {
      await storeAddress({ ...addr, chain: actualChain, isCurrent: true })
    } else if (addr.isCurrent) {
      await storeAddress({ ...addr, chain: actualChain, isCurrent: false })
    }
  }

  return { ...targetAddr, chain: actualChain, isCurrent: true }
}

/**
 * Archive address (hide from UI)
 * @param {string} chain - 'bitcoin' or 'ethereum'
 * @param {number} index - Address index
 */
async function archiveAddress(chain, index) {
  if (!autobase) {
    throw new Error('Storage not initialized')
  }

  const addresses = await getAddresses(chain)
  const existing = addresses.find(a => a.index === index)
  if (!existing) {
    throw new Error(`Address not found: ${chain}:${index}`)
  }

  await storeAddress({
    ...existing,
    isArchived: true
  })

  return { ...existing, isArchived: true }
}

// ============================================================================
// UTXO MANAGEMENT (Phase 6.3)
// ============================================================================

/**
 * Store UTXO in Autobase
 * @param {Object} utxoData - { chain, txid, vout, address, amount, ... }
 */
async function storeUTXO(utxoData) {
  if (!autobase) {
    throw new Error('Storage not initialized')
  }

  const op = {
    type: 'put-utxo',
    key: `utxo:${utxoData.chain}:${utxoData.txid}:${utxoData.vout}`,
    value: utxoData
  }

  await autobase.append(JSON.stringify(op))

  // Wait for view to update
  await new Promise(resolve => setTimeout(resolve, 100))
}

/**
 * Get all UTXOs for a chain
 * @param {string} chain - 'bitcoin' or 'ethereum' or 'all'
 * @returns {Array} - Array of UTXO objects
 */
async function getUTXOs(chain) {
  if (!txHistoryDb) {
    throw new Error('Storage not initialized')
  }

  const utxos = []
  const prefix = chain === 'all' ? 'utxo:' : `utxo:${chain}:`

  for await (const entry of txHistoryDb.createReadStream()) {
    if (entry.key.startsWith(prefix)) {
      utxos.push(JSON.parse(entry.value))
    }
  }

  return utxos
}

/**
 * Update UTXO (e.g., mark as spent)
 * @param {string} chain - Chain identifier
 * @param {string} txid - Transaction ID
 * @param {number} vout - Output index
 * @param {Object} updates - Fields to update
 */
async function updateUTXO(chain, txid, vout, updates) {
  if (!autobase) {
    throw new Error('Storage not initialized')
  }

  // Get existing UTXO
  const key = `utxo:${chain}:${txid}:${vout}`
  const existing = await txHistoryDb.get(key)

  if (!existing) {
    throw new Error(`UTXO not found: ${key}`)
  }

  const utxoData = JSON.parse(existing.value)

  // Merge updates
  const updated = {
    ...utxoData,
    ...updates
  }

  const op = {
    type: 'put-utxo',
    key,
    value: updated
  }

  await autobase.append(JSON.stringify(op))

  // Wait for view to update
  await new Promise(resolve => setTimeout(resolve, 100))
}

/**
 * Delete UTXO from storage
 * @param {string} chain - Chain identifier
 * @param {string} txid - Transaction ID
 * @param {number} vout - Output index
 */
async function deleteUTXO(chain, txid, vout) {
  if (!autobase) {
    throw new Error('Storage not initialized')
  }

  const op = {
    type: 'del-utxo',
    key: `utxo:${chain}:${txid}:${vout}`
  }

  await autobase.append(JSON.stringify(op))

  // Wait for view to update
  await new Promise(resolve => setTimeout(resolve, 100))
}

// ============================================================================
// TRANSACTION STORAGE (Phase 6.6)
// ============================================================================

/**
 * Store a transaction in Autobase
 * @param {Object} tx - Transaction details
 * @param {string} tx.hash - Transaction ID
 * @param {string} tx.chain - Chain (bitcoin, ethereum, etc)
 * @param {string} tx.direction - Direction (sent, received)
 * @param {string} tx.from - From address
 * @param {string} tx.to - To address
 * @param {bigint|number} tx.amount - Amount in smallest unit
 * @param {bigint|number} tx.fee - Fee in smallest unit
 * @param {string} tx.status - Status (pending, confirmed, failed)
 * @param {number} tx.timestamp - Unix timestamp
 * @param {number} tx.blockHeight - Block height (if confirmed)
 */
async function storeTransaction(tx) {
  if (!autobase) {
    throw new Error('Storage not initialized')
  }

  const transaction = {
    hash: tx.hash,
    chain: tx.chain,
    network: tx.network || 'testnet', // Add network field (testnet/mainnet)
    direction: tx.direction,
    from: tx.from,
    to: tx.to,
    amount: typeof tx.amount === 'string' ? tx.amount : String(tx.amount), // Ensure string
    fee: typeof tx.fee === 'string' ? tx.fee : String(tx.fee || 0),
    status: tx.status || 'pending',
    timestamp: tx.timestamp || Date.now(),
    blockHeight: tx.blockHeight || null
  }

  const op = {
    type: 'store-transaction',
    key: `tx:${tx.chain}:${tx.hash}`,
    value: transaction
  }

  await autobase.append(JSON.stringify(op))

  // Wait for view to update
  await new Promise(resolve => setTimeout(resolve, 100))
}

/**
 * Get all transactions for a chain
 * @param {string} chain - Chain name (bitcoin, ethereum, all)
 * @returns {Promise<Array>} - Array of transactions sorted by timestamp (newest first)
 */
async function getTransactions(chain = 'all') {
  if (!txHistoryDb) {
    throw new Error('Storage not initialized')
  }

  const transactions = []
  const prefix = chain === 'all' ? 'tx:' : `tx:${chain}:`

  for await (const data of txHistoryDb.createReadStream({ gte: prefix, lt: prefix + '\xff' })) {
    try {
      const tx = JSON.parse(data.value)
      // Convert string amounts back to numbers for display
      tx.amount = BigInt(tx.amount)
      tx.fee = tx.fee ? BigInt(tx.fee) : 0n
      transactions.push(tx)
    } catch (err) {
      console.error('Error parsing transaction:', err.message)
    }
  }

  // Sort by timestamp, newest first
  transactions.sort((a, b) => b.timestamp - a.timestamp)

  return transactions
}

/**
 * Get a specific transaction by hash
 * @param {string} chain - Chain name
 * @param {string} hash - Transaction hash
 * @returns {Promise<Object|null>} - Transaction object or null
 */
async function getTransaction(chain, hash) {
  if (!txHistoryDb) {
    throw new Error('Storage not initialized')
  }

  try {
    const data = await txHistoryDb.get(`tx:${chain}:${hash}`)
    if (!data) return null
    const tx = JSON.parse(data.value)

    // Handle case where tx or its fields might be null/undefined
    if (!tx) {
      return null
    }

    // Safely convert amount and fee to BigInt
    tx.amount = tx.amount ? BigInt(tx.amount) : 0n
    tx.fee = tx.fee ? BigInt(tx.fee) : 0n
    return tx
  } catch (err) {
    if (err.code === 'LEVEL_NOT_FOUND') {
      return null
    }
    // Handle corrupted/malformed transaction data - treat as not found
    // The corrupted entry will be overwritten by the new correct entry
    if (err instanceof SyntaxError) {
      return null
    }
    throw err
  }
}

/**
 * Delete transaction (for repair/cleanup)
 * @param {string} chain - Chain name
 * @param {string} hash - Transaction hash
 */
async function deleteTransaction(chain, hash) {
  if (!autobase) {
    throw new Error('Storage not initialized')
  }

  const op = {
    type: 'delete-transaction',
    key: `tx:${chain}:${hash}`
  }

  await autobase.append(JSON.stringify(op))

  // Wait longer for view to update and propagate delete
  await new Promise(resolve => setTimeout(resolve, 500))
}

/**
 * Update transaction status (Phase 6.7)
 * @param {string} chain - Chain name
 * @param {string} hash - Transaction hash
 * @param {Object} updates - Fields to update (status, blockHeight, confirmations)
 */
async function updateTransactionStatus(chain, hash, updates) {
  if (!autobase) {
    throw new Error('Storage not initialized')
  }

  // Get existing transaction
  const existing = await getTransaction(chain, hash)
  if (!existing) {
    throw new Error('Transaction not found')
  }

  // Merge updates, ensuring BigInt values are converted to strings
  const updated = {
    hash: existing.hash,
    chain: existing.chain,
    direction: existing.direction,
    from: existing.from,
    to: existing.to,
    amount: existing.amount.toString(),
    fee: existing.fee ? existing.fee.toString() : '0',
    status: updates.status !== undefined ? updates.status : existing.status,
    timestamp: existing.timestamp,
    blockHeight: updates.blockHeight !== undefined ? updates.blockHeight : existing.blockHeight
  }

  const op = {
    type: 'store-transaction',
    key: `tx:${chain}:${hash}`,
    value: updated
  }

  await autobase.append(JSON.stringify(op))

  // Wait for view to update
  await new Promise(resolve => setTimeout(resolve, 100))
}

/**
 * Sync transactions from WDK to local Hyperbee storage
 * Fetches transaction history using Electrum for Bitcoin and provider for Ethereum
 * @param {Object} wdk - WDK instance
 * @param {Object} config - Configuration with chains to sync
 * @returns {Promise<Object>} - Sync results { synced, errors }
 */
async function syncTransactionsFromWDK(wdk, config = {}) {
  if (!wdk || !txHistoryDb) {
    throw new Error('WDK or storage not initialized')
  }

  const chains = config.chains || ['bitcoin', 'ethereum']
  const results = {
    synced: 0,
    errors: [],
    chains: {}
  }

  for (const chain of chains) {
    try {
      // Determine base chain for WDK (ethereum-usdt -> ethereum)
      const baseChain = chain.replace(/-usdt$/, '')

      // Use explicit account index if provided, otherwise default to 0
      // WDK design: Application must explicitly specify which account to query
      const accountIndex = config.accountIndex !== undefined ? config.accountIndex : 0

      // Get WDK account for this chain at the correct index
      const account = await wdk.getAccount(baseChain, accountIndex)
      if (!account) {
        results.errors.push(`No account found for chain: ${chain}`)
        continue
      }

      // Get current address
      const currentAddress = await account.getAddress()

      let chainSynced = 0
      let chainSkipped = 0
      let transactions = []

      // Chain-specific transaction fetching
      if (chain === 'bitcoin') {
        // Bitcoin: Use public WDK getTransfers() API (documented in wdk-wallet-btc)
        try {
          const transfers = await account.getTransfers({
            direction: 'all',
            limit: 100
          })

          for (const transfer of transfers) {
            const existing = await getTransaction(chain, transfer.txid)
            if (existing) {
              chainSkipped++
              continue
            }

            const tx = {
              hash: transfer.txid,
              chain,
              network: config.network || 'testnet',
              direction: transfer.direction,  // 'incoming' or 'outgoing'
              from: transfer.direction === 'incoming' ? '(external)' : currentAddress,
              to: transfer.direction === 'incoming' ? currentAddress : (transfer.recipient || '(unknown)'),
              amount: String(transfer.value),
              fee: transfer.fee ? String(transfer.fee) : '0',
              status: transfer.height > 0 ? 'confirmed' : 'pending',
              timestamp: Date.now(),
              blockHeight: transfer.height || null
            }

            await storeTransaction(tx)
            chainSynced++
            results.synced++
          }
        } catch (err) {
          console.error(`Bitcoin sync error: ${err.message}`)
          results.errors.push(`${chain}: ${err.message}`)
        }
      } else if (baseChain === 'ethereum') {
        // Ethereum (including USDT): Fetch transactions from blockchain provider
        try {
          const config = getConfig()
          const network = config.network || 'testnet'

          // Determine if this is a token (USDT or USDC) or native ETH
          const isUSDT = chain.includes('-usdt')
          const isUSDC = chain.includes('-usdc')
          const isToken = isUSDT || isUSDC
          let providerTxs = []
          let tokenSymbol = null

          if (isToken) {
            // Fetch ERC-20 token transactions
            console.log(`   📡 Fetching token transactions for ${chain}...`)
            console.log(`   📍 Address: ${currentAddress} (index ${accountIndex})`)

            // Token contract addresses
            let tokenContract
            if (isUSDT) {
              tokenSymbol = 'USDT'
              tokenContract = network === 'testnet'
                ? '0x7169D38820dfd117C3FA1f22a697dBA58d90BA06'  // Sepolia testnet USDT
                : '0xdAC17F958D2ee523a2206206994597C13D831ec7'  // Ethereum mainnet USDT
            } else if (isUSDC) {
              tokenSymbol = 'USDC'
              tokenContract = network === 'testnet'
                ? '0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238'  // Sepolia testnet USDC (Circle official)
                : '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'  // Ethereum mainnet USDC
            }
            console.log(`   🏦 ${tokenSymbol} Contract: ${tokenContract}`)

            if (!tokenContract) {
              throw new Error(`Token contract address not configured for ${chain} on ${network}`)
            }

            providerTxs = await fetchTokenTransactions(currentAddress, network, config, tokenContract)
            console.log(`   📊 API returned ${providerTxs.length} transactions`)
          } else {
            // Fetch native ETH transactions
            console.log(`   📡 Fetching ETH transactions for ${chain}...`)
            providerTxs = await fetchTransactions(baseChain, currentAddress, network, config)
          }

          for (const tx of providerTxs) {
            console.log(`   🔍 Checking tx: ${tx.hash.substring(0, 16)}...`)
            console.log(`      From: ${tx.from?.substring(0, 10)}...`)
            console.log(`      To: ${tx.to?.substring(0, 10)}...`)
            console.log(`      Value: ${tx.value}`)
            if (tx.tokenSymbol) {
              console.log(`      Token: ${tx.tokenSymbol}`)
            }

            // Check if already stored
            const existing = await getTransaction(chain, tx.hash)
            if (existing) {
              // Check if transaction data is corrupted (amount = 0 for token transfers)
              const isCorrupted = existing.amount === 0n && tx.value !== '0'
              if (isCorrupted) {
                console.log(`   🔧 REPAIRING corrupted tx: ${tx.hash.substring(0, 16)}... (amount was 0, should be ${tx.value})`)
                // Don't skip - will overwrite with correct data
              } else {
                console.log(`   ⏭️  SKIPPED (already exists): ${tx.hash.substring(0, 16)}...`)
                chainSkipped++
                continue
              }
            }

            // Determine direction
            const direction = tx.to.toLowerCase() === currentAddress.toLowerCase() ? 'received' : 'sent'
            console.log(`   💾 STORING (${direction}): ${tx.hash.substring(0, 16)}...`)

            // Store transaction
            const storedTx = {
              hash: tx.hash,
              chain,
              network,
              direction,
              from: tx.from,
              to: tx.to,
              amount: tx.value,
              fee: tx.fee,
              status: tx.status,
              timestamp: tx.timestamp * 1000, // Convert to milliseconds
              blockHeight: tx.blockNumber,
              // Add token info if this is a token transfer
              token: tokenSymbol || tx.tokenSymbol || null,
              decimals: isToken ? 6 : 18  // USDT/USDC have 6 decimals, ETH has 18
            }

            await storeTransaction(storedTx)
            chainSynced++
            results.synced++
            console.log(`   ✅ Stored successfully`)
          }
        } catch (err) {
          console.error(`Error fetching ${chain} transactions:`, err.message)
          results.errors.push(`${chain}: ${err.message}`)
        }
      }

      results.chains[chain] = {
        synced: chainSynced,
        skipped: chainSkipped
      }

    } catch (err) {
      console.error(`Error syncing ${chain} transactions:`, err.message)
      results.errors.push(`${chain}: ${err.message}`)
    }
  }

  return results
}

// ============================================================================
// DEVICE MANAGEMENT
// ============================================================================

/**
 * Load device list from file
 */
function loadDevices() {
  if (!fs.existsSync(DEVICES_FILE)) {
    return []
  }

  const data = fs.readFileSync(DEVICES_FILE, 'utf8')
  const devices = JSON.parse(data)

  // Convert hex strings back to Buffers
  return devices.map(d => ({
    ...d,
    key: b4a.from(d.key, 'hex'),
    proof: d.proof ? b4a.from(d.proof, 'hex') : null
  }))
}

/**
 * Save device list to file
 */
function saveDevices(devices) {
  const dir = path.dirname(DEVICES_FILE)
  if (!fs.existsSync(dir)) {
    fs.mkdirSync(dir, { recursive: true, mode: 0o700 })
  }

  // Convert Buffers to hex strings for JSON
  const serializable = devices.map(d => ({
    ...d,
    key: b4a.toString(d.key, 'hex'),
    proof: d.proof ? b4a.toString(d.proof, 'hex') : null
  }))

  fs.writeFileSync(DEVICES_FILE, JSON.stringify(serializable, null, 2), { mode: 0o600 })
}

// ============================================================================
// CONTACT MANAGEMENT
// ============================================================================

/**
 * Add a contact to the address book
 * @param {Object} contact - Contact details
 * @param {string} contact.address - Blockchain address
 * @param {string} contact.chain - Chain name (bitcoin, ethereum)
 * @param {string} contact.name - Display name
 * @param {string} [contact.notes] - Optional notes
 * @returns {Promise<Object>} - Created contact with ID
 */
async function addContact({ address, chain, name, notes = '' }) {
  if (!autobase) {
    throw new Error('Storage not initialized')
  }

  // Generate unique ID for contact
  const id = crypto.randomBytes(16).toString('hex')

  const contact = {
    id,
    address,
    chain,
    name,
    notes,
    addedAt: Date.now(),
    addedBy: getCurrentDevice()?.key ? b4a.toString(getCurrentDevice().key, 'hex').slice(0, 16) : 'unknown',
    lastUsedAt: null,
    metadata: {
      favorite: false,
      category: null
    }
  }

  const op = {
    type: 'put-contact',
    key: `contact:${id}`,
    value: contact
  }

  await autobase.append(JSON.stringify(op))

  // Wait for view to update
  await new Promise(resolve => setTimeout(resolve, 100))

  return contact
}

/**
 * Get all contacts, optionally filtered by chain
 * @param {string} [chain] - Chain name to filter by (optional)
 * @returns {Promise<Array>} - Array of contacts sorted by name
 */
/**
 * Migrate a contact to new multi-address format with IDs
 * @param {Object} contact - Contact to migrate
 * @returns {Object} - Migrated contact
 */
function migrateContactFormat(contact) {
  // Already in new format with proper IDs - check if ALL addresses have IDs
  if (contact.addresses && Array.isArray(contact.addresses)) {
    // Check if any address is missing an ID
    const needsMigration = contact.addresses.some(addr => !addr.id)

    if (!needsMigration) {
      // All addresses have IDs, no migration needed
      return contact
    }

    // Some addresses missing IDs - add them
    const migratedAddresses = contact.addresses.map(addr => {
      if (!addr.id) {
        return {
          ...addr,
          id: crypto.randomBytes(8).toString('hex'),
          network: addr.network || getConfig().network || 'testnet',
          label: addr.label || '',
          addedAt: addr.addedAt || Date.now()
        }
      }
      return addr
    })

    return {
      ...contact,
      addresses: migratedAddresses
    }
  }

  // Old single-address format - migrate to multi-address
  if (contact.address && contact.chain) {
    const config = getConfig()
    const defaultNetwork = config.network || 'testnet'

    return {
      ...contact,
      addresses: [{
        id: crypto.randomBytes(8).toString('hex'),
        chain: contact.chain,
        address: contact.address,
        network: contact.network || defaultNetwork,
        label: '',
        addedAt: contact.addedAt || Date.now()
      }],
      // Remove old fields
      address: undefined,
      chain: undefined
    }
  }

  // Contact with no addresses - return as is
  return contact
}

async function getContacts(chain = null) {
  if (!txHistoryDb) {
    throw new Error('Storage not initialized')
  }

  const contacts = []
  const prefix = 'contact:'

  for await (const data of txHistoryDb.createReadStream({ gte: prefix, lt: prefix + '\xff' })) {
    try {
      let contact = JSON.parse(data.value)

      // Migrate contact format if needed
      contact = migrateContactFormat(contact)

      // Filter by chain if specified (check both old and new format)
      if (chain) {
        // New format: check if any address matches chain
        if (contact.addresses && Array.isArray(contact.addresses)) {
          const hasChain = contact.addresses.some(addr => addr.chain === chain)
          if (!hasChain) continue
        } else if (contact.chain && contact.chain !== chain) {
          // Old format fallback
          continue
        }
      }

      contacts.push(contact)
    } catch (err) {
      console.error('Error parsing contact:', err.message)
    }
  }

  // Sort by name (case-insensitive)
  contacts.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()))

  return contacts
}

/**
 * Find a contact by name or address
 * @param {string} nameOrAddress - Contact name or blockchain address
 * @param {string} [chain] - Chain to search (optional, makes search faster)
 * @returns {Promise<Object|null>} - Contact object or null
 */
async function findContact(nameOrAddress, chain = null) {
  const contacts = await getContacts(chain)

  // Try exact name match first (case-insensitive)
  const byName = contacts.find(c =>
    c.name.toLowerCase() === nameOrAddress.toLowerCase()
  )
  if (byName) return byName

  // Try exact address match (check in addresses array)
  const byAddress = contacts.find(c => {
    if (c.addresses && Array.isArray(c.addresses)) {
      return c.addresses.some(addr =>
        addr.address.toLowerCase() === nameOrAddress.toLowerCase()
      )
    }
    return false
  })
  if (byAddress) return byAddress

  // Try partial name match
  const byPartial = contacts.find(c =>
    c.name.toLowerCase().includes(nameOrAddress.toLowerCase())
  )

  return byPartial || null
}

/**
 * Get a contact by ID
 * @param {string} id - Contact ID
 * @returns {Promise<Object|null>} - Contact object or null
 */
async function getContactById(id) {
  if (!txHistoryDb) {
    throw new Error('Storage not initialized')
  }

  try {
    const data = await txHistoryDb.get(`contact:${id}`)
    if (!data) return null

    let contact = JSON.parse(data.value)

    // Migrate contact format if needed
    contact = migrateContactFormat(contact)

    return contact
  } catch (err) {
    if (err.code === 'LEVEL_NOT_FOUND') {
      return null
    }
    throw err
  }
}

/**
 * Update a contact
 * @param {string} id - Contact ID
 * @param {Object} updates - Fields to update
 * @returns {Promise<Object>} - Updated contact
 */
async function updateContact(id, updates) {
  if (!autobase) {
    throw new Error('Storage not initialized')
  }

  const existing = await getContactById(id)
  if (!existing) {
    throw new Error('Contact not found')
  }

  const updated = {
    ...existing,
    ...updates,
    id: existing.id, // Preserve ID
    addedAt: existing.addedAt, // Preserve creation time
    modifiedAt: Date.now()
  }

  const op = {
    type: 'put-contact',
    key: `contact:${id}`,
    value: updated
  }

  await autobase.append(JSON.stringify(op))

  // Wait for view to update
  await new Promise(resolve => setTimeout(resolve, 100))

  return updated
}

/**
 * Delete a contact
 * @param {string} id - Contact ID
 */
async function deleteContact(id) {
  if (!autobase) {
    throw new Error('Storage not initialized')
  }

  const op = {
    type: 'del-contact',
    key: `contact:${id}`
  }

  await autobase.append(JSON.stringify(op))

  // Wait for view to update
  await new Promise(resolve => setTimeout(resolve, 100))
}

/**
 * Find contact by address (helper for transaction display)
 * UPDATED: Supports both old (single address) and new (multiple addresses) format
 * @param {string} address - Blockchain address
 * @param {string} chain - Chain name
 * @returns {Promise<Object|null>} - Contact or null
 */
async function findContactByAddress(address, chain) {
  const contacts = await getContacts()

  return contacts.find(c => {
    // Old format: contact.address
    if (c.address && c.address.toLowerCase() === address.toLowerCase() && c.chain === chain) {
      return true
    }

    // New format: contact.addresses[]
    if (c.addresses && Array.isArray(c.addresses)) {
      return c.addresses.some(addr =>
        addr.chain === chain && addr.address.toLowerCase() === address.toLowerCase()
      )
    }

    return false
  }) || null
}

/**
 * Add an address to an existing contact (supports multiple addresses per chain)
 * @param {string} contactId - Contact ID
 * @param {Object} addressInfo - Address details
 * @param {string} addressInfo.chain - Chain name (bitcoin, ethereum, polygon, etc.)
 * @param {string} addressInfo.address - Blockchain address
 * @param {string} addressInfo.network - Network (testnet, mainnet, sepolia, etc.)
 * @param {string} [addressInfo.label] - Optional label (e.g., "Hardware wallet", "Hot wallet")
 * @returns {Promise<Object>} - Updated contact
 */
async function addAddressToContact(contactId, { chain, address, network, label = '' }) {
  if (!autobase) {
    throw new Error('Storage not initialized')
  }

  const existing = await getContactById(contactId)
  if (!existing) {
    throw new Error('Contact not found')
  }

  // Migrate old format to new format if needed
  let addresses = []
  if (existing.addresses && Array.isArray(existing.addresses)) {
    addresses = existing.addresses
  } else if (existing.address && existing.chain) {
    // Convert old single-address format (add ID to migrated address)
    addresses = [{
      id: crypto.randomBytes(8).toString('hex'),
      chain: existing.chain,
      address: existing.address,
      network: existing.network || network,
      label: ''
    }]
  }

  // Check if EXACT same address already exists (same address + same chain)
  const exactMatch = addresses.find(a =>
    a.chain === chain && a.address.toLowerCase() === address.toLowerCase()
  )
  if (exactMatch) {
    throw new Error(`This exact address already exists for ${chain}`)
  }

  // Generate unique ID for this address
  const addressId = crypto.randomBytes(8).toString('hex')

  // Add new address with unique ID
  addresses.push({
    id: addressId,
    chain,
    address,
    network,
    label: label || '',
    addedAt: Date.now()
  })

  // Update contact with new addresses array
  const updated = {
    ...existing,
    addresses,
    // Remove old format fields
    address: undefined,
    chain: undefined,
    modifiedAt: Date.now()
  }

  const op = {
    type: 'put-contact',
    key: `contact:${contactId}`,
    value: updated
  }

  await autobase.append(JSON.stringify(op))

  // Wait for view to update
  await new Promise(resolve => setTimeout(resolve, 100))

  return updated
}

/**
 * Remove an address from a contact by address ID
 * @param {string} contactId - Contact ID
 * @param {string} addressId - Address ID to remove
 * @returns {Promise<Object>} - Updated contact
 */
async function removeAddressFromContact(contactId, addressId) {
  if (!autobase) {
    throw new Error('Storage not initialized')
  }

  const existing = await getContactById(contactId)
  if (!existing) {
    throw new Error('Contact not found')
  }

  // Migrate old format to new format if needed
  let addresses = []
  if (!existing.addresses || !Array.isArray(existing.addresses)) {
    // Old format: single address
    if (existing.address && existing.chain) {
      // Get default network from config for migration
      const config = getConfig()
      const defaultNetwork = config.network || 'testnet'

      addresses = [{
        id: crypto.randomBytes(8).toString('hex'),
        chain: existing.chain,
        address: existing.address,
        network: existing.network || defaultNetwork,
        label: '',
        addedAt: existing.addedAt || Date.now()
      }]
    } else {
      throw new Error('Contact has no addresses')
    }
  } else {
    addresses = existing.addresses
  }

  // Filter out the address with this ID
  const newAddresses = addresses.filter(a => a.id !== addressId)

  if (newAddresses.length === addresses.length) {
    throw new Error(`No address found with ID: ${addressId}`)
  }

  if (newAddresses.length === 0) {
    throw new Error('Cannot remove last address. Delete contact instead.')
  }

  const updated = {
    ...existing,
    addresses: newAddresses,
    // Remove old format fields
    address: undefined,
    chain: undefined,
    modifiedAt: Date.now()
  }

  const op = {
    type: 'put-contact',
    key: `contact:${contactId}`,
    value: updated
  }

  await autobase.append(JSON.stringify(op))

  // Wait for view to update
  await new Promise(resolve => setTimeout(resolve, 100))

  return updated
}

/**
 * Bootstrap initial device (this device)
 * Creates attestation proof for the first device
 */
async function bootstrapDevice(identity, deviceName) {
  if (!autobase) {
    throw new Error('Storage not initialized')
  }

  // With Autobase, the local writer is autobase.local
  const localWriter = autobase.local
  await localWriter.ready()

  // Create bootstrap proof (self-attestation)
  const proof = await identity.bootstrap(localWriter.key)

  const device = {
    name: deviceName,
    key: localWriter.key,
    identityKey: identityKeyPair.publicKey,  // Shared identity key
    proof: proof,
    addedAt: Date.now(),
    isLocal: true
  }

  const devices = loadDevices()

  // Check if already bootstrapped
  const existing = devices.find(d => b4a.equals(d.key, localWriter.key))
  if (existing) {
    return existing
  }

  devices.push(device)
  saveDevices(devices)

  return device
}

/**
 * Create invite for another device to join
 * Returns invite code that includes discovery key and autobase bootstrap key
 */
async function createInvite(identity) {
  if (!identityKeyPair) {
    throw new Error('Identity not loaded')
  }

  if (!autobase) {
    throw new Error('Storage not initialized')
  }

  const now = Date.now()
  const INVITE_EXPIRATION = 15 * 60 * 1000 // 15 minutes

  // Create invite data (without signature first)
  const inviteData = {
    discoveryKey: b4a.toString(crypto.discoveryKey(identityKeyPair.publicKey), 'hex'),
    identityKey: b4a.toString(identityKeyPair.publicKey, 'hex'),
    autobaseKey: b4a.toString(autobase.key, 'hex'),
    timestamp: now,
    expiresAt: now + INVITE_EXPIRATION
  }

  // Sign the invite data with identity secret key
  const message = b4a.from(JSON.stringify(inviteData))
  const signature = crypto.sign(message, identityKeyPair.secretKey)

  // Add signature to invite
  const signedInvite = {
    ...inviteData,
    signature: b4a.toString(signature, 'hex')
  }

  // Encode as base64
  const inviteStr = b4a.toString(b4a.from(JSON.stringify(signedInvite)), 'base64')

  console.log(`✅ Created signed invite (expires in 15 minutes)`)
  return inviteStr
}

/**
 * Join wallet using invite code
 * Receives mnemonic from primary device over secure Protomux channel
 * Initializes Autobase with bootstrap key from invite
 *
 * NOTE: This reinitializes storage! Close existing storage first.
 * @param {string} inviteCode - Invite code from primary device
 * @param {string} deviceName - Name for this device
 * @returns {Object} { device, mnemonic } - Device info and received mnemonic
 */
async function joinWallet(inviteCode, deviceName) {
  // Decode invite
  let invite
  try {
    const inviteJson = b4a.toString(b4a.from(inviteCode, 'base64'))
    invite = JSON.parse(inviteJson)
  } catch (err) {
    throw new Error('Invalid invite code format')
  }

  // Verify invite has required fields
  if (!invite.discoveryKey || !invite.identityKey || !invite.autobaseKey ||
      !invite.timestamp || !invite.expiresAt || !invite.signature) {
    throw new Error('Invalid invite: missing required fields')
  }

  // Check expiration (15 minutes)
  const now = Date.now()
  if (now > invite.expiresAt) {
    const expirationAge = Math.floor((now - invite.expiresAt) / 1000 / 60) // minutes ago
    throw new Error(`Invite code expired ${expirationAge} minutes ago. Request a new invite.`)
  }

  // Verify signature
  const { signature, ...inviteData } = invite
  const message = b4a.from(JSON.stringify(inviteData))
  const sig = b4a.from(signature, 'hex')
  const pubKey = b4a.from(invite.identityKey, 'hex')

  if (!crypto.verify(message, sig, pubKey)) {
    throw new Error('Invalid invite signature - invite may be forged or corrupted')
  }

  console.log('✅ Invite signature verified')
  console.log(`⏰ Invite expires in ${Math.floor((invite.expiresAt - now) / 1000 / 60)} minutes`)

  // Initialize Corestore for temporary writer key
  if (!store) {
    store = new Corestore(STORAGE_DIR)
    await store.ready()
  }

  // Get namespace
  const namespace = await Autobase.getLocalKey(store)

  // View class for Autobase
  class WalletView {
    open(store) {
      const core = store.get('view')
      return new Hyperbee(core, {
        keyEncoding: 'utf-8',
        valueEncoding: 'binary'
      })
    }

    async apply(nodes, view, base) {
      return apply(nodes, view, base)
    }

    close(view) {
      return view.close()
    }
  }

  // Initialize Autobase with BOOTSTRAP KEY from invite
  const bootstrapKey = b4a.from(invite.autobaseKey, 'hex')
  autobase = new Autobase(store.namespace(namespace), bootstrapKey, new WalletView())
  await autobase.ready()

  // Get our local writer key (to send to primary device)
  const localWriterKey = autobase.local.key

  // Get view
  txHistoryDb = autobase.view

  // Initialize Hyperswarm for pairing
  if (!swarm) {
    swarm = new Hyperswarm()
  }

  console.log('📡 Connecting to primary device...')

  // Set up connection handler BEFORE joining swarm (avoid race condition)
  const { conn, identityMnemonic, walletMnemonic } = await new Promise((resolve, reject) => {
    let timeout = null

    const handler = async (conn) => {
      try {
        if (timeout) clearTimeout(timeout)
        swarm.off('connection', handler)

        console.log('✅ Connected to primary device')
        console.log('🔐 Starting pairing protocol...')

        // Receive mnemonics via pairing protocol
        const { identityMnemonic, walletMnemonic } = await createPairingClient(conn, localWriterKey, deviceName)

        // Start replication AFTER pairing completes
        store.replicate(conn)

        resolve({ conn, identityMnemonic, walletMnemonic })
      } catch (err) {
        if (timeout) clearTimeout(timeout)
        reject(err)
      }
    }

    swarm.on('connection', handler)

    // Timeout after 60 seconds
    timeout = setTimeout(() => {
      swarm.off('connection', handler)
      reject(new Error('Connection timeout'))
    }, 60000)

    // Join discovery key AFTER setting up listener (avoid race condition)
    const discoveryKey = b4a.from(invite.discoveryKey, 'hex')
    swarm.join(discoveryKey, { server: true, client: true }).flushed().catch(reject)
  })

  console.log('✅ Received wallet mnemonics')
  console.log('')

  // Create identity from received identity mnemonic
  const identity = await IdentityKey.from({ mnemonic: identityMnemonic })
  identityKeyPair = identity.identityKeyPair

  // Create bootstrap proof for this device
  const proof = await identity.bootstrap(localWriterKey)

  // Save local device info
  const device = {
    name: deviceName,
    key: localWriterKey,
    publicKey: b4a.toString(identityKeyPair.publicKey, 'hex'),
    proof: proof,
    addedAt: Date.now(),
    isLocal: true
  }

  const devices = [device]
  saveDevices(devices)

  // NOTE: Don't try to add ourselves as a writer here - we can't append until
  // we're already a writer (chicken-egg problem). The primary device handles
  // adding our writer via the addDevice function during pairing.

  return { device, identityMnemonic, walletMnemonic }
}

/**
 * Add another device's writer to Autobase
 * Called after attestation is verified
 */
async function addDevice(deviceKey, deviceName, proof) {
  const remoteWriter = store.get(deviceKey)
  await remoteWriter.ready()

  // Add to Autobase by appending addWriter operation
  const op = { addWriter: b4a.toString(deviceKey, 'hex') }
  await autobase.append(JSON.stringify(op))

  // Save to device list
  const devices = loadDevices()
  devices.push({
    name: deviceName,
    key: deviceKey,
    proof: proof,
    addedAt: Date.now(),
    isLocal: false
  })
  saveDevices(devices)
}

/**
 * Get current device info
 */
function getCurrentDevice() {
  if (!autobase) return null

  const devices = loadDevices()
  return devices.find(d => d.isLocal && b4a.equals(d.key, autobase.local.key))
}

/**
 * List all devices
 */
function listDevices() {
  return loadDevices()
}

/**
 * Remove device (revoke access)
 * Note: Autobase doesn't have removeInput, so we append a removeWriter operation
 */
async function removeDevice(deviceKey) {
  if (!autobase) {
    throw new Error('Storage not initialized')
  }

  let devices = loadDevices()

  const device = devices.find(d => b4a.equals(d.key, deviceKey))
  if (!device) {
    throw new Error('Device not found')
  }

  // Prevent removing local device
  if (device.isLocal) {
    throw new Error('Cannot remove local device')
  }

  // Append removeWriter operation - Autobase will process this
  // Note: Autobase doesn't have built-in removeInput, so we use custom operation
  const op = {
    removeWriter: b4a.toString(deviceKey, 'hex'),
    timestamp: Date.now()
  }
  await autobase.append(JSON.stringify(op))

  // Wait for operation to be processed
  await autobase.update()
  await new Promise(resolve => setTimeout(resolve, 500))

  // Remove from device list
  devices = devices.filter(d => !b4a.equals(d.key, deviceKey))
  saveDevices(devices)
}

/**
 * Deduplicate devices in the device list
 * Removes duplicate entries keeping only the most recent one per writer key
 */
function deduplicateDevices() {
  let devices = loadDevices()
  const seen = new Map()
  const deduplicated = []

  // Sort by addedAt (newest first)
  devices.sort((a, b) => b.addedAt - a.addedAt)

  for (const device of devices) {
    const writerKeyHex = b4a.toString(device.key, 'hex')

    if (!seen.has(writerKeyHex)) {
      seen.set(writerKeyHex, true)
      deduplicated.push(device)
    }
  }

  // Sort back by addedAt (oldest first) for consistent ordering
  deduplicated.sort((a, b) => a.addedAt - b.addedAt)

  const removed = devices.length - deduplicated.length

  if (removed > 0) {
    saveDevices(deduplicated)
  }

  return { removed, total: deduplicated.length }
}

/**
 * Get connection status
 */
function getConnectionStatus() {
  return {
    connected: connections.size > 0,
    count: connections.size,
    connections: Array.from(connections).map(conn => ({
      remotePublicKey: conn.remotePublicKey ? b4a.toString(conn.remotePublicKey, 'hex').slice(0, 16) + '...' : 'unknown'
    }))
  }
}

/**
 * Wait for incoming connections (for accepting join requests)
 * Returns when a new device connects
 * @param {number} timeout - Timeout in ms (Infinity = wait forever)
 */
async function waitForConnection(timeout = 30000) {
  return new Promise((resolve, reject) => {
    let timer = null

    // Only set timeout if not infinite
    if (timeout !== Infinity && timeout > 0) {
      timer = setTimeout(() => {
        swarm.off('connection', handler)
        reject(new Error('Timeout waiting for connection'))
      }, timeout)
    }

    const handler = (conn) => {
      if (timer) clearTimeout(timer)
      swarm.off('connection', handler)
      resolve(conn)
    }

    swarm.on('connection', handler)
  })
}

/**
 * Wait for a new device to connect and auto-approve it
 * Sends mnemonics to joining device over secure Protomux channel
 * @param {number} timeout - Timeout in ms (0 = infinite)
 * @param {string} identityMnemonic - Identity mnemonic to send to joining device
 * @param {string} walletMnemonic - Wallet mnemonic to send to joining device
 * @returns {Object} { writerKey, deviceName }
 */
async function waitForNewDevice(timeout = 120000, identityMnemonic = null, walletMnemonic = null) {
  if (!identityMnemonic || !walletMnemonic) {
    throw new Error('Both identity and wallet mnemonics required to pair new device')
  }

  // Get current device info
  const currentDevice = getCurrentDevice()
  const myDeviceName = currentDevice ? currentDevice.name : 'Primary Device'

  // Wait for connection (infinite if timeout=0)
  const actualTimeout = timeout === 0 ? Infinity : timeout
  const conn = await waitForConnection(actualTimeout)

  console.log('✅ Device connected, exchanging pairing information...')

  // Start pairing server protocol - this will send BOTH mnemonics
  const { remoteDeviceName, remoteWriterKey } = await createPairingServer(
    conn,
    identityMnemonic,
    walletMnemonic,
    myDeviceName
  )

  console.log(`🔍 Received device info: ${remoteDeviceName}`)
  console.log(`   Writer: ${b4a.toString(remoteWriterKey, 'hex').slice(0, 16)}...`)
  console.log('')

  // Start replication AFTER pairing protocol completes
  store.replicate(conn)

  // Auto-approve the writer
  console.log('🔐 Auto-approving writer...')

  // Append addWriter operation
  const op = {
    addWriter: b4a.toString(remoteWriterKey, 'hex'),
    timestamp: Date.now()
  }
  await autobase.append(JSON.stringify(op))
  await autobase.update()

  // Wait for operation to be processed
  await new Promise(resolve => setTimeout(resolve, 1000))

  // Check for duplicate writer key (prevent re-adding same device)
  const devices = loadDevices()
  const writerKeyHex = b4a.toString(remoteWriterKey, 'hex')
  const existing = devices.find(d => b4a.toString(d.key, 'hex') === writerKeyHex)

  if (existing) {
    console.log(`⚠️ Device already exists: ${existing.name} (writer: ${writerKeyHex.slice(0, 16)}...)`)
    console.log('   Skipping duplicate entry')
    return { writerKey: remoteWriterKey, deviceName: remoteDeviceName, isDuplicate: true }
  }

  // Get identity key (shared across all devices using same identity mnemonic)
  const identity = await IdentityKey.from({ mnemonic: identityMnemonic })
  const identityKey = identity.identityKeyPair.publicKey

  // Save to device list with both keys
  devices.push({
    name: remoteDeviceName,
    key: remoteWriterKey,
    identityKey: identityKey,  // Shared identity key
    proof: null,
    addedAt: Date.now(),
    isLocal: false
  })
  saveDevices(devices)

  return { writerKey: remoteWriterKey, deviceName: remoteDeviceName, isDuplicate: false }
}

/**
 * Save pending writer to file with attestation proof
 * @param {Buffer} writerKey - The writer key
 * @param {Buffer} proof - Optional attestation proof
 */
function savePendingWriter(writerKey, proof = null) {
  const dir = path.dirname(PENDING_FILE)
  if (!fs.existsSync(dir)) {
    fs.mkdirSync(dir, { recursive: true, mode: 0o700 })
  }

  const pending = loadPendingWriters()
  const keyHex = b4a.toString(writerKey, 'hex')

  // Check if already exists
  const existing = pending.find(p => p.key === keyHex)
  if (!existing) {
    pending.push({
      key: keyHex,
      proof: proof ? b4a.toString(proof, 'hex') : null,
      addedAt: Date.now()
    })
  }

  fs.writeFileSync(PENDING_FILE, JSON.stringify(pending, null, 2), { mode: 0o600 })
}

/**
 * Load pending writers from file
 * Returns array of {key, proof, addedAt} objects
 */
function loadPendingWriters() {
  if (!fs.existsSync(PENDING_FILE)) {
    return []
  }

  const data = fs.readFileSync(PENDING_FILE, 'utf8')
  return JSON.parse(data)
}

/**
 * Remove a pending writer from the file
 */
function removePendingWriter(writerKey) {
  const pending = loadPendingWriters()
  const keyHex = b4a.toString(writerKey, 'hex')
  const filtered = pending.filter(p => p.key !== keyHex)

  if (filtered.length === 0) {
    // Remove file if empty
    if (fs.existsSync(PENDING_FILE)) {
      fs.unlinkSync(PENDING_FILE)
    }
  } else {
    fs.writeFileSync(PENDING_FILE, JSON.stringify(filtered, null, 2), { mode: 0o600 })
  }
}

/**
 * Get list of pending writers (connected but not approved)
 * Returns array of {key, proof} objects with Buffers
 */
function getPendingWriters() {
  const pendingData = loadPendingWriters()

  // Convert hex strings back to Buffers
  return pendingData.map(p => ({
    key: b4a.from(p.key, 'hex'),
    proof: p.proof ? b4a.from(p.proof, 'hex') : null,
    addedAt: p.addedAt
  }))
}

/**
 * Approve a pending writer (add to Autobase)
 * @param {Buffer} writerKey - The writer key to approve
 * @param {string} deviceName - Optional name for the device
 * @param {boolean} skipVerification - Skip attestation verification (for testing)
 */
async function approveWriter(writerKey, deviceName = 'Unknown Device', skipVerification = false) {
  if (!autobase) {
    throw new Error('Storage not initialized')
  }

  // Check if already added
  const devices = loadDevices()
  const existing = devices.find(d => b4a.equals(d.key, writerKey))
  if (existing) {
    throw new Error('Writer already approved')
  }

  console.log('🔍 Approving writer:', b4a.toString(writerKey, 'hex').slice(0, 16) + '...')

  // Get attestation proof from pending writers
  const pending = getPendingWriters()
  const pendingWriter = pending.find(p => b4a.equals(p.key, writerKey))

  let proof = null
  if (pendingWriter && pendingWriter.proof) {
    proof = pendingWriter.proof

    // Verify attestation proof
    if (!skipVerification) {
      console.log('🔐 Verifying attestation proof...')

      try {
        const verified = IdentityKey.verify(proof)

        if (!verified) {
          throw new Error('Attestation verification failed')
        }

        console.log('✅ Attestation verified')
      } catch (err) {
        console.log('❌ Attestation verification failed:', err.message)
        console.log('')
        console.log('⚠️  WARNING: Proof verification failed!')
        console.log('   This device may not be authorized.')
        console.log('')
        console.log('To approve anyway (UNSAFE), set skipVerification=true')
        console.log('')
        throw new Error('Attestation verification failed')
      }
    } else {
      console.log('⚠️  Skipping attestation verification (unsafe mode)')
    }
  } else {
    console.log('⚠️  No attestation proof found')
    console.log('   Continuing without verification (Phase 5.4 limitation)')
  }

  // Append addWriter operation - Autobase processes this internally
  const op = {
    addWriter: b4a.toString(writerKey, 'hex'),
    timestamp: Date.now()
  }
  await autobase.append(JSON.stringify(op))

  // Wait for operation to be processed
  await autobase.update()

  console.log('✅ Writer added to Autobase')

  // Wait for view to update
  await new Promise(resolve => setTimeout(resolve, 1000))

  // Save to device list
  devices.push({
    name: deviceName,
    key: writerKey,
    proof: proof,
    addedAt: Date.now(),
    isLocal: false
  })
  saveDevices(devices)

  // Remove from pending list
  removePendingWriter(writerKey)

  console.log('✅ Device approved:', deviceName)

  return writerKey
}

/**
 * Enable replication for device sync mode
 * Call this to start replicating on new connections
 */
function enableReplication() {
  if (!swarm || !store) {
    throw new Error('Storage not initialized')
  }

  // Add replication handler for new connections
  swarm.on('connection', (conn) => {
    store.replicate(conn)
  })

  // Replicate on existing connections
  for (const conn of connections) {
    store.replicate(conn)
  }
}

/**
 * Close storage and network
 */
async function closeStorage() {
  if (swarm) {
    await swarm.destroy()
    swarm = null
  }
  if (txHistoryDb) {
    await txHistoryDb.close()
    txHistoryDb = null
  }
  if (autobase) {
    await autobase.close()
    autobase = null
  }
  if (store) {
    await store.close()
    store = null
  }
  connections.clear()
}

module.exports = {
  initializeStorage,
  storeTx,
  getTxHistory,
  getTx,
  updateTxStatus,
  // WDK wallet functions (Phase 6.2)
  initializeWallets,
  storeAddress,
  getAddresses,
  getNextAddressIndex,
  getCurrentAddress,
  updateAddressLabel,
  setCurrentAddress,
  archiveAddress,
  // UTXO functions (Phase 6.3)
  storeUTXO,
  getUTXOs,
  updateUTXO,
  deleteUTXO,
  // Transaction storage (Phase 6.6)
  storeTransaction,
  getTransactions,
  getTransaction,
  deleteTransaction,
  // Transaction status updates (Phase 6.7)
  updateTransactionStatus,
  // Transaction sync
  syncTransactionsFromWDK,
  // Device management
  bootstrapDevice,
  createInvite,
  joinWallet,
  addDevice,
  getCurrentDevice,
  listDevices,
  removeDevice,
  deduplicateDevices,
  getConnectionStatus,
  waitForNewDevice,
  getPendingWriters,
  approveWriter,
  enableReplication,
  // Contact management
  addContact,
  getContacts,
  findContact,
  getContactById,
  updateContact,
  deleteContact,
  findContactByAddress,
  // NEW: Multi-address contact support
  addAddressToContact,
  removeAddressFromContact,
  closeStorage
}