implement and test auto-refresh
This commit is contained in:
parent
bf9c2ae7f9
commit
f51a59bc72
@ -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") {
|
||||
|
@ -64,3 +64,4 @@ export * from "./types/talerTypes";
|
||||
export * from "./types/walletTypes";
|
||||
export * from "./types/notifications";
|
||||
export * from "./types/transactions";
|
||||
export * from "./types/pending";
|
@ -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,
|
||||
|
@ -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({
|
||||
|
@ -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:
|
||||
|
@ -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);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
@ -538,6 +538,7 @@ export enum RefreshReason {
|
||||
AbortPay = "abort-pay",
|
||||
Recoup = "recoup",
|
||||
BackupRestored = "backup-restored",
|
||||
Scheduled = "scheduled",
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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") {
|
||||
|
@ -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);
|
||||
|
Loading…
Reference in New Issue
Block a user