OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-wallet / deprecated / lib / wallet-context.js

197 lines · javascript ref:
// lib/wallet-context.js - Shared Wallet State
//
// Manages shared state across all protocols:
// - Identity (keet-identity-key)
// - Storage (Corestore)
// - Database (Hyperbee)
// - Transaction history (Autobase)

import IdentityKey from './identity.js'
import Corestore from 'corestore'
import Hyperbee from 'hyperbee'
import Autobase from 'autobase'
import path from '#path'
import os from '#os'

class WalletContext {
  /**
   * Create wallet context
   * @param {Object} config - Configuration object
   */
  constructor(config) {
    this.config = config
    this.protocols = {} // Initialized protocol instances
    this.identity = null
    this.store = null
    this.db = null
    this.txHistory = null
  }

  /**
   * Initialize wallet with mnemonic
   * @param {String} mnemonic - BIP39 mnemonic phrase
   * @returns {WalletContext} - This instance
   */
  async init(mnemonic) {
    // 1. Initialize identity (keet-identity-key)
    this.identity = await IdentityKey.from({ mnemonic })

    // 2. Initialize storage (Corestore)
    const storagePath = this.resolveStoragePath(this.config.storagePath)
    // Note: Using identity seed as primaryKey for deterministic key derivation
    this.store = new Corestore(storagePath, {
      primaryKey: this.identity.seed,
      unsafe: true // Acknowledge we're intentionally using a custom key
    })
    await this.store.ready()

    // 3. Initialize database (Hyperbee)
    const dbCore = this.store.get({ name: 'wallet-db' })
    this.db = new Hyperbee(dbCore, {
      keyEncoding: 'utf-8',
      valueEncoding: 'json'
    })
    await this.db.ready()

    // 4. Initialize transaction history (Autobase)
    this.txHistory = new Autobase(this.store, null, {
      apply: this.applyTransaction.bind(this),
      open: (store) => store.get({ name: 'tx-log' })
    })
    await this.txHistory.ready()

    console.log('✅ Wallet context initialized')
    console.log(`   Storage: ${storagePath}`)
    console.log(`   Identity: ${this.identity.identityPublicKey.toString('hex').slice(0, 16)}...`)

    return this
  }

  /**
   * Apply transaction to Autobase view
   * This is called automatically by Autobase when syncing across devices
   */
  async applyTransaction(nodes, view, base) {
    for (const node of nodes) {
      try {
        const tx = JSON.parse(node.value.toString())
        await view.append(JSON.stringify(tx))
      } catch (err) {
        console.error('Failed to apply transaction:', err)
      }
    }
  }

  /**
   * Load enabled protocols
   * @param {Object} enabled - Enabled protocol objects
   */
  async loadProtocols(enabled) {
    console.log('🔌 Loading protocols...')

    const failures = []

    for (const [name, protocol] of Object.entries(enabled)) {
      try {
        this.protocols[name] = await protocol.init(this)
        console.log(`   ✅ ${name}`)
      } catch (err) {
        failures.push({ protocol: name, error: err.message })
        console.error(`   ❌ ${name}: ${err.message}`)
      }
    }

    // Check if any protocols loaded successfully
    if (Object.keys(this.protocols).length === 0) {
      console.error('')
      console.error('❌ All protocols failed to initialize')
      console.error('   Failures:')
      failures.forEach(({ protocol, error }) => {
        console.error(`     - ${protocol}: ${error}`)
      })
      throw new Error('No protocols available')
    }

    // Warn about partial failures
    if (failures.length > 0) {
      console.warn('')
      console.warn('⚠️  Some protocols failed to load:')
      failures.forEach(({ protocol, error }) => {
        console.warn(`   - ${protocol}: ${error}`)
      })
      console.warn(`   ${Object.keys(this.protocols).length} protocol(s) loaded successfully`)
    } else {
      console.log('✅ All protocols loaded')
    }
  }

  /**
   * Get balances from all protocols
   * @returns {Object} - { protocolName: balance }
   */
  async getBalances() {
    const balances = {}

    for (const [name, protocol] of Object.entries(this.protocols)) {
      try {
        balances[name] = await protocol.getBalance()
      } catch (err) {
        balances[name] = { error: err.message }
      }
    }

    return balances
  }

  /**
   * Check if device is authorized
   * @param {Buffer} devicePublicKey - Device public key
   * @returns {Boolean} - True if authorized
   */
  async isDeviceAuthorized(devicePublicKey) {
    // Get device proof
    const deviceId = devicePublicKey.toString('hex').slice(0, 8)
    const device = await this.db.get(`devices/${deviceId}`)

    if (!device) return false

    // Verify proof is valid
    const verified = IdentityKey.verify(device.value.proof)
    if (!verified) return false

    // Check revocation list
    const revoked = await this.db.get('devices/revoked')
    if (revoked && revoked.value) {
      const isRevoked = revoked.value.some(r =>
        r.publicKey.equals(devicePublicKey)
      )
      if (isRevoked) return false
    }

    return true
  }

  /**
   * Resolve storage path (expand ~)
   * @param {String} storagePath - Path from config
   * @returns {String} - Absolute path
   */
  resolveStoragePath(storagePath) {
    if (storagePath.startsWith('~')) {
      return path.join(os.homedir(), storagePath.slice(1))
    }
    return storagePath
  }

  /**
   * Close wallet (cleanup resources)
   */
  async close() {
    if (this.txHistory) await this.txHistory.close()
    if (this.db) await this.db.close()
    if (this.store) await this.store.close()
  }
}

export default WalletContext