// 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
}