OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-wallet-lib / wdk-integration.cjs

220 lines · javascript ref:
// lib/wdk-integration.cjs - WDK Integration Layer
// 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')

// Import WDK packages directly
// bare-node-runtime/global provides Node.js compatibility for Bare runtime
const WDK = require('@tetherto/wdk')
const WalletManagerBtc = require('@tetherto/wdk-wallet-btc')
const WalletManagerEvm = require('@tetherto/wdk-wallet-evm')

// Import encryption module
const { encrypt, decrypt, isValidFormat } = require('./encryption.cjs')

// Storage paths
const STORAGE_DIR = path.join(os.homedir(), '.hyper-wallet')
const WALLET_MNEMONIC_FILE = path.join(STORAGE_DIR, '.wallet-mnemonic')

/**
 * Generate new wallet mnemonic (12 words)
 * Uses WDK.getRandomSeedPhrase() - official WDK method
 * @returns {string} - 12-word BIP-39 mnemonic
 */
function generateWalletMnemonic() {
  return WDK.default.getRandomSeedPhrase()
}

/**
 * Save wallet mnemonic to file (with password-based encryption)
 * Stored separately from identity mnemonic for security separation
 * @param {string} mnemonic - 12-word mnemonic
 * @param {string} password - Encryption password (min 8 chars)
 */
function saveWalletMnemonic(mnemonic, password) {
  if (!fs.existsSync(STORAGE_DIR)) {
    fs.mkdirSync(STORAGE_DIR, { recursive: true, mode: 0o700 })
  }

  // Require password for new mnemonics
  if (!password || password.length < 8) {
    throw new Error('Password must be at least 8 characters')
  }

  // Encrypt mnemonic before saving
  const encrypted = encrypt(mnemonic, password)
  fs.writeFileSync(WALLET_MNEMONIC_FILE, encrypted, { mode: 0o600 })
}

/**
 * Load wallet mnemonic from file (with password-based decryption)
 * @param {string} password - Decryption password
 * @returns {string|null} - 12-word mnemonic or null if not exists
 */
function loadWalletMnemonic(password) {
  if (!fs.existsSync(WALLET_MNEMONIC_FILE)) {
    return null
  }

  const encrypted = fs.readFileSync(WALLET_MNEMONIC_FILE, 'utf8').trim()

  // Check if this is plaintext (legacy format) - contains spaces and 12 words
  if (encrypted.includes(' ') && encrypted.split(' ').length === 12) {
    console.warn('⚠️  WARNING: Wallet mnemonic is stored in plaintext (legacy format)')
    console.warn('⚠️  Please set a password to encrypt your mnemonics')
    return encrypted
  }

  // Decrypt with password
  if (!password) {
    throw new Error('Password required to decrypt wallet mnemonic')
  }

  try {
    return decrypt(encrypted, password)
  } catch (err) {
    throw new Error('Invalid password or corrupted mnemonic data')
  }
}

/**
 * Initialize WDK with wallet mnemonic
 * Creates WDK instance and registers Bitcoin + Ethereum wallets
 * @param {string} walletMnemonic - 12-word BIP-39 mnemonic
 * @param {Object} config - Configuration { testnet: boolean }
 * @returns {Object} - { wdk } - WDK instance with registered wallets
 */
async function initializeWDK(walletMnemonic, config = { testnet: true, testnet4: false }) {
  try {
    // Initialize WDK with mnemonic and register wallets (chainable)
    const wdk = new WDK.default(walletMnemonic)
      .registerWallet('bitcoin', WalletManagerBtc.default, {
        network: config.testnet4 ? 'testnet4' : (config.testnet ? 'testnet' : 'bitcoin'),
        client: {
          type: 'electrum',
          clientConfig: {
            host: config.testnet4 ? 'blackie.c3-soft.com' : 'electrum.blockstream.info',
            port: config.testnet4 ? 50001 : (config.testnet ? 60001 : 50001)
          }
        }
      })
      .registerWallet('ethereum', WalletManagerEvm.default, {
        provider: config.testnet
          ? 'https://ethereum-sepolia-rpc.publicnode.com'
          : 'https://eth.drpc.org'
      })

    return { wdk }
  } catch (err) {
    // Graceful error handling with clear user feedback
    if (err.message.includes('network') || err.message.includes('provider')) {
      throw new Error('Cannot connect to blockchain network. Check internet connection.')
    } else if (err.message.includes('mnemonic') || err.message.includes('seed')) {
      throw new Error('Invalid wallet mnemonic format.')
    } else {
      throw new Error(`WDK initialization failed: ${err.message}`)
    }
  }
}

/**
 * Get first address for testing/display
 * Uses account index 0, address index 0
 * @param {Object} wdk - WDK instance
 * @param {string} chain - 'bitcoin' or 'ethereum'
 * @returns {string} - First derived address
 */
async function getFirstAddress(wdk, chain = 'bitcoin') {
  try {
    const account = await wdk.getAccount(chain, 0)
    return await account.getAddress()
  } catch (err) {
    throw new Error(`Failed to derive ${chain} address: ${err.message}`)
  }
}

/**
 * Check if wallet mnemonic exists
 * @returns {boolean}
 */
function walletMnemonicExists() {
  return fs.existsSync(WALLET_MNEMONIC_FILE)
}

/**
 * Token contract addresses for ERC-20 tokens
 * Format: { 'chain-token': { mainnet: 'address', testnet: 'address' } }
 */
const TOKEN_CONTRACTS = {
  'ethereum-usdt': {
    mainnet: '0xdAC17F958D2ee523a2206206994597C13D831ec7',  // Checksummed address (important!)
    testnet: '0x7169D38820dfd117C3FA1f22a697dBA58d90BA06' // Sepolia USDT
  },
  'ethereum-usdc': {
    mainnet: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
    testnet: '0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238' // Sepolia USDC
  }
}

/**
 * Check if a chain parameter refers to a token (e.g., 'ethereum-usdt')
 * @param {string} chain - Chain identifier
 * @returns {boolean}
 */
function isTokenChain(chain) {
  return chain.includes('-') && chain !== 'testnet' && chain !== 'mainnet'
}

/**
 * Get base chain from token chain identifier
 * @param {string} chain - Chain identifier (e.g., 'ethereum-usdt' or 'ethereum')
 * @returns {string} - Base chain (e.g., 'ethereum')
 */
function getBaseChain(chain) {
  if (!isTokenChain(chain)) {
    return chain
  }
  // Extract base chain from 'ethereum-usdt' -> 'ethereum'
  return chain.split('-')[0]
}

/**
 * Get token contract information
 * @param {string} chain - Token chain identifier (e.g., 'ethereum-usdt')
 * @param {boolean} isTestnet - Whether to use testnet contracts
 * @returns {Object} - Token info { contractAddress, symbol, decimals }
 */
function getTokenInfo(chain, isTestnet = true) {
  const tokenContracts = TOKEN_CONTRACTS[chain]
  if (!tokenContracts) {
    throw new Error(`Unsupported token: ${chain}`)
  }

  const contractAddress = isTestnet ? tokenContracts.testnet : tokenContracts.mainnet

  // Extract token symbol from chain (ethereum-usdt -> USDT)
  const symbol = chain.split('-')[1]?.toUpperCase() || 'TOKEN'

  return {
    contractAddress,  // Changed from 'contract' to match usage in wallet-server.cjs
    symbol,
    decimals: 6 // USDT/USDC use 6 decimals
  }
}

module.exports = {
  generateWalletMnemonic,
  saveWalletMnemonic,
  loadWalletMnemonic,
  initializeWDK,
  getFirstAddress,
  walletMnemonicExists,
  isTokenChain,
  getBaseChain,
  getTokenInfo,
  TOKEN_CONTRACTS
}