From 8ad36d89f55783c34043ee9ef37759cd94bcec7c Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Thu, 10 Jun 2021 16:32:37 +0200 Subject: [PATCH] simplify pending transactions, make more tests pass again --- packages/taler-util/src/time.ts | 8 + .../test-timetravel-autorefresh.ts | 4 + packages/taler-wallet-core/src/db.ts | 54 +--- .../src/operations/backup/import.ts | 8 +- .../src/operations/exchanges.ts | 36 ++- .../taler-wallet-core/src/operations/pay.ts | 2 +- .../src/operations/pending.ts | 300 +++--------------- .../src/operations/refresh.ts | 30 +- .../taler-wallet-core/src/pending-types.ts | 62 +--- .../src/util/contractTerms.ts | 2 - .../taler-wallet-core/src/util/retries.ts | 5 +- packages/taler-wallet-core/src/wallet.ts | 96 +++--- 12 files changed, 176 insertions(+), 431 deletions(-) diff --git a/packages/taler-util/src/time.ts b/packages/taler-util/src/time.ts index 980f42db4..c0858ada6 100644 --- a/packages/taler-util/src/time.ts +++ b/packages/taler-util/src/time.ts @@ -217,6 +217,14 @@ export function timestampDifference(t1: Timestamp, t2: Timestamp): Duration { return { d_ms: Math.abs(t1.t_ms - t2.t_ms) }; } +export function timestampToIsoString(t: Timestamp): string { + if (t.t_ms === "never") { + return ""; + } else { + return new Date(t.t_ms).toISOString(); + } +} + export function timestampIsBetween( t: Timestamp, start: Timestamp, diff --git a/packages/taler-wallet-cli/src/integrationtests/test-timetravel-autorefresh.ts b/packages/taler-wallet-cli/src/integrationtests/test-timetravel-autorefresh.ts index 3f26aaf0d..8146eafc5 100644 --- a/packages/taler-wallet-cli/src/integrationtests/test-timetravel-autorefresh.ts +++ b/packages/taler-wallet-cli/src/integrationtests/test-timetravel-autorefresh.ts @@ -167,6 +167,10 @@ export async function runTimetravelAutorefreshTest(t: GlobalTestState) { merchant, }); + // At this point, the original coins should've been refreshed. + // It would be too late to refresh them now, as we're past + // the two year deposit expiration. + await wallet.runUntilDone(); const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", { diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts index d02ea192f..ca613e5e5 100644 --- a/packages/taler-wallet-core/src/db.ts +++ b/packages/taler-wallet-core/src/db.ts @@ -515,25 +515,11 @@ export interface DenominationRecord { exchangeBaseUrl: string; } -export enum ExchangeUpdateStatus { - FetchKeys = "fetch-keys", - FetchWire = "fetch-wire", - FetchTerms = "fetch-terms", - FinalizeUpdate = "finalize-update", - Finished = "finished", -} - export interface ExchangeBankAccount { payto_uri: string; master_sig: string; } -export enum ExchangeUpdateReason { - Initial = "initial", - Forced = "forced", - Scheduled = "scheduled", -} - export interface ExchangeDetailsRecord { /** * Master public key of the exchange. @@ -582,16 +568,6 @@ export interface ExchangeDetailsRecord { */ termsOfServiceAcceptedEtag: string | undefined; - /** - * Timestamp for last update. - */ - lastUpdateTime: Timestamp; - - /** - * When should we next update the information about the exchange? - */ - nextUpdateTime: Timestamp; - wireInfo: WireInfo; } @@ -629,27 +605,16 @@ export interface ExchangeRecord { permanent: boolean; /** - * Time when the update to the exchange has been started or - * undefined if no update is in progress. + * Last time when the exchange was updated. */ - updateStarted: Timestamp | undefined; + lastUpdate: Timestamp | undefined; /** - * Status of updating the info about the exchange. + * Next scheduled update for the exchange. * - * FIXME: Adapt this to recent changes regarding how - * updating exchange details works. + * (This field must always be present, so we can index on the timestamp.) */ - updateStatus: ExchangeUpdateStatus; - - updateReason?: ExchangeUpdateReason; - - lastError?: TalerErrorDetails; - - /** - * Retry status for fetching updated information about the exchange. - */ - retryInfo: RetryInfo; + nextUpdate: Timestamp; /** * Next time that we should check if coins need to be refreshed. @@ -657,7 +622,14 @@ export interface ExchangeRecord { * Updated whenever the exchange's denominations are updated or when * the refresh check has been done. */ - nextRefreshCheck?: Timestamp; + nextRefreshCheck: Timestamp; + + lastError?: TalerErrorDetails; + + /** + * Retry status for fetching updated information about the exchange. + */ + retryInfo: RetryInfo; } /** diff --git a/packages/taler-wallet-core/src/operations/backup/import.ts b/packages/taler-wallet-core/src/operations/backup/import.ts index e024b76ab..9363ecfba 100644 --- a/packages/taler-wallet-core/src/operations/backup/import.ts +++ b/packages/taler-wallet-core/src/operations/backup/import.ts @@ -31,7 +31,6 @@ import { import { WalletContractData, DenomSelectionState, - ExchangeUpdateStatus, DenominationStatus, CoinSource, CoinSourceType, @@ -265,8 +264,9 @@ export async function importBackup( }, permanent: true, retryInfo: initRetryInfo(false), - updateStarted: { t_ms: "never" }, - updateStatus: ExchangeUpdateStatus.Finished, + lastUpdate: undefined, + nextUpdate: getTimestampNow(), + nextRefreshCheck: getTimestampNow(), }); } @@ -307,9 +307,7 @@ export async function importBackup( auditor_url: x.auditor_url, denomination_keys: x.denomination_keys, })), - lastUpdateTime: { t_ms: "never" }, masterPublicKey: backupExchangeDetails.master_public_key, - nextUpdateTime: { t_ms: "never" }, protocolVersion: backupExchangeDetails.protocol_version, reserveClosingDelay: backupExchangeDetails.reserve_closing_delay, signingKeys: backupExchangeDetails.signing_keys.map((x) => ({ diff --git a/packages/taler-wallet-core/src/operations/exchanges.ts b/packages/taler-wallet-core/src/operations/exchanges.ts index 789ce1da4..bea4b668d 100644 --- a/packages/taler-wallet-core/src/operations/exchanges.ts +++ b/packages/taler-wallet-core/src/operations/exchanges.ts @@ -42,9 +42,7 @@ import { DenominationRecord, DenominationStatus, ExchangeRecord, - ExchangeUpdateStatus, WireFee, - ExchangeUpdateReason, ExchangeDetailsRecord, WireInfo, WalletStoresV1, @@ -299,11 +297,11 @@ async function provideExchangeRecord( r = { permanent: true, baseUrl: baseUrl, - updateStatus: ExchangeUpdateStatus.FetchKeys, - updateStarted: now, - updateReason: ExchangeUpdateReason.Initial, retryInfo: initRetryInfo(false), detailsPointer: undefined, + lastUpdate: undefined, + nextUpdate: now, + nextRefreshCheck: now, }; await tx.exchanges.put(r); } @@ -411,6 +409,27 @@ async function updateExchangeFromUrlImpl( const r = await provideExchangeRecord(ws, baseUrl, now); + if (!forceNow && r && !isTimestampExpired(r.nextUpdate)) { + const res = await ws.db.mktx((x) => ({ + exchanges: x.exchanges, + exchangeDetails: x.exchangeDetails, + })).runReadOnly(async (tx) => { + const exchange = await tx.exchanges.get(baseUrl); + if (!exchange) { + return; + } + const exchangeDetails = await getExchangeDetails(tx, baseUrl); + if (!exchangeDetails) { + return; + } + return { exchange, exchangeDetails }; + }); + if (res) { + logger.info("using existing exchange info"); + return res; + } + } + logger.info("updating exchange /keys info"); const timeout = getExchangeRequestTimeout(r); @@ -460,11 +479,9 @@ async function updateExchangeFromUrlImpl( details = { auditors: keysInfo.auditors, currency: keysInfo.currency, - lastUpdateTime: now, masterPublicKey: keysInfo.masterPublicKey, protocolVersion: keysInfo.protocolVersion, signingKeys: keysInfo.signingKeys, - nextUpdateTime: keysInfo.expiry, reserveClosingDelay: keysInfo.reserveClosingDelay, exchangeBaseUrl: r.baseUrl, wireInfo, @@ -472,12 +489,13 @@ async function updateExchangeFromUrlImpl( termsOfServiceAcceptedEtag: undefined, termsOfServiceLastEtag: tosDownload.tosEtag, }; - r.updateStatus = ExchangeUpdateStatus.FetchWire; // FIXME: only update if pointer got updated r.lastError = undefined; r.retryInfo = initRetryInfo(false); + r.lastUpdate = getTimestampNow(); + r.nextUpdate = keysInfo.expiry, // New denominations might be available. - r.nextRefreshCheck = undefined; + r.nextRefreshCheck = getTimestampNow(); r.detailsPointer = { currency: details.currency, masterPublicKey: details.masterPublicKey, diff --git a/packages/taler-wallet-core/src/operations/pay.ts b/packages/taler-wallet-core/src/operations/pay.ts index 9e23f6a17..cbb92dc86 100644 --- a/packages/taler-wallet-core/src/operations/pay.ts +++ b/packages/taler-wallet-core/src/operations/pay.ts @@ -468,7 +468,7 @@ async function recordConfirmPay( const p = await tx.proposals.get(proposal.proposalId); if (p) { p.proposalStatus = ProposalStatus.ACCEPTED; - p.lastError = undefined; + delete p.lastError; p.retryInfo = initRetryInfo(false); await tx.proposals.put(p); } diff --git a/packages/taler-wallet-core/src/operations/pending.ts b/packages/taler-wallet-core/src/operations/pending.ts index 4eee85278..b40c33c5c 100644 --- a/packages/taler-wallet-core/src/operations/pending.ts +++ b/packages/taler-wallet-core/src/operations/pending.ts @@ -18,7 +18,6 @@ * Imports. */ import { - ExchangeUpdateStatus, ProposalStatus, ReserveRecordStatus, AbortStatus, @@ -27,31 +26,13 @@ import { import { PendingOperationsResponse, PendingOperationType, - ExchangeUpdateOperationStage, ReserveType, } from "../pending-types"; -import { - Duration, - getTimestampNow, - Timestamp, - getDurationRemaining, - durationMin, -} from "@gnu-taler/taler-util"; +import { getTimestampNow, Timestamp } from "@gnu-taler/taler-util"; import { InternalWalletState } from "./state"; import { getBalancesInsideTransaction } from "./balance"; -import { getExchangeDetails } from "./exchanges.js"; import { GetReadOnlyAccess } from "../util/query.js"; -function updateRetryDelay( - oldDelay: Duration, - now: Timestamp, - retryTimestamp: Timestamp, -): Duration { - const remaining = getDurationRemaining(retryTimestamp, now); - const nextDelay = durationMin(oldDelay, remaining); - return nextDelay; -} - async function gatherExchangePending( tx: GetReadOnlyAccess<{ exchanges: typeof WalletStoresV1.exchanges; @@ -59,97 +40,22 @@ async function gatherExchangePending( }>, now: Timestamp, resp: PendingOperationsResponse, - onlyDue = false, ): Promise { await tx.exchanges.iter().forEachAsync(async (e) => { - switch (e.updateStatus) { - case ExchangeUpdateStatus.Finished: - if (e.lastError) { - resp.pendingOperations.push({ - type: PendingOperationType.Bug, - givesLifeness: false, - message: - "Exchange record is in FINISHED state but has lastError set", - details: { - exchangeBaseUrl: e.baseUrl, - }, - }); - } - const details = await getExchangeDetails(tx, e.baseUrl); - const keysUpdateRequired = - details && details.nextUpdateTime.t_ms < now.t_ms; - if (keysUpdateRequired) { - resp.pendingOperations.push({ - type: PendingOperationType.ExchangeUpdate, - givesLifeness: false, - stage: ExchangeUpdateOperationStage.FetchKeys, - exchangeBaseUrl: e.baseUrl, - lastError: e.lastError, - reason: "scheduled", - }); - } - if ( - details && - (!e.nextRefreshCheck || e.nextRefreshCheck.t_ms < now.t_ms) - ) { - resp.pendingOperations.push({ - type: PendingOperationType.ExchangeCheckRefresh, - exchangeBaseUrl: e.baseUrl, - givesLifeness: false, - }); - } - break; - case ExchangeUpdateStatus.FetchKeys: - if (onlyDue && e.retryInfo.nextRetry.t_ms > now.t_ms) { - return; - } - resp.pendingOperations.push({ - type: PendingOperationType.ExchangeUpdate, - givesLifeness: false, - stage: ExchangeUpdateOperationStage.FetchKeys, - exchangeBaseUrl: e.baseUrl, - lastError: e.lastError, - reason: e.updateReason || "unknown", - }); - break; - case ExchangeUpdateStatus.FetchWire: - if (onlyDue && e.retryInfo.nextRetry.t_ms > now.t_ms) { - return; - } - resp.pendingOperations.push({ - type: PendingOperationType.ExchangeUpdate, - givesLifeness: false, - stage: ExchangeUpdateOperationStage.FetchWire, - exchangeBaseUrl: e.baseUrl, - lastError: e.lastError, - reason: e.updateReason || "unknown", - }); - break; - case ExchangeUpdateStatus.FinalizeUpdate: - 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", - }); - break; - default: - resp.pendingOperations.push({ - type: PendingOperationType.Bug, - givesLifeness: false, - message: "Unknown exchangeUpdateStatus", - details: { - exchangeBaseUrl: e.baseUrl, - exchangeUpdateStatus: e.updateStatus, - }, - }); - break; - } + resp.pendingOperations.push({ + type: PendingOperationType.ExchangeUpdate, + givesLifeness: false, + timestampDue: e.nextUpdate, + exchangeBaseUrl: e.baseUrl, + lastError: e.lastError, + }); + + resp.pendingOperations.push({ + type: PendingOperationType.ExchangeCheckRefresh, + timestampDue: e.nextRefreshCheck, + givesLifeness: false, + exchangeBaseUrl: e.baseUrl, + }); }); } @@ -157,16 +63,11 @@ async function gatherReservePending( tx: GetReadOnlyAccess<{ reserves: typeof WalletStoresV1.reserves }>, now: Timestamp, resp: PendingOperationsResponse, - onlyDue = false, ): Promise { - // FIXME: this should be optimized by using an index for "onlyDue==true". await tx.reserves.iter().forEach((reserve) => { const reserveType = reserve.bankInfo ? ReserveType.TalerBankWithdraw : ReserveType.Manual; - if (!reserve.retryInfo.active) { - return; - } switch (reserve.reserveStatus) { case ReserveRecordStatus.DORMANT: // nothing to report as pending @@ -174,17 +75,10 @@ async function gatherReservePending( case ReserveRecordStatus.WAIT_CONFIRM_BANK: 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, givesLifeness: true, + timestampDue: reserve.retryInfo.nextRetry, stage: reserve.reserveStatus, timestampCreated: reserve.timestampCreated, reserveType, @@ -193,15 +87,7 @@ async function gatherReservePending( }); break; default: - resp.pendingOperations.push({ - type: PendingOperationType.Bug, - givesLifeness: false, - message: "Unknown reserve record status", - details: { - reservePub: reserve.reservePub, - reserveStatus: reserve.reserveStatus, - }, - }); + // FIXME: report problem! break; } }); @@ -211,24 +97,15 @@ async function gatherRefreshPending( tx: GetReadOnlyAccess<{ refreshGroups: typeof WalletStoresV1.refreshGroups }>, now: Timestamp, resp: PendingOperationsResponse, - onlyDue = false, ): Promise { await tx.refreshGroups.iter().forEach((r) => { if (r.timestampFinished) { 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, givesLifeness: true, + timestampDue: r.retryInfo.nextRetry, refreshGroupId: r.refreshGroupId, finishedPerCoin: r.finishedPerCoin, retryInfo: r.retryInfo, @@ -243,20 +120,11 @@ async function gatherWithdrawalPending( }>, now: Timestamp, resp: PendingOperationsResponse, - onlyDue = false, ): Promise { await tx.withdrawalGroups.iter().forEachAsync(async (wsr) => { if (wsr.timestampFinish) { 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.planchets.indexes.byGroup @@ -270,8 +138,7 @@ async function gatherWithdrawalPending( resp.pendingOperations.push({ type: PendingOperationType.Withdraw, givesLifeness: true, - numCoinsTotal, - numCoinsWithdrawn, + timestampDue: wsr.retryInfo.nextRetry, withdrawalGroupId: wsr.withdrawalGroupId, lastError: wsr.lastError, retryInfo: wsr.retryInfo, @@ -283,42 +150,15 @@ async function gatherProposalPending( tx: GetReadOnlyAccess<{ proposals: typeof WalletStoresV1.proposals }>, now: Timestamp, resp: PendingOperationsResponse, - onlyDue = false, ): Promise { await tx.proposals.iter().forEach((proposal) => { if (proposal.proposalStatus == ProposalStatus.PROPOSED) { - if (onlyDue) { - return; - } - 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, - }); - } + // Nothing to do, user needs to choose. } 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, givesLifeness: true, + timestampDue: proposal.retryInfo.nextRetry, merchantBaseUrl: proposal.merchantBaseUrl, orderId: proposal.orderId, proposalId: proposal.proposalId, @@ -334,24 +174,16 @@ async function gatherTipPending( tx: GetReadOnlyAccess<{ tips: typeof WalletStoresV1.tips }>, now: Timestamp, resp: PendingOperationsResponse, - onlyDue = false, ): Promise { await tx.tips.iter().forEach((tip) => { if (tip.pickedUpTimestamp) { return; } - resp.nextRetryDelay = updateRetryDelay( - resp.nextRetryDelay, - now, - tip.retryInfo.nextRetry, - ); - if (onlyDue && tip.retryInfo.nextRetry.t_ms > now.t_ms) { - return; - } if (tip.acceptedTimestamp) { resp.pendingOperations.push({ type: PendingOperationType.TipPickup, givesLifeness: true, + timestampDue: tip.retryInfo.nextRetry, merchantBaseUrl: tip.merchantBaseUrl, tipId: tip.walletTipId, merchantTipId: tip.merchantTipId, @@ -364,41 +196,28 @@ async function gatherPurchasePending( tx: GetReadOnlyAccess<{ purchases: typeof WalletStoresV1.purchases }>, now: Timestamp, resp: PendingOperationsResponse, - onlyDue = false, ): Promise { await tx.purchases.iter().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, - }); - } + resp.pendingOperations.push({ + type: PendingOperationType.Pay, + givesLifeness: true, + timestampDue: pr.payRetryInfo.nextRetry, + isReplay: false, + proposalId: pr.proposalId, + retryInfo: pr.payRetryInfo, + lastError: pr.lastPayError, + }); } 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, - }); - } + resp.pendingOperations.push({ + type: PendingOperationType.RefundQuery, + givesLifeness: true, + timestampDue: pr.refundStatusRetryInfo.nextRetry, + proposalId: pr.proposalId, + retryInfo: pr.refundStatusRetryInfo, + lastError: pr.lastRefundStatusError, + }); } }); } @@ -407,23 +226,15 @@ async function gatherRecoupPending( tx: GetReadOnlyAccess<{ recoupGroups: typeof WalletStoresV1.recoupGroups }>, now: Timestamp, resp: PendingOperationsResponse, - onlyDue = false, ): Promise { await tx.recoupGroups.iter().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, + timestampDue: rg.retryInfo.nextRetry, recoupGroupId: rg.recoupGroupId, retryInfo: rg.retryInfo, lastError: rg.lastError, @@ -435,23 +246,15 @@ async function gatherDepositPending( tx: GetReadOnlyAccess<{ depositGroups: typeof WalletStoresV1.depositGroups }>, now: Timestamp, resp: PendingOperationsResponse, - onlyDue = false, ): Promise { await tx.depositGroups.iter().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, + timestampDue: dg.retryInfo.nextRetry, depositGroupId: dg.depositGroupId, retryInfo: dg.retryInfo, lastError: dg.lastError, @@ -461,7 +264,6 @@ async function gatherDepositPending( export async function getPendingOperations( ws: InternalWalletState, - { onlyDue = false } = {}, ): Promise { const now = getTimestampNow(); return await ws.db @@ -482,20 +284,18 @@ export async function getPendingOperations( .runReadWrite(async (tx) => { const walletBalance = await getBalancesInsideTransaction(ws, tx); const resp: PendingOperationsResponse = { - nextRetryDelay: { d_ms: Number.MAX_SAFE_INTEGER }, - onlyDue: onlyDue, walletBalance, pendingOperations: [], }; - 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); - await gatherDepositPending(tx, now, resp, onlyDue); + await gatherExchangePending(tx, now, resp); + await gatherReservePending(tx, now, resp); + await gatherRefreshPending(tx, now, resp); + await gatherWithdrawalPending(tx, now, resp); + await gatherProposalPending(tx, now, resp); + await gatherTipPending(tx, now, resp); + await gatherPurchasePending(tx, now, resp); + await gatherRecoupPending(tx, now, resp); + await gatherDepositPending(tx, now, resp); return resp; }); } diff --git a/packages/taler-wallet-core/src/operations/refresh.ts b/packages/taler-wallet-core/src/operations/refresh.ts index 8d21e811d..21c92c1b7 100644 --- a/packages/taler-wallet-core/src/operations/refresh.ts +++ b/packages/taler-wallet-core/src/operations/refresh.ts @@ -32,6 +32,7 @@ import { RefreshGroupId, RefreshReason, TalerErrorDetails, + timestampToIsoString, } from "@gnu-taler/taler-util"; import { AmountJson, Amounts } from "@gnu-taler/taler-util"; import { amountToPretty } from "@gnu-taler/taler-util"; @@ -864,7 +865,12 @@ export async function autoRefresh( ws: InternalWalletState, exchangeBaseUrl: string, ): Promise { + logger.info(`doing auto-refresh check for '${exchangeBaseUrl}'`); await updateExchangeFromUrl(ws, exchangeBaseUrl, true); + let minCheckThreshold = timestampAddDuration( + getTimestampNow(), + durationFromSpec({ days: 1 }), + ); await ws.db .mktx((x) => ({ coins: x.coins, @@ -899,28 +905,20 @@ export async function autoRefresh( const executeThreshold = getAutoRefreshExecuteThreshold(denom); if (isTimestampExpired(executeThreshold)) { refreshCoins.push(coin); + } else { + const checkThreshold = getAutoRefreshCheckThreshold(denom); + minCheckThreshold = timestampMin(minCheckThreshold, checkThreshold); } } if (refreshCoins.length > 0) { await createRefreshGroup(ws, tx, refreshCoins, RefreshReason.Scheduled); } - - const denoms = await tx.denominations.indexes.byExchangeBaseUrl - .iter(exchangeBaseUrl) - .toArray(); - let minCheckThreshold = timestampAddDuration( - getTimestampNow(), - durationFromSpec({ days: 1 }), + logger.info( + `current wallet time: ${timestampToIsoString(getTimestampNow())}`, + ); + logger.info( + `next refresh check at ${timestampToIsoString(minCheckThreshold)}`, ); - for (const denom of denoms) { - const checkThreshold = getAutoRefreshCheckThreshold(denom); - const executeThreshold = getAutoRefreshExecuteThreshold(denom); - if (isTimestampExpired(executeThreshold)) { - // No need to consider this denomination, we already did an auto refresh check. - continue; - } - minCheckThreshold = timestampMin(minCheckThreshold, checkThreshold); - } exchange.nextRefreshCheck = minCheckThreshold; await tx.exchanges.put(exchange); }); diff --git a/packages/taler-wallet-core/src/pending-types.ts b/packages/taler-wallet-core/src/pending-types.ts index 78e01416c..5586903f5 100644 --- a/packages/taler-wallet-core/src/pending-types.ts +++ b/packages/taler-wallet-core/src/pending-types.ts @@ -34,7 +34,6 @@ import { ReserveRecordStatus } from "./db.js"; import { RetryInfo } from "./util/retries.js"; export enum PendingOperationType { - Bug = "bug", ExchangeUpdate = "exchange-update", ExchangeCheckRefresh = "exchange-check-refresh", Pay = "pay", @@ -44,7 +43,6 @@ export enum PendingOperationType { Reserve = "reserve", Recoup = "recoup", RefundQuery = "refund-query", - TipChoice = "tip-choice", TipPickup = "tip-pickup", Withdraw = "withdraw", Deposit = "deposit", @@ -55,16 +53,13 @@ export enum PendingOperationType { */ export type PendingOperationInfo = PendingOperationInfoCommon & ( - | PendingBugOperation | PendingExchangeUpdateOperation | PendingExchangeCheckRefreshOperation | PendingPayOperation - | PendingProposalChoiceOperation | PendingProposalDownloadOperation | PendingRefreshOperation | PendingRefundQueryOperation | PendingReserveOperation - | PendingTipChoiceOperation | PendingTipPickupOperation | PendingWithdrawOperation | PendingRecoupOperation @@ -76,8 +71,6 @@ export type PendingOperationInfo = PendingOperationInfoCommon & */ export interface PendingExchangeUpdateOperation { type: PendingOperationType.ExchangeUpdate; - stage: ExchangeUpdateOperationStage; - reason: string; exchangeBaseUrl: string; lastError: TalerErrorDetails | undefined; } @@ -91,26 +84,6 @@ export interface PendingExchangeCheckRefreshOperation { exchangeBaseUrl: string; } -/** - * Some internal error happened in the wallet. This pending operation - * should *only* be reported for problems in the wallet, not when - * a problem with a merchant/exchange/etc. occurs. - */ -export interface PendingBugOperation { - type: PendingOperationType.Bug; - message: string; - details: any; -} - -/** - * Current state of an exchange update operation. - */ -export enum ExchangeUpdateOperationStage { - FetchKeys = "fetch-keys", - FetchWire = "fetch-wire", - FinalizeUpdate = "finalize-update", -} - export enum ReserveType { /** * Manually created. @@ -183,17 +156,6 @@ export interface PendingTipPickupOperation { merchantTipId: string; } -/** - * The wallet has been offered a tip, and the user now needs to - * decide whether to accept or reject the tip. - */ -export interface PendingTipChoiceOperation { - type: PendingOperationType.TipChoice; - tipId: string; - merchantBaseUrl: string; - merchantTipId: string; -} - /** * The wallet is signing coins and then sending them to * the merchant. @@ -232,8 +194,6 @@ export interface PendingWithdrawOperation { lastError: TalerErrorDetails | undefined; retryInfo: RetryInfo; withdrawalGroupId: string; - numCoinsWithdrawn: number; - numCoinsTotal: number; } /** @@ -257,13 +217,18 @@ export interface PendingOperationInfoCommon { /** * Set to true if the operation indicates that something is really in progress, - * as opposed to some regular scheduled operation or a permanent failure. + * as opposed to some regular scheduled operation that can be tried later. */ givesLifeness: boolean; /** - * Retry info, not available on all pending operations. - * If it is available, it must have the same name. + * Timestamp when the pending operation should be executed next. + */ + timestampDue: Timestamp; + + /** + * Retry info. Currently used to stop the wallet after any operation + * exceeds a number of retries. */ retryInfo?: RetryInfo; } @@ -281,15 +246,4 @@ export interface PendingOperationsResponse { * Current wallet balance, including pending balances. */ walletBalance: BalancesResponse; - - /** - * When is the next pending operation due to be re-tried? - */ - nextRetryDelay: Duration; - - /** - * Does this response only include pending operations that - * are due to be executed right now? - */ - onlyDue: boolean; } diff --git a/packages/taler-wallet-core/src/util/contractTerms.ts b/packages/taler-wallet-core/src/util/contractTerms.ts index cf61cc05f..5fb23cf8c 100644 --- a/packages/taler-wallet-core/src/util/contractTerms.ts +++ b/packages/taler-wallet-core/src/util/contractTerms.ts @@ -121,7 +121,6 @@ export namespace ContractTermsUtil { * to forgettable fields and other restrictions for forgettable JSON. */ export function validateForgettable(anyJson: any): boolean { - console.warn("calling validateForgettable", anyJson); if (typeof anyJson === "string") { return true; } @@ -206,7 +205,6 @@ export namespace ContractTermsUtil { } } } else { - console.warn("invalid type"); return false; } } diff --git a/packages/taler-wallet-core/src/util/retries.ts b/packages/taler-wallet-core/src/util/retries.ts index 54bb0b2ee..a7f4cd281 100644 --- a/packages/taler-wallet-core/src/util/retries.ts +++ b/packages/taler-wallet-core/src/util/retries.ts @@ -81,10 +81,11 @@ export function initRetryInfo( retryCounter: 0, }; } + const now = getTimestampNow(); const info = { - firstTry: getTimestampNow(), + firstTry: now, active: true, - nextRetry: { t_ms: 0 }, + nextRetry: now, retryCounter: 0, }; updateRetryInfoTimeout(info, p); diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts index 70ddaffa8..854039a8f 100644 --- a/packages/taler-wallet-core/src/wallet.ts +++ b/packages/taler-wallet-core/src/wallet.ts @@ -27,7 +27,15 @@ import { codecForAny, codecForDeleteTransactionRequest, DeleteTransactionRequest, + durationFromSpec, + durationMax, + durationMin, + getDurationRemaining, + isTimestampExpired, + j2s, TalerErrorCode, + Timestamp, + timestampMin, WalletCurrencyInfo, } from "@gnu-taler/taler-util"; import { CryptoWorkerFactory } from "./crypto/workers/cryptoApi"; @@ -105,11 +113,8 @@ import { AuditorTrustRecord, CoinRecord, CoinSourceType, - DenominationRecord, ExchangeDetailsRecord, ExchangeRecord, - PurchaseRecord, - RefundState, ReserveRecord, ReserveRecordStatus, WalletStoresV1, @@ -164,7 +169,6 @@ import { ManualWithdrawalDetails, PreparePayResult, PrepareTipResult, - PurchaseDetails, RecoveryLoadRequest, RefreshReason, ReturnCoinsRequest, @@ -180,7 +184,6 @@ import { AsyncOpMemoSingle } from "./util/asyncMemo"; import { HttpRequestLibrary } from "./util/http"; import { Logger } from "@gnu-taler/taler-util"; import { AsyncCondition } from "./util/promiseUtils"; -import { Duration, durationMin } from "@gnu-taler/taler-util"; import { TimerGroup } from "./util/timer"; import { getExchangeTrust } from "./operations/currencies.js"; import { DbAccess } from "./util/query.js"; @@ -261,9 +264,6 @@ export class Wallet { ): Promise { logger.trace(`running pending ${JSON.stringify(pending, undefined, 2)}`); switch (pending.type) { - case PendingOperationType.Bug: - // Nothing to do, will just be displayed to the user - return; case PendingOperationType.ExchangeUpdate: await updateExchangeFromUrl(this.ws, pending.exchangeBaseUrl, forceNow); break; @@ -280,15 +280,9 @@ export class Wallet { forceNow, ); break; - case PendingOperationType.ProposalChoice: - // Nothing to do, user needs to accept/reject - break; case PendingOperationType.ProposalDownload: await processDownloadProposal(this.ws, pending.proposalId, forceNow); break; - case PendingOperationType.TipChoice: - // Nothing to do, user needs to accept/reject - break; case PendingOperationType.TipPickup: await processTip(this.ws, pending.tipId, forceNow); break; @@ -316,9 +310,11 @@ export class Wallet { * Process pending operations. */ public async runPending(forceNow = false): Promise { - const onlyDue = !forceNow; - const pendingOpsResponse = await this.getPendingOperations({ onlyDue }); + const pendingOpsResponse = await this.getPendingOperations(); for (const p of pendingOpsResponse.pendingOperations) { + if (!forceNow && !isTimestampExpired(p.timestampDue)) { + continue; + } try { await this.processOnePendingOperation(p, forceNow); } catch (e) { @@ -364,7 +360,7 @@ export class Wallet { if (!maxRetries) { return; } - this.getPendingOperations({ onlyDue: false }) + this.getPendingOperations() .then((pending) => { for (const p of pending.pendingOperations) { if (p.retryInfo && p.retryInfo.retryCounter > maxRetries) { @@ -408,51 +404,53 @@ export class Wallet { } private async runRetryLoopImpl(): Promise { - let iteration = 0; - for (; !this.stopped; iteration++) { - const pending = await this.getPendingOperations({ onlyDue: true }); - let numDueAndLive = 0; + for (let iteration = 0; !this.stopped; iteration++) { + const pending = await this.getPendingOperations(); + logger.trace(`pending operations: ${j2s(pending)}`); + let numGivingLiveness = 0; + let numDue = 0; + let minDue: Timestamp = { t_ms: "never" }; for (const p of pending.pendingOperations) { + minDue = timestampMin(minDue, p.timestampDue); + if (isTimestampExpired(p.timestampDue)) { + numDue++; + } if (p.givesLifeness) { - numDueAndLive++; + numGivingLiveness++; } } // Make sure that we run tasks that don't give lifeness at least // one time. - if (iteration !== 0 && numDueAndLive === 0) { - const allPending = await this.getPendingOperations({ onlyDue: false }); - let numPending = 0; - let numGivingLiveness = 0; - for (const p of allPending.pendingOperations) { - numPending++; - if (p.givesLifeness) { - numGivingLiveness++; - } - } - let dt: Duration; - if ( - allPending.pendingOperations.length === 0 || - allPending.nextRetryDelay.d_ms === Number.MAX_SAFE_INTEGER - ) { - // Wait for 5 seconds - dt = { d_ms: 5000 }; - } else { - dt = durationMin({ d_ms: 5000 }, allPending.nextRetryDelay); - } + if (iteration !== 0 && numDue === 0) { + // We've executed pending, due operations at least one. + // Now we don't have any more operations available, + // and need to wait. + + // Wait for at most 5 seconds to the next check. + const dt = durationMin( + durationFromSpec({ + seconds: 5, + }), + getDurationRemaining(minDue), + ); + logger.trace(`waiting for at most ${dt.d_ms} ms`) const timeout = this.timerGroup.resolveAfter(dt); this.ws.notify({ type: NotificationType.WaitingForRetry, numGivingLiveness, - numPending, + numPending: pending.pendingOperations.length, }); + // Wait until either the timeout, or we are notified (via the latch) + // that more work might be available. await Promise.race([timeout, this.latch.wait()]); } else { - // FIXME: maybe be a bit smarter about executing these - // operations in parallel? logger.trace( `running ${pending.pendingOperations.length} pending operations`, ); for (const p of pending.pendingOperations) { + if (!isTimestampExpired(p.timestampDue)) { + continue; + } try { await this.processOnePendingOperation(p); } catch (e) { @@ -650,12 +648,8 @@ export class Wallet { } } - async getPendingOperations({ - onlyDue = false, - } = {}): Promise { - return this.ws.memoGetPending.memo(() => - getPendingOperations(this.ws, { onlyDue }), - ); + async getPendingOperations(): Promise { + return this.ws.memoGetPending.memo(() => getPendingOperations(this.ws)); } async acceptExchangeTermsOfService(