taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

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:
Mpackages/taler-wallet-core/src/coinSelection.ts | 5+----
Mpackages/taler-wallet-core/src/db-common.ts | 12++++++++++++
Mpackages/taler-wallet-core/src/db-indexeddb.ts | 14++------------
Mpackages/taler-wallet-core/src/dbtx-indexeddb.ts | 35+++++++++++++++++++++++++++++++++++
Mpackages/taler-wallet-core/src/dbtx.ts | 21+++++++++++++++++++++
Mpackages/taler-wallet-core/src/dev-experiments.ts | 6+++---
Mpackages/taler-wallet-core/src/exchanges.ts | 76+++++++++++++++++++++++++++++++---------------------------------------------
Mpackages/taler-wallet-core/src/instructedAmountConversion.ts | 7++-----
Mpackages/taler-wallet-core/src/pay-merchant.ts | 9+++------
Mpackages/taler-wallet-core/src/wallet.ts | 14+++++---------
Mpackages/taler-wallet-core/src/withdraw.ts | 5+----
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;