implement backup scheduling, other tweaks

This commit is contained in:
Florian Dold 2021-06-25 13:27:06 +02:00
parent 3603a68669
commit 42fe576320
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
17 changed files with 329 additions and 152 deletions

View File

@ -50,6 +50,7 @@ export enum NotificationType {
RefundApplyOperationError = "refund-apply-error", RefundApplyOperationError = "refund-apply-error",
RefundStatusOperationError = "refund-status-error", RefundStatusOperationError = "refund-status-error",
ProposalOperationError = "proposal-error", ProposalOperationError = "proposal-error",
BackupOperationError = "backup-error",
TipOperationError = "tip-error", TipOperationError = "tip-error",
PayOperationError = "pay-error", PayOperationError = "pay-error",
PayOperationSuccess = "pay-operation-success", PayOperationSuccess = "pay-operation-success",
@ -159,6 +160,11 @@ export interface RefreshOperationErrorNotification {
error: TalerErrorDetails; error: TalerErrorDetails;
} }
export interface BackupOperationErrorNotification {
type: NotificationType.BackupOperationError;
error: TalerErrorDetails;
}
export interface RefundStatusOperationErrorNotification { export interface RefundStatusOperationErrorNotification {
type: NotificationType.RefundStatusOperationError; type: NotificationType.RefundStatusOperationError;
error: TalerErrorDetails; error: TalerErrorDetails;
@ -234,6 +240,7 @@ export interface PayOperationSuccessNotification {
} }
export type WalletNotification = export type WalletNotification =
| BackupOperationErrorNotification
| WithdrawOperationErrorNotification | WithdrawOperationErrorNotification
| ReserveOperationErrorNotification | ReserveOperationErrorNotification
| ExchangeOperationErrorNotification | ExchangeOperationErrorNotification

View File

@ -1552,11 +1552,26 @@ export interface RecoupGroupRecord {
lastError: TalerErrorDetails | undefined; lastError: TalerErrorDetails | undefined;
} }
export enum BackupProviderStatus { export enum BackupProviderStateTag {
PaymentRequired = "payment-required", Provisional = "provisional",
Ready = "ready", Ready = "ready",
Retrying = "retrying",
} }
export type BackupProviderState =
| {
tag: BackupProviderStateTag.Provisional;
}
| {
tag: BackupProviderStateTag.Ready;
nextBackupTimestamp: Timestamp;
}
| {
tag: BackupProviderStateTag.Retrying;
retryInfo: RetryInfo;
lastError?: TalerErrorDetails;
};
export interface BackupProviderTerms { export interface BackupProviderTerms {
supportedProtocolVersion: string; supportedProtocolVersion: string;
annualFee: AmountString; annualFee: AmountString;
@ -1578,8 +1593,6 @@ export interface BackupProviderRecord {
*/ */
terms?: BackupProviderTerms; terms?: BackupProviderTerms;
active: boolean;
/** /**
* Hash of the last encrypted backup that we already merged * Hash of the last encrypted backup that we already merged
* or successfully uploaded ourselves. * or successfully uploaded ourselves.
@ -1599,6 +1612,8 @@ export interface BackupProviderRecord {
* Proposal that we're currently trying to pay for. * Proposal that we're currently trying to pay for.
* *
* (Also included in paymentProposalIds.) * (Also included in paymentProposalIds.)
*
* FIXME: Make this part of a proper BackupProviderState?
*/ */
currentPaymentProposalId?: string; currentPaymentProposalId?: string;
@ -1610,20 +1625,7 @@ export interface BackupProviderRecord {
*/ */
paymentProposalIds: string[]; paymentProposalIds: string[];
/** state: BackupProviderState;
* Next scheduled backup.
*/
nextBackupTimestamp?: Timestamp;
/**
* Retry info.
*/
retryInfo: RetryInfo;
/**
* Last error that occurred, if any.
*/
lastError: TalerErrorDetails | undefined;
/** /**
* UIDs for the operation that added the backup provider. * UIDs for the operation that added the backup provider.
@ -1851,7 +1853,15 @@ export const WalletStoresV1 = {
describeContents<BackupProviderRecord>("backupProviders", { describeContents<BackupProviderRecord>("backupProviders", {
keyPath: "baseUrl", keyPath: "baseUrl",
}), }),
{}, {
byPaymentProposalId: describeIndex(
"byPaymentProposalId",
"paymentProposalIds",
{
multiEntry: true,
},
),
},
), ),
depositGroups: describeStore( depositGroups: describeStore(
describeContents<DepositGroupRecord>("depositGroups", { describeContents<DepositGroupRecord>("depositGroups", {

View File

@ -263,7 +263,7 @@ export async function importBackup(
updateClock: backupExchange.update_clock, updateClock: backupExchange.update_clock,
}, },
permanent: true, permanent: true,
retryInfo: initRetryInfo(false), retryInfo: initRetryInfo(),
lastUpdate: undefined, lastUpdate: undefined,
nextUpdate: getTimestampNow(), nextUpdate: getTimestampNow(),
nextRefreshCheck: getTimestampNow(), nextRefreshCheck: getTimestampNow(),
@ -443,7 +443,7 @@ export async function importBackup(
timestampReserveInfoPosted: timestampReserveInfoPosted:
backupReserve.bank_info?.timestamp_reserve_info_posted, backupReserve.bank_info?.timestamp_reserve_info_posted,
senderWire: backupReserve.sender_wire, senderWire: backupReserve.sender_wire,
retryInfo: initRetryInfo(false), retryInfo: initRetryInfo(),
lastError: undefined, lastError: undefined,
lastSuccessfulStatusQuery: { t_ms: "never" }, lastSuccessfulStatusQuery: { t_ms: "never" },
initialWithdrawalGroupId: initialWithdrawalGroupId:
@ -483,7 +483,7 @@ export async function importBackup(
backupWg.raw_withdrawal_amount, backupWg.raw_withdrawal_amount,
), ),
reservePub, reservePub,
retryInfo: initRetryInfo(false), retryInfo: initRetryInfo(),
secretSeed: backupWg.secret_seed, secretSeed: backupWg.secret_seed,
timestampStart: backupWg.timestamp_created, timestampStart: backupWg.timestamp_created,
timestampFinish: backupWg.timestamp_finish, timestampFinish: backupWg.timestamp_finish,
@ -593,7 +593,7 @@ export async function importBackup(
cryptoComp.proposalNoncePrivToPub[backupProposal.nonce_priv], cryptoComp.proposalNoncePrivToPub[backupProposal.nonce_priv],
proposalId: backupProposal.proposal_id, proposalId: backupProposal.proposal_id,
repurchaseProposalId: backupProposal.repurchase_proposal_id, repurchaseProposalId: backupProposal.repurchase_proposal_id,
retryInfo: initRetryInfo(false), retryInfo: initRetryInfo(),
download, download,
proposalStatus, proposalStatus,
}); });
@ -728,7 +728,7 @@ export async function importBackup(
cryptoComp.proposalNoncePrivToPub[backupPurchase.nonce_priv], cryptoComp.proposalNoncePrivToPub[backupPurchase.nonce_priv],
lastPayError: undefined, lastPayError: undefined,
autoRefundDeadline: { t_ms: "never" }, autoRefundDeadline: { t_ms: "never" },
refundStatusRetryInfo: initRetryInfo(false), refundStatusRetryInfo: initRetryInfo(),
lastRefundStatusError: undefined, lastRefundStatusError: undefined,
timestampAccept: backupPurchase.timestamp_accept, timestampAccept: backupPurchase.timestamp_accept,
timestampFirstSuccessfulPay: timestampFirstSuccessfulPay:
@ -738,7 +738,7 @@ export async function importBackup(
lastSessionId: undefined, lastSessionId: undefined,
abortStatus, abortStatus,
// FIXME! // FIXME!
payRetryInfo: initRetryInfo(false), payRetryInfo: initRetryInfo(),
download, download,
paymentSubmitPending: !backupPurchase.timestamp_first_successful_pay, paymentSubmitPending: !backupPurchase.timestamp_first_successful_pay,
refundQueryRequested: false, refundQueryRequested: false,
@ -835,7 +835,7 @@ export async function importBackup(
Amounts.parseOrThrow(x.estimated_output_amount), Amounts.parseOrThrow(x.estimated_output_amount),
), ),
refreshSessionPerCoin, refreshSessionPerCoin,
retryInfo: initRetryInfo(false), retryInfo: initRetryInfo(),
}); });
} }
} }
@ -861,7 +861,7 @@ export async function importBackup(
merchantBaseUrl: backupTip.exchange_base_url, merchantBaseUrl: backupTip.exchange_base_url,
merchantTipId: backupTip.merchant_tip_id, merchantTipId: backupTip.merchant_tip_id,
pickedUpTimestamp: backupTip.timestamp_finished, pickedUpTimestamp: backupTip.timestamp_finished,
retryInfo: initRetryInfo(false), retryInfo: initRetryInfo(),
secretSeed: backupTip.secret_seed, secretSeed: backupTip.secret_seed,
tipAmountEffective: denomsSel.totalCoinValue, tipAmountEffective: denomsSel.totalCoinValue,
tipAmountRaw: Amounts.parseOrThrow(backupTip.tip_amount_raw), tipAmountRaw: Amounts.parseOrThrow(backupTip.tip_amount_raw),

View File

@ -41,6 +41,7 @@ import {
getTimestampNow, getTimestampNow,
j2s, j2s,
Logger, Logger,
NotificationType,
PreparePayResultType, PreparePayResultType,
RecoveryLoadRequest, RecoveryLoadRequest,
RecoveryMergeStrategy, RecoveryMergeStrategy,
@ -71,11 +72,15 @@ import {
import { CryptoApi } from "../../crypto/workers/cryptoApi.js"; import { CryptoApi } from "../../crypto/workers/cryptoApi.js";
import { import {
BackupProviderRecord, BackupProviderRecord,
BackupProviderState,
BackupProviderStateTag,
BackupProviderTerms, BackupProviderTerms,
ConfigRecord, ConfigRecord,
WalletBackupConfState, WalletBackupConfState,
WalletStoresV1,
WALLET_BACKUP_STATE_KEY, WALLET_BACKUP_STATE_KEY,
} from "../../db.js"; } from "../../db.js";
import { guardOperationException } from "../../errors.js";
import { import {
HttpResponseStatus, HttpResponseStatus,
readSuccessResponseJsonOrThrow, readSuccessResponseJsonOrThrow,
@ -85,7 +90,8 @@ import {
checkDbInvariant, checkDbInvariant,
checkLogicInvariant, checkLogicInvariant,
} from "../../util/invariants.js"; } from "../../util/invariants.js";
import { initRetryInfo } from "../../util/retries.js"; import { GetReadWriteAccess } from "../../util/query.js";
import { initRetryInfo, updateRetryInfoTimeout } from "../../util/retries.js";
import { import {
checkPaymentByProposalId, checkPaymentByProposalId,
confirmPay, confirmPay,
@ -247,6 +253,14 @@ interface BackupForProviderArgs {
retryAfterPayment: boolean; retryAfterPayment: boolean;
} }
function getNextBackupTimestamp(): Timestamp {
// FIXME: Randomize!
return timestampAddDuration(
getTimestampNow(),
durationFromSpec({ minutes: 5 }),
);
}
async function runBackupCycleForProvider( async function runBackupCycleForProvider(
ws: InternalWalletState, ws: InternalWalletState,
args: BackupForProviderArgs, args: BackupForProviderArgs,
@ -304,8 +318,11 @@ async function runBackupCycleForProvider(
if (!prov) { if (!prov) {
return; return;
} }
delete prov.lastError;
prov.lastBackupCycleTimestamp = getTimestampNow(); prov.lastBackupCycleTimestamp = getTimestampNow();
prov.state = {
tag: BackupProviderStateTag.Ready,
nextBackupTimestamp: getNextBackupTimestamp(),
};
await tx.backupProvider.put(prov); await tx.backupProvider.put(prov);
}); });
return; return;
@ -345,7 +362,9 @@ async function runBackupCycleForProvider(
ids.add(proposalId); ids.add(proposalId);
provRec.paymentProposalIds = Array.from(ids).sort(); provRec.paymentProposalIds = Array.from(ids).sort();
provRec.currentPaymentProposalId = proposalId; provRec.currentPaymentProposalId = proposalId;
// FIXME: allocate error code for this!
await tx.backupProviders.put(provRec); await tx.backupProviders.put(provRec);
await incrementBackupRetryInTx(tx, args.provider.baseUrl, undefined);
}); });
if (doPay) { if (doPay) {
@ -376,7 +395,10 @@ async function runBackupCycleForProvider(
} }
prov.lastBackupHash = encodeCrock(currentBackupHash); prov.lastBackupHash = encodeCrock(currentBackupHash);
prov.lastBackupCycleTimestamp = getTimestampNow(); prov.lastBackupCycleTimestamp = getTimestampNow();
prov.lastError = undefined; prov.state = {
tag: BackupProviderStateTag.Ready,
nextBackupTimestamp: getNextBackupTimestamp(),
};
await tx.backupProviders.put(prov); await tx.backupProviders.put(prov);
}); });
return; return;
@ -397,11 +419,19 @@ async function runBackupCycleForProvider(
return; return;
} }
prov.lastBackupHash = encodeCrock(hash(backupEnc)); prov.lastBackupHash = encodeCrock(hash(backupEnc));
prov.lastBackupCycleTimestamp = getTimestampNow(); // FIXME: Allocate error code for this situation?
prov.lastError = undefined; prov.state = {
tag: BackupProviderStateTag.Retrying,
retryInfo: initRetryInfo(),
};
await tx.backupProvider.put(prov); await tx.backupProvider.put(prov);
}); });
logger.info("processed existing backup"); logger.info("processed existing backup");
// Now upload our own, merged backup.
await runBackupCycleForProvider(ws, {
...args,
retryAfterPayment: false,
});
return; return;
} }
@ -412,17 +442,84 @@ async function runBackupCycleForProvider(
const err = await readTalerErrorResponse(resp); const err = await readTalerErrorResponse(resp);
logger.error(`got error response from backup provider: ${j2s(err)}`); logger.error(`got error response from backup provider: ${j2s(err)}`);
await ws.db await ws.db
.mktx((x) => ({ backupProvider: x.backupProviders })) .mktx((x) => ({ backupProviders: x.backupProviders }))
.runReadWrite(async (tx) => { .runReadWrite(async (tx) => {
const prov = await tx.backupProvider.get(provider.baseUrl); incrementBackupRetryInTx(tx, args.provider.baseUrl, err);
if (!prov) {
return;
}
prov.lastError = err;
await tx.backupProvider.put(prov);
}); });
} }
async function incrementBackupRetryInTx(
tx: GetReadWriteAccess<{
backupProviders: typeof WalletStoresV1.backupProviders;
}>,
backupProviderBaseUrl: string,
err: TalerErrorDetails | undefined,
): Promise<void> {
const pr = await tx.backupProviders.get(backupProviderBaseUrl);
if (!pr) {
return;
}
if (pr.state.tag === BackupProviderStateTag.Retrying) {
pr.state.retryInfo.retryCounter++;
pr.state.lastError = err;
updateRetryInfoTimeout(pr.state.retryInfo);
} else if (pr.state.tag === BackupProviderStateTag.Ready) {
pr.state = {
tag: BackupProviderStateTag.Retrying,
retryInfo: initRetryInfo(),
lastError: err,
};
}
await tx.backupProviders.put(pr);
}
async function incrementBackupRetry(
ws: InternalWalletState,
backupProviderBaseUrl: string,
err: TalerErrorDetails | undefined,
): Promise<void> {
await ws.db
.mktx((x) => ({ backupProviders: x.backupProviders }))
.runReadWrite(async (tx) =>
incrementBackupRetryInTx(tx, backupProviderBaseUrl, err),
);
}
export async function processBackupForProvider(
ws: InternalWalletState,
backupProviderBaseUrl: string,
): Promise<void> {
const provider = await ws.db
.mktx((x) => ({ backupProviders: x.backupProviders }))
.runReadOnly(async (tx) => {
return await tx.backupProviders.get(backupProviderBaseUrl);
});
if (!provider) {
throw Error("unknown backup provider");
}
const onOpErr = (err: TalerErrorDetails): Promise<void> =>
incrementBackupRetry(ws, backupProviderBaseUrl, err);
const run = async () => {
const backupJson = await exportBackup(ws);
const backupConfig = await provideBackupState(ws);
const encBackup = await encryptBackup(backupConfig, backupJson);
const currentBackupHash = hash(encBackup);
await runBackupCycleForProvider(ws, {
provider,
backupJson,
backupConfig,
encBackup,
currentBackupHash,
retryAfterPayment: true,
});
};
await guardOperationException(run, onOpErr);
}
/** /**
* Do one backup cycle that consists of: * Do one backup cycle that consists of:
* 1. Exporting a backup and try to upload it. * 1. Exporting a backup and try to upload it.
@ -436,14 +533,9 @@ export async function runBackupCycle(ws: InternalWalletState): Promise<void> {
.runReadOnly(async (tx) => { .runReadOnly(async (tx) => {
return await tx.backupProviders.iter().toArray(); return await tx.backupProviders.iter().toArray();
}); });
logger.trace("got backup providers", providers);
const backupJson = await exportBackup(ws); const backupJson = await exportBackup(ws);
logger.trace(`running backup cycle with backup JSON: ${j2s(backupJson)}`);
const backupConfig = await provideBackupState(ws); const backupConfig = await provideBackupState(ws);
const encBackup = await encryptBackup(backupConfig, backupJson); const encBackup = await encryptBackup(backupConfig, backupJson);
const currentBackupHash = hash(encBackup); const currentBackupHash = hash(encBackup);
for (const provider of providers) { for (const provider of providers) {
@ -506,7 +598,10 @@ export async function addBackupProvider(
if (oldProv) { if (oldProv) {
logger.info("old backup provider found"); logger.info("old backup provider found");
if (req.activate) { if (req.activate) {
oldProv.active = true; oldProv.state = {
tag: BackupProviderStateTag.Ready,
nextBackupTimestamp: getTimestampNow(),
};
logger.info("setting existing backup provider to active"); logger.info("setting existing backup provider to active");
await tx.backupProviders.put(oldProv); await tx.backupProviders.put(oldProv);
} }
@ -522,8 +617,19 @@ export async function addBackupProvider(
await ws.db await ws.db
.mktx((x) => ({ backupProviders: x.backupProviders })) .mktx((x) => ({ backupProviders: x.backupProviders }))
.runReadWrite(async (tx) => { .runReadWrite(async (tx) => {
let state: BackupProviderState;
if (req.activate) {
state = {
tag: BackupProviderStateTag.Ready,
nextBackupTimestamp: getTimestampNow(),
};
} else {
state = {
tag: BackupProviderStateTag.Provisional,
};
}
await tx.backupProviders.put({ await tx.backupProviders.put({
active: !!req.activate, state,
terms: { terms: {
annualFee: terms.annual_fee, annualFee: terms.annual_fee,
storageLimitInMegabytes: terms.storage_limit_in_megabytes, storageLimitInMegabytes: terms.storage_limit_in_megabytes,
@ -531,8 +637,6 @@ export async function addBackupProvider(
}, },
paymentProposalIds: [], paymentProposalIds: [],
baseUrl: canonUrl, baseUrl: canonUrl,
lastError: undefined,
retryInfo: initRetryInfo(false),
uids: [encodeCrock(getRandomBytes(32))], uids: [encodeCrock(getRandomBytes(32))],
}); });
}); });
@ -697,11 +801,14 @@ export async function getBackupInfo(
const providers: ProviderInfo[] = []; const providers: ProviderInfo[] = [];
for (const x of providerRecords) { for (const x of providerRecords) {
providers.push({ providers.push({
active: x.active, active: x.state.tag !== BackupProviderStateTag.Provisional,
syncProviderBaseUrl: x.baseUrl, syncProviderBaseUrl: x.baseUrl,
lastSuccessfulBackupTimestamp: x.lastBackupCycleTimestamp, lastSuccessfulBackupTimestamp: x.lastBackupCycleTimestamp,
paymentProposalIds: x.paymentProposalIds, paymentProposalIds: x.paymentProposalIds,
lastError: x.lastError, lastError:
x.state.tag === BackupProviderStateTag.Retrying
? x.state.lastError
: undefined,
paymentStatus: await getProviderPaymentInfo(ws, x), paymentStatus: await getProviderPaymentInfo(ws, x),
terms: x.terms, terms: x.terms,
}); });
@ -728,7 +835,7 @@ export async function getBackupRecovery(
}); });
return { return {
providers: providers providers: providers
.filter((x) => x.active) .filter((x) => x.state.tag !== BackupProviderStateTag.Provisional)
.map((x) => { .map((x) => {
return { return {
url: x.baseUrl, url: x.baseUrl,
@ -763,11 +870,12 @@ async function backupRecoveryTheirs(
const existingProv = await tx.backupProviders.get(prov.url); const existingProv = await tx.backupProviders.get(prov.url);
if (!existingProv) { if (!existingProv) {
await tx.backupProviders.put({ await tx.backupProviders.put({
active: true,
baseUrl: prov.url, baseUrl: prov.url,
paymentProposalIds: [], paymentProposalIds: [],
retryInfo: initRetryInfo(false), state: {
lastError: undefined, tag: BackupProviderStateTag.Ready,
nextBackupTimestamp: getTimestampNow(),
},
uids: [encodeCrock(getRandomBytes(32))], uids: [encodeCrock(getRandomBytes(32))],
}); });
} }

View File

@ -443,7 +443,7 @@ export async function createDepositGroup(
payto_uri: req.depositPaytoUri, payto_uri: req.depositPaytoUri,
salt: wireSalt, salt: wireSalt,
}, },
retryInfo: initRetryInfo(true), retryInfo: initRetryInfo(),
lastError: undefined, lastError: undefined,
}; };

View File

@ -297,7 +297,7 @@ async function provideExchangeRecord(
r = { r = {
permanent: true, permanent: true,
baseUrl: baseUrl, baseUrl: baseUrl,
retryInfo: initRetryInfo(false), retryInfo: initRetryInfo(),
detailsPointer: undefined, detailsPointer: undefined,
lastUpdate: undefined, lastUpdate: undefined,
nextUpdate: now, nextUpdate: now,
@ -498,7 +498,7 @@ async function updateExchangeFromUrlImpl(
}; };
// FIXME: only update if pointer got updated // FIXME: only update if pointer got updated
r.lastError = undefined; r.lastError = undefined;
r.retryInfo = initRetryInfo(false); r.retryInfo = initRetryInfo();
r.lastUpdate = getTimestampNow(); r.lastUpdate = getTimestampNow();
(r.nextUpdate = keysInfo.expiry), (r.nextUpdate = keysInfo.expiry),
// New denominations might be available. // New denominations might be available.

View File

@ -77,6 +77,7 @@ import {
AbortStatus, AbortStatus,
AllowedAuditorInfo, AllowedAuditorInfo,
AllowedExchangeInfo, AllowedExchangeInfo,
BackupProviderStateTag,
CoinRecord, CoinRecord,
CoinStatus, CoinStatus,
DenominationRecord, DenominationRecord,
@ -489,7 +490,7 @@ async function recordConfirmPay(
if (p) { if (p) {
p.proposalStatus = ProposalStatus.ACCEPTED; p.proposalStatus = ProposalStatus.ACCEPTED;
delete p.lastError; delete p.lastError;
p.retryInfo = initRetryInfo(false); p.retryInfo = initRetryInfo();
await tx.proposals.put(p); await tx.proposals.put(p);
} }
await tx.purchases.put(t); await tx.purchases.put(t);
@ -942,7 +943,7 @@ async function storeFirstPaySuccess(
purchase.paymentSubmitPending = false; purchase.paymentSubmitPending = false;
purchase.lastPayError = undefined; purchase.lastPayError = undefined;
purchase.lastSessionId = sessionId; purchase.lastSessionId = sessionId;
purchase.payRetryInfo = initRetryInfo(false); purchase.payRetryInfo = initRetryInfo();
purchase.merchantPaySig = paySig; purchase.merchantPaySig = paySig;
if (isFirst) { if (isFirst) {
const ar = purchase.download.contractData.autoRefund; const ar = purchase.download.contractData.autoRefund;
@ -978,7 +979,7 @@ async function storePayReplaySuccess(
} }
purchase.paymentSubmitPending = false; purchase.paymentSubmitPending = false;
purchase.lastPayError = undefined; purchase.lastPayError = undefined;
purchase.payRetryInfo = initRetryInfo(false); purchase.payRetryInfo = initRetryInfo();
purchase.lastSessionId = sessionId; purchase.lastSessionId = sessionId;
await tx.purchases.put(purchase); await tx.purchases.put(purchase);
}); });
@ -1100,6 +1101,26 @@ async function handleInsufficientFunds(
}); });
} }
async function unblockBackup(
ws: InternalWalletState,
proposalId: string,
): Promise<void> {
await ws.db
.mktx((x) => ({ backupProviders: x.backupProviders }))
.runReadWrite(async (tx) => {
const bp = await tx.backupProviders.indexes.byPaymentProposalId
.iter(proposalId)
.forEachAsync(async (bp) => {
if (bp.state.tag === BackupProviderStateTag.Retrying) {
bp.state = {
tag: BackupProviderStateTag.Ready,
nextBackupTimestamp: getTimestampNow(),
};
}
});
});
}
/** /**
* Submit a payment to the merchant. * Submit a payment to the merchant.
* *
@ -1228,6 +1249,7 @@ async function submitPay(
} }
await storeFirstPaySuccess(ws, proposalId, sessionId, merchantResp.sig); await storeFirstPaySuccess(ws, proposalId, sessionId, merchantResp.sig);
await unblockBackup(ws, proposalId);
} else { } else {
const payAgainUrl = new URL( const payAgainUrl = new URL(
`orders/${purchase.download.contractData.orderId}/paid`, `orders/${purchase.download.contractData.orderId}/paid`,
@ -1266,6 +1288,7 @@ async function submitPay(
); );
} }
await storePayReplaySuccess(ws, proposalId, sessionId); await storePayReplaySuccess(ws, proposalId, sessionId);
await unblockBackup(ws, proposalId);
} }
ws.notify({ ws.notify({

View File

@ -14,6 +14,10 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/ */
/**
* Derive pending tasks from the wallet database.
*/
/** /**
* Imports. * Imports.
*/ */
@ -22,13 +26,18 @@ import {
ReserveRecordStatus, ReserveRecordStatus,
AbortStatus, AbortStatus,
WalletStoresV1, WalletStoresV1,
BackupProviderStateTag,
} from "../db.js"; } from "../db.js";
import { import {
PendingOperationsResponse, PendingOperationsResponse,
PendingOperationType, PendingTaskType,
ReserveType, ReserveType,
} from "../pending-types.js"; } from "../pending-types.js";
import { getTimestampNow, Timestamp } from "@gnu-taler/taler-util"; import {
getTimestampNow,
isTimestampExpired,
Timestamp,
} from "@gnu-taler/taler-util";
import { InternalWalletState } from "../common.js"; import { InternalWalletState } from "../common.js";
import { getBalancesInsideTransaction } from "./balance.js"; import { getBalancesInsideTransaction } from "./balance.js";
import { GetReadOnlyAccess } from "../util/query.js"; import { GetReadOnlyAccess } from "../util/query.js";
@ -43,7 +52,7 @@ async function gatherExchangePending(
): Promise<void> { ): Promise<void> {
await tx.exchanges.iter().forEachAsync(async (e) => { await tx.exchanges.iter().forEachAsync(async (e) => {
resp.pendingOperations.push({ resp.pendingOperations.push({
type: PendingOperationType.ExchangeUpdate, type: PendingTaskType.ExchangeUpdate,
givesLifeness: false, givesLifeness: false,
timestampDue: e.nextUpdate, timestampDue: e.nextUpdate,
exchangeBaseUrl: e.baseUrl, exchangeBaseUrl: e.baseUrl,
@ -51,7 +60,7 @@ async function gatherExchangePending(
}); });
resp.pendingOperations.push({ resp.pendingOperations.push({
type: PendingOperationType.ExchangeCheckRefresh, type: PendingTaskType.ExchangeCheckRefresh,
timestampDue: e.nextRefreshCheck, timestampDue: e.nextRefreshCheck,
givesLifeness: false, givesLifeness: false,
exchangeBaseUrl: e.baseUrl, exchangeBaseUrl: e.baseUrl,
@ -76,7 +85,7 @@ async function gatherReservePending(
case ReserveRecordStatus.QUERYING_STATUS: case ReserveRecordStatus.QUERYING_STATUS:
case ReserveRecordStatus.REGISTERING_BANK: case ReserveRecordStatus.REGISTERING_BANK:
resp.pendingOperations.push({ resp.pendingOperations.push({
type: PendingOperationType.Reserve, type: PendingTaskType.Reserve,
givesLifeness: true, givesLifeness: true,
timestampDue: reserve.retryInfo.nextRetry, timestampDue: reserve.retryInfo.nextRetry,
stage: reserve.reserveStatus, stage: reserve.reserveStatus,
@ -103,7 +112,7 @@ async function gatherRefreshPending(
return; return;
} }
resp.pendingOperations.push({ resp.pendingOperations.push({
type: PendingOperationType.Refresh, type: PendingTaskType.Refresh,
givesLifeness: true, givesLifeness: true,
timestampDue: r.retryInfo.nextRetry, timestampDue: r.retryInfo.nextRetry,
refreshGroupId: r.refreshGroupId, refreshGroupId: r.refreshGroupId,
@ -136,7 +145,7 @@ async function gatherWithdrawalPending(
} }
}); });
resp.pendingOperations.push({ resp.pendingOperations.push({
type: PendingOperationType.Withdraw, type: PendingTaskType.Withdraw,
givesLifeness: true, givesLifeness: true,
timestampDue: wsr.retryInfo.nextRetry, timestampDue: wsr.retryInfo.nextRetry,
withdrawalGroupId: wsr.withdrawalGroupId, withdrawalGroupId: wsr.withdrawalGroupId,
@ -157,7 +166,7 @@ async function gatherProposalPending(
} else if (proposal.proposalStatus == ProposalStatus.DOWNLOADING) { } else if (proposal.proposalStatus == ProposalStatus.DOWNLOADING) {
const timestampDue = proposal.retryInfo?.nextRetry ?? getTimestampNow(); const timestampDue = proposal.retryInfo?.nextRetry ?? getTimestampNow();
resp.pendingOperations.push({ resp.pendingOperations.push({
type: PendingOperationType.ProposalDownload, type: PendingTaskType.ProposalDownload,
givesLifeness: true, givesLifeness: true,
timestampDue, timestampDue,
merchantBaseUrl: proposal.merchantBaseUrl, merchantBaseUrl: proposal.merchantBaseUrl,
@ -182,7 +191,7 @@ async function gatherTipPending(
} }
if (tip.acceptedTimestamp) { if (tip.acceptedTimestamp) {
resp.pendingOperations.push({ resp.pendingOperations.push({
type: PendingOperationType.TipPickup, type: PendingTaskType.TipPickup,
givesLifeness: true, givesLifeness: true,
timestampDue: tip.retryInfo.nextRetry, timestampDue: tip.retryInfo.nextRetry,
merchantBaseUrl: tip.merchantBaseUrl, merchantBaseUrl: tip.merchantBaseUrl,
@ -202,7 +211,7 @@ async function gatherPurchasePending(
if (pr.paymentSubmitPending && pr.abortStatus === AbortStatus.None) { if (pr.paymentSubmitPending && pr.abortStatus === AbortStatus.None) {
const timestampDue = pr.payRetryInfo?.nextRetry ?? getTimestampNow(); const timestampDue = pr.payRetryInfo?.nextRetry ?? getTimestampNow();
resp.pendingOperations.push({ resp.pendingOperations.push({
type: PendingOperationType.Pay, type: PendingTaskType.Pay,
givesLifeness: true, givesLifeness: true,
timestampDue, timestampDue,
isReplay: false, isReplay: false,
@ -213,7 +222,7 @@ async function gatherPurchasePending(
} }
if (pr.refundQueryRequested) { if (pr.refundQueryRequested) {
resp.pendingOperations.push({ resp.pendingOperations.push({
type: PendingOperationType.RefundQuery, type: PendingTaskType.RefundQuery,
givesLifeness: true, givesLifeness: true,
timestampDue: pr.refundStatusRetryInfo.nextRetry, timestampDue: pr.refundStatusRetryInfo.nextRetry,
proposalId: pr.proposalId, proposalId: pr.proposalId,
@ -234,7 +243,7 @@ async function gatherRecoupPending(
return; return;
} }
resp.pendingOperations.push({ resp.pendingOperations.push({
type: PendingOperationType.Recoup, type: PendingTaskType.Recoup,
givesLifeness: true, givesLifeness: true,
timestampDue: rg.retryInfo.nextRetry, timestampDue: rg.retryInfo.nextRetry,
recoupGroupId: rg.recoupGroupId, recoupGroupId: rg.recoupGroupId,
@ -244,23 +253,32 @@ async function gatherRecoupPending(
}); });
} }
async function gatherDepositPending( async function gatherBackupPending(
tx: GetReadOnlyAccess<{ depositGroups: typeof WalletStoresV1.depositGroups }>, tx: GetReadOnlyAccess<{
backupProviders: typeof WalletStoresV1.backupProviders;
}>,
now: Timestamp, now: Timestamp,
resp: PendingOperationsResponse, resp: PendingOperationsResponse,
): Promise<void> { ): Promise<void> {
await tx.depositGroups.iter().forEach((dg) => { await tx.backupProviders.iter().forEach((bp) => {
if (dg.timestampFinished) { if (bp.state.tag === BackupProviderStateTag.Ready) {
return; resp.pendingOperations.push({
type: PendingTaskType.Backup,
givesLifeness: false,
timestampDue: bp.state.nextBackupTimestamp,
backupProviderBaseUrl: bp.baseUrl,
lastError: undefined,
});
} else if (bp.state.tag === BackupProviderStateTag.Retrying) {
resp.pendingOperations.push({
type: PendingTaskType.Backup,
givesLifeness: false,
timestampDue: bp.state.retryInfo.nextRetry,
backupProviderBaseUrl: bp.baseUrl,
retryInfo: bp.state.retryInfo,
lastError: bp.state.lastError,
});
} }
resp.pendingOperations.push({
type: PendingOperationType.Deposit,
givesLifeness: true,
timestampDue: dg.retryInfo.nextRetry,
depositGroupId: dg.depositGroupId,
retryInfo: dg.retryInfo,
lastError: dg.lastError,
});
}); });
} }
@ -270,6 +288,7 @@ export async function getPendingOperations(
const now = getTimestampNow(); const now = getTimestampNow();
return await ws.db return await ws.db
.mktx((x) => ({ .mktx((x) => ({
backupProviders: x.backupProviders,
exchanges: x.exchanges, exchanges: x.exchanges,
exchangeDetails: x.exchangeDetails, exchangeDetails: x.exchangeDetails,
reserves: x.reserves, reserves: x.reserves,
@ -297,7 +316,7 @@ export async function getPendingOperations(
await gatherTipPending(tx, now, resp); await gatherTipPending(tx, now, resp);
await gatherPurchasePending(tx, now, resp); await gatherPurchasePending(tx, now, resp);
await gatherRecoupPending(tx, now, resp); await gatherRecoupPending(tx, now, resp);
await gatherDepositPending(tx, now, resp); await gatherBackupPending(tx, now, resp);
return resp; return resp;
}); });
} }

View File

@ -109,7 +109,7 @@ async function putGroupAsFinished(
if (allFinished) { if (allFinished) {
logger.trace("all recoups of recoup group are finished"); logger.trace("all recoups of recoup group are finished");
recoupGroup.timestampFinished = getTimestampNow(); recoupGroup.timestampFinished = getTimestampNow();
recoupGroup.retryInfo = initRetryInfo(false); recoupGroup.retryInfo = initRetryInfo();
recoupGroup.lastError = undefined; recoupGroup.lastError = undefined;
if (recoupGroup.scheduleRefreshCoins.length > 0) { if (recoupGroup.scheduleRefreshCoins.length > 0) {
const refreshGroupId = await createRefreshGroup( const refreshGroupId = await createRefreshGroup(

View File

@ -203,7 +203,7 @@ async function refreshCreateSession(
} }
if (allDone) { if (allDone) {
rg.timestampFinished = getTimestampNow(); rg.timestampFinished = getTimestampNow();
rg.retryInfo = initRetryInfo(false); rg.retryInfo = initRetryInfo();
} }
await tx.refreshGroups.put(rg); await tx.refreshGroups.put(rg);
}); });
@ -590,7 +590,7 @@ async function refreshReveal(
} }
if (allDone) { if (allDone) {
rg.timestampFinished = getTimestampNow(); rg.timestampFinished = getTimestampNow();
rg.retryInfo = initRetryInfo(false); rg.retryInfo = initRetryInfo();
} }
for (const coin of coins) { for (const coin of coins) {
await tx.coins.put(coin); await tx.coins.put(coin);

View File

@ -405,7 +405,7 @@ async function acceptRefunds(
if (queryDone) { if (queryDone) {
p.timestampLastRefundStatus = now; p.timestampLastRefundStatus = now;
p.lastRefundStatusError = undefined; p.lastRefundStatusError = undefined;
p.refundStatusRetryInfo = initRetryInfo(false); p.refundStatusRetryInfo = initRetryInfo();
p.refundQueryRequested = false; p.refundQueryRequested = false;
if (p.abortStatus === AbortStatus.AbortRefund) { if (p.abortStatus === AbortStatus.AbortRefund) {
p.abortStatus = AbortStatus.AbortFinished; p.abortStatus = AbortStatus.AbortFinished;
@ -768,7 +768,7 @@ export async function abortFailedPayWithRefund(
purchase.paymentSubmitPending = false; purchase.paymentSubmitPending = false;
purchase.abortStatus = AbortStatus.AbortRefund; purchase.abortStatus = AbortStatus.AbortRefund;
purchase.lastPayError = undefined; purchase.lastPayError = undefined;
purchase.payRetryInfo = initRetryInfo(false); purchase.payRetryInfo = initRetryInfo();
await tx.purchases.put(purchase); await tx.purchases.put(purchase);
}); });
processPurchaseQueryRefund(ws, proposalId, true).catch((e) => { processPurchaseQueryRefund(ws, proposalId, true).catch((e) => {

View File

@ -651,7 +651,7 @@ async function updateReserve(
if (denomSelInfo.selectedDenoms.length === 0) { if (denomSelInfo.selectedDenoms.length === 0) {
newReserve.reserveStatus = ReserveRecordStatus.DORMANT; newReserve.reserveStatus = ReserveRecordStatus.DORMANT;
newReserve.lastError = undefined; newReserve.lastError = undefined;
newReserve.retryInfo = initRetryInfo(false); newReserve.retryInfo = initRetryInfo();
await tx.reserves.put(newReserve); await tx.reserves.put(newReserve);
return; return;
} }
@ -679,7 +679,7 @@ async function updateReserve(
}; };
newReserve.lastError = undefined; newReserve.lastError = undefined;
newReserve.retryInfo = initRetryInfo(false); newReserve.retryInfo = initRetryInfo();
newReserve.reserveStatus = ReserveRecordStatus.DORMANT; newReserve.reserveStatus = ReserveRecordStatus.DORMANT;
await tx.reserves.put(newReserve); await tx.reserves.put(newReserve);

View File

@ -388,7 +388,7 @@ async function processTipImpl(
} }
tr.pickedUpTimestamp = getTimestampNow(); tr.pickedUpTimestamp = getTimestampNow();
tr.lastError = undefined; tr.lastError = undefined;
tr.retryInfo = initRetryInfo(false); tr.retryInfo = initRetryInfo();
await tx.tips.put(tr); await tx.tips.put(tr);
for (const cr of newCoinRecords) { for (const cr of newCoinRecords) {
await tx.coins.put(cr); await tx.coins.put(cr);

View File

@ -875,7 +875,7 @@ async function processWithdrawGroupImpl(
finishedForFirstTime = true; finishedForFirstTime = true;
wg.timestampFinish = getTimestampNow(); wg.timestampFinish = getTimestampNow();
wg.lastError = undefined; wg.lastError = undefined;
wg.retryInfo = initRetryInfo(false); wg.retryInfo = initRetryInfo();
} }
await tx.withdrawalGroups.put(wg); await tx.withdrawalGroups.put(wg);

View File

@ -15,9 +15,9 @@
*/ */
/** /**
* Type and schema definitions for pending operations in the wallet. * Type and schema definitions for pending tasks in the wallet.
* *
* These are only used internally, and are not part of the public * These are only used internally, and are not part of the stable public
* interface to the wallet. * interface to the wallet.
*/ */
@ -32,7 +32,7 @@ import {
import { ReserveRecordStatus } from "./db.js"; import { ReserveRecordStatus } from "./db.js";
import { RetryInfo } from "./util/retries.js"; import { RetryInfo } from "./util/retries.js";
export enum PendingOperationType { export enum PendingTaskType {
ExchangeUpdate = "exchange-update", ExchangeUpdate = "exchange-update",
ExchangeCheckRefresh = "exchange-check-refresh", ExchangeCheckRefresh = "exchange-check-refresh",
Pay = "pay", Pay = "pay",
@ -45,31 +45,39 @@ export enum PendingOperationType {
TipPickup = "tip-pickup", TipPickup = "tip-pickup",
Withdraw = "withdraw", Withdraw = "withdraw",
Deposit = "deposit", Deposit = "deposit",
Backup = "backup",
} }
/** /**
* Information about a pending operation. * Information about a pending operation.
*/ */
export type PendingOperationInfo = PendingOperationInfoCommon & export type PendingTaskInfo = PendingTaskInfoCommon &
( (
| PendingExchangeUpdateOperation | PendingExchangeUpdateTask
| PendingExchangeCheckRefreshOperation | PendingExchangeCheckRefreshTask
| PendingPayOperation | PendingPayTask
| PendingProposalDownloadOperation | PendingProposalDownloadTask
| PendingRefreshOperation | PendingRefreshTask
| PendingRefundQueryOperation | PendingRefundQueryTask
| PendingReserveOperation | PendingReserveTask
| PendingTipPickupOperation | PendingTipPickupTask
| PendingWithdrawOperation | PendingWithdrawTask
| PendingRecoupOperation | PendingRecoupTask
| PendingDepositOperation | PendingDepositTask
| PendingBackupTask
); );
export interface PendingBackupTask {
type: PendingTaskType.Backup;
backupProviderBaseUrl: string;
lastError: TalerErrorDetails | undefined;
}
/** /**
* The wallet is currently updating information about an exchange. * The wallet is currently updating information about an exchange.
*/ */
export interface PendingExchangeUpdateOperation { export interface PendingExchangeUpdateTask {
type: PendingOperationType.ExchangeUpdate; type: PendingTaskType.ExchangeUpdate;
exchangeBaseUrl: string; exchangeBaseUrl: string;
lastError: TalerErrorDetails | undefined; lastError: TalerErrorDetails | undefined;
} }
@ -78,8 +86,8 @@ export interface PendingExchangeUpdateOperation {
* The wallet should check whether coins from this exchange * The wallet should check whether coins from this exchange
* need to be auto-refreshed. * need to be auto-refreshed.
*/ */
export interface PendingExchangeCheckRefreshOperation { export interface PendingExchangeCheckRefreshTask {
type: PendingOperationType.ExchangeCheckRefresh; type: PendingTaskType.ExchangeCheckRefresh;
exchangeBaseUrl: string; exchangeBaseUrl: string;
} }
@ -100,8 +108,8 @@ export enum ReserveType {
* Does *not* include the withdrawal operation that might result * Does *not* include the withdrawal operation that might result
* from this. * from this.
*/ */
export interface PendingReserveOperation { export interface PendingReserveTask {
type: PendingOperationType.Reserve; type: PendingTaskType.Reserve;
retryInfo: RetryInfo | undefined; retryInfo: RetryInfo | undefined;
stage: ReserveRecordStatus; stage: ReserveRecordStatus;
timestampCreated: Timestamp; timestampCreated: Timestamp;
@ -113,8 +121,8 @@ export interface PendingReserveOperation {
/** /**
* Status of an ongoing withdrawal operation. * Status of an ongoing withdrawal operation.
*/ */
export interface PendingRefreshOperation { export interface PendingRefreshTask {
type: PendingOperationType.Refresh; type: PendingTaskType.Refresh;
lastError?: TalerErrorDetails; lastError?: TalerErrorDetails;
refreshGroupId: string; refreshGroupId: string;
finishedPerCoin: boolean[]; finishedPerCoin: boolean[];
@ -124,8 +132,8 @@ export interface PendingRefreshOperation {
/** /**
* Status of downloading signed contract terms from a merchant. * Status of downloading signed contract terms from a merchant.
*/ */
export interface PendingProposalDownloadOperation { export interface PendingProposalDownloadTask {
type: PendingOperationType.ProposalDownload; type: PendingTaskType.ProposalDownload;
merchantBaseUrl: string; merchantBaseUrl: string;
proposalTimestamp: Timestamp; proposalTimestamp: Timestamp;
proposalId: string; proposalId: string;
@ -139,7 +147,7 @@ export interface PendingProposalDownloadOperation {
* proposed contract terms. * proposed contract terms.
*/ */
export interface PendingProposalChoiceOperation { export interface PendingProposalChoiceOperation {
type: PendingOperationType.ProposalChoice; type: PendingTaskType.ProposalChoice;
merchantBaseUrl: string; merchantBaseUrl: string;
proposalTimestamp: Timestamp; proposalTimestamp: Timestamp;
proposalId: string; proposalId: string;
@ -148,8 +156,8 @@ export interface PendingProposalChoiceOperation {
/** /**
* The wallet is picking up a tip that the user has accepted. * The wallet is picking up a tip that the user has accepted.
*/ */
export interface PendingTipPickupOperation { export interface PendingTipPickupTask {
type: PendingOperationType.TipPickup; type: PendingTaskType.TipPickup;
tipId: string; tipId: string;
merchantBaseUrl: string; merchantBaseUrl: string;
merchantTipId: string; merchantTipId: string;
@ -159,8 +167,8 @@ export interface PendingTipPickupOperation {
* The wallet is signing coins and then sending them to * The wallet is signing coins and then sending them to
* the merchant. * the merchant.
*/ */
export interface PendingPayOperation { export interface PendingPayTask {
type: PendingOperationType.Pay; type: PendingTaskType.Pay;
proposalId: string; proposalId: string;
isReplay: boolean; isReplay: boolean;
retryInfo?: RetryInfo; retryInfo?: RetryInfo;
@ -171,15 +179,15 @@ export interface PendingPayOperation {
* The wallet is querying the merchant about whether any refund * The wallet is querying the merchant about whether any refund
* permissions are available for a purchase. * permissions are available for a purchase.
*/ */
export interface PendingRefundQueryOperation { export interface PendingRefundQueryTask {
type: PendingOperationType.RefundQuery; type: PendingTaskType.RefundQuery;
proposalId: string; proposalId: string;
retryInfo: RetryInfo; retryInfo: RetryInfo;
lastError: TalerErrorDetails | undefined; lastError: TalerErrorDetails | undefined;
} }
export interface PendingRecoupOperation { export interface PendingRecoupTask {
type: PendingOperationType.Recoup; type: PendingTaskType.Recoup;
recoupGroupId: string; recoupGroupId: string;
retryInfo: RetryInfo; retryInfo: RetryInfo;
lastError: TalerErrorDetails | undefined; lastError: TalerErrorDetails | undefined;
@ -188,8 +196,8 @@ export interface PendingRecoupOperation {
/** /**
* Status of an ongoing withdrawal operation. * Status of an ongoing withdrawal operation.
*/ */
export interface PendingWithdrawOperation { export interface PendingWithdrawTask {
type: PendingOperationType.Withdraw; type: PendingTaskType.Withdraw;
lastError: TalerErrorDetails | undefined; lastError: TalerErrorDetails | undefined;
retryInfo: RetryInfo; retryInfo: RetryInfo;
withdrawalGroupId: string; withdrawalGroupId: string;
@ -198,8 +206,8 @@ export interface PendingWithdrawOperation {
/** /**
* Status of an ongoing deposit operation. * Status of an ongoing deposit operation.
*/ */
export interface PendingDepositOperation { export interface PendingDepositTask {
type: PendingOperationType.Deposit; type: PendingTaskType.Deposit;
lastError: TalerErrorDetails | undefined; lastError: TalerErrorDetails | undefined;
retryInfo: RetryInfo; retryInfo: RetryInfo;
depositGroupId: string; depositGroupId: string;
@ -208,11 +216,11 @@ export interface PendingDepositOperation {
/** /**
* Fields that are present in every pending operation. * Fields that are present in every pending operation.
*/ */
export interface PendingOperationInfoCommon { export interface PendingTaskInfoCommon {
/** /**
* Type of the pending operation. * Type of the pending operation.
*/ */
type: PendingOperationType; type: PendingTaskType;
/** /**
* Set to true if the operation indicates that something is really in progress, * Set to true if the operation indicates that something is really in progress,
@ -239,7 +247,7 @@ export interface PendingOperationsResponse {
/** /**
* List of pending operations. * List of pending operations.
*/ */
pendingOperations: PendingOperationInfo[]; pendingOperations: PendingTaskInfo[];
/** /**
* Current wallet balance, including pending balances. * Current wallet balance, including pending balances.

View File

@ -72,13 +72,11 @@ export function getRetryDuration(
} }
export function initRetryInfo( export function initRetryInfo(
active = true,
p: RetryPolicy = defaultRetryPolicy, p: RetryPolicy = defaultRetryPolicy,
): RetryInfo { ): RetryInfo {
const now = getTimestampNow(); const now = getTimestampNow();
const info = { const info = {
firstTry: now, firstTry: now,
active: true,
nextRetry: now, nextRetry: now,
retryCounter: 0, retryCounter: 0,
}; };

View File

@ -44,6 +44,7 @@ import {
getBackupInfo, getBackupInfo,
getBackupRecovery, getBackupRecovery,
loadBackupRecovery, loadBackupRecovery,
processBackupForProvider,
runBackupCycle, runBackupCycle,
} from "./operations/backup/index.js"; } from "./operations/backup/index.js";
import { exportBackup } from "./operations/backup/export.js"; import { exportBackup } from "./operations/backup/export.js";
@ -118,9 +119,9 @@ import {
} from "./db.js"; } from "./db.js";
import { NotificationType } from "@gnu-taler/taler-util"; import { NotificationType } from "@gnu-taler/taler-util";
import { import {
PendingOperationInfo, PendingTaskInfo,
PendingOperationsResponse, PendingOperationsResponse,
PendingOperationType, PendingTaskType,
} from "./pending-types.js"; } from "./pending-types.js";
import { CoinDumpJson } from "@gnu-taler/taler-util"; import { CoinDumpJson } from "@gnu-taler/taler-util";
import { codecForTransactionsRequest } from "@gnu-taler/taler-util"; import { codecForTransactionsRequest } from "@gnu-taler/taler-util";
@ -206,44 +207,47 @@ async function getWithdrawalDetailsForAmount(
*/ */
async function processOnePendingOperation( async function processOnePendingOperation(
ws: InternalWalletState, ws: InternalWalletState,
pending: PendingOperationInfo, pending: PendingTaskInfo,
forceNow = false, forceNow = false,
): Promise<void> { ): Promise<void> {
logger.trace(`running pending ${JSON.stringify(pending, undefined, 2)}`); logger.trace(`running pending ${JSON.stringify(pending, undefined, 2)}`);
switch (pending.type) { switch (pending.type) {
case PendingOperationType.ExchangeUpdate: case PendingTaskType.ExchangeUpdate:
await updateExchangeFromUrl(ws, pending.exchangeBaseUrl, forceNow); await updateExchangeFromUrl(ws, pending.exchangeBaseUrl, forceNow);
break; break;
case PendingOperationType.Refresh: case PendingTaskType.Refresh:
await processRefreshGroup(ws, pending.refreshGroupId, forceNow); await processRefreshGroup(ws, pending.refreshGroupId, forceNow);
break; break;
case PendingOperationType.Reserve: case PendingTaskType.Reserve:
await processReserve(ws, pending.reservePub, forceNow); await processReserve(ws, pending.reservePub, forceNow);
break; break;
case PendingOperationType.Withdraw: case PendingTaskType.Withdraw:
await processWithdrawGroup(ws, pending.withdrawalGroupId, forceNow); await processWithdrawGroup(ws, pending.withdrawalGroupId, forceNow);
break; break;
case PendingOperationType.ProposalDownload: case PendingTaskType.ProposalDownload:
await processDownloadProposal(ws, pending.proposalId, forceNow); await processDownloadProposal(ws, pending.proposalId, forceNow);
break; break;
case PendingOperationType.TipPickup: case PendingTaskType.TipPickup:
await processTip(ws, pending.tipId, forceNow); await processTip(ws, pending.tipId, forceNow);
break; break;
case PendingOperationType.Pay: case PendingTaskType.Pay:
await processPurchasePay(ws, pending.proposalId, forceNow); await processPurchasePay(ws, pending.proposalId, forceNow);
break; break;
case PendingOperationType.RefundQuery: case PendingTaskType.RefundQuery:
await processPurchaseQueryRefund(ws, pending.proposalId, forceNow); await processPurchaseQueryRefund(ws, pending.proposalId, forceNow);
break; break;
case PendingOperationType.Recoup: case PendingTaskType.Recoup:
await processRecoupGroup(ws, pending.recoupGroupId, forceNow); await processRecoupGroup(ws, pending.recoupGroupId, forceNow);
break; break;
case PendingOperationType.ExchangeCheckRefresh: case PendingTaskType.ExchangeCheckRefresh:
await autoRefresh(ws, pending.exchangeBaseUrl); await autoRefresh(ws, pending.exchangeBaseUrl);
break; break;
case PendingOperationType.Deposit: case PendingTaskType.Deposit:
await processDepositGroup(ws, pending.depositGroupId); await processDepositGroup(ws, pending.depositGroupId);
break; break;
case PendingTaskType.Backup:
await processBackupForProvider(ws, pending.backupProviderBaseUrl);
break;
default: default:
assertUnreachable(pending); assertUnreachable(pending);
} }