taler-typescript-core

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

commit 20b8855efedcbe4b5544b36576b306247fd89158
parent 3f5ec8575d3cef89f126747ea2e94fa991d6d15b
Author: Florian Dold <dold@taler.net>
Date:   Thu, 23 Jul 2026 01:00:47 +0200

wallet: fix progressToken mechanism, pass it through

Diffstat:
Mpackages/taler-util/src/types-taler-wallet.ts | 132++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------
Mpackages/taler-wallet-core/src/exchanges.ts | 75++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------
Mpackages/taler-wallet-core/src/progress.ts | 102++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------------
Mpackages/taler-wallet-core/src/requests.ts | 99+++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------
4 files changed, 272 insertions(+), 136 deletions(-)

diff --git a/packages/taler-util/src/types-taler-wallet.ts b/packages/taler-util/src/types-taler-wallet.ts @@ -300,12 +300,13 @@ export interface GetMaxDepositAmountRequest { restrictScope?: ScopeInfo; } -export const codecForGetMaxDepositAmountRequest = (): Codec<GetMaxDepositAmountRequest> => - buildCodecForObject<GetMaxDepositAmountRequest>() - .property("currency", codecForString()) - .property("depositPaytoUri", codecOptional(codecForString())) - .property("restrictScope", codecOptional(codecForScopeInfo())) - .build("GetAmountRequest"); +export const codecForGetMaxDepositAmountRequest = + (): Codec<GetMaxDepositAmountRequest> => + buildCodecForObject<GetMaxDepositAmountRequest>() + .property("currency", codecForString()) + .property("depositPaytoUri", codecOptional(codecForString())) + .property("restrictScope", codecOptional(codecForScopeInfo())) + .build("GetAmountRequest"); export interface GetMaxPeerPushDebitAmountRequest { currency: string; @@ -1420,7 +1421,9 @@ export interface ContactListResponse { contacts: ContactEntry[]; } -export type TaldirRegistrationResponse = TaldirAlreadyPaidResponse | EmptyObject; +export type TaldirRegistrationResponse = + | TaldirAlreadyPaidResponse + | EmptyObject; export interface TaldirRegistrationCompletionRequest { alias: string; @@ -1430,14 +1433,15 @@ export interface TaldirRegistrationCompletionRequest { targetUri: string; } -export const codecForTaldirRegistrationCompletionRequest = (): Codec<TaldirRegistrationCompletionRequest> => - buildCodecForObject<TaldirRegistrationCompletionRequest>() - .property("targetUri", codecForString()) - .property("taldirBaseUrl", codecForString()) - .property("alias", codecForString()) - .property("aliasType", codecForString()) - .property("challenge", codecForString()) - .build("TaldirRegistrationCompletionRequest"); +export const codecForTaldirRegistrationCompletionRequest = + (): Codec<TaldirRegistrationCompletionRequest> => + buildCodecForObject<TaldirRegistrationCompletionRequest>() + .property("targetUri", codecForString()) + .property("taldirBaseUrl", codecForString()) + .property("alias", codecForString()) + .property("aliasType", codecForString()) + .property("challenge", codecForString()) + .build("TaldirRegistrationCompletionRequest"); export interface TaldirRegistrationRequest { alias: string; @@ -1447,15 +1451,15 @@ export interface TaldirRegistrationRequest { duration: RelativeTime; } -export const codecForTaldirRegistrationRequest = (): Codec<TaldirRegistrationRequest> => - buildCodecForObject<TaldirRegistrationRequest>() - .property("targetUri", codecForString()) - .property("taldirBaseUrl", codecForString()) - .property("alias", codecForString()) - .property("aliasType", codecForString()) - .property("duration", codecForDuration) - .build("TaldirRegistrationRequest"); - +export const codecForTaldirRegistrationRequest = + (): Codec<TaldirRegistrationRequest> => + buildCodecForObject<TaldirRegistrationRequest>() + .property("targetUri", codecForString()) + .property("taldirBaseUrl", codecForString()) + .property("alias", codecForString()) + .property("aliasType", codecForString()) + .property("duration", codecForDuration) + .build("TaldirRegistrationRequest"); export interface TaldirLookupRequest { alias: string; @@ -1479,8 +1483,6 @@ export const codecForTaldirLookupResponse = (): Codec<TaldirLookupResponse> => .property("targetUri", codecOptional(codecForString())) .build("TaldirLookupResponse"); - - export interface GetMailboxResponse { mailboxConfiguration?: MailboxConfiguration; } @@ -1489,7 +1491,6 @@ export interface MailboxMessageRecordsResponse { messages: MailboxMessageRecord[]; } - export interface MailboxBaseUrl { mailboxBaseUrl: string; } @@ -2316,6 +2317,8 @@ export interface AddExchangeRequest { * @deprecated Use {@link uri} instead */ exchangeBaseUrl?: string; + + progressToken?: string; } export interface AddExchangeResponse { @@ -2332,6 +2335,7 @@ export const codecForAddExchangeRequest = (): Codec<AddExchangeRequest> => .property("uri", codecOptional(codecForString())) .property("forceUpdate", codecOptional(codecForBoolean())) .property("ephemeral", codecOptional(codecForBoolean())) + .property("progressToken", codecOptional(codecForString())) .build("AddExchangeRequest"); export interface UpdateExchangeEntryRequest { @@ -2413,6 +2417,8 @@ export interface AcceptManualWithdrawalRequest { * to developer mode. */ forceReservePriv?: EddsaPrivateKeyString; + + progressToken?: string; } export const codecForAcceptManualWithdrawalRequest = @@ -2422,6 +2428,7 @@ export const codecForAcceptManualWithdrawalRequest = .property("amount", codecForAmountString()) .property("restrictAge", codecOptional(codecForNumber())) .property("forceReservePriv", codecOptional(codecForEddsaPrivateKey())) + .property("progressToken", codecOptional(codecForString())) .build("AcceptManualWithdrawalRequest"); export interface GetWithdrawalDetailsForAmountRequest { @@ -2443,12 +2450,15 @@ export interface GetWithdrawalDetailsForAmountRequest { export interface PrepareBankIntegratedWithdrawalRequest { talerWithdrawUri: string; + + progressToken?: string; } export const codecForPrepareBankIntegratedWithdrawalRequest = (): Codec<PrepareBankIntegratedWithdrawalRequest> => buildCodecForObject<PrepareBankIntegratedWithdrawalRequest>() .property("talerWithdrawUri", codecForString()) + .property("progressToken", codecOptional(codecForString())) .build("PrepareBankIntegratedWithdrawalRequest"); export interface PrepareBankIntegratedWithdrawalResponse { @@ -2462,6 +2472,8 @@ export interface ConfirmWithdrawalRequest { amount?: AmountString | undefined; forcedDenomSel?: ForcedDenomSel; restrictAge?: number; + + progressToken?: string; } export const codecForConfirmWithdrawalRequestRequest = @@ -2472,6 +2484,7 @@ export const codecForConfirmWithdrawalRequestRequest = .property("exchangeBaseUrl", codecForCanonBaseUrl()) .property("forcedDenomSel", codecForAny()) .property("restrictAge", codecOptional(codecForNumber())) + .property("progressToken", codecOptional(codecForString())) .build("ConfirmWithdrawalRequest"); export interface AcceptBankIntegratedWithdrawalRequest { @@ -2486,6 +2499,8 @@ export interface AcceptBankIntegratedWithdrawalRequest { */ amount?: AmountString; restrictAge?: number; + + progressToken?: string; } export const codecForAcceptBankIntegratedWithdrawalRequest = @@ -2496,6 +2511,7 @@ export const codecForAcceptBankIntegratedWithdrawalRequest = .property("forcedDenomSel", codecForAny()) .property("amount", codecOptional(codecForAmountString())) .property("restrictAge", codecOptional(codecForNumber())) + .property("progressToken", codecOptional(codecForString())) .build("AcceptBankIntegratedWithdrawalRequest"); export const codecForGetWithdrawalDetailsForAmountRequest = @@ -2553,6 +2569,8 @@ export interface GetWithdrawalDetailsForUriRequest { * @deprecated not used */ restrictAge?: number; + + progressToken?: string; } export const codecForGetWithdrawalDetailsForUri = @@ -2560,6 +2578,7 @@ export const codecForGetWithdrawalDetailsForUri = buildCodecForObject<GetWithdrawalDetailsForUriRequest>() .property("talerWithdrawUri", codecForString()) .property("restrictAge", codecOptional(codecForNumber())) + .property("progressToken", codecOptional(codecForString())) .build("GetWithdrawalDetailsForUriRequest"); export interface ListBankAccountsRequest { @@ -2731,6 +2750,8 @@ export const codecForSharePaymentResult = (): Codec<SharePaymentResult> => export interface CheckPayTemplateRequest { talerPayTemplateUri: string; + + progressToken?: string; } export type CheckPayTemplateReponse = { @@ -2742,11 +2763,14 @@ export const codecForCheckPayTemplateRequest = (): Codec<CheckPayTemplateRequest> => buildCodecForObject<CheckPayTemplateRequest>() .property("talerPayTemplateUri", codecForString()) + .property("progressToken", codecOptional(codecForString())) .build("CheckPayTemplateRequest"); export interface PreparePayTemplateRequest { talerPayTemplateUri: string; templateParams?: TemplateParams; + + progressToken?: string; } export const codecForPreparePayTemplateRequest = @@ -2754,6 +2778,7 @@ export const codecForPreparePayTemplateRequest = buildCodecForObject<PreparePayTemplateRequest>() .property("talerPayTemplateUri", codecForString()) .property("templateParams", codecForAny()) + .property("progressToken", codecOptional(codecForString())) .build("PreparePayTemplate"); export interface ConfirmPayRequest { @@ -3187,6 +3212,8 @@ export interface CreateDepositGroupRequest { * before the creation request has returned. */ transactionId?: TransactionIdStr; + + progressToken?: string; } export interface CheckDepositRequest { @@ -3242,6 +3269,7 @@ export const codecForCreateDepositGroupRequest = .property("transactionId", codecOptional(codecForTransactionIdStr())) .property("testingFixedPriv", codecOptional(codecForString())) .property("wireDeadline", codecOptional(codecForTimestamp)) + .property("progressToken", codecOptional(codecForString())) .build("CreateDepositGroupRequest"); /** @@ -3571,6 +3599,8 @@ export const codecForInitiatePeerPushDebitRequest = export interface PreparePeerPushCreditRequest { talerUri?: string; transactionId?: string; + + progressToken?: string; } /** @@ -3581,6 +3611,8 @@ export interface PreparePeerPushCreditRequest { export interface PreparePeerPullDebitRequest { talerUri?: string; transactionId?: string; + + progressToken?: string; } export interface PreparePeerPushCreditResponse { @@ -3633,6 +3665,7 @@ export const codecForPreparePeerPushCreditRequest = buildCodecForObject<PreparePeerPushCreditRequest>() .property("talerUri", codecOptional(codecForString())) .property("transactionId", codecOptional(codecForString())) + .property("progressToken", codecOptional(codecForString())) .build("CheckPeerPushPaymentRequest"); export const codecForCheckPeerPullPaymentRequest = @@ -3640,10 +3673,13 @@ export const codecForCheckPeerPullPaymentRequest = buildCodecForObject<PreparePeerPullDebitRequest>() .property("talerUri", codecOptional(codecForString())) .property("transactionId", codecOptional(codecForString())) + .property("progressToken", codecOptional(codecForString())) .build("PreparePeerPullDebitRequest"); export interface ConfirmPeerPushCreditRequest { transactionId: string; + + progressToken?: string; } export interface AcceptPeerPushPaymentResponse { transactionId: TransactionIdStr; @@ -3657,6 +3693,7 @@ export const codecForConfirmPeerPushPaymentRequest = (): Codec<ConfirmPeerPushCreditRequest> => buildCodecForObject<ConfirmPeerPushCreditRequest>() .property("transactionId", codecForString()) + .property("progressToken", codecOptional(codecForString())) .build("ConfirmPeerPushCreditRequest"); export interface ConfirmPeerPullDebitRequest { @@ -3716,6 +3753,8 @@ export interface CheckPeerPullCreditResponse { export interface InitiatePeerPullCreditRequest { exchangeBaseUrl?: string; partialContractTerms: PeerContractTerms; + + progressToken?: string; } export const codecForInitiatePeerPullPaymentRequest = @@ -3723,6 +3762,7 @@ export const codecForInitiatePeerPullPaymentRequest = buildCodecForObject<InitiatePeerPullCreditRequest>() .property("partialContractTerms", codecForPeerContractTerms()) .property("exchangeBaseUrl", codecOptional(codecForCanonBaseUrl())) + .property("progressToken", codecOptional(codecForString())) .build("InitiatePeerPullCreditRequest"); export interface InitiatePeerPullCreditResponse { @@ -4312,10 +4352,11 @@ export interface GetQrCodesForPaytoRequest { paytoUri: string; } -export const codecForGetQrCodesForPaytoRequest = (): Codec<GetQrCodesForPaytoRequest> => - buildCodecForObject<GetQrCodesForPaytoRequest>() - .property("paytoUri", codecForString()) - .build("GetQrCodesForPaytoRequest"); +export const codecForGetQrCodesForPaytoRequest = + (): Codec<GetQrCodesForPaytoRequest> => + buildCodecForObject<GetQrCodesForPaytoRequest>() + .property("paytoUri", codecForString()) + .build("GetQrCodesForPaytoRequest"); export interface GetQrCodesForPaytoResponse { codes: QrCodeSpec[]; @@ -4325,30 +4366,33 @@ export interface GetBankingChoicesForPaytoRequest { paytoUri: string; } -export const codecForGetBankingChoicesForPaytoRequest = (): Codec<GetBankingChoicesForPaytoRequest> => - buildCodecForObject<GetBankingChoicesForPaytoRequest>() - .property("paytoUri", codecForString()) - .build("GetBankingChoicesForPaytoRequest"); +export const codecForGetBankingChoicesForPaytoRequest = + (): Codec<GetBankingChoicesForPaytoRequest> => + buildCodecForObject<GetBankingChoicesForPaytoRequest>() + .property("paytoUri", codecForString()) + .build("GetBankingChoicesForPaytoRequest"); export interface ConvertIbanAccountFieldToPaytoRequest { value: string; currency: string; } -export const codecForConvertIbanAccountFieldToPaytoRequest = (): Codec<ConvertIbanAccountFieldToPaytoRequest> => - buildCodecForObject<ConvertIbanAccountFieldToPaytoRequest>() - .property("value", codecForString()) - .property("currency", codecForString()) - .build("ConvertIbanAccountFieldToPaytoRequest"); +export const codecForConvertIbanAccountFieldToPaytoRequest = + (): Codec<ConvertIbanAccountFieldToPaytoRequest> => + buildCodecForObject<ConvertIbanAccountFieldToPaytoRequest>() + .property("value", codecForString()) + .property("currency", codecForString()) + .build("ConvertIbanAccountFieldToPaytoRequest"); export interface ConvertIbanPaytoToAccountFieldRequest { paytoUri: string; } -export const codecForConvertIbanPaytoToAccountFieldRequest = (): Codec<ConvertIbanPaytoToAccountFieldRequest> => - buildCodecForObject<ConvertIbanPaytoToAccountFieldRequest>() - .property("paytoUri", codecForString()) - .build("ConvertIbanPaytoToAccountFieldRequest"); +export const codecForConvertIbanPaytoToAccountFieldRequest = + (): Codec<ConvertIbanPaytoToAccountFieldRequest> => + buildCodecForObject<ConvertIbanPaytoToAccountFieldRequest>() + .property("paytoUri", codecForString()) + .build("ConvertIbanPaytoToAccountFieldRequest"); export interface ConvertIbanPaytoToAccountFieldResponse { type: "iban" | "bban"; diff --git a/packages/taler-wallet-core/src/exchanges.ts b/packages/taler-wallet-core/src/exchanges.ts @@ -174,7 +174,7 @@ import { PeerPullCreditTransactionContext } from "./pay-peer-pull-credit.js"; import { PeerPullDebitTransactionContext } from "./pay-peer-pull-debit.js"; import { PeerPushCreditTransactionContext } from "./pay-peer-push-credit.js"; import { PeerPushDebitTransactionContext } from "./pay-peer-push-debit.js"; -import { ProgressContext, runWithProgressRetries } from "./progress.js"; +import { runWithProgressRetries } from "./progress.js"; import { RecoupTransactionContext, createRecoupGroup } from "./recoup.js"; import { RefreshTransactionContext, createRefreshGroup } from "./refresh.js"; import { @@ -1283,13 +1283,12 @@ export async function fetchFreshExchange( ): Promise<ReadyExchangeSummary> { logger.trace(`fetch fresh ${baseUrl} forced ${options.forceUpdate}`); - if (wex.progressContext != null && !options.noBail) { - // If there is a progress context, we usually want to keep - // retrying and not bail out early. - logger.warn( - "fetchFreshExchange: progressContext specified but request can bail", - ); - } + // Under a progress context the client can cancel and retry the request, so + // the sensible default is to keep waiting for the exchange rather than give + // up on it: the client decides when to stop. A caller that instead wants + // best-effort behaviour (fail fast and try another exchange) passes + // noBail: false explicitly. + const noBail = options.noBail ?? wex.progressContext != null; const startRes = await startUpdateExchangeEntry(wex, baseUrl, { forceUpdate: options.forceUpdate, @@ -1301,7 +1300,40 @@ export async function fetchFreshExchange( return startRes.readySummary; } - return await waitReadyExchange(wex, startRes.exchangeBaseUrl, options); + return await waitReadyExchange(wex, startRes.exchangeBaseUrl, { + ...options, + noBail, + }); +} + +/** + * Like fetchFreshExchange, but while it waits, a client's "retry now" resets + * the backoff of the exchange update task — which is what actually holds the + * request up. Used by request handlers that block on a specific exchange + * becoming ready before there is a transaction to watch. + */ +export async function fetchFreshExchangeWithRetryNow( + wex: WalletExecutionContext, + baseUrl: string, + options: { + forceUpdate?: boolean; + expectedMasterPub?: string; + noBail?: boolean; + } = {}, +): Promise<ReadyExchangeSummary> { + const progressContext = wex.progressContext; + if (progressContext) { + progressContext.onRetryNow = async () => { + await resetExchangeRetries(wex, baseUrl); + }; + } + try { + return await fetchFreshExchange(wex, baseUrl, options); + } finally { + if (progressContext) { + progressContext.onRetryNow = undefined; + } + } } /** @@ -1337,7 +1369,6 @@ export async function waitReadyExchange( expectedMasterPub?: string; noBail?: boolean; waitAutoRefresh?: boolean; - progressContext?: ProgressContext; } = {}, ): Promise<ReadyExchangeSummary> { logger.trace(`waiting for exchange ${exchangeBaseUrl} to become ready`); @@ -1456,11 +1487,14 @@ export async function waitReadyExchange( } if (!ready) { - if (options.progressContext != null && retryInfo != null) { + // The exchange update runs as its own task, so its retry info is the + // only progress a request waiting here can report. + const progressContext = wex.progressContext; + if (progressContext != null && retryInfo != null) { wex.ws.notify({ type: NotificationType.RequestProgressError, - progressToken: options.progressContext.progressToken, - operation: options.progressContext.operation, + progressToken: progressContext.progressToken, + operation: progressContext.operation, error: retryInfo?.lastError ?? { code: TalerErrorCode.GENERIC_TIMEOUT, }, @@ -2760,18 +2794,9 @@ export async function getExchangeTos( acceptedFormat?: string[], acceptLanguage?: string, ): Promise<GetExchangeTosResult> { - if (wex.progressContext) { - wex.progressContext.onRetryNow = async () => { - await resetExchangeRetries(wex, exchangeBaseUrl); - }; - } - const exch = await fetchFreshExchange(wex, exchangeBaseUrl, { - noBail: wex.progressContext != null, - }); - - if (wex.progressContext) { - delete wex.progressContext.onRetryNow; - } + // noBail follows the progress context (see fetchFreshExchange): with a token + // we keep waiting for the exchange rather than give up on it. + const exch = await fetchFreshExchangeWithRetryNow(wex, exchangeBaseUrl); switch (exch.tosStatus) { case ExchangeTosStatus.MissingTos: diff --git a/packages/taler-wallet-core/src/progress.ts b/packages/taler-wallet-core/src/progress.ts @@ -82,18 +82,27 @@ export async function runWithMaybeProgressContext<T>( oldPc.finished = true; wex.ws.progressMap.delete(key); } + // Restored below, so that a nested context does not clobber the outer one. + const prevPc = wex.progressContext; try { if (!wex.cts) { logger.warn(`no cancellation source for ${op}:${tok}`); } wex.ws.progressMap.set(key, pc); + // Published on the execution context, as that is how code far away from + // the handler (fetchFreshExchange, runWithProgressRetries) finds out + // that the request is retryable and cancellable. + wex.progressContext = pc; emitProgressPhaseNotifications(wex, pc).catch((e) => { logger.warn(safeStringifyException(e)); }); - const res = await f(pc); - pc.finished = true; - return res; + return await f(pc); } finally { + // Marked finished whether the request succeeded or threw, otherwise the + // phase notification loop keeps sending "delayed"/"stalled" after the + // "done" below. + pc.finished = true; + wex.progressContext = prevPc; // Clean up progress token registration, // but only if it is *our* context wex.ws.notify({ @@ -191,6 +200,23 @@ export async function handleRetryProgressTokenNow( } /** + * Throw if the request this progress context belongs to should not run + * anymore, either because the client cancelled it or because a newer request + * with the same progress token superseded it. + */ +function checkRequestNotCancelled( + wex: WalletExecutionContext, + pc: ProgressContext, +): void { + if (wex.cancellationToken.isCancelled || pc.finished) { + throw TalerError.fromDetail( + TalerErrorCode.WALLET_CORE_REQUEST_CANCELLED, + {}, + ); + } +} + +/** * Run with retries if there is a progress context. * Otherwise, just try once. */ @@ -198,45 +224,47 @@ export async function runWithProgressRetries<T>( wex: WalletExecutionContext, f: () => Promise<T>, ): Promise<T> { + const pc = wex.progressContext; + if (!pc) { + return await f(); + } let retryCount = 0; - while (1) { - if ( - wex.progressContext?.cts?.token.isCancelled || - wex.progressContext?.finished - ) { - throw TalerError.fromDetail( - TalerErrorCode.WALLET_CORE_REQUEST_CANCELLED, - {}, - ); - } - const cts = CancellationToken.create(); - if (wex.progressContext != null) { - wex.progressContext.onRetryNow = async () => { + try { + while (true) { + checkRequestNotCancelled(wex, pc); + const cts = CancellationToken.create(); + pc.onRetryNow = async () => { cts.cancel(); retryCount = 0; }; - } - try { - return await f(); - } catch (e) { - if (!wex.progressContext) { - throw e; - } - const delay = getRetryDuration(retryCount); - wex.ws.notify({ - type: NotificationType.RequestProgressError, - operation: wex.progressContext.operation, - progressToken: wex.progressContext.progressToken, - error: TalerError.fromException(e).errorDetail, - nextRetryDelay: Duration.toTalerProtocolDuration(delay), - retryCounter: retryCount, - }); - retryCount++; - const didWait = await wex.ws.timerGroup.resolveAfter(delay, cts.token); - if (!didWait) { - logger.info(`timeout cancelled, retrying immediately`); + try { + return await f(); + } catch (e) { + // A cancelled request must not be reported as an attempt that is + // about to be retried, as no further attempt will follow. + checkRequestNotCancelled(wex, pc); + const delay = getRetryDuration(retryCount); + wex.ws.notify({ + type: NotificationType.RequestProgressError, + operation: pc.operation, + progressToken: pc.progressToken, + error: TalerError.fromException(e).errorDetail, + nextRetryDelay: Duration.toTalerProtocolDuration(delay), + retryCounter: retryCount, + }); + retryCount++; + // Raced against the request's own token, so that cancelling does not + // have to wait out the backoff, which grows to minutes. + const didWait = await wex.ws.timerGroup.resolveAfter( + delay, + CancellationToken.race(cts.token, wex.cancellationToken), + ); + if (!didWait) { + logger.info(`timeout cancelled, retrying immediately`); + } } } + } finally { + pc.onRetryNow = undefined; } - throw Error("not reached"); } diff --git a/packages/taler-wallet-core/src/requests.ts b/packages/taler-wallet-core/src/requests.ts @@ -297,6 +297,7 @@ import { acceptExchangeTermsOfService, deleteExchange, fetchFreshExchange, + fetchFreshExchangeWithRetryNow, forgetExchangeTermsOfService, getExchangeDetailedInfo, getExchangeDetailsInTx, @@ -929,18 +930,25 @@ async function handleAddExchange( // FIXME: Check /config before adding the exchange entry. - // FIXME: We probably should not wait synchronously here. - await fetchFreshExchange(wex, exchangeBaseUrl, {}); - // Exchange has been explicitly added upon user request. - // Thus, we mark it as "used". - if (!req.ephemeral) { - await wex.runWalletDbTx(async (tx) => { - await markExchangeUsed(tx, exchangeBaseUrl); - }); - } - return { - exchangeBaseUrl, - }; + return await runWithMaybeProgressContext( + wex, + "addExchange", + req.progressToken, + async () => { + // FIXME: We probably should not wait synchronously here. + await fetchFreshExchangeWithRetryNow(wex, exchangeBaseUrl); + // Exchange has been explicitly added upon user request. + // Thus, we mark it as "used". + if (!req.ephemeral) { + await wex.runWalletDbTx(async (tx) => { + await markExchangeUsed(tx, exchangeBaseUrl); + }); + } + return { + exchangeBaseUrl, + }; + }, + ); } async function handleUpdateExchangeEntry( @@ -1055,13 +1063,18 @@ async function handleAcceptManualWithdrawal( wex: WalletExecutionContext, req: AcceptManualWithdrawalRequest, ): Promise<AcceptManualWithdrawalResult> { - const res = await createManualWithdrawal(wex, { - amount: Amounts.parseOrThrow(req.amount), - exchangeBaseUrl: req.exchangeBaseUrl, - restrictAge: req.restrictAge, - forceReservePriv: req.forceReservePriv, - }); - return res; + return await runWithMaybeProgressContext( + wex, + "acceptManualWithdrawal", + req.progressToken, + () => + createManualWithdrawal(wex, { + amount: Amounts.parseOrThrow(req.amount), + exchangeBaseUrl: req.exchangeBaseUrl, + restrictAge: req.restrictAge, + forceReservePriv: req.forceReservePriv, + }), + ); } async function handleGetExchangeTos( @@ -1072,7 +1085,7 @@ async function handleGetExchangeTos( wex, "getExchangeTos", req.progressToken, - async (pc) => { + async () => { return getExchangeTos( wex, req.exchangeBaseUrl, @@ -1674,13 +1687,22 @@ async function handleAcceptBankIntegratedWithdrawal( wex: WalletExecutionContext, req: AcceptBankIntegratedWithdrawalRequest, ): Promise<AcceptWithdrawalResponse> { - return await acceptBankIntegratedWithdrawal(wex, { - selectedExchange: req.exchangeBaseUrl, - talerWithdrawUri: req.talerWithdrawUri, - forcedDenomSel: req.forcedDenomSel, - restrictAge: req.restrictAge, - amount: req.amount, - }); + // The context is established here, so the prepare and confirm steps that + // acceptBankIntegratedWithdrawal runs internally report their progress under + // this one token rather than establishing contexts of their own. + return await runWithMaybeProgressContext( + wex, + "acceptBankIntegratedWithdrawal", + req.progressToken, + () => + acceptBankIntegratedWithdrawal(wex, { + selectedExchange: req.exchangeBaseUrl, + talerWithdrawUri: req.talerWithdrawUri, + forcedDenomSel: req.forcedDenomSel, + restrictAge: req.restrictAge, + amount: req.amount, + }), + ); } async function handleGetCurrencySpecification( @@ -2273,7 +2295,12 @@ const handlers: { [T in WalletApiOperation]: HandlerWithValidator<T> } = { [WalletApiOperation.GetWithdrawalDetailsForUri]: { codec: codecForGetWithdrawalDetailsForUri(), handler: (wex, req) => - getWithdrawalDetailsForUri(wex, req.talerWithdrawUri), + runWithMaybeProgressContext( + wex, + "getWithdrawalDetailsForUri", + req.progressToken, + () => getWithdrawalDetailsForUri(wex, req.talerWithdrawUri), + ), }, [WalletApiOperation.TestingGetReserveHistory]: { codec: codecForTestingGetReserveHistoryRequest(), @@ -2315,11 +2342,23 @@ const handlers: { [T in WalletApiOperation]: HandlerWithValidator<T> } = { }, [WalletApiOperation.ConfirmWithdrawal]: { codec: codecForConfirmWithdrawalRequestRequest(), - handler: confirmWithdrawal, + handler: (wex, req) => + runWithMaybeProgressContext( + wex, + "confirmWithdrawal", + req.progressToken, + () => confirmWithdrawal(wex, req), + ), }, [WalletApiOperation.PrepareBankIntegratedWithdrawal]: { codec: codecForPrepareBankIntegratedWithdrawalRequest(), - handler: prepareBankIntegratedWithdrawal, + handler: (wex, req) => + runWithMaybeProgressContext( + wex, + "prepareBankIntegratedWithdrawal", + req.progressToken, + () => prepareBankIntegratedWithdrawal(wex, req), + ), }, [WalletApiOperation.GetExchangeTos]: { codec: codecForGetExchangeTosRequest(),