commit 53bace0191f0694d05875225c5b4593c8b3b8dbc
parent 5c33ef95ba30ec44c5a9b3c044bb2b4f3c6e1eb6
Author: Florian Dold <florian@dold.me>
Date: Mon, 8 Sep 2025 19:03:14 +0200
wallet-core: fixup for TOPS-blunder
Also, do not accidentally update preset exchange entry in the
auto-refresh task handler.
Diffstat:
5 files changed, 94 insertions(+), 7 deletions(-)
diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts
@@ -3595,7 +3595,62 @@ export interface FixupDescription {
/**
* Manual migrations between minor versions of the DB schema.
*/
-export const walletDbFixups: FixupDescription[] = [];
+export const walletDbFixups: FixupDescription[] = [
+ // Can be removed 2025-11-01.
+ {
+ fn: fixup20250908TopsBlunder,
+ name: "fixup20250908TopsBlunder",
+ },
+];
+
+/**
+ * TOPS accidentally revoked keys.
+ * Make sure to re-request keys and re-do denom selection
+ * for withdrawal groups with zero selected denominations.
+ */
+async function fixup20250908TopsBlunder(
+ tx: WalletDbAllStoresReadWriteTransaction,
+): Promise<void> {
+ const exchangeUrls = [
+ "https://exchange.taler-ops.ch/",
+ "https://exchange.stage.taler-ops.ch/",
+ ];
+
+ for (const exch of exchangeUrls) {
+ const exchRec = await tx.exchanges.get(exch);
+ if (!exchRec) {
+ continue;
+ }
+ exchRec.lastUpdate = undefined;
+ exchRec.lastKeysEtag = undefined;
+ switch (exchRec.updateStatus) {
+ case ExchangeEntryDbUpdateStatus.Ready:
+ break;
+ default:
+ continue;
+ }
+ logger.info(`fixup: forcing update of exchange ${exch}`);
+ exchRec.lastKeysEtag = undefined;
+ exchRec.lastUpdate = undefined;
+ exchRec.updateRetryCounter = undefined;
+ exchRec.updateStatus = ExchangeEntryDbUpdateStatus.UnavailableUpdate;
+ exchRec.nextUpdateStamp = timestampPreciseToDb(TalerPreciseTimestamp.now());
+ await tx.exchanges.put(exchRec);
+
+ const wgs =
+ await tx.withdrawalGroups.indexes.byExchangeBaseUrl.getAll(exch);
+ for (const wg of wgs) {
+ if (wg.status !== WithdrawalGroupStatus.Done) {
+ continue;
+ }
+ if (wg.denomsSel?.selectedDenoms.length != 0) {
+ continue;
+ }
+ wg.status = WithdrawalGroupStatus.PendingQueryingStatus;
+ await tx.withdrawalGroups.put(wg);
+ }
+ }
+}
export async function applyFixups(
db: DbAccess<typeof WalletStoresV1>,
diff --git a/packages/taler-wallet-core/src/dev-experiments.ts b/packages/taler-wallet-core/src/dev-experiments.ts
@@ -310,6 +310,10 @@ export async function applyDevExperiment(
}
return;
}
+ case "pretend-no-denoms": {
+ wex.ws.devExperimentState.pretendNoDenoms = true;
+ return;
+ }
default:
throw Error(
`dev-experiment id not understood ${parsedUri.devExperimentId}`,
diff --git a/packages/taler-wallet-core/src/exchanges.ts b/packages/taler-wallet-core/src/exchanges.ts
@@ -1725,6 +1725,7 @@ export async function updateExchangeFromUrlHandler(
);
if (
+ oldExchangeRec.lastUpdate != null &&
!AbsoluteTime.isNever(nextUpdateStamp) &&
!AbsoluteTime.isExpired(nextUpdateStamp)
) {
@@ -2243,6 +2244,14 @@ export async function processTaskExchangeAutoRefresh(
return TaskRunResult.finished();
}
+ switch (oldExchangeRec.entryStatus) {
+ case ExchangeEntryDbRecordStatus.Preset:
+ logger.trace(
+ "exchange auto-refresh check not necessary, exchange is preset",
+ );
+ return TaskRunResult.finished();
+ }
+
let nextRefreshCheckStamp = timestampAbsoluteFromDb(
oldExchangeRec.nextRefreshCheckStamp,
);
diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts
@@ -2638,10 +2638,10 @@ export class Wallet {
export interface DevExperimentState {
blockRefreshes?: boolean;
- /**
- * Pretend that exchanges have no fees.
- */
+ /** Pretend that exchanges have no fees.*/
pretendNoFees?: boolean;
+ /** Pretend exchange has no withdrawable denoms. */
+ pretendNoDenoms?: boolean;
pretendPostWopFailed?: boolean;
merchantDepositInsufficient?: boolean;
/** Map from base URL to faked version for /config or /keys */
diff --git a/packages/taler-wallet-core/src/withdraw.ts b/packages/taler-wallet-core/src/withdraw.ts
@@ -2191,12 +2191,14 @@ async function processQueryReserve(
// We only allow changing the amount *down*, so that user error
// in the wire transfer won't result in a giant withdrawal.
// See https://bugs.taler.net/n/9732
+ // We also re-select when the initial selection had zero coins.
let amountChanged = false;
if (
Amounts.cmp(
result.response.balance,
withdrawalGroup.denomsSel.totalWithdrawCost,
- ) === -1
+ ) === -1 ||
+ withdrawalGroup.denomsSel.selectedDenoms.length === 0
) {
amountChanged = true;
}
@@ -2208,6 +2210,12 @@ async function processQueryReserve(
const exchangeBaseUrl = withdrawalGroup.exchangeBaseUrl;
const currency = Amounts.currencyOf(withdrawalGroup.instructedAmount);
+ // If we re-select denominations, make sure we have current
+ // information about the exchange.
+ if (amountChanged) {
+ await fetchFreshExchange(wex, withdrawalGroup.exchangeBaseUrl);
+ }
+
const transitionResult = await ctx.transition(
{
extraStores: ["denominations", "bankAccountsV2"],
@@ -2236,10 +2244,13 @@ async function processQueryReserve(
exchangeBaseUrl,
currency,
);
- wg.denomsSel = selectWithdrawalDenominations(
+ const denomsSel = selectWithdrawalDenominations(
Amounts.parseOrThrow(result.response.balance),
candidates,
);
+ wg.denomsSel = denomsSel;
+ wg.rawWithdrawalAmount = denomsSel.totalWithdrawCost;
+ wg.effectiveWithdrawalAmount = denomsSel.totalCoinValue;
}
wg.status = WithdrawalGroupStatus.PendingReady;
wg.reserveBalanceAmount = Amounts.stringify(result.response.balance);
@@ -2723,7 +2734,11 @@ async function processWithdrawalGroupPendingReady(
}
}
- if (wg.timestampFinish === undefined && numActive === 0) {
+ if (
+ (wg.timestampFinish === undefined ||
+ wg.status !== WithdrawalGroupStatus.Done) &&
+ numActive === 0
+ ) {
wg.timestampFinish = timestampPreciseToDb(TalerPreciseTimestamp.now());
wg.status = WithdrawalGroupStatus.Done;
await makeCoinsVisible(wex, tx, ctx.transactionId);
@@ -3447,6 +3462,10 @@ async function getInitialDenomsSelection(
amount: AmountJson,
forcedDenoms: ForcedDenomSel | undefined,
): Promise<DenomSelectionState> {
+ if (wex.ws.devExperimentState.pretendNoDenoms) {
+ return selectWithdrawalDenominations(amount, []);
+ }
+
const currency = Amounts.currencyOf(amount);
await updateWithdrawalDenomsForExchange(wex, exchange);
const denoms = await getWithdrawableDenoms(wex, exchange, currency);