diff options
author | Florian Dold <florian.dold@gmail.com> | 2019-12-12 20:53:15 +0100 |
---|---|---|
committer | Florian Dold <florian.dold@gmail.com> | 2019-12-12 20:53:15 +0100 |
commit | 74433c3e05734aa1194049fcbcaa92c70ce61c74 (patch) | |
tree | d30e79c9ac3fd5720de628f6a9764354ec69c648 /src/operations/pending.ts | |
parent | cc137c87394ec34d2f54d69fe896dfdf3feec5ea (diff) | |
download | wallet-core-74433c3e05734aa1194049fcbcaa92c70ce61c74.tar.xz |
refactor: re-structure type definitions
Diffstat (limited to 'src/operations/pending.ts')
-rw-r--r-- | src/operations/pending.ts | 452 |
1 files changed, 452 insertions, 0 deletions
diff --git a/src/operations/pending.ts b/src/operations/pending.ts new file mode 100644 index 000000000..b9fc1d203 --- /dev/null +++ b/src/operations/pending.ts @@ -0,0 +1,452 @@ +/* + 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 <http://www.gnu.org/licenses/> + */ + +/** + * Imports. + */ +import { + getTimestampNow, + Timestamp, + Duration, +} from "../types/walletTypes"; +import { runWithReadTransaction, TransactionHandle } from "../util/query"; +import { InternalWalletState } from "./state"; +import { + Stores, + ExchangeUpdateStatus, + ReserveRecordStatus, + CoinStatus, + ProposalStatus, +} from "../types/dbTypes"; +import { PendingOperationsResponse } from "../types/pending"; + +function updateRetryDelay( + oldDelay: Duration, + now: Timestamp, + retryTimestamp: Timestamp, +): Duration { + if (retryTimestamp.t_ms <= now.t_ms) { + return { d_ms: 0 }; + } + return { d_ms: Math.min(oldDelay.d_ms, retryTimestamp.t_ms - now.t_ms) }; +} + +async function gatherExchangePending( + tx: TransactionHandle, + now: Timestamp, + resp: PendingOperationsResponse, + onlyDue: boolean = false, +): Promise<void> { + if (onlyDue) { + // FIXME: exchanges should also be updated regularly + return; + } + await tx.iter(Stores.exchanges).forEach(e => { + switch (e.updateStatus) { + case ExchangeUpdateStatus.FINISHED: + if (e.lastError) { + resp.pendingOperations.push({ + type: "bug", + givesLifeness: false, + message: + "Exchange record is in FINISHED state but has lastError set", + details: { + exchangeBaseUrl: e.baseUrl, + }, + }); + } + if (!e.details) { + resp.pendingOperations.push({ + type: "bug", + givesLifeness: false, + message: + "Exchange record does not have details, but no update in progress.", + details: { + exchangeBaseUrl: e.baseUrl, + }, + }); + } + if (!e.wireInfo) { + resp.pendingOperations.push({ + type: "bug", + givesLifeness: false, + message: + "Exchange record does not have wire info, but no update in progress.", + details: { + exchangeBaseUrl: e.baseUrl, + }, + }); + } + break; + case ExchangeUpdateStatus.FETCH_KEYS: + resp.pendingOperations.push({ + type: "exchange-update", + givesLifeness: false, + stage: "fetch-keys", + exchangeBaseUrl: e.baseUrl, + lastError: e.lastError, + reason: e.updateReason || "unknown", + }); + break; + case ExchangeUpdateStatus.FETCH_WIRE: + resp.pendingOperations.push({ + type: "exchange-update", + givesLifeness: false, + stage: "fetch-wire", + exchangeBaseUrl: e.baseUrl, + lastError: e.lastError, + reason: e.updateReason || "unknown", + }); + break; + default: + resp.pendingOperations.push({ + type: "bug", + givesLifeness: false, + message: "Unknown exchangeUpdateStatus", + details: { + exchangeBaseUrl: e.baseUrl, + exchangeUpdateStatus: e.updateStatus, + }, + }); + break; + } + }); +} + +async function gatherReservePending( + tx: TransactionHandle, + now: Timestamp, + resp: PendingOperationsResponse, + onlyDue: boolean = false, +): Promise<void> { + // FIXME: this should be optimized by using an index for "onlyDue==true". + await tx.iter(Stores.reserves).forEach(reserve => { + const reserveType = reserve.bankWithdrawStatusUrl ? "taler-bank" : "manual"; + if (!reserve.retryInfo.active) { + return; + } + switch (reserve.reserveStatus) { + case ReserveRecordStatus.DORMANT: + // nothing to report as pending + break; + case ReserveRecordStatus.UNCONFIRMED: + if (onlyDue) { + break; + } + resp.pendingOperations.push({ + type: "reserve", + givesLifeness: false, + stage: reserve.reserveStatus, + timestampCreated: reserve.created, + reserveType, + reservePub: reserve.reservePub, + retryInfo: reserve.retryInfo, + }); + break; + case ReserveRecordStatus.WAIT_CONFIRM_BANK: + case ReserveRecordStatus.WITHDRAWING: + case ReserveRecordStatus.QUERYING_STATUS: + case ReserveRecordStatus.REGISTERING_BANK: + resp.nextRetryDelay = updateRetryDelay( + resp.nextRetryDelay, + now, + reserve.retryInfo.nextRetry, + ); + if (onlyDue && reserve.retryInfo.nextRetry.t_ms > now.t_ms) { + return; + } + resp.pendingOperations.push({ + type: "reserve", + givesLifeness: true, + stage: reserve.reserveStatus, + timestampCreated: reserve.created, + reserveType, + reservePub: reserve.reservePub, + retryInfo: reserve.retryInfo, + }); + break; + default: + resp.pendingOperations.push({ + type: "bug", + givesLifeness: false, + message: "Unknown reserve record status", + details: { + reservePub: reserve.reservePub, + reserveStatus: reserve.reserveStatus, + }, + }); + break; + } + }); +} + +async function gatherRefreshPending( + tx: TransactionHandle, + now: Timestamp, + resp: PendingOperationsResponse, + onlyDue: boolean = false, +): Promise<void> { + await tx.iter(Stores.refresh).forEach(r => { + if (r.finishedTimestamp) { + return; + } + resp.nextRetryDelay = updateRetryDelay( + resp.nextRetryDelay, + now, + r.retryInfo.nextRetry, + ); + if (onlyDue && r.retryInfo.nextRetry.t_ms > now.t_ms) { + return; + } + let refreshStatus: string; + if (r.norevealIndex === undefined) { + refreshStatus = "melt"; + } else { + refreshStatus = "reveal"; + } + + resp.pendingOperations.push({ + type: "refresh", + givesLifeness: true, + oldCoinPub: r.meltCoinPub, + refreshStatus, + refreshOutputSize: r.newDenoms.length, + refreshSessionId: r.refreshSessionId, + }); + }); +} + +async function gatherCoinsPending( + tx: TransactionHandle, + now: Timestamp, + resp: PendingOperationsResponse, + onlyDue: boolean = false, +): Promise<void> { + // Refreshing dirty coins is always due. + await tx.iter(Stores.coins).forEach(coin => { + if (coin.status == CoinStatus.Dirty) { + resp.nextRetryDelay = { d_ms: 0 }; + resp.pendingOperations.push({ + givesLifeness: true, + type: "dirty-coin", + coinPub: coin.coinPub, + }); + } + }); +} + +async function gatherWithdrawalPending( + tx: TransactionHandle, + now: Timestamp, + resp: PendingOperationsResponse, + onlyDue: boolean = false, +): Promise<void> { + await tx.iter(Stores.withdrawalSession).forEach(wsr => { + if (wsr.finishTimestamp) { + return; + } + resp.nextRetryDelay = updateRetryDelay( + resp.nextRetryDelay, + now, + wsr.retryInfo.nextRetry, + ); + if (onlyDue && wsr.retryInfo.nextRetry.t_ms > now.t_ms) { + return; + } + const numCoinsWithdrawn = wsr.withdrawn.reduce( + (a, x) => a + (x ? 1 : 0), + 0, + ); + const numCoinsTotal = wsr.withdrawn.length; + resp.pendingOperations.push({ + type: "withdraw", + givesLifeness: true, + numCoinsTotal, + numCoinsWithdrawn, + source: wsr.source, + withdrawSessionId: wsr.withdrawSessionId, + }); + }); +} + +async function gatherProposalPending( + tx: TransactionHandle, + now: Timestamp, + resp: PendingOperationsResponse, + onlyDue: boolean = false, +): Promise<void> { + await tx.iter(Stores.proposals).forEach(proposal => { + if (proposal.proposalStatus == ProposalStatus.PROPOSED) { + if (onlyDue) { + return; + } + resp.pendingOperations.push({ + type: "proposal-choice", + givesLifeness: false, + merchantBaseUrl: proposal.download!!.contractTerms.merchant_base_url, + proposalId: proposal.proposalId, + proposalTimestamp: proposal.timestamp, + }); + } else if (proposal.proposalStatus == ProposalStatus.DOWNLOADING) { + resp.nextRetryDelay = updateRetryDelay( + resp.nextRetryDelay, + now, + proposal.retryInfo.nextRetry, + ); + if (onlyDue && proposal.retryInfo.nextRetry.t_ms > now.t_ms) { + return; + } + resp.pendingOperations.push({ + type: "proposal-download", + givesLifeness: true, + merchantBaseUrl: proposal.merchantBaseUrl, + orderId: proposal.orderId, + proposalId: proposal.proposalId, + proposalTimestamp: proposal.timestamp, + lastError: proposal.lastError, + retryInfo: proposal.retryInfo, + }); + } + }); +} + +async function gatherTipPending( + tx: TransactionHandle, + now: Timestamp, + resp: PendingOperationsResponse, + onlyDue: boolean = false, +): Promise<void> { + await tx.iter(Stores.tips).forEach(tip => { + if (tip.pickedUp) { + return; + } + resp.nextRetryDelay = updateRetryDelay( + resp.nextRetryDelay, + now, + tip.retryInfo.nextRetry, + ); + if (onlyDue && tip.retryInfo.nextRetry.t_ms > now.t_ms) { + return; + } + if (tip.accepted) { + resp.pendingOperations.push({ + type: "tip", + givesLifeness: true, + merchantBaseUrl: tip.merchantBaseUrl, + tipId: tip.tipId, + merchantTipId: tip.merchantTipId, + }); + } + }); +} + +async function gatherPurchasePending( + tx: TransactionHandle, + now: Timestamp, + resp: PendingOperationsResponse, + onlyDue: boolean = false, +): Promise<void> { + await tx.iter(Stores.purchases).forEach(pr => { + if (pr.paymentSubmitPending) { + resp.nextRetryDelay = updateRetryDelay( + resp.nextRetryDelay, + now, + pr.payRetryInfo.nextRetry, + ); + if (!onlyDue || pr.payRetryInfo.nextRetry.t_ms <= now.t_ms) { + resp.pendingOperations.push({ + type: "pay", + givesLifeness: true, + isReplay: false, + proposalId: pr.proposalId, + retryInfo: pr.payRetryInfo, + lastError: pr.lastPayError, + }); + } + } + if (pr.refundStatusRequested) { + resp.nextRetryDelay = updateRetryDelay( + resp.nextRetryDelay, + now, + pr.refundStatusRetryInfo.nextRetry, + ); + if (!onlyDue || pr.refundStatusRetryInfo.nextRetry.t_ms <= now.t_ms) { + resp.pendingOperations.push({ + type: "refund-query", + givesLifeness: true, + proposalId: pr.proposalId, + retryInfo: pr.refundStatusRetryInfo, + lastError: pr.lastRefundStatusError, + }); + } + } + const numRefundsPending = Object.keys(pr.refundsPending).length; + if (numRefundsPending > 0) { + const numRefundsDone = Object.keys(pr.refundsDone).length; + resp.nextRetryDelay = updateRetryDelay( + resp.nextRetryDelay, + now, + pr.refundApplyRetryInfo.nextRetry, + ); + if (!onlyDue || pr.refundApplyRetryInfo.nextRetry.t_ms <= now.t_ms) { + resp.pendingOperations.push({ + type: "refund-apply", + numRefundsDone, + numRefundsPending, + givesLifeness: true, + proposalId: pr.proposalId, + retryInfo: pr.refundApplyRetryInfo, + lastError: pr.lastRefundApplyError, + }); + } + } + }); +} + +export async function getPendingOperations( + ws: InternalWalletState, + onlyDue: boolean = false, +): Promise<PendingOperationsResponse> { + const resp: PendingOperationsResponse = { + nextRetryDelay: { d_ms: Number.MAX_SAFE_INTEGER }, + pendingOperations: [], + }; + const now = getTimestampNow(); + await runWithReadTransaction( + ws.db, + [ + Stores.exchanges, + Stores.reserves, + Stores.refresh, + Stores.coins, + Stores.withdrawalSession, + Stores.proposals, + Stores.tips, + Stores.purchases, + ], + async tx => { + await gatherExchangePending(tx, now, resp, onlyDue); + await gatherReservePending(tx, now, resp, onlyDue); + await gatherRefreshPending(tx, now, resp, onlyDue); + await gatherCoinsPending(tx, now, resp, onlyDue); + await gatherWithdrawalPending(tx, now, resp, onlyDue); + await gatherProposalPending(tx, now, resp, onlyDue); + await gatherTipPending(tx, now, resp, onlyDue); + await gatherPurchasePending(tx, now, resp, onlyDue); + }, + ); + return resp; +} |