taler-typescript-core

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

commit b381cce803bf456f83290b06ead4e33dd8e58744
parent 90f41f416de17a0ee84ea50fa7f4ae9b94b7a488
Author: Florian Dold <dold@taler.net>
Date:   Sat, 18 Jul 2026 23:47:30 +0200

db: add exchange details and sign key accessors

Diffstat:
Mpackages/taler-wallet-core/src/db-common.ts | 19+++++++++++++++++++
Mpackages/taler-wallet-core/src/db-indexeddb.ts | 20++------------------
Mpackages/taler-wallet-core/src/dbtx-indexeddb.ts | 76++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-wallet-core/src/dbtx.ts | 47+++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-wallet-core/src/exchanges.ts | 49+++++++++++++++++--------------------------------
5 files changed, 161 insertions(+), 50 deletions(-)

diff --git a/packages/taler-wallet-core/src/db-common.ts b/packages/taler-wallet-core/src/db-common.ts @@ -48,6 +48,8 @@ import { AccountLimit, ZeroLimitedOperation, DenomLossEventType, + EddsaPublicKeyString, + EddsaSignatureString, CoinStatus, AgeCommitmentProof, TransactionIdStr, @@ -1328,6 +1330,23 @@ export interface WalletDenomLossEvent { exchangeBaseUrl: string; } +/** + * Denomination record as stored in the wallet's database. + */ + +export interface WalletExchangeSignkeys { + stampStart: DbProtocolTimestamp; + stampExpire: DbProtocolTimestamp; + stampEnd: DbProtocolTimestamp; + signkeyPub: EddsaPublicKeyString; + masterSig: EddsaSignatureString; + + /** + * Exchange details that thiis signkeys record belongs to. + */ + exchangeDetailsRowId: number; +} + 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 @@ -143,6 +143,7 @@ import { WalletExchangeDetails, WalletSlate, WalletDenomLossEvent, + WalletExchangeSignkeys, WalletToken, TokenFamilyInfo, WalletRefundGroup, @@ -273,23 +274,6 @@ export interface DenominationFamilyRecord { // * familyParamsHash } -/** - * Denomination record as stored in the wallet's database. - */ - -export interface ExchangeSignkeysRecord { - stampStart: DbProtocolTimestamp; - stampExpire: DbProtocolTimestamp; - stampEnd: DbProtocolTimestamp; - signkeyPub: EddsaPublicKeyString; - masterSig: EddsaSignatureString; - - /** - * Exchange details that thiis signkeys record belongs to. - */ - exchangeDetailsRowId: number; -} - // FIXME: Should these be numeric codes? export type KycUserType = "individual" | "business"; @@ -944,7 +928,7 @@ export const WalletIndexedDbStoresV1 = { ), exchangeSignKeys: describeStore( "exchangeSignKeys", - describeContents<ExchangeSignkeysRecord>({ + describeContents<WalletExchangeSignkeys>({ keyPath: ["exchangeDetailsRowId", "signkeyPub"], }), { diff --git a/packages/taler-wallet-core/src/dbtx-indexeddb.ts b/packages/taler-wallet-core/src/dbtx-indexeddb.ts @@ -73,6 +73,7 @@ import { WalletTombstone, WalletExchangeEntry, WalletDenomLossEvent, + WalletExchangeSignkeys, WalletExchangeDetails, } from "./db-common.js"; import type { @@ -349,6 +350,81 @@ export class IdbWalletTransaction implements WalletDbTransaction { }); } + async getExchangeDetailsByPointer( + exchangeBaseUrl: string, + currency: string, + masterPublicKey: string, + ): Promise<WalletExchangeDetails | undefined> { + const tx = this.tx; + return await tx.exchangeDetails.indexes.byPointer.get([ + exchangeBaseUrl, + currency, + masterPublicKey, + ]); + } + + async getExchangeDetailsByBaseUrl( + exchangeBaseUrl: string, + ): Promise<WalletExchangeDetails | undefined> { + const tx = this.tx; + return await tx.exchangeDetails.indexes.byExchangeBaseUrl.get( + exchangeBaseUrl, + ); + } + + async listExchangeDetailsByBaseUrl( + exchangeBaseUrl: string, + ): Promise<WalletExchangeDetails[]> { + const tx = this.tx; + return await tx.exchangeDetails.indexes.byExchangeBaseUrl.getAll( + exchangeBaseUrl, + ); + } + + async listAllExchangeDetails(): Promise<WalletExchangeDetails[]> { + const tx = this.tx; + return await tx.exchangeDetails.indexes.byExchangeBaseUrl.getAll(); + } + + async upsertExchangeDetails(rec: WalletExchangeDetails): Promise<number> { + const tx = this.tx; + const res = await tx.exchangeDetails.put(rec); + checkDbInvariant( + typeof res.key === "number", + "exchange details row id must be a number", + ); + return res.key; + } + + async deleteExchangeDetails(rowId: number): Promise<void> { + const tx = this.tx; + await tx.exchangeDetails.delete(rowId); + } + + async getExchangeSignKeysByDetailsRowId( + exchangeDetailsRowId: number, + ): Promise<WalletExchangeSignkeys[]> { + const tx = this.tx; + // byExchangeDetailsRowId has an array keyPath (["exchangeDetailsRowId"]), + // so its keys are single-element arrays and a bare number matches nothing. + return await tx.exchangeSignKeys.indexes.byExchangeDetailsRowId.getAll([ + exchangeDetailsRowId, + ]); + } + + async upsertExchangeSignKey(rec: WalletExchangeSignkeys): Promise<void> { + const tx = this.tx; + await tx.exchangeSignKeys.put(rec); + } + + async deleteExchangeSignKey( + exchangeDetailsRowId: number, + signkeyPub: string, + ): Promise<void> { + const tx = this.tx; + await tx.exchangeSignKeys.delete([exchangeDetailsRowId, signkeyPub]); + } + async getDenomLossEvent( denomLossEventId: string, ): Promise<WalletDenomLossEvent | undefined> { diff --git a/packages/taler-wallet-core/src/dbtx.ts b/packages/taler-wallet-core/src/dbtx.ts @@ -73,6 +73,7 @@ import { WalletTombstone, WalletExchangeEntry, WalletDenomLossEvent, + WalletExchangeSignkeys, WalletExchangeDetails, } from "./db-common.js"; export interface GetCurrencyInfoDbResult { @@ -240,6 +241,52 @@ export interface WalletDbTransaction { /** * Get an exchange entry by its base URL. */ + /** + * Get exchange details by the (baseUrl, currency, masterPub) pointer. + */ + getExchangeDetailsByPointer( + exchangeBaseUrl: string, + currency: string, + masterPublicKey: string, + ): Promise<WalletExchangeDetails | undefined>; + + /** + * Get the exchange details record for a base URL, if there is exactly one. + */ + getExchangeDetailsByBaseUrl( + exchangeBaseUrl: string, + ): Promise<WalletExchangeDetails | undefined>; + + /** + * Get every exchange details record for a base URL. + */ + listExchangeDetailsByBaseUrl( + exchangeBaseUrl: string, + ): Promise<WalletExchangeDetails[]>; + + listAllExchangeDetails(): Promise<WalletExchangeDetails[]>; + + /** + * Create or update an exchange details record, returning its row id. + * + * The exchangeDetails store is auto-incrementing on rowId, and callers need + * the generated id to attach sign keys to it. + */ + upsertExchangeDetails(rec: WalletExchangeDetails): Promise<number>; + + deleteExchangeDetails(rowId: number): Promise<void>; + + getExchangeSignKeysByDetailsRowId( + exchangeDetailsRowId: number, + ): Promise<WalletExchangeSignkeys[]>; + + upsertExchangeSignKey(rec: WalletExchangeSignkeys): Promise<void>; + + deleteExchangeSignKey( + exchangeDetailsRowId: number, + signkeyPub: string, + ): Promise<void>; + getDenomLossEvent( denomLossEventId: string, ): Promise<WalletDenomLossEvent | undefined>; diff --git a/packages/taler-wallet-core/src/exchanges.ts b/packages/taler-wallet-core/src/exchanges.ts @@ -246,11 +246,7 @@ async function getExchangeRecordsInternal( return; } const { currency, masterPublicKey } = dp; - const details = await tx.exchangeDetails.indexes.byPointer.get([ - r.baseUrl, - currency, - masterPublicKey, - ]); + const details = await tx.wtx.getExchangeDetailsByPointer(r.baseUrl, currency, masterPublicKey); if (!details) { logger.warn( `no exchange details with pointer ${j2s(dp)} for ${exchangeBaseUrl}`, @@ -2019,16 +2015,12 @@ export async function updateExchangeFromUrlHandler( }); } - const drRowId = await tx.exchangeDetails.put(newDetails); - checkDbInvariant( - typeof drRowId.key === "number", - "exchange details key is not a number", - ); + const drRowId = await tx.wtx.upsertExchangeDetails(newDetails); for (const sk of keysInfo.signkeys) { // FIXME: validate signing keys before inserting them - await tx.exchangeSignKeys.put({ - exchangeDetailsRowId: drRowId.key, + await tx.wtx.upsertExchangeSignKey({ + exchangeDetailsRowId: drRowId, masterSig: sk.master_sig, signkeyPub: sk.key, stampEnd: timestampProtocolToDb(sk.stamp_end), @@ -3161,7 +3153,7 @@ async function purgeExchange( purgeTransactions?: boolean, ): Promise<void> { const exchangeBaseUrl = exchangeRec.baseUrl; - const detRecs = await tx.exchangeDetails.indexes.byExchangeBaseUrl.getAll(); + const detRecs = await tx.wtx.listAllExchangeDetails(); // Remove all exchange detail records for that exchange for (const r of detRecs) { if (r.rowId == null) { @@ -3171,13 +3163,11 @@ async function purgeExchange( if (r.exchangeBaseUrl !== exchangeBaseUrl) { continue; } - await tx.exchangeDetails.delete(r.rowId); + await tx.wtx.deleteExchangeDetails(r.rowId); const signkeyRecs = - await tx.exchangeSignKeys.indexes.byExchangeDetailsRowId.getAll([ - r.rowId, - ]); + await tx.wtx.getExchangeSignKeysByDetailsRowId(r.rowId); for (const rec of signkeyRecs) { - await tx.exchangeSignKeys.delete([r.rowId, rec.signkeyPub]); + await tx.wtx.deleteExchangeSignKey(r.rowId, rec.signkeyPub); } } @@ -3403,11 +3393,7 @@ export async function getExchangeWireFee( const exchangeDetails = await wex.runLegacyWalletDbTx(async (tx) => { const ex = await tx.wtx.getExchange(baseUrl); if (!ex || !ex.detailsPointer) return undefined; - return await tx.exchangeDetails.indexes.byPointer.get([ - baseUrl, - ex.detailsPointer.currency, - ex.detailsPointer.masterPublicKey, - ]); + return await tx.wtx.getExchangeDetailsByPointer(baseUrl, ex.detailsPointer.currency, ex.detailsPointer.masterPublicKey); }); if (!exchangeDetails) { @@ -4233,16 +4219,15 @@ export async function migrateExchange( { const existingNewExchangeDetails = - await tx.exchangeDetails.indexes.byExchangeBaseUrl.get( - req.newExchangeBaseUrl, - ); + await tx.wtx.getExchangeDetailsByBaseUrl(req.newExchangeBaseUrl); if (!existingNewExchangeDetails) { - await tx.exchangeDetails.indexes.byExchangeBaseUrl - .iter(req.oldExchangeBaseUrl) - .forEachAsync(async (rec) => { - rec.exchangeBaseUrl = req.newExchangeBaseUrl; - await tx.exchangeDetails.put(rec); - }); + const oldDetails = await tx.wtx.listExchangeDetailsByBaseUrl( + req.oldExchangeBaseUrl, + ); + for (const rec of oldDetails) { + rec.exchangeBaseUrl = req.newExchangeBaseUrl; + await tx.wtx.upsertExchangeDetails(rec); + } } }