commit 4c651dcda0749046d495916227f39a5214bb527f
parent 7535b45024251cfc23489f48b9b2bc7ff8a1cf34
Author: Florian Dold <florian@dold.me>
Date: Wed, 15 Jul 2026 16:39:35 +0200
support progress token for getExchangeTos
Diffstat:
5 files changed, 116 insertions(+), 110 deletions(-)
diff --git a/packages/taler-util/src/http-client/exchange-client.ts b/packages/taler-util/src/http-client/exchange-client.ts
@@ -266,6 +266,30 @@ export class TalerExchangeHttpClient {
}
}
+ async getTermsText(
+ args: {
+ acceptLanguage?: string;
+ acceptFormats?: string[];
+ } = {},
+ ): Promise<OperationOk<{ text: string }>> {
+ const acceptFormats = args.acceptFormats ?? ["text/plain"];
+ const headers = {
+ Accept: acceptFormats.join(", "),
+ "Accept-Language": args.acceptLanguage,
+ };
+ const resp = await this.fetch("terms", {
+ headers,
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opFixedSuccess(resp, {
+ text: await resp.text(),
+ });
+ default:
+ return opUnknownHttpFailure(resp);
+ }
+ }
+
// WALLET TO WALLET
/**
diff --git a/packages/taler-util/src/types-taler-wallet.ts b/packages/taler-util/src/types-taler-wallet.ts
@@ -2334,6 +2334,7 @@ export interface GetExchangeTosRequest {
exchangeBaseUrl: string;
acceptedFormat?: string[];
acceptLanguage?: string;
+ progressToken?: string;
}
export const codecForGetExchangeTosRequest = (): Codec<GetExchangeTosRequest> =>
@@ -2341,6 +2342,7 @@ export const codecForGetExchangeTosRequest = (): Codec<GetExchangeTosRequest> =>
.property("exchangeBaseUrl", codecForCanonBaseUrl())
.property("acceptedFormat", codecOptional(codecForList(codecForString())))
.property("acceptLanguage", codecOptional(codecForString()))
+ .property("progressToken", codecOptional(codecForString()))
.build("GetExchangeTosRequest");
export interface AcceptManualWithdrawalRequest {
diff --git a/packages/taler-wallet-core/src/common.ts b/packages/taler-wallet-core/src/common.ts
@@ -567,6 +567,11 @@ const defaultRetryPolicy: RetryPolicy = {
maxTimeout: Duration.fromSpec({ minutes: 2 }),
};
+export function getRetryDuration(count: number): Duration {
+ const p = defaultRetryPolicy;
+ return Duration.multiply(p.backoffDelta, Math.pow(p.backoffBase, count));
+}
+
function updateTimeout(
r: DbRetryInfo,
p: RetryPolicy = defaultRetryPolicy,
diff --git a/packages/taler-wallet-core/src/exchanges.ts b/packages/taler-wallet-core/src/exchanges.ts
@@ -73,6 +73,7 @@ import {
TalerError,
TalerErrorCode,
TalerErrorDetail,
+ TalerExchangeHttpClient,
TalerPreciseTimestamp,
TalerProtocolDuration,
TalerProtocolTimestamp,
@@ -114,7 +115,6 @@ import {
HttpRequestLibrary,
readResponseJsonOrThrow,
readSuccessResponseJsonOrThrow,
- readSuccessResponseTextOrThrow,
readTalerErrorResponse,
throwUnexpectedRequestError,
} from "@gnu-taler/taler-util/http";
@@ -133,6 +133,7 @@ import {
getExchangeState,
getExchangeTosStatusFromRecord,
getExchangeUpdateStatusFromRecord,
+ getRetryDuration,
} from "./common.js";
import {
DenomFamilyParams,
@@ -208,64 +209,6 @@ interface ExchangeTosDownloadResult {
tosAvailableLanguages: string[];
}
-async function downloadExchangeWithTermsOfService(
- wex: WalletExecutionContext,
- exchangeBaseUrl: string,
- http: HttpRequestLibrary,
- timeout: Duration,
- acceptFormat: string,
- acceptLanguage: string | undefined,
-): Promise<ExchangeTosDownloadResult> {
- logger.trace(`downloading exchange tos (type ${acceptFormat})`);
- const reqUrl = new URL("terms", exchangeBaseUrl);
- const headers: {
- Accept: string;
- "Accept-Language"?: string;
- } = {
- Accept: acceptFormat,
- };
-
- if (acceptLanguage) {
- headers["Accept-Language"] = acceptLanguage;
- }
-
- const resp = await http.fetch(reqUrl.href, {
- headers,
- timeout,
- cancellationToken: wex.cancellationToken,
- });
-
- if (
- (resp.status >= 500 && resp.status <= 599) ||
- (resp.status >= 400 && resp.status >= 499)
- ) {
- const innerError = await readTalerErrorResponse(resp);
- throw TalerError.fromDetail(TalerErrorCode.WALLET_EXCHANGE_UNAVAILABLE, {
- exchangeBaseUrl,
- innerError,
- });
- }
-
- const tosText = await readSuccessResponseTextOrThrow(resp);
- const tosEtag = resp.headers.get("taler-terms-version") || "unknown";
- const tosContentLanguage = resp.headers.get("content-language") || undefined;
- const tosContentType = resp.headers.get("content-type") || "text/plain";
- const availLangStr = resp.headers.get("avail-languages") || "";
- // Work around exchange bug that reports the same language multiple times.
- const availLangSet = new Set<string>(
- availLangStr.split(",").map((x) => x.trim()),
- );
- const tosAvailableLanguages = [...availLangSet];
-
- return {
- tosText,
- tosEtag,
- tosContentType,
- tosContentLanguage,
- tosAvailableLanguages,
- };
-}
-
/**
* Get exchange details from the database.
*/
@@ -1008,44 +951,6 @@ async function downloadTosMeta(
};
}
-async function downloadTosFromAcceptedFormat(
- wex: WalletExecutionContext,
- baseUrl: string,
- timeout: Duration,
- acceptedFormat?: string[],
- acceptLanguage?: string,
-): Promise<ExchangeTosDownloadResult> {
- let tosFound: ExchangeTosDownloadResult | undefined;
- // Remove this when exchange supports multiple content-type in accept header
- if (acceptedFormat)
- for (const format of acceptedFormat) {
- const resp = await downloadExchangeWithTermsOfService(
- wex,
- baseUrl,
- wex.http,
- timeout,
- format,
- acceptLanguage,
- );
- if (resp.tosContentType === format) {
- tosFound = resp;
- break;
- }
- }
- if (tosFound !== undefined) {
- return tosFound;
- }
- // If none of the specified format was found try text/plain
- return await downloadExchangeWithTermsOfService(
- wex,
- baseUrl,
- wex.http,
- timeout,
- "text/plain",
- acceptLanguage,
- );
-}
-
/**
* Check if an exchange entry should be considered
* to be outdated.
@@ -2811,8 +2716,11 @@ export async function getExchangeTos(
exchangeBaseUrl: string,
acceptedFormat?: string[],
acceptLanguage?: string,
+ progressContext?: ProgressContext,
): Promise<GetExchangeTosResult> {
- const exch = await fetchFreshExchange(wex, exchangeBaseUrl);
+ const exch = await fetchFreshExchange(wex, exchangeBaseUrl, {
+ progressContext,
+ });
switch (exch.tosStatus) {
case ExchangeTosStatus.MissingTos:
@@ -2827,13 +2735,67 @@ export async function getExchangeTos(
};
}
- const tosDownload = await downloadTosFromAcceptedFormat(
- wex,
- exchangeBaseUrl,
- getExchangeRequestTimeout(),
- acceptedFormat,
- acceptLanguage,
- );
+ const exchangeClient = new TalerExchangeHttpClient(exchangeBaseUrl, {
+ cancelationToken: wex.cancellationToken,
+ httpClient: wex.http,
+ });
+
+ let tosDownload: ExchangeTosDownloadResult | undefined;
+
+ // FIXME: pull out retry logic into helper.
+
+ let retryCount = 0;
+
+ while (1) {
+ if (progressContext?.cts?.token.isCancelled || progressContext?.finished) {
+ // FIXME: Error code!
+ throw Error("cancelled");
+ }
+ try {
+ const tosRes = await exchangeClient.getTermsText({
+ acceptFormats: acceptedFormat,
+ acceptLanguage: acceptLanguage,
+ });
+
+ switch (tosRes.type) {
+ case "ok":
+ tosDownload = {
+ tosAvailableLanguages:
+ tosRes.response.headers.get("avail-languages")?.split(",") ?? [],
+ tosContentLanguage:
+ tosRes.response.headers.get("content-language") ?? "en",
+ tosContentType:
+ tosRes.response.headers.get("content-type") ?? "text/plain",
+ tosEtag: tosRes.response.headers.get("etag") ?? "",
+ tosText: tosRes.body.text,
+ };
+ break;
+ default:
+ continue;
+ }
+ } catch (e) {
+ if (!progressContext) {
+ throw e;
+ }
+ const delay = getRetryDuration(retryCount);
+ wex.ws.notify({
+ type: NotificationType.RequestProgress,
+ operation: progressContext.operation,
+ progressToken: progressContext.progressToken,
+ error: TalerError.fromException(e).errorDetail,
+ nextRetryDelay: Duration.toTalerProtocolDuration(delay),
+ retryCounter: retryCount,
+ });
+ retryCount++;
+ await wex.ws.timerGroup.resolveAfter(delay);
+ }
+
+ if (tosDownload != null) {
+ break;
+ }
+ }
+
+ checkLogicInvariant(!!tosDownload);
await wex.runLegacyWalletDbTx(async (tx) => {
const updateExchangeEntry = await tx.exchanges.get(exchangeBaseUrl);
diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts
@@ -331,7 +331,11 @@ import {
} from "./denominations.js";
import { UnverifiedDenomError } from "./denomSelection.js";
import { checkDepositGroup, createDepositGroup } from "./deposits.js";
-import { DevExperimentHttpLib, DevExperimentState, applyDevExperiment } from "./dev-experiments.js";
+import {
+ DevExperimentHttpLib,
+ DevExperimentState,
+ applyDevExperiment,
+} from "./dev-experiments.js";
import {
handleGetDonau,
handleGetDonauStatements,
@@ -1272,11 +1276,20 @@ async function handleGetExchangeTos(
wex: WalletExecutionContext,
req: GetExchangeTosRequest,
): Promise<GetExchangeTosResult> {
- return getExchangeTos(
+ return await withMaybeProgressContext(
wex,
- req.exchangeBaseUrl,
- req.acceptedFormat,
- req.acceptLanguage,
+ "getExchangeTos",
+ req.progressToken,
+ undefined,
+ async (pc) => {
+ return getExchangeTos(
+ wex,
+ req.exchangeBaseUrl,
+ req.acceptedFormat,
+ req.acceptLanguage,
+ pc,
+ );
+ },
);
}