aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/taler-integrationtests/src/harness.ts11
-rw-r--r--packages/taler-wallet-core/src/index.ts1
-rw-r--r--packages/taler-wallet-core/src/operations/exchanges.ts3
-rw-r--r--packages/taler-wallet-core/src/operations/pay.ts21
-rw-r--r--packages/taler-wallet-core/src/operations/pending.ts8
-rw-r--r--packages/taler-wallet-core/src/operations/refresh.ts100
-rw-r--r--packages/taler-wallet-core/src/types/walletTypes.ts1
-rw-r--r--packages/taler-wallet-core/src/util/time.ts7
-rw-r--r--packages/taler-wallet-core/src/wallet.ts9
9 files changed, 152 insertions, 9 deletions
diff --git a/packages/taler-integrationtests/src/harness.ts b/packages/taler-integrationtests/src/harness.ts
index b46525267..cc30df618 100644
--- a/packages/taler-integrationtests/src/harness.ts
+++ b/packages/taler-integrationtests/src/harness.ts
@@ -68,6 +68,7 @@ import {
AmountString,
ApplyRefundRequest,
codecForApplyRefundResponse,
+ codecForAny,
} from "taler-wallet-core";
import { URL } from "url";
import axios, { AxiosError } from "axios";
@@ -79,6 +80,7 @@ import {
MerchantOrderPrivateStatusResponse,
} from "./merchantApiTypes";
import { ApplyRefundResponse } from "taler-wallet-core";
+import { PendingOperationsResponse } from "taler-wallet-core";
const exec = util.promisify(require("child_process").exec);
@@ -1562,6 +1564,15 @@ export class WalletCli {
throw new OperationFailedError(resp.error);
}
+ async getPendingOperations(): Promise<PendingOperationsResponse> {
+ const resp = await this.apiRequest("getPendingOperations", {});
+ if (resp.type === "response") {
+ // FIXME: validate properly!
+ return codecForAny().decode(resp.result);
+ }
+ throw new OperationFailedError(resp.error);
+ }
+
async getTransactions(): Promise<TransactionsResponse> {
const resp = await this.apiRequest("getTransactions", {});
if (resp.type === "response") {
diff --git a/packages/taler-wallet-core/src/index.ts b/packages/taler-wallet-core/src/index.ts
index 8d5d46b4f..b78d7b823 100644
--- a/packages/taler-wallet-core/src/index.ts
+++ b/packages/taler-wallet-core/src/index.ts
@@ -64,3 +64,4 @@ export * from "./types/talerTypes";
export * from "./types/walletTypes";
export * from "./types/notifications";
export * from "./types/transactions";
+export * from "./types/pending"; \ No newline at end of file
diff --git a/packages/taler-wallet-core/src/operations/exchanges.ts b/packages/taler-wallet-core/src/operations/exchanges.ts
index d162ca3b8..d3c72d164 100644
--- a/packages/taler-wallet-core/src/operations/exchanges.ts
+++ b/packages/taler-wallet-core/src/operations/exchanges.ts
@@ -303,6 +303,9 @@ async function updateExchangeFinalize(
}
r.addComplete = true;
r.updateStatus = ExchangeUpdateStatus.Finished;
+ // Reset time to next auto refresh check,
+ // as now new denominations might be available.
+ r.nextRefreshCheck = undefined;
await tx.put(Stores.exchanges, r);
const updateEvent: ExchangeUpdatedEventRecord = {
exchangeBaseUrl: exchange.baseUrl,
diff --git a/packages/taler-wallet-core/src/operations/pay.ts b/packages/taler-wallet-core/src/operations/pay.ts
index 2c491ec6c..c6f39858d 100644
--- a/packages/taler-wallet-core/src/operations/pay.ts
+++ b/packages/taler-wallet-core/src/operations/pay.ts
@@ -36,6 +36,8 @@ import {
PayEventRecord,
WalletContractData,
getRetryDuration,
+ CoinRecord,
+ DenominationRecord,
} from "../types/dbTypes";
import { NotificationType } from "../types/notifications";
import {
@@ -65,6 +67,7 @@ import {
Duration,
durationMax,
durationMin,
+ isTimestampExpired,
} from "../util/time";
import { strcmp, canonicalJson } from "../util/helpers";
import {
@@ -285,6 +288,19 @@ export function selectPayCoins(
return undefined;
}
+export function isSpendableCoin(coin: CoinRecord, denom: DenominationRecord): boolean {
+ if (coin.suspended) {
+ return false;
+ }
+ if (coin.status !== CoinStatus.Fresh) {
+ return false;
+ }
+ if (isTimestampExpired(denom.stampExpireDeposit)) {
+ return false;
+ }
+ return true;
+}
+
/**
* Select coins from the wallet's database that can be used
* to pay for the given contract.
@@ -370,10 +386,7 @@ async function getCoinsForPayment(
);
continue;
}
- if (coin.suspended) {
- continue;
- }
- if (coin.status !== CoinStatus.Fresh) {
+ if (!isSpendableCoin(coin, denom)) {
continue;
}
acis.push({
diff --git a/packages/taler-wallet-core/src/operations/pending.ts b/packages/taler-wallet-core/src/operations/pending.ts
index e24e8fc4e..e51f37702 100644
--- a/packages/taler-wallet-core/src/operations/pending.ts
+++ b/packages/taler-wallet-core/src/operations/pending.ts
@@ -102,7 +102,13 @@ async function gatherExchangePending(
lastError: e.lastError,
reason: "scheduled",
});
- break;
+ }
+ if (e.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:
diff --git a/packages/taler-wallet-core/src/operations/refresh.ts b/packages/taler-wallet-core/src/operations/refresh.ts
index 6c1e643a6..76f3015f3 100644
--- a/packages/taler-wallet-core/src/operations/refresh.ts
+++ b/packages/taler-wallet-core/src/operations/refresh.ts
@@ -42,8 +42,23 @@ import {
import { guardOperationException } from "./errors";
import { NotificationType } from "../types/notifications";
import { getRandomBytes, encodeCrock } from "../crypto/talerCrypto";
-import { getTimestampNow, Duration } from "../util/time";
-import { readSuccessResponseJsonOrThrow, HttpResponse } from "../util/http";
+import {
+ getTimestampNow,
+ Duration,
+ Timestamp,
+ isTimestampExpired,
+ durationFromSpec,
+ timestampMin,
+ timestampAddDuration,
+ timestampDifference,
+ durationMax,
+ durationMul,
+} from "../util/time";
+import {
+ readSuccessResponseJsonOrThrow,
+ HttpResponse,
+ throwUnexpectedRequestError,
+} from "../util/http";
import {
codecForExchangeMeltResponse,
codecForExchangeRevealResponse,
@@ -635,7 +650,86 @@ export async function createRefreshGroup(
};
}
+/**
+ * Timestamp after which the wallet would do the next check for an auto-refresh.
+ */
+function getAutoRefreshCheckThreshold(d: DenominationRecord): Timestamp {
+ const delta = timestampDifference(d.stampExpireWithdraw, d.stampExpireDeposit);
+ const deltaDiv = durationMul(delta, 0.75);
+ return timestampAddDuration(d.stampExpireWithdraw, deltaDiv);
+}
+
+/**
+ * Timestamp after which the wallet would do an auto-refresh.
+ */
+function getAutoRefreshExecuteThreshold(d: DenominationRecord): Timestamp {
+ const delta = timestampDifference(d.stampExpireWithdraw, d.stampExpireDeposit);
+ const deltaDiv = durationMul(delta, 0.5);
+ return timestampAddDuration(d.stampExpireWithdraw, deltaDiv);
+}
+
export async function autoRefresh(
ws: InternalWalletState,
exchangeBaseUrl: string,
-): Promise<void> {}
+): Promise<void> {
+ await ws.db.runWithWriteTransaction(
+ [
+ Stores.coins,
+ Stores.denominations,
+ Stores.refreshGroups,
+ Stores.exchanges,
+ ],
+ async (tx) => {
+ const exchange = await tx.get(Stores.exchanges, exchangeBaseUrl);
+ if (!exchange) {
+ return;
+ }
+ const coins = await tx
+ .iterIndexed(Stores.coins.exchangeBaseUrlIndex, exchangeBaseUrl)
+ .toArray();
+ const refreshCoins: CoinPublicKey[] = [];
+ for (const coin of coins) {
+ if (coin.status !== CoinStatus.Fresh) {
+ continue;
+ }
+ if (coin.suspended) {
+ continue;
+ }
+ const denom = await tx.get(Stores.denominations, [
+ exchangeBaseUrl,
+ coin.denomPub,
+ ]);
+ if (!denom) {
+ logger.warn("denomination not in database");
+ continue;
+ }
+ const executeThreshold = getAutoRefreshExecuteThreshold(denom);
+ if (isTimestampExpired(executeThreshold)) {
+ refreshCoins.push(coin);
+ }
+ }
+ if (refreshCoins.length > 0) {
+ await createRefreshGroup(ws, tx, refreshCoins, RefreshReason.Scheduled);
+ }
+
+ const denoms = await tx
+ .iterIndexed(Stores.denominations.exchangeBaseUrlIndex, exchangeBaseUrl)
+ .toArray();
+ let minCheckThreshold = timestampAddDuration(
+ getTimestampNow(),
+ durationFromSpec({ days: 1 }),
+ );
+ 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.put(Stores.exchanges, exchange);
+ },
+ );
+}
diff --git a/packages/taler-wallet-core/src/types/walletTypes.ts b/packages/taler-wallet-core/src/types/walletTypes.ts
index bde4fee66..5686ee61c 100644
--- a/packages/taler-wallet-core/src/types/walletTypes.ts
+++ b/packages/taler-wallet-core/src/types/walletTypes.ts
@@ -538,6 +538,7 @@ export enum RefreshReason {
AbortPay = "abort-pay",
Recoup = "recoup",
BackupRestored = "backup-restored",
+ Scheduled = "scheduled",
}
/**
diff --git a/packages/taler-wallet-core/src/util/time.ts b/packages/taler-wallet-core/src/util/time.ts
index 512d5e908..1f085107f 100644
--- a/packages/taler-wallet-core/src/util/time.ts
+++ b/packages/taler-wallet-core/src/util/time.ts
@@ -144,6 +144,13 @@ export function durationMax(d1: Duration, d2: Duration): Duration {
return { d_ms: Math.max(d1.d_ms, d2.d_ms) };
}
+export function durationMul(d: Duration, n: number): Duration {
+ if (d.d_ms === "forever") {
+ return { d_ms: "forever" };
+ }
+ return { d_ms: Math.round( d.d_ms * n) };
+}
+
export function timestampCmp(t1: Timestamp, t2: Timestamp): number {
if (t1.t_ms === "never") {
if (t2.t_ms === "never") {
diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts
index 5ca3581ad..21de541e5 100644
--- a/packages/taler-wallet-core/src/wallet.ts
+++ b/packages/taler-wallet-core/src/wallet.ts
@@ -373,7 +373,13 @@ export class Wallet {
private async runRetryLoopImpl(): Promise<void> {
while (!this.stopped) {
const pending = await this.getPendingOperations({ onlyDue: true });
- if (pending.pendingOperations.length === 0) {
+ let numDueAndLive = 0;
+ for (const p of pending.pendingOperations) {
+ if (p.givesLifeness) {
+ numDueAndLive++;
+ }
+ }
+ if (numDueAndLive === 0) {
const allPending = await this.getPendingOperations({ onlyDue: false });
let numPending = 0;
let numGivingLiveness = 0;
@@ -404,6 +410,7 @@ export class Wallet {
} 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) {
try {
await this.processOnePendingOperation(p);