/* 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 { GlobalIDB } from "@gnu-taler/idb-bridge"; import { AbsoluteTime, TalerErrorDetail, TalerPreciseTimestamp, TransactionRecordFilter, } from "@gnu-taler/taler-util"; import { BackupProviderStateTag, DbPreciseTimestamp, DepositElementStatus, DepositGroupRecord, ExchangeEntryDbUpdateStatus, OPERATION_STATUS_ACTIVE_FIRST, OPERATION_STATUS_ACTIVE_LAST, PeerPullCreditRecord, PeerPullDebitRecordStatus, PeerPullPaymentCreditStatus, PeerPullPaymentIncomingRecord, PeerPushCreditStatus, PeerPushDebitRecord, PeerPushDebitStatus, PeerPushPaymentIncomingRecord, PurchaseRecord, PurchaseStatus, RefreshCoinStatus, RefreshGroupRecord, RefreshOperationStatus, RefundGroupRecord, RefundGroupStatus, RewardRecord, RewardRecordStatus, WalletStoresV1, WithdrawalGroupRecord, depositOperationNonfinalStatusRange, timestampAbsoluteFromDb, timestampOptionalAbsoluteFromDb, timestampPreciseFromDb, timestampPreciseToDb, withdrawalGroupNonfinalRange, } from "../db.js"; import { InternalWalletState } from "../internal-wallet-state.js"; import { PendingOperationsResponse, PendingTaskType, TaskId, } from "../pending-types.js"; import { GetReadOnlyAccess } from "../util/query.js"; 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 { let timestampDue: DbPreciseTimestamp | undefined = undefined; await tx.exchanges.iter().forEachAsync(async (exch) => { switch (exch.updateStatus) { case ExchangeEntryDbUpdateStatus.Initial: case ExchangeEntryDbUpdateStatus.Suspended: return; } const opUpdateExchangeTag = TaskIdentifiers.forExchangeUpdate(exch); let opr = await tx.operationRetries.get(opUpdateExchangeTag); switch (exch.updateStatus) { case ExchangeEntryDbUpdateStatus.Ready: timestampDue = opr?.retryInfo.nextRetry ?? exch.nextRefreshCheckStamp; break; case ExchangeEntryDbUpdateStatus.ReadyUpdate: case ExchangeEntryDbUpdateStatus.InitialUpdate: case ExchangeEntryDbUpdateStatus.UnavailableUpdate: timestampDue = opr?.retryInfo.nextRetry ?? timestampPreciseToDb(TalerPreciseTimestamp.now()); break; } resp.pendingOperations.push({ type: PendingTaskType.ExchangeUpdate, ...getPendingCommon( ws, opUpdateExchangeTag, AbsoluteTime.fromPreciseTimestamp(timestampPreciseFromDb(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) { const opCheckRefreshTag = TaskIdentifiers.forExchangeCheckRefresh(exch); resp.pendingOperations.push({ type: PendingTaskType.ExchangeCheckRefresh, ...getPendingCommon( ws, opCheckRefreshTag, AbsoluteTime.fromPreciseTimestamp( timestampPreciseFromDb(timestampDue), ), ), timestampDue: AbsoluteTime.fromPreciseTimestamp( timestampPreciseFromDb(exch.nextRefreshCheckStamp), ), 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( RefreshOperationStatus.Pending, RefreshOperationStatus.Suspended, ); 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 = timestampOptionalAbsoluteFromDb(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") { withdrawalGroupRecords = await tx.withdrawalGroups.indexes.byStatus.getAll( withdrawalGroupNonfinalRange, ); } 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); /** * kyc pending operation don't give lifeness * since the user need to complete kyc procedure */ const userNeedToCompleteKYC = wsr.kycUrl !== undefined; const now = AbsoluteTime.now(); if (!opr) { opr = { id: opTag, retryInfo: { firstTry: timestampPreciseToDb(AbsoluteTime.toPreciseTimestamp(now)), nextRetry: timestampPreciseToDb(AbsoluteTime.toPreciseTimestamp(now)), retryCounter: 0, }, }; } resp.pendingOperations.push({ type: PendingTaskType.Withdraw, ...getPendingCommon( ws, opTag, timestampOptionalAbsoluteFromDb(opr.retryInfo?.nextRetry) ?? AbsoluteTime.now(), ), givesLifeness: !userNeedToCompleteKYC, 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( depositOperationNonfinalStatusRange, ); } 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.statusPerCoin) { if (d === DepositElementStatus.DepositPending) { deposited = false; } } /** * kyc pending operation don't give lifeness * since the user need to complete kyc procedure */ const userNeedToCompleteKYC = dg.kycInfo !== undefined; const opId = TaskIdentifiers.forDeposit(dg); const retryRecord = await tx.operationRetries.get(opId); const timestampDue = timestampOptionalAbsoluteFromDb(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 && !userNeedToCompleteKYC, 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 = timestampOptionalAbsoluteFromDb(retryRecord?.retryInfo.nextRetry) ?? AbsoluteTime.now(); /** * kyc pending operation don't give lifeness * since the user need to complete kyc procedure */ // const userNeedToCompleteKYC = tip. if (tip.acceptedTimestamp) { resp.pendingOperations.push({ type: PendingTaskType.RewardPickup, ...getPendingCommon(ws, opId, timestampDue), givesLifeness: true, timestampDue, 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( OPERATION_STATUS_ACTIVE_FIRST, OPERATION_STATUS_ACTIVE_LAST, ); 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 = timestampOptionalAbsoluteFromDb(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 = timestampOptionalAbsoluteFromDb(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 = timestampAbsoluteFromDb( 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 = timestampOptionalAbsoluteFromDb(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<{ peerPullCredit: typeof WalletStoresV1.peerPullCredit; }>, filter: TransactionRecordFilter, f: (r: PeerPullCreditRecord) => Promise, ): Promise { if (filter.onlyState === "nonfinal") { const keyRange = GlobalIDB.KeyRange.bound( PeerPullPaymentCreditStatus.PendingCreatePurse, PeerPullPaymentCreditStatus.AbortingDeletePurse, ); await tx.peerPullCredit.indexes.byStatus.iter(keyRange).forEachAsync(f); } else { await tx.peerPullCredit.indexes.byStatus.iter().forEachAsync(f); } } async function gatherPeerPullInitiationPending( ws: InternalWalletState, tx: GetReadOnlyAccess<{ peerPullCredit: typeof WalletStoresV1.peerPullCredit; 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 = timestampOptionalAbsoluteFromDb(retryRecord?.retryInfo.nextRetry) ?? AbsoluteTime.now(); /** * kyc pending operation don't give lifeness * since the user need to complete kyc procedure */ const userNeedToCompleteKYC = pi.kycUrl !== undefined; resp.pendingOperations.push({ type: PendingTaskType.PeerPullCredit, ...getPendingCommon(ws, opId, timestampDue), givesLifeness: !userNeedToCompleteKYC, retryInfo: retryRecord?.retryInfo, pursePub: pi.pursePub, internalOperationStatus: `0x${pi.status.toString(16)}`, }); }, ); } export async function iterRecordsForPeerPullDebit( tx: GetReadOnlyAccess<{ peerPullDebit: typeof WalletStoresV1.peerPullDebit; }>, filter: TransactionRecordFilter, f: (r: PeerPullPaymentIncomingRecord) => Promise, ): Promise { if (filter.onlyState === "nonfinal") { const keyRange = GlobalIDB.KeyRange.bound( PeerPullDebitRecordStatus.PendingDeposit, PeerPullDebitRecordStatus.AbortingRefresh, ); await tx.peerPullDebit.indexes.byStatus.iter(keyRange).forEachAsync(f); } else { await tx.peerPullDebit.indexes.byStatus.iter().forEachAsync(f); } } async function gatherPeerPullDebitPending( ws: InternalWalletState, tx: GetReadOnlyAccess<{ peerPullDebit: typeof WalletStoresV1.peerPullDebit; 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 = timestampOptionalAbsoluteFromDb(retryRecord?.retryInfo.nextRetry) ?? AbsoluteTime.now(); switch (pi.status) { case PeerPullDebitRecordStatus.DialogProposed: return; } resp.pendingOperations.push({ type: PendingTaskType.PeerPullDebit, ...getPendingCommon(ws, opId, timestampDue), givesLifeness: true, retryInfo: retryRecord?.retryInfo, peerPullDebitId: pi.peerPullDebitId, internalOperationStatus: `0x${pi.status.toString(16)}`, }); }, ); } export async function iterRecordsForPeerPushInitiation( tx: GetReadOnlyAccess<{ peerPushDebit: typeof WalletStoresV1.peerPushDebit; }>, filter: TransactionRecordFilter, f: (r: PeerPushDebitRecord) => Promise, ): Promise { if (filter.onlyState === "nonfinal") { const keyRange = GlobalIDB.KeyRange.bound( PeerPushDebitStatus.PendingCreatePurse, PeerPushDebitStatus.AbortingRefreshExpired, ); await tx.peerPushDebit.indexes.byStatus.iter(keyRange).forEachAsync(f); } else { await tx.peerPushDebit.indexes.byStatus.iter().forEachAsync(f); } } async function gatherPeerPushInitiationPending( ws: InternalWalletState, tx: GetReadOnlyAccess<{ peerPushDebit: typeof WalletStoresV1.peerPushDebit; 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 = timestampOptionalAbsoluteFromDb(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<{ peerPushCredit: typeof WalletStoresV1.peerPushCredit; }>, filter: TransactionRecordFilter, f: (r: PeerPushPaymentIncomingRecord) => Promise, ): Promise { if (filter.onlyState === "nonfinal") { const keyRange = GlobalIDB.KeyRange.bound( PeerPushCreditStatus.PendingMerge, PeerPushCreditStatus.PendingWithdrawing, ); await tx.peerPushCredit.indexes.byStatus.iter(keyRange).forEachAsync(f); } else { await tx.peerPushCredit.indexes.byStatus.iter().forEachAsync(f); } } async function gatherPeerPushCreditPending( ws: InternalWalletState, tx: GetReadOnlyAccess<{ peerPushCredit: typeof WalletStoresV1.peerPushCredit; operationRetries: typeof WalletStoresV1.operationRetries; }>, now: AbsoluteTime, resp: PendingOperationsResponse, ): Promise { const keyRange = GlobalIDB.KeyRange.bound( PeerPushCreditStatus.PendingMerge, PeerPushCreditStatus.PendingWithdrawing, ); await iterRecordsForPeerPushCredit( tx, { onlyState: "nonfinal" }, async (pi) => { const opId = TaskIdentifiers.forPeerPushCredit(pi); const retryRecord = await tx.operationRetries.get(opId); const timestampDue = timestampOptionalAbsoluteFromDb(retryRecord?.retryInfo.nextRetry) ?? AbsoluteTime.now(); /** * kyc pending operation don't give lifeness * since the user need to complete kyc procedure */ const userNeedToCompleteKYC = pi.kycUrl !== undefined; resp.pendingOperations.push({ type: PendingTaskType.PeerPushCredit, ...getPendingCommon(ws, opId, timestampDue), givesLifeness: !userNeedToCompleteKYC, retryInfo: retryRecord?.retryInfo, peerPushCreditId: pi.peerPushCreditId, }); }, ); } const taskPrio: { [X in PendingTaskType]: number } = { [PendingTaskType.Deposit]: 2, [PendingTaskType.ExchangeUpdate]: 1, [PendingTaskType.PeerPullCredit]: 2, [PendingTaskType.PeerPullDebit]: 2, [PendingTaskType.PeerPushCredit]: 2, [PendingTaskType.Purchase]: 2, [PendingTaskType.Recoup]: 3, [PendingTaskType.RewardPickup]: 2, [PendingTaskType.Refresh]: 3, [PendingTaskType.Withdraw]: 3, [PendingTaskType.ExchangeCheckRefresh]: 3, [PendingTaskType.PeerPushDebit]: 2, [PendingTaskType.Backup]: 4, }; export async function getPendingOperations( ws: InternalWalletState, ): Promise { const now = AbsoluteTime.now(); const resp = 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.peerPullCredit, x.peerPushDebit, x.peerPullDebit, x.peerPushCredit, ]) .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; }); resp.pendingOperations.sort((a, b) => { let prioA = taskPrio[a.type]; let prioB = taskPrio[b.type]; return Math.sign(prioA - prioB); }); return resp; }