taler-typescript-core

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

commit b178f297ebb84eee9675a2007363a636fcc82d29
parent 45ae39c9d2e1d24b7aceb37d4cd511591e2c9b78
Author: Florian Dold <florian@dold.me>
Date:   Tue,  3 Feb 2026 16:49:52 +0100

more trace logging, use env variable to choose DB backend

Diffstat:
Mpackages/idb-bridge/src/MemoryBackend.ts | 67+++++++++++++++++++++++++++++++++----------------------------------
Mpackages/idb-bridge/src/testingdb.ts | 32+++++++++++++++++++-------------
Mpackages/taler-harness/src/harness/harness.ts | 1+
Mpackages/taler-harness/src/integrationtests/test-exchange-deposit.ts | 5+++++
Mpackages/taler-wallet-core/src/dbless.ts | 8+++++++-
Mpackages/taler-wallet-core/src/denomSelection.ts | 15+++++++++++----
Mpackages/taler-wallet-core/src/host-impl.node.ts | 2+-
Mpackages/taler-wallet-core/src/withdraw.ts | 25+++++++++++++++++++++++--
8 files changed, 100 insertions(+), 55 deletions(-)

diff --git a/packages/idb-bridge/src/MemoryBackend.ts b/packages/idb-bridge/src/MemoryBackend.ts @@ -224,6 +224,11 @@ export interface AccessStats { readItemsPerStore: Record<string, number>; } +function logtrace(m: string, ...args: any[]): void { + // Use stderr + console.error(m, ...args); +} + /** * Primitive in-memory backend. * @@ -385,7 +390,7 @@ export class MemoryBackend implements Backend { * Only exports data that has been committed. */ exportDump(): MemoryBackendDump { - this.enableTracing && console.log("exporting dump"); + this.enableTracing && logtrace("exporting dump"); const dbDumps: { [name: string]: DatabaseDump } = {}; for (const dbName of Object.keys(this.databases)) { const db = this.databases[dbName]; @@ -413,7 +418,7 @@ export class MemoryBackend implements Backend { async getDatabases(): Promise<{ name: string; version: number }[]> { if (this.enableTracing) { - console.log("TRACING: getDatabase"); + logtrace("TRACING: getDatabase"); } const dbList = []; for (const name in this.databases) { @@ -427,7 +432,7 @@ export class MemoryBackend implements Backend { async deleteDatabase(name: string): Promise<void> { if (this.enableTracing) { - console.log(`TRACING: deleteDatabase(${name})`); + logtrace(`TRACING: deleteDatabase(${name})`); } const myDb = this.databases[name]; if (!myDb) { @@ -447,7 +452,7 @@ export class MemoryBackend implements Backend { async connectDatabase(name: string): Promise<ConnectResult> { if (this.enableTracing) { - console.log(`TRACING: connectDatabase(${name})`); + logtrace(`TRACING: connectDatabase(${name})`); } const connectionId = this.connectionIdCounter++; const connectionCookie = `connection-${connectionId}`; @@ -498,7 +503,7 @@ export class MemoryBackend implements Backend { ): Promise<DatabaseTransaction> { const transactionCookie = `tx-${this.transactionIdCounter++}`; if (this.enableTracing) { - console.log(`TRACING: beginTransaction ${transactionCookie}`); + logtrace(`TRACING: beginTransaction ${transactionCookie}`); } const myConn = this.connections[conn.connectionCookie]; if (!myConn) { @@ -511,7 +516,7 @@ export class MemoryBackend implements Backend { while (myDb.txLevel !== TransactionLevel.None) { if (this.enableTracing) { - console.log(`TRACING: beginTransaction -- waiting for others to close`); + logtrace(`TRACING: beginTransaction -- waiting for others to close`); } await this.transactionDoneCond.wait(); } @@ -544,7 +549,7 @@ export class MemoryBackend implements Backend { newVersion: number, ): Promise<DatabaseTransaction> { if (this.enableTracing) { - console.log(`TRACING: enterVersionChange`); + logtrace(`TRACING: enterVersionChange`); } const transactionCookie = `tx-vc-${this.transactionIdCounter++}`; const myConn = this.connections[conn.connectionCookie]; @@ -574,7 +579,7 @@ export class MemoryBackend implements Backend { async close(conn: DatabaseConnection): Promise<void> { if (this.enableTracing) { - console.log(`TRACING: close (${conn.connectionCookie})`); + logtrace(`TRACING: close (${conn.connectionCookie})`); } const myConn = this.connections[conn.connectionCookie]; if (!myConn) { @@ -608,7 +613,7 @@ export class MemoryBackend implements Backend { newName: string, ): void { if (this.enableTracing) { - console.log(`TRACING: renameIndex(?, ${oldName}, ${newName})`); + logtrace(`TRACING: renameIndex(?, ${oldName}, ${newName})`); } const myConn = this.requireConnectionFromTransaction(btx); const db = this.databases[myConn.dbName]; @@ -647,7 +652,7 @@ export class MemoryBackend implements Backend { indexName: string, ): void { if (this.enableTracing) { - console.log(`TRACING: deleteIndex(${indexName})`); + logtrace(`TRACING: deleteIndex(${indexName})`); } const myConn = this.requireConnectionFromTransaction(btx); const db = this.databases[myConn.dbName]; @@ -676,7 +681,7 @@ export class MemoryBackend implements Backend { deleteObjectStore(btx: DatabaseTransaction, name: string): void { if (this.enableTracing) { - console.log( + logtrace( `TRACING: deleteObjectStore(${name}) in ${btx.transactionCookie}`, ); } @@ -716,7 +721,7 @@ export class MemoryBackend implements Backend { newName: string, ): void { if (this.enableTracing) { - console.log(`TRACING: renameObjectStore(?, ${oldName}, ${newName})`); + logtrace(`TRACING: renameObjectStore(?, ${oldName}, ${newName})`); } const myConn = this.requireConnectionFromTransaction(btx); @@ -755,9 +760,7 @@ export class MemoryBackend implements Backend { autoIncrement: boolean, ): void { if (this.enableTracing) { - console.log( - `TRACING: createObjectStore(${btx.transactionCookie}, ${name})`, - ); + logtrace(`TRACING: createObjectStore(${btx.transactionCookie}, ${name})`); } const myConn = this.requireConnectionFromTransaction(btx); const db = this.databases[myConn.dbName]; @@ -799,7 +802,7 @@ export class MemoryBackend implements Backend { unique: boolean, ): void { if (this.enableTracing) { - console.log(`TRACING: createIndex(${indexName})`); + logtrace(`TRACING: createIndex(${indexName})`); } const myConn = this.requireConnectionFromTransaction(btx); const db = this.databases[myConn.dbName]; @@ -899,7 +902,7 @@ export class MemoryBackend implements Backend { range: IDBKeyRange, ): Promise<void> { if (this.enableTracing) { - console.log(`TRACING: deleteRecord from store ${objectStoreName}`); + logtrace(`TRACING: deleteRecord from store ${objectStoreName}`); } const myConn = this.requireConnectionFromTransaction(btx); const db = this.databases[myConn.dbName]; @@ -994,7 +997,7 @@ export class MemoryBackend implements Backend { throw Error("index referenced by object store does not exist"); } this.enableTracing && - console.log( + logtrace( `deleting from index ${indexName} for object store ${objectStoreName}`, ); const indexProperties = @@ -1022,9 +1025,7 @@ export class MemoryBackend implements Backend { indexProperties: IndexProperties, ): void { if (this.enableTracing) { - console.log( - `deleteFromIndex(${index.modifiedName || index.originalName})`, - ); + logtrace(`deleteFromIndex(${index.modifiedName || index.originalName})`); } if (value === undefined || value === null) { throw Error("cannot delete null/undefined value from index"); @@ -1059,8 +1060,8 @@ export class MemoryBackend implements Backend { req: ObjectStoreGetQuery, ): Promise<RecordGetResponse> { if (this.enableTracing) { - console.log(`TRACING: getObjectStoreRecords`); - console.log("query", req); + logtrace(`TRACING: getObjectStoreRecords`); + logtrace("query", req); } const myConn = this.requireConnectionFromTransaction(btx); const db = this.databases[myConn.dbName]; @@ -1128,7 +1129,7 @@ export class MemoryBackend implements Backend { (this.accessStats.readItemsPerStore[k] ?? 0) + resp.count; } if (this.enableTracing) { - console.log(`TRACING: getRecords got ${resp.count} results`); + logtrace(`TRACING: getRecords got ${resp.count} results`); } return resp; } @@ -1138,8 +1139,8 @@ export class MemoryBackend implements Backend { req: IndexGetQuery, ): Promise<RecordGetResponse> { if (this.enableTracing) { - console.log(`TRACING: getIndexRecords`); - console.log("query", req); + logtrace(`TRACING: getIndexRecords`); + logtrace("query", req); } const myConn = this.requireConnectionFromTransaction(btx); const db = this.databases[myConn.dbName]; @@ -1216,7 +1217,7 @@ export class MemoryBackend implements Backend { (this.accessStats.readItemsPerIndex[k] ?? 0) + resp.count; } if (this.enableTracing) { - console.log(`TRACING: getIndexRecords got ${resp.count} results`); + logtrace(`TRACING: getIndexRecords got ${resp.count} results`); } return resp; } @@ -1226,8 +1227,8 @@ export class MemoryBackend implements Backend { storeReq: RecordStoreRequest, ): Promise<RecordStoreResponse> { if (this.enableTracing) { - console.log(`TRACING: storeRecord`); - console.log( + logtrace(`TRACING: storeRecord`); + logtrace( `key ${storeReq.key}, record ${JSON.stringify( structuredEncapsulate(storeReq.value), )}`, @@ -1397,9 +1398,7 @@ export class MemoryBackend implements Backend { indexProperties: IndexProperties, ): void { if (this.enableTracing) { - console.log( - `insertIntoIndex(${index.modifiedName || index.originalName})`, - ); + logtrace(`insertIntoIndex(${index.modifiedName || index.originalName})`); } let indexData = index.modifiedData || index.originalData; let indexKeys; @@ -1451,7 +1450,7 @@ export class MemoryBackend implements Backend { async rollback(btx: DatabaseTransaction): Promise<void> { if (this.enableTracing) { - console.log(`TRACING: rollback`); + logtrace(`TRACING: rollback`); } const myConn = this.connectionsByTransaction[btx.transactionCookie]; if (!myConn) { @@ -1491,7 +1490,7 @@ export class MemoryBackend implements Backend { async commit(btx: DatabaseTransaction): Promise<void> { if (this.enableTracing) { - console.log(`TRACING: commit`); + logtrace(`TRACING: commit`); } const myConn = this.requireConnectionFromTransaction(btx); const db = this.databases[myConn.dbName]; diff --git a/packages/idb-bridge/src/testingdb.ts b/packages/idb-bridge/src/testingdb.ts @@ -13,7 +13,9 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ +import { MemoryBackend } from "./MemoryBackend.js"; import { createSqliteBackend } from "./SqliteBackend.js"; +import { Backend } from "./backend-interface.js"; import { BridgeIDBFactory } from "./bridge-idb.js"; import { IDBFactory } from "./idbtypes.js"; import { createNodeHelperSqlite3Impl } from "./node-helper-sqlite3-impl.js"; @@ -21,21 +23,25 @@ import { createNodeHelperSqlite3Impl } from "./node-helper-sqlite3-impl.js"; let idbFactory: IDBFactory | undefined = undefined; export async function initTestIndexedDB(): Promise<void> { - // const backend = new MemoryBackend(); - // backend.enableTracing = true; - - const sqlite3Impl = await createNodeHelperSqlite3Impl({ - enableTracing: false, - }); - - // const sqlite3Impl = await createNodeBetterSqlite3Impl(); - - const backend = await createSqliteBackend(sqlite3Impl, { - filename: ":memory:", - }); + let backend: Backend; + if (process.env.IDB_TEST_BACKEND == "memory") { + const memBackend = new MemoryBackend(); + backend = memBackend; + // backend.enableTracing = true; + memBackend.enableTracing = false; + } else { + const sqlite3Impl = await createNodeHelperSqlite3Impl({ + enableTracing: false, + }); + + // const sqlite3Impl = await createNodeBetterSqlite3Impl(); + + backend = await createSqliteBackend(sqlite3Impl, { + filename: ":memory:", + }); + } idbFactory = new BridgeIDBFactory(backend); - backend.enableTracing = false; BridgeIDBFactory.enableTracing = false; } diff --git a/packages/taler-harness/src/harness/harness.ts b/packages/taler-harness/src/harness/harness.ts @@ -2749,6 +2749,7 @@ export class WalletCli { self.dbfile }' api '${op}' ${shellWrap(JSON.stringify(payload))}`; const resp = await sh(self.globalTestState, logName, command); + logger.trace(`command: ${j2s(command)}`); logger.info("--- wallet core response ---"); logger.info(resp); logger.info("--- end of response ---"); diff --git a/packages/taler-harness/src/integrationtests/test-exchange-deposit.ts b/packages/taler-harness/src/integrationtests/test-exchange-deposit.ts @@ -22,6 +22,7 @@ import { encodeCrock, getRandomBytes, j2s, + Logger, TalerError, } from "@gnu-taler/taler-util"; import { createPlatformHttpLib } from "@gnu-taler/taler-util/http"; @@ -40,6 +41,8 @@ import { import { createSimpleTestkudosEnvironmentV2 } from "../harness/environments.js"; import { GlobalTestState } from "../harness/harness.js"; +const logger = new Logger("test-exchange-deposit.ts"); + /** * Run test for basic, bank-integrated withdrawal and payment. */ @@ -85,6 +88,8 @@ export async function runExchangeDepositTest(t: GlobalTestState) { {}, ); + logger.info(`found denomination: ${j2s(d1)}`); + const coin = await withdrawCoin({ http, cryptoApi, diff --git a/packages/taler-wallet-core/src/dbless.ts b/packages/taler-wallet-core/src/dbless.ts @@ -59,7 +59,6 @@ import { readSuccessResponseJsonOrThrow, } from "@gnu-taler/taler-util/http"; import { TalerCryptoInterface } from "./crypto/cryptoImplementation.js"; -import { DenominationRecord } from "./db.js"; import { ExchangeInfo, downloadExchangeInfo } from "./exchanges.js"; import { getBankStatusUrl, getBankWithdrawalInfo } from "./withdraw.js"; @@ -191,6 +190,10 @@ export async function withdrawCoin(args: { export interface FindDenomOptions {} +/** + * Find a denomination offered by the exchange + * that matches exactly the requested value. + */ export function findDenomOrThrow( exchangeInfo: ExchangeInfo, amount: AmountString, @@ -206,6 +209,9 @@ export function findDenomOrThrow( } case "RSA": case "RSA+age_restricted": { + if (Amounts.cmp(denomFamily.value, amount) != 0) { + continue; + } if (denomFamily.cipher === "RSA+age_restricted") { ageMask = denomFamily.age_mask; } diff --git a/packages/taler-wallet-core/src/denomSelection.ts b/packages/taler-wallet-core/src/denomSelection.ts @@ -45,7 +45,8 @@ const logger = new Logger("denomSelection.ts"); * * @param amountAvailable available withdrawal amount * @param denoms list of denominations that can be used, - * must be validated to be withdrawable + * must be validated to be withdrawable and sorted + * by descending denom value * @param opts extra options */ export function selectWithdrawalDenominations( @@ -65,16 +66,22 @@ export function selectWithdrawalDenominations( let earliestDepositExpiration: AbsoluteTime | undefined; let hasDenomWithAgeRestriction = false; - for (const d of denoms) { + // Precondition check + for (let i = 0; i < denoms.length; i++) { + const d = denoms[i]; if (!isWithdrawableDenom(d)) { throw Error( "non-withdrawable denom passed to selectWithdrawalDenominations", ); } + if ( + i < denoms.length - 1 && + Amounts.cmp(denoms[i].value, denoms[i + 1].value) < 0 + ) { + throw Error("denoms must be sorted by descending value"); + } } - denoms.sort((d1, d2) => Amounts.cmp(d2.value, d1.value)); - if (logger.shouldLogTrace()) { logger.trace( `selecting withdrawal denoms for ${Amounts.stringify(amountAvailable)}`, diff --git a/packages/taler-wallet-core/src/host-impl.node.ts b/packages/taler-wallet-core/src/host-impl.node.ts @@ -52,7 +52,7 @@ async function makeFileDb( args: DefaultNodeWalletArgs = {}, ): Promise<WalletDatabaseImplementation> { const myBackend = new MemoryBackend(); - myBackend.enableTracing = false; + myBackend.enableTracing = true; const storagePath = args.persistentStoragePath; if (storagePath) { try { diff --git a/packages/taler-wallet-core/src/withdraw.ts b/packages/taler-wallet-core/src/withdraw.ts @@ -1388,7 +1388,7 @@ async function getWithdrawableDenoms( * isn't validated yet. */ export async function getWithdrawableDenomsTx( - wex: WalletExecutionContext, + _wex: WalletExecutionContext, tx: WalletDbReadOnlyTransaction<["denominations", "denominationFamilies"]>, exchangeBaseUrl: string, currency: string, @@ -1399,6 +1399,13 @@ export async function getWithdrawableDenomsTx( await tx.denominationFamilies.indexes.byExchangeBaseUrl.getAll( exchangeBaseUrl, ); + if (logger.shouldLogTrace()) { + const maxStr = maxAmount ? Amounts.stringify(maxAmount) : "<unknown>"; + logger.trace( + `getting withdrawable denoms for ${currency} / ${maxStr} at ${exchangeBaseUrl}`, + ); + logger.trace(`have ${allFamilies.length} families`); + } const relevantDenoms: DenominationRecord[] = []; for (const fam of allFamilies) { const famCurrency = Amounts.currencyOf(fam.familyParams.value); @@ -1415,6 +1422,9 @@ export async function getWithdrawableDenomsTx( // Denom value too high, no point in selecting. continue; } + if (logger.shouldLogTrace()) { + logger.trace(`finding representative for denom family: ${j2s(fam)}`); + } const fpSerial = fam.denominationFamilySerial; checkDbInvariant(typeof fpSerial === "number", "denominationFamilySerial"); // Now we need to find a representative denom for the family. @@ -1433,15 +1443,18 @@ export async function getWithdrawableDenomsTx( dr0.value.stampExpireWithdraw >= dbNow) ) ) { + logger.trace(`continuing cursor past ${j2s([fpSerial, dbNow])}`); denomCursor.continue([fpSerial, dbNow]); } while (1) { const dr = await denomCursor.current(); if (!dr.hasValue) { + logger.trace(`cursor exhausted`); break; } if (dr.value.denominationFamilySerial != fpSerial) { // Cursor went to next serial already, we need to stop. + logger.trace(`cursor past target`); break; } if (isCandidateWithdrawableDenomRec(dr.value)) { @@ -1449,12 +1462,20 @@ export async function getWithdrawableDenomsTx( break; } + logger.trace(`continuing cursor to next record`); + denomCursor.continue(); } if (denom) { relevantDenoms.push(denom); + } else { + logger.warn(`no representative denom for family with serial=${fpSerial}`); } } + relevantDenoms.sort((d1, d2) => Amounts.cmp(d2.value, d1.value)); + if (logger.shouldLogTrace()) { + logger.trace(`relevant denoms: ${j2s(relevantDenoms)}`); + } return relevantDenoms; } @@ -2280,7 +2301,7 @@ async function getWithdrawalCandidateDenoms( ): Promise<DenominationRecord[]> { await updateWithdrawalDenomsForExchange(wex, exchangeBaseUrl); return await wex.db.runAllStoresReadWriteTx({}, async (tx) => { - return getWithdrawableDenomsTx( + return await getWithdrawableDenomsTx( wex, tx, exchangeBaseUrl,