 function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
import { base32 } from 'rfc4648'
import { bridgifyObject, onMethod, watchMethod } from 'yaob'


import { AccountSync, fixUsername } from '../../client-side'
























import { base58 } from '../../util/encoding'
import { getPublicWalletInfo } from '../currency/wallet/currency-wallet-pixie'
import {
  finishWalletCreation,
  makeCurrencyWalletKeys,
  makeKeysKit
} from '../login/keys'
import { applyKit, decryptChildKey, searchTree } from '../login/login'
import { deleteLogin } from '../login/login-delete'
import { changeUsername } from '../login/login-username'
import { cancelOtpReset, disableOtp, enableOtp, repairOtp } from '../login/otp'
import {
  changePassword,
  checkPassword,
  deletePassword
} from '../login/password'
import { changePin, checkPin2, deletePin } from '../login/pin2'
import { changeRecovery, deleteRecovery } from '../login/recovery2'
import { listSplittableWalletTypes, splitWalletInfo } from '../login/splitting'
import { changeVoucherStatus } from '../login/vouchers'
import {
  findCurrencyPluginId,
  getCurrencyTools
} from '../plugins/plugins-selectors'

import { makeLocalDisklet } from '../storage/repo'
import { makeStorageWalletApi } from '../storage/storage-api'
import { fetchSwapQuotes } from '../swap/swap-api'
import { changeWalletStates } from './account-files'
import { ensureAccountExists } from './account-init'

import { makeDataStoreApi } from './data-store-api'
import { makeLobbyApi } from './lobby-api'
import { makeMemoryWalletInner } from './memory-wallet'
import { CurrencyConfig, SwapConfig } from './plugin-api'

/**
 * Creates an unwrapped account API object around an account state object.
 */
export function makeAccountApi(ai, accountId) {
  // We don't want accountState to be undefined when we log out,
  // so preserve a snapshot of our last state:
  let lastState = ai.props.state.accounts[accountId]
  const accountState = () => {
    const nextState = ai.props.state.accounts[accountId]
    if (nextState != null) lastState = nextState
    return lastState
  }

  const { accountWalletInfo, loginType } = accountState()

  // Plugin config API's:
  const currencyConfigs = {}
  for (const pluginId of Object.keys(ai.props.state.plugins.currency)) {
    const api = new CurrencyConfig(ai, accountId, pluginId)
    currencyConfigs[pluginId] = api
  }
  const swapConfigs = {}
  for (const pluginId of Object.keys(ai.props.state.plugins.swap)) {
    const api = new SwapConfig(ai, accountId, pluginId)
    swapConfigs[pluginId] = api
  }

  // Specialty API's:
  const dataStore = makeDataStoreApi(ai, accountId)
  const storageWalletApi = makeStorageWalletApi(ai, accountWalletInfo)

  function lockdown() {
    if (ai.props.state.hideKeys) {
      throw new Error('Not available when `hideKeys` is enabled')
    }
  }

  const out = {
    on: onMethod,
    watch: watchMethod,

    // ----------------------------------------------------------------
    // Data store:
    // ----------------------------------------------------------------

    get id() {
      return storageWalletApi.id
    },

    get type() {
      return storageWalletApi.type
    },

    get disklet() {
      lockdown()
      return storageWalletApi.disklet
    },

    get localDisklet() {
      lockdown()
      return storageWalletApi.localDisklet
    },

    async sync() {
      await storageWalletApi.sync()
    },

    // ----------------------------------------------------------------
    // Basic login information:
    // ----------------------------------------------------------------

    get appId() {
      return accountState().login.appId
    },

    get created() {
      return accountState().login.created
    },

    async getLoginKey() {
      lockdown()
      return base58.stringify(accountState().login.loginKey)
    },

    get lastLogin() {
      return accountState().login.lastLogin
    },

    get loggedIn() {
      return ai.props.state.accounts[accountId] != null
    },

    get recoveryKey() {
      lockdown()
      const { login } = accountState()
      return login.recovery2Key != null
        ? base58.stringify(login.recovery2Key)
        : undefined
    },

    get rootLoginId() {
      lockdown()
      const { loginTree } = accountState()
      return base58.stringify(loginTree.loginId)
    },

    get username() {
      const { loginTree } = accountState()
      return loginTree.username
    },

    // ----------------------------------------------------------------
    // Duress mode:
    // ----------------------------------------------------------------

    get canDuressLogin() {
      const { activeAppId } = accountState()
      const duressAppId = activeAppId.endsWith('.duress')
        ? activeAppId
        : activeAppId + '.duress'
      const duressStash = searchTree(
        accountState().loginTree,
        stash => stash.appId === duressAppId
      )
      return _optionalChain([duressStash, 'optionalAccess', _ => _.pin2Key]) != null
    },

    get isDuressAccount() {
      const { activeAppId } = accountState()
      return activeAppId.endsWith('.duress')
    },

    // ----------------------------------------------------------------
    // Specialty API's:
    // ----------------------------------------------------------------

    get currencyConfig() {
      return currencyConfigs
    },

    get swapConfig() {
      return swapConfigs
    },

    get dataStore() {
      return dataStore
    },

    // ----------------------------------------------------------------
    // What login method was used?
    // ----------------------------------------------------------------

    get edgeLogin() {
      const { loginTree } = accountState()
      return loginTree.loginKey == null
    },

    keyLogin: loginType === 'keyLogin',
    newAccount: loginType === 'newAccount',
    passwordLogin: loginType === 'passwordLogin',
    pinLogin: loginType === 'pinLogin',
    recoveryLogin: loginType === 'recoveryLogin',

    // ----------------------------------------------------------------
    // Change or create credentials:
    // ----------------------------------------------------------------

    async changePassword(password) {
      lockdown()
      await changePassword(ai, accountId, password)
    },

    async changePin(opts) {
      lockdown()
      // For crash errors:
      ai.props.log.breadcrumb('EdgeAccount.changePin', {})
      const { forDuressAccount = false } = opts
      const { activeAppId } = accountState()
      const duressAppId = activeAppId.endsWith('.duress')
        ? activeAppId
        : activeAppId + '.duress'
      // We only call ensureAccountExists if there is no duress account because
      // ensureAccountExists will throw an error if the duress account already
      // exists.
      if (forDuressAccount) {
        await ensureAccountExists(
          ai,
          accountState().stashTree,
          accountState().sessionKey,
          duressAppId
        )
      }
      await changePin(ai, accountId, opts)
      const login = forDuressAccount
        ? searchTree(accountState().login, stash => stash.appId === duressAppId)
        : accountState().login
      if (login == null) {
        // This shouldn't ever happen because not finding the duress account
        // when we have called `ensureAccountExists` is a bug in
        // `ensureAccountExists`.
        throw new Error('Failed to find account.')
      }
      return login.pin2Key != null ? base58.stringify(login.pin2Key) : ''
    },

    async changeRecovery(
      questions,
      answers
    ) {
      lockdown()
      await changeRecovery(ai, accountId, questions, answers)
      const { loginTree } = accountState()
      if (loginTree.recovery2Key == null) {
        throw new Error('Missing recoveryKey')
      }
      return base58.stringify(loginTree.recovery2Key)
    },

    async changeUsername(change) {
      lockdown()
      change.username = fixUsername(change.username)
      await changeUsername(ai, accountId, change)
    },

    // ----------------------------------------------------------------
    // Verify existing credentials:
    // ----------------------------------------------------------------

    async checkPassword(password) {
      lockdown()
      const { loginTree } = accountState()
      return await checkPassword(ai, loginTree, password)
    },

    async checkPin(pin) {
      lockdown()
      const { login, loginTree } = accountState()

      // Try to check the PIN locally, then fall back on the server:
      return login.pin != null
        ? pin === login.pin
        : await checkPin2(ai, loginTree, pin)
    },

    async getPin() {
      const { login, loginTree } = accountState()
      return _nullishCoalesce(login.pin, () => ( loginTree.pin))
    },

    // ----------------------------------------------------------------
    // Remove credentials:
    // ----------------------------------------------------------------

    async deletePassword() {
      lockdown()
      await deletePassword(ai, accountId)
    },

    async deletePin() {
      lockdown()
      await deletePin(ai, accountId)
    },

    async deleteRecovery() {
      lockdown()
      await deleteRecovery(ai, accountId)
    },

    // ----------------------------------------------------------------
    // OTP:
    // ----------------------------------------------------------------

    get otpKey() {
      lockdown()
      const { loginTree } = accountState()
      return loginTree.otpKey != null
        ? base32.stringify(loginTree.otpKey, { pad: false })
        : undefined
    },

    get otpResetDate() {
      lockdown()
      const { loginTree } = accountState()
      return loginTree.otpResetDate
    },

    async cancelOtpReset() {
      lockdown()
      await cancelOtpReset(ai, accountId)
    },

    async enableOtp(timeout = 7 * 24 * 60 * 60) {
      lockdown()
      await enableOtp(ai, accountId, timeout)
    },

    async disableOtp() {
      lockdown()
      await disableOtp(ai, accountId)
    },

    async repairOtp(otpKey) {
      lockdown()
      await repairOtp(ai, accountId, base32.parse(otpKey, { loose: true }))
    },

    // ----------------------------------------------------------------
    // 2fa bypass voucher approval / rejection:
    // ----------------------------------------------------------------

    get pendingVouchers() {
      const { login } = accountState()
      return login.pendingVouchers
    },

    async approveVoucher(voucherId) {
      const { loginTree } = accountState()
      return await changeVoucherStatus(ai, loginTree, {
        approvedVouchers: [voucherId]
      })
    },

    async rejectVoucher(voucherId) {
      const { loginTree } = accountState()
      return await changeVoucherStatus(ai, loginTree, {
        rejectedVouchers: [voucherId]
      })
    },

    // ----------------------------------------------------------------
    // Edge login approval:
    // ----------------------------------------------------------------

    async fetchLobby(lobbyId) {
      // For crash errors:
      ai.props.log.breadcrumb('EdgeAccount.fetchLobby', {})

      lockdown()
      return await makeLobbyApi(ai, accountId, lobbyId)
    },

    // ----------------------------------------------------------------
    // Login management:
    // ----------------------------------------------------------------

    async deleteRemoteAccount() {
      const { loginTree } = accountState()
      await deleteLogin(ai, loginTree)
    },

    async logout() {
      ai.props.dispatch({ type: 'LOGOUT', payload: { accountId } })
    },

    // ----------------------------------------------------------------
    // Master wallet list:
    // ----------------------------------------------------------------

    get allKeys() {
      return ai.props.state.accounts[accountId].allWalletInfosClean
    },

    async changeWalletStates(walletStates) {
      await changeWalletStates(ai, accountId, walletStates)
    },

    async createWallet(walletType, keys) {
      const { login, sessionKey, stashTree } = accountState()

      // For crash errors:
      ai.props.log.breadcrumb('EdgeAccount.createWallet', {})

      const walletInfo = await makeCurrencyWalletKeys(ai, walletType, { keys })
      const childKey = decryptChildKey(stashTree, sessionKey, login.loginId)
      await applyKit(ai, sessionKey, makeKeysKit(ai, childKey, [walletInfo]))
      return walletInfo.id
    },

    getFirstWalletInfo: AccountSync.prototype.getFirstWalletInfo,
    getWalletInfo: AccountSync.prototype.getWalletInfo,
    listWalletIds: AccountSync.prototype.listWalletIds,

    async splitWalletInfo(
      walletId,
      newWalletType
    ) {
      return await splitWalletInfo(ai, accountId, walletId, newWalletType)
    },

    async listSplittableWalletTypes(walletId) {
      return await listSplittableWalletTypes(ai, accountId, walletId)
    },

    // ----------------------------------------------------------------
    // Key access:
    // ----------------------------------------------------------------

    async getDisplayPrivateKey(walletId) {
      const info = getRawPrivateKey(ai, accountId, walletId)
      const pluginId = findCurrencyPluginId(
        ai.props.state.plugins.currency,
        info.type
      )
      const tools = await getCurrencyTools(ai, pluginId)
      if (tools.getDisplayPrivateKey != null) {
        return await tools.getDisplayPrivateKey(info)
      }

      const { engine } = ai.props.output.currency.wallets[walletId]
      if (engine == null || engine.getDisplayPrivateSeed == null) {
        throw new Error('Wallet has not yet loaded')
      }
      const out = await engine.getDisplayPrivateSeed(info.keys)
      if (out == null) throw new Error('The engine failed to return a key')
      return out
    },

    async getDisplayPublicKey(walletId) {
      const info = getRawPrivateKey(ai, accountId, walletId)
      const pluginId = findCurrencyPluginId(
        ai.props.state.plugins.currency,
        info.type
      )
      const tools = await getCurrencyTools(ai, pluginId)
      if (tools.getDisplayPublicKey != null) {
        const disklet = makeLocalDisklet(ai.props.io, walletId)
        const publicInfo = await getPublicWalletInfo(info, disklet, tools)
        return await tools.getDisplayPublicKey(publicInfo)
      }

      const { engine } = ai.props.output.currency.wallets[walletId]
      if (engine == null || engine.getDisplayPublicSeed == null) {
        throw new Error('Wallet has not yet loaded')
      }
      const out = await engine.getDisplayPublicSeed()
      if (out == null) throw new Error('The engine failed to return a key')
      return out
    },

    async getRawPrivateKey(walletId) {
      return getRawPrivateKey(ai, accountId, walletId).keys
    },

    async getRawPublicKey(walletId) {
      const info = getRawPrivateKey(ai, accountId, walletId)
      const pluginId = findCurrencyPluginId(
        ai.props.state.plugins.currency,
        info.type
      )
      const tools = await getCurrencyTools(ai, pluginId)
      const disklet = makeLocalDisklet(ai.props.io, walletId)
      const publicInfo = await getPublicWalletInfo(info, disklet, tools)

      return publicInfo.keys
    },

    // ----------------------------------------------------------------
    // Currency wallets:
    // ----------------------------------------------------------------

    get activeWalletIds() {
      return ai.props.state.accounts[accountId].activeWalletIds
    },

    get archivedWalletIds() {
      return ai.props.state.accounts[accountId].archivedWalletIds
    },

    get hiddenWalletIds() {
      return ai.props.state.accounts[accountId].hiddenWalletIds
    },

    get currencyWallets() {
      return ai.props.output.accounts[accountId].currencyWallets
    },

    get currencyWalletErrors() {
      return ai.props.state.accounts[accountId].currencyWalletErrors
    },

    async createCurrencyWallet(
      walletType,
      opts = {}
    ) {
      const { login, sessionKey, stashTree } = accountState()

      // For crash errors:
      ai.props.log.breadcrumb('EdgeAccount.createCurrencyWallet', {})

      const walletInfo = await makeCurrencyWalletKeys(ai, walletType, opts)
      const childKey = decryptChildKey(stashTree, sessionKey, login.loginId)
      await applyKit(ai, sessionKey, makeKeysKit(ai, childKey, [walletInfo]))
      return await finishWalletCreation(ai, accountId, walletInfo.id, opts)
    },

    async makeMemoryWallet(
      walletType,
      opts = {}
    ) {
      const config = Object.values(currencyConfigs).find(
        plugin => plugin.currencyInfo.walletType === walletType
      )
      if (config == null) throw new Error('Invalid walletType')

      return await makeMemoryWalletInner(ai, config, walletType, opts)
    },

    async createCurrencyWallets(
      createWallets
    ) {
      const { login, sessionKey, stashTree } = accountState()

      // For crash errors:
      ai.props.log.breadcrumb('EdgeAccount.makeMemoryWallet', {})

      // Create the keys:
      const walletInfos = await Promise.all(
        createWallets.map(
          async opts => await makeCurrencyWalletKeys(ai, opts.walletType, opts)
        )
      )

      // Store the keys on the server:
      const childKey = decryptChildKey(stashTree, sessionKey, login.loginId)
      await applyKit(ai, sessionKey, makeKeysKit(ai, childKey, walletInfos))

      // Set up options:
      return await Promise.all(
        walletInfos.map(
          async (info, i) =>
            await makeEdgeResult(
              finishWalletCreation(ai, accountId, info.id, createWallets[i])
            )
        )
      )
    },

    async waitForCurrencyWallet(walletId) {
      return await new Promise((resolve, reject) => {
        const check = () => {
          const wallet = this.currencyWallets[walletId]
          if (wallet != null) {
            resolve(wallet)
            cleanup()
          }
          const error = this.currencyWalletErrors[walletId]
          if (error != null) {
            reject(error)
            cleanup()
          }
        }

        const cleanup = () => {
          for (const cleanup of cleanups) cleanup()
        }

        const cleanups = [
          this.watch('currencyWallets', check),
          this.watch('currencyWalletErrors', check)
        ]

        check()
      })
    },

    async waitForAllWallets() {
      return await new Promise((resolve, reject) => {
        const check = () => {
          const busyWallet = this.activeWalletIds.find(
            id =>
              this.currencyWallets[id] == null &&
              this.currencyWalletErrors[id] == null
          )
          if (busyWallet == null) {
            for (const cleanup of cleanups) cleanup()
            resolve()
          }
        }

        const cleanups = [
          this.watch('activeWalletIds', check),
          this.watch('currencyWallets', check),
          this.watch('currencyWalletErrors', check)
        ]

        check()
      })
    },

    // ----------------------------------------------------------------
    // Token & wallet activation:
    // ----------------------------------------------------------------

    async getActivationAssets({
      activateWalletId,
      activateTokenIds
    }) {
      const { currencyWallets } = ai.props.output.accounts[accountId]
      const walletOutput = ai.props.output.currency.wallets[activateWalletId]
      const { engine } = walletOutput

      if (engine == null)
        throw new Error(`Invalid wallet: ${activateWalletId} not found`)

      if (engine.engineGetActivationAssets == null)
        throw new Error(
          `getActivationAssets unsupported by walletId ${activateWalletId}`
        )

      const out = await engine.engineGetActivationAssets({
        currencyWallets,
        activateTokenIds
      })

      // Added for backward compatibility for plugins using core 1.x
      out.assetOptions.forEach(asset => (asset.tokenId = _nullishCoalesce(asset.tokenId, () => ( null))))
      return out
    },

    async activateWallet(
      opts
    ) {
      const { activateWalletId, activateTokenIds, paymentInfo } = opts
      const { currencyWallets } = ai.props.output.accounts[accountId]
      const walletOutput = ai.props.output.currency.wallets[activateWalletId]
      const { engine } = walletOutput

      if (engine == null)
        throw new Error(`Invalid wallet: ${activateWalletId} not found`)

      if (engine.engineActivateWallet == null)
        throw new Error(
          `activateWallet unsupported by walletId ${activateWalletId}`
        )
      const walletId = _nullishCoalesce(_optionalChain([paymentInfo, 'optionalAccess', _2 => _2.walletId]), () => ( ''))
      const wallet = currencyWallets[walletId]

      if (wallet == null) {
        throw new Error(`No wallet for walletId ${walletId}`)
      }

      const out = await engine.engineActivateWallet({
        activateTokenIds,
        paymentInfo:
          paymentInfo != null
            ? {
                wallet,
                tokenId: paymentInfo.tokenId
              }
            : undefined,

        // Added for backward compatibility for plugins using core 1.x
        // @ts-expect-error
        paymentTokenId: _optionalChain([paymentInfo, 'optionalAccess', _3 => _3.tokenId]),
        paymentWallet: wallet
      })

      // Added for backward compatibility for plugins using core 1.x
      if (out.networkFee.tokenId === undefined) out.networkFee.tokenId = null

      return bridgifyObject(out)
    },

    // ----------------------------------------------------------------
    // Swapping:
    // ----------------------------------------------------------------

    async fetchSwapQuote(
      request,
      opts
    ) {
      const [bestQuote, ...otherQuotes] = await fetchSwapQuotes(
        ai,
        accountId,
        request,
        opts
      )

      // Close unused quotes:
      for (const otherQuote of otherQuotes) {
        otherQuote.close().catch(() => undefined)
      }

      // Return the front quote:
      if (bestQuote == null) throw new Error('No swap providers enabled')
      return bestQuote
    },

    async fetchSwapQuotes(
      request,
      opts
    ) {
      return await fetchSwapQuotes(ai, accountId, request, opts)
    }
  }
  bridgifyObject(out)

  return out
}

function getRawPrivateKey(
  ai,
  accountId,
  walletId
) {
  const infos = ai.props.state.accounts[accountId].allWalletInfosFull
  const info = infos.find(key => key.id === walletId)
  if (info == null) {
    throw new Error(`Invalid wallet: ${walletId} not found`)
  }
  return info
}

async function makeEdgeResult(promise) {
  try {
    return { ok: true, result: await promise }
  } catch (error) {
    return { ok: false, error }
  }
}
