diff options
-rw-r--r-- | packages/taler-integrationtests/src/harness.ts | 11 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/index.ts | 1 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/operations/exchanges.ts | 3 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/operations/pay.ts | 21 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/operations/pending.ts | 8 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/operations/refresh.ts | 100 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/types/walletTypes.ts | 1 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/util/time.ts | 7 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/wallet.ts | 9 |
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); |