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:
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(),