OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-wallet / tests / manual / hyper-wallet-client.cjs

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

// hyper-wallet-client.cjs - HTTP Client for Hyper Wallet Daemon
// Communicates with wallet-server.cjs daemon via HTTP API

require('bare-node-runtime/global')

const os = require('bare-os')

const DAEMON_URL = process.env.WALLET_DAEMON || 'http://localhost:4004'

// Helper to check if daemon is running
async function checkDaemon() {
  try {
    const res = await fetch(`${DAEMON_URL}/api/status`)
    if (!res.ok) throw new Error('Daemon not responding')
    return true
  } catch (err) {
    return false
  }
}

// Helper to make API calls
async function apiCall(endpoint, options = {}) {
  const url = `${DAEMON_URL}${endpoint}`

  try {
    const res = await fetch(url, {
      headers: {
        'Content-Type': 'application/json',
        ...options.headers
      },
      ...options
    })

    const data = await res.json()

    if (data.error) {
      throw new Error(data.error)
    }

    return data
  } catch (err) {
    if (err.code === 'ECONNREFUSED') {
      console.error('❌ Error: Wallet daemon not running')
      console.error('')
      console.error('Start the daemon with:')
      console.error('  bare wallet-server.cjs')
      console.error('')
      process.exit(1)
    }
    throw err
  }
}

// Commands

async function cmdStatus() {
  const data = await apiCall('/api/status')

  console.log('💰 Hyper Wallet Status')
  console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
  console.log('')
  console.log(`Initialized:  ${data.initialized ? '✅' : '❌'}`)
  console.log(`Has Identity: ${data.hasIdentity ? '✅' : '❌'}`)
  console.log(`Has Wallet:   ${data.hasWallet ? '✅' : '❌'}`)
  console.log(`Network:      ${data.network}`)
  console.log(`Version:      ${data.version}`)
  console.log('')
}

async function cmdBalance(chain) {
  if (!chain) {
    console.error('Usage: bare hyper-wallet-client.cjs balance <chain>')
    console.error('')
    console.error('Examples:')
    console.error('  bare hyper-wallet-client.cjs balance testnet-bitcoin')
    console.error('  bare hyper-wallet-client.cjs balance testnet-ethereum')
    console.error('')
    process.exit(1)
  }

  const data = await apiCall(`/api/balance/${chain}`)

  console.log(`💰 Balance: ${chain}`)
  console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
  console.log('')
  console.log(`Confirmed:   ${data.confirmed}`)
  console.log(`Unconfirmed: ${data.unconfirmed}`)
  console.log(`Total:       ${data.total}`)
  console.log('')
}

async function cmdAddress(subcommand, chain) {
  if (subcommand === 'current') {
    if (!chain) {
      console.error('Usage: bare hyper-wallet-client.cjs address current <chain>')
      console.error('')
      console.error('Examples:')
      console.error('  bare hyper-wallet-client.cjs address current testnet-bitcoin')
      console.error('  bare hyper-wallet-client.cjs address current testnet-ethereum')
      console.error('')
      process.exit(1)
    }

    const data = await apiCall(`/api/address/${chain}`)

    console.log(`📍 Current Address: ${chain}`)
    console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
    console.log('')
    console.log(data.address)
    console.log('')
  } else {
    console.error('Usage: bare hyper-wallet-client.cjs address <subcommand>')
    console.error('')
    console.error('Subcommands:')
    console.error('  current <chain>  - Show current receiving address')
    console.error('')
    process.exit(1)
  }
}

async function cmdTransactions(chain = 'all') {
  const data = await apiCall(`/api/transactions/${chain}`)

  console.log(`📜 Transactions: ${chain}`)
  console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
  console.log('')
  console.log(`Total: ${data.count}`)
  console.log('')

  if (data.transactions.length === 0) {
    console.log('No transactions found.')
    console.log('')
    return
  }

  for (const tx of data.transactions) {
    const date = new Date(tx.timestamp).toLocaleString()
    const type = tx.type === 'send' ? '📤' : '📥'

    console.log(`${type} ${tx.type.toUpperCase()}`)
    console.log(`   Chain:  ${tx.chain}`)
    console.log(`   Amount: ${tx.amount}`)
    console.log(`   Status: ${tx.status}`)
    console.log(`   Date:   ${date}`)
    if (tx.txid) {
      console.log(`   TxID:   ${tx.txid.slice(0, 16)}...`)
    }
    console.log('')
  }
}

async function cmdDeviceList() {
  const data = await apiCall('/api/device/list')

  console.log('📱 Devices')
  console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
  console.log('')
  console.log(`Total: ${data.devices.length}`)
  console.log('')

  if (data.devices.length === 0) {
    console.log('No devices found.')
    console.log('')
    return
  }

  for (const device of data.devices) {
    const badge = device.isCurrentDevice ? ' (current)' : ''
    const date = new Date(device.addedAt).toLocaleString()

    console.log(`${device.name}${badge}`)
    console.log(`   Identity: ${device.identityKeyShort || 'N/A'}`)
    console.log(`   Writer:   ${device.writerKeyShort}`)
    console.log(`   Added:    ${date}`)
    console.log('')
  }
}

async function cmdDeviceAdd() {
  console.log('📱 Adding New Device')
  console.log('')

  // Start device add
  const data = await apiCall('/api/device/add', { method: 'POST' })

  console.log('✅ Invite created!')
  console.log('')
  console.log('Share this invite code with your other device:')
  console.log('')
  console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
  console.log(data.inviteCode)
  console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
  console.log('')
  console.log('On the other device, run:')
  console.log(`bare hyper-wallet-client.cjs device join "<invite>" "Device Name"`)
  console.log('')
  console.log('📡 Waiting for device to connect...')
  console.log('   (Press Ctrl+C to cancel)')
  console.log('')

  // Poll for status
  const sessionId = data.sessionId
  let completed = false

  const interval = setInterval(async () => {
    try {
      const status = await apiCall(`/api/device/add/${sessionId}`)

      if (status.status === 'completed') {
        clearInterval(interval)
        completed = true

        console.log('')
        console.log('✅ Device added successfully!')
        console.log(`   Name:   ${status.deviceName}`)
        console.log(`   Writer: ${status.writerKey.slice(0, 16)}...`)
        console.log('')
        process.exit(0)
      } else if (status.status === 'error') {
        clearInterval(interval)
        console.log('')
        console.log(`❌ Error: ${status.error}`)
        console.log('')
        process.exit(1)
      }
    } catch (err) {
      clearInterval(interval)
      console.error('Error polling status:', err.message)
      process.exit(1)
    }
  }, 2000)

  // Handle Ctrl+C
  process.on('SIGINT', () => {
    clearInterval(interval)
    console.log('')
    console.log('Cancelled.')
    console.log('')
    process.exit(0)
  })
}

async function cmdDeviceJoin(inviteCode, deviceName) {
  if (!inviteCode || !deviceName) {
    console.error('Usage: bare hyper-wallet-client.cjs device join <invite> "Device Name"')
    console.error('')
    process.exit(1)
  }

  console.log('🔗 Joining Wallet')
  console.log('')
  console.log(`Device Name: ${deviceName}`)
  console.log('')

  const data = await apiCall('/api/device/join', {
    method: 'POST',
    body: JSON.stringify({ inviteCode, deviceName })
  })

  console.log('✅ Joined wallet successfully!')
  console.log('')
  console.log(`Name:   ${data.device.name}`)
  console.log(`Writer: ${data.device.writerKey}`)
  console.log('')
  console.log('🔐 Mnemonics received and saved securely')
  console.log('   Identity: ~/.hyper-wallet/.mnemonic (24 words)')
  console.log('   Wallet:   ~/.hyper-wallet/.wallet-mnemonic (12 words)')
  console.log('')
}

async function cmdWhoAmI() {
  const data = await apiCall('/api/whoami')

  console.log('👤 Device Identity')
  console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
  console.log('')
  console.log(`Public Key: ${data.publicKey}`)
  console.log(`Hostname:   ${data.hostname}`)
  console.log(`Platform:   ${data.platform || os.platform()}`)
  console.log('')
}

async function cmdHelp() {
  console.log('💰 Hyper Wallet Client (HTTP Mode)')
  console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
  console.log('')
  console.log('Connects to wallet daemon at:', DAEMON_URL)
  console.log('')
  console.log('Usage: bare hyper-wallet-client.cjs <command> [options]')
  console.log('')
  console.log('Commands:')
  console.log('')
  console.log('  status                        - Show wallet status')
  console.log('  whoami                        - Show device identity')
  console.log('  balance <chain>               - Show balance for chain')
  console.log('  address current <chain>       - Show current receiving address')
  console.log('  transactions [chain]          - List transactions (default: all)')
  console.log('')
  console.log('  device list                   - List all devices')
  console.log('  device add                    - Create invite for new device')
  console.log('  device join <invite> "name"   - Join wallet using invite')
  console.log('')
  console.log('Environment:')
  console.log('  WALLET_DAEMON - Override daemon URL (default: http://localhost:4004)')
  console.log('')
  console.log('Examples:')
  console.log('  bare hyper-wallet-client.cjs status')
  console.log('  bare hyper-wallet-client.cjs balance testnet-bitcoin')
  console.log('  bare hyper-wallet-client.cjs device list')
  console.log('  bare hyper-wallet-client.cjs device add')
  console.log('')
  console.log('Start daemon:')
  console.log('  bare wallet-server.cjs')
  console.log('')
}

// Main
async function main() {
  const args = process.argv.slice(1)
  const command = args[1]

  // Check daemon first (except for help)
  if (command !== 'help' && command !== '--help') {
    const running = await checkDaemon()
    if (!running) {
      console.error('❌ Error: Wallet daemon not running')
      console.error('')
      console.error('Start the daemon with:')
      console.error('  bare wallet-server.cjs')
      console.error('')
      console.error('Or set WALLET_DAEMON environment variable:')
      console.error('  WALLET_DAEMON=http://other-host:4004 bare hyper-wallet-client.cjs ...')
      console.error('')
      process.exit(1)
    }
  }

  try {
    if (!command || command === 'help' || command === '--help') {
      await cmdHelp()
    } else if (command === 'status') {
      await cmdStatus()
    } else if (command === 'whoami') {
      await cmdWhoAmI()
    } else if (command === 'balance') {
      await cmdBalance(args[2])
    } else if (command === 'address') {
      await cmdAddress(args[2], args[3])
    } else if (command === 'transactions') {
      await cmdTransactions(args[2])
    } else if (command === 'device') {
      const subcmd = args[2]
      if (subcmd === 'list') {
        await cmdDeviceList()
      } else if (subcmd === 'add') {
        await cmdDeviceAdd()
      } else if (subcmd === 'join') {
        await cmdDeviceJoin(args[3], args[4])
      } else {
        console.error('Unknown device subcommand:', subcmd)
        console.error('')
        console.error('Available: list, add, join')
        console.error('')
        process.exit(1)
      }
    } else {
      console.error('Unknown command:', command)
      console.error('')
      console.error('Run: bare hyper-wallet-client.cjs help')
      console.error('')
      process.exit(1)
    }
  } catch (err) {
    console.error('Error:', err.message)
    console.error('')
    process.exit(1)
  }
}

main()