cache denomination lookups

This commit is contained in:
Florian Dold 2022-01-13 12:08:31 +01:00
parent cea0ac02b6
commit cd2473e1ad
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
4 changed files with 210 additions and 117 deletions

View File

@ -20,7 +20,7 @@
* management, etc.). * management, etc.).
* *
* Some operations can be accessed via this state object. This allows mutual * Some operations can be accessed via this state object. This allows mutual
* recursion between operations, without having cycling dependencies between * recursion between operations, without having cyclic dependencies between
* the respective TypeScript files. * the respective TypeScript files.
* *
* (You can think of this as a "header file" for the wallet implementation.) * (You can think of this as a "header file" for the wallet implementation.)
@ -29,7 +29,13 @@
/** /**
* Imports. * Imports.
*/ */
import { WalletNotification, BalancesResponse } from "@gnu-taler/taler-util"; import {
WalletNotification,
BalancesResponse,
AmountJson,
DenominationPubKey,
Timestamp,
} from "@gnu-taler/taler-util";
import { CryptoApi } from "./crypto/workers/cryptoApi.js"; import { CryptoApi } from "./crypto/workers/cryptoApi.js";
import { ExchangeDetailsRecord, ExchangeRecord, WalletStoresV1 } from "./db.js"; import { ExchangeDetailsRecord, ExchangeRecord, WalletStoresV1 } from "./db.js";
import { PendingOperationsResponse } from "./pending-types.js"; import { PendingOperationsResponse } from "./pending-types.js";
@ -119,6 +125,64 @@ export interface RecoupOperations {
): Promise<void>; ): Promise<void>;
} }
export interface DenomInfo {
/**
* Value of one coin of the denomination.
*/
value: AmountJson;
/**
* The denomination public key.
*/
denomPub: DenominationPubKey;
/**
* Hash of the denomination public key.
* Stored in the database for faster lookups.
*/
denomPubHash: string;
/**
* Fee for withdrawing.
*/
feeWithdraw: AmountJson;
/**
* Fee for depositing.
*/
feeDeposit: AmountJson;
/**
* Fee for refreshing.
*/
feeRefresh: AmountJson;
/**
* Fee for refunding.
*/
feeRefund: AmountJson;
/**
* Validity start date of the denomination.
*/
stampStart: Timestamp;
/**
* Date after which the currency can't be withdrawn anymore.
*/
stampExpireWithdraw: Timestamp;
/**
* Date after the denomination officially doesn't exist anymore.
*/
stampExpireLegal: Timestamp;
/**
* Data after which coins of this denomination can't be deposited anymore.
*/
stampExpireDeposit: Timestamp;
}
export type NotificationListener = (n: WalletNotification) => void; export type NotificationListener = (n: WalletNotification) => void;
/** /**
@ -162,6 +226,15 @@ export interface InternalWalletState {
merchantOps: MerchantOperations; merchantOps: MerchantOperations;
reserveOps: ReserveOperations; reserveOps: ReserveOperations;
getDenomInfo(
ws: InternalWalletState,
tx: GetReadWriteAccess<{
denominations: typeof WalletStoresV1.denominations;
}>,
exchangeBaseUrl: string,
denomPubHash: string,
): Promise<DenomInfo | undefined>;
db: DbAccess<typeof WalletStoresV1>; db: DbAccess<typeof WalletStoresV1>;
http: HttpRequestLibrary; http: HttpRequestLibrary;

View File

@ -35,10 +35,7 @@ import {
PendingTaskType, PendingTaskType,
ReserveType, ReserveType,
} from "../pending-types.js"; } from "../pending-types.js";
import { import { getTimestampNow, Timestamp } from "@gnu-taler/taler-util";
getTimestampNow,
Timestamp,
} from "@gnu-taler/taler-util";
import { InternalWalletState } from "../common.js"; import { InternalWalletState } from "../common.js";
import { GetReadOnlyAccess } from "../util/query.js"; import { GetReadOnlyAccess } from "../util/query.js";
@ -74,35 +71,36 @@ async function gatherReservePending(
now: Timestamp, now: Timestamp,
resp: PendingOperationsResponse, resp: PendingOperationsResponse,
): Promise<void> { ): Promise<void> {
await tx.reserves.indexes.byStatus const reserves = await tx.reserves.indexes.byStatus.getAll(
.iter(OperationStatus.Pending) OperationStatus.Pending,
.forEach((reserve) => { );
const reserveType = reserve.bankInfo for (const reserve of reserves) {
? ReserveType.TalerBankWithdraw const reserveType = reserve.bankInfo
: ReserveType.Manual; ? ReserveType.TalerBankWithdraw
switch (reserve.reserveStatus) { : ReserveType.Manual;
case ReserveRecordStatus.DORMANT: switch (reserve.reserveStatus) {
// nothing to report as pending case ReserveRecordStatus.DORMANT:
break; // nothing to report as pending
case ReserveRecordStatus.WAIT_CONFIRM_BANK: break;
case ReserveRecordStatus.QUERYING_STATUS: case ReserveRecordStatus.WAIT_CONFIRM_BANK:
case ReserveRecordStatus.REGISTERING_BANK: case ReserveRecordStatus.QUERYING_STATUS:
resp.pendingOperations.push({ case ReserveRecordStatus.REGISTERING_BANK:
type: PendingTaskType.Reserve, resp.pendingOperations.push({
givesLifeness: true, type: PendingTaskType.Reserve,
timestampDue: reserve.retryInfo.nextRetry, givesLifeness: true,
stage: reserve.reserveStatus, timestampDue: reserve.retryInfo.nextRetry,
timestampCreated: reserve.timestampCreated, stage: reserve.reserveStatus,
reserveType, timestampCreated: reserve.timestampCreated,
reservePub: reserve.reservePub, reserveType,
retryInfo: reserve.retryInfo, reservePub: reserve.reservePub,
}); retryInfo: reserve.retryInfo,
break; });
default: break;
// FIXME: report problem! default:
break; // FIXME: report problem!
} break;
}); }
}
} }
async function gatherRefreshPending( async function gatherRefreshPending(
@ -110,26 +108,27 @@ async function gatherRefreshPending(
now: Timestamp, now: Timestamp,
resp: PendingOperationsResponse, resp: PendingOperationsResponse,
): Promise<void> { ): Promise<void> {
await tx.refreshGroups.indexes.byStatus const refreshGroups = await tx.refreshGroups.indexes.byStatus.getAll(
.iter(OperationStatus.Pending) OperationStatus.Pending,
.forEach((r) => { );
if (r.timestampFinished) { for (const r of refreshGroups) {
return; if (r.timestampFinished) {
} return;
if (r.frozen) { }
return; if (r.frozen) {
} return;
resp.pendingOperations.push({ }
type: PendingTaskType.Refresh, resp.pendingOperations.push({
givesLifeness: true, type: PendingTaskType.Refresh,
timestampDue: r.retryInfo.nextRetry, givesLifeness: true,
refreshGroupId: r.refreshGroupId, timestampDue: r.retryInfo.nextRetry,
finishedPerCoin: r.statusPerCoin.map( refreshGroupId: r.refreshGroupId,
(x) => x === RefreshCoinStatus.Finished, finishedPerCoin: r.statusPerCoin.map(
), (x) => x === RefreshCoinStatus.Finished,
retryInfo: r.retryInfo, ),
}); retryInfo: r.retryInfo,
}); });
}
} }
async function gatherWithdrawalPending( async function gatherWithdrawalPending(
@ -140,31 +139,32 @@ async function gatherWithdrawalPending(
now: Timestamp, now: Timestamp,
resp: PendingOperationsResponse, resp: PendingOperationsResponse,
): Promise<void> { ): Promise<void> {
await tx.withdrawalGroups.indexes.byStatus const wsrs = await tx.withdrawalGroups.indexes.byStatus.getAll(
.iter(OperationStatus.Pending) OperationStatus.Pending,
.forEachAsync(async (wsr) => { );
if (wsr.timestampFinish) { for (const wsr of wsrs) {
return; if (wsr.timestampFinish) {
} return;
let numCoinsWithdrawn = 0; }
let numCoinsTotal = 0; let numCoinsWithdrawn = 0;
await tx.planchets.indexes.byGroup let numCoinsTotal = 0;
.iter(wsr.withdrawalGroupId) await tx.planchets.indexes.byGroup
.forEach((x) => { .iter(wsr.withdrawalGroupId)
numCoinsTotal++; .forEach((x) => {
if (x.withdrawalDone) { numCoinsTotal++;
numCoinsWithdrawn++; if (x.withdrawalDone) {
} numCoinsWithdrawn++;
}); }
resp.pendingOperations.push({
type: PendingTaskType.Withdraw,
givesLifeness: true,
timestampDue: wsr.retryInfo.nextRetry,
withdrawalGroupId: wsr.withdrawalGroupId,
lastError: wsr.lastError,
retryInfo: wsr.retryInfo,
}); });
resp.pendingOperations.push({
type: PendingTaskType.Withdraw,
givesLifeness: true,
timestampDue: wsr.retryInfo.nextRetry,
withdrawalGroupId: wsr.withdrawalGroupId,
lastError: wsr.lastError,
retryInfo: wsr.retryInfo,
}); });
}
} }
async function gatherProposalPending( async function gatherProposalPending(
@ -197,22 +197,23 @@ async function gatherDepositPending(
now: Timestamp, now: Timestamp,
resp: PendingOperationsResponse, resp: PendingOperationsResponse,
): Promise<void> { ): Promise<void> {
await tx.depositGroups.indexes.byStatus const dgs = await tx.depositGroups.indexes.byStatus.getAll(
.iter(OperationStatus.Pending) OperationStatus.Pending,
.forEach((dg) => { );
if (dg.timestampFinished) { for (const dg of dgs) {
return; if (dg.timestampFinished) {
} return;
const timestampDue = dg.retryInfo?.nextRetry ?? getTimestampNow(); }
resp.pendingOperations.push({ const timestampDue = dg.retryInfo?.nextRetry ?? getTimestampNow();
type: PendingTaskType.Deposit, resp.pendingOperations.push({
givesLifeness: true, type: PendingTaskType.Deposit,
timestampDue, givesLifeness: true,
depositGroupId: dg.depositGroupId, timestampDue,
lastError: dg.lastError, depositGroupId: dg.depositGroupId,
retryInfo: dg.retryInfo, lastError: dg.lastError,
}); retryInfo: dg.retryInfo,
}); });
}
} }
async function gatherTipPending( async function gatherTipPending(

View File

@ -975,13 +975,13 @@ async function processWithdrawGroupImpl(
export async function getExchangeWithdrawalInfo( export async function getExchangeWithdrawalInfo(
ws: InternalWalletState, ws: InternalWalletState,
baseUrl: string, exchangeBaseUrl: string,
amount: AmountJson, amount: AmountJson,
): Promise<ExchangeWithdrawDetails> { ): Promise<ExchangeWithdrawDetails> {
const { exchange, exchangeDetails } = const { exchange, exchangeDetails } =
await ws.exchangeOps.updateExchangeFromUrl(ws, baseUrl); await ws.exchangeOps.updateExchangeFromUrl(ws, exchangeBaseUrl);
await updateWithdrawalDenoms(ws, baseUrl); await updateWithdrawalDenoms(ws, exchangeBaseUrl);
const denoms = await getCandidateWithdrawalDenoms(ws, baseUrl); const denoms = await getCandidateWithdrawalDenoms(ws, exchangeBaseUrl);
const selectedDenoms = selectWithdrawalDenominations(amount, denoms); const selectedDenoms = selectWithdrawalDenominations(amount, denoms);
const exchangeWireAccounts: string[] = []; const exchangeWireAccounts: string[] = [];
for (const account of exchangeDetails.wireInfo.accounts) { for (const account of exchangeDetails.wireInfo.accounts) {
@ -1006,9 +1006,10 @@ export async function getExchangeWithdrawalInfo(
const possibleDenoms = await ws.db const possibleDenoms = await ws.db
.mktx((x) => ({ denominations: x.denominations })) .mktx((x) => ({ denominations: x.denominations }))
.runReadOnly(async (tx) => { .runReadOnly(async (tx) => {
return tx.denominations.indexes.byExchangeBaseUrl const ds = await tx.denominations.indexes.byExchangeBaseUrl.getAll(
.iter() exchangeBaseUrl,
.filter((d) => d.isOffered); );
return ds.filter((x) => x.isOffered);
}); });
let versionMatch; let versionMatch;

View File

@ -103,6 +103,7 @@ import {
processReserve, processReserve,
} from "./operations/reserves.js"; } from "./operations/reserves.js";
import { import {
DenomInfo,
ExchangeOperations, ExchangeOperations,
InternalWalletState, InternalWalletState,
MerchantInfo, MerchantInfo,
@ -186,13 +187,12 @@ import {
OpenedPromise, OpenedPromise,
openPromise, openPromise,
} from "./util/promiseUtils.js"; } from "./util/promiseUtils.js";
import { DbAccess } from "./util/query.js"; import { DbAccess, GetReadWriteAccess } from "./util/query.js";
import { import {
HttpRequestLibrary, HttpRequestLibrary,
readSuccessResponseJsonOrThrow, readSuccessResponseJsonOrThrow,
} from "./util/http.js"; } from "./util/http.js";
import { getMerchantInfo } from "./operations/merchants.js"; import { getMerchantInfo } from "./operations/merchants.js";
import { Event, IDBDatabase } from "@gnu-taler/idb-bridge";
const builtinAuditors: AuditorTrustRecord[] = [ const builtinAuditors: AuditorTrustRecord[] = [
{ {
@ -506,24 +506,24 @@ async function listKnownBankAccounts(
ws: InternalWalletState, ws: InternalWalletState,
currency?: string, currency?: string,
): Promise<KnownBankAccounts> { ): Promise<KnownBankAccounts> {
const accounts: PaytoUri[] = [] const accounts: PaytoUri[] = [];
await ws.db await ws.db
.mktx((x) => ({ .mktx((x) => ({
reserves: x.reserves, reserves: x.reserves,
})) }))
.runReadOnly(async (tx) => { .runReadOnly(async (tx) => {
const reservesRecords = await tx.reserves.iter().toArray() const reservesRecords = await tx.reserves.iter().toArray();
for (const r of reservesRecords) { for (const r of reservesRecords) {
if (currency && currency !== r.currency) { if (currency && currency !== r.currency) {
continue continue;
} }
const payto = r.senderWire ? parsePaytoUri(r.senderWire) : undefined const payto = r.senderWire ? parsePaytoUri(r.senderWire) : undefined;
if (payto) { if (payto) {
accounts.push(payto) accounts.push(payto);
} }
} }
}) });
return { accounts } return { accounts };
} }
async function getExchanges( async function getExchanges(
@ -785,9 +785,8 @@ async function dispatchRequestInternal(
return res; return res;
} }
case "getWithdrawalDetailsForAmount": { case "getWithdrawalDetailsForAmount": {
const req = codecForGetWithdrawalDetailsForAmountRequest().decode( const req =
payload, codecForGetWithdrawalDetailsForAmountRequest().decode(payload);
);
return await getWithdrawalDetailsForAmount( return await getWithdrawalDetailsForAmount(
ws, ws,
req.exchangeBaseUrl, req.exchangeBaseUrl,
@ -810,9 +809,8 @@ async function dispatchRequestInternal(
return await applyRefund(ws, req.talerRefundUri); return await applyRefund(ws, req.talerRefundUri);
} }
case "acceptBankIntegratedWithdrawal": { case "acceptBankIntegratedWithdrawal": {
const req = codecForAcceptBankIntegratedWithdrawalRequest().decode( const req =
payload, codecForAcceptBankIntegratedWithdrawalRequest().decode(payload);
);
return await acceptWithdrawal( return await acceptWithdrawal(
ws, ws,
req.talerWithdrawUri, req.talerWithdrawUri,
@ -1048,7 +1046,7 @@ export async function handleCoreApiRequest(
try { try {
logger.error("Caught unexpected exception:"); logger.error("Caught unexpected exception:");
logger.error(e.stack); logger.error(e.stack);
} catch (e) { } } catch (e) {}
return { return {
type: "error", type: "error",
operation, operation,
@ -1133,7 +1131,8 @@ export class Wallet {
class InternalWalletStateImpl implements InternalWalletState { class InternalWalletStateImpl implements InternalWalletState {
memoProcessReserve: AsyncOpMemoMap<void> = new AsyncOpMemoMap(); memoProcessReserve: AsyncOpMemoMap<void> = new AsyncOpMemoMap();
memoMakePlanchet: AsyncOpMemoMap<void> = new AsyncOpMemoMap(); memoMakePlanchet: AsyncOpMemoMap<void> = new AsyncOpMemoMap();
memoGetPending: AsyncOpMemoSingle<PendingOperationsResponse> = new AsyncOpMemoSingle(); memoGetPending: AsyncOpMemoSingle<PendingOperationsResponse> =
new AsyncOpMemoSingle();
memoGetBalance: AsyncOpMemoSingle<BalancesResponse> = new AsyncOpMemoSingle(); memoGetBalance: AsyncOpMemoSingle<BalancesResponse> = new AsyncOpMemoSingle();
memoProcessRefresh: AsyncOpMemoMap<void> = new AsyncOpMemoMap(); memoProcessRefresh: AsyncOpMemoMap<void> = new AsyncOpMemoMap();
memoProcessRecoup: AsyncOpMemoMap<void> = new AsyncOpMemoMap(); memoProcessRecoup: AsyncOpMemoMap<void> = new AsyncOpMemoMap();
@ -1169,7 +1168,10 @@ class InternalWalletStateImpl implements InternalWalletState {
reserveOps: ReserveOperations = { reserveOps: ReserveOperations = {
processReserve: processReserve, processReserve: processReserve,
} };
// FIXME: Use an LRU cache here.
private denomCache: Record<string, DenomInfo> = {};
/** /**
* Promises that are waiting for a particular resource. * Promises that are waiting for a particular resource.
@ -1193,6 +1195,22 @@ class InternalWalletStateImpl implements InternalWalletState {
this.cryptoApi = new CryptoApi(cryptoWorkerFactory); this.cryptoApi = new CryptoApi(cryptoWorkerFactory);
} }
async getDenomInfo(
ws: InternalWalletState,
tx: GetReadWriteAccess<{
denominations: typeof WalletStoresV1.denominations;
}>,
exchangeBaseUrl: string,
denomPubHash: string,
): Promise<DenomInfo | undefined> {
const key = `${exchangeBaseUrl}:${denomPubHash}`;
const cached = this.denomCache[key];
if (cached) {
return cached;
}
return await tx.denominations.get([exchangeBaseUrl, denomPubHash]);
}
notify(n: WalletNotification): void { notify(n: WalletNotification): void {
logger.trace("Notification", n); logger.trace("Notification", n);
for (const l of this.listeners) { for (const l of this.listeners) {