 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 { abs, div, lt, mul } from 'biggystring'

import { base64 } from 'rfc4648'
import { bridgifyObject, onMethod, watchMethod } from 'yaob'

import {


  streamTransactions
} from '../../../client-side'
import {
  upgradeCurrencyCode,
  upgradeTxNetworkFees
} from '../../../types/type-helpers'



























import { makeMetaTokens } from '../../account/custom-tokens'
import { toApiInput } from '../../root-pixie'
import { makeStorageWalletApi } from '../../storage/storage-api'
import { getCurrencyMultiplier } from '../currency-selectors'
import { makeCurrencyWalletCallbacks } from './currency-wallet-callbacks'
import {
  asEdgeAssetAction,
  asEdgeTxAction,
  asEdgeTxSwap,

} from './currency-wallet-cleaners'
import { dateFilter, searchStringFilter } from './currency-wallet-export'
import {
  loadTxFiles,
  renameCurrencyWallet,
  saveTxMetadataFile,
  setCurrencyWalletFiat,
  setupNewTxMetadata,
  updateCurrencyWalletTxMetadata
} from './currency-wallet-files'


import { uniqueStrings } from './enabled-tokens'
import { getMaxSpendableInner } from './max-spend'
import { mergeMetadata } from './metadata'
import { upgradeMemos } from './upgrade-memos'

const fakeMetadata = {
  bizId: 0,
  category: '',
  exchangeAmount: {},
  name: '',
  notes: ''
}

// The EdgeTransaction.spendTargets type, but non-null:


/**
 * Creates an `EdgeCurrencyWallet` API object.
 */
export function makeCurrencyWalletApi(
  input,
  engine,
  tools,
  publicWalletInfo
) {
  const ai = toApiInput(input)
  const { accountId, pluginId, walletInfo } = input.props.walletState
  const plugin = input.props.state.plugins.currency[pluginId]
  const { unsafeBroadcastTx = false, unsafeMakeSpend = false } =
    plugin.currencyInfo

  const storageWalletApi = makeStorageWalletApi(ai, walletInfo)

  const fakeCallbacks = makeCurrencyWalletCallbacks(input)

  const otherMethods = {}
  if (engine.otherMethods != null) {
    for (const name of Object.keys(engine.otherMethods)) {
      const method = engine.otherMethods[name]
      if (typeof method !== 'function') continue
      otherMethods[name] = method
    }
  }
  if (engine.otherMethodsWithKeys != null) {
    for (const name of Object.keys(engine.otherMethodsWithKeys)) {
      const method = engine.otherMethodsWithKeys[name]
      if (typeof method !== 'function') continue
      otherMethods[name] = (...args) => method(walletInfo.keys, ...args)
    }
  }
  bridgifyObject(otherMethods)

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

    // Data store:
    get created() {
      return walletInfo.created
    },
    get disklet() {
      return storageWalletApi.disklet
    },
    get id() {
      return storageWalletApi.id
    },
    get localDisklet() {
      return storageWalletApi.localDisklet
    },
    publicWalletInfo,
    async sync() {
      await storageWalletApi.sync()
    },
    get type() {
      return storageWalletApi.type
    },

    // Wallet name:
    get name() {
      return input.props.walletState.name
    },
    async renameWallet(name) {
      await renameCurrencyWallet(input, name)
    },

    // Fiat currency option:
    get fiatCurrencyCode() {
      return input.props.walletState.fiat
    },
    async setFiatCurrencyCode(fiatCurrencyCode) {
      await setCurrencyWalletFiat(input, fiatCurrencyCode)
    },

    // Currency info:
    get currencyConfig() {
      const { accountApi } = input.props.output.accounts[accountId]
      return accountApi.currencyConfig[pluginId]
    },
    get currencyInfo() {
      return plugin.currencyInfo
    },
    async denominationToNative(
      denominatedAmount,
      currencyCode
    ) {
      const multiplier = getCurrencyMultiplier(
        plugin.currencyInfo,
        input.props.state.accounts[accountId].allTokens[pluginId],
        currencyCode
      )
      return mul(denominatedAmount, multiplier)
    },
    async nativeToDenomination(
      nativeAmount,
      currencyCode
    ) {
      const multiplier = getCurrencyMultiplier(
        plugin.currencyInfo,
        input.props.state.accounts[accountId].allTokens[pluginId],
        currencyCode
      )
      return div(nativeAmount, multiplier, multiplier.length)
    },

    // Chain state:
    get balances() {
      return input.props.walletState.balances
    },
    get balanceMap() {
      return input.props.walletState.balanceMap
    },
    get blockHeight() {
      const { skipBlockHeight } = input.props.state
      return skipBlockHeight ? 0 : input.props.walletState.height
    },
    get syncRatio() {
      return input.props.walletState.syncRatio
    },
    get unactivatedTokenIds() {
      return input.props.walletState.unactivatedTokenIds
    },

    // Running state:
    async changePaused(paused) {
      input.props.dispatch({
        type: 'CURRENCY_WALLET_CHANGED_PAUSED',
        payload: { walletId: input.props.walletId, paused }
      })
    },
    get paused() {
      return input.props.walletState.paused
    },

    // Tokens:
    async changeEnabledTokenIds(tokenIds) {
      const { dispatch, walletId, walletState } = input.props
      const { accountId, pluginId } = walletState
      const accountState = input.props.state.accounts[accountId]
      const allTokens = _nullishCoalesce(accountState.allTokens[pluginId], () => ( {}))

      const enabledTokenIds = uniqueStrings(tokenIds).filter(
        tokenId => allTokens[tokenId] != null
      )

      const shortId = walletId.slice(0, 2)
      input.props.log.warn(`enabledTokenIds: ${shortId} changeEnabledTokenIds`)
      dispatch({
        type: 'CURRENCY_WALLET_ENABLED_TOKENS_CHANGED',
        payload: { walletId, enabledTokenIds }
      })
    },

    get detectedTokenIds() {
      return input.props.walletState.detectedTokenIds
    },

    get enabledTokenIds() {
      return input.props.walletState.enabledTokenIds
    },

    // Transactions history:
    async getNumTransactions(opts) {
      const upgradedCurrency = upgradeCurrencyCode({
        allTokens: input.props.state.accounts[accountId].allTokens[pluginId],
        currencyInfo: plugin.currencyInfo,
        tokenId: opts.tokenId
      })

      return engine.getNumTransactions(upgradedCurrency)
    },

    async $internalStreamTransactions(
      opts
    ) {
      const {
        afterDate,
        batchSize = 10,
        beforeDate,
        firstBatchSize = batchSize,
        searchString,
        spamThreshold = '0',
        tokenId = null
      } = opts
      const { currencyCode } =
        tokenId == null
          ? this.currencyInfo
          : this.currencyConfig.allTokens[tokenId]
      const upgradedCurrency = { currencyCode, tokenId }

      // Load transactions from the engine if necessary:
      let state = input.props.walletState
      if (!state.gotTxs.has(tokenId)) {
        const txs = await engine.getTransactions(upgradedCurrency)
        fakeCallbacks.onTransactionsChanged(txs)
        input.props.dispatch({
          type: 'CURRENCY_ENGINE_GOT_TXS',
          payload: {
            walletId: input.props.walletId,
            tokenId
          }
        })
        state = input.props.walletState
      }

      const {
        // All the files we have loaded from disk:
        files,
        // All the txid hashes we know about from either the engine or disk,
        // sorted using the lowest available date.
        // Some may not exist on disk, and some may not exist on chain:
        sortedTxidHashes,
        // Maps from txid hashes to original txids:
        txidHashes,
        // All the transactions we have from the engine:
        txs
      } = state

      let i = 0
      let isFirst = true
      let lastFile = 0
      return bridgifyObject({
        async next() {
          const thisBatchSize = isFirst ? firstBatchSize : batchSize
          const out = []
          while (i < sortedTxidHashes.length && out.length < thisBatchSize) {
            // Load a batch of files if we need that:
            if (i >= lastFile) {
              const missingTxIdHashes = sortedTxidHashes
                .slice(lastFile, lastFile + thisBatchSize)
                .filter(txidHash => files[txidHash] == null)
              const missingFiles = await loadTxFiles(input, missingTxIdHashes)
              Object.assign(files, missingFiles)
              lastFile = lastFile + thisBatchSize
            }

            const txidHash = sortedTxidHashes[i++]
            const file = files[txidHash]
            const txid = _nullishCoalesce(_optionalChain([file, 'optionalAccess', _ => _.txid]), () => ( _optionalChain([txidHashes, 'access', _2 => _2[txidHash], 'optionalAccess', _3 => _3.txid])))
            if (txid == null) continue
            const tx = txs[txid]

            // Filter transactions with missing amounts (nativeAmount/networkFee)
            const nativeAmount = _optionalChain([tx, 'optionalAccess', _4 => _4.nativeAmount, 'access', _5 => _5.get, 'call', _6 => _6(tokenId)])
            const networkFee = _optionalChain([tx, 'optionalAccess', _7 => _7.networkFee, 'access', _8 => _8.get, 'call', _9 => _9(tokenId)])
            if (tx == null || nativeAmount == null || networkFee == null) {
              continue
            }

            // Filter transactions based on search criteria:
            const edgeTx = combineTxWithFile(input, tx, file, tokenId)
            upgradeTxNetworkFees(edgeTx)
            if (!searchStringFilter(ai, edgeTx, searchString)) continue
            if (!dateFilter(edgeTx, afterDate, beforeDate)) continue
            const isKnown =
              tx.isSend ||
              edgeTx.assetAction != null ||
              edgeTx.chainAction != null ||
              edgeTx.chainAssetAction != null ||
              edgeTx.savedAction != null
            if (!isKnown && lt(abs(nativeAmount), spamThreshold)) continue

            out.push(edgeTx)
          }

          isFirst = false
          return { done: out.length === 0, value: out }
        }
      })
    },

    async getTransactions(
      opts
    ) {
      const {
        endDate: beforeDate,
        startDate: afterDate,
        searchString,
        spamThreshold
      } = opts
      const upgradedCurrency = upgradeCurrencyCode({
        allTokens: input.props.state.accounts[accountId].allTokens[pluginId],
        currencyInfo: plugin.currencyInfo,
        tokenId: opts.tokenId
      })

      const stream = await out.$internalStreamTransactions({
        ...upgradedCurrency,
        afterDate,
        beforeDate,
        searchString,
        spamThreshold
      })

      // We have no length, so iterate to get everything:
      const txs = []
      while (true) {
        const batch = await stream.next()
        if (batch.done) return txs
        txs.push(...batch.value)
      }
    },

    streamTransactions,

    // Addresses:
    async getAddresses(
      opts
    ) {
      if (engine.getAddresses != null) {
        return await engine.getAddresses(opts)
      } else {
        const upgradedCurrency = upgradeCurrencyCode({
          allTokens: input.props.state.accounts[accountId].allTokens[pluginId],
          currencyInfo: plugin.currencyInfo,
          tokenId: opts.tokenId
        })

        const freshAddress = await engine.getFreshAddress({
          ...upgradedCurrency,
          forceIndex: opts.forceIndex
        })

        const {
          publicAddress,
          legacyAddress,
          segwitAddress,
          nativeBalance,
          legacyNativeBalance,
          segwitNativeBalance
        } = freshAddress

        const addresses = [
          {
            addressType: 'publicAddress',
            publicAddress,
            nativeBalance
          }
        ]

        if (segwitAddress != null) {
          addresses.unshift({
            addressType: 'segwitAddress',
            publicAddress: segwitAddress,
            nativeBalance: segwitNativeBalance
          })
        }

        if (legacyAddress != null) {
          addresses.push({
            addressType: 'legacyAddress',
            publicAddress: legacyAddress,
            nativeBalance: legacyNativeBalance
          })
        }

        return addresses
      }
    },

    async getReceiveAddress(
      opts
    ) {
      const addresses = await this.getAddresses(opts)
      if (addresses.length < 1) throw new Error('No addresses available')

      const primaryAddress =
        _nullishCoalesce(addresses.find(address => {
          return address.addressType === 'publicAddress'
        }), () => ( addresses[0]))

      const receiveAddress = {
        publicAddress: primaryAddress.publicAddress,
        nativeBalance: primaryAddress.nativeBalance,
        metadata: fakeMetadata,
        nativeAmount: '0'
      }

      const segwitAddress = addresses.find(address => {
        return address.addressType === 'segwitAddress'
      })
      if (segwitAddress != null) {
        receiveAddress.segwitAddress = segwitAddress.publicAddress
        receiveAddress.segwitNativeBalance = segwitAddress.nativeBalance
      }
      const legacyAddress = addresses.find(address => {
        return address.addressType === 'legacyAddress'
      })
      if (legacyAddress != null) {
        receiveAddress.legacyAddress = legacyAddress.publicAddress
        receiveAddress.legacyNativeBalance = legacyAddress.nativeBalance
      }

      return receiveAddress
    },
    async lockReceiveAddress(
      receiveAddress
    ) {
      // TODO: Address metadata
    },
    async saveReceiveAddress(
      receiveAddress
    ) {
      // TODO: Address metadata
    },

    // Sending:
    async broadcastTx(tx) {
      // Only provide wallet info if currency requires it:
      const privateKeys = unsafeBroadcastTx ? walletInfo.keys : undefined

      return await engine.broadcastTx(tx, { privateKeys })
    },
    async getMaxSpendable(spendInfo) {
      return await getMaxSpendableInner(
        spendInfo,
        plugin,
        engine,
        input.props.state.accounts[accountId].allTokens[pluginId],
        walletInfo
      )
    },
    async getPaymentProtocolInfo(
      paymentProtocolUrl
    ) {
      if (engine.getPaymentProtocolInfo == null) {
        throw new Error(
          "'getPaymentProtocolInfo' is not implemented on wallets of this type"
        )
      }
      return await engine.getPaymentProtocolInfo(paymentProtocolUrl)
    },
    async makeSpend(spendInfo) {
      spendInfo = upgradeMemos(spendInfo, plugin.currencyInfo)
      const {
        assetAction,
        customNetworkFee,
        enableRbf,
        memos,
        metadata,
        networkFeeOption = 'standard',
        noUnconfirmed = false,
        otherParams,
        pendingTxs,
        rbfTxid,
        savedAction,
        skipChecks,
        spendTargets = [],
        swapData
      } = spendInfo

      // Figure out which asset this is:
      const upgradedCurrency = upgradeCurrencyCode({
        allTokens: input.props.state.accounts[accountId].allTokens[pluginId],
        currencyInfo: plugin.currencyInfo,
        tokenId: spendInfo.tokenId
      })

      // Check the spend targets:
      const cleanTargets = []
      const savedTargets = []
      for (const target of spendTargets) {
        const {
          memo,
          publicAddress,
          nativeAmount = '0',
          otherParams = {}
        } = target
        if (publicAddress == null) continue

        cleanTargets.push({
          memo,
          nativeAmount,
          otherParams,
          publicAddress,
          uniqueIdentifier: memo
        })
        savedTargets.push({
          currencyCode: upgradedCurrency.currencyCode,
          memo,
          nativeAmount,
          publicAddress,
          uniqueIdentifier: memo
        })
      }

      if (spendInfo.privateKeys != null) {
        throw new TypeError('Only sweepPrivateKeys takes private keys')
      }

      // Only provide wallet info if currency requires it:
      const privateKeys = unsafeMakeSpend ? walletInfo.keys : undefined

      const tx = await engine.makeSpend(
        {
          ...upgradedCurrency,
          customNetworkFee,
          enableRbf,
          memos,
          metadata,
          networkFeeOption,
          noUnconfirmed,
          otherParams,
          pendingTxs,
          rbfTxid,
          skipChecks,
          spendTargets: cleanTargets
        },
        { privateKeys }
      )
      upgradeTxNetworkFees(tx)
      tx.networkFeeOption = networkFeeOption
      tx.requestedCustomFee = customNetworkFee
      tx.spendTargets = savedTargets
      tx.currencyCode = upgradedCurrency.currencyCode
      tx.tokenId = upgradedCurrency.tokenId
      if (metadata != null) tx.metadata = metadata
      if (swapData != null) tx.swapData = asEdgeTxSwap(swapData)
      if (savedAction != null) tx.savedAction = asEdgeTxAction(savedAction)
      if (assetAction != null) tx.assetAction = asEdgeAssetAction(assetAction)
      if (input.props.state.login.deviceDescription != null)
        tx.deviceDescription = input.props.state.login.deviceDescription

      return tx
    },
    async saveTx(transaction) {
      if (input.props.walletState.txs[transaction.txid] == null) {
        const { fileName, txFile } = await setupNewTxMetadata(
          input,
          transaction
        )
        await saveTxMetadataFile(input, fileName, txFile)
        fakeCallbacks.onTransactions([{ isNew: true, transaction }])
      } else {
        await updateCurrencyWalletTxMetadata(
          input,
          transaction.txid,
          transaction.tokenId,
          fakeCallbacks
        )
      }
      await engine.saveTx(transaction)
    },

    async saveTxAction(opts) {
      const { txid, tokenId, assetAction, savedAction } = opts
      await updateCurrencyWalletTxMetadata(
        input,
        txid,
        tokenId,
        fakeCallbacks,
        undefined,
        assetAction,
        savedAction
      )
    },

    async saveTxMetadata(opts) {
      const { txid, tokenId, metadata } = opts

      await updateCurrencyWalletTxMetadata(
        input,
        txid,
        tokenId,
        fakeCallbacks,
        metadata
      )
    },

    async signBytes(
      bytes,
      opts = {}
    ) {
      const privateKeys = walletInfo.keys

      if (engine.signBytes != null) {
        return await engine.signBytes(bytes, privateKeys, opts)
      }

      // Various plugins expect specific encodings for signing messages
      // (base16, base64, etc).
      // Do the conversion here temporarily if `signMessage` is implemented
      // while we migrate to `signBytes`.
      else if (pluginId === 'bitcoin' && engine.signMessage != null) {
        return await engine.signMessage(
          base64.stringify(bytes),
          privateKeys,
          opts
        )
      }

      throw new Error(`${pluginId} doesn't support signBytes`)
    },

    async signMessage(
      message,
      opts = {}
    ) {
      if (engine.signMessage == null) {
        throw new Error(`${pluginId} doesn't support signing messages`)
      }
      const privateKeys = walletInfo.keys
      return await engine.signMessage(message, privateKeys, opts)
    },
    async signTx(tx) {
      const privateKeys = walletInfo.keys

      return await engine.signTx(tx, privateKeys)
    },
    async sweepPrivateKeys(spendInfo) {
      if (engine.sweepPrivateKeys == null) {
        throw new Error('Sweeping this currency is not supported.')
      }
      return await engine.sweepPrivateKeys(spendInfo)
    },

    // Accelerating:
    async accelerate(tx) {
      if (engine.accelerate == null) return null
      return await engine.accelerate(tx)
    },

    // Staking:
    get stakingStatus() {
      return input.props.walletState.stakingStatus
    },

    // Wallet management:
    async dumpData() {
      return await engine.dumpData()
    },
    async resyncBlockchain() {
      const shortId = input.props.walletId.slice(0, 2)
      input.props.log.warn(`enabledTokenIds: ${shortId} resyncBlockchain`)
      ai.props.dispatch({
        type: 'CURRENCY_ENGINE_CLEARED',
        payload: { walletId: input.props.walletId }
      })
      await engine.resyncBlockchain()
    },

    // URI handling:
    async encodeUri(options) {
      return await tools.encodeUri(
        options,
        makeMetaTokens(
          input.props.state.accounts[accountId].customTokens[pluginId]
        )
      )
    },
    async parseUri(uri, currencyCode) {
      const parsedUri = await tools.parseUri(
        uri,
        currencyCode,
        makeMetaTokens(
          input.props.state.accounts[accountId].customTokens[pluginId]
        )
      )

      if (parsedUri.tokenId === undefined) {
        const { tokenId = null } = upgradeCurrencyCode({
          allTokens: input.props.state.accounts[accountId].allTokens[pluginId],
          currencyInfo: plugin.currencyInfo,
          currencyCode: _nullishCoalesce(parsedUri.currencyCode, () => ( currencyCode))
        })
        parsedUri.tokenId = tokenId
      }
      return parsedUri
    },

    // Generic:
    otherMethods
  }

  return bridgifyObject(out)
}

export function combineTxWithFile(
  input,
  tx,
  file,
  tokenId
) {
  const walletId = input.props.walletId
  const { accountId, currencyInfo, pluginId } = input.props.walletState
  const allTokens = input.props.state.accounts[accountId].allTokens[pluginId]

  const { currencyCode } = tokenId == null ? currencyInfo : allTokens[tokenId]
  const walletCurrency = currencyInfo.currencyCode

  // Copy the tx properties to the output:
  const out = {
    chainAction: tx.chainAction,
    chainAssetAction: tx.chainAssetAction.get(tokenId),
    blockHeight: tx.blockHeight,
    confirmations: tx.confirmations,
    currencyCode,
    feeRateUsed: tx.feeRateUsed,
    date: tx.date,
    isSend: tx.isSend,
    memos: tx.memos,
    metadata: {},
    nativeAmount: _nullishCoalesce(tx.nativeAmount.get(tokenId), () => ( '0')),
    networkFee: _nullishCoalesce(tx.networkFee.get(tokenId), () => ( '0')),
    networkFees: [],
    otherParams: { ...tx.otherParams },
    ourReceiveAddresses: tx.ourReceiveAddresses,
    parentNetworkFee:
      walletCurrency === currencyCode
        ? undefined
        : _nullishCoalesce(tx.networkFee.get(null), () => ( '0')),
    signedTx: tx.signedTx,
    tokenId,
    txid: tx.txid,
    walletId
  }

  // If we have a file, use it to override the defaults:
  if (file != null) {
    if (file.creationDate < out.date) out.date = file.creationDate

    out.metadata = mergeMetadata(
      _nullishCoalesce(_nullishCoalesce(_optionalChain([file, 'access', _10 => _10.tokens, 'access', _11 => _11.get, 'call', _12 => _12(null), 'optionalAccess', _13 => _13.metadata]), () => (
        _optionalChain([file, 'access', _14 => _14.currencies, 'access', _15 => _15.get, 'call', _16 => _16(walletCurrency), 'optionalAccess', _17 => _17.metadata]))), () => (
        {})),
      _nullishCoalesce(_nullishCoalesce(_optionalChain([file, 'access', _18 => _18.tokens, 'access', _19 => _19.get, 'call', _20 => _20(tokenId), 'optionalAccess', _21 => _21.metadata]), () => (
        _optionalChain([file, 'access', _22 => _22.currencies, 'access', _23 => _23.get, 'call', _24 => _24(currencyCode), 'optionalAccess', _25 => _25.metadata]))), () => (
        {}))
    )

    if (file.feeRateRequested != null) {
      if (typeof file.feeRateRequested === 'string') {
        out.networkFeeOption = file.feeRateRequested
      } else {
        out.networkFeeOption = 'custom'
        out.requestedCustomFee = file.feeRateRequested
      }
    }
    if (out.feeRateUsed == null) {
      out.feeRateUsed = file.feeRateUsed
    }

    if (file.payees != null) {
      out.spendTargets = file.payees.map(payee => ({
        currencyCode: payee.currency,
        memo: payee.tag,
        nativeAmount: payee.amount,
        publicAddress: payee.address,
        uniqueIdentifier: payee.tag
      }))
    }

    const assetAction = _optionalChain([file, 'access', _26 => _26.tokens, 'access', _27 => _27.get, 'call', _28 => _28(tokenId), 'optionalAccess', _29 => _29.assetAction])
    if (assetAction != null) out.assetAction = assetAction
    if (file.savedAction != null) out.savedAction = file.savedAction
    if (file.swap != null) out.swapData = file.swap
    if (file.secret != null) out.txSecret = file.secret
    if (file.deviceDescription != null)
      out.deviceDescription = file.deviceDescription
  }

  return out
}
