taler-typescript-core

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

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:
Mpackages/taler-wallet-core/src/db.ts | 57++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Mpackages/taler-wallet-core/src/dev-experiments.ts | 4++++
Mpackages/taler-wallet-core/src/exchanges.ts | 9+++++++++
Mpackages/taler-wallet-core/src/wallet.ts | 6+++---
Mpackages/taler-wallet-core/src/withdraw.ts | 25++++++++++++++++++++++---
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);