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

512 lines
16 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/>
*/
/**
* Derive pending tasks from the wallet database.
*/
2019-12-02 17:35:47 +01:00
/**
* Imports.
*/
import {
2022-10-08 23:45:49 +02:00
PurchaseStatus,
2021-06-09 15:14:17 +02:00
WalletStoresV1,
BackupProviderStateTag,
2021-08-24 14:25:46 +02:00
RefreshCoinStatus,
2022-01-11 21:00:12 +01:00
OperationStatus,
OperationStatusRange,
PeerPushPaymentInitiationStatus,
2023-02-19 23:13:44 +01:00
PeerPullPaymentIncomingStatus,
PeerPushPaymentIncomingStatus,
PeerPullPaymentInitiationStatus,
2021-03-17 17:56:37 +01:00
} from "../db.js";
2019-12-15 19:08:07 +01:00
import {
PendingOperationsResponse,
PendingTaskType,
TaskId,
2021-06-14 16:08:58 +02:00
} from "../pending-types.js";
2022-03-18 15:32:41 +01:00
import { AbsoluteTime } from "@gnu-taler/taler-util";
import { InternalWalletState } from "../internal-wallet-state.js";
2021-06-09 15:14:17 +02:00
import { GetReadOnlyAccess } from "../util/query.js";
import { TaskIdentifiers } from "../util/retries.js";
import { GlobalIDB } from "@gnu-taler/idb-bridge";
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],
};
}
2019-12-05 19:38:19 +01:00
async function gatherExchangePending(
ws: InternalWalletState,
2021-06-09 15:14:17 +02:00
tx: GetReadOnlyAccess<{
exchanges: typeof WalletStoresV1.exchanges;
exchangeDetails: typeof WalletStoresV1.exchangeDetails;
2022-09-05 18:12:30 +02:00
operationRetries: typeof WalletStoresV1.operationRetries;
2021-06-09 15:14:17 +02:00
}>,
2022-03-18 15:32:41 +01:00
now: AbsoluteTime,
2019-12-05 19:38:19 +01:00
resp: PendingOperationsResponse,
): Promise<void> {
// FIXME: We should do a range query here based on the update time.
2022-09-05 18:12:30 +02:00
await tx.exchanges.iter().forEachAsync(async (exch) => {
const opTag = TaskIdentifiers.forExchangeUpdate(exch);
2022-09-05 18:12:30 +02:00
let opr = await tx.operationRetries.get(opTag);
const timestampDue =
opr?.retryInfo.nextRetry ?? AbsoluteTime.fromTimestamp(exch.nextUpdate);
resp.pendingOperations.push({
type: PendingTaskType.ExchangeUpdate,
...getPendingCommon(ws, opTag, timestampDue),
givesLifeness: false,
2022-09-05 18:12:30 +02:00
exchangeBaseUrl: exch.baseUrl,
lastError: opr?.lastError,
});
// We only schedule a check for auto-refresh if the exchange update
// was successful.
2022-09-05 18:12:30 +02:00
if (!opr?.lastError) {
resp.pendingOperations.push({
type: PendingTaskType.ExchangeCheckRefresh,
...getPendingCommon(ws, opTag, timestampDue),
2022-09-05 18:12:30 +02:00
timestampDue: AbsoluteTime.fromTimestamp(exch.nextRefreshCheck),
givesLifeness: false,
2022-09-05 18:12:30 +02:00
exchangeBaseUrl: exch.baseUrl,
});
}
2019-12-05 19:38:19 +01:00
});
}
async function gatherRefreshPending(
ws: InternalWalletState,
2022-09-05 18:12:30 +02:00
tx: GetReadOnlyAccess<{
refreshGroups: typeof WalletStoresV1.refreshGroups;
operationRetries: typeof WalletStoresV1.operationRetries;
}>,
2022-03-18 15:32:41 +01:00
now: AbsoluteTime,
2019-12-05 19:38:19 +01:00
resp: PendingOperationsResponse,
): Promise<void> {
2022-10-14 21:00:13 +02:00
const keyRange = GlobalIDB.KeyRange.bound(
OperationStatusRange.ACTIVE_START,
OperationStatusRange.ACTIVE_END,
);
2022-01-13 12:08:31 +01:00
const refreshGroups = await tx.refreshGroups.indexes.byStatus.getAll(
2022-10-14 21:00:13 +02:00
keyRange,
2022-01-13 12:08:31 +01:00
);
for (const r of refreshGroups) {
if (r.timestampFinished) {
return;
}
const opId = TaskIdentifiers.forRefresh(r);
2022-09-05 18:12:30 +02:00
const retryRecord = await tx.operationRetries.get(opId);
const timestampDue = retryRecord?.retryInfo.nextRetry ?? AbsoluteTime.now();
2022-01-13 12:08:31 +01:00
resp.pendingOperations.push({
type: PendingTaskType.Refresh,
...getPendingCommon(ws, opId, timestampDue),
2022-01-13 12:08:31 +01:00
givesLifeness: true,
refreshGroupId: r.refreshGroupId,
finishedPerCoin: r.statusPerCoin.map(
(x) => x === RefreshCoinStatus.Finished,
),
2022-09-05 18:12:30 +02:00
retryInfo: retryRecord?.retryInfo,
2019-12-05 19:38:19 +01:00
});
2022-01-13 12:08:31 +01:00
}
2019-12-05 19:38:19 +01:00
}
async function gatherWithdrawalPending(
ws: InternalWalletState,
2021-06-09 15:14:17 +02:00
tx: GetReadOnlyAccess<{
withdrawalGroups: typeof WalletStoresV1.withdrawalGroups;
2021-06-09 15:26:18 +02:00
planchets: typeof WalletStoresV1.planchets;
2022-09-05 18:12:30 +02:00
operationRetries: typeof WalletStoresV1.operationRetries;
2021-06-09 15:14:17 +02:00
}>,
2022-03-18 15:32:41 +01:00
now: AbsoluteTime,
2019-12-05 19:38:19 +01:00
resp: PendingOperationsResponse,
): Promise<void> {
2022-01-13 12:08:31 +01:00
const wsrs = await tx.withdrawalGroups.indexes.byStatus.getAll(
GlobalIDB.KeyRange.bound(
OperationStatusRange.ACTIVE_START,
OperationStatusRange.ACTIVE_END,
),
2022-01-13 12:08:31 +01:00
);
for (const wsr of wsrs) {
if (wsr.timestampFinish) {
return;
}
const opTag = TaskIdentifiers.forWithdrawal(wsr);
2022-09-05 18:12:30 +02:00
let opr = await tx.operationRetries.get(opTag);
const now = AbsoluteTime.now();
if (!opr) {
opr = {
id: opTag,
retryInfo: {
firstTry: now,
nextRetry: now,
retryCounter: 0,
},
};
}
2022-01-13 12:08:31 +01:00
resp.pendingOperations.push({
type: PendingTaskType.Withdraw,
...getPendingCommon(
ws,
opTag,
opr.retryInfo?.nextRetry ?? AbsoluteTime.now(),
),
2022-01-13 12:08:31 +01:00
givesLifeness: true,
withdrawalGroupId: wsr.withdrawalGroupId,
2022-09-05 18:12:30 +02:00
lastError: opr.lastError,
retryInfo: opr.retryInfo,
2019-12-05 19:38:19 +01:00
});
2022-01-13 12:08:31 +01:00
}
2019-12-05 19:38:19 +01:00
}
async function gatherDepositPending(
ws: InternalWalletState,
2022-09-05 18:12:30 +02:00
tx: GetReadOnlyAccess<{
depositGroups: typeof WalletStoresV1.depositGroups;
operationRetries: typeof WalletStoresV1.operationRetries;
}>,
2022-03-18 15:32:41 +01:00
now: AbsoluteTime,
resp: PendingOperationsResponse,
): Promise<void> {
2022-01-13 12:08:31 +01:00
const dgs = await tx.depositGroups.indexes.byStatus.getAll(
OperationStatus.Pending,
);
for (const dg of dgs) {
if (dg.timestampFinished) {
return;
}
let deposited = true;
for (const d of dg.depositedPerCoin) {
if (!d) {
deposited = false;
}
}
const opId = TaskIdentifiers.forDeposit(dg);
2022-09-05 18:12:30 +02:00
const retryRecord = await tx.operationRetries.get(opId);
const timestampDue = retryRecord?.retryInfo.nextRetry ?? AbsoluteTime.now();
2022-01-13 12:08:31 +01:00
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,
2022-01-13 12:08:31 +01:00
depositGroupId: dg.depositGroupId,
2022-09-05 18:12:30 +02:00
lastError: retryRecord?.lastError,
retryInfo: retryRecord?.retryInfo,
});
2022-01-13 12:08:31 +01:00
}
}
2019-12-05 19:38:19 +01:00
async function gatherTipPending(
ws: InternalWalletState,
2022-09-05 18:12:30 +02:00
tx: GetReadOnlyAccess<{
tips: typeof WalletStoresV1.tips;
operationRetries: typeof WalletStoresV1.operationRetries;
}>,
2022-03-18 15:32:41 +01:00
now: AbsoluteTime,
2019-12-05 19:38:19 +01:00
resp: PendingOperationsResponse,
): Promise<void> {
2022-09-05 18:12:30 +02:00
await tx.tips.iter().forEachAsync(async (tip) => {
// FIXME: The tip record needs a proper status field!
if (tip.pickedUpTimestamp) {
2019-12-05 19:38:19 +01:00
return;
}
const opId = TaskIdentifiers.forTipPickup(tip);
2022-09-05 18:12:30 +02:00
const retryRecord = await tx.operationRetries.get(opId);
const timestampDue = retryRecord?.retryInfo.nextRetry ?? AbsoluteTime.now();
2019-12-16 12:53:22 +01:00
if (tip.acceptedTimestamp) {
2019-12-05 19:38:19 +01:00
resp.pendingOperations.push({
type: PendingTaskType.TipPickup,
...getPendingCommon(ws, opId, timestampDue),
2019-12-05 19:38:19 +01:00
givesLifeness: true,
2022-09-05 18:12:30 +02:00
timestampDue: retryRecord?.retryInfo.nextRetry ?? AbsoluteTime.now(),
2019-12-05 19:38:19 +01:00
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(
ws: InternalWalletState,
2022-09-05 18:12:30 +02:00
tx: GetReadOnlyAccess<{
purchases: typeof WalletStoresV1.purchases;
operationRetries: typeof WalletStoresV1.operationRetries;
}>,
2022-03-18 15:32:41 +01:00
now: AbsoluteTime,
2019-12-05 19:38:19 +01:00
resp: PendingOperationsResponse,
): Promise<void> {
const keyRange = GlobalIDB.KeyRange.bound(
OperationStatusRange.ACTIVE_START,
OperationStatusRange.ACTIVE_END,
);
await tx.purchases.indexes.byStatus
.iter(keyRange)
.forEachAsync(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,
2022-10-08 23:45:49 +02:00
statusStr: PurchaseStatus[pr.purchaseStatus],
proposalId: pr.proposalId,
retryInfo: retryRecord?.retryInfo,
lastError: retryRecord?.lastError,
});
});
2019-12-05 19:38:19 +01:00
}
async function gatherRecoupPending(
ws: InternalWalletState,
2022-09-05 18:12:30 +02:00
tx: GetReadOnlyAccess<{
recoupGroups: typeof WalletStoresV1.recoupGroups;
operationRetries: typeof WalletStoresV1.operationRetries;
}>,
2022-03-18 15:32:41 +01:00
now: AbsoluteTime,
resp: PendingOperationsResponse,
): Promise<void> {
2022-09-05 18:12:30 +02:00
await tx.recoupGroups.iter().forEachAsync(async (rg) => {
if (rg.timestampFinished) {
return;
}
const opId = TaskIdentifiers.forRecoup(rg);
2022-09-05 18:12:30 +02:00
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,
2022-09-05 18:12:30 +02:00
retryInfo: retryRecord?.retryInfo,
lastError: retryRecord?.lastError,
});
});
}
async function gatherBackupPending(
ws: InternalWalletState,
tx: GetReadOnlyAccess<{
backupProviders: typeof WalletStoresV1.backupProviders;
2022-09-05 18:12:30 +02:00
operationRetries: typeof WalletStoresV1.operationRetries;
}>,
2022-03-18 15:32:41 +01:00
now: AbsoluteTime,
2021-01-18 23:35:41 +01:00
resp: PendingOperationsResponse,
): Promise<void> {
2022-09-05 18:12:30 +02:00
await tx.backupProviders.iter().forEachAsync(async (bp) => {
const opId = TaskIdentifiers.forBackup(bp);
2022-09-05 18:12:30 +02:00
const retryRecord = await tx.operationRetries.get(opId);
if (bp.state.tag === BackupProviderStateTag.Ready) {
const timestampDue = AbsoluteTime.fromTimestamp(
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,
2022-09-05 18:12:30 +02:00
retryInfo: retryRecord?.retryInfo,
lastError: retryRecord?.lastError,
});
2021-01-18 23:35:41 +01:00
}
});
}
async function gatherPeerPullInitiationPending(
ws: InternalWalletState,
tx: GetReadOnlyAccess<{
peerPullPaymentInitiations: typeof WalletStoresV1.peerPullPaymentInitiations;
operationRetries: typeof WalletStoresV1.operationRetries;
}>,
now: AbsoluteTime,
resp: PendingOperationsResponse,
): Promise<void> {
await tx.peerPullPaymentInitiations.iter().forEachAsync(async (pi) => {
2023-05-05 10:56:42 +02:00
if (pi.status === PeerPullPaymentInitiationStatus.DonePurseDeposited) {
return;
}
const opId = TaskIdentifiers.forPeerPullPaymentInitiation(pi);
const retryRecord = await tx.operationRetries.get(opId);
const timestampDue = retryRecord?.retryInfo.nextRetry ?? AbsoluteTime.now();
resp.pendingOperations.push({
2023-05-05 10:56:42 +02:00
type: PendingTaskType.PeerPullCredit,
...getPendingCommon(ws, opId, timestampDue),
givesLifeness: true,
retryInfo: retryRecord?.retryInfo,
pursePub: pi.pursePub,
});
});
}
2023-02-19 23:13:44 +01:00
async function gatherPeerPullDebitPending(
ws: InternalWalletState,
tx: GetReadOnlyAccess<{
peerPullPaymentIncoming: typeof WalletStoresV1.peerPullPaymentIncoming;
operationRetries: typeof WalletStoresV1.operationRetries;
}>,
now: AbsoluteTime,
resp: PendingOperationsResponse,
): Promise<void> {
await tx.peerPullPaymentIncoming.iter().forEachAsync(async (pi) => {
switch (pi.status) {
case PeerPullPaymentIncomingStatus.Paid:
return;
case PeerPullPaymentIncomingStatus.Proposed:
return;
case PeerPullPaymentIncomingStatus.Accepted:
break;
2023-02-19 23:13:44 +01:00
}
const opId = TaskIdentifiers.forPeerPullPaymentDebit(pi);
2023-02-19 23:13:44 +01:00
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,
});
});
}
async function gatherPeerPushInitiationPending(
ws: InternalWalletState,
tx: GetReadOnlyAccess<{
peerPushPaymentInitiations: typeof WalletStoresV1.peerPushPaymentInitiations;
operationRetries: typeof WalletStoresV1.operationRetries;
}>,
now: AbsoluteTime,
resp: PendingOperationsResponse,
): Promise<void> {
await tx.peerPushPaymentInitiations.iter().forEachAsync(async (pi) => {
2023-05-05 10:56:42 +02:00
if (pi.status === PeerPushPaymentInitiationStatus.Done) {
return;
}
const opId = TaskIdentifiers.forPeerPushPaymentInitiation(pi);
const retryRecord = await tx.operationRetries.get(opId);
const timestampDue = retryRecord?.retryInfo.nextRetry ?? AbsoluteTime.now();
resp.pendingOperations.push({
2023-05-05 10:56:42 +02:00
type: PendingTaskType.PeerPushDebit,
...getPendingCommon(ws, opId, timestampDue),
givesLifeness: true,
retryInfo: retryRecord?.retryInfo,
pursePub: pi.pursePub,
});
});
}
async function gatherPeerPushCreditPending(
ws: InternalWalletState,
tx: GetReadOnlyAccess<{
peerPushPaymentIncoming: typeof WalletStoresV1.peerPushPaymentIncoming;
operationRetries: typeof WalletStoresV1.operationRetries;
}>,
now: AbsoluteTime,
resp: PendingOperationsResponse,
): Promise<void> {
await tx.peerPushPaymentIncoming.iter().forEachAsync(async (pi) => {
switch (pi.status) {
2023-02-20 04:23:53 +01:00
case PeerPushPaymentIncomingStatus.Proposed:
return;
2023-05-05 10:56:42 +02:00
case PeerPushPaymentIncomingStatus.Done:
return;
}
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<PendingOperationsResponse> {
2022-03-18 15:32:41 +01:00
const now = AbsoluteTime.now();
2021-06-09 15:26:18 +02:00
return await ws.db
.mktx((x) => [
x.backupProviders,
x.exchanges,
x.exchangeDetails,
x.refreshGroups,
x.coins,
x.withdrawalGroups,
x.tips,
x.purchases,
x.planchets,
x.depositGroups,
x.recoupGroups,
x.operationRetries,
x.peerPullPaymentInitiations,
x.peerPushPaymentInitiations,
2023-02-19 23:13:44 +01:00
x.peerPullPaymentIncoming,
x.peerPushPaymentIncoming,
])
2021-06-09 15:26:18 +02:00
.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 gatherTipPending(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);
2023-02-19 23:13:44 +01:00
await gatherPeerPullDebitPending(ws, tx, now, resp);
await gatherPeerPushCreditPending(ws, tx, now, resp);
return resp;
2021-06-09 15:26:18 +02:00
});
}