commit 94e39e6b62c1ccf6f97dea8b7d359b229e6a03e1
parent 76278d5bce0e73a54c269a093649aaf87e416ec9
Author: Florian Dold <dold@taler.net>
Date: Thu, 23 Jul 2026 03:07:48 +0200
wallet: use typed exchange client for exchange-side KYC
Diffstat:
2 files changed, 59 insertions(+), 98 deletions(-)
diff --git a/packages/taler-util/src/http-client/exchange-client.ts b/packages/taler-util/src/http-client/exchange-client.ts
@@ -632,12 +632,15 @@ export class TalerExchangeHttpClient {
LegitimizationNeededResponse
>
> {
- const url = new URL(`kyc-wallet`, this.baseUrl);
-
- const resp = await this.httpLib.fetch(url.href, {
- method: "POST",
- body,
- });
+ // Long-polls until the wallet's KYC/balance limit is resolved.
+ const resp = await this.fetch(
+ `kyc-wallet`,
+ {
+ method: "POST",
+ body,
+ },
+ true,
+ );
switch (resp.status) {
case HttpStatusCode.Ok:
diff --git a/packages/taler-wallet-core/src/exchanges.ts b/packages/taler-wallet-core/src/exchanges.ts
@@ -98,10 +98,7 @@ import {
assertUnreachable,
checkDbInvariant,
checkLogicInvariant,
- codecForAccountKycStatus,
- codecForAmlWalletKycCheckResponse,
codecForExchangeKeysResponse,
- codecForLegitimizationNeededResponse,
durationMul,
encodeCrock,
getRandomBytes,
@@ -115,7 +112,6 @@ import {
import {
HttpRequestLibrary,
expectSuccessResponseOrThrow,
- readResponseJsonOrThrow,
readSuccessResponseJsonOrThrow,
readTalerErrorResponse,
throwUnexpectedRequestError,
@@ -127,7 +123,6 @@ import {
TaskRunResult,
TransactionContext,
cancelableFetch,
- cancelableLongPoll,
constructTaskIdentifier,
genericWaitForState,
getAutoRefreshExecuteThreshold,
@@ -3743,53 +3738,36 @@ async function handleExchangeKycPendingWallet(
reservePub: reserve.reservePub,
threshold,
});
- const requestUrl = new URL("kyc-wallet", exchange.baseUrl);
const body: WalletKycRequest = {
balance: reserve.thresholdRequested,
reserve_pub: reserve.reservePub,
reserve_sig: sigResp.sig,
};
logger.info(`kyc-wallet request body: ${j2s(body)}`);
- const res = await cancelableLongPoll(wex, requestUrl, {
- method: "POST",
- body,
- });
+ const exchangeClient = walletExchangeClient(exchange.baseUrl, wex);
+ const res = await exchangeClient.notifyKycBalanceLimit(body);
- logger.info(`kyc-wallet response status is ${res.status}`);
+ logger.info(`kyc-wallet response status is ${res.response.status}`);
- switch (res.status) {
- case HttpStatusCode.Ok: {
- // KYC somehow already passed
- const walletKycResp = await readSuccessResponseJsonOrThrow(
- res,
- codecForAmlWalletKycCheckResponse(),
- );
+ switch (res.case) {
+ case "ok":
+ // 200: KYC somehow already passed (with a next threshold);
+ // 204: KYC disabled at the exchange.
return handleExchangeKycSuccess(
wex,
exchange.baseUrl,
- walletKycResp.next_threshold,
+ res.body?.next_threshold,
);
- }
- case HttpStatusCode.NoContent: {
- // KYC disabled at exchange.
- return handleExchangeKycSuccess(wex, exchange.baseUrl, undefined);
- }
- case HttpStatusCode.Forbidden: {
- // Did not work!
- const err = await readTalerErrorResponse(res);
- throwUnexpectedRequestError(res, err);
- }
- case HttpStatusCode.UnavailableForLegalReasons: {
- const kycBody = await readResponseJsonOrThrow(
- res,
- codecForLegitimizationNeededResponse(),
+ case HttpStatusCode.UnavailableForLegalReasons:
+ return handleExchangeKycRespLegi(
+ wex,
+ exchange.baseUrl,
+ reserve,
+ res.body,
);
- return handleExchangeKycRespLegi(wex, exchange.baseUrl, reserve, kycBody);
- }
- default: {
- const err = await readTalerErrorResponse(res);
- throwUnexpectedRequestError(res, err);
- }
+ default:
+ // Forbidden (or, via the client, any unexpected status) => error.
+ return throwUnexpectedRequestError(res.response, res.detail!);
}
}
@@ -3869,49 +3847,36 @@ async function handleExchangeKycRespLegi(
accountPriv: reserve.reservePriv,
accountPub: reserve.reservePub,
});
- const reqUrl = new URL(`kyc-check/${kycBody.h_payto}`, exchangeBaseUrl);
- const resp = await cancelableFetch(wex, reqUrl, {
- method: "GET",
- headers: {
- ["Account-Owner-Signature"]: sigResp.sig,
- ["Account-Owner-Pub"]: reserve.reservePub,
- },
+ const exchangeClient = walletExchangeClient(exchangeBaseUrl, wex);
+ const resp = await exchangeClient.checkKycStatus({
+ paytoHash: kycBody.h_payto,
+ accountPub: reserve.reservePub,
+ accountSig: sigResp.sig,
});
- logger.info(`kyc-check response status ${resp.status}`);
+ logger.info(`kyc-check response status ${resp.response.status}`);
- switch (resp.status) {
- case HttpStatusCode.Ok: {
+ switch (resp.case) {
+ case "ok": {
// FIXME: Store information about next limit!
- const accountKycStatus = await readSuccessResponseJsonOrThrow(
- resp,
- codecForAccountKycStatus(),
- );
- logger.trace(`balance KYC account status: ${j2s(accountKycStatus)}`);
+ logger.trace(`balance KYC account status: ${j2s(resp.body)}`);
const nextLimit = findNextBalanceLimit(
- accountKycStatus,
+ resp.body,
reserve.thresholdGranted,
);
return handleExchangeKycSuccess(wex, exchangeBaseUrl, nextLimit);
}
- case HttpStatusCode.Accepted: {
- // Store the result in the DB!
+ case HttpStatusCode.Accepted:
+ // Store the result in the DB (below).
break;
- }
- case HttpStatusCode.NoContent: {
+ case HttpStatusCode.NoContent:
// KYC not configured, so already satisfied
return handleExchangeKycSuccess(wex, exchangeBaseUrl, undefined);
- }
- default: {
- const err = await readTalerErrorResponse(resp);
- throwUnexpectedRequestError(resp, err);
- }
+ default:
+ return throwUnexpectedRequestError(resp.response, resp.detail!);
}
- const accountKycStatusResp = await readResponseJsonOrThrow(
- resp,
- codecForAccountKycStatus(),
- );
+ const accountKycStatusResp = resp.body;
await wex.runWalletDbTx(async (tx) => {
const exchange = await tx.getExchange(exchangeBaseUrl);
@@ -3971,30 +3936,26 @@ async function handleExchangeKycPendingLegitimization(
const paytoHash = encodeCrock(Paytos.hashFull(reservePayto));
- const reqUrl = new URL(`kyc-check/${paytoHash}`, exchange.baseUrl);
// We already have the access token.
- // Long-poll for target 3 <-> KYC status to be OK
- reqUrl.searchParams.set("lpt", "3");
- logger.info(`long-polling wallet KYC status at ${reqUrl.href}`);
- const resp = await cancelableLongPoll(wex, reqUrl, {
- headers: {
- ["Account-Owner-Signature"]: sigResp.sig,
- ["Account-Owner-Pub"]: reserve.reservePub,
- },
+ // Long-poll for target 3 <-> KYC status to be OK.
+ logger.info(`long-polling wallet KYC status for ${paytoHash}`);
+ const exchangeClient = walletExchangeClient(exchange.baseUrl, wex);
+ const resp = await exchangeClient.checkKycStatus({
+ paytoHash,
+ accountPub: reserve.reservePub,
+ accountSig: sigResp.sig,
+ longpoll: true,
+ lpt: 3,
});
- logger.info(`kyc-check (long-poll) response status ${resp.status}`);
+ logger.info(`kyc-check (long-poll) response status ${resp.response.status}`);
- switch (resp.status) {
- case HttpStatusCode.Ok: {
+ switch (resp.case) {
+ case "ok": {
// FIXME: Store information about next limit!
- const accountKycStatus = await readSuccessResponseJsonOrThrow(
- resp,
- codecForAccountKycStatus(),
- );
- logger.info(`balance KYC account status: ${j2s(accountKycStatus)}`);
+ logger.info(`balance KYC account status: ${j2s(resp.body)}`);
const nextLimit = findNextBalanceLimit(
- accountKycStatus,
+ resp.body,
reserve.thresholdGranted,
);
logger.info(
@@ -4007,14 +3968,11 @@ async function handleExchangeKycPendingLegitimization(
case HttpStatusCode.Accepted:
// FIXME: Do we ever need to update the access token?
return TaskRunResult.longpollReturnedPending();
- case HttpStatusCode.NoContent: {
+ case HttpStatusCode.NoContent:
// KYC not configured, so already satisfied
return handleExchangeKycSuccess(wex, exchange.baseUrl, undefined);
- }
- default: {
- const err = await readTalerErrorResponse(resp);
- throwUnexpectedRequestError(resp, err);
- }
+ default:
+ return throwUnexpectedRequestError(resp.response, resp.detail!);
}
}