taler-typescript-core

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

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:
Mpackages/taler-util/src/http-client/exchange-client.ts | 15+++++++++------
Mpackages/taler-wallet-core/src/exchanges.ts | 142++++++++++++++++++++++++++++---------------------------------------------------
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!); } }