commit 6a1ebc106552be354906630c916a4639d05435a8
parent 94e39e6b62c1ccf6f97dea8b7d359b229e6a03e1
Author: Florian Dold <dold@taler.net>
Date: Thu, 23 Jul 2026 03:14:52 +0200
wallet: use typed exchange client for /keys and /terms
Diffstat:
2 files changed, 53 insertions(+), 90 deletions(-)
diff --git a/packages/taler-util/src/http-client/exchange-client.ts b/packages/taler-util/src/http-client/exchange-client.ts
@@ -286,8 +286,14 @@ export class TalerExchangeHttpClient {
*
* PARTIALLY IMPLEMENTED!!
*/
- async getKeys(): Promise<OperationOk<ExchangeKeysResponse>> {
- const resp = await this.fetch("keys");
+ async getKeys(
+ opts: { noCache?: boolean } = {},
+ ): Promise<OperationOk<ExchangeKeysResponse>> {
+ const headers: Record<string, string> = {};
+ if (opts.noCache) {
+ headers["cache-control"] = "no-cache";
+ }
+ const resp = await this.fetch("keys", { headers });
switch (resp.status) {
case HttpStatusCode.Ok:
return opSuccessFromHttp(resp, codecForExchangeKeysResponse());
@@ -1575,6 +1581,36 @@ export class TalerExchangeHttpClient {
* https://docs.taler.net/core/api-exchange.html#withdrawal
*/
/**
+ * Metadata about the exchange's terms of service: the current
+ * `taler-terms-version` etag. (A GET is used since not every HTTP backend
+ * supports HEAD.)
+ *
+ * https://docs.taler.net/core/api-exchange.html#terms-of-service
+ */
+ async getTermsMeta(): Promise<
+ | OperationOk<{ etag: string }>
+ | OperationFail<HttpStatusCode.NotFound>
+ | OperationFail<HttpStatusCode.NotImplemented>
+ > {
+ const resp = await this.fetch("terms");
+ switch (resp.status) {
+ case HttpStatusCode.Ok: {
+ const etag = resp.headers.get("taler-terms-version") || "unknown";
+ return opFixedSuccess(resp, { etag });
+ }
+ // The body of these responses is not necessarily a Taler error JSON
+ // (e.g. the exchange returns 501 "not configured" as plain text), so we
+ // must not try to parse it.
+ case HttpStatusCode.NotFound:
+ return opKnownFailure(resp, HttpStatusCode.NotFound);
+ case HttpStatusCode.NotImplemented:
+ return opKnownFailure(resp, HttpStatusCode.NotImplemented);
+ default:
+ return opUnknownHttpFailure(resp);
+ }
+ }
+
+ /**
* https://docs.taler.net/core/api-exchange.html#get--reserves-$RESERVE_PUB
*/
async getReserveStatus(
diff --git a/packages/taler-wallet-core/src/exchanges.ts b/packages/taler-wallet-core/src/exchanges.ts
@@ -98,7 +98,6 @@ import {
assertUnreachable,
checkDbInvariant,
checkLogicInvariant,
- codecForExchangeKeysResponse,
durationMul,
encodeCrock,
getRandomBytes,
@@ -111,9 +110,6 @@ import {
} from "@gnu-taler/taler-util";
import {
HttpRequestLibrary,
- expectSuccessResponseOrThrow,
- readSuccessResponseJsonOrThrow,
- readTalerErrorResponse,
throwUnexpectedRequestError,
} from "@gnu-taler/taler-util/http";
import {
@@ -122,7 +118,6 @@ import {
TaskIdentifiers,
TaskRunResult,
TransactionContext,
- cancelableFetch,
constructTaskIdentifier,
genericWaitForState,
getAutoRefreshExecuteThreshold,
@@ -847,85 +842,26 @@ async function downloadExchangeKeysInfo(
cancellationToken: CancellationToken,
noCache: boolean,
): Promise<ExchangeKeysDownloadResult> {
- const keysUrl = new URL("keys", baseUrl);
-
- const headers: Record<string, string> = {};
- if (noCache) {
- headers["cache-control"] = "no-cache";
- }
- const resp = await http.fetch(keysUrl.href, {
+ const exchangeClient = new TalerExchangeHttpClient(baseUrl, {
+ httpClient: http,
+ cancelationToken: cancellationToken,
timeout,
- cancellationToken,
- headers,
});
+ const resp = await exchangeClient.getKeys({ noCache });
logger.trace("got response to /keys request");
- await expectSuccessResponseOrThrow(resp);
-
- // We must make sure to parse out the protocol version
- // before we validate the body.
- // Otherwise the parser might complain with a hard to understand
- // message about some other field, when it is just a version
- // incompatibility.
-
- let keysJson;
-
- try {
- keysJson = await resp.json();
- } catch (e) {
- throw TalerError.fromDetail(
- TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
- {
- requestUrl: resp.requestUrl,
- requestMethod: resp.requestMethod,
- httpStatusCode: resp.status,
- validationError: e instanceof Error ? e.message : String(e),
- },
- "Couldn't parse JSON format from response",
- );
- }
- const protocolVersion = keysJson.version;
- if (typeof protocolVersion !== "string") {
- throw TalerError.fromDetail(
- TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
- {
- requestUrl: resp.requestUrl,
- requestMethod: resp.requestMethod,
- httpStatusCode: resp.status,
- validationError: "version field missing",
- },
- "bad exchange, does not even specify protocol version",
- );
- }
+ const exchangeKeysResponseParsed = resp.body;
- const versionRes = LibtoolVersion.compare(
- WALLET_EXCHANGE_PROTOCOL_VERSION,
- protocolVersion,
- );
- if (!versionRes) {
- throw TalerError.fromDetail(
- TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
- {
- requestUrl: resp.requestUrl,
- httpStatusCode: resp.status,
- requestMethod: resp.requestMethod,
- },
- "exchange protocol version malformed",
- );
- }
- if (!versionRes.compatible) {
+ if (
+ !TalerExchangeHttpClient.isCompatible(exchangeKeysResponseParsed.version)
+ ) {
return {
type: "version-incompatible",
- exchangeProtocolVersion: protocolVersion,
+ exchangeProtocolVersion: exchangeKeysResponseParsed.version,
};
}
- const exchangeKeysResponseParsed = await readSuccessResponseJsonOrThrow(
- resp,
- codecForExchangeKeysResponse(),
- );
-
if (exchangeKeysResponseParsed.denominations.length === 0) {
throw TalerError.fromDetail(
TalerErrorCode.WALLET_EXCHANGE_DENOMINATIONS_INSUFFICIENT,
@@ -952,27 +888,18 @@ async function downloadTosMeta(
exchangeBaseUrl: string,
): Promise<TosMetaResult> {
logger.trace(`downloading exchange tos metadata for ${exchangeBaseUrl}`);
- const reqUrl = new URL("terms", exchangeBaseUrl);
-
- // FIXME: We can/should make a HEAD request here.
- // Not sure if qtart supports it at the moment.
- const resp = await cancelableFetch(wex, reqUrl);
+ const exchangeClient = walletExchangeClient(exchangeBaseUrl, wex);
+ const resp = await exchangeClient.getTermsMeta();
- switch (resp.status) {
+ switch (resp.case) {
+ case "ok":
+ return { type: "ok", etag: resp.body.etag };
case HttpStatusCode.NotFound:
case HttpStatusCode.NotImplemented:
return { type: "not-found" };
- case HttpStatusCode.Ok:
- break;
default:
- throwUnexpectedRequestError(resp, await readTalerErrorResponse(resp));
+ return assertUnreachable(resp);
}
-
- const etag = resp.headers.get("taler-terms-version") || "unknown";
- return {
- type: "ok",
- etag,
- };
}
/**