/* 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 { ProposalStatus, AbortStatus, WalletStoresV1, BackupProviderStateTag, RefreshCoinStatus, OperationStatus, } from "../db.js"; import { PendingOperationsResponse, PendingTaskType, } from "../pending-types.js"; import { AbsoluteTime } from "@gnu-taler/taler-util"; import { InternalWalletState } from "../internal-wallet-state.js"; import { GetReadOnlyAccess } from "../util/query.js"; import { RetryTags } from "../util/retries.js"; import { Wallet } from "../wallet.js"; async function gatherExchangePending( tx: GetReadOnlyAccess<{ exchanges: typeof WalletStoresV1.exchanges; exchangeDetails: typeof WalletStoresV1.exchangeDetails; operationRetries: typeof WalletStoresV1.operationRetries; }>, now: AbsoluteTime, resp: PendingOperationsResponse, ): Promise { await tx.exchanges.iter().forEachAsync(async (exch) => { const opTag = RetryTags.forExchangeUpdate(exch); let opr = await tx.operationRetries.get(opTag); resp.pendingOperations.push({ type: PendingTaskType.ExchangeUpdate, id: opTag, givesLifeness: false, timestampDue: opr?.retryInfo.nextRetry ?? AbsoluteTime.fromTimestamp(exch.nextUpdate), 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, id: RetryTags.forExchangeCheckRefresh(exch), timestampDue: AbsoluteTime.fromTimestamp(exch.nextRefreshCheck), givesLifeness: false, exchangeBaseUrl: exch.baseUrl, }); } }); } async function gatherRefreshPending( tx: GetReadOnlyAccess<{ refreshGroups: typeof WalletStoresV1.refreshGroups; operationRetries: typeof WalletStoresV1.operationRetries; }>, now: AbsoluteTime, resp: PendingOperationsResponse, ): Promise { const refreshGroups = await tx.refreshGroups.indexes.byStatus.getAll( OperationStatus.Pending, ); for (const r of refreshGroups) { if (r.timestampFinished) { return; } if (r.frozen) { return; } const opId = RetryTags.forRefresh(r); const retryRecord = await tx.operationRetries.get(opId); resp.pendingOperations.push({ type: PendingTaskType.Refresh, id: opId, givesLifeness: true, timestampDue: retryRecord?.retryInfo.nextRetry ?? AbsoluteTime.now(), refreshGroupId: r.refreshGroupId, finishedPerCoin: r.statusPerCoin.map( (x) => x === RefreshCoinStatus.Finished, ), retryInfo: retryRecord?.retryInfo, }); } } async function gatherWithdrawalPending( tx: GetReadOnlyAccess<{ withdrawalGroups: typeof WalletStoresV1.withdrawalGroups; planchets: typeof WalletStoresV1.planchets; operationRetries: typeof WalletStoresV1.operationRetries; }>, now: AbsoluteTime, resp: PendingOperationsResponse, ): Promise { const wsrs = await tx.withdrawalGroups.indexes.byStatus.getAll( OperationStatus.Pending, ); for (const wsr of wsrs) { if (wsr.timestampFinish) { return; } const opTag = RetryTags.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, id: opTag, givesLifeness: true, timestampDue: opr.retryInfo?.nextRetry ?? AbsoluteTime.now(), withdrawalGroupId: wsr.withdrawalGroupId, lastError: opr.lastError, retryInfo: opr.retryInfo, }); } } async function gatherProposalPending( tx: GetReadOnlyAccess<{ proposals: typeof WalletStoresV1.proposals; operationRetries: typeof WalletStoresV1.operationRetries; }>, now: AbsoluteTime, resp: PendingOperationsResponse, ): Promise { await tx.proposals.iter().forEachAsync(async (proposal) => { if (proposal.proposalStatus == ProposalStatus.Proposed) { // Nothing to do, user needs to choose. } else if (proposal.proposalStatus == ProposalStatus.Downloading) { const opId = RetryTags.forProposalClaim(proposal); const retryRecord = await tx.operationRetries.get(opId); const timestampDue = retryRecord?.retryInfo?.nextRetry ?? AbsoluteTime.now(); resp.pendingOperations.push({ type: PendingTaskType.ProposalDownload, id: opId, givesLifeness: true, timestampDue, merchantBaseUrl: proposal.merchantBaseUrl, orderId: proposal.orderId, proposalId: proposal.proposalId, proposalTimestamp: proposal.timestamp, lastError: retryRecord?.lastError, retryInfo: retryRecord?.retryInfo, }); } }); } async function gatherDepositPending( tx: GetReadOnlyAccess<{ depositGroups: typeof WalletStoresV1.depositGroups; operationRetries: typeof WalletStoresV1.operationRetries; }>, now: AbsoluteTime, resp: PendingOperationsResponse, ): Promise { const dgs = await tx.depositGroups.indexes.byStatus.getAll( OperationStatus.Pending, ); for (const dg of dgs) { if (dg.timestampFinished) { return; } const opId = RetryTags.forDeposit(dg); const retryRecord = await tx.operationRetries.get(opId); const timestampDue = retryRecord?.retryInfo.nextRetry ?? AbsoluteTime.now(); resp.pendingOperations.push({ type: PendingTaskType.Deposit, id: opId, givesLifeness: true, timestampDue, depositGroupId: dg.depositGroupId, lastError: retryRecord?.lastError, retryInfo: retryRecord?.retryInfo, }); } } async function gatherTipPending( tx: GetReadOnlyAccess<{ tips: typeof WalletStoresV1.tips; operationRetries: typeof WalletStoresV1.operationRetries; }>, now: AbsoluteTime, resp: PendingOperationsResponse, ): Promise { await tx.tips.iter().forEachAsync(async (tip) => { // FIXME: The tip record needs a proper status field! if (tip.pickedUpTimestamp) { return; } const opId = RetryTags.forTipPickup(tip); const retryRecord = await tx.operationRetries.get(opId); if (tip.acceptedTimestamp) { resp.pendingOperations.push({ type: PendingTaskType.TipPickup, id: opId, givesLifeness: true, timestampDue: retryRecord?.retryInfo.nextRetry ?? AbsoluteTime.now(), merchantBaseUrl: tip.merchantBaseUrl, tipId: tip.walletTipId, merchantTipId: tip.merchantTipId, }); } }); } async function gatherPurchasePending( tx: GetReadOnlyAccess<{ purchases: typeof WalletStoresV1.purchases; operationRetries: typeof WalletStoresV1.operationRetries; }>, now: AbsoluteTime, resp: PendingOperationsResponse, ): Promise { // FIXME: Only iter purchases with some "active" flag! await tx.purchases.iter().forEachAsync(async (pr) => { if ( pr.paymentSubmitPending && pr.abortStatus === AbortStatus.None && !pr.payFrozen ) { const payOpId = RetryTags.forPay(pr); const payRetryRecord = await tx.operationRetries.get(payOpId); const timestampDue = payRetryRecord?.retryInfo.nextRetry ?? AbsoluteTime.now(); resp.pendingOperations.push({ type: PendingTaskType.Pay, id: payOpId, givesLifeness: true, timestampDue, isReplay: false, proposalId: pr.proposalId, retryInfo: payRetryRecord?.retryInfo, lastError: payRetryRecord?.lastError, }); } if (pr.refundQueryRequested) { const refundQueryOpId = RetryTags.forRefundQuery(pr); const refundQueryRetryRecord = await tx.operationRetries.get( refundQueryOpId, ); resp.pendingOperations.push({ type: PendingTaskType.RefundQuery, id: refundQueryOpId, givesLifeness: true, timestampDue: refundQueryRetryRecord?.retryInfo.nextRetry ?? AbsoluteTime.now(), proposalId: pr.proposalId, retryInfo: refundQueryRetryRecord?.retryInfo, lastError: refundQueryRetryRecord?.lastError, }); } }); } async function gatherRecoupPending( tx: GetReadOnlyAccess<{ recoupGroups: typeof WalletStoresV1.recoupGroups; operationRetries: typeof WalletStoresV1.operationRetries; }>, now: AbsoluteTime, resp: PendingOperationsResponse, ): Promise { await tx.recoupGroups.iter().forEachAsync(async (rg) => { if (rg.timestampFinished) { return; } const opId = RetryTags.forRecoup(rg); const retryRecord = await tx.operationRetries.get(opId); resp.pendingOperations.push({ type: PendingTaskType.Recoup, id: opId, givesLifeness: true, timestampDue: retryRecord?.retryInfo.nextRetry ?? AbsoluteTime.now(), recoupGroupId: rg.recoupGroupId, retryInfo: retryRecord?.retryInfo, lastError: retryRecord?.lastError, }); }); } async function gatherBackupPending( tx: GetReadOnlyAccess<{ backupProviders: typeof WalletStoresV1.backupProviders; operationRetries: typeof WalletStoresV1.operationRetries; }>, now: AbsoluteTime, resp: PendingOperationsResponse, ): Promise { await tx.backupProviders.iter().forEachAsync(async (bp) => { const opId = RetryTags.forBackup(bp); const retryRecord = await tx.operationRetries.get(opId); if (bp.state.tag === BackupProviderStateTag.Ready) { resp.pendingOperations.push({ type: PendingTaskType.Backup, id: opId, givesLifeness: false, timestampDue: AbsoluteTime.fromTimestamp(bp.state.nextBackupTimestamp), backupProviderBaseUrl: bp.baseUrl, lastError: undefined, }); } else if (bp.state.tag === BackupProviderStateTag.Retrying) { resp.pendingOperations.push({ type: PendingTaskType.Backup, id: opId, givesLifeness: false, timestampDue: retryRecord?.retryInfo?.nextRetry ?? AbsoluteTime.now(), backupProviderBaseUrl: bp.baseUrl, retryInfo: retryRecord?.retryInfo, lastError: retryRecord?.lastError, }); } }); } 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.proposals, x.tips, x.purchases, x.planchets, x.depositGroups, x.recoupGroups, x.operationRetries, ]) .runReadWrite(async (tx) => { const resp: PendingOperationsResponse = { pendingOperations: [], }; await gatherExchangePending(tx, now, resp); await gatherRefreshPending(tx, now, resp); await gatherWithdrawalPending(tx, now, resp); await gatherProposalPending(tx, now, resp); await gatherDepositPending(tx, now, resp); await gatherTipPending(tx, now, resp); await gatherPurchasePending(tx, now, resp); await gatherRecoupPending(tx, now, resp); await gatherBackupPending(tx, now, resp); return resp; }); }