taler-typescript-core

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

commit 084eebe909d5aad613d5f31f729f0d2910f3f326
parent fa0b2336d29ef7419158b647b3f304aaf5bc87f2
Author: Florian Dold <dold@taler.net>
Date:   Sat, 18 Jul 2026 21:22:46 +0200

db: add transaction listing queries

Diffstat:
Mpackages/taler-wallet-core/src/dbtx-indexeddb.ts | 71+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-wallet-core/src/dbtx.ts | 58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-wallet-core/src/transactions.ts | 66+++++++++++++++++++-----------------------------------------------
3 files changed, 148 insertions(+), 47 deletions(-)

diff --git a/packages/taler-wallet-core/src/dbtx-indexeddb.ts b/packages/taler-wallet-core/src/dbtx-indexeddb.ts @@ -46,6 +46,7 @@ import { WalletToken, WalletDenomination, WalletTransactionMeta, + DbPreciseTimestamp, WalletOperationRetry, WalletContractTerms, DenominationVerificationStatus, @@ -240,6 +241,76 @@ export class IdbWalletTransaction implements WalletDbTransaction { await tx.transactionsMeta.delete(transactionId); } + async getTransactionMeta( + transactionId: string, + ): Promise<WalletTransactionMeta | undefined> { + const tx = this.tx; + return await tx.transactionsMeta.get(transactionId); + } + + async getTransactionMetaAtTimestamp( + timestamp: DbPreciseTimestamp, + ): Promise<WalletTransactionMeta | undefined> { + const tx = this.tx; + return await tx.transactionsMeta.indexes.byTimestamp.get(timestamp); + } + + async getTransactionMetaBefore( + timestamp: DbPreciseTimestamp, + ): Promise<WalletTransactionMeta | undefined> { + const tx = this.tx; + // Slow, but only reached when a pagination offset transaction was deleted. + const recs = await tx.transactionsMeta.indexes.byTimestamp.getAll( + GlobalIDB.KeyRange.upperBound(timestamp, false), + ); + if (recs.length > 0) { + return recs[recs.length - 1]; + } + return undefined; + } + + async getTransactionMetaAfter( + timestamp: DbPreciseTimestamp, + ): Promise<WalletTransactionMeta | undefined> { + const tx = this.tx; + const recs = await tx.transactionsMeta.indexes.byTimestamp.getAll( + GlobalIDB.KeyRange.lowerBound(timestamp, false), + 1, + ); + return recs[0]; + } + + async listTransactionMetaByTimestamp(req: { + afterTimestamp?: DbPreciseTimestamp; + limit?: number; + }): Promise<WalletTransactionMeta[]> { + const tx = this.tx; + const range = + req.afterTimestamp != null + ? GlobalIDB.KeyRange.lowerBound(req.afterTimestamp, true) + : undefined; + return await tx.transactionsMeta.indexes.byTimestamp.getAll( + range, + req.limit, + ); + } + + async listTransactionMetaByStatus(req: { + onlyActive: boolean; + }): Promise<WalletTransactionMeta[]> { + const tx = this.tx; + const range = req.onlyActive ? getActiveKeyRange() : undefined; + return await tx.transactionsMeta.indexes.byStatus.getAll(range); + } + + async deleteAllTransactionMeta(): Promise<void> { + const tx = this.tx; + const all = await tx.transactionsMeta.getAll(); + for (const rec of all) { + await tx.transactionsMeta.delete(rec.transactionId); + } + } + async getOperationRetry( taskId: string, ): Promise<WalletOperationRetry | undefined> { diff --git a/packages/taler-wallet-core/src/dbtx.ts b/packages/taler-wallet-core/src/dbtx.ts @@ -52,6 +52,7 @@ import { WalletToken, WalletDenomination, WalletTransactionMeta, + DbPreciseTimestamp, WalletOperationRetry, WalletContractTerms, DenominationVerificationStatus, @@ -158,6 +159,63 @@ export interface WalletDbTransaction { deleteTransactionMeta(transactionId: string): Promise<void>; /** + * Get the transaction metadata for a transaction. + */ + getTransactionMeta( + transactionId: string, + ): Promise<WalletTransactionMeta | undefined>; + + /** + * Get the transaction metadata at exactly the given timestamp, if any. + */ + getTransactionMetaAtTimestamp( + timestamp: DbPreciseTimestamp, + ): Promise<WalletTransactionMeta | undefined>; + + /** + * Get the latest transaction metadata at or before the given timestamp. + * + * Used to resolve a pagination offset whose transaction has been deleted. + */ + getTransactionMetaBefore( + timestamp: DbPreciseTimestamp, + ): Promise<WalletTransactionMeta | undefined>; + + /** + * Get the earliest transaction metadata at or after the given timestamp. + */ + getTransactionMetaAfter( + timestamp: DbPreciseTimestamp, + ): Promise<WalletTransactionMeta | undefined>; + + /** + * List transaction metadata ordered by timestamp ascending. + * + * If afterTimestamp is given, only records strictly after it are returned. + * If limit is given, at most that many records are returned. + */ + listTransactionMetaByTimestamp(req: { + afterTimestamp?: DbPreciseTimestamp; + limit?: number; + }): Promise<WalletTransactionMeta[]>; + + /** + * List all transaction metadata, optionally restricted to transactions in a + * non-final ("active") state. + */ + listTransactionMetaByStatus(req: { + onlyActive: boolean; + }): Promise<WalletTransactionMeta[]>; + + /** + * Delete all transaction metadata. + * + * Used when re-materializing the transactionsMeta view from the underlying + * transaction stores. + */ + deleteAllTransactionMeta(): Promise<void>; + + /** * Get the retry state of a task, if the task has been retried before. */ getOperationRetry(taskId: string): Promise<WalletOperationRetry | undefined>; diff --git a/packages/taler-wallet-core/src/transactions.ts b/packages/taler-wallet-core/src/transactions.ts @@ -18,11 +18,6 @@ * Imports. */ import { - BridgeIDBKeyRange, - GlobalIDB, - IDBKeyRange, -} from "@gnu-taler/idb-bridge"; -import { AbsoluteTime, Amounts, assertUnreachable, @@ -314,7 +309,7 @@ async function findOffsetTransaction( let forwards = req?.limit == null || req.limit >= 0; let closestTimestamp: DbPreciseTimestamp | undefined = undefined; if (req?.offsetTransactionId) { - const res = await tx.transactionsMeta.get(req.offsetTransactionId); + const res = await tx.wtx.getTransactionMeta(req.offsetTransactionId); if (res) { return res; } @@ -327,7 +322,7 @@ async function findOffsetTransaction( } } else if (req?.offsetTimestamp) { const dbStamp = timestampPreciseToDb(req.offsetTimestamp); - const res = await tx.transactionsMeta.indexes.byTimestamp.get(dbStamp); + const res = await tx.wtx.getTransactionMetaAtTimestamp(dbStamp); if (res) { return res; } @@ -345,21 +340,10 @@ async function findOffsetTransaction( // We don't want to skip transactions in pagination, // so get the transaction before the timestamp - // Slow query, but should not happen often! - const recs = await tx.transactionsMeta.indexes.byTimestamp.getAll( - BridgeIDBKeyRange.upperBound(closestTimestamp, false), - ); - if (recs.length > 0) { - return recs[recs.length - 1]; - } - return undefined; + return await tx.wtx.getTransactionMetaBefore(closestTimestamp); } else { // Likewise, get the transaction after the timestamp - const recs = await tx.transactionsMeta.indexes.byTimestamp.getAll( - BridgeIDBKeyRange.lowerBound(closestTimestamp, false), - 1, - ); - return recs[0]; + return await tx.wtx.getTransactionMetaAfter(closestTimestamp); } } @@ -387,13 +371,15 @@ export async function getTransactionsV2( if (limit == null && offsetMtx == null) { // Fast path for returning *everything* that matches the filter. // FIXME: We could use the DB for filtering here - const res = await tx.transactionsMeta.indexes.byStatus.getAll(); + const res = await tx.wtx.listTransactionMetaByStatus({ + onlyActive: false, + }); await addFiltered(wex, tx, transactionsRequest, resultTransactions, res); } else if (!forwards) { // Descending, backwards request. // Slow implementation. Doing it properly would require using cursors, // which are also slow in IndexedDB. - const res = await tx.transactionsMeta.indexes.byTimestamp.getAll(); + const res = await tx.wtx.listTransactionMetaByTimestamp({}); res.reverse(); let start: number; if (offsetMtx != null) { @@ -415,15 +401,13 @@ export async function getTransactionsV2( } } else { // Forward request - let query: BridgeIDBKeyRange | undefined = undefined; - if (offsetMtx != null) { - query = GlobalIDB.KeyRange.lowerBound(offsetMtx.timestamp, true); - } + let afterTimestamp: DbPreciseTimestamp | undefined = + offsetMtx != null ? offsetMtx.timestamp : undefined; while (true) { - const res = await tx.transactionsMeta.indexes.byTimestamp.getAll( - query, + const res = await tx.wtx.listTransactionMetaByTimestamp({ + afterTimestamp, limit, - ); + }); if (res.length === 0) { break; } @@ -438,10 +422,7 @@ export async function getTransactionsV2( break; } // Continue after last result - query = BridgeIDBKeyRange.lowerBound( - res[res.length - 1].timestamp, - true, - ); + afterTimestamp = res[res.length - 1].timestamp; } } }); @@ -462,18 +443,12 @@ export async function getTransactions( ): Promise<TransactionsResponse> { const transactions: Transaction[] = []; - let keyRange: IDBKeyRange | undefined = undefined; - - if (transactionsRequest?.filterByState === "nonfinal") { - keyRange = GlobalIDB.KeyRange.bound( - OPERATION_STATUS_NONFINAL_FIRST, - OPERATION_STATUS_NONFINAL_LAST, - ); - } + const onlyActive = transactionsRequest?.filterByState === "nonfinal"; await wex.runLegacyWalletDbTx(async (tx) => { - const allMetaTransactions = - await tx.transactionsMeta.indexes.byStatus.getAll(keyRange); + const allMetaTransactions = await tx.wtx.listTransactionMetaByStatus({ + onlyActive, + }); for (const metaTx of allMetaTransactions) { if ( shouldSkipCurrency( @@ -567,10 +542,7 @@ export async function rematerializeTransactions( ): Promise<void> { logger.trace("re-materializing transactions"); - const allTxMeta = await tx.transactionsMeta.getAll(); - for (const txMeta of allTxMeta) { - await tx.transactionsMeta.delete(txMeta.transactionId); - } + await tx.wtx.deleteAllTransactionMeta(); await tx.peerPushDebit.iter().forEachAsync(async (x) => { const ctx = new PeerPushDebitTransactionContext(wex, x.pursePub);