 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 { lt } from 'biggystring'
import { asMaybe } from 'cleaners'
import { isPixieShutdownError } from 'redux-pixies'
import { emit } from 'yaob'

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








import { compare } from '../../../util/compare'
import { enableTestMode, pushUpdate } from '../../../util/updateQueue'
import {
  getStorageWalletLastChanges,
  hashStorageWalletFilename
} from '../../storage/storage-selectors'
import { combineTxWithFile } from './currency-wallet-api'
import { asIntegerString } from './currency-wallet-cleaners'
import {
  loadAddressFiles,
  loadFiatFile,
  loadNameFile,
  loadTokensFile,
  loadTxFileNames,
  saveSeenTxCheckpointFile,
  setupNewTxMetadata
} from './currency-wallet-files'




import {

  mergeTx,

} from './currency-wallet-reducer'
import { uniqueStrings } from './enabled-tokens'

let throttleRateLimitMs = 5000

/**
 * Wraps a transaction-accepting callback with throttling logic.
 * Returns a function that can be called at high frequency, and batches its
 * inputs to only call the real callback every 5 seconds.
 */
function makeThrottledTxCallback(
  input,
  callback
) {
  const { log, walletId } = input.props

  let delayCallback = false
  let lastCallbackTime = 0
  let pendingTxs = []

  return (txArray) => {
    if (delayCallback) {
      log(`throttledTxCallback delay, walletId: ${walletId}`)
      pendingTxs.push(...txArray)
    } else {
      const now = Date.now()
      if (now - lastCallbackTime > throttleRateLimitMs) {
        lastCallbackTime = now
        callback(txArray)
      } else {
        log(`throttledTxCallback delay, walletId: ${walletId}`)
        delayCallback = true
        pendingTxs = txArray
        setTimeout(() => {
          lastCallbackTime = Date.now()
          callback(pendingTxs)
          delayCallback = false
          pendingTxs = []
        }, throttleRateLimitMs)
      }
    }
  }
}

/**
 * Returns a callback structure suitable for passing to a currency engine.
 */
export function makeCurrencyWalletCallbacks(
  input
) {
  const { walletId } = input.props

  // If this is a unit test, lower throttling to something testable:
  if (walletId === 'narfavJN4rp9ZzYigcRj1i0vrU2OAGGp4+KksAksj54=') {
    throttleRateLimitMs = 25
    enableTestMode()
  }

  const throttledOnTxChanged = makeThrottledTxCallback(
    input,
    (txArray) => {
      if (_optionalChain([input, 'access', _ => _.props, 'access', _2 => _2.walletOutput, 'optionalAccess', _3 => _3.walletApi]) != null) {
        emit(input.props.walletOutput.walletApi, 'transactionsChanged', txArray)
      }
    }
  )

  const throttledOnNewTx = makeThrottledTxCallback(
    input,
    (txArray) => {
      if (_optionalChain([input, 'access', _4 => _4.props, 'access', _5 => _5.walletOutput, 'optionalAccess', _6 => _6.walletApi]) != null) {
        emit(input.props.walletOutput.walletApi, 'newTransactions', txArray)
      }
    }
  )

  const out = {
    onAddressesChecked(ratio) {
      pushUpdate({
        id: walletId,
        action: 'onAddressesChecked',
        updateFunc: () => {
          input.props.dispatch({
            type: 'CURRENCY_ENGINE_CHANGED_SYNC_RATIO',
            payload: { ratio, walletId }
          })
        }
      })
    },

    onNewTokens(tokenIds) {
      pushUpdate({
        id: walletId,
        action: 'onNewTokens',
        updateFunc: () => {
          // Before we update redux, figure out what's truly new:
          const { detectedTokenIds, enabledTokenIds } = input.props.walletState
          const enablingTokenIds = uniqueStrings(tokenIds, [
            ...detectedTokenIds,
            ...enabledTokenIds
          ])

          // Update redux:
          const shortId = walletId.slice(0, 2)
          input.props.log.warn(
            `enabledTokenIds: ${shortId} engine detected tokens`
          )
          input.props.dispatch({
            type: 'CURRENCY_ENGINE_DETECTED_TOKENS',
            payload: {
              detectedTokenIds: tokenIds,
              enablingTokenIds,
              walletId
            }
          })

          // Fire an event to the GUI:
          if (enablingTokenIds.length > 0) {
            const walletApi = _optionalChain([input, 'access', _7 => _7.props, 'optionalAccess', _8 => _8.walletOutput, 'optionalAccess', _9 => _9.walletApi])
            if (walletApi != null) {
              emit(walletApi, 'enabledDetectedTokens', enablingTokenIds)
            }
          }
        }
      })
    },

    onUnactivatedTokenIdsChanged(unactivatedTokenIds) {
      pushUpdate({
        id: walletId,
        action: 'onUnactivatedTokenIdsChanged',
        updateFunc: () => {
          input.props.dispatch({
            type: 'CURRENCY_ENGINE_CHANGED_UNACTIVATED_TOKEN_IDS',
            payload: { unactivatedTokenIds, walletId }
          })
        }
      })
    },

    onBalanceChanged(currencyCode, balance) {
      const { accountId, currencyInfo, pluginId } = input.props.walletState
      const allTokens =
        input.props.state.accounts[accountId].allTokens[pluginId]

      if (currencyCode === currencyInfo.currencyCode) {
        out.onTokenBalanceChanged(null, balance)
      } else {
        const { tokenId } = upgradeCurrencyCode({
          allTokens,
          currencyInfo,
          currencyCode
        })
        if (tokenId != null) out.onTokenBalanceChanged(tokenId, balance)
      }
    },

    onTokenBalanceChanged(tokenId, balance) {
      const clean = asMaybe(asIntegerString)(balance)
      if (clean == null) {
        input.props.onError(
          new Error(
            `Plugin sent bogus balance for ${String(tokenId)}: "${balance}"`
          )
        )
        return
      }
      pushUpdate({
        id: `${walletId}==${String(tokenId)}`,
        action: 'onTokenBalanceChanged',
        updateFunc: () => {
          input.props.dispatch({
            type: 'CURRENCY_ENGINE_CHANGED_BALANCE',
            payload: { balance: clean, tokenId, walletId }
          })
        }
      })
    },

    // DEPRECATE: After all currency plugins implement new Confirmations API
    onBlockHeightChanged(height) {
      pushUpdate({
        id: walletId,
        action: 'onBlockHeightChanged',
        updateFunc: () => {
          // Update transaction confirmation status
          const { txs } = input.props.walletState
          for (const txid of Object.keys(txs)) {
            const reduxTx = txs[txid]
            if (shouldCoreDetermineConfirmations(reduxTx.confirmations)) {
              const { requiredConfirmations } =
                input.props.walletState.currencyInfo
              const { height } = input.props.walletState

              reduxTx.confirmations = determineConfirmations(
                reduxTx,
                height,
                requiredConfirmations
              )

              // Recreate the EdgeTransaction object
              const txidHash = hashStorageWalletFilename(
                input.props.state,
                walletId,
                reduxTx.txid
              )
              const { files } = input.props.walletState
              const changedTx = combineTxWithFile(
                input,
                reduxTx,
                files[txidHash],
                null
              )

              // Dispatch event to update the redux transaction object
              input.props.dispatch({
                type: 'CHANGE_MERGE_TX',
                payload: { tx: reduxTx }
              })
              // Dispatch event to update the EdgeTransaction object
              throttledOnTxChanged([changedTx])
            }
          }

          input.props.dispatch({
            type: 'CURRENCY_ENGINE_CHANGED_HEIGHT',
            payload: { height, walletId }
          })
        }
      })
    },

    onSeenTxCheckpoint(checkpoint) {
      saveSeenTxCheckpointFile(input, checkpoint).catch(error =>
        input.props.onError(error)
      )
    },

    onStakingStatusChanged(stakingStatus) {
      pushUpdate({
        id: walletId,
        action: 'onStakingStatusChanged',
        updateFunc: () => {
          input.props.dispatch({
            type: 'CURRENCY_ENGINE_CHANGED_STAKING',
            payload: { stakingStatus, walletId }
          })
        }
      })
    },

    onSubscribeAddresses(addresses) {
      const changeServiceSubscriptions =
        input.props.state.currency.wallets[walletId].changeServiceSubscriptions
      const subscriptions = [
        ...changeServiceSubscriptions,
        ...addresses.map(address => ({
          address,
          status: 'subscribing' 
        }))
      ]
      // TODO: We currently have no way to remove addresses from this list.
      // This could be an issue for chains like Hedera where activating the wallet
      // causes its address to permanently change. For now we'll accept the extra
      // noise since it doesn't affect most chains.
      input.props.dispatch({
        type: 'CURRENCY_ENGINE_UPDATE_CHANGE_SERVICE_SUBSCRIPTIONS',
        payload: {
          subscriptions,
          walletId
        }
      })
    },

    onTransactions(txEvents) {
      const { accountId, currencyInfo, pluginId } = input.props.walletState
      const allTokens =
        input.props.state.accounts[accountId].allTokens[pluginId]

      // Sanity-check incoming transactions:
      if (txEvents == null || txEvents.length === 0) return
      const allTxs = []
      for (const txEvent of txEvents) {
        const tx = txEvent.transaction
        // Backwards/Forwards compatibility:
        upgradeTxNetworkFees(tx)
        if (
          typeof tx.txid !== 'string' ||
          typeof tx.date !== 'number' ||
          typeof tx.networkFee !== 'string' ||
          typeof tx.blockHeight !== 'number' ||
          typeof tx.nativeAmount !== 'string' ||
          typeof tx.ourReceiveAddresses !== 'object'
        ) {
          input.props.onError(
            new Error(`Plugin sent bogus tx: ${JSON.stringify(tx, null, 2)}`)
          )
          return
        }
        if (tx.isSend == null) tx.isSend = lt(tx.nativeAmount, '0')
        if (tx.memos == null) tx.memos = []
        if (tx.tokenId === undefined) {
          const { tokenId } = upgradeCurrencyCode({
            allTokens,
            currencyInfo,
            currencyCode: tx.currencyCode
          })
          tx.tokenId = _nullishCoalesce(tokenId, () => ( null))
        }
        // Accumulate the transactions for the dispatch:
        allTxs.push(tx)
      }

      // Grab stuff from redux:
      const { state } = input.props
      const { txs: reduxTxs } = input.props.walletState

      const txidHashes = {}
      const changed = []
      const created = []
      for (const txEvent of txEvents) {
        const { isNew, transaction: tx } = txEvent
        const { txid } = tx

        // DEPRECATE: After all currency plugins implement new Confirmations API
        if (shouldCoreDetermineConfirmations(tx.confirmations)) {
          const { requiredConfirmations } = input.props.walletState.currencyInfo
          const { height } = input.props.walletState

          tx.confirmations = determineConfirmations(
            tx,
            height,
            requiredConfirmations
          )
        }

        // Verify that something has changed:
        const reduxTx = mergeTx(tx, reduxTxs[txid])
        if (compare(reduxTx, reduxTxs[txid]) && tx.metadata == null) continue

        // Ensure the transaction has metadata:
        const txidHash = hashStorageWalletFilename(state, walletId, txid)

        // Setup the metadata in memory only if we don't have if for the
        // transaction. Transaction metadata share a single file with the core,
        // so we only need to do this once (regardless of whether the
        // transaction is new).
        // TODO: Remove this once the core is refactored to no longer depend on
        // this state being in memory to get its job done for transaction
        // related routines.
        if (input.props.walletState.fileNames[txidHash] == null) {
          setupNewTxMetadata(input, tx).catch(error =>
            input.props.onError(error)
          )
        }

        // Build the final transaction to show the user:
        const { files } = input.props.walletState
        const combinedTx = combineTxWithFile(
          input,
          reduxTx,
          files[txidHash],
          tx.tokenId
        )
        if (isNew) {
          created.push(combinedTx)
        } else {
          changed.push(combinedTx)
        }
        txidHashes[txidHash] = { date: combinedTx.date, txid }
      }

      // Tell everyone who cares:
      input.props.dispatch({
        type: 'CURRENCY_ENGINE_CHANGED_TXS',
        payload: { txs: allTxs, walletId, txidHashes }
      })
      if (changed.length > 0) throttledOnTxChanged(changed)
      if (created.length > 0) throttledOnNewTx(created)
    },
    onTransactionsChanged(txs) {
      out.onTransactions(
        txs.map(transaction => {
          return { isNew: false, transaction }
        })
      )
    },
    onAddressChanged() {
      if (input.props.walletOutput.walletApi != null) {
        emit(input.props.walletOutput.walletApi, 'addressChanged', undefined)
      }
    },
    onWcNewContractCall(payload) {
      if (input.props.walletOutput.walletApi != null) {
        emit(input.props.walletOutput.walletApi, 'wcNewContractCall', payload)
      }
    },
    onTxidsChanged() {}
  }
  return out
}

/**
 * Monitors a currency wallet for changes and fires appropriate callbacks.
 */
export function watchCurrencyWallet(input) {
  const { walletId } = input.props

  let lastChanges
  async function checkChanges(props) {
    // Check for data changes:
    const changes = getStorageWalletLastChanges(props.state, walletId)
    if (changes !== lastChanges) {
      lastChanges = changes
      await loadTokensFile(input)
      await loadFiatFile(input)
      await loadNameFile(input)
      await loadTxFileNames(input)
      await loadAddressFiles(input)
    }
  }
  function checkChangesLoop() {
    input
      .nextProps()
      .then(checkChanges)
      .then(checkChangesLoop, error => {
        if (isPixieShutdownError(error)) return
        input.props.onError(error)
        checkChangesLoop()
      })
  }
  checkChangesLoop()
}

/**
 * Returns true if the core needs to calculate the transaction's confirmation state,
 * because it still depends on the current block height.
 *
 * @deprecated Remove once all currency plugins support the new confirmations API.
 */
const shouldCoreDetermineConfirmations = (
  confirmations
) => {
  return (
    confirmations !== 'confirmed' &&
    confirmations !== 'dropped' &&
    confirmations !== 'failed'
  )
}

export const determineConfirmations = (
  tx, // Either EdgeTransaction or MergedTransaction
  blockHeight,
  requiredConfirmations = 1 // Default confirmation rule is 1 block
) => {
  // If the transaction has a blockHeight >0, then it has been mined in a block
  if (tx.blockHeight > 0) {
    // Add 1 to the diff because there is 1 confirmation if the tx and network
    // block heights are equal:
    const blockConfirmations = 1 + blockHeight - tx.blockHeight
    // Negative confirmations mean the network blockHeight hasn't caught up:
    if (blockConfirmations <= 0) {
      return 'syncing'
    }
    // Return confirmed if it meets the minimum:
    if (blockConfirmations >= requiredConfirmations) {
      return 'confirmed'
    }
    // Otherwise, return the number of confirmations:
    return blockConfirmations
  }
  // Historically, tx.blockHeight === -1 has meant the transaction has been dropped
  if (tx.blockHeight < 0) {
    return 'dropped'
  }
  // Historically, tx.blockHeight === 0 has meant unconfirmed in our API.
  return 'unconfirmed'
}
