/*
 This file is part of GNU Taler
 (C) 2019 GNUnet e.V.
 GNU Taler is free software; you can redistribute it and/or modify it under the
 terms of the GNU General Public License as published by the Free Software
 Foundation; either version 3, or (at your option) any later version.
 GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
 You should have received a copy of the GNU General Public License along with
 GNU Taler; see the file COPYING.  If not, see 
 */
/**
 * Derive pending tasks from the wallet database.
 */
/**
 * Imports.
 */
import {
  PurchaseStatus,
  WalletStoresV1,
  BackupProviderStateTag,
  RefreshCoinStatus,
  OperationStatusRange,
  PeerPushPaymentInitiationStatus,
  PeerPullDebitRecordStatus,
  PeerPushPaymentIncomingStatus,
  PeerPullPaymentInitiationStatus,
  WithdrawalGroupStatus,
  RewardRecordStatus,
  DepositOperationStatus,
  RefreshGroupRecord,
  WithdrawalGroupRecord,
  DepositGroupRecord,
  RewardRecord,
  PurchaseRecord,
  PeerPullPaymentInitiationRecord,
  PeerPullPaymentIncomingRecord,
  PeerPushPaymentInitiationRecord,
  PeerPushPaymentIncomingRecord,
  RefundGroupRecord,
  RefundGroupStatus,
  ExchangeEntryDbUpdateStatus,
} from "../db.js";
import {
  PendingOperationsResponse,
  PendingTaskType,
  TaskId,
} from "../pending-types.js";
import { AbsoluteTime, TransactionRecordFilter } from "@gnu-taler/taler-util";
import { InternalWalletState } from "../internal-wallet-state.js";
import { GetReadOnlyAccess } from "../util/query.js";
import { GlobalIDB } from "@gnu-taler/idb-bridge";
import { TaskIdentifiers } from "./common.js";
function getPendingCommon(
  ws: InternalWalletState,
  opTag: TaskId,
  timestampDue: AbsoluteTime,
): {
  id: TaskId;
  isDue: boolean;
  timestampDue: AbsoluteTime;
  isLongpolling: boolean;
} {
  const isDue =
    AbsoluteTime.isExpired(timestampDue) && !ws.activeLongpoll[opTag];
  return {
    id: opTag,
    isDue,
    timestampDue,
    isLongpolling: !!ws.activeLongpoll[opTag],
  };
}
async function gatherExchangePending(
  ws: InternalWalletState,
  tx: GetReadOnlyAccess<{
    exchanges: typeof WalletStoresV1.exchanges;
    operationRetries: typeof WalletStoresV1.operationRetries;
  }>,
  now: AbsoluteTime,
  resp: PendingOperationsResponse,
): Promise {
  // FIXME: We should do a range query here based on the update time
  // and/or the entry state.
  await tx.exchanges.iter().forEachAsync(async (exch) => {
    switch (exch.updateStatus) {
      case ExchangeEntryDbUpdateStatus.Initial:
      case ExchangeEntryDbUpdateStatus.Suspended:
      case ExchangeEntryDbUpdateStatus.Failed:
        return;
    }
    const opTag = TaskIdentifiers.forExchangeUpdate(exch);
    let opr = await tx.operationRetries.get(opTag);
    const timestampDue =
      opr?.retryInfo.nextRetry ??
      AbsoluteTime.fromStampMs(exch.nextUpdateStampMs);
    resp.pendingOperations.push({
      type: PendingTaskType.ExchangeUpdate,
      ...getPendingCommon(ws, opTag, timestampDue),
      givesLifeness: false,
      exchangeBaseUrl: exch.baseUrl,
      lastError: opr?.lastError,
    });
    // We only schedule a check for auto-refresh if the exchange update
    // was successful.
    if (!opr?.lastError) {
      resp.pendingOperations.push({
        type: PendingTaskType.ExchangeCheckRefresh,
        ...getPendingCommon(ws, opTag, timestampDue),
        timestampDue: AbsoluteTime.fromStampMs(exch.nextRefreshCheckStampMs),
        givesLifeness: false,
        exchangeBaseUrl: exch.baseUrl,
      });
    }
  });
}
/**
 * Iterate refresh records based on a filter.
 */
export async function iterRecordsForRefresh(
  tx: GetReadOnlyAccess<{
    refreshGroups: typeof WalletStoresV1.refreshGroups;
  }>,
  filter: TransactionRecordFilter,
  f: (r: RefreshGroupRecord) => Promise,
): Promise {
  let refreshGroups: RefreshGroupRecord[];
  if (filter.onlyState === "nonfinal") {
    const keyRange = GlobalIDB.KeyRange.bound(
      OperationStatusRange.ACTIVE_START,
      OperationStatusRange.ACTIVE_END,
    );
    refreshGroups = await tx.refreshGroups.indexes.byStatus.getAll(keyRange);
  } else {
    refreshGroups = await tx.refreshGroups.indexes.byStatus.getAll();
  }
  for (const r of refreshGroups) {
    await f(r);
  }
}
async function gatherRefreshPending(
  ws: InternalWalletState,
  tx: GetReadOnlyAccess<{
    refreshGroups: typeof WalletStoresV1.refreshGroups;
    operationRetries: typeof WalletStoresV1.operationRetries;
  }>,
  now: AbsoluteTime,
  resp: PendingOperationsResponse,
): Promise {
  await iterRecordsForRefresh(tx, { onlyState: "nonfinal" }, async (r) => {
    if (r.timestampFinished) {
      return;
    }
    const opId = TaskIdentifiers.forRefresh(r);
    const retryRecord = await tx.operationRetries.get(opId);
    const timestampDue = retryRecord?.retryInfo.nextRetry ?? AbsoluteTime.now();
    resp.pendingOperations.push({
      type: PendingTaskType.Refresh,
      ...getPendingCommon(ws, opId, timestampDue),
      givesLifeness: true,
      refreshGroupId: r.refreshGroupId,
      finishedPerCoin: r.statusPerCoin.map(
        (x) => x === RefreshCoinStatus.Finished,
      ),
      retryInfo: retryRecord?.retryInfo,
    });
  });
}
export async function iterRecordsForWithdrawal(
  tx: GetReadOnlyAccess<{
    withdrawalGroups: typeof WalletStoresV1.withdrawalGroups;
  }>,
  filter: TransactionRecordFilter,
  f: (r: WithdrawalGroupRecord) => Promise,
): Promise {
  let withdrawalGroupRecords: WithdrawalGroupRecord[];
  if (filter.onlyState === "nonfinal") {
    const range = GlobalIDB.KeyRange.bound(
      WithdrawalGroupStatus.PendingRegisteringBank,
      WithdrawalGroupStatus.PendingAml,
    );
    withdrawalGroupRecords = await tx.withdrawalGroups.indexes.byStatus.getAll(
      range,
    );
  } else {
    withdrawalGroupRecords =
      await tx.withdrawalGroups.indexes.byStatus.getAll();
  }
  for (const wgr of withdrawalGroupRecords) {
    await f(wgr);
  }
}
async function gatherWithdrawalPending(
  ws: InternalWalletState,
  tx: GetReadOnlyAccess<{
    withdrawalGroups: typeof WalletStoresV1.withdrawalGroups;
    planchets: typeof WalletStoresV1.planchets;
    operationRetries: typeof WalletStoresV1.operationRetries;
  }>,
  now: AbsoluteTime,
  resp: PendingOperationsResponse,
): Promise {
  await iterRecordsForWithdrawal(tx, { onlyState: "nonfinal" }, async (wsr) => {
    const opTag = TaskIdentifiers.forWithdrawal(wsr);
    let opr = await tx.operationRetries.get(opTag);
    const now = AbsoluteTime.now();
    if (!opr) {
      opr = {
        id: opTag,
        retryInfo: {
          firstTry: now,
          nextRetry: now,
          retryCounter: 0,
        },
      };
    }
    resp.pendingOperations.push({
      type: PendingTaskType.Withdraw,
      ...getPendingCommon(
        ws,
        opTag,
        opr.retryInfo?.nextRetry ?? AbsoluteTime.now(),
      ),
      givesLifeness: true,
      withdrawalGroupId: wsr.withdrawalGroupId,
      lastError: opr.lastError,
      retryInfo: opr.retryInfo,
    });
  });
}
export async function iterRecordsForDeposit(
  tx: GetReadOnlyAccess<{
    depositGroups: typeof WalletStoresV1.depositGroups;
  }>,
  filter: TransactionRecordFilter,
  f: (r: DepositGroupRecord) => Promise,
): Promise {
  let dgs: DepositGroupRecord[];
  if (filter.onlyState === "nonfinal") {
    dgs = await tx.depositGroups.indexes.byStatus.getAll(
      GlobalIDB.KeyRange.bound(
        DepositOperationStatus.PendingDeposit,
        DepositOperationStatus.PendingKyc,
      ),
    );
  } else {
    dgs = await tx.depositGroups.indexes.byStatus.getAll();
  }
  for (const dg of dgs) {
    await f(dg);
  }
}
async function gatherDepositPending(
  ws: InternalWalletState,
  tx: GetReadOnlyAccess<{
    depositGroups: typeof WalletStoresV1.depositGroups;
    operationRetries: typeof WalletStoresV1.operationRetries;
  }>,
  now: AbsoluteTime,
  resp: PendingOperationsResponse,
): Promise {
  await iterRecordsForDeposit(tx, { onlyState: "nonfinal" }, async (dg) => {
    let deposited = true;
    for (const d of dg.depositedPerCoin) {
      if (!d) {
        deposited = false;
      }
    }
    const opId = TaskIdentifiers.forDeposit(dg);
    const retryRecord = await tx.operationRetries.get(opId);
    const timestampDue = retryRecord?.retryInfo.nextRetry ?? AbsoluteTime.now();
    resp.pendingOperations.push({
      type: PendingTaskType.Deposit,
      ...getPendingCommon(ws, opId, timestampDue),
      // Fully deposited operations don't give lifeness,
      // because there is no reason to wait on the
      // deposit tracking status.
      givesLifeness: !deposited,
      depositGroupId: dg.depositGroupId,
      lastError: retryRecord?.lastError,
      retryInfo: retryRecord?.retryInfo,
    });
  });
}
export async function iterRecordsForReward(
  tx: GetReadOnlyAccess<{
    rewards: typeof WalletStoresV1.rewards;
  }>,
  filter: TransactionRecordFilter,
  f: (r: RewardRecord) => Promise,
): Promise {
  if (filter.onlyState === "nonfinal") {
    const range = GlobalIDB.KeyRange.bound(
      RewardRecordStatus.PendingPickup,
      RewardRecordStatus.PendingPickup,
    );
    await tx.rewards.indexes.byStatus.iter(range).forEachAsync(f);
  } else {
    await tx.rewards.indexes.byStatus.iter().forEachAsync(f);
  }
}
async function gatherRewardPending(
  ws: InternalWalletState,
  tx: GetReadOnlyAccess<{
    rewards: typeof WalletStoresV1.rewards;
    operationRetries: typeof WalletStoresV1.operationRetries;
  }>,
  now: AbsoluteTime,
  resp: PendingOperationsResponse,
): Promise {
  await iterRecordsForReward(tx, { onlyState: "nonfinal" }, async (tip) => {
    const opId = TaskIdentifiers.forTipPickup(tip);
    const retryRecord = await tx.operationRetries.get(opId);
    const timestampDue = retryRecord?.retryInfo.nextRetry ?? AbsoluteTime.now();
    if (tip.acceptedTimestamp) {
      resp.pendingOperations.push({
        type: PendingTaskType.RewardPickup,
        ...getPendingCommon(ws, opId, timestampDue),
        givesLifeness: true,
        timestampDue: retryRecord?.retryInfo.nextRetry ?? AbsoluteTime.now(),
        merchantBaseUrl: tip.merchantBaseUrl,
        tipId: tip.walletRewardId,
        merchantTipId: tip.merchantRewardId,
      });
    }
  });
}
export async function iterRecordsForRefund(
  tx: GetReadOnlyAccess<{
    refundGroups: typeof WalletStoresV1.refundGroups;
  }>,
  filter: TransactionRecordFilter,
  f: (r: RefundGroupRecord) => Promise,
): Promise {
  if (filter.onlyState === "nonfinal") {
    const keyRange = GlobalIDB.KeyRange.only(RefundGroupStatus.Pending);
    await tx.refundGroups.indexes.byStatus.iter(keyRange).forEachAsync(f);
  } else {
    await tx.refundGroups.iter().forEachAsync(f);
  }
}
export async function iterRecordsForPurchase(
  tx: GetReadOnlyAccess<{
    purchases: typeof WalletStoresV1.purchases;
  }>,
  filter: TransactionRecordFilter,
  f: (r: PurchaseRecord) => Promise,
): Promise {
  if (filter.onlyState === "nonfinal") {
    const keyRange = GlobalIDB.KeyRange.bound(
      PurchaseStatus.PendingDownloadingProposal,
      PurchaseStatus.PendingAcceptRefund,
    );
    await tx.purchases.indexes.byStatus.iter(keyRange).forEachAsync(f);
  } else {
    await tx.purchases.indexes.byStatus.iter().forEachAsync(f);
  }
}
async function gatherPurchasePending(
  ws: InternalWalletState,
  tx: GetReadOnlyAccess<{
    purchases: typeof WalletStoresV1.purchases;
    operationRetries: typeof WalletStoresV1.operationRetries;
  }>,
  now: AbsoluteTime,
  resp: PendingOperationsResponse,
): Promise {
  await iterRecordsForPurchase(tx, { onlyState: "nonfinal" }, async (pr) => {
    const opId = TaskIdentifiers.forPay(pr);
    const retryRecord = await tx.operationRetries.get(opId);
    const timestampDue = retryRecord?.retryInfo.nextRetry ?? AbsoluteTime.now();
    resp.pendingOperations.push({
      type: PendingTaskType.Purchase,
      ...getPendingCommon(ws, opId, timestampDue),
      givesLifeness: true,
      statusStr: PurchaseStatus[pr.purchaseStatus],
      proposalId: pr.proposalId,
      retryInfo: retryRecord?.retryInfo,
      lastError: retryRecord?.lastError,
    });
  });
}
async function gatherRecoupPending(
  ws: InternalWalletState,
  tx: GetReadOnlyAccess<{
    recoupGroups: typeof WalletStoresV1.recoupGroups;
    operationRetries: typeof WalletStoresV1.operationRetries;
  }>,
  now: AbsoluteTime,
  resp: PendingOperationsResponse,
): Promise {
  // FIXME: Have a status field!
  await tx.recoupGroups.iter().forEachAsync(async (rg) => {
    if (rg.timestampFinished) {
      return;
    }
    const opId = TaskIdentifiers.forRecoup(rg);
    const retryRecord = await tx.operationRetries.get(opId);
    const timestampDue = retryRecord?.retryInfo.nextRetry ?? AbsoluteTime.now();
    resp.pendingOperations.push({
      type: PendingTaskType.Recoup,
      ...getPendingCommon(ws, opId, timestampDue),
      givesLifeness: true,
      recoupGroupId: rg.recoupGroupId,
      retryInfo: retryRecord?.retryInfo,
      lastError: retryRecord?.lastError,
    });
  });
}
async function gatherBackupPending(
  ws: InternalWalletState,
  tx: GetReadOnlyAccess<{
    backupProviders: typeof WalletStoresV1.backupProviders;
    operationRetries: typeof WalletStoresV1.operationRetries;
  }>,
  now: AbsoluteTime,
  resp: PendingOperationsResponse,
): Promise {
  await tx.backupProviders.iter().forEachAsync(async (bp) => {
    const opId = TaskIdentifiers.forBackup(bp);
    const retryRecord = await tx.operationRetries.get(opId);
    if (bp.state.tag === BackupProviderStateTag.Ready) {
      const timestampDue = AbsoluteTime.fromPreciseTimestamp(
        bp.state.nextBackupTimestamp,
      );
      resp.pendingOperations.push({
        type: PendingTaskType.Backup,
        ...getPendingCommon(ws, opId, timestampDue),
        givesLifeness: false,
        backupProviderBaseUrl: bp.baseUrl,
        lastError: undefined,
      });
    } else if (bp.state.tag === BackupProviderStateTag.Retrying) {
      const timestampDue =
        retryRecord?.retryInfo?.nextRetry ?? AbsoluteTime.now();
      resp.pendingOperations.push({
        type: PendingTaskType.Backup,
        ...getPendingCommon(ws, opId, timestampDue),
        givesLifeness: false,
        backupProviderBaseUrl: bp.baseUrl,
        retryInfo: retryRecord?.retryInfo,
        lastError: retryRecord?.lastError,
      });
    }
  });
}
export async function iterRecordsForPeerPullInitiation(
  tx: GetReadOnlyAccess<{
    peerPullPaymentInitiations: typeof WalletStoresV1.peerPullPaymentInitiations;
  }>,
  filter: TransactionRecordFilter,
  f: (r: PeerPullPaymentInitiationRecord) => Promise,
): Promise {
  if (filter.onlyState === "nonfinal") {
    const keyRange = GlobalIDB.KeyRange.bound(
      PeerPullPaymentInitiationStatus.PendingCreatePurse,
      PeerPullPaymentInitiationStatus.AbortingDeletePurse,
    );
    await tx.peerPullPaymentInitiations.indexes.byStatus
      .iter(keyRange)
      .forEachAsync(f);
  } else {
    await tx.peerPullPaymentInitiations.indexes.byStatus.iter().forEachAsync(f);
  }
}
async function gatherPeerPullInitiationPending(
  ws: InternalWalletState,
  tx: GetReadOnlyAccess<{
    peerPullPaymentInitiations: typeof WalletStoresV1.peerPullPaymentInitiations;
    operationRetries: typeof WalletStoresV1.operationRetries;
  }>,
  now: AbsoluteTime,
  resp: PendingOperationsResponse,
): Promise {
  await iterRecordsForPeerPullInitiation(
    tx,
    { onlyState: "nonfinal" },
    async (pi) => {
      const opId = TaskIdentifiers.forPeerPullPaymentInitiation(pi);
      const retryRecord = await tx.operationRetries.get(opId);
      const timestampDue =
        retryRecord?.retryInfo.nextRetry ?? AbsoluteTime.now();
      resp.pendingOperations.push({
        type: PendingTaskType.PeerPullCredit,
        ...getPendingCommon(ws, opId, timestampDue),
        givesLifeness: true,
        retryInfo: retryRecord?.retryInfo,
        pursePub: pi.pursePub,
      });
    },
  );
}
export async function iterRecordsForPeerPullDebit(
  tx: GetReadOnlyAccess<{
    peerPullPaymentIncoming: typeof WalletStoresV1.peerPullPaymentIncoming;
  }>,
  filter: TransactionRecordFilter,
  f: (r: PeerPullPaymentIncomingRecord) => Promise,
): Promise {
  if (filter.onlyState === "nonfinal") {
    const keyRange = GlobalIDB.KeyRange.bound(
      PeerPullDebitRecordStatus.PendingDeposit,
      PeerPullDebitRecordStatus.AbortingRefresh,
    );
    await tx.peerPullPaymentIncoming.indexes.byStatus
      .iter(keyRange)
      .forEachAsync(f);
  } else {
    await tx.peerPullPaymentIncoming.indexes.byStatus.iter().forEachAsync(f);
  }
}
async function gatherPeerPullDebitPending(
  ws: InternalWalletState,
  tx: GetReadOnlyAccess<{
    peerPullPaymentIncoming: typeof WalletStoresV1.peerPullPaymentIncoming;
    operationRetries: typeof WalletStoresV1.operationRetries;
  }>,
  now: AbsoluteTime,
  resp: PendingOperationsResponse,
): Promise {
  await iterRecordsForPeerPullDebit(
    tx,
    { onlyState: "nonfinal" },
    async (pi) => {
      const opId = TaskIdentifiers.forPeerPullPaymentDebit(pi);
      const retryRecord = await tx.operationRetries.get(opId);
      const timestampDue =
        retryRecord?.retryInfo.nextRetry ?? AbsoluteTime.now();
      resp.pendingOperations.push({
        type: PendingTaskType.PeerPullDebit,
        ...getPendingCommon(ws, opId, timestampDue),
        givesLifeness: true,
        retryInfo: retryRecord?.retryInfo,
        peerPullPaymentIncomingId: pi.peerPullPaymentIncomingId,
      });
    },
  );
}
export async function iterRecordsForPeerPushInitiation(
  tx: GetReadOnlyAccess<{
    peerPushPaymentInitiations: typeof WalletStoresV1.peerPushPaymentInitiations;
  }>,
  filter: TransactionRecordFilter,
  f: (r: PeerPushPaymentInitiationRecord) => Promise,
): Promise {
  if (filter.onlyState === "nonfinal") {
    const keyRange = GlobalIDB.KeyRange.bound(
      PeerPushPaymentInitiationStatus.PendingCreatePurse,
      PeerPushPaymentInitiationStatus.AbortingRefresh,
    );
    await tx.peerPushPaymentInitiations.indexes.byStatus
      .iter(keyRange)
      .forEachAsync(f);
  } else {
    await tx.peerPushPaymentInitiations.indexes.byStatus.iter().forEachAsync(f);
  }
}
async function gatherPeerPushInitiationPending(
  ws: InternalWalletState,
  tx: GetReadOnlyAccess<{
    peerPushPaymentInitiations: typeof WalletStoresV1.peerPushPaymentInitiations;
    operationRetries: typeof WalletStoresV1.operationRetries;
  }>,
  now: AbsoluteTime,
  resp: PendingOperationsResponse,
): Promise {
  await iterRecordsForPeerPushInitiation(
    tx,
    { onlyState: "nonfinal" },
    async (pi) => {
      const opId = TaskIdentifiers.forPeerPushPaymentInitiation(pi);
      const retryRecord = await tx.operationRetries.get(opId);
      const timestampDue =
        retryRecord?.retryInfo.nextRetry ?? AbsoluteTime.now();
      resp.pendingOperations.push({
        type: PendingTaskType.PeerPushDebit,
        ...getPendingCommon(ws, opId, timestampDue),
        givesLifeness: true,
        retryInfo: retryRecord?.retryInfo,
        pursePub: pi.pursePub,
      });
    },
  );
}
export async function iterRecordsForPeerPushCredit(
  tx: GetReadOnlyAccess<{
    peerPushPaymentIncoming: typeof WalletStoresV1.peerPushPaymentIncoming;
  }>,
  filter: TransactionRecordFilter,
  f: (r: PeerPushPaymentIncomingRecord) => Promise,
): Promise {
  if (filter.onlyState === "nonfinal") {
    const keyRange = GlobalIDB.KeyRange.bound(
      PeerPushPaymentIncomingStatus.PendingMerge,
      PeerPushPaymentIncomingStatus.PendingWithdrawing,
    );
    await tx.peerPushPaymentIncoming.indexes.byStatus
      .iter(keyRange)
      .forEachAsync(f);
  } else {
    await tx.peerPushPaymentIncoming.indexes.byStatus.iter().forEachAsync(f);
  }
}
async function gatherPeerPushCreditPending(
  ws: InternalWalletState,
  tx: GetReadOnlyAccess<{
    peerPushPaymentIncoming: typeof WalletStoresV1.peerPushPaymentIncoming;
    operationRetries: typeof WalletStoresV1.operationRetries;
  }>,
  now: AbsoluteTime,
  resp: PendingOperationsResponse,
): Promise {
  const keyRange = GlobalIDB.KeyRange.bound(
    PeerPushPaymentIncomingStatus.PendingMerge,
    PeerPushPaymentIncomingStatus.PendingWithdrawing,
  );
  await iterRecordsForPeerPushCredit(
    tx,
    { onlyState: "nonfinal" },
    async (pi) => {
      const opId = TaskIdentifiers.forPeerPushCredit(pi);
      const retryRecord = await tx.operationRetries.get(opId);
      const timestampDue =
        retryRecord?.retryInfo.nextRetry ?? AbsoluteTime.now();
      resp.pendingOperations.push({
        type: PendingTaskType.PeerPushCredit,
        ...getPendingCommon(ws, opId, timestampDue),
        givesLifeness: true,
        retryInfo: retryRecord?.retryInfo,
        peerPushPaymentIncomingId: pi.peerPushPaymentIncomingId,
      });
    },
  );
}
export async function getPendingOperations(
  ws: InternalWalletState,
): Promise {
  const now = AbsoluteTime.now();
  return await ws.db
    .mktx((x) => [
      x.backupProviders,
      x.exchanges,
      x.exchangeDetails,
      x.refreshGroups,
      x.coins,
      x.withdrawalGroups,
      x.rewards,
      x.purchases,
      x.planchets,
      x.depositGroups,
      x.recoupGroups,
      x.operationRetries,
      x.peerPullPaymentInitiations,
      x.peerPushPaymentInitiations,
      x.peerPullPaymentIncoming,
      x.peerPushPaymentIncoming,
    ])
    .runReadWrite(async (tx) => {
      const resp: PendingOperationsResponse = {
        pendingOperations: [],
      };
      await gatherExchangePending(ws, tx, now, resp);
      await gatherRefreshPending(ws, tx, now, resp);
      await gatherWithdrawalPending(ws, tx, now, resp);
      await gatherDepositPending(ws, tx, now, resp);
      await gatherRewardPending(ws, tx, now, resp);
      await gatherPurchasePending(ws, tx, now, resp);
      await gatherRecoupPending(ws, tx, now, resp);
      await gatherBackupPending(ws, tx, now, resp);
      await gatherPeerPushInitiationPending(ws, tx, now, resp);
      await gatherPeerPullInitiationPending(ws, tx, now, resp);
      await gatherPeerPullDebitPending(ws, tx, now, resp);
      await gatherPeerPushCreditPending(ws, tx, now, resp);
      return resp;
    });
}