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:
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