taler-typescript-core

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

commit a9b1e425bba7023740ec2883f18fb9e636f48759
parent 5ae45ca44def6eadeebb42e11336802f9180012c
Author: Florian Dold <florian@dold.me>
Date:   Tue,  3 Sep 2024 21:15:18 +0200

wallet-core: use new /kyc-check/H_PAYTO API, fix test

Diffstat:
Mpackages/taler-harness/src/integrationtests/test-kyc-peer-pull.ts | 42+++++++++++++++++++++---------------------
Mpackages/taler-util/src/payto.ts | 29+++++++++++++++++++++++++++++
Mpackages/taler-util/src/types-taler-common.ts | 18+-----------------
Mpackages/taler-util/src/types-taler-exchange.ts | 4++--
Mpackages/taler-wallet-core/src/db.ts | 20++++----------------
Mpackages/taler-wallet-core/src/deposits.ts | 17++++++++---------
Mpackages/taler-wallet-core/src/exchanges.ts | 17++++++++++++-----
Mpackages/taler-wallet-core/src/pay-peer-pull-credit.ts | 69++++++++++++++++++++++++++++++++++++++-------------------------------
Mpackages/taler-wallet-core/src/pay-peer-push-credit.ts | 64+++++++++++++++++++++++++++++++++++-----------------------------
Mpackages/taler-wallet-core/src/withdraw.ts | 70+++++++++++++++++++++++++++-------------------------------------------
10 files changed, 177 insertions(+), 173 deletions(-)

diff --git a/packages/taler-harness/src/integrationtests/test-kyc-peer-pull.ts b/packages/taler-harness/src/integrationtests/test-kyc-peer-pull.ts @@ -252,25 +252,6 @@ export async function runKycPeerPullTest(t: GlobalTestState) { summary: "test123", }); - const prepRes = await w0.walletClient.call( - WalletApiOperation.PreparePeerPullDebit, - { - talerUri: pullRes.talerUri, - }, - ); - - await w0.walletClient.call(WalletApiOperation.ConfirmPeerPullDebit, { - transactionId: prepRes.transactionId, - }); - - await walletClient.call(WalletApiOperation.TestingWaitTransactionState, { - transactionId: pullRes.transactionId as TransactionIdStr, - txState: { - major: TransactionMajorState.Pending, - minor: TransactionMinorState.MergeKycRequired, - }, - }); - const txDet = await walletClient.call(WalletApiOperation.GetTransactionById, { transactionId: pullRes.transactionId, }); @@ -291,13 +272,32 @@ export async function runKycPeerPullTest(t: GlobalTestState) { await walletClient.call(WalletApiOperation.TestingWaitTransactionState, { transactionId: pullRes.transactionId as TransactionIdStr, txState: { + major: TransactionMajorState.Pending, + minor: TransactionMinorState.Ready, + }, + }); + + const prepRes = await w0.walletClient.call( + WalletApiOperation.PreparePeerPullDebit, + { + talerUri: pullRes.talerUri, + }, + ); + + await w0.walletClient.call(WalletApiOperation.ConfirmPeerPullDebit, { + transactionId: prepRes.transactionId, + }); + + await walletClient.call(WalletApiOperation.TestingWaitTransactionState, { + transactionId: pullRes.transactionId as TransactionIdStr, + txState: { major: TransactionMajorState.Done, }, }); } /** - * Initiate a pull debit transaction, wait until the transaction + * Initiate a pull credit transaction, wait until the transaction * is ready. */ async function doPeerPullCredit( @@ -332,7 +332,7 @@ async function doPeerPullCredit( transactionId: initRet.transactionId, txState: { major: TransactionMajorState.Pending, - minor: TransactionMinorState.Ready, + minor: TransactionMinorState.MergeKycRequired, }, }); diff --git a/packages/taler-util/src/payto.ts b/packages/taler-util/src/payto.ts @@ -218,6 +218,35 @@ export function hashPaytoUri(p: PaytoUri | string): Uint8Array { return hashTruncate32(stringToBytes(paytoUri + "\0")); } +export function stringifyReservePaytoUri( + exchangeBaseUrl: string, + reservePub: string, +): string { + const url = new URL(exchangeBaseUrl); + let target: string; + let domainWithOptPort: string; + if (url.protocol === "https:") { + target = "taler-reserve"; + if (url.port != "443" && url.port !== "") { + domainWithOptPort = `${url.hostname}:${url.port}`; + } else { + domainWithOptPort = `${url.hostname}`; + } + } else { + target = "taler-reserve-http"; + if (url.port != "80" && url.port !== "") { + domainWithOptPort = `${url.hostname}:${url.port}`; + } else { + domainWithOptPort = `${url.hostname}`; + } + } + let optPath = ""; + if (url.pathname !== "/" && url.pathname !== "") { + optPath = url.pathname; + } + return `payto://${target}/${domainWithOptPort}${optPath}/${reservePub}`; +} + /** * Parse a valid payto:// uri into a PaytoUri object * RFC 8905 diff --git a/packages/taler-util/src/types-taler-common.ts b/packages/taler-util/src/types-taler-common.ts @@ -40,7 +40,7 @@ import { codecForString, codecOptional, } from "./codec.js"; -import { codecForEither, ReservePub } from "./index.js"; +import { ReservePub, codecForEither } from "./index.js"; import { TalerProtocolDuration, TalerProtocolTimestamp, @@ -291,21 +291,6 @@ export type HashCodeString = string; export type WireSalt = string; -export interface WalletKycUuid { - // UUID that the wallet should use when initiating - // the KYC check. - requirement_row: number; - - // Hash of the payto:// account URI for the wallet. - h_payto: string; -} - -export const codecForWalletKycUuid = (): Codec<WalletKycUuid> => - buildCodecForObject<WalletKycUuid>() - .property("requirement_row", codecForNumber()) - .property("h_payto", codecForString()) - .build("WalletKycUuid"); - export interface MerchantUsingTemplateDetails { summary?: string; amount?: AmountString; @@ -545,7 +530,6 @@ export interface ReserveAccount { signingKey: SigningKey; } - export type PaginationParams = { /** * row identifier as the starting point of the query diff --git a/packages/taler-util/src/types-taler-exchange.ts b/packages/taler-util/src/types-taler-exchange.ts @@ -1616,7 +1616,7 @@ export interface LegitimizationNeededResponse { // transaction was blocked and how to lift it. The account holder // should use the number to check for the account's AML/KYC status // using the /kyc-check/$REQUIREMENT_ROW endpoint. - requirement_row: Integer; + requirement_row: Integer | undefined; } export interface AccountKycStatus { @@ -2422,7 +2422,7 @@ export const codecForLegitimizationNeededResponse = .property("hint", codecOptional(codecForString())) .property("h_payto", codecForString()) .property("account_pub", codecOptional(codecForString())) - .property("requirement_row", codecForNumber()) + .property("requirement_row", codecOptional(codecForNumber())) .build("TalerExchangeApi.LegitimizationNeededResponse"); export const codecForAccountKycStatus = (): Codec<AccountKycStatus> => diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts @@ -1466,11 +1466,6 @@ export type WgInfo = export type KycUserType = "individual" | "business"; -export interface KycPendingInfo { - paytoHash: string; - requirementRow: number; -} - /** * Group of withdrawal operations that need to be executed. * (Either for a normal withdrawal or from a reward.) @@ -1486,9 +1481,7 @@ export interface WithdrawalGroupRecord { wgInfo: WgInfo; - kycPending?: KycPendingInfo; - - kycUrl?: string; + kycPaytoHash?: string; kycAccessToken?: string; @@ -1838,8 +1831,7 @@ export interface DepositGroupRecord { } export interface DepositKycInfo { - kycUrl: string; - requirementRow: number; + accessToken: string; paytoHash: string; exchangeBaseUrl: string; } @@ -2029,9 +2021,7 @@ export interface PeerPullCreditRecord { */ status: PeerPullPaymentCreditStatus; - kycInfo?: KycPendingInfo; - - kycUrl?: string; + kycPaytoHash?: string; kycAccessToken?: string; @@ -2109,9 +2099,7 @@ export interface PeerPushPaymentIncomingRecord { */ currency: string | undefined; - kycInfo?: KycPendingInfo; - - kycUrl?: string; + kycPaytoHash?: string; kycAccessToken?: string; } diff --git a/packages/taler-wallet-core/src/deposits.ts b/packages/taler-wallet-core/src/deposits.ts @@ -88,9 +88,9 @@ import { DepositElementStatus, DepositGroupRecord, DepositInfoPerExchange, + DepositKycInfo, DepositOperationStatus, DepositTrackingInfo, - KycPendingInfo, RefreshOperationStatus, WalletDbAllStoresReadOnlyTransaction, WalletDbReadWriteTransaction, @@ -779,7 +779,7 @@ async function processDepositGroupPendingKyc( } const url = new URL( - `kyc-check/${kycInfo.requirementRow}`, + `kyc-check/${kycInfo.paytoHash}`, kycInfo.exchangeBaseUrl, ); @@ -839,14 +839,14 @@ async function processDepositGroupPendingKyc( async function transitionToKycRequired( wex: WalletExecutionContext, depositGroup: DepositGroupRecord, - kycInfo: KycPendingInfo, + kycInfo: DepositKycInfo, exchangeUrl: string, ): Promise<TaskRunResult> { const { depositGroupId } = depositGroup; const ctx = new DepositTransactionContext(wex, depositGroupId); - const url = new URL(`kyc-check/${kycInfo.requirementRow}`, exchangeUrl); + const url = new URL(`kyc-check/${kycInfo.paytoHash}`, exchangeUrl); logger.info(`kyc url ${url.href}`); const kycStatusReq = await wex.http.fetch(url.href, { method: "GET", @@ -870,9 +870,8 @@ async function transitionToKycRequired( const oldTxState = computeDepositTransactionStatus(dg); dg.kycInfo = { exchangeBaseUrl: exchangeUrl, - kycUrl: kycStatus.kyc_url, paytoHash: kycInfo.paytoHash, - requirementRow: kycInfo.requirementRow, + accessToken: null as any, // FIXME! }; await tx.depositGroups.put(dg); await ctx.updateTransactionMeta(tx); @@ -938,10 +937,10 @@ async function processDepositGroupPendingTrack( const paytoHash = encodeCrock( hashTruncate32(stringToBytes(depositGroup.wire.payto_uri + "\0")), ); - const { requirement_row: requirementRow } = track; - const kycInfo: KycPendingInfo = { + const kycInfo: DepositKycInfo = { paytoHash, - requirementRow, + exchangeBaseUrl: exchangeBaseUrl, + accessToken: null as any, // FIXME! }; return transitionToKycRequired( wex, diff --git a/packages/taler-wallet-core/src/exchanges.ts b/packages/taler-wallet-core/src/exchanges.ts @@ -101,10 +101,12 @@ import { encodeCrock, getRandomBytes, hashDenomPub, + hashPaytoUri, j2s, makeErrorDetail, makeTalerErrorDetail, parsePaytoUri, + stringifyReservePaytoUri, } from "@gnu-taler/taler-util"; import { HttpRequestLibrary, @@ -3370,8 +3372,7 @@ async function handleExchangeKycRespLegi( accountPriv: reserve.reservePriv, accountPub: reserve.reservePub, }); - const requirementRow = kycBody.requirement_row; - const reqUrl = new URL(`kyc-check/${requirementRow}`, exchangeBaseUrl); + const reqUrl = new URL(`kyc-check/${kycBody.h_payto}`, exchangeBaseUrl); const resp = await wex.http.fetch(reqUrl.href, { method: "GET", headers: { @@ -3466,13 +3467,19 @@ async function handleExchangeKycPendingLegitimization( accountPriv: reserve.reservePriv, accountPub: reserve.reservePub, }); - const requirementRow = reserve.requirementRow; - checkDbInvariant(!!requirementRow, "requirement row"); + + const reservePayto = stringifyReservePaytoUri( + exchange.baseUrl, + reserve.reservePub, + ); + + const paytoHash = encodeCrock(hashPaytoUri(reservePayto)); + const resp = await wex.ws.runLongpollQueueing( wex, exchange.baseUrl, async (timeoutMs) => { - const reqUrl = new URL(`kyc-check/${requirementRow}`, exchange.baseUrl); + const reqUrl = new URL(`kyc-check/${paytoHash}`, exchange.baseUrl); reqUrl.searchParams.set("timeout_ms", `${timeoutMs}`); logger.info(`long-polling wallet KYC status at ${reqUrl.href}`); return await wex.http.fetch(reqUrl.href, { diff --git a/packages/taler-wallet-core/src/pay-peer-pull-credit.ts b/packages/taler-wallet-core/src/pay-peer-pull-credit.ts @@ -43,11 +43,11 @@ import { TransactionState, TransactionType, WalletAccountMergeFlags, - WalletKycUuid, assertUnreachable, checkDbInvariant, + codecForAccountKycStatus, codecForAny, - codecForWalletKycUuid, + codecForLegitimizationNeededResponse, encodeCrock, getRandomBytes, j2s, @@ -55,7 +55,10 @@ import { stringifyTalerUri, talerPaytoFromExchangeReserve, } from "@gnu-taler/taler-util"; -import { readSuccessResponseJsonOrThrow } from "@gnu-taler/taler-util/http"; +import { + readResponseJsonOrThrow, + readSuccessResponseJsonOrThrow, +} from "@gnu-taler/taler-util/http"; import { PendingTaskType, TaskIdStr, @@ -71,7 +74,6 @@ import { runWithClientCancellation, } from "./common.js"; import { - KycPendingInfo, OperationRetryRecord, PeerPullCreditRecord, PeerPullPaymentCreditStatus, @@ -262,6 +264,14 @@ export class PeerPullCreditTransactionContext implements TransactionContext { TaskIdentifiers.forPeerPullPaymentInitiation(pullCredit); let pullCreditOrt = await tx.operationRetries.get(pullCreditOpId); + let kycUrl: string | undefined = undefined; + if (pullCredit.kycPaytoHash) { + kycUrl = new URL( + `kyc-spa/${pullCredit.kycPaytoHash}`, + pullCredit.exchangeBaseUrl, + ).href; + } + if (wsr) { if (wsr.wgInfo.withdrawalType !== WithdrawalRecordType.PeerPullCredit) { throw Error(`Unexpected withdrawalType: ${wsr.wgInfo.withdrawalType}`); @@ -308,7 +318,8 @@ export class PeerPullCreditTransactionContext implements TransactionContext { tag: TransactionType.PeerPullCredit, pursePub: pullCredit.pursePub, }), - kycUrl: pullCredit.kycUrl, + // FIXME: Is this the KYC URL of the withdrawal group?! + kycUrl: kycUrl, ...(wsrOrt?.lastError ? { error: silentWithdrawalErrorForInvoice @@ -343,7 +354,9 @@ export class PeerPullCreditTransactionContext implements TransactionContext { tag: TransactionType.PeerPullCredit, pursePub: pullCredit.pursePub, }), - kycUrl: pullCredit.kycUrl, + kycUrl, + kycAccessToken: pullCredit.kycAccessToken, + kycPaytoHash: pullCredit.kycPaytoHash, ...(pullCreditOrt?.lastError ? { error: pullCreditOrt.lastError } : {}), }; } @@ -758,7 +771,7 @@ async function longpollKycStatus( wex: WalletExecutionContext, pursePub: string, exchangeUrl: string, - kycInfo: KycPendingInfo, + kycPaytoHash: string, ): Promise<TaskRunResult> { // FIXME: What if this changes? Should be part of the p2p record const mergeReserveInfo = await getMergeReserveInfo(wex, { @@ -771,7 +784,7 @@ async function longpollKycStatus( }); const ctx = new PeerPullCreditTransactionContext(wex, pursePub); - const url = new URL(`kyc-check/${kycInfo.requirementRow}`, exchangeUrl); + const url = new URL(`kyc-check/${kycPaytoHash}`, exchangeUrl); const kycStatusRes = await wex.ws.runLongpollQueueing( wex, url.hostname, @@ -1049,9 +1062,9 @@ async function handlePeerPullCreditCreatePurse( if (httpResp.status === HttpStatusCode.UnavailableForLegalReasons) { const respJson = await httpResp.json(); - const kycPending = codecForWalletKycUuid().decode(respJson); + const kycPending = codecForLegitimizationNeededResponse().decode(respJson); logger.info(`kyc uuid response: ${j2s(kycPending)}`); - return processPeerPullCreditKycRequired(wex, pullIni, kycPending); + return processPeerPullCreditKycRequired(wex, pullIni, kycPending.h_payto); } const resp = await readSuccessResponseJsonOrThrow(httpResp, codecForAny()); @@ -1111,14 +1124,14 @@ export async function processPeerPullCredit( case PeerPullPaymentCreditStatus.PendingReady: return queryPurseForPeerPullCredit(wex, pullIni); case PeerPullPaymentCreditStatus.PendingMergeKycRequired: { - if (!pullIni.kycInfo) { - throw Error("invalid state, kycInfo required"); + if (!pullIni.kycPaytoHash) { + throw Error("invalid state, kycPaytoHash required"); } return await longpollKycStatus( wex, pursePub, pullIni.exchangeBaseUrl, - pullIni.kycInfo, + pullIni.kycPaytoHash, ); } case PeerPullPaymentCreditStatus.PendingCreatePurse: @@ -1217,13 +1230,7 @@ async function processPeerPullCreditBalanceKyc( return TransitionResult.stay(); } rec.status = PeerPullPaymentCreditStatus.PendingBalanceKycRequired; - delete rec.kycInfo; rec.kycAccessToken = ret.walletKycAccessToken; - // FIXME: #9109 this should not be constructed here, it should be an opaque URL from exchange response - rec.kycUrl = new URL( - `kyc-spa/${ret.walletKycAccessToken}`, - exchangeBaseUrl, - ).href; return TransitionResult.transition(rec); }); return TaskRunResult.progress(); @@ -1235,7 +1242,7 @@ async function processPeerPullCreditBalanceKyc( async function processPeerPullCreditKycRequired( wex: WalletExecutionContext, peerIni: PeerPullCreditRecord, - kycPending: WalletKycUuid, + kycPayoHash: string, ): Promise<TaskRunResult> { const ctx = new PeerPullCreditTransactionContext(wex, peerIni.pursePub); const { pursePub } = peerIni; @@ -1250,10 +1257,7 @@ async function processPeerPullCreditKycRequired( accountPub: mergeReserveInfo.reservePub, }); - const url = new URL( - `kyc-check/${kycPending.requirement_row}`, - peerIni.exchangeBaseUrl, - ); + const url = new URL(`kyc-check/${kycPayoHash}`, peerIni.exchangeBaseUrl); logger.info(`kyc url ${url.href}`); const kycStatusRes = await wex.http.fetch(url.href, { @@ -1271,7 +1275,10 @@ async function processPeerPullCreditKycRequired( logger.warn("kyc requested, but already fulfilled"); return TaskRunResult.backoff(); } else if (kycStatusRes.status === HttpStatusCode.Accepted) { - const kycStatus = await kycStatusRes.json(); + const kycStatus = await readResponseJsonOrThrow( + kycStatusRes, + codecForAccountKycStatus(), + ); logger.info(`kyc status: ${j2s(kycStatus)}`); const { transitionInfo, result } = await wex.db.runReadWriteTx( { storeNames: ["peerPullCredit", "transactionsMeta"] }, @@ -1284,11 +1291,11 @@ async function processPeerPullCreditKycRequired( }; } const oldTxState = computePeerPullCreditTransactionState(peerInc); - peerInc.kycInfo = { - paytoHash: kycPending.h_payto, - requirementRow: kycPending.requirement_row, - }; - peerInc.kycUrl = kycStatus.kyc_url; + peerInc.kycPaytoHash = kycPayoHash; + logger.info( + `setting peer-pull-credit kyc payto hash to ${kycPayoHash}`, + ); + peerInc.kycAccessToken = kycStatus.access_token; peerInc.status = PeerPullPaymentCreditStatus.PendingMergeKycRequired; const newTxState = computePeerPullCreditTransactionState(peerInc); await tx.peerPullCredit.put(peerInc); @@ -1300,7 +1307,7 @@ async function processPeerPullCreditKycRequired( }, ); notifyTransition(wex, ctx.transactionId, transitionInfo); - return TaskRunResult.backoff(); + return result; } else { throw Error(`unexpected response from kyc-check (${kycStatusRes.status})`); } diff --git a/packages/taler-wallet-core/src/pay-peer-push-credit.ts b/packages/taler-wallet-core/src/pay-peer-push-credit.ts @@ -23,6 +23,7 @@ import { ExchangePurseMergeRequest, ExchangeWalletKycStatus, HttpStatusCode, + LegitimizationNeededResponse, Logger, NotificationType, PeerContractTerms, @@ -37,13 +38,13 @@ import { TransactionState, TransactionType, WalletAccountMergeFlags, - WalletKycUuid, assertUnreachable, checkDbInvariant, + codecForAccountKycStatus, codecForAny, codecForExchangeGetContractResponse, + codecForLegitimizationNeededResponse, codecForPeerContractTerms, - codecForWalletKycUuid, decodeCrock, eddsaGetPublic, encodeCrock, @@ -52,7 +53,10 @@ import { parsePayPushUri, talerPaytoFromExchangeReserve, } from "@gnu-taler/taler-util"; -import { readSuccessResponseJsonOrThrow } from "@gnu-taler/taler-util/http"; +import { + readResponseJsonOrThrow, + readSuccessResponseJsonOrThrow, +} from "@gnu-taler/taler-util/http"; import { PendingTaskType, TaskIdStr, @@ -67,7 +71,6 @@ import { requireExchangeTosAcceptedOrThrow, } from "./common.js"; import { - KycPendingInfo, OperationRetryRecord, PeerPushCreditStatus, PeerPushPaymentIncomingRecord, @@ -263,6 +266,11 @@ export class PeerPushCreditTransactionContext implements TransactionContext { const peerContractTerms = ct.contractTermsRaw; + let kycUrl: string | undefined = undefined; + if (wg?.kycAccessToken && wg.exchangeBaseUrl) { + kycUrl = new URL(`kyc-spa/${wg.kycAccessToken}`, wg.exchangeBaseUrl).href; + } + if (wg) { if (wg.wgInfo.withdrawalType !== WithdrawalRecordType.PeerPushCredit) { throw Error("invalid withdrawal group type for push payment credit"); @@ -291,8 +299,8 @@ export class PeerPushCreditTransactionContext implements TransactionContext { tag: TransactionType.PeerPushCredit, peerPushCreditId: pushInc.peerPushCreditId, }), - kycUrl: wg.kycUrl, - kycPaytoHash: wg.kycPending?.paytoHash, + kycUrl, + kycPaytoHash: wg.kycPaytoHash, ...(wgRetryRecord?.lastError ? { error: wgRetryRecord.lastError } : {}), }; } @@ -313,8 +321,8 @@ export class PeerPushCreditTransactionContext implements TransactionContext { expiration: peerContractTerms.purse_expiration, summary: peerContractTerms.summary, }, - kycUrl: pushInc.kycUrl, - kycPaytoHash: pushInc.kycInfo?.paytoHash, + kycUrl, + kycPaytoHash: pushInc.kycPaytoHash, timestamp: timestampPreciseFromDb(pushInc.timestamp), transactionId: constructTransactionIdentifier({ tag: TransactionType.PeerPushCredit, @@ -750,7 +758,7 @@ async function longpollKycStatus( wex: WalletExecutionContext, peerPushCreditId: string, exchangeUrl: string, - kycInfo: KycPendingInfo, + kycPaytoHash: string, ): Promise<TaskRunResult> { const ctx = new PeerPushCreditTransactionContext(wex, peerPushCreditId); @@ -764,7 +772,7 @@ async function longpollKycStatus( accountPub: mergeReserveInfo.reservePub, }); - const url = new URL(`kyc-check/${kycInfo.requirementRow}`, exchangeUrl); + const url = new URL(`kyc-check/${kycPaytoHash}`, exchangeUrl); logger.info(`kyc url ${url.href}`); const kycStatusRes = await wex.ws.runLongpollQueueing( wex, @@ -816,7 +824,7 @@ async function longpollKycStatus( async function processPeerPushCreditKycRequired( wex: WalletExecutionContext, peerInc: PeerPushPaymentIncomingRecord, - kycPending: WalletKycUuid, + kycPending: LegitimizationNeededResponse, ): Promise<TaskRunResult> { const transactionId = constructTransactionIdentifier({ tag: TransactionType.PeerPushCredit, @@ -839,7 +847,7 @@ async function processPeerPushCreditKycRequired( }); const url = new URL( - `kyc-check/${kycPending.requirement_row}`, + `kyc-check/${kycPending.h_payto}`, peerInc.exchangeBaseUrl, ); @@ -863,6 +871,10 @@ async function processPeerPushCreditKycRequired( } else if (kycStatusRes.status === HttpStatusCode.Accepted) { const kycStatus = await kycStatusRes.json(); logger.info(`kyc status: ${j2s(kycStatus)}`); + const statusResp = await readResponseJsonOrThrow( + kycStatusRes, + codecForAccountKycStatus(), + ); const { transitionInfo, result } = await wex.db.runReadWriteTx( { storeNames: ["peerPushCredit", "transactionsMeta"] }, async (tx) => { @@ -874,11 +886,8 @@ async function processPeerPushCreditKycRequired( }; } const oldTxState = computePeerPushCreditTransactionState(peerInc); - peerInc.kycInfo = { - paytoHash: kycPending.h_payto, - requirementRow: kycPending.requirement_row, - }; - peerInc.kycUrl = kycStatus.kyc_url; + peerInc.kycPaytoHash = kycPending.h_payto; + peerInc.kycAccessToken = statusResp.access_token; peerInc.status = PeerPushCreditStatus.PendingMergeKycRequired; const newTxState = computePeerPushCreditTransactionState(peerInc); await tx.peerPushCredit.put(peerInc); @@ -979,9 +988,12 @@ async function handlePendingMerge( if (mergeHttpResp.status === HttpStatusCode.UnavailableForLegalReasons) { const respJson = await mergeHttpResp.json(); - const kycPending = codecForWalletKycUuid().decode(respJson); - logger.info(`kyc uuid response: ${j2s(kycPending)}`); - return processPeerPushCreditKycRequired(wex, peerInc, kycPending); + const kycLegiNeededResp = + codecForLegitimizationNeededResponse().decode(respJson); + logger.info( + `kyc legitimization needed response: ${j2s(kycLegiNeededResp)}`, + ); + return processPeerPushCreditKycRequired(wex, peerInc, kycLegiNeededResp); } logger.trace(`merge request: ${j2s(mergeReq)}`); @@ -1163,14 +1175,14 @@ export async function processPeerPushCredit( switch (peerInc.status) { case PeerPushCreditStatus.PendingMergeKycRequired: { - if (!peerInc.kycInfo) { - throw Error("invalid state, kycInfo required"); + if (!peerInc.kycPaytoHash) { + throw Error("invalid state, kycPaytoHash required"); } return await longpollKycStatus( wex, peerPushCreditId, peerInc.exchangeBaseUrl, - peerInc.kycInfo, + peerInc.kycPaytoHash, ); } case PeerPushCreditStatus.PendingMerge: { @@ -1255,13 +1267,7 @@ async function processPeerPushCreditBalanceKyc( return TransitionResult.stay(); } rec.status = PeerPushCreditStatus.PendingBalanceKycRequired; - delete rec.kycInfo; rec.kycAccessToken = ret.walletKycAccessToken; - // FIXME: #9109 this should not be constructed here, it should be an opaque URL from exchange response - rec.kycUrl = new URL( - `kyc-spa/${ret.walletKycAccessToken}`, - exchangeBaseUrl, - ).href; return TransitionResult.transition(rec); }); return TaskRunResult.progress(); diff --git a/packages/taler-wallet-core/src/withdraw.ts b/packages/taler-wallet-core/src/withdraw.ts @@ -93,8 +93,8 @@ import { codecForCashinConversionResponse, codecForConversionBankConfig, codecForExchangeWithdrawBatchResponse, + codecForLegitimizationNeededResponse, codecForReserveStatus, - codecForWalletKycUuid, codecForWithdrawOperationStatusResponse, encodeCrock, getErrorDetailFromException, @@ -135,7 +135,6 @@ import { CoinSourceType, DenominationRecord, DenominationVerificationStatus, - KycPendingInfo, OperationRetryRecord, PlanchetRecord, PlanchetStatus, @@ -241,9 +240,9 @@ function buildTransactionForBankIntegratedWithdraw( wg.status === WithdrawalGroupStatus.Done || wg.status === WithdrawalGroupStatus.PendingReady, }, - kycUrl: wg.kycUrl, + kycUrl: kycDetails?.kycUrl, kycAccessToken: wg.kycAccessToken, - kycPaytoHash: wg.kycPending?.paytoHash, + kycPaytoHash: wg.kycPaytoHash, timestamp: timestampPreciseFromDb(wg.timestampStart), transactionId: constructTransactionIdentifier({ tag: TransactionType.Withdrawal, @@ -302,7 +301,7 @@ function buildTransactionForManualWithdraw( wg.status === WithdrawalGroupStatus.PendingReady, reserveClosingDelay: exchangeDetails?.reserveClosingDelay ?? { d_us: 0 }, }, - kycUrl: wg.kycUrl, + kycUrl: kycDetails?.kycUrl, exchangeBaseUrl: wg.exchangeBaseUrl, timestamp: timestampPreciseFromDb(wg.timestampStart), transactionId: constructTransactionIdentifier({ @@ -365,13 +364,20 @@ export class WithdrawTransactionContext implements TransactionContext { let kycDetails: TxKycDetails | undefined = undefined; + if (!exchangeBaseUrl) { + return undefined; + } + switch (withdrawalGroupRecord.status) { case WithdrawalGroupStatus.PendingKyc: case WithdrawalGroupStatus.SuspendedKyc: { kycDetails = { kycAccessToken: withdrawalGroupRecord.kycAccessToken, - kycPaytoHash: withdrawalGroupRecord.kycPending?.paytoHash, - kycUrl: withdrawalGroupRecord.kycUrl, + kycPaytoHash: withdrawalGroupRecord.kycPaytoHash, + kycUrl: new URL( + `kyc-spa/${withdrawalGroupRecord.kycAccessToken}`, + exchangeBaseUrl, + ).href, }; break; } @@ -1027,13 +1033,8 @@ async function processWithdrawalGroupBalanceKyc( return TransitionResult.stay(); } wg.status = WithdrawalGroupStatus.PendingBalanceKyc; - wg.kycPending = undefined; wg.kycAccessToken = ret.walletKycAccessToken; - // FIXME: #9109 this should not be constructed here, it should be an opaque URL from exchange response - wg.kycUrl = new URL( - `kyc-spa/${ret.walletKycAccessToken}`, - exchangeBaseUrl, - ).href; + delete wg.kycPaytoHash; return TransitionResult.transition(wg); }); return TaskRunResult.progress(); @@ -1152,7 +1153,9 @@ export async function getBankWithdrawalInfo( if (resp.detail) { throw TalerError.fromUncheckedDetail(resp.detail); } else { - throw TalerError.fromException(new Error("failed to get bank remote config")) + throw TalerError.fromException( + new Error("failed to get bank remote config"), + ); } } const { body: status } = resp; @@ -1347,13 +1350,6 @@ interface WithdrawalBatchResult { batchResp: ExchangeWithdrawBatchResponse; } -// FIXME: Move to exchange API types -enum ExchangeAmlStatus { - Normal = 0, - Pending = 1, - Frozen = 2, -} - /** * Transition a transaction from pending(ready) * into a pending(kyc|aml) state, in case KYC is required. @@ -1367,20 +1363,17 @@ async function handleKycRequired( ): Promise<void> { logger.info("withdrawal requires KYC"); const respJson = await resp.json(); - const uuidResp = codecForWalletKycUuid().decode(respJson); + const legiRequiredResp = + codecForLegitimizationNeededResponse().decode(respJson); const withdrawalGroupId = withdrawalGroup.withdrawalGroupId; const ctx = new WithdrawTransactionContext(wex, withdrawalGroupId); - logger.info(`kyc uuid response: ${j2s(uuidResp)}`); + logger.info(`kyc uuid response: ${j2s(legiRequiredResp)}`); const exchangeUrl = withdrawalGroup.exchangeBaseUrl; - const kycInfo: KycPendingInfo = { - paytoHash: uuidResp.h_payto, - requirementRow: uuidResp.requirement_row, - }; const sigResp = await wex.cryptoApi.signWalletKycAuth({ accountPriv: withdrawalGroup.reservePriv, accountPub: withdrawalGroup.reservePub, }); - const url = new URL(`kyc-check/${kycInfo.requirementRow}`, exchangeUrl); + const url = new URL(`kyc-check/${legiRequiredResp.h_payto}`, exchangeUrl); logger.info(`kyc url ${url.href}`); // We do not longpoll here, as this is the initial request to get information about the KYC. const kycStatusRes = await wex.http.fetch(url.href, { @@ -1434,16 +1427,7 @@ async function handleKycRequired( if (wg2.status !== WithdrawalGroupStatus.PendingReady) { return TransitionResult.stay(); } - // FIXME: Why not just store the whole kycState?! - wg2.kycPending = { - paytoHash: uuidResp.h_payto, - requirementRow: uuidResp.requirement_row, - }; - // FIXME: #9109 this should not be constructed here, it should be an opaque URL from exchange response - wg2.kycUrl = new URL( - `kyc-spa/${kycStatus.access_token}`, - exchangeUrl, - ).href; + wg2.kycPaytoHash = legiRequiredResp.h_payto; wg2.kycAccessToken = kycStatus.access_token; wg2.status = WithdrawalGroupStatus.PendingKyc; return TransitionResult.transition(wg2); @@ -2019,12 +2003,12 @@ async function processWithdrawalGroupPendingKyc( wex, withdrawalGroup.withdrawalGroupId, ); - const kycInfo = withdrawalGroup.kycPending; - if (!kycInfo) { + const kycPaytoHash = withdrawalGroup.kycPaytoHash; + if (!kycPaytoHash) { throw Error("no kyc info available in pending(kyc)"); } const exchangeUrl = withdrawalGroup.exchangeBaseUrl; - const url = new URL(`kyc-check/${kycInfo.requirementRow}`, exchangeUrl); + const url = new URL(`kyc-check/${kycPaytoHash}`, exchangeUrl); const sigResp = await wex.cryptoApi.signWalletKycAuth({ accountPriv: withdrawalGroup.reservePriv, accountPub: withdrawalGroup.reservePub, @@ -2056,8 +2040,8 @@ async function processWithdrawalGroupPendingKyc( } switch (rec.status) { case WithdrawalGroupStatus.PendingKyc: { - delete rec.kycPending; - delete rec.kycUrl; + delete rec.kycAccessToken; + delete rec.kycAccessToken; rec.status = WithdrawalGroupStatus.PendingReady; return TransitionResult.transition(rec); }