From f51a59bc72c2886cb2bb88b149a3353857e3eb44 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Thu, 3 Sep 2020 20:38:26 +0530 Subject: implement and test auto-refresh --- .../taler-wallet-core/src/operations/exchanges.ts | 3 + packages/taler-wallet-core/src/operations/pay.ts | 21 ++++- .../taler-wallet-core/src/operations/pending.ts | 8 +- .../taler-wallet-core/src/operations/refresh.ts | 100 ++++++++++++++++++++- 4 files changed, 124 insertions(+), 8 deletions(-) (limited to 'packages/taler-wallet-core/src/operations') 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 {} +): Promise { + 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); + }, + ); +} -- cgit v1.2.3