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:
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);
+ }
}
}