OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-wallet / protocols / wdk.js

276 lines · javascript ref:
// protocols/wdk.js - Tether WDK Protocol Integration
//
// Implements the standard protocol interface for Tether WDK
// Supports: Bitcoin, Ethereum, and EVM-compatible chains
//
// WDK Documentation: https://docs.wallet.tether.io

// NOTE: This file is legacy - Phase 6 uses lib/wdk-integration.cjs instead
// Kept for reference or future ESM-based implementation

import 'bare-node-runtime/global'
import WDK from '@tetherto/wdk'
import WalletManagerEvm from '@tetherto/wdk-wallet-evm'
import WalletManagerBtc from '@tetherto/wdk-wallet-btc'

export default {
  name: 'wdk',

  // Protocol metadata
  info: {
    displayName: 'Tether WDK',
    description: 'Multi-chain wallet with Bitcoin and Ethereum support',
    chains: ['bitcoin', 'ethereum'],
    stablecoin: true,
    privacy: 'public',
    speed: 'varies', // Depends on chain
    fees: 'low' // Varies by chain
  },

  // Internal state
  context: null,
  wdk: null,
  accounts: {},

  /**
   * Initialize WDK protocol
   * @param {Object} context - Wallet context { identity, corestore, db, config }
   * @returns {Object} - This protocol instance
   */
  async init(context) {
    this.context = context

    // Get mnemonic from identity
    // keet-identity-key stores the mnemonic, we can access it
    const mnemonic = context.identity.mnemonic

    // Initialize WDK manager
    this.wdk = new WDK(mnemonic)

    // Register enabled blockchains
    const chains = context.config.wdk.chains || ['ethereum', 'bitcoin']
    const networks = context.config.wdk.networks

    for (const chain of chains) {
      if (chain === 'ethereum' && networks.ethereum) {
        this.wdk.registerWallet('ethereum', WalletManagerEvm, {
          provider: networks.ethereum.provider
        })
      } else if (chain === 'bitcoin' && networks.bitcoin) {
        this.wdk.registerWallet('bitcoin', WalletManagerBtc, {
          network: networks.bitcoin.network,
          host: networks.bitcoin.host,
          port: networks.bitcoin.port
        })
      }
    }

    // Get accounts (index 0 for each chain)
    for (const chain of chains) {
      try {
        this.accounts[chain] = await this.wdk.getAccount(chain, 0)
      } catch (err) {
        console.error(`   ⚠️  Failed to get ${chain} account: ${err.message}`)
      }
    }

    // Store wallet addresses in Hyperbee for multi-device sync
    await this.storeWalletInfo()

    console.log('   Chains:', chains.join(', '))

    return this
  },

  /**
   * Store wallet information in Hyperbee for multi-device sync
   */
  async storeWalletInfo() {
    for (const [chain, account] of Object.entries(this.accounts)) {
      try {
        const address = await account.getAddress()
        await this.context.db.put(`wallets/${chain}`, {
          chain,
          address,
          createdAt: Date.now()
        })
      } catch (err) {
        console.error(`   ⚠️  Failed to store ${chain} wallet info: ${err.message}`)
      }
    }
  },

  /**
   * Get balances across all enabled chains
   * @returns {Array<Object>} - Array of { chain, currency, amount }
   */
  async getBalance() {
    const balances = []

    for (const [chain, account] of Object.entries(this.accounts)) {
      try {
        const balance = await account.getBalance()
        const address = await account.getAddress()

        balances.push({
          chain,
          address,
          currency: this.getCurrency(chain),
          amount: balance.toString(), // bigint to string
          wei: balance // Keep raw bigint for calculations
        })
      } catch (err) {
        balances.push({
          chain,
          currency: this.getCurrency(chain),
          error: err.message
        })
      }
    }

    return balances
  },

  /**
   * Send payment
   * @param {Object} params - { to, amount, chain, memo }
   * @returns {Object} - Transaction details
   */
  async send({ to, amount, chain, memo }) {
    // Select chain if not specified
    if (!chain) {
      chain = this.selectChain(to)
    }

    const account = this.accounts[chain]
    if (!account) {
      throw new Error(`Chain "${chain}" not enabled`)
    }

    // Send transaction
    const result = await account.sendTransaction({
      to,
      value: BigInt(amount)
    })

    // Store transaction in Hyperbee
    await this.context.db.put(`tx/${chain}/${result.hash}`, {
      timestamp: Date.now(),
      chain,
      type: 'send',
      to,
      amount: amount.toString(),
      hash: result.hash,
      fee: result.fee.toString(),
      memo
    })

    // Append to Autobase for multi-device sync
    await this.context.txHistory.append(JSON.stringify({
      timestamp: Date.now(),
      chain,
      type: 'send',
      to,
      amount: amount.toString(),
      hash: result.hash
    }))

    return result
  },

  /**
   * Create address for receiving payment
   * @param {Object} params - { chain, amount, memo }
   * @returns {Object} - Address details
   */
  async receive({ chain, amount, memo }) {
    // Default to ethereum if not specified
    if (!chain) {
      chain = 'ethereum'
    }

    const account = this.accounts[chain]
    if (!account) {
      throw new Error(`Chain "${chain}" not enabled`)
    }

    // Get address
    const address = await account.getAddress()

    return {
      address,
      chain,
      currency: this.getCurrency(chain),
      amount,
      memo
    }
  },

  /**
   * Get transaction history
   * @param {Object} params - { limit, chain }
   * @returns {Array} - Transaction history
   */
  async history({ limit = 50, chain = null }) {
    const txs = []
    const prefix = chain ? `tx/${chain}/` : 'tx/'

    for await (const { key, value } of this.context.db.createReadStream()) {
      if (key.startsWith(prefix)) {
        txs.push(value)
      }

      if (txs.length >= limit) break
    }

    // Sort by timestamp (most recent first)
    return txs.sort((a, b) => b.timestamp - a.timestamp)
  },

  /**
   * Select appropriate chain based on address format
   * @param {String} to - Recipient address
   * @returns {String} - Chain name
   */
  selectChain(to) {
    // Auto-detect from address format
    if (to.startsWith('0x')) return 'ethereum'
    if (to.startsWith('bc1') || to.startsWith('tb1')) return 'bitcoin' // Native SegWit
    if (to.startsWith('1') || to.startsWith('3')) return 'bitcoin' // Legacy (though WDK only supports SegWit)

    // Default to ethereum
    return 'ethereum'
  },

  /**
   * Get currency symbol for chain
   * @param {String} chain - Chain name
   * @returns {String} - Currency symbol
   */
  getCurrency(chain) {
    const currencyMap = {
      bitcoin: 'BTC',
      ethereum: 'ETH'
    }

    return currencyMap[chain] || chain.toUpperCase()
  },

  /**
   * Format balance for display
   * @param {bigint} balance - Balance in wei or satoshis
   * @param {String} chain - Chain name
   * @returns {String} - Formatted balance
   */
  formatBalance(balance, chain) {
    if (chain === 'bitcoin') {
      // Bitcoin: satoshis to BTC (1 BTC = 100,000,000 satoshis)
      return (Number(balance) / 1e8).toFixed(8)
    } else if (chain === 'ethereum') {
      // Ethereum: wei to ETH (1 ETH = 10^18 wei)
      return (Number(balance) / 1e18).toFixed(18)
    }
    return balance.toString()
  }
}