commit 90f41f416de17a0ee84ea50fa7f4ae9b94b7a488
parent 82746164481154de96e449484a8fae989d37b461
Author: Florian Dold <dold@taler.net>
Date: Sat, 18 Jul 2026 23:44:36 +0200
db: add denomination and denom-loss accessors
Diffstat:
11 files changed, 116 insertions(+), 88 deletions(-)
diff --git a/packages/taler-wallet-core/src/coinSelection.ts b/packages/taler-wallet-core/src/coinSelection.ts
@@ -1048,10 +1048,7 @@ async function selectPayCandidates(
// FIXME: Should we exclude denominations that are
// not spendable anymore?
for (const coinAvail of myExchangeCoins) {
- const denom = await tx.denominations.get([
- coinAvail.exchangeBaseUrl,
- coinAvail.denomPubHash,
- ]);
+ const denom = await tx.wtx.getDenomination(coinAvail.exchangeBaseUrl, coinAvail.denomPubHash);
checkDbInvariant(
!!denom,
`denomination of a coin is missing hash: ${coinAvail.denomPubHash}`,
diff --git a/packages/taler-wallet-core/src/db-common.ts b/packages/taler-wallet-core/src/db-common.ts
@@ -47,6 +47,7 @@ import {
WireInfo,
AccountLimit,
ZeroLimitedOperation,
+ DenomLossEventType,
CoinStatus,
AgeCommitmentProof,
TransactionIdStr,
@@ -1316,6 +1317,17 @@ export interface WalletExchangeDetails {
defaultPeerPushExpiration: TalerProtocolDuration | undefined;
}
+export interface WalletDenomLossEvent {
+ denomLossEventId: string;
+ currency: string;
+ denomPubHashes: string[];
+ status: DenomLossStatus;
+ timestampCreated: DbPreciseTimestamp;
+ amount: string;
+ eventType: DenomLossEventType;
+ exchangeBaseUrl: string;
+}
+
export interface WalletWithdrawCoinSource {
type: CoinSourceType.Withdraw;
diff --git a/packages/taler-wallet-core/src/db-indexeddb.ts b/packages/taler-wallet-core/src/db-indexeddb.ts
@@ -142,6 +142,7 @@ import {
WalletExchangeEntry,
WalletExchangeDetails,
WalletSlate,
+ WalletDenomLossEvent,
WalletToken,
TokenFamilyInfo,
WalletRefundGroup,
@@ -585,17 +586,6 @@ export interface GlobalCurrencyExchangeRecord {
exchangeMasterPub: string;
}
-export interface DenomLossEventRecord {
- denomLossEventId: string;
- currency: string;
- denomPubHashes: string[];
- status: DenomLossStatus;
- timestampCreated: DbPreciseTimestamp;
- amount: string;
- eventType: DenomLossEventType;
- exchangeBaseUrl: string;
-}
-
export interface CurrencyInfoRecord {
/**
* Stringified scope info.
@@ -680,7 +670,7 @@ export const WalletIndexedDbStoresV1 = {
indexes: {},
}),
denomLossEvents: describeStoreV2({
- recordCodec: passthroughCodec<DenomLossEventRecord>(),
+ recordCodec: passthroughCodec<WalletDenomLossEvent>(),
storeName: "denomLossEvents",
keyPath: "denomLossEventId",
versionAdded: 9,
diff --git a/packages/taler-wallet-core/src/dbtx-indexeddb.ts b/packages/taler-wallet-core/src/dbtx-indexeddb.ts
@@ -72,6 +72,7 @@ import {
WalletRefundItem,
WalletTombstone,
WalletExchangeEntry,
+ WalletDenomLossEvent,
WalletExchangeDetails,
} from "./db-common.js";
import type {
@@ -348,6 +349,23 @@ export class IdbWalletTransaction implements WalletDbTransaction {
});
}
+ async getDenomLossEvent(
+ denomLossEventId: string,
+ ): Promise<WalletDenomLossEvent | undefined> {
+ const tx = this.tx;
+ return await tx.denomLossEvents.get(denomLossEventId);
+ }
+
+ async upsertDenomLossEvent(rec: WalletDenomLossEvent): Promise<void> {
+ const tx = this.tx;
+ await tx.denomLossEvents.put(rec);
+ }
+
+ async deleteDenomLossEvent(denomLossEventId: string): Promise<void> {
+ const tx = this.tx;
+ await tx.denomLossEvents.delete(denomLossEventId);
+ }
+
async getExchange(
baseUrl: string,
): Promise<WalletExchangeEntry | undefined> {
@@ -1132,6 +1150,23 @@ export class IdbWalletTransaction implements WalletDbTransaction {
return await tx.denominations.get([exchangeBaseUrl, denomPubHash]);
}
+ async getDenominationsByExchange(
+ exchangeBaseUrl: string,
+ ): Promise<WalletDenomination[]> {
+ const tx = this.tx;
+ return await tx.denominations.indexes.byExchangeBaseUrl.getAll(
+ exchangeBaseUrl,
+ );
+ }
+
+ async deleteDenomination(
+ exchangeBaseUrl: string,
+ denomPubHash: string,
+ ): Promise<void> {
+ const tx = this.tx;
+ await tx.denominations.delete([exchangeBaseUrl, denomPubHash]);
+ }
+
async getDenominationsByVerificationStatus(
verificationStatus: DenominationVerificationStatus,
): Promise<WalletDenomination[]> {
diff --git a/packages/taler-wallet-core/src/dbtx.ts b/packages/taler-wallet-core/src/dbtx.ts
@@ -72,6 +72,7 @@ import {
WalletRefundItem,
WalletTombstone,
WalletExchangeEntry,
+ WalletDenomLossEvent,
WalletExchangeDetails,
} from "./db-common.js";
export interface GetCurrencyInfoDbResult {
@@ -239,6 +240,14 @@ export interface WalletDbTransaction {
/**
* Get an exchange entry by its base URL.
*/
+ getDenomLossEvent(
+ denomLossEventId: string,
+ ): Promise<WalletDenomLossEvent | undefined>;
+
+ upsertDenomLossEvent(rec: WalletDenomLossEvent): Promise<void>;
+
+ deleteDenomLossEvent(denomLossEventId: string): Promise<void>;
+
getExchange(baseUrl: string): Promise<WalletExchangeEntry | undefined>;
upsertExchange(rec: WalletExchangeEntry): Promise<void>;
@@ -581,6 +590,18 @@ export interface WalletDbTransaction {
denomPubHash: string,
): Promise<WalletDenomination | undefined>;
+ /**
+ * Get all denominations offered by an exchange.
+ */
+ getDenominationsByExchange(
+ exchangeBaseUrl: string,
+ ): Promise<WalletDenomination[]>;
+
+ deleteDenomination(
+ exchangeBaseUrl: string,
+ denomPubHash: string,
+ ): Promise<void>;
+
getDenominationsByVerificationStatus(
verificationStatus: DenominationVerificationStatus,
): Promise<WalletDenomination[]>;
diff --git a/packages/taler-wallet-core/src/dev-experiments.ts b/packages/taler-wallet-core/src/dev-experiments.ts
@@ -65,9 +65,9 @@ import {
timestampProtocolToDb,
WalletRefreshGroup,
WithdrawalRecordType,
+ WalletDenomLossEvent,
} from "./db-common.js";
import {
- DenomLossEventRecord,
} from "./db-indexeddb.js";
import {
DenomLossTransactionContext,
@@ -215,7 +215,7 @@ export async function applyDevExperiment(
case "insert-denom-loss": {
await wex.runLegacyWalletDbTx(async (tx) => {
const eventId = encodeCrock(getRandomBytes(32));
- const newRg: DenomLossEventRecord = {
+ const newRg: WalletDenomLossEvent = {
amount: "TESTKUDOS:42",
currency: "TESTKUDOS",
exchangeBaseUrl: "https://exchange.test.taler.net/",
@@ -228,7 +228,7 @@ export async function applyDevExperiment(
status: DenomLossStatus.Done,
timestampCreated: timestampPreciseToDb(TalerPreciseTimestamp.now()),
};
- await tx.denomLossEvents.put(newRg);
+ await tx.wtx.upsertDenomLossEvent(newRg);
const ctx = new DenomLossTransactionContext(
wex,
newRg.denomLossEventId,
diff --git a/packages/taler-wallet-core/src/exchanges.ts b/packages/taler-wallet-core/src/exchanges.ts
@@ -152,10 +152,10 @@ import {
WalletReserve,
WalletExchangeDetails,
WalletExchangeEntry,
+ WalletDenomLossEvent,
} from "./db-common.js";
import {
DenomFamilyParams,
- DenomLossEventRecord,
DenominationFamilyRecord,
ExchangeMigrationReason,
} from "./db-indexeddb.js";
@@ -972,7 +972,7 @@ async function checkExchangeEntryOutdated(
logger.trace(`checking if exchange entry for ${exchangeBaseUrl} is outdated`);
let numOkay = 0;
let denoms =
- await tx.denominations.indexes.byExchangeBaseUrl.getAll(exchangeBaseUrl);
+ await tx.wtx.getDenominationsByExchange(exchangeBaseUrl);
logger.trace(`exchange entry has ${denoms.length} denominations`);
for (const denom of denoms) {
const denomOkay = isCandidateWithdrawableDenomRec(denom);
@@ -2039,7 +2039,7 @@ export async function updateExchangeFromUrlHandler(
// In the future: Filter out old denominations by index
const allOldDenoms =
- await tx.denominations.indexes.byExchangeBaseUrl.getAll(exchangeBaseUrl);
+ await tx.wtx.getDenominationsByExchange(exchangeBaseUrl);
const oldDenomByDph = new Map<string, WalletDenomination>();
for (const denom of allOldDenoms) {
oldDenomByDph.set(denom.denomPubHash, denom);
@@ -2140,10 +2140,10 @@ export async function updateExchangeFromUrlHandler(
changed = true;
}
if (changed) {
- await tx.denominations.put(oldDenom);
+ await tx.wtx.upsertDenomination(oldDenom);
}
} else {
- await tx.denominations.put(denomRec);
+ await tx.wtx.upsertDenomination(denomRec);
}
}
@@ -2158,13 +2158,13 @@ export async function updateExchangeFromUrlHandler(
logger.info(
`setting denomination ${x.denomPubHash} to offered=false`,
);
- await tx.denominations.put(x);
+ await tx.wtx.upsertDenomination(x);
}
} else {
if (!x.isOffered) {
x.isOffered = true;
logger.info(`setting denomination ${x.denomPubHash} to offered=true`);
- await tx.denominations.put(x);
+ await tx.wtx.upsertDenomination(x);
}
}
}
@@ -2230,10 +2230,7 @@ async function doExchangeAutoRefresh(
if (coin.status !== CoinStatus.Fresh) {
continue;
}
- const denom = await tx.denominations.get([
- exchangeBaseUrl,
- coin.denomPubHash,
- ]);
+ const denom = await tx.wtx.getDenomination(exchangeBaseUrl, coin.denomPubHash);
if (!denom) {
logger.warn("denomination not in database");
continue;
@@ -2350,10 +2347,7 @@ async function handleDenomLoss(
continue;
}
const n = coinAv.freshCoinCount;
- const denom = await tx.denominations.get([
- coinAv.exchangeBaseUrl,
- coinAv.denomPubHash,
- ]);
+ const denom = await tx.wtx.getDenomination(coinAv.exchangeBaseUrl, coinAv.denomPubHash);
const timestampExpireDeposit = !denom
? undefined
: timestampAbsoluteFromDb(denom.stampExpireDeposit);
@@ -2408,7 +2402,7 @@ async function handleDenomLoss(
if (denomsVanished.length > 0) {
const denomLossEventId = encodeCrock(getRandomBytes(32));
- const rec: DenomLossEventRecord = {
+ const rec: WalletDenomLossEvent = {
denomLossEventId,
amount: amountVanished.toString(),
currency,
@@ -2418,7 +2412,7 @@ async function handleDenomLoss(
status: DenomLossStatus.Done,
timestampCreated: timestampPreciseToDb(TalerPreciseTimestamp.now()),
};
- await tx.denomLossEvents.add(rec);
+ await tx.wtx.upsertDenomLossEvent(rec);
const ctx = new DenomLossTransactionContext(wex, denomLossEventId);
await ctx.updateTransactionMeta(tx);
tx.notify({
@@ -2440,7 +2434,7 @@ async function handleDenomLoss(
if (denomsUnoffered.length > 0) {
const denomLossEventId = encodeCrock(getRandomBytes(32));
- const rec: DenomLossEventRecord = {
+ const rec: WalletDenomLossEvent = {
denomLossEventId,
amount: amountUnoffered.toString(),
currency,
@@ -2450,7 +2444,7 @@ async function handleDenomLoss(
status: DenomLossStatus.Done,
timestampCreated: timestampPreciseToDb(TalerPreciseTimestamp.now()),
};
- await tx.denomLossEvents.add(rec);
+ await tx.wtx.upsertDenomLossEvent(rec);
const ctx = new DenomLossTransactionContext(wex, denomLossEventId);
await ctx.updateTransactionMeta(tx);
tx.notify({
@@ -2472,7 +2466,7 @@ async function handleDenomLoss(
if (denomsExpired.length > 0) {
const denomLossEventId = encodeCrock(getRandomBytes(32));
- const rec: DenomLossEventRecord = {
+ const rec: WalletDenomLossEvent = {
denomLossEventId,
amount: amountExpired.toString(),
currency,
@@ -2482,7 +2476,7 @@ async function handleDenomLoss(
status: DenomLossStatus.Done,
timestampCreated: timestampPreciseToDb(TalerPreciseTimestamp.now()),
};
- await tx.denomLossEvents.add(rec);
+ await tx.wtx.upsertDenomLossEvent(rec);
const transactionId = constructTransactionIdentifier({
tag: TransactionType.DenomLoss,
denomLossEventId,
@@ -2506,7 +2500,7 @@ async function handleDenomLoss(
}
export function computeDenomLossTransactionStatus(
- rec: DenomLossEventRecord,
+ rec: WalletDenomLossEvent,
): TransactionState {
switch (rec.status) {
case DenomLossStatus.Aborted:
@@ -2538,7 +2532,7 @@ export class DenomLossTransactionContext implements TransactionContext {
}
async updateTransactionMeta(tx: LegacyWalletTxHandle): Promise<void> {
- const denomLossRec = await tx.denomLossEvents.get(this.denomLossEventId);
+ const denomLossRec = await tx.wtx.getDenomLossEvent(this.denomLossEventId);
if (!denomLossRec) {
await tx.wtx.deleteTransactionMeta(this.transactionId);
return;
@@ -2578,12 +2572,12 @@ export class DenomLossTransactionContext implements TransactionContext {
async userDeleteTransaction(): Promise<void> {
await this.wex.runLegacyWalletDbTx(async (tx) => {
- const rec = await tx.denomLossEvents.get(this.denomLossEventId);
+ const rec = await tx.wtx.getDenomLossEvent(this.denomLossEventId);
if (!rec) {
return;
}
const oldTxState = computeDenomLossTransactionStatus(rec);
- await tx.denomLossEvents.delete(this.denomLossEventId);
+ await tx.wtx.deleteDenomLossEvent(this.denomLossEventId);
applyNotifyTransition(tx.notify, this.transactionId, {
oldTxState,
newTxState: {
@@ -2599,7 +2593,7 @@ export class DenomLossTransactionContext implements TransactionContext {
async lookupFullTransaction(
tx: LegacyWalletTxHandle,
): Promise<Transaction | undefined> {
- const rec = await tx.denomLossEvents.get(this.denomLossEventId);
+ const rec = await tx.wtx.getDenomLossEvent(this.denomLossEventId);
if (!rec) {
return undefined;
}
@@ -2634,10 +2628,7 @@ async function handleRecoup(
const newlyRevokedCoinPubs: string[] = [];
logger.trace("recoup list from exchange", recoupDenomList);
for (const recoupInfo of recoupDenomList) {
- const oldDenom = await tx.denominations.get([
- exchangeBaseUrl,
- recoupInfo.h_denom_pub,
- ]);
+ const oldDenom = await tx.wtx.getDenomination(exchangeBaseUrl, recoupInfo.h_denom_pub);
if (!oldDenom) {
// We never even knew about the revoked denomination, all good.
continue;
@@ -2650,7 +2641,7 @@ async function handleRecoup(
}
logger.info("revoking denom", recoupInfo.h_denom_pub);
oldDenom.isRevoked = true;
- await tx.denominations.put(oldDenom);
+ await tx.wtx.upsertDenomination(oldDenom);
const affectedCoins = await tx.coins.indexes.byDenomPubHash.getAll(
recoupInfo.h_denom_pub,
);
@@ -3008,7 +2999,7 @@ export async function getExchangeDetailedInfo(
return;
}
const denominationRecords =
- await tx.denominations.indexes.byExchangeBaseUrl.getAll(ex.baseUrl);
+ await tx.wtx.getDenominationsByExchange(ex.baseUrl);
if (!denominationRecords) {
return;
@@ -3225,9 +3216,9 @@ async function purgeExchange(
{
const denomRecs =
- await tx.denominations.indexes.byExchangeBaseUrl.getAll(exchangeBaseUrl);
+ await tx.wtx.getDenominationsByExchange(exchangeBaseUrl);
for (const rec of denomRecs) {
- await tx.denominations.delete([rec.exchangeBaseUrl, rec.denomPubHash]);
+ await tx.wtx.deleteDenomination(rec.exchangeBaseUrl, rec.denomPubHash);
}
}
@@ -4212,18 +4203,13 @@ export async function migrateExchange(
});
{
- const denomKeys =
- await tx.denominations.indexes.byExchangeBaseUrl.getAllKeys(
- req.oldExchangeBaseUrl,
- );
+ const denoms = await tx.wtx.getDenominationsByExchange(
+ req.oldExchangeBaseUrl,
+ );
- for (const dk of denomKeys) {
- const rec = await tx.denominations.get(dk);
- if (!rec) {
- continue;
- }
+ for (const rec of denoms) {
rec.exchangeBaseUrl = req.newExchangeBaseUrl;
- await tx.denominations.put(rec);
+ await tx.wtx.upsertDenomination(rec);
}
}
@@ -4231,7 +4217,7 @@ export async function migrateExchange(
await tx.denomLossEvents.iter().forEachAsync(async (rec) => {
if (rec.exchangeBaseUrl === req.oldExchangeBaseUrl) {
rec.exchangeBaseUrl = req.newExchangeBaseUrl;
- await tx.denomLossEvents.put(rec);
+ await tx.wtx.upsertDenomLossEvent(rec);
}
});
}
diff --git a/packages/taler-wallet-core/src/instructedAmountConversion.ts b/packages/taler-wallet-core/src/instructedAmountConversion.ts
@@ -203,7 +203,7 @@ async function getAvailableCoins(
if (operationType === OperationType.Credit) {
// FIXME: Use denom groups instead of querying all denominations!
const ds =
- await tx.denominations.indexes.byExchangeBaseUrl.getAll(
+ await tx.wtx.getDenominationsByExchange(
exchangeBaseUrl,
);
for (const denom of ds) {
@@ -245,10 +245,7 @@ async function getAvailableCoins(
// FIXME: Should we exclude denominations that are
// not spendable anymore?
for (const coinAvail of myExchangeCoins) {
- const denom = await tx.denominations.get([
- coinAvail.exchangeBaseUrl,
- coinAvail.denomPubHash,
- ]);
+ const denom = await tx.wtx.getDenomination(coinAvail.exchangeBaseUrl, coinAvail.denomPubHash);
checkDbInvariant(
!!denom,
`denomination of a coin is missing hash: ${coinAvail.denomPubHash}`,
diff --git a/packages/taler-wallet-core/src/pay-merchant.ts b/packages/taler-wallet-core/src/pay-merchant.ts
@@ -837,10 +837,10 @@ export async function getTotalPaymentCostInTx(
): Promise<AmountJson> {
const costs: AmountJson[] = [];
for (let i = 0; i < pcs.length; i++) {
- const denom = await tx.denominations.get([
+ const denom = await tx.wtx.getDenomination(
pcs[i].exchangeBaseUrl,
pcs[i].denomPubHash,
- ]);
+ );
if (!denom) {
throw Error(
"can't calculate payment cost, denomination for coin not found",
@@ -2320,10 +2320,7 @@ export async function generateDepositPermissions(
if (!coin) {
throw Error("can't pay, allocated coin not found anymore");
}
- const denom = await tx.denominations.get([
- coin.exchangeBaseUrl,
- coin.denomPubHash,
- ]);
+ const denom = await tx.wtx.getDenomination(coin.exchangeBaseUrl, coin.denomPubHash);
if (!denom) {
throw Error(
"can't pay, denomination of allocated coin not found anymore",
diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts
@@ -615,7 +615,7 @@ export async function getDenomInfo(
const d =
"getDenomination" in tx
? await tx.getDenomination(exchangeBaseUrl, denomPubHash)
- : await tx.denominations.get([exchangeBaseUrl, denomPubHash]);
+ : await tx.wtx.getDenomination(exchangeBaseUrl, denomPubHash);
if (d != null) {
return WalletDenomination.toDenomInfo(d);
} else {
@@ -741,10 +741,7 @@ async function dumpCoins(wex: WalletExecutionContext): Promise<CoinDumpJson> {
await wex.runLegacyWalletDbTx(async (tx) => {
const coins = await tx.coins.iter().toArray();
for (const c of coins) {
- const denom = await tx.denominations.get([
- c.exchangeBaseUrl,
- c.denomPubHash,
- ]);
+ const denom = await tx.wtx.getDenomination(c.exchangeBaseUrl, c.denomPubHash);
if (!denom) {
logger.warn("no denom found for coin");
continue;
@@ -1186,7 +1183,7 @@ async function handleTestingGetDenomStats(
numOffered: 0,
};
await wex.runLegacyWalletDbTx(async (tx) => {
- const denoms = await tx.denominations.indexes.byExchangeBaseUrl.getAll(
+ const denoms = await tx.wtx.getDenominationsByExchange(
req.exchangeBaseUrl,
);
for (const d of denoms) {
@@ -2153,9 +2150,8 @@ export async function handleGetDiagnostics(
cnt["exchangeSignKeys"] = await tx.exchangeSignKeys.count();
cnt["exchanges"] = await tx.exchanges.count();
for (const exch of await tx.exchanges.getAll()) {
- const denoms = await tx.denominations.indexes.byExchangeBaseUrl.getAll(
- exch.baseUrl,
- );
+ const denoms =
+ await tx.denominations.indexes.byExchangeBaseUrl.getAll(exch.baseUrl);
let numWithdrawableDenoms = 0;
let numCandidateWithdrawableDenoms = 0;
for (let i = 0; i < denoms.length; i++) {
diff --git a/packages/taler-wallet-core/src/withdraw.ts b/packages/taler-wallet-core/src/withdraw.ts
@@ -2417,10 +2417,7 @@ async function redenominateWithdrawal(
let coinIndex = 0;
for (let i = 0; i < oldSel.selectedDenoms.length; i++) {
const sel = wg.denomsSel.selectedDenoms[i];
- const denom = await tx.denominations.get([
- exchangeBaseUrl,
- sel.denomPubHash,
- ]);
+ const denom = await tx.wtx.getDenomination(exchangeBaseUrl, sel.denomPubHash);
let denomOkay: boolean = false;