taler-typescript-core

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

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

db: move exchange entry and details types to db-common.ts

Diffstat:
Mpackages/taler-wallet-core/src/common.ts | 12++++++------
Mpackages/taler-wallet-core/src/db-common.ts | 205+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-wallet-core/src/db-indexeddb.ts | 208+++----------------------------------------------------------------------------
Mpackages/taler-wallet-core/src/dbtx-indexeddb.ts | 8++++----
Mpackages/taler-wallet-core/src/dbtx.ts | 19++++++-------------
Mpackages/taler-wallet-core/src/exchanges.ts | 34+++++++++++++++++-----------------
6 files changed, 244 insertions(+), 242 deletions(-)

diff --git a/packages/taler-wallet-core/src/common.ts b/packages/taler-wallet-core/src/common.ts @@ -73,9 +73,9 @@ import { WalletRefreshGroup, WalletWithdrawalGroup, WalletPurchase, + WalletExchangeEntry, } from "./db-common.js"; import { - ExchangeEntryRecord, PeerPullCreditRecord, PeerPullPaymentIncomingRecord, PeerPushCreditRecord, @@ -310,7 +310,7 @@ export enum TombstoneTag { } export function getExchangeTosStatusFromRecord( - exchange: ExchangeEntryRecord, + exchange: WalletExchangeEntry, ): ExchangeTosStatus { if (exchange.tosCurrentEtag == null) { return ExchangeTosStatus.MissingTos; @@ -325,7 +325,7 @@ export function getExchangeTosStatusFromRecord( } export function getExchangeUpdateStatusFromRecord( - r: ExchangeEntryRecord, + r: WalletExchangeEntry, ): ExchangeUpdateStatus { switch (r.updateStatus) { case ExchangeEntryDbUpdateStatus.UnavailableUpdate: @@ -348,7 +348,7 @@ export function getExchangeUpdateStatusFromRecord( } export function getExchangeEntryStatusFromRecord( - r: ExchangeEntryRecord, + r: WalletExchangeEntry, ): ExchangeEntryStatus { switch (r.entryStatus) { case ExchangeEntryDbRecordStatus.Ephemeral: @@ -366,7 +366,7 @@ export function getExchangeEntryStatusFromRecord( * Compute the state of an exchange entry from the DB * record. */ -export function getExchangeState(r: ExchangeEntryRecord): ExchangeEntryState { +export function getExchangeState(r: WalletExchangeEntry): ExchangeEntryState { return { exchangeEntryStatus: getExchangeEntryStatusFromRecord(r), exchangeUpdateStatus: getExchangeUpdateStatusFromRecord(r), @@ -804,7 +804,7 @@ export namespace TaskIdentifiers { export function forWithdrawal(wg: WalletWithdrawalGroup): TaskIdStr { return `${PendingTaskType.Withdraw}:${wg.withdrawalGroupId}` as TaskIdStr; } - export function forExchangeUpdate(exch: ExchangeEntryRecord): TaskIdStr { + export function forExchangeUpdate(exch: WalletExchangeEntry): TaskIdStr { return `${PendingTaskType.ExchangeUpdate}:${encodeURIComponent( exch.baseUrl, )}` as TaskIdStr; diff --git a/packages/taler-wallet-core/src/db-common.ts b/packages/taler-wallet-core/src/db-common.ts @@ -41,6 +41,12 @@ import { HashCodeString, BlindedUniqueDonationIdentifier, SignedTokenEnvelope, + CurrencySpecification, + ExchangeAuditor, + ExchangeGlobalFees, + WireInfo, + AccountLimit, + ZeroLimitedOperation, CoinStatus, AgeCommitmentProof, TransactionIdStr, @@ -1106,6 +1112,205 @@ export interface WalletTombstone { id: string; } +export interface WalletExchangeDetailsPointer { + masterPublicKey: string; + + currency: string; + + /** + * Timestamp when the (masterPublicKey, currency) pointer + * has been updated. + */ + updateClock: DbPreciseTimestamp; +} + +/** + * Exchange record as stored in the wallet's database. + */ +export interface WalletExchangeEntry { + /** + * Base url of the exchange. + */ + baseUrl: string; + + /** + * Currency hint for a preset exchange, relevant + * when we didn't contact a preset exchange yet. + */ + presetCurrencyHint?: string; + + /** + * Currency spec for a preset exchange, relevant + * when we didn't contact a preset exchange yet. + */ + presetCurrencySpec?: CurrencySpecification; + + /** + * Type of the exchange, if it was a preset entry. + */ + presetType?: string; + + /** + * When did we confirm the last withdrawal from this exchange? + * + * Used mostly in the UI to suggest exchanges. + */ + lastWithdrawal?: DbPreciseTimestamp; + + /** + * Pointer to the current exchange details. + * + * Should usually not change. Only changes when the + * exchange advertises a different master public key and/or + * currency. + * + * We could use a rowID here, but having the currency in the + * details pointer lets us do fewer DB queries + */ + detailsPointer: WalletExchangeDetailsPointer | undefined; + + entryStatus: ExchangeEntryDbRecordStatus; + + updateStatus: ExchangeEntryDbUpdateStatus; + + unavailableReason?: TalerErrorDetail; + + /** + * If set to true, the next update to the exchange + * status will request /keys with no-cache headers set. + */ + cachebreakNextUpdate?: boolean; + + /** + * Etag of the current ToS of the exchange. + */ + tosCurrentEtag: string | undefined; + + tosAcceptedEtag: string | undefined; + + tosAcceptedTimestamp: DbPreciseTimestamp | undefined; + + /** + * Last time when the exchange /keys info was updated + * successfully. + */ + lastUpdate: DbPreciseTimestamp | undefined; + + /** + * Next scheduled update for the exchange. + */ + nextUpdateStamp: DbPreciseTimestamp; + + lastKeysEtag: string | undefined; + + /** + * Next time that we should check if coins need to be refreshed. + * + * Updated whenever the exchange's denominations are updated or when + * the refresh check has been done. + */ + nextRefreshCheckStamp: DbPreciseTimestamp; + + /** + * Public key of the reserve that we're currently using for + * receiving P2P payments. + */ + currentMergeReserveRowId?: number; + + /** + * Current account private key. The corresponding public + * key is used as the merchant public key in deposits. + * + * When unset or reset, we use a heuristic to find an + * account priv/pub that likely already has KYC auth. + */ + currentAccountPriv?: string; + + /** + * @see currentAccountPriv + */ + currentAccountPub?: string; + + /** + * Defaults to false. + */ + peerPaymentsDisabled?: boolean; + + /** + * Are direct deposits using this exchange disabled? + * Defaults to false. + */ + directDepositDisabled?: boolean; + + /** + * Defaults to false. + */ + noFees?: boolean; +} + +/** + * Exchange details for a particular + * (exchangeBaseUrl, masterPublicKey, currency) tuple. + */ +export interface WalletExchangeDetails { + rowId?: number; + + /** + * Master public key of the exchange. + */ + masterPublicKey: string; + + exchangeBaseUrl: string; + + /** + * Currency that the exchange offers. + */ + currency: string; + + /** + * Auditors (partially) auditing the exchange. + */ + auditors: ExchangeAuditor[]; + + /** + * Last observed protocol version. + */ + protocolVersionRange: string; + + tinyAmount: AmountString; + + reserveClosingDelay: TalerProtocolDuration; + + shoppingUrl?: string; + + /** + * Fees for exchange services + */ + globalFees: ExchangeGlobalFees[]; + + wireInfo: WireInfo; + + /** + * Age restrictions supported by the exchange (bitmask). + */ + ageMask?: number; + + walletBalanceLimits?: AmountString[]; + + hardLimits?: AccountLimit[]; + + zeroLimits?: ZeroLimitedOperation[]; + + /** + * Instructs wallets to use certain bank-specific + * language (for buttons) and/or other UI/UX customization + * for compliance with the rules of that bank. + */ + bankComplianceLanguage: string | undefined; + + defaultPeerPushExpiration: TalerProtocolDuration | undefined; +} + 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 @@ -138,6 +138,9 @@ import { WalletTokenSelection, WalletPurchasePayInfo, WalletPurchase, + WalletExchangeDetailsPointer, + WalletExchangeEntry, + WalletExchangeDetails, WalletRefundGroup, WalletRefundItem, WalletTombstone, @@ -284,205 +287,6 @@ export interface ExchangeSignkeysRecord { } /** - * Exchange details for a particular - * (exchangeBaseUrl, masterPublicKey, currency) tuple. - */ -export interface ExchangeDetailsRecord { - rowId?: number; - - /** - * Master public key of the exchange. - */ - masterPublicKey: string; - - exchangeBaseUrl: string; - - /** - * Currency that the exchange offers. - */ - currency: string; - - /** - * Auditors (partially) auditing the exchange. - */ - auditors: ExchangeAuditor[]; - - /** - * Last observed protocol version. - */ - protocolVersionRange: string; - - tinyAmount: AmountString; - - reserveClosingDelay: TalerProtocolDuration; - - shoppingUrl?: string; - - /** - * Fees for exchange services - */ - globalFees: ExchangeGlobalFees[]; - - wireInfo: WireInfo; - - /** - * Age restrictions supported by the exchange (bitmask). - */ - ageMask?: number; - - walletBalanceLimits?: AmountString[]; - - hardLimits?: AccountLimit[]; - - zeroLimits?: ZeroLimitedOperation[]; - - /** - * Instructs wallets to use certain bank-specific - * language (for buttons) and/or other UI/UX customization - * for compliance with the rules of that bank. - */ - bankComplianceLanguage: string | undefined; - - defaultPeerPushExpiration: TalerProtocolDuration | undefined; -} - -export interface ExchangeDetailsPointer { - masterPublicKey: string; - - currency: string; - - /** - * Timestamp when the (masterPublicKey, currency) pointer - * has been updated. - */ - updateClock: DbPreciseTimestamp; -} - -/** - * Exchange record as stored in the wallet's database. - */ -export interface ExchangeEntryRecord { - /** - * Base url of the exchange. - */ - baseUrl: string; - - /** - * Currency hint for a preset exchange, relevant - * when we didn't contact a preset exchange yet. - */ - presetCurrencyHint?: string; - - /** - * Currency spec for a preset exchange, relevant - * when we didn't contact a preset exchange yet. - */ - presetCurrencySpec?: CurrencySpecification; - - /** - * Type of the exchange, if it was a preset entry. - */ - presetType?: string; - - /** - * When did we confirm the last withdrawal from this exchange? - * - * Used mostly in the UI to suggest exchanges. - */ - lastWithdrawal?: DbPreciseTimestamp; - - /** - * Pointer to the current exchange details. - * - * Should usually not change. Only changes when the - * exchange advertises a different master public key and/or - * currency. - * - * We could use a rowID here, but having the currency in the - * details pointer lets us do fewer DB queries - */ - detailsPointer: ExchangeDetailsPointer | undefined; - - entryStatus: ExchangeEntryDbRecordStatus; - - updateStatus: ExchangeEntryDbUpdateStatus; - - unavailableReason?: TalerErrorDetail; - - /** - * If set to true, the next update to the exchange - * status will request /keys with no-cache headers set. - */ - cachebreakNextUpdate?: boolean; - - /** - * Etag of the current ToS of the exchange. - */ - tosCurrentEtag: string | undefined; - - tosAcceptedEtag: string | undefined; - - tosAcceptedTimestamp: DbPreciseTimestamp | undefined; - - /** - * Last time when the exchange /keys info was updated - * successfully. - */ - lastUpdate: DbPreciseTimestamp | undefined; - - /** - * Next scheduled update for the exchange. - */ - nextUpdateStamp: DbPreciseTimestamp; - - lastKeysEtag: string | undefined; - - /** - * Next time that we should check if coins need to be refreshed. - * - * Updated whenever the exchange's denominations are updated or when - * the refresh check has been done. - */ - nextRefreshCheckStamp: DbPreciseTimestamp; - - /** - * Public key of the reserve that we're currently using for - * receiving P2P payments. - */ - currentMergeReserveRowId?: number; - - /** - * Current account private key. The corresponding public - * key is used as the merchant public key in deposits. - * - * When unset or reset, we use a heuristic to find an - * account priv/pub that likely already has KYC auth. - */ - currentAccountPriv?: string; - - /** - * @see currentAccountPriv - */ - currentAccountPub?: string; - - /** - * Defaults to false. - */ - peerPaymentsDisabled?: boolean; - - /** - * Are direct deposits using this exchange disabled? - * Defaults to false. - */ - directDepositDisabled?: boolean; - - /** - * Defaults to false. - */ - noFees?: boolean; -} - -/** * Object to be hashed for use as a grouping key for token listings, such that * any change in token family details results in a separate list item. */ @@ -1280,14 +1084,14 @@ export const WalletIndexedDbStoresV1 = { ), exchanges: describeStore( "exchanges", - describeContents<ExchangeEntryRecord>({ + describeContents<WalletExchangeEntry>({ keyPath: "baseUrl", }), {}, ), exchangeDetails: describeStore( "exchangeDetails", - describeContents<ExchangeDetailsRecord>({ + describeContents<WalletExchangeDetails>({ keyPath: "rowId", autoIncrement: true, }), @@ -2210,7 +2014,7 @@ async function fixupCoinAvailabilityExchangePub( tx: WalletIndexedDbTransaction, ): Promise<void> { const cars = await tx.coinAvailability.getAll(); - const exchanges: Record<string, ExchangeDetailsRecord | undefined> = {}; + const exchanges: Record<string, WalletExchangeDetails | undefined> = {}; for (const car of cars) { if (car.exchangeMasterPub === undefined) { if (exchanges[car.exchangeBaseUrl] === undefined) { diff --git a/packages/taler-wallet-core/src/dbtx-indexeddb.ts b/packages/taler-wallet-core/src/dbtx-indexeddb.ts @@ -70,10 +70,10 @@ import { WalletRefundGroup, WalletRefundItem, WalletTombstone, + WalletExchangeEntry, + WalletExchangeDetails, } from "./db-common.js"; import type { - ExchangeEntryRecord, - ExchangeDetailsRecord, } from "./db-indexeddb.js"; import { WalletIndexedDbTransaction, @@ -1142,7 +1142,7 @@ export class IdbWalletTransaction implements WalletDbTransaction { return await this.tx.donationSummaries.iter().toArray(); } - async getExchanges(): Promise<ExchangeEntryRecord[]> { + async getExchanges(): Promise<WalletExchangeEntry[]> { return await this.tx.exchanges.iter().toArray(); } @@ -1209,7 +1209,7 @@ export class IdbWalletTransaction implements WalletDbTransaction { async getExchangeDetails( exchangeBaseUrl: string, - ): Promise<ExchangeDetailsRecord | undefined> { + ): Promise<WalletExchangeDetails | undefined> { const r = await this.tx.exchanges.get(exchangeBaseUrl); if (!r || !r.detailsPointer) { return undefined; diff --git a/packages/taler-wallet-core/src/dbtx.ts b/packages/taler-wallet-core/src/dbtx.ts @@ -26,12 +26,8 @@ * Wallet<Name>. The <Name>Record types in db-indexeddb.ts describe the * IndexedDB object stores and must not appear here. * - * FIXME: The nine type-only imports from db-indexeddb.js below still violate - * that rule. They are converted lazily: each gets its Wallet<Name> - * counterpart in db-common.ts when the migration phase that touches its store - * arrives, rather than all at once up front (see DB_MIGRATION_PLAN.md). - * They are type-only, so this file still emits an empty JS module and has no - * runtime dependency on IndexedDB. + * This file imports nothing from db-indexeddb.ts, not even as types, and + * emits an empty JS module. */ import { @@ -74,12 +70,9 @@ import { WalletRefundGroup, WalletRefundItem, WalletTombstone, + WalletExchangeEntry, + WalletExchangeDetails, } from "./db-common.js"; -import type { - ExchangeEntryRecord, - ExchangeDetailsRecord, -} from "./db-indexeddb.js"; - export interface GetCurrencyInfoDbResult { /** * Currency specification. @@ -571,7 +564,7 @@ export interface WalletDbTransaction { getDonationSummaries(): Promise<WalletDonationSummary[]>; - getExchanges(): Promise<ExchangeEntryRecord[]>; + getExchanges(): Promise<WalletExchangeEntry[]>; getCoinAvailabilities(): Promise<WalletCoinAvailability[]>; @@ -595,7 +588,7 @@ export interface WalletDbTransaction { getExchangeDetails( exchangeBaseUrl: string, - ): Promise<ExchangeDetailsRecord | undefined>; + ): Promise<WalletExchangeDetails | undefined>; checkExchangeInScope( exchangeBaseUrl: string, diff --git a/packages/taler-wallet-core/src/exchanges.ts b/packages/taler-wallet-core/src/exchanges.ts @@ -150,13 +150,13 @@ import { timestampProtocolToDb, WalletDenomination, WalletReserve, + WalletExchangeDetails, + WalletExchangeEntry, } from "./db-common.js"; import { DenomFamilyParams, DenomLossEventRecord, DenominationFamilyRecord, - ExchangeDetailsRecord, - ExchangeEntryRecord, ExchangeMigrationReason, } from "./db-indexeddb.js"; import { @@ -217,7 +217,7 @@ interface ExchangeTosDownloadResult { async function getExchangeRecordsInternal( tx: LegacyWalletTxHandle, exchangeBaseUrl: string, -): Promise<ExchangeDetailsRecord | undefined> { +): Promise<WalletExchangeDetails | undefined> { const r = await tx.exchanges.get(exchangeBaseUrl); if (!r) { logger.warn(`no exchange found for ${exchangeBaseUrl}`); @@ -327,7 +327,7 @@ export async function getExchangeScopeInfo( async function internalGetExchangeScopeInfo( tx: LegacyWalletTxHandle, - exchangeDetails: ExchangeDetailsRecord, + exchangeDetails: WalletExchangeDetails, ): Promise<ScopeInfo> { const globalExchangeRec = await tx.globalCurrencyExchanges.indexes.byCurrencyAndUrlAndPub.get([ @@ -384,8 +384,8 @@ function getKycStatusFromReserveStatus( async function makeExchangeListItem( wex: WalletExecutionContext, tx: LegacyWalletTxHandle, - r: ExchangeEntryRecord, - exchangeDetails: ExchangeDetailsRecord | undefined, + r: WalletExchangeEntry, + exchangeDetails: WalletExchangeDetails | undefined, reserveRec: WalletReserve | undefined, lastError: TalerErrorDetail | undefined, ): Promise<ExchangeListItem> { @@ -731,7 +731,7 @@ export async function putPresetExchangeEntry( ): Promise<void> { let exchange = await tx.exchanges.get(exchangeBaseUrl); if (!exchange) { - const r: ExchangeEntryRecord = { + const r: WalletExchangeEntry = { entryStatus: ExchangeEntryDbRecordStatus.Preset, updateStatus: ExchangeEntryDbUpdateStatus.Initial, baseUrl: exchangeBaseUrl, @@ -773,12 +773,12 @@ async function provideExchangeRecordInTx( tx: LegacyWalletTxHandle, baseUrl: string, ): Promise<{ - exchange: ExchangeEntryRecord; - exchangeDetails: ExchangeDetailsRecord | undefined; + exchange: WalletExchangeEntry; + exchangeDetails: WalletExchangeDetails | undefined; }> { let exchange = await tx.exchanges.get(baseUrl); if (!exchange) { - const r: ExchangeEntryRecord = { + const r: WalletExchangeEntry = { entryStatus: ExchangeEntryDbRecordStatus.Ephemeral, updateStatus: ExchangeEntryDbUpdateStatus.InitialUpdate, baseUrl: baseUrl, @@ -1481,8 +1481,8 @@ export async function waitReadyExchange( } function constructReadyExchangeSummary( - exchangeRec: ExchangeEntryRecord, - exchangeDetails: ExchangeDetailsRecord, + exchangeRec: WalletExchangeEntry, + exchangeDetails: WalletExchangeDetails, scopeInfo: ScopeInfo, ): ReadyExchangeSummary { return { @@ -1507,7 +1507,7 @@ function constructReadyExchangeSummary( async function loadReadyExchangeSummary( tx: LegacyWalletTxHandle, - exchangeRec: ExchangeEntryRecord, + exchangeRec: WalletExchangeEntry, ): Promise<ReadyExchangeSummary | undefined> { const exchangeDetails = await getExchangeRecordsInternal( tx, @@ -1946,7 +1946,7 @@ export async function updateExchangeFromUrlHandler( return TaskRunResult.backoff(); } delete r.unavailableReason; - const newDetails: ExchangeDetailsRecord = { + const newDetails: WalletExchangeDetails = { auditors: keysInfo.auditors, currency: keysInfo.currency, masterPublicKey: keysInfo.master_public_key, @@ -3166,7 +3166,7 @@ async function internalGetExchangeResources( async function purgeExchange( wex: WalletExecutionContext, tx: LegacyWalletTxHandle, - exchangeRec: ExchangeEntryRecord, + exchangeRec: WalletExchangeEntry, purgeTransactions?: boolean, ): Promise<void> { const exchangeBaseUrl = exchangeRec.baseUrl; @@ -3738,7 +3738,7 @@ export async function handleStartExchangeWalletKyc( async function handleExchangeKycPendingWallet( wex: WalletExecutionContext, - exchange: ExchangeEntryRecord, + exchange: WalletExchangeEntry, reserve: WalletReserve, ): Promise<TaskRunResult> { checkDbInvariant(!!reserve.thresholdRequested, "threshold"); @@ -3960,7 +3960,7 @@ async function handleExchangeKycRespLegi( */ async function handleExchangeKycPendingLegitimization( wex: WalletExecutionContext, - exchange: ExchangeEntryRecord, + exchange: WalletExchangeEntry, reserve: WalletReserve, ): Promise<TaskRunResult> { // FIXME: Cache this signature