wallet-core/packages/taler-wallet-core/src/operations/pending.ts

516 lines
15 KiB
TypeScript
Raw Normal View History

/*
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/>
*/
2019-12-02 17:35:47 +01:00
/**
* Imports.
*/
import {
ExchangeUpdateStatus,
ProposalStatus,
2019-12-15 19:08:07 +01:00
ReserveRecordStatus,
Stores,
AbortStatus,
2021-03-17 17:56:37 +01:00
} from "../db.js";
2019-12-15 19:08:07 +01:00
import {
PendingOperationsResponse,
PendingOperationType,
ExchangeUpdateOperationStage,
2020-07-23 15:00:08 +02:00
ReserveType,
2021-03-17 17:56:37 +01:00
} from "../pending-types";
import {
Duration,
getTimestampNow,
Timestamp,
getDurationRemaining,
durationMin,
2021-03-17 17:56:37 +01:00
} from "@gnu-taler/taler-util";
2020-11-27 11:23:06 +01:00
import { Store, TransactionHandle } from "../util/query";
2019-12-15 19:08:07 +01:00
import { InternalWalletState } from "./state";
2020-04-07 10:07:32 +02:00
import { getBalancesInsideTransaction } from "./balance";
2019-12-05 19:38:19 +01:00
function updateRetryDelay(
oldDelay: Duration,
now: Timestamp,
retryTimestamp: Timestamp,
): Duration {
const remaining = getDurationRemaining(retryTimestamp, now);
const nextDelay = durationMin(oldDelay, remaining);
return nextDelay;
2019-12-05 19:38:19 +01:00
}
async function gatherExchangePending(
2020-11-27 11:23:06 +01:00
tx: TransactionHandle<typeof Stores.exchanges>,
2019-12-05 19:38:19 +01:00
now: Timestamp,
resp: PendingOperationsResponse,
2020-04-06 17:45:41 +02:00
onlyDue = false,
2019-12-05 19:38:19 +01:00
): Promise<void> {
2020-03-30 12:39:32 +02:00
await tx.iter(Stores.exchanges).forEach((e) => {
2019-12-05 19:38:19 +01:00
switch (e.updateStatus) {
2019-12-16 12:53:22 +01:00
case ExchangeUpdateStatus.Finished:
2019-12-05 19:38:19 +01:00
if (e.lastError) {
resp.pendingOperations.push({
type: PendingOperationType.Bug,
2019-12-05 19:38:19 +01:00
givesLifeness: false,
message:
"Exchange record is in FINISHED state but has lastError set",
details: {
exchangeBaseUrl: e.baseUrl,
},
});
}
if (!e.details) {
resp.pendingOperations.push({
type: PendingOperationType.Bug,
2019-12-05 19:38:19 +01:00
givesLifeness: false,
message:
2020-09-02 11:14:36 +02:00
"Exchange record does not have details, but no update finished.",
2019-12-05 19:38:19 +01:00
details: {
exchangeBaseUrl: e.baseUrl,
},
});
}
if (!e.wireInfo) {
resp.pendingOperations.push({
type: PendingOperationType.Bug,
2019-12-05 19:38:19 +01:00
givesLifeness: false,
message:
2020-09-02 11:14:36 +02:00
"Exchange record does not have wire info, but no update finished.",
2019-12-05 19:38:19 +01:00
details: {
exchangeBaseUrl: e.baseUrl,
},
});
}
2020-09-04 08:34:11 +02:00
const keysUpdateRequired =
e.details && e.details.nextUpdateTime.t_ms < now.t_ms;
2020-09-03 13:59:09 +02:00
if (keysUpdateRequired) {
2020-09-02 11:14:36 +02:00
resp.pendingOperations.push({
type: PendingOperationType.ExchangeUpdate,
givesLifeness: false,
stage: ExchangeUpdateOperationStage.FetchKeys,
exchangeBaseUrl: e.baseUrl,
lastError: e.lastError,
reason: "scheduled",
});
2020-09-03 17:08:26 +02:00
}
2020-09-04 08:34:11 +02:00
if (
e.details &&
(!e.nextRefreshCheck || e.nextRefreshCheck.t_ms < now.t_ms)
) {
2020-09-03 17:08:26 +02:00
resp.pendingOperations.push({
type: PendingOperationType.ExchangeCheckRefresh,
exchangeBaseUrl: e.baseUrl,
givesLifeness: false,
});
2020-09-02 11:14:36 +02:00
}
2019-12-05 19:38:19 +01:00
break;
2019-12-16 12:53:22 +01:00
case ExchangeUpdateStatus.FetchKeys:
2020-09-02 11:14:36 +02:00
if (onlyDue && e.retryInfo.nextRetry.t_ms > now.t_ms) {
return;
}
2019-12-05 19:38:19 +01:00
resp.pendingOperations.push({
type: PendingOperationType.ExchangeUpdate,
2019-12-05 19:38:19 +01:00
givesLifeness: false,
stage: ExchangeUpdateOperationStage.FetchKeys,
2019-12-05 19:38:19 +01:00
exchangeBaseUrl: e.baseUrl,
lastError: e.lastError,
reason: e.updateReason || "unknown",
});
break;
2019-12-16 12:53:22 +01:00
case ExchangeUpdateStatus.FetchWire:
2020-09-02 11:14:36 +02:00
if (onlyDue && e.retryInfo.nextRetry.t_ms > now.t_ms) {
return;
}
2019-12-05 19:38:19 +01:00
resp.pendingOperations.push({
type: PendingOperationType.ExchangeUpdate,
2019-12-05 19:38:19 +01:00
givesLifeness: false,
stage: ExchangeUpdateOperationStage.FetchWire,
2019-12-05 19:38:19 +01:00
exchangeBaseUrl: e.baseUrl,
lastError: e.lastError,
reason: e.updateReason || "unknown",
});
break;
2019-12-16 12:53:22 +01:00
case ExchangeUpdateStatus.FinalizeUpdate:
2020-09-02 11:14:36 +02:00
if (onlyDue && e.retryInfo.nextRetry.t_ms > now.t_ms) {
return;
}
resp.pendingOperations.push({
type: PendingOperationType.ExchangeUpdate,
givesLifeness: false,
stage: ExchangeUpdateOperationStage.FinalizeUpdate,
exchangeBaseUrl: e.baseUrl,
lastError: e.lastError,
reason: e.updateReason || "unknown",
});
2019-12-16 12:53:22 +01:00
break;
2019-12-05 19:38:19 +01:00
default:
resp.pendingOperations.push({
type: PendingOperationType.Bug,
2019-12-05 19:38:19 +01:00
givesLifeness: false,
message: "Unknown exchangeUpdateStatus",
details: {
exchangeBaseUrl: e.baseUrl,
exchangeUpdateStatus: e.updateStatus,
},
});
break;
}
});
}
async function gatherReservePending(
2020-11-27 11:23:06 +01:00
tx: TransactionHandle<typeof Stores.reserves>,
2019-12-05 19:38:19 +01:00
now: Timestamp,
resp: PendingOperationsResponse,
2020-04-06 17:45:41 +02:00
onlyDue = false,
2019-12-05 19:38:19 +01:00
): Promise<void> {
// FIXME: this should be optimized by using an index for "onlyDue==true".
2020-03-30 12:39:32 +02:00
await tx.iter(Stores.reserves).forEach((reserve) => {
2020-05-12 10:38:58 +02:00
const reserveType = reserve.bankInfo
2020-04-06 09:24:49 +02:00
? ReserveType.TalerBankWithdraw
: ReserveType.Manual;
2019-12-05 19:38:19 +01:00
if (!reserve.retryInfo.active) {
return;
}
switch (reserve.reserveStatus) {
case ReserveRecordStatus.DORMANT:
// nothing to report as pending
break;
case ReserveRecordStatus.WAIT_CONFIRM_BANK:
2019-12-06 03:23:35 +01:00
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: PendingOperationType.Reserve,
2019-12-06 03:23:35 +01:00
givesLifeness: true,
stage: reserve.reserveStatus,
2019-12-16 16:20:45 +01:00
timestampCreated: reserve.timestampCreated,
2019-12-06 03:23:35 +01:00
reserveType,
reservePub: reserve.reservePub,
retryInfo: reserve.retryInfo,
});
break;
2019-12-05 19:38:19 +01:00
default:
resp.pendingOperations.push({
type: PendingOperationType.Bug,
2019-12-05 19:38:19 +01:00
givesLifeness: false,
message: "Unknown reserve record status",
details: {
reservePub: reserve.reservePub,
reserveStatus: reserve.reserveStatus,
},
});
break;
}
});
}
async function gatherRefreshPending(
2020-11-27 11:23:06 +01:00
tx: TransactionHandle<typeof Stores.refreshGroups>,
2019-12-05 19:38:19 +01:00
now: Timestamp,
resp: PendingOperationsResponse,
2020-04-06 17:45:41 +02:00
onlyDue = false,
2019-12-05 19:38:19 +01:00
): Promise<void> {
2020-03-30 12:39:32 +02:00
await tx.iter(Stores.refreshGroups).forEach((r) => {
2019-12-16 16:20:45 +01:00
if (r.timestampFinished) {
2019-12-05 19:38:19 +01:00
return;
}
resp.nextRetryDelay = updateRetryDelay(
resp.nextRetryDelay,
now,
r.retryInfo.nextRetry,
);
if (onlyDue && r.retryInfo.nextRetry.t_ms > now.t_ms) {
return;
}
resp.pendingOperations.push({
type: PendingOperationType.Refresh,
2019-12-05 19:38:19 +01:00
givesLifeness: true,
refreshGroupId: r.refreshGroupId,
finishedPerCoin: r.finishedPerCoin,
retryInfo: r.retryInfo,
2019-12-05 19:38:19 +01:00
});
});
}
async function gatherWithdrawalPending(
2020-11-27 11:23:06 +01:00
tx: TransactionHandle<typeof Stores.withdrawalGroups>,
2019-12-05 19:38:19 +01:00
now: Timestamp,
resp: PendingOperationsResponse,
2020-04-06 17:45:41 +02:00
onlyDue = false,
2019-12-05 19:38:19 +01:00
): Promise<void> {
await tx.iter(Stores.withdrawalGroups).forEachAsync(async (wsr) => {
2019-12-16 16:20:45 +01:00
if (wsr.timestampFinish) {
2019-12-05 19:38:19 +01:00
return;
}
resp.nextRetryDelay = updateRetryDelay(
resp.nextRetryDelay,
now,
wsr.retryInfo.nextRetry,
);
if (onlyDue && wsr.retryInfo.nextRetry.t_ms > now.t_ms) {
return;
}
let numCoinsWithdrawn = 0;
let numCoinsTotal = 0;
await tx
.iterIndexed(Stores.planchets.byGroup, wsr.withdrawalGroupId)
.forEach((x) => {
numCoinsTotal++;
if (x.withdrawalDone) {
numCoinsWithdrawn++;
}
});
2019-12-05 19:38:19 +01:00
resp.pendingOperations.push({
type: PendingOperationType.Withdraw,
2019-12-05 19:38:19 +01:00
givesLifeness: true,
numCoinsTotal,
numCoinsWithdrawn,
withdrawalGroupId: wsr.withdrawalGroupId,
lastError: wsr.lastError,
2020-09-01 14:30:46 +02:00
retryInfo: wsr.retryInfo,
2019-12-05 19:38:19 +01:00
});
});
}
async function gatherProposalPending(
2020-11-27 11:23:06 +01:00
tx: TransactionHandle<typeof Stores.proposals>,
2019-12-05 19:38:19 +01:00
now: Timestamp,
resp: PendingOperationsResponse,
2020-04-06 17:45:41 +02:00
onlyDue = false,
2019-12-05 19:38:19 +01:00
): Promise<void> {
2020-03-30 12:39:32 +02:00
await tx.iter(Stores.proposals).forEach((proposal) => {
2019-12-05 19:38:19 +01:00
if (proposal.proposalStatus == ProposalStatus.PROPOSED) {
if (onlyDue) {
return;
}
2020-04-07 10:07:32 +02:00
const dl = proposal.download;
if (!dl) {
resp.pendingOperations.push({
type: PendingOperationType.Bug,
message: "proposal is in invalid state",
details: {},
givesLifeness: false,
});
} else {
resp.pendingOperations.push({
type: PendingOperationType.ProposalChoice,
givesLifeness: false,
merchantBaseUrl: dl.contractData.merchantBaseUrl,
proposalId: proposal.proposalId,
proposalTimestamp: proposal.timestamp,
});
}
2019-12-05 19:38:19 +01:00
} 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: PendingOperationType.ProposalDownload,
2019-12-05 19:38:19 +01:00
givesLifeness: true,
2019-12-06 12:47:28 +01:00
merchantBaseUrl: proposal.merchantBaseUrl,
orderId: proposal.orderId,
2019-12-05 19:38:19 +01:00
proposalId: proposal.proposalId,
proposalTimestamp: proposal.timestamp,
lastError: proposal.lastError,
retryInfo: proposal.retryInfo,
2019-12-05 19:38:19 +01:00
});
}
});
}
async function gatherTipPending(
2020-11-27 11:23:06 +01:00
tx: TransactionHandle<typeof Stores.tips>,
2019-12-05 19:38:19 +01:00
now: Timestamp,
resp: PendingOperationsResponse,
2020-04-06 17:45:41 +02:00
onlyDue = false,
2019-12-05 19:38:19 +01:00
): Promise<void> {
2020-03-30 12:39:32 +02:00
await tx.iter(Stores.tips).forEach((tip) => {
if (tip.pickedUpTimestamp) {
2019-12-05 19:38:19 +01:00
return;
}
resp.nextRetryDelay = updateRetryDelay(
resp.nextRetryDelay,
now,
tip.retryInfo.nextRetry,
);
if (onlyDue && tip.retryInfo.nextRetry.t_ms > now.t_ms) {
return;
}
2019-12-16 12:53:22 +01:00
if (tip.acceptedTimestamp) {
2019-12-05 19:38:19 +01:00
resp.pendingOperations.push({
type: PendingOperationType.TipPickup,
2019-12-05 19:38:19 +01:00
givesLifeness: true,
merchantBaseUrl: tip.merchantBaseUrl,
2020-09-08 14:10:47 +02:00
tipId: tip.walletTipId,
2019-12-05 19:38:19 +01:00
merchantTipId: tip.merchantTipId,
});
}
});
}
async function gatherPurchasePending(
2020-11-27 11:23:06 +01:00
tx: TransactionHandle<typeof Stores.purchases>,
2019-12-05 19:38:19 +01:00
now: Timestamp,
resp: PendingOperationsResponse,
2020-04-06 17:45:41 +02:00
onlyDue = false,
2019-12-05 19:38:19 +01:00
): Promise<void> {
2020-03-30 12:39:32 +02:00
await tx.iter(Stores.purchases).forEach((pr) => {
if (pr.paymentSubmitPending && pr.abortStatus === AbortStatus.None) {
resp.nextRetryDelay = updateRetryDelay(
resp.nextRetryDelay,
now,
pr.payRetryInfo.nextRetry,
);
if (!onlyDue || pr.payRetryInfo.nextRetry.t_ms <= now.t_ms) {
resp.pendingOperations.push({
type: PendingOperationType.Pay,
givesLifeness: true,
isReplay: false,
proposalId: pr.proposalId,
retryInfo: pr.payRetryInfo,
lastError: pr.lastPayError,
});
}
2019-12-05 19:38:19 +01:00
}
if (pr.refundQueryRequested) {
resp.nextRetryDelay = updateRetryDelay(
resp.nextRetryDelay,
now,
pr.refundStatusRetryInfo.nextRetry,
);
if (!onlyDue || pr.refundStatusRetryInfo.nextRetry.t_ms <= now.t_ms) {
resp.pendingOperations.push({
type: PendingOperationType.RefundQuery,
givesLifeness: true,
proposalId: pr.proposalId,
retryInfo: pr.refundStatusRetryInfo,
lastError: pr.lastRefundStatusError,
});
}
}
2019-12-05 19:38:19 +01:00
});
}
async function gatherRecoupPending(
2020-11-27 11:23:06 +01:00
tx: TransactionHandle<typeof Stores.recoupGroups>,
now: Timestamp,
resp: PendingOperationsResponse,
2020-04-06 17:45:41 +02:00
onlyDue = false,
): Promise<void> {
2020-03-30 12:39:32 +02:00
await tx.iter(Stores.recoupGroups).forEach((rg) => {
if (rg.timestampFinished) {
return;
}
resp.nextRetryDelay = updateRetryDelay(
resp.nextRetryDelay,
now,
rg.retryInfo.nextRetry,
);
if (onlyDue && rg.retryInfo.nextRetry.t_ms > now.t_ms) {
return;
}
resp.pendingOperations.push({
type: PendingOperationType.Recoup,
givesLifeness: true,
recoupGroupId: rg.recoupGroupId,
retryInfo: rg.retryInfo,
lastError: rg.lastError,
});
});
}
2021-01-18 23:35:41 +01:00
async function gatherDepositPending(
tx: TransactionHandle<typeof Stores.depositGroups>,
now: Timestamp,
resp: PendingOperationsResponse,
onlyDue = false,
): Promise<void> {
await tx.iter(Stores.depositGroups).forEach((dg) => {
if (dg.timestampFinished) {
return;
}
resp.nextRetryDelay = updateRetryDelay(
resp.nextRetryDelay,
now,
dg.retryInfo.nextRetry,
);
if (onlyDue && dg.retryInfo.nextRetry.t_ms > now.t_ms) {
return;
}
resp.pendingOperations.push({
type: PendingOperationType.Deposit,
givesLifeness: true,
depositGroupId: dg.depositGroupId,
retryInfo: dg.retryInfo,
lastError: dg.lastError,
});
});
}
export async function getPendingOperations(
ws: InternalWalletState,
{ onlyDue = false } = {},
): Promise<PendingOperationsResponse> {
2019-12-05 19:38:19 +01:00
const now = getTimestampNow();
return await ws.db.runWithReadTransaction(
2019-12-03 00:52:15 +01:00
[
Stores.exchanges,
Stores.reserves,
Stores.refreshGroups,
2019-12-03 00:52:15 +01:00
Stores.coins,
Stores.withdrawalGroups,
2019-12-03 00:52:15 +01:00
Stores.proposals,
Stores.tips,
2019-12-05 19:38:19 +01:00
Stores.purchases,
Stores.recoupGroups,
Stores.planchets,
2021-01-18 23:35:41 +01:00
Stores.depositGroups,
2019-12-03 00:52:15 +01:00
],
2020-03-30 12:39:32 +02:00
async (tx) => {
const walletBalance = await getBalancesInsideTransaction(ws, tx);
const resp: PendingOperationsResponse = {
nextRetryDelay: { d_ms: Number.MAX_SAFE_INTEGER },
onlyDue: onlyDue,
walletBalance,
pendingOperations: [],
};
2019-12-05 19:38:19 +01:00
await gatherExchangePending(tx, now, resp, onlyDue);
await gatherReservePending(tx, now, resp, onlyDue);
await gatherRefreshPending(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);
await gatherRecoupPending(tx, now, resp, onlyDue);
2021-01-18 23:35:41 +01:00
await gatherDepositPending(tx, now, resp, onlyDue);
return resp;
2019-12-03 00:52:15 +01:00
},
);
}