/*
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
*/
/**
* Imports.
*/
import {
PendingOperationInfo,
PendingOperationsResponse,
getTimestampNow,
} from "../walletTypes";
import { runWithReadTransaction } from "../util/query";
import { InternalWalletState } from "./state";
import {
Stores,
ExchangeUpdateStatus,
ReserveRecordStatus,
CoinStatus,
ProposalStatus,
} from "../dbTypes";
export async function getPendingOperations(
ws: InternalWalletState,
): Promise {
const pendingOperations: PendingOperationInfo[] = [];
let minRetryDurationMs = 5000;
await runWithReadTransaction(
ws.db,
[
Stores.exchanges,
Stores.reserves,
Stores.refresh,
Stores.coins,
Stores.withdrawalSession,
Stores.proposals,
Stores.tips,
],
async tx => {
await tx.iter(Stores.exchanges).forEach(e => {
switch (e.updateStatus) {
case ExchangeUpdateStatus.FINISHED:
if (e.lastError) {
pendingOperations.push({
type: "bug",
message:
"Exchange record is in FINISHED state but has lastError set",
details: {
exchangeBaseUrl: e.baseUrl,
},
});
}
if (!e.details) {
pendingOperations.push({
type: "bug",
message:
"Exchange record does not have details, but no update in progress.",
details: {
exchangeBaseUrl: e.baseUrl,
},
});
}
if (!e.wireInfo) {
pendingOperations.push({
type: "bug",
message:
"Exchange record does not have wire info, but no update in progress.",
details: {
exchangeBaseUrl: e.baseUrl,
},
});
}
break;
case ExchangeUpdateStatus.FETCH_KEYS:
pendingOperations.push({
type: "exchange-update",
stage: "fetch-keys",
exchangeBaseUrl: e.baseUrl,
lastError: e.lastError,
reason: e.updateReason || "unknown",
});
break;
case ExchangeUpdateStatus.FETCH_WIRE:
pendingOperations.push({
type: "exchange-update",
stage: "fetch-wire",
exchangeBaseUrl: e.baseUrl,
lastError: e.lastError,
reason: e.updateReason || "unknown",
});
break;
default:
pendingOperations.push({
type: "bug",
message: "Unknown exchangeUpdateStatus",
details: {
exchangeBaseUrl: e.baseUrl,
exchangeUpdateStatus: e.updateStatus,
},
});
break;
}
});
await tx.iter(Stores.reserves).forEach(reserve => {
const reserveType = reserve.bankWithdrawStatusUrl
? "taler-bank"
: "manual";
const now = getTimestampNow();
switch (reserve.reserveStatus) {
case ReserveRecordStatus.DORMANT:
// nothing to report as pending
break;
case ReserveRecordStatus.WITHDRAWING:
case ReserveRecordStatus.UNCONFIRMED:
case ReserveRecordStatus.QUERYING_STATUS:
case ReserveRecordStatus.REGISTERING_BANK:
pendingOperations.push({
type: "reserve",
stage: reserve.reserveStatus,
timestampCreated: reserve.created,
reserveType,
reservePub: reserve.reservePub,
});
if (reserve.created.t_ms < now.t_ms - 5000) {
minRetryDurationMs = 500;
} else if (reserve.created.t_ms < now.t_ms - 30000) {
minRetryDurationMs = 2000;
}
break;
case ReserveRecordStatus.WAIT_CONFIRM_BANK:
pendingOperations.push({
type: "reserve",
stage: reserve.reserveStatus,
timestampCreated: reserve.created,
reserveType,
reservePub: reserve.reservePub,
bankWithdrawConfirmUrl: reserve.bankWithdrawConfirmUrl,
});
if (reserve.created.t_ms < now.t_ms - 5000) {
minRetryDurationMs = 500;
} else if (reserve.created.t_ms < now.t_ms - 30000) {
minRetryDurationMs = 2000;
}
break;
default:
pendingOperations.push({
type: "bug",
message: "Unknown reserve record status",
details: {
reservePub: reserve.reservePub,
reserveStatus: reserve.reserveStatus,
},
});
break;
}
});
await tx.iter(Stores.refresh).forEach(r => {
if (r.finished) {
return;
}
let refreshStatus: string;
if (r.norevealIndex === undefined) {
refreshStatus = "melt";
} else {
refreshStatus = "reveal";
}
pendingOperations.push({
type: "refresh",
oldCoinPub: r.meltCoinPub,
refreshStatus,
refreshOutputSize: r.newDenoms.length,
refreshSessionId: r.refreshSessionId,
});
});
await tx.iter(Stores.coins).forEach(coin => {
if (coin.status == CoinStatus.Dirty) {
pendingOperations.push({
type: "dirty-coin",
coinPub: coin.coinPub,
});
}
});
await tx.iter(Stores.withdrawalSession).forEach(ws => {
const numCoinsWithdrawn = ws.withdrawn.reduce(
(a, x) => a + (x ? 1 : 0),
0,
);
const numCoinsTotal = ws.withdrawn.length;
if (numCoinsWithdrawn < numCoinsTotal) {
pendingOperations.push({
type: "withdraw",
numCoinsTotal,
numCoinsWithdrawn,
source: ws.source,
withdrawSessionId: ws.withdrawSessionId,
});
}
});
await tx.iter(Stores.proposals).forEach((proposal) => {
if (proposal.proposalStatus == ProposalStatus.PROPOSED) {
pendingOperations.push({
type: "proposal-choice",
merchantBaseUrl: proposal.download!!.contractTerms.merchant_base_url,
proposalId: proposal.proposalId,
proposalTimestamp: proposal.timestamp,
});
} else if (proposal.proposalStatus == ProposalStatus.DOWNLOADING) {
pendingOperations.push({
type: "proposal-download",
merchantBaseUrl: proposal.download!!.contractTerms.merchant_base_url,
proposalId: proposal.proposalId,
proposalTimestamp: proposal.timestamp,
});
}
});
await tx.iter(Stores.tips).forEach((tip) => {
if (tip.accepted && !tip.pickedUp) {
pendingOperations.push({
type: "tip",
merchantBaseUrl: tip.merchantBaseUrl,
tipId: tip.tipId,
merchantTipId: tip.merchantTipId,
});
}
});
},
);
return {
pendingOperations,
nextRetryDelay: {
d_ms: minRetryDurationMs,
},
};
}