From 8ad36d89f55783c34043ee9ef37759cd94bcec7c Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Thu, 10 Jun 2021 16:32:37 +0200 Subject: simplify pending transactions, make more tests pass again --- .../src/operations/backup/import.ts | 8 +- .../taler-wallet-core/src/operations/exchanges.ts | 36 ++- packages/taler-wallet-core/src/operations/pay.ts | 2 +- .../taler-wallet-core/src/operations/pending.ts | 300 ++++----------------- .../taler-wallet-core/src/operations/refresh.ts | 30 +-- 5 files changed, 95 insertions(+), 281 deletions(-) (limited to 'packages/taler-wallet-core/src/operations') 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); }); -- cgit v1.2.3