taler-typescript-core

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

commit 3c04a0703f3d3a3b05c68d470539aa8f0b0b5dc7
parent 357f9d6eef887a5616c67c58dffaea78f1e5eb41
Author: Antoine A <>
Date:   Thu, 24 Apr 2025 10:44:45 +0200

wallet-core: clean P2P and KYC status

Diffstat:
Mpackages/kyc-ui/src/pages/TriggerKyc.tsx | 70+++++++++++++++++++++++++++++++++++-----------------------------------
Mpackages/taler-harness/src/index.ts | 16+++++++++-------
Mpackages/taler-util/src/http-client/exchange.ts | 98+++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------
Mpackages/taler-wallet-core/src/pay-peer-pull-credit.ts | 135+++++++++++++++++++++++++++++++------------------------------------------------
Mpackages/taler-wallet-core/src/pay-peer-pull-debit.ts | 5+----
Mpackages/taler-wallet-core/src/pay-peer-push-credit.ts | 29+++++++++--------------------
6 files changed, 173 insertions(+), 180 deletions(-)

diff --git a/packages/kyc-ui/src/pages/TriggerKyc.tsx b/packages/kyc-ui/src/pages/TriggerKyc.tsx @@ -13,6 +13,7 @@ You should have received a copy of the GNU General Public License along with GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ +import { CancellationToken } from "@gnu-taler/taler-util"; import { AbsoluteTime, AccessToken, @@ -90,44 +91,43 @@ export function TriggerKyc({ onKycStarted }: Props): VNode { const paytoHash = kycAccount; async function check() { const { signingKey } = await accountPromise; - const result = await lib.exchange.checkKycStatus(signingKey, paytoHash); - if (result.type === "ok") { - if (result.body) { - onKycStarted(result.body.access_token); - } else { + const result = await lib.exchange.checkKycStatus(signingKey, paytoHash, CancellationToken.CONTINUE); + switch (result.case) { + case "ok": console.log("empty body"); - } - } else { - switch (result.case) { - case HttpStatusCode.Forbidden: { - notify({ - type: "error", - title: i18n.str`could not create token`, - description: i18n.str`access denied`, - when: AbsoluteTime.now(), - }); + break; + case HttpStatusCode.Ok: + case HttpStatusCode.Accepted: + onKycStarted(result.body.access_token); + break + case HttpStatusCode.Forbidden: { + notify({ + type: "error", + title: i18n.str`could not create token`, + description: i18n.str`access denied`, + when: AbsoluteTime.now(), + }); - break; - } - case HttpStatusCode.NotFound: { - notify({ - type: "error", - title: i18n.str`could not create token`, - description: i18n.str`not found`, - when: AbsoluteTime.now(), - }); + break; + } + case HttpStatusCode.NotFound: { + notify({ + type: "error", + title: i18n.str`could not create token`, + description: i18n.str`not found`, + when: AbsoluteTime.now(), + }); - break; - } - case HttpStatusCode.Conflict: { - notify({ - type: "error", - title: i18n.str`could not create token`, - description: i18n.str`conflict`, - when: AbsoluteTime.now(), - }); - break; - } + break; + } + case HttpStatusCode.Conflict: { + notify({ + type: "error", + title: i18n.str`could not create token`, + description: i18n.str`conflict`, + when: AbsoluteTime.now(), + }); + break; } } } diff --git a/packages/taler-harness/src/index.ts b/packages/taler-harness/src/index.ts @@ -23,6 +23,7 @@ import { AmountString, Amounts, BalancesResponse, + CancellationToken, Configuration, Duration, EddsaPrivP, @@ -919,9 +920,9 @@ deploymentCli contact_data: email || phone ? { - email: email, - phone: phone, - } + email: email, + phone: phone, + } : undefined, }); @@ -1266,10 +1267,10 @@ deploymentCli credit_facade_credentials: bankUser && bankPassword ? { - type: "basic", - username: bankUser, - password: bankPassword, - } + type: "basic", + username: bankUser, + password: bankPassword, + } : undefined, }, ); @@ -1746,6 +1747,7 @@ merchantCli const kyc_status = await exchangeApi.checkKycStatus( merchant_priv, encodeCrock(hashNormalizedPaytoUri(parsePaytoUri(payto_uri)!)), + CancellationToken.CONTINUE ); return { payto_uri, diff --git a/packages/taler-util/src/http-client/exchange.ts b/packages/taler-util/src/http-client/exchange.ts @@ -32,7 +32,6 @@ import { opEmptySuccess, opFixedSuccess, opKnownAlternativeFailure, - opKnownFailure, opKnownHttpFailure, opSuccessFromHttp, opUnknownFailure, @@ -48,6 +47,7 @@ import { codecForTalerCommonConfigResponse, } from "../types-taler-common.js"; import { + AccountKycStatus, AmlDecisionRequest, BatchWithdrawResponse, ExchangeKycUploadFormRequest, @@ -55,6 +55,7 @@ import { ExchangeMergeConflictResponse, ExchangeMergeSuccessResponse, ExchangePurseMergeRequest, + ExchangeReservePurseRequest, ExchangeVersionResponse, KycRequirementInformationId, LegitimizationNeededResponse, @@ -157,12 +158,11 @@ export class TalerExchangeHttpClient { case HttpStatusCode.Ok: const buffer = await resp.bytes(); const uintar = new Uint8Array(buffer); - return opFixedSuccess(uintar); case HttpStatusCode.NotFound: return opKnownHttpFailure(resp.status, resp); default: - return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + return opUnknownHttpFailure(resp); } } /** @@ -214,7 +214,7 @@ export class TalerExchangeHttpClient { case HttpStatusCode.NotFound: return opKnownHttpFailure(resp.status, resp); default: - return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + return opUnknownHttpFailure(resp); } } @@ -232,7 +232,7 @@ export class TalerExchangeHttpClient { case HttpStatusCode.Ok: return opSuccessFromHttp(resp, codecForExchangeKeysResponse()); default: - return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + return opUnknownHttpFailure(resp); } } @@ -394,13 +394,9 @@ export class TalerExchangeHttpClient { codecForAny() as Codec<BatchWithdrawResponse>, ); case HttpStatusCode.Forbidden: - return opKnownHttpFailure(resp.status, resp); case HttpStatusCode.BadRequest: - return opKnownHttpFailure(resp.status, resp); case HttpStatusCode.NotFound: - return opKnownHttpFailure(resp.status, resp); case HttpStatusCode.Conflict: - return opKnownHttpFailure(resp.status, resp); case HttpStatusCode.Gone: return opKnownHttpFailure(resp.status, resp); case HttpStatusCode.UnavailableForLegalReasons: @@ -410,7 +406,7 @@ export class TalerExchangeHttpClient { codecForLegitimizationNeededResponse(), ); default: - return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + return opUnknownHttpFailure(resp); } } @@ -581,11 +577,11 @@ export class TalerExchangeHttpClient { * * https://docs.taler.net/core/api-exchange.html#post--purses-$PURSE_PUB-merge */ - async postPurseMerge(args: { - pursePub: string; - body: ExchangePurseMergeRequest; - cancellationToken: CancellationToken; - }): Promise< + async postPurseMerge( + pursePub: string, + body: ExchangePurseMergeRequest, + cancellationToken: CancellationToken + ): Promise< | OperationOk<ExchangeMergeSuccessResponse> | OperationAlternative< HttpStatusCode.UnavailableForLegalReasons, @@ -598,14 +594,14 @@ export class TalerExchangeHttpClient { | OperationFail<HttpStatusCode.NotFound> | OperationFail<HttpStatusCode.Gone> > { - const mergePurseUrl = new URL( - `purses/${args.pursePub}/merge`, + const url = new URL( + `purses/${pursePub}/merge`, this.baseUrl, ); - const resp = await this.httpLib.fetch(mergePurseUrl.href, { + const resp = await this.httpLib.fetch(url.href, { method: "POST", - body: args.body, - cancellationToken: args.cancellationToken + body, + cancellationToken }); switch (resp.status) { case HttpStatusCode.Ok: @@ -634,8 +630,39 @@ export class TalerExchangeHttpClient { * https://docs.taler.net/core/api-exchange.html#post--reserves-$RESERVE_PUB-purse * */ - async createPurseFromReserve(): Promise<never> { - throw Error("not yet implemented"); + async createPurseFromReserve( + pursePub: string, + body: ExchangeReservePurseRequest, + cancellationToken: CancellationToken, + ): Promise< + | OperationOk<void> + | OperationAlternative< + HttpStatusCode.UnavailableForLegalReasons, + LegitimizationNeededResponse + > + > { + const url = new URL( + `reserves/${pursePub}/purse`, + this.baseUrl, + ); + const resp = await this.httpLib.fetch(url.href, { + method: "POST", + body, + cancellationToken + }); + switch (resp.status) { + case HttpStatusCode.Ok: + // FIXME: parse PurseCreateSuccessResponse + return opSuccessFromHttp(resp, codecForAny()); + case HttpStatusCode.UnavailableForLegalReasons: + return opKnownAlternativeFailure( + resp, + resp.status, + codecForLegitimizationNeededResponse(), + ); + default: + return opUnknownHttpFailure(resp); + } } /** @@ -699,17 +726,25 @@ export class TalerExchangeHttpClient { } /** - * https://docs.taler.net/core/api-exchange.html#get--kyc-check-$H_PAYTO + * https://docs.taler.net/core/api-exchange.html#get--kyc-check-$H_NORMALIZED_PAYTO * */ async checkKycStatus( - signingKey: EddsaPrivP, + signingKey: EddsaPrivP | string, paytoHash: string, + cancellationToken: CancellationToken, params: { timeout?: number; awaitAuth?: boolean; } = {}, - ) { + ): Promise< + | OperationOk<void> + | OperationAlternative<HttpStatusCode.Ok, AccountKycStatus> + | OperationAlternative<HttpStatusCode.Accepted, AccountKycStatus> + | OperationFail<HttpStatusCode.Forbidden> + | OperationFail<HttpStatusCode.NotFound> + | OperationFail<HttpStatusCode.Conflict> + > { const url = new URL(`kyc-check/${paytoHash}`, this.baseUrl); if (params.timeout !== undefined) { @@ -719,28 +754,27 @@ export class TalerExchangeHttpClient { url.searchParams.set("await_auth", params.awaitAuth ? "YES" : "NO"); } + const signature = typeof signingKey === 'string' ? signingKey : encodeCrock(signKycAuth(signingKey)); + const resp = await this.httpLib.fetch(url.href, { - method: "GET", headers: { - "Account-Owner-Signature": encodeCrock(signKycAuth(signingKey)), + "Account-Owner-Signature": signature, }, + cancellationToken }); switch (resp.status) { case HttpStatusCode.Ok: - return opSuccessFromHttp(resp, codecForAccountKycStatus()); case HttpStatusCode.Accepted: - return opSuccessFromHttp(resp, codecForAccountKycStatus()); + return opKnownAlternativeFailure(resp, resp.status, codecForAccountKycStatus()); case HttpStatusCode.NoContent: return opFixedSuccess(undefined); case HttpStatusCode.Forbidden: - return opKnownHttpFailure(resp.status, resp); case HttpStatusCode.NotFound: - return opKnownHttpFailure(resp.status, resp); case HttpStatusCode.Conflict: return opKnownHttpFailure(resp.status, resp); default: - return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + return opUnknownHttpFailure(resp); } } diff --git a/packages/taler-wallet-core/src/pay-peer-pull-credit.ts b/packages/taler-wallet-core/src/pay-peer-pull-credit.ts @@ -31,6 +31,7 @@ import { ScopeType, TalerErrorCode, TalerErrorDetail, + TalerExchangeHttpClient, TalerPreciseTimestamp, TalerProtocolTimestamp, TalerUriAction, @@ -46,8 +47,6 @@ import { assertUnreachable, checkDbInvariant, codecForAccountKycStatus, - codecForAny, - codecForLegitimizationNeededResponse, encodeCrock, getRandomBytes, j2s, @@ -364,10 +363,7 @@ export class PeerPullCreditTransactionContext implements TransactionContext { exchangeBaseUrl: wsr.exchangeBaseUrl, contractPriv: wsr.wgInfo.contractPriv, }), - transactionId: constructTransactionIdentifier({ - tag: TransactionType.PeerPullCredit, - pursePub: pullCredit.pursePub, - }), + transactionId: this.transactionId, abortReason: pullCredit.abortReason, failReason: pullCredit.failReason, // FIXME: Is this the KYC URL of the withdrawal group?! @@ -402,10 +398,7 @@ export class PeerPullCreditTransactionContext implements TransactionContext { exchangeBaseUrl: pullCredit.exchangeBaseUrl, contractPriv: pullCredit.contractPriv, }), - transactionId: constructTransactionIdentifier({ - tag: TransactionType.PeerPullCredit, - pursePub: pullCredit.pursePub, - }), + transactionId: this.transactionId, kycUrl, kycAccessToken: pullCredit.kycAccessToken, kycPaytoHash: pullCredit.kycPaytoHash, @@ -633,9 +626,7 @@ async function queryPurseForPeerPullCredit( const reserve = await wex.db.runReadOnlyTx( { storeNames: ["reserves"] }, - async (tx) => { - return await tx.reserves.get(pullIni.mergeReserveRowId); - }, + (tx) => tx.reserves.get(pullIni.mergeReserveRowId) ); if (!reserve) { @@ -848,29 +839,31 @@ async function handlePeerPullCreditCreatePurse( econtract: econtractResp.econtract, }; - logger.info(`reserve purse request: ${j2s(reservePurseReqBody)}`); - - const reservePurseMergeUrl = new URL( - `reserves/${mergeReserve.reservePub}/purse`, + const exchangeClient = new TalerExchangeHttpClient( pullIni.exchangeBaseUrl, + wex.http, ); - const httpResp = await cancelableFetch(wex, reservePurseMergeUrl, { - method: "POST", - body: reservePurseReqBody, - }); + logger.info(`reserve purse request: ${j2s(reservePurseReqBody)}`); - if (httpResp.status === HttpStatusCode.UnavailableForLegalReasons) { - const respJson = await httpResp.json(); - const kycPending = codecForLegitimizationNeededResponse().decode(respJson); - logger.info(`kyc uuid response: ${j2s(kycPending)}`); - return processPeerPullCreditKycRequired(wex, pullIni, kycPending.h_payto); + const httpResp = await exchangeClient.createPurseFromReserve( + mergeReserve.reservePub, + reservePurseReqBody, + wex.cancellationToken + ) + + switch (httpResp.case) { + case "ok": + logger.info(`reserve merge response: ${j2s((await httpResp).body)}`); + break; + case HttpStatusCode.UnavailableForLegalReasons: { + logger.info(`kyc uuid response: ${j2s(httpResp.body)}`); + return processPeerPullCreditKycRequired(wex, pullIni, httpResp.body.h_payto); + } + default: + assertUnreachable(httpResp); } - const resp = await readSuccessResponseJsonOrThrow(httpResp, codecForAny()); - - logger.info(`reserve merge response: ${j2s(resp)}`); - await ctx.transition({}, async (rec, _) => { rec.status = PeerPullPaymentCreditStatus.PendingReady return TransitionResultType.Transition @@ -894,19 +887,12 @@ export async function processPeerPullCredit( throw Error("peer pull payment initiation not found in database"); } - const retryTag = constructTaskIdentifier({ - tag: PendingTaskType.PeerPullCredit, - pursePub, - }); - - logger.trace(`processing ${retryTag}, status=${pullIni.status}`); - const ctx = new PeerPullCreditTransactionContext(wex, pullIni.pursePub); + logger.trace(`processing ${ctx.taskId}, status=${pullIni.status}`); switch (pullIni.status) { - case PeerPullPaymentCreditStatus.Done: { + case PeerPullPaymentCreditStatus.Done: return TaskRunResult.finished(); - } case PeerPullPaymentCreditStatus.PendingReady: return queryPurseForPeerPullCredit(wex, pullIni); case PeerPullPaymentCreditStatus.PendingMergeKycRequired: { @@ -966,8 +952,7 @@ async function processPeerPullCreditBalanceKyc( ); if (checkRes.result === "ok") { return checkRes; - } - if ( + } else if ( peerInc.status === PeerPullPaymentCreditStatus.PendingBalanceKycInit && checkRes.walletKycStatus === ExchangeWalletKycStatus.Legi ) { @@ -979,13 +964,10 @@ async function processPeerPullCreditBalanceKyc( }); return undefined; }, - filterNotification(notif) { - return ( - (notif.type === NotificationType.ExchangeStateTransition && - notif.exchangeBaseUrl === exchangeBaseUrl) || - notif.type === NotificationType.BalanceChange - ); - }, + filterNotification: (notif) => + (notif.type === NotificationType.ExchangeStateTransition && + notif.exchangeBaseUrl === exchangeBaseUrl) || + notif.type === NotificationType.BalanceChange }); if (ret.result === "ok") { @@ -1026,43 +1008,32 @@ async function processPeerPullCreditKycRequired( accountPub: mergeReserveInfo.reservePub, }); - const url = new URL(`kyc-check/${kycPayoHash}`, peerIni.exchangeBaseUrl); + const exchangeClient = new TalerExchangeHttpClient( + peerIni.exchangeBaseUrl, + wex.http + ) + const res = await exchangeClient.checkKycStatus(sigResp.sig, kycPayoHash, wex.cancellationToken); - logger.info(`kyc url ${url.href}`); - const kycStatusRes = await cancelableFetch(wex, url, { - headers: { - ["Account-Owner-Signature"]: sigResp.sig, - } - }); - - if ( - kycStatusRes.status === HttpStatusCode.Ok || - kycStatusRes.status === HttpStatusCode.NoContent - ) { - logger.warn("kyc requested, but already fulfilled"); - return TaskRunResult.backoff(); - } else if (kycStatusRes.status === HttpStatusCode.Accepted) { - const kycStatus = await readResponseJsonOrThrow( - kycStatusRes, - codecForAccountKycStatus(), - ); - logger.info(`kyc status: ${j2s(kycStatus)}`); - const info = await ctx.transition({}, async (rec) => { - rec.kycPaytoHash = kycPayoHash; - logger.info( - `setting peer-pull-credit kyc payto hash to ${kycPayoHash}`, - ); - rec.kycAccessToken = kycStatus.access_token; - rec.status = PeerPullPaymentCreditStatus.PendingMergeKycRequired; - return TransitionResultType.Transition - }) - if (info == null) { - return TaskRunResult.finished() - } else { + switch (res.case) { + case "ok": + case HttpStatusCode.Ok: // FIXME: voluntary check ? + logger.warn("kyc requested, but already fulfilled"); + return TaskRunResult.finished(); + case HttpStatusCode.Accepted: { + logger.info(`kyc status: ${j2s(res.body)}`); + await ctx.transition({}, async (rec) => { + rec.kycPaytoHash = kycPayoHash; + logger.info( + `setting peer-pull-credit kyc payto hash to ${kycPayoHash}`, + ); + rec.kycAccessToken = res.body.access_token; + rec.status = PeerPullPaymentCreditStatus.PendingMergeKycRequired; + return TransitionResultType.Transition + }) return TaskRunResult.progress() } - } else { - throw Error(`unexpected response from kyc-check (${kycStatusRes.status})`); + default: + throw Error(`unexpected response from kyc-check (${res.case})`); } } diff --git a/packages/taler-wallet-core/src/pay-peer-pull-debit.ts b/packages/taler-wallet-core/src/pay-peer-pull-debit.ts @@ -186,10 +186,7 @@ export class PeerPullDebitTransactionContext implements TransactionContext { abortReason: pi.abortReason, failReason: pi.failReason, timestamp: timestampPreciseFromDb(pi.timestampCreated), - transactionId: constructTransactionIdentifier({ - tag: TransactionType.PeerPullDebit, - peerPullDebitId: pi.peerPullDebitId, - }), + transactionId: this.transactionId, ...(ort?.lastError ? { error: ort.lastError } : {}), }; } diff --git a/packages/taler-wallet-core/src/pay-peer-push-credit.ts b/packages/taler-wallet-core/src/pay-peer-push-credit.ts @@ -296,10 +296,7 @@ export class PeerPushCreditTransactionContext implements TransactionContext { summary: peerContractTerms.summary, }, timestamp: timestampPreciseFromDb(wg.timestampStart), - transactionId: constructTransactionIdentifier({ - tag: TransactionType.PeerPushCredit, - peerPushCreditId: pushInc.peerPushCreditId, - }), + transactionId: this.transactionId, abortReason: pushInc.abortReason, failReason: pushInc.failReason, kycUrl, @@ -327,10 +324,7 @@ export class PeerPushCreditTransactionContext implements TransactionContext { kycUrl, kycPaytoHash: pushInc.kycPaytoHash, timestamp: timestampPreciseFromDb(pushInc.timestamp), - transactionId: constructTransactionIdentifier({ - tag: TransactionType.PeerPushCredit, - peerPushCreditId: pushInc.peerPushCreditId, - }), + transactionId: this.transactionId, abortReason: pushInc.abortReason, failReason: pushInc.failReason, ...(pushRetryRecord?.lastError @@ -753,15 +747,10 @@ async function processPeerPushCreditKycRequired( peerInc: PeerPushPaymentIncomingRecord, kycPending: LegitimizationNeededResponse, ): Promise<TaskRunResult> { - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.PeerPushCredit, - peerPushCreditId: peerInc.peerPushCreditId, - }); const ctx = new PeerPushCreditTransactionContext( wex, peerInc.peerPushCreditId, ); - const { peerPushCreditId } = peerInc; // FIXME: What if this changes? Should be part of the p2p record const mergeReserveInfo = await getMergeReserveInfo(wex, { @@ -802,7 +791,7 @@ async function processPeerPushCreditKycRequired( const { transitionInfo, result } = await wex.db.runReadWriteTx( { storeNames: ["peerPushCredit", "transactionsMeta"] }, async (tx) => { - const peerInc = await tx.peerPushCredit.get(peerPushCreditId); + const peerInc = await tx.peerPushCredit.get(ctx.peerPushCreditId); if (!peerInc) { return { transitionInfo: undefined, @@ -826,7 +815,7 @@ async function processPeerPushCreditKycRequired( }; }, ); - notifyTransition(wex, transactionId, transitionInfo); + notifyTransition(wex, ctx.transactionId, transitionInfo); return result; } else { throw Error(`unexpected response from kyc-check (${kycStatusRes.status})`); @@ -903,11 +892,11 @@ async function handlePendingMerge( reserve_sig: sigRes.accountSig, }; - const mergeResp = await exchangeClient.postPurseMerge({ - pursePub: peerInc.pursePub, - cancellationToken: wex.cancellationToken, - body: mergeReq, - }); + const mergeResp = await exchangeClient.postPurseMerge( + peerInc.pursePub, + mergeReq, + wex.cancellationToken, + ); logger.trace(`merge request: ${j2s(mergeReq)}`);