OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-wallet-desktop / debug-mnemonic-mismatch.cjs

338 lines · javascript ref:
#!/usr/bin/env node
/**
 * Deep Debugging: Mnemonic vs Storage Address Mismatch
 *
 * This script performs a systematic analysis of:
 * 1. What addresses are stored in the database
 * 2. What addresses the current mnemonic derives to
 * 3. Where the mismatch occurs
 * 4. Device and timestamp information
 */

// CRITICAL: Load bare-node-runtime FIRST
require('bare-node-runtime/global')

const fs = require('bare-fs')
const path = require('bare-path')
const os = require('bare-os')
const Corestore = require('corestore')
const Autobase = require('autobase')
const Hyperbee = require('hyperbee')
const WDK = require('@tetherto/wdk')
const WalletManagerEvm = require('@tetherto/wdk-wallet-evm')
const WalletManagerBtc = require('@tetherto/wdk-wallet-btc')
const { decrypt } = require('./lib/encryption.cjs')

const STORAGE_DIR = path.join(os.homedir(), '.hyper-wallet')
const DB_DIR = path.join(STORAGE_DIR, 'storage')
const WALLET_MNEMONIC_FILE = path.join(STORAGE_DIR, '.wallet-mnemonic')
const IDENTITY_MNEMONIC_FILE = path.join(STORAGE_DIR, '.mnemonic')

// Colors for terminal output
const colors = {
  reset: '\x1b[0m',
  red: '\x1b[31m',
  green: '\x1b[32m',
  yellow: '\x1b[33m',
  blue: '\x1b[34m',
  magenta: '\x1b[35m',
  cyan: '\x1b[36m',
  bold: '\x1b[1m'
}

function log(msg, color = 'reset') {
  console.log(`${colors[color]}${msg}${colors.reset}`)
}

function header(msg) {
  console.log()
  log('='.repeat(80), 'cyan')
  log(msg, 'bold')
  log('='.repeat(80), 'cyan')
  console.log()
}

async function debugMnemonicMismatch(password) {
  header('DEEP DEBUGGING: Mnemonic vs Storage Mismatch Analysis')

  // ========================================
  // STEP 1: Read stored mnemonics
  // ========================================
  header('STEP 1: Read Stored Mnemonics')

  let walletMnemonic = null
  let identityMnemonic = null

  try {
    const walletEncrypted = fs.readFileSync(WALLET_MNEMONIC_FILE, 'utf8').trim()
    walletMnemonic = decrypt(walletEncrypted, password)
    log(`✓ Wallet mnemonic loaded: ${walletMnemonic.split(' ').length} words`, 'green')
    log(`  First 3 words: ${walletMnemonic.split(' ').slice(0, 3).join(' ')}...`, 'blue')
  } catch (err) {
    log(`✗ Failed to load wallet mnemonic: ${err.message}`, 'red')
    process.exit(1)
  }

  try {
    const identityEncrypted = fs.readFileSync(IDENTITY_MNEMONIC_FILE, 'utf8').trim()
    identityMnemonic = decrypt(identityEncrypted, password)
    log(`✓ Identity mnemonic loaded: ${identityMnemonic.split(' ').length} words`, 'green')
    log(`  First 3 words: ${identityMnemonic.split(' ').slice(0, 3).join(' ')}...`, 'blue')
  } catch (err) {
    log(`⚠ No identity mnemonic found`, 'yellow')
  }

  // ========================================
  // STEP 2: Derive expected addresses from wallet mnemonic
  // ========================================
  header('STEP 2: Derive Expected Addresses from Wallet Mnemonic')

  const wdk = new WDK.default(walletMnemonic)
    .registerWallet('ethereum', WalletManagerEvm.default, {
      provider: 'https://ethereum-sepolia-rpc.publicnode.com'
    })
    .registerWallet('bitcoin', WalletManagerBtc.default, {
      network: 'testnet',
      host: 'electrum.blockstream.info',
      port: 60001
    })

  const expectedAddresses = {}

  // Derive Ethereum addresses (indices 0-5)
  log('Deriving Ethereum addresses...', 'cyan')
  expectedAddresses.ethereum = []
  for (let i = 0; i < 5; i++) {
    const account = await wdk.getAccount('ethereum', i)
    const address = await account.getAddress()
    expectedAddresses.ethereum.push({ index: i, address })
    log(`  [${i}] ${address}`, i === 0 ? 'green' : 'blue')
  }

  // Derive Bitcoin addresses (indices 0-5)
  log('\nDeriving Bitcoin addresses...', 'cyan')
  expectedAddresses.bitcoin = []
  for (let i = 0; i < 5; i++) {
    const account = await wdk.getAccount('bitcoin', i)
    const address = await account.getAddress()
    expectedAddresses.bitcoin.push({ index: i, address })
    log(`  [${i}] ${address}`, i === 0 ? 'green' : 'blue')
  }

  // ========================================
  // STEP 3: Read stored addresses from database
  // ========================================
  header('STEP 3: Read Stored Addresses from Database')

  const store = new Corestore(DB_DIR)
  const autobase = new Autobase(store, null, {
    valueEncoding: 'json',
    open: () => store.get({ name: 'main' })
  })
  const db = new Hyperbee(autobase.view, {
    keyEncoding: 'utf-8',
    valueEncoding: 'json'
  })

  await db.ready()

  const storedAddresses = {
    ethereum: [],
    'ethereum-usdt': [],
    bitcoin: []
  }

  // Read all stored addresses
  log('Reading Ethereum addresses from storage...', 'cyan')
  for await (const { key, value } of db.createReadStream({
    gte: 'wallet:address:ethereum:',
    lt: 'wallet:address:ethereum:\xFF'
  })) {
    storedAddresses.ethereum.push({ key, ...value })
    const current = value.isCurrent ? ' [CURRENT]' : ''
    const color = value.isCurrent ? 'green' : 'blue'
    log(`  [${value.index}] ${value.address}${current}`, color)
    log(`      Device: ${value.device || 'unknown'} | Created: ${new Date(value.createdAt).toISOString()}`, 'blue')
  }

  log('\nReading Ethereum-USDT addresses from storage...', 'cyan')
  for await (const { key, value } of db.createReadStream({
    gte: 'wallet:address:ethereum-usdt:',
    lt: 'wallet:address:ethereum-usdt:\xFF'
  })) {
    storedAddresses['ethereum-usdt'].push({ key, ...value })
    const current = value.isCurrent ? ' [CURRENT]' : ''
    const color = value.isCurrent ? 'green' : 'blue'
    log(`  [${value.index}] ${value.address}${current}`, color)
    log(`      Device: ${value.device || 'unknown'} | Created: ${new Date(value.createdAt).toISOString()}`, 'blue')
  }

  log('\nReading Bitcoin addresses from storage...', 'cyan')
  for await (const { key, value } of db.createReadStream({
    gte: 'wallet:address:bitcoin:',
    lt: 'wallet:address:bitcoin:\xFF'
  })) {
    storedAddresses.bitcoin.push({ key, ...value })
    const current = value.isCurrent ? ' [CURRENT]' : ''
    const color = value.isCurrent ? 'green' : 'blue'
    log(`  [${value.index}] ${value.address}${current}`, color)
    log(`      Device: ${value.device || 'unknown'} | Created: ${new Date(value.createdAt).toISOString()}`, 'blue')
  }

  // ========================================
  // STEP 4: Compare derived vs stored
  // ========================================
  header('STEP 4: Validation - Compare Derived vs Stored')

  const mismatches = []

  // Check Ethereum
  log('Validating Ethereum addresses...', 'cyan')
  for (const stored of storedAddresses.ethereum) {
    const expected = expectedAddresses.ethereum.find(e => e.index === stored.index)
    if (!expected) {
      log(`  [${stored.index}] WARNING: Stored address has no expected derivation`, 'yellow')
      mismatches.push({ chain: 'ethereum', index: stored.index, stored: stored.address, expected: null })
      continue
    }

    const match = stored.address.toLowerCase() === expected.address.toLowerCase()
    if (match) {
      log(`  [${stored.index}] ✓ MATCH: ${stored.address}`, 'green')
    } else {
      log(`  [${stored.index}] ✗ MISMATCH:`, 'red')
      log(`      Stored:   ${stored.address}`, 'red')
      log(`      Expected: ${expected.address}`, 'green')
      mismatches.push({
        chain: 'ethereum',
        index: stored.index,
        stored: stored.address,
        expected: expected.address,
        device: stored.device,
        createdAt: new Date(stored.createdAt).toISOString()
      })
    }
  }

  // Check Ethereum-USDT (should use same addresses as Ethereum)
  log('\nValidating Ethereum-USDT addresses...', 'cyan')
  for (const stored of storedAddresses['ethereum-usdt']) {
    const expected = expectedAddresses.ethereum.find(e => e.index === stored.index)
    if (!expected) {
      log(`  [${stored.index}] WARNING: Stored address has no expected derivation`, 'yellow')
      mismatches.push({ chain: 'ethereum-usdt', index: stored.index, stored: stored.address, expected: null })
      continue
    }

    const match = stored.address.toLowerCase() === expected.address.toLowerCase()
    if (match) {
      log(`  [${stored.index}] ✓ MATCH: ${stored.address}`, 'green')
    } else {
      log(`  [${stored.index}] ✗ MISMATCH:`, 'red')
      log(`      Stored:   ${stored.address}`, 'red')
      log(`      Expected: ${expected.address}`, 'green')
      mismatches.push({
        chain: 'ethereum-usdt',
        index: stored.index,
        stored: stored.address,
        expected: expected.address,
        device: stored.device,
        createdAt: new Date(stored.createdAt).toISOString()
      })
    }
  }

  // Check Bitcoin
  log('\nValidating Bitcoin addresses...', 'cyan')
  for (const stored of storedAddresses.bitcoin) {
    const expected = expectedAddresses.bitcoin.find(e => e.index === stored.index)
    if (!expected) {
      log(`  [${stored.index}] WARNING: Stored address has no expected derivation`, 'yellow')
      continue
    }

    const match = stored.address === expected.address
    if (match) {
      log(`  [${stored.index}] ✓ MATCH: ${stored.address}`, 'green')
    } else {
      log(`  [${stored.index}] ✗ MISMATCH:`, 'red')
      log(`      Stored:   ${stored.address}`, 'red')
      log(`      Expected: ${expected.address}`, 'green')
      mismatches.push({
        chain: 'bitcoin',
        index: stored.index,
        stored: stored.address,
        expected: expected.address,
        device: stored.device,
        createdAt: new Date(stored.createdAt).toISOString()
      })
    }
  }

  // ========================================
  // STEP 5: Check devices
  // ========================================
  header('STEP 5: Check Devices')

  const devices = []
  for await (const { key, value } of db.createReadStream({
    gte: 'wallet:device:',
    lt: 'wallet:device:\xFF'
  })) {
    devices.push(value)
    log(`Device: ${value.name}`, 'cyan')
    log(`  Writer Key: ${Buffer.from(value.key).toString('hex')}`, 'blue')
    log(`  Added: ${new Date(value.addedAt).toISOString()}`, 'blue')
  }

  if (devices.length === 0) {
    log('No devices found in storage', 'yellow')
  }

  // ========================================
  // STEP 6: Summary
  // ========================================
  header('SUMMARY')

  log(`Total stored addresses:`, 'cyan')
  log(`  Ethereum: ${storedAddresses.ethereum.length}`, 'blue')
  log(`  Ethereum-USDT: ${storedAddresses['ethereum-usdt'].length}`, 'blue')
  log(`  Bitcoin: ${storedAddresses.bitcoin.length}`, 'blue')

  log(`\nTotal mismatches: ${mismatches.length}`, mismatches.length > 0 ? 'red' : 'green')

  if (mismatches.length > 0) {
    log('\nMismatch Details:', 'red')
    for (const m of mismatches) {
      log(`  ${m.chain} [${m.index}]:`, 'red')
      log(`    Stored:   ${m.stored}`, 'red')
      log(`    Expected: ${m.expected || 'N/A'}`, 'green')
      if (m.device) log(`    Device:   ${m.device}`, 'yellow')
      if (m.createdAt) log(`    Created:  ${m.createdAt}`, 'yellow')
    }

    log('\n⚠️  CRITICAL: Storage contains addresses that do NOT match the current wallet mnemonic!', 'red')
    log('This means the exported mnemonic will NOT recover these addresses.', 'red')
  } else {
    log('\n✓ All stored addresses match the wallet mnemonic!', 'green')
  }

  await db.close()
  await store.close()
}

// Main
const password = process.argv[2]

if (!password) {
  console.error('Usage: node debug-mnemonic-mismatch.cjs <password>')
  console.error('Example: node debug-mnemonic-mismatch.cjs mypassword')
  process.exit(1)
}

debugMnemonicMismatch(password).catch(err => {
  console.error('\n❌ Error:', err.message)
  console.error(err.stack)
  process.exit(1)
})