commit 111daccf6fe4ecf791fd7294d958d03abceea355
parent 886df1dc4a88d2b9eb372497e3751fbfe0532468
Author: Florian Dold <florian@dold.me>
Date: Tue, 3 Feb 2026 21:17:29 +0100
wallet-core: fix old denoms in fixup for denom families
Diffstat:
3 files changed, 88 insertions(+), 50 deletions(-)
diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts
@@ -19,8 +19,10 @@
*/
import {
Event,
+ GlobalIDB,
IDBDatabase,
IDBFactory,
+ IDBKeyRange,
IDBObjectStore,
IDBRequest,
IDBTransaction,
@@ -3887,6 +3889,8 @@ export interface FixupDescription {
/**
* Manual migrations between minor versions of the DB schema.
+ *
+ * Fixups *must* be idempotent.
*/
export const walletDbFixups: FixupDescription[] = [
// Removing this would cause old transactions
@@ -3912,48 +3916,75 @@ export const walletDbFixups: FixupDescription[] = [
// This migration creates denom families
// for existing denomination records.
{
- fn: fixup20260129DenomFamilyMigration,
- name: "fixup20260129DenomFamilyMigration",
+ fn: fixup20260203DenomFamilyMigration,
+ name: "fixup20260203DenomFamilyMigration",
},
];
-async function fixup20260129DenomFamilyMigration(
+async function fixup20260203DenomFamilyMigration(
tx: WalletDbAllStoresReadWriteTransaction,
): Promise<void> {
- // FIXME: Batch the DB fetches, with some forEachAsyncBatched
- await tx.denominations.iter().forEachAsync(async (r) => {
- if (r.denominationFamilySerial != null) {
- return;
- }
- const fp: DenomFamilyParams = {
- exchangeBaseUrl: r.exchangeBaseUrl,
- exchangeMasterPub: r.exchangeMasterPub,
- feeDeposit: r.fees.feeDeposit,
- feeRefresh: r.fees.feeRefresh,
- feeRefund: r.fees.feeRefund,
- feeWithdraw: r.fees.feeWithdraw,
- value: r.value,
- };
- const fph = hashDenomFamilyParams(fp);
- const dfRec =
- await tx.denominationFamilies.indexes.byFamilyParamsHash.get(fph);
- let denominationFamilySerial;
- if (dfRec) {
- denominationFamilySerial = dfRec.denominationFamilySerial;
- } else {
- const insRes = await tx.denominationFamilies.put({
- familyParams: fp,
- familyParamsHash: fph,
- });
- denominationFamilySerial = insRes.key;
+ const batchSize = 500;
+
+ let range: IDBKeyRange | undefined = undefined;
+
+ while (1) {
+ const batch = await tx.denominations.getAll(range, batchSize);
+ logger.info(`fixing up batch of ${batch.length} denominations`);
+
+ if (batch.length === 0) {
+ break;
}
- checkDbInvariant(
- typeof denominationFamilySerial == "number",
- "denominationFamilySerial",
+
+ const last = batch[batch.length - 1];
+ range = GlobalIDB.KeyRange.lowerBound(
+ [last.exchangeBaseUrl, last.denomPubHash],
+ true,
);
- r.denominationFamilySerial = denominationFamilySerial;
- await tx.denominations.put(r);
- });
+
+ for (const r of batch) {
+ const fp: DenomFamilyParams = {
+ exchangeBaseUrl: r.exchangeBaseUrl,
+ exchangeMasterPub: r.exchangeMasterPub,
+ feeDeposit: r.fees.feeDeposit,
+ feeRefresh: r.fees.feeRefresh,
+ feeRefund: r.fees.feeRefund,
+ feeWithdraw: r.fees.feeWithdraw,
+ value: r.value,
+ };
+ if (r.denominationFamilySerial != null) {
+ // Fast path: Check if family exists and is correct.
+ const oldFpRec = await tx.denominationFamilies.get(
+ r.denominationFamilySerial,
+ );
+ if (
+ oldFpRec &&
+ canonicalJson(fp) == canonicalJson(oldFpRec.familyParams)
+ ) {
+ continue;
+ }
+ }
+ const fph = hashDenomFamilyParams(fp);
+ const dfRec =
+ await tx.denominationFamilies.indexes.byFamilyParamsHash.get(fph);
+ let denominationFamilySerial;
+ if (dfRec) {
+ denominationFamilySerial = dfRec.denominationFamilySerial;
+ } else {
+ const insRes = await tx.denominationFamilies.put({
+ familyParams: fp,
+ familyParamsHash: fph,
+ });
+ denominationFamilySerial = insRes.key;
+ }
+ checkDbInvariant(
+ typeof denominationFamilySerial == "number",
+ "denominationFamilySerial",
+ );
+ r.denominationFamilySerial = denominationFamilySerial;
+ await tx.denominations.put(r);
+ }
+ }
}
async function fixup20260116BadRefreshCoinSelection(
@@ -4051,12 +4082,12 @@ export async function applyFixups(
): Promise<number> {
logger.trace("applying fixups");
let count = 0;
- await db.runAllStoresReadWriteTx({}, async (tx) => {
- for (const fixupInstruction of walletDbFixups) {
+ for (const fixupInstruction of walletDbFixups) {
+ await db.runAllStoresReadWriteTx({}, async (tx) => {
logger.trace(`checking fixup ${fixupInstruction.name}`);
const fixupRecord = await tx.fixups.get(fixupInstruction.name);
if (fixupRecord) {
- continue;
+ return;
}
logger.info(`applying DB fixup ${fixupInstruction.name}`);
await fixupInstruction.fn(tx);
@@ -4064,8 +4095,8 @@ export async function applyFixups(
fixupName: fixupInstruction.name,
});
count++;
- }
- });
+ });
+ }
return count;
}
diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts
@@ -880,6 +880,11 @@ async function recoverStoredBackup(
await importDb(wex.db.idbHandle(), bd);
await wex.db.runAllStoresReadWriteTx({}, async (tx) => {
await rematerializeTransactions(wex, tx);
+ // Clear fixups. Okay since they are idempotent.
+ const fixups = await tx.fixups.getAll();
+ for (const f of fixups) {
+ await tx.fixups.delete(f.fixupName);
+ }
});
logger.info(`import done`);
}
@@ -1817,6 +1822,11 @@ async function handleImportDb(
await importDb(wex.db.idbHandle(), req.dump);
await wex.db.runAllStoresReadWriteTx({}, async (tx) => {
await rematerializeTransactions(wex, tx);
+ // Clear fixups. Okay since they are idempotent.
+ const fixups = await tx.fixups.getAll();
+ for (const f of fixups) {
+ await tx.fixups.delete(f.fixupName);
+ }
});
return {};
}
diff --git a/packages/taler-wallet-core/src/withdraw.ts b/packages/taler-wallet-core/src/withdraw.ts
@@ -1437,11 +1437,9 @@ export async function getWithdrawableDenomsTx(
continue;
}
if (
- !(
- dr0.value.denominationFamilySerial > fpSerial ||
- (dr0.value.denominationFamilySerial === fpSerial &&
- dr0.value.stampExpireWithdraw >= dbNow)
- )
+ dr0.value.denominationFamilySerial < fpSerial ||
+ (dr0.value.denominationFamilySerial === fpSerial &&
+ dr0.value.stampExpireWithdraw < dbNow)
) {
logger.trace(`continuing cursor past ${j2s([fpSerial, dbNow])}`);
denomCursor.continue([fpSerial, dbNow]);
@@ -1457,6 +1455,7 @@ export async function getWithdrawableDenomsTx(
logger.trace(`cursor past target`);
break;
}
+ logger.trace(`considering ${j2s(dr.value)}`);
if (isCandidateWithdrawableDenomRec(dr.value)) {
denom = dr.value;
break;
@@ -2183,11 +2182,9 @@ export async function updateWithdrawalDenomsForExchange(
continue;
}
if (
- !(
- dr0.value.denominationFamilySerial > fpSerial ||
- (dr0.value.denominationFamilySerial === fpSerial &&
- dr0.value.stampExpireWithdraw >= dbNow)
- )
+ dr0.value.denominationFamilySerial < fpSerial ||
+ (dr0.value.denominationFamilySerial === fpSerial &&
+ dr0.value.stampExpireWithdraw < dbNow)
) {
denomCursor.continue([fpSerial, dbNow]);
}