taler-typescript-core

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

commit 886d86262fb909f0939dfd95118846760da5d57d
parent 08dbab158e9e38cfc3fb95125cbcb24f28512955
Author: Florian Dold <florian@dold.me>
Date:   Thu, 19 Mar 2026 23:04:47 +0100

wallet-core: fix/simplify tx retry logic

Diffstat:
Mpackages/taler-wallet-core/src/wallet.ts | 61+++++++++++++++++++++++++------------------------------------
1 file changed, 25 insertions(+), 36 deletions(-)

diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts @@ -489,6 +489,11 @@ export interface WalletExecutionContext { readonly oc: ObservabilityContext; readonly cts: CancellationToken.Source | undefined; readonly taskScheduler: TaskScheduler; + readonly dbRetryState: DbRetryState; +} + +export interface DbRetryState { + retriedExchangeUpdate?: Set<string>; } export function walletExchangeClient( @@ -2775,6 +2780,9 @@ export function getObservedWalletExecutionContext( cts, cryptoApi: observeTalerCrypto(ws.cryptoApi, oc), db: new ObservableDbAccess(db, oc), + dbRetryState: { + retriedExchangeUpdate: new Set(), + }, http: new ObservableHttpClientLibrary(ws.http, oc), taskScheduler: new ObservableTaskScheduler(ws.taskScheduler, oc), oc, @@ -2802,7 +2810,6 @@ async function handleTxRetries<T>( wex: WalletExecutionContext, f: () => Promise<T>, ): Promise<T> { - const coveredExchanges = new Set<string>(); while (1) { wex.cancellationToken.throwIfCancelled(); try { @@ -2812,48 +2819,29 @@ async function handleTxRetries<T>( if (exn == null) { throw e; } + let url: string | undefined = undefined; if (exn instanceof UnverifiedDenomError) { - const url = exn.denomInfo.exchangeBaseUrl; + url = exn.denomInfo.exchangeBaseUrl; logger.info(`got unverified denominations, updating ${url}`); - if (coveredExchanges.has(url)) { - logger.error(`exchange was already covered, giving up`); - throw e; - } - // Make sure this doesn't recurse - if (wex.ws.disableTransactionRetries) { - throw e; - } - try { - wex.ws.disableTransactionRetries = true; - await fetchFreshExchange(wex, url); - await updateWithdrawalDenomsForExchange(wex, url); - coveredExchanges.add(url); - } catch (e) { - logger.error( - `Exception thrown while trying to heal UnverifiedDenomError`, - ); - throw e; - } finally { - wex.ws.disableTransactionRetries = true; - } } else if (exn instanceof OutdatedExchangeError) { const url = exn.exchangeBaseUrl; logger.info(`got outdated exchange entry, updating ${url}`); - // Make sure this doesn't recurse - if (wex.ws.disableTransactionRetries) { - throw e; + } + if (url) { + if (!wex.dbRetryState.retriedExchangeUpdate) { + wex.dbRetryState.retriedExchangeUpdate = new Set(); } - try { - wex.ws.disableTransactionRetries = true; - await fetchFreshExchange(wex, url); - } catch (e) { + if (wex.dbRetryState.retriedExchangeUpdate.has(url)) { logger.error( - `Exception thrown while trying to heal UnverifiedDenomError`, + `updating exchange already failed in execution context, not retrying`, ); - throw e; - } finally { - wex.ws.disableTransactionRetries = true; + throw exn; } + // Prevent both recursion and multiple updates + // per wallet execution context. + wex.dbRetryState.retriedExchangeUpdate.add(url); + await fetchFreshExchange(wex, url); + await updateWithdrawalDenomsForExchange(wex, url); } } } @@ -2908,6 +2896,9 @@ export function getNormalWalletExecutionContext( cts, cryptoApi: ws.cryptoApi, db, + dbRetryState: { + retriedExchangeUpdate: new Set(), + }, get http() { if (ws.initCalled) { return ws.http; @@ -3360,8 +3351,6 @@ export class InternalWalletState { */ refcntIgnoreTos: number = 0; - disableTransactionRetries: boolean = false; - clearAllCaches(): void { this.exchangeCache.clear(); this.denomInfoCache.clear();