OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-wallet-lib / config.cjs

238 lines · javascript ref:
// lib/config.cjs - Configuration management system
// CRITICAL: Load bare-node-runtime FIRST before any other requires
require('bare-node-runtime/global')

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

const CONFIG_DIR = path.join(os.homedir(), '.hyper-wallet')
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json')

// Default configuration
const DEFAULT_CONFIG = {
  version: 1,
  network: 'testnet',

  blockchain: {
    ethereum: {
      provider: 'blockscout', // Default: no API key needed!
      apiKey: null,
      rpcUrl: null
    },
    bitcoin: {
      client: {
        type: 'electrum',
        clientConfig: {
          host: 'electrum.blockstream.info',
          port: 60001
        }
      }
    }
  },

  ui: {
    theme: 'dark',
    autoLock: 30, // 30 minutes
    currency: 'USD'
  },

  sync: {
    autoSync: true,
    syncInterval: 15 // 15 minutes
  },

  security: {
    requirePasswordOnSend: true,
    sessionTimeout: 30 // 30 minutes
  }
}

/**
 * Deep merge objects
 * Later objects override earlier ones
 */
function mergeDeep (...objects) {
  const result = {}

  for (const obj of objects) {
    if (!obj) continue

    for (const key in obj) {
      if (obj[key] && typeof obj[key] === 'object' && !Array.isArray(obj[key])) {
        result[key] = mergeDeep(result[key] || {}, obj[key])
      } else {
        result[key] = obj[key]
      }
    }
  }

  return result
}

/**
 * Load user config file
 * Creates default if doesn't exist
 */
function loadUserConfig () {
  // Create directory if needed
  if (!fs.existsSync(CONFIG_DIR)) {
    fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 })
  }

  // Create default config if doesn't exist
  if (!fs.existsSync(CONFIG_FILE)) {
    saveConfig(DEFAULT_CONFIG)
    return DEFAULT_CONFIG
  }

  try {
    const data = fs.readFileSync(CONFIG_FILE, 'utf8')
    const userConfig = JSON.parse(data)

    // Merge with defaults (in case new fields added in updates)
    return mergeDeep(DEFAULT_CONFIG, userConfig)
  } catch (err) {
    console.error('⚠️  Error loading config, using defaults:', err.message)
    return DEFAULT_CONFIG
  }
}

/**
 * Load config from environment variables
 * Overrides user config
 */
function loadEnvConfig () {
  const envConfig = {}

  if (process.env.WALLET_NETWORK) {
    envConfig.network = process.env.WALLET_NETWORK
  }

  if (process.env.ETHEREUM_PROVIDER) {
    envConfig.blockchain = envConfig.blockchain || {}
    envConfig.blockchain.ethereum = envConfig.blockchain.ethereum || {}
    envConfig.blockchain.ethereum.provider = process.env.ETHEREUM_PROVIDER
  }

  if (process.env.ETHERSCAN_API_KEY) {
    envConfig.blockchain = envConfig.blockchain || {}
    envConfig.blockchain.ethereum = envConfig.blockchain.ethereum || {}
    envConfig.blockchain.ethereum.apiKey = process.env.ETHERSCAN_API_KEY
  }

  if (process.env.ETHEREUM_RPC_URL) {
    envConfig.blockchain = envConfig.blockchain || {}
    envConfig.blockchain.ethereum = envConfig.blockchain.ethereum || {}
    envConfig.blockchain.ethereum.rpcUrl = process.env.ETHEREUM_RPC_URL
  }

  // Legacy support: Check .etherscan-api-key file
  const legacyKeyFile = path.join(CONFIG_DIR, '.etherscan-api-key')
  if (fs.existsSync(legacyKeyFile)) {
    try {
      const apiKey = fs.readFileSync(legacyKeyFile, 'utf8').trim()
      if (apiKey) {
        envConfig.blockchain = envConfig.blockchain || {}
        envConfig.blockchain.ethereum = envConfig.blockchain.ethereum || {}
        envConfig.blockchain.ethereum.apiKey = apiKey
        // Don't override provider if already set
        if (!envConfig.blockchain.ethereum.provider) {
          envConfig.blockchain.ethereum.provider = 'etherscan'
        }
      }
    } catch (err) {
      // Ignore error reading legacy file
    }
  }

  return envConfig
}

/**
 * Get complete configuration
 * Merges defaults + user config + env vars
 */
function getConfig () {
  const defaults = DEFAULT_CONFIG
  const user = loadUserConfig()
  const env = loadEnvConfig()

  return mergeDeep(defaults, user, env)
}

/**
 * Save configuration to file
 */
function saveConfig (config) {
  // Ensure directory exists
  if (!fs.existsSync(CONFIG_DIR)) {
    fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 })
  }

  const data = JSON.stringify(config, null, 2)
  fs.writeFileSync(CONFIG_FILE, data, { mode: 0o600 })
}

/**
 * Update specific config value
 * Example: updateConfig('blockchain.ethereum.provider', 'etherscan')
 */
function updateConfig (path, value) {
  const config = loadUserConfig()

  // Set nested value
  const parts = path.split('.')
  let current = config

  for (let i = 0; i < parts.length - 1; i++) {
    if (!current[parts[i]]) current[parts[i]] = {}
    current = current[parts[i]]
  }

  current[parts[parts.length - 1]] = value

  saveConfig(config)
  return config
}

/**
 * Migrate legacy .etherscan-api-key file to config.json
 * Called automatically on first config load
 */
function migrateLegacyConfig () {
  const legacyKeyFile = path.join(CONFIG_DIR, '.etherscan-api-key')

  if (fs.existsSync(legacyKeyFile)) {
    try {
      const apiKey = fs.readFileSync(legacyKeyFile, 'utf8').trim()

      if (apiKey) {
        // Update config
        const config = loadUserConfig()
        config.blockchain.ethereum.apiKey = apiKey
        config.blockchain.ethereum.provider = 'etherscan' // They were using Etherscan
        saveConfig(config)

        // Rename old file (don't delete, for safety)
        fs.renameSync(legacyKeyFile, legacyKeyFile + '.migrated')

        console.log('✅ Migrated Etherscan API key to config.json')
        console.log(`   Old file renamed to ${path.basename(legacyKeyFile)}.migrated`)
      }
    } catch (err) {
      console.error('⚠️  Error migrating legacy config:', err.message)
    }
  }
}

module.exports = {
  getConfig,
  saveConfig,
  updateConfig,
  migrateLegacyConfig,
  DEFAULT_CONFIG,
  CONFIG_FILE,
  CONFIG_DIR
}