summaryrefslogtreecommitdiff
path: root/packages
diff options
context:
space:
mode:
Diffstat (limited to 'packages')
-rw-r--r--packages/taler-util/src/time.ts8
-rw-r--r--packages/taler-wallet-cli/src/integrationtests/test-timetravel-autorefresh.ts4
-rw-r--r--packages/taler-wallet-core/src/db.ts52
-rw-r--r--packages/taler-wallet-core/src/operations/backup/import.ts8
-rw-r--r--packages/taler-wallet-core/src/operations/exchanges.ts36
-rw-r--r--packages/taler-wallet-core/src/operations/pay.ts2
-rw-r--r--packages/taler-wallet-core/src/operations/pending.ts300
-rw-r--r--packages/taler-wallet-core/src/operations/refresh.ts30
-rw-r--r--packages/taler-wallet-core/src/pending-types.ts62
-rw-r--r--packages/taler-wallet-core/src/util/contractTerms.ts2
-rw-r--r--packages/taler-wallet-core/src/util/retries.ts5
-rw-r--r--packages/taler-wallet-core/src/wallet.ts96
12 files changed, 175 insertions, 430 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 "<never>";
+ } 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,20 +605,24 @@ 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;
+ nextUpdate: Timestamp;
- updateReason?: ExchangeUpdateReason;
+ /**
+ * Next time that we should check if coins need to be refreshed.
+ *
+ * Updated whenever the exchange's denominations are updated or when
+ * the refresh check has been done.
+ */
+ nextRefreshCheck: Timestamp;
lastError?: TalerErrorDetails;
@@ -650,14 +630,6 @@ export interface ExchangeRecord {
* Retry status for fetching updated information about the exchange.
*/
retryInfo: RetryInfo;
-
- /**
- * Next time that we should check if coins need to be refreshed.
- *
- * Updated whenever the exchange's denominations are updated or when
- * the refresh check has been done.
- */
- nextRefreshCheck?: Timestamp;
}
/**
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<void> {
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<void> {
- // 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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<PendingOperationsResponse> {
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<void> {
+ 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.
@@ -184,17 +157,6 @@ export interface PendingTipPickupOperation {
}
/**
- * 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<void> {
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<void> {
- 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<void> {
- 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<PendingOperationsResponse> {
- return this.ws.memoGetPending.memo(() =>
- getPendingOperations(this.ws, { onlyDue }),
- );
+ async getPendingOperations(): Promise<PendingOperationsResponse> {
+ return this.ws.memoGetPending.memo(() => getPendingOperations(this.ws));
}
async acceptExchangeTermsOfService(