taler-typescript-core

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

commit 96461ab529e5cfa07f100268681bb34aae41c0fb
parent a6524293f1fd25704b876048f7b564000d76b6e3
Author: Florian Dold <florian@dold.me>
Date:   Tue,  8 Jul 2025 23:42:25 +0200

wallet-core: DD64 for p2p transactions

Diffstat:
Mpackages/taler-util/src/invariants.ts | 16++++++++++++++++
Mpackages/taler-wallet-core/src/db.ts | 12++++++++++++
Mpackages/taler-wallet-core/src/deposits.ts | 147+++++++++++++++++++++++++++++++------------------------------------------------
Mpackages/taler-wallet-core/src/kyc.ts | 94++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
Mpackages/taler-wallet-core/src/pay-peer-common.ts | 44+-------------------------------------------
Mpackages/taler-wallet-core/src/pay-peer-pull-credit.ts | 151+++++++++++++++++++++++++++++++++++++++++--------------------------------------
Mpackages/taler-wallet-core/src/pay-peer-push-credit.ts | 175++++++++++++++++++++++++++++++++++++++++++-------------------------------------
7 files changed, 319 insertions(+), 320 deletions(-)

diff --git a/packages/taler-util/src/invariants.ts b/packages/taler-util/src/invariants.ts @@ -57,3 +57,19 @@ export function checkLogicInvariant(b: boolean, m?: string): asserts b { } } } + +/** + * Check a logic invariant. + * + * A violation of this invariant means that another peer violated a + * protocol invariant. + */ +export function checkProtocolInvariant(b: boolean, m?: string): asserts b { + if (!b) { + if (m) { + throw Error(`BUG: protocol invariant failed (${m})`); + } else { + throw Error("BUG: protocol invariant failed"); + } + } +} diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts @@ -2289,6 +2289,12 @@ export interface PeerPullCreditRecord { kycAccessToken?: string; + kycLastCheckStatus?: number | undefined; + kycLastCheckCode?: number | undefined; + kycLastRuleGen?: number | undefined; + kycLastAmlReview?: boolean | undefined; + kycLastDeny?: DbPreciseTimestamp | undefined; + abortReason?: TalerErrorDetail; failReason?: TalerErrorDetail; @@ -2372,6 +2378,12 @@ export interface PeerPushPaymentIncomingRecord { kycPaytoHash?: string; kycAccessToken?: string; + + kycLastCheckStatus?: number | undefined; + kycLastCheckCode?: number | undefined; + kycLastRuleGen?: number | undefined; + kycLastAmlReview?: boolean | undefined; + kycLastDeny?: DbPreciseTimestamp | undefined; } export enum PeerPullDebitRecordStatus { diff --git a/packages/taler-wallet-core/src/deposits.ts b/packages/taler-wallet-core/src/deposits.ts @@ -1078,15 +1078,19 @@ async function processDepositGroupPendingKyc( } if (myKycState == null || isKycOperationDue(myKycState)) { - logger.info( - `deposit group is in pending(kyc), but trying deposit anyway after two minutes since last attempt`, - ); switch (depositGroup.operationStatus) { case DepositOperationStatus.PendingDepositKyc: - case DepositOperationStatus.PendingDeposit: + logger.info( + `deposit group is in pending(deposit-kyc), but trying deposit anyway after two minutes since last attempt`, + ); return await processDepositGroupPendingDeposit(wex, depositGroup); case DepositOperationStatus.PendingAggregateKyc: return await processDepositGroupTrack(wex, depositGroup); + case DepositOperationStatus.PendingDepositKycAuth: + logger.info( + `deposit group is in pending(deposit-kyc-auth), but trying deposit anyway after two minutes since last attempt`, + ); + return await processDepositGroupPendingDeposit(wex, depositGroup); default: return TaskRunResult.backoff(); } @@ -1094,23 +1098,21 @@ async function processDepositGroupPendingKyc( const algoRes = await runKycCheckAlgo(wex, myKycState); - if (!algoRes) { - return TaskRunResult.backoff(); + if (!algoRes.updatedStatus) { + return algoRes.taskResult; } checkLogicInvariant(!!maybeKycInfo); const kycInfo = maybeKycInfo; - kycInfo.lastAmlReview = algoRes.lastAmlReview; - kycInfo.lastCheckStatus = algoRes.lastCheckStatus; - kycInfo.lastCheckCode = algoRes.lastCheckCode; - kycInfo.lastDeny = algoRes.lastDeny; - kycInfo.lastRuleGen = algoRes.lastRuleGen; + kycInfo.lastAmlReview = algoRes.updatedStatus.lastAmlReview; + kycInfo.lastCheckStatus = algoRes.updatedStatus.lastCheckStatus; + kycInfo.lastCheckCode = algoRes.updatedStatus.lastCheckCode; + kycInfo.lastDeny = algoRes.updatedStatus.lastDeny; + kycInfo.lastRuleGen = algoRes.updatedStatus.lastRuleGen; - const requiresAuth = - algoRes.lastCheckStatus === HttpStatusCode.Conflict || - algoRes.lastCheckStatus === HttpStatusCode.Forbidden; + const requiresAuth = algoRes.requiresAuth; // Now store the result. @@ -1151,7 +1153,7 @@ async function processDepositGroupPendingKyc( newTxState, balanceEffect: BalanceEffect.Any, }); - return TaskRunResult.progress(); + return algoRes.taskResult; }, ); } @@ -1237,8 +1239,11 @@ async function getLastWithdrawalKeyPair( async function transitionToKycRequired( wex: WalletExecutionContext, depositGroup: DepositGroupRecord, - kycPaytoHash: string, - exchangeUrl: string, + args: { + kycPaytoHash: string; + exchangeUrl: string; + kycAuth: boolean; + }, ): Promise<TaskRunResult> { const { depositGroupId } = depositGroup; @@ -1255,71 +1260,44 @@ async function transitionToKycRequired( switch (dg.operationStatus) { case DepositOperationStatus.LegacyPendingTrack: case DepositOperationStatus.FinalizingTrack: - dg.operationStatus = DepositOperationStatus.PendingAggregateKyc; + if (args.kycAuth) { + throw Error("not yet supported"); + } else { + dg.operationStatus = DepositOperationStatus.PendingAggregateKyc; + } break; case DepositOperationStatus.PendingDeposit: - dg.operationStatus = DepositOperationStatus.PendingDepositKyc; - break; - default: - return; - } - dg.kycInfo = { - exchangeBaseUrl: exchangeUrl, - paytoHash: kycPaytoHash, - }; - await tx.depositGroups.put(dg); - await ctx.updateTransactionMeta(tx); - const newTxState = computeDepositTransactionStatus(dg); - applyNotifyTransition(tx.notify, ctx.transactionId, { - oldTxState, - newTxState, - balanceEffect: BalanceEffect.Any, - }); - }, - ); - return TaskRunResult.progress(); -} - -async function transitionToKycAuthRequired( - wex: WalletExecutionContext, - depositGroup: DepositGroupRecord, - kycPaytoHash: string, - exchangeUrl: string, -): Promise<TaskRunResult> { - const { depositGroupId } = depositGroup; - - const ctx = new DepositTransactionContext(wex, depositGroupId); - - await wex.db.runReadWriteTx( - { storeNames: ["depositGroups", "transactionsMeta"] }, - async (tx) => { - const dg = await tx.depositGroups.get(depositGroupId); - if (!dg) { - return undefined; - } - const oldTxState = computeDepositTransactionStatus(dg); - switch (dg.operationStatus) { - case DepositOperationStatus.LegacyPendingTrack: - case DepositOperationStatus.FinalizingTrack: - throw Error("not yet supported"); + if (args.kycAuth) { + dg.operationStatus = DepositOperationStatus.PendingDepositKycAuth; + } else { + dg.operationStatus = DepositOperationStatus.PendingDepositKyc; + } break; - case DepositOperationStatus.PendingDeposit: - dg.operationStatus = DepositOperationStatus.PendingDepositKycAuth; + case DepositOperationStatus.PendingDepositKycAuth: + if (!args.kycAuth) { + dg.operationStatus = DepositOperationStatus.PendingDepositKyc; + } break; default: return; } - dg.kycInfo = { - exchangeBaseUrl: exchangeUrl, - paytoHash: kycPaytoHash, - }; + if (dg.kycInfo && dg.kycInfo.exchangeBaseUrl === args.exchangeUrl) { + dg.kycInfo.lastDeny = timestampPreciseToDb(TalerPreciseTimestamp.now()); + } else { + // Reset other info when new exchange is involved. + dg.kycInfo = { + exchangeBaseUrl: args.exchangeUrl, + paytoHash: args.kycPaytoHash, + lastDeny: timestampPreciseToDb(TalerPreciseTimestamp.now()), + }; + } await tx.depositGroups.put(dg); await ctx.updateTransactionMeta(tx); const newTxState = computeDepositTransactionStatus(dg); applyNotifyTransition(tx.notify, ctx.transactionId, { oldTxState, newTxState, - balanceEffect: BalanceEffect.Flags, + balanceEffect: BalanceEffect.Any, }); }, ); @@ -1380,12 +1358,11 @@ async function processDepositGroupTrack( const paytoHash = encodeCrock( hashNormalizedPaytoUri(depositGroup.wire.payto_uri), ); - return transitionToKycRequired( - wex, - depositGroup, - paytoHash, - exchangeBaseUrl, - ); + return transitionToKycRequired(wex, depositGroup, { + exchangeUrl: exchangeBaseUrl, + kycPaytoHash: paytoHash, + kycAuth: false, // ?? + }); } else { updatedTxStatus = DepositElementStatus.Tracking; } @@ -1715,21 +1692,11 @@ async function processDepositGroupPendingDeposit( logger.info( `kyc legitimization needed response: ${j2s(kycLegiNeededResp)}`, ); - if (kycLegiNeededResp.bad_kyc_auth) { - return transitionToKycAuthRequired( - wex, - depositGroup, - kycLegiNeededResp.h_payto, - exchangeBaseUrl, - ); - } else { - return transitionToKycRequired( - wex, - depositGroup, - kycLegiNeededResp.h_payto, - exchangeBaseUrl, - ); - } + return transitionToKycRequired(wex, depositGroup, { + exchangeUrl: exchangeBaseUrl, + kycPaytoHash: kycLegiNeededResp.h_payto, + kycAuth: kycLegiNeededResp.bad_kyc_auth ?? false, + }); } } diff --git a/packages/taler-wallet-core/src/kyc.ts b/packages/taler-wallet-core/src/kyc.ts @@ -31,7 +31,11 @@ import { HttpResponse, readSuccessResponseJsonOrThrow, } from "@gnu-taler/taler-util/http"; -import { cancelableFetch, cancelableLongPoll } from "./common.js"; +import { + cancelableFetch, + cancelableLongPoll, + TaskRunResult, +} from "./common.js"; import { DbPreciseTimestamp, timestampAbsoluteFromDb, @@ -309,27 +313,34 @@ export async function checkLimit( } export interface GenericKycStatusReq { - exchangeBaseUrl: string; - paytoHash: string; - accountPub: string; - accountPriv: string; - operation: string; - amount: AmountLike; - lastCheckStatus?: number | undefined; - lastCheckCode?: number | undefined; - lastRuleGen?: number | undefined; - lastAmlReview?: boolean | undefined; - lastDeny?: DbPreciseTimestamp | undefined; + readonly exchangeBaseUrl: string; + readonly paytoHash: string; + readonly accountPub: string; + readonly accountPriv: string; + readonly operation: string; + readonly amount: AmountLike; + readonly lastCheckStatus?: number | undefined; + readonly lastCheckCode?: number | undefined; + readonly lastRuleGen?: number | undefined; + readonly lastAmlReview?: boolean | undefined; + readonly lastDeny?: DbPreciseTimestamp | undefined; } export interface GenericKycStatusResp { - accountPub: string; - accountPriv: string; - lastCheckStatus?: number | undefined; - lastCheckCode?: number | undefined; - lastRuleGen?: number | undefined; - lastAmlReview?: boolean | undefined; - lastDeny?: DbPreciseTimestamp | undefined; + /** If no updated status is present, finish the task with this status. */ + taskResult: TaskRunResult; + requiresAuth?: boolean; + updatedStatus?: { + lastCheckStatus?: number | undefined; + lastCheckCode?: number | undefined; + lastRuleGen?: number | undefined; + lastAmlReview?: boolean | undefined; + lastDeny?: DbPreciseTimestamp | undefined; + /** New account public key, only present if updated. */ + accountPub?: string; + /** New account private key, only present if updated. */ + accountPriv?: string; + }; } export function isKycOperationDue(st: GenericKycStatusReq): boolean { @@ -355,7 +366,7 @@ export function isKycOperationDue(st: GenericKycStatusReq): boolean { export async function runKycCheckAlgo( wex: WalletExecutionContext, st: GenericKycStatusReq, -): Promise<GenericKycStatusResp | undefined> { +): Promise<GenericKycStatusResp> { const sigResp = await wex.cryptoApi.signWalletKycAuth({ accountPriv: st.accountPriv, accountPub: st.accountPub, @@ -418,27 +429,36 @@ export async function runKycCheckAlgo( if (sameResp) { logger.trace(`kyc-check response didn't change, retrying with back-off`); - return undefined; + return { + taskResult: TaskRunResult.backoff(), + }; } - const rst: GenericKycStatusResp = { - accountPriv: st.accountPriv, - accountPub: st.accountPub, - lastAmlReview: st.lastAmlReview, - lastCheckCode: st.lastCheckCode, - lastCheckStatus: st.lastCheckStatus, + const updatedStatus: GenericKycStatusResp["updatedStatus"] = { + lastAmlReview: respJson.aml_review, + lastCheckCode: respJson.code, + lastCheckStatus: kycStatusRes.status, lastDeny: st.lastDeny, lastRuleGen: st.lastRuleGen, }; + const rst: GenericKycStatusResp = { + // FIXME: take from response or update!! + taskResult: TaskRunResult.progress(), + updatedStatus, + requiresAuth: + kycStatusRes.status === HttpStatusCode.Conflict || + kycStatusRes.status === HttpStatusCode.Forbidden, + }; + let exposedLimits: AccountLimit[] | undefined = undefined; switch (kycStatusRes.status) { case HttpStatusCode.NoContent: - rst.lastDeny = undefined; + updatedStatus.lastDeny = undefined; break; case HttpStatusCode.Ok: { - rst.lastDeny = undefined; + updatedStatus.lastDeny = undefined; break; } case HttpStatusCode.Accepted: @@ -447,24 +467,34 @@ export async function runKycCheckAlgo( codecForAccountKycStatus(), ); exposedLimits = resp.limits; + rst.taskResult = TaskRunResult.longpollReturnedPending(); + break; + case HttpStatusCode.NotFound: + // FIXME: Check if the private key for the indicated public key is available + rst.taskResult = TaskRunResult.backoff(); break; case HttpStatusCode.Forbidden: // FIXME: Check if we know the key that the exchange // claims as the current account pub for KYC. + rst.taskResult = TaskRunResult.backoff(); break; } if (exposedLimits) { switch (await checkLimit(exposedLimits, "DEPOSIT", st.amount)) { case LimitCheckResult.Allowed: - rst.lastDeny = undefined; + updatedStatus.lastDeny = undefined; break; case LimitCheckResult.DeniedSoft: - rst.lastDeny = timestampPreciseToDb(TalerPreciseTimestamp.now()); + updatedStatus.lastDeny = timestampPreciseToDb( + TalerPreciseTimestamp.now(), + ); break; case LimitCheckResult.DeniedVerboten: // FIXME: This should transition the transaction to failed! - rst.lastDeny = timestampPreciseToDb(TalerPreciseTimestamp.now()); + updatedStatus.lastDeny = timestampPreciseToDb( + TalerPreciseTimestamp.now(), + ); break; } } diff --git a/packages/taler-wallet-core/src/pay-peer-common.ts b/packages/taler-wallet-core/src/pay-peer-common.ts @@ -177,48 +177,6 @@ export async function getMergeReserveInfo( return mergeReserveRecord; } -export async function waitForKycCompletion( - wex: WalletExecutionContext, - exchangeUrl: string, - kycPaytoHash: string, -): Promise<boolean> { - // FIXME: What if this changes? Should be part of the p2p record - - const mergeReserveInfo = await getMergeReserveInfo(wex, { - exchangeBaseUrl: exchangeUrl, - }); - - const accountPub = mergeReserveInfo.reservePub; - const accountPriv = mergeReserveInfo.reservePriv; - - const sigResp = await wex.cryptoApi.signWalletKycAuth({ - accountPriv, - accountPub, - }); - - const exchangeClient = walletExchangeClient(exchangeUrl, wex); - const resp = await exchangeClient.checkKycStatus({ - accountPub, - accountSig: sigResp.sig, - paytoHash: kycPaytoHash, - longpoll: true, - }); - - switch (resp.case) { - case "ok": - case HttpStatusCode.Ok: - return true; - case HttpStatusCode.Conflict: - case HttpStatusCode.Accepted: - return false; - case HttpStatusCode.Forbidden: - case HttpStatusCode.NotFound: - throw Error(`unexpected kyc status response ${resp.case}`); - default: - assertUnreachable(resp); - } -} - /** Check if a purse is merged */ export function isPurseMerged(purse: ExchangePurseStatus): boolean { const mergeTimestamp = purse.merge_timestamp; @@ -307,7 +265,7 @@ export async function recordTransition< const storeNames = opts.extraStores ? [...baseStore, ...opts.extraStores] : baseStore; - const transitionInfo = await ctx.wex.db.runReadWriteTx( + await ctx.wex.db.runReadWriteTx( { storeNames, label: opts.label }, async (tx) => { const rec = await tx[ctx.store].get(ctx.recordId); diff --git a/packages/taler-wallet-core/src/pay-peer-pull-credit.ts b/packages/taler-wallet-core/src/pay-peer-pull-credit.ts @@ -44,6 +44,7 @@ import { WalletNotification, assertUnreachable, checkDbInvariant, + checkProtocolInvariant, encodeCrock, getRandomBytes, j2s, @@ -83,7 +84,12 @@ import { getScopeForAllExchanges, handleStartExchangeWalletKyc, } from "./exchanges.js"; -import { checkPeerCreditHardLimitExceeded } from "./kyc.js"; +import { + GenericKycStatusReq, + checkPeerCreditHardLimitExceeded, + isKycOperationDue, + runKycCheckAlgo, +} from "./kyc.js"; import { getMergeReserveInfo, isPurseDeposited, @@ -92,7 +98,6 @@ import { recordTransition, recordTransitionStatus, recordUpdateMeta, - waitForKycCompletion, } from "./pay-peer-common.js"; import { BalanceEffect, @@ -531,24 +536,63 @@ async function queryPurseForPeerPullCredit( return TaskRunResult.progress(); } -async function longpollKycStatus( +async function processPendingMergeKycRequired( wex: WalletExecutionContext, - pursePub: string, - exchangeUrl: string, - kycPaytoHash: string, + pullIni: PeerPullCreditRecord, ): Promise<TaskRunResult> { - const done = await waitForKycCompletion(wex, exchangeUrl, kycPaytoHash); - if (done) { - const ctx = new PeerPullCreditTransactionContext(wex, pursePub); - await recordTransitionStatus( - ctx, - PeerPullPaymentCreditStatus.PendingMergeKycRequired, - PeerPullPaymentCreditStatus.PendingCreatePurse, - ); - return TaskRunResult.progress(); - } else { - return TaskRunResult.longpollReturnedPending(); + const ctx = new PeerPullCreditTransactionContext(wex, pullIni.pursePub); + const { exchangeBaseUrl, kycPaytoHash } = pullIni; + + // FIXME: What if this changes? Should be part of the p2p record + const mergeReserveInfo = await getMergeReserveInfo(wex, { + exchangeBaseUrl, + }); + + const accountPub = mergeReserveInfo.reservePub; + const accountPriv = mergeReserveInfo.reservePriv; + + let myKycState: GenericKycStatusReq | undefined; + + if (kycPaytoHash) { + myKycState = { + accountPriv, + accountPub, + // FIXME: Is this the correct amount? + amount: pullIni.estimatedAmountEffective, + exchangeBaseUrl, + operation: "MERGE", + paytoHash: kycPaytoHash, + lastAmlReview: pullIni.kycLastAmlReview, + lastCheckCode: pullIni.kycLastCheckCode, + lastCheckStatus: pullIni.kycLastCheckStatus, + lastDeny: pullIni.kycLastDeny, + lastRuleGen: pullIni.kycLastRuleGen, + }; + } + + if (myKycState == null || isKycOperationDue(myKycState)) { + return processPeerPullCreditCreatePurse(wex, pullIni); + } + + const algoRes = await runKycCheckAlgo(wex, myKycState); + + if (!algoRes.updatedStatus) { + return algoRes.taskResult; } + + const updatedStatus = algoRes.updatedStatus; + + checkProtocolInvariant(algoRes.requiresAuth != true); + + recordTransition(ctx, {}, async (rec) => { + rec.kycLastAmlReview = updatedStatus.lastAmlReview; + rec.kycLastCheckStatus = updatedStatus.lastCheckStatus; + rec.kycLastCheckCode = updatedStatus.lastCheckCode; + rec.kycLastDeny = updatedStatus.lastDeny; + rec.kycLastRuleGen = updatedStatus.lastRuleGen; + return TransitionResultType.Transition; + }); + return algoRes.taskResult; } async function processPeerPullCreditAbortingDeletePurse( @@ -582,7 +626,7 @@ async function processPeerPullCreditAbortingDeletePurse( } } -async function handlePeerPullCreditWithdrawing( +async function processPeerPullCreditWithdrawing( wex: WalletExecutionContext, pullIni: PeerPullCreditRecord, ): Promise<TaskRunResult> { @@ -625,7 +669,7 @@ async function handlePeerPullCreditWithdrawing( } } -async function handlePeerPullCreditCreatePurse( +async function processPeerPullCreditCreatePurse( wex: WalletExecutionContext, pullIni: PeerPullCreditRecord, ): Promise<TaskRunResult> { @@ -731,7 +775,7 @@ async function handlePeerPullCreditCreatePurse( break; case HttpStatusCode.UnavailableForLegalReasons: { logger.info(`kyc uuid response: ${j2s(resp.body)}`); - return processPeerPullCreditKycRequired(wex, pullIni, resp.body.h_payto); + return handlePeerPullCreditKycRequired(wex, pullIni, resp.body.h_payto); } case HttpStatusCode.Forbidden: case HttpStatusCode.NotFound: @@ -776,27 +820,22 @@ export async function processPeerPullCredit( case PeerPullPaymentCreditStatus.Done: return TaskRunResult.finished(); case PeerPullPaymentCreditStatus.PendingReady: - return queryPurseForPeerPullCredit(wex, pullIni); + return await queryPurseForPeerPullCredit(wex, pullIni); case PeerPullPaymentCreditStatus.PendingMergeKycRequired: { if (!pullIni.kycPaytoHash) { throw Error("invalid state, kycPaytoHash required"); } - return await longpollKycStatus( - wex, - pursePub, - pullIni.exchangeBaseUrl, - pullIni.kycPaytoHash, - ); + return await processPendingMergeKycRequired(wex, pullIni); } case PeerPullPaymentCreditStatus.PendingCreatePurse: - return handlePeerPullCreditCreatePurse(wex, pullIni); + return await processPeerPullCreditCreatePurse(wex, pullIni); case PeerPullPaymentCreditStatus.AbortingDeletePurse: return await processPeerPullCreditAbortingDeletePurse(wex, pullIni); case PeerPullPaymentCreditStatus.PendingWithdrawing: - return handlePeerPullCreditWithdrawing(wex, pullIni); + return await processPeerPullCreditWithdrawing(wex, pullIni); case PeerPullPaymentCreditStatus.PendingBalanceKycRequired: case PeerPullPaymentCreditStatus.PendingBalanceKycInit: - return processPeerPullCreditBalanceKyc(ctx, pullIni); + return await processPeerPullCreditBalanceKyc(ctx, pullIni); case PeerPullPaymentCreditStatus.Aborted: case PeerPullPaymentCreditStatus.Failed: case PeerPullPaymentCreditStatus.Expired: @@ -877,56 +916,22 @@ async function processPeerPullCreditBalanceKyc( } } -async function processPeerPullCreditKycRequired( +async function handlePeerPullCreditKycRequired( wex: WalletExecutionContext, peerIni: PeerPullCreditRecord, kycPaytoHash: string, ): Promise<TaskRunResult> { const ctx = new PeerPullCreditTransactionContext(wex, peerIni.pursePub); - // FIXME: What if this changes? Should be part of the p2p record - const mergeReserveInfo = await getMergeReserveInfo(wex, { - exchangeBaseUrl: peerIni.exchangeBaseUrl, - }); - - const sigResp = await wex.cryptoApi.signWalletKycAuth({ - accountPriv: mergeReserveInfo.reservePriv, - accountPub: mergeReserveInfo.reservePub, - }); - - const exchangeClient = walletExchangeClient(peerIni.exchangeBaseUrl, wex); - const res = await exchangeClient.checkKycStatus({ - accountPub: mergeReserveInfo.reservePub, - accountSig: sigResp.sig, - paytoHash: kycPaytoHash, + await recordTransition(ctx, {}, async (rec, tx) => { + logger.info(`setting peer-pull-credit kyc payto hash to ${kycPaytoHash}`); + rec.kycLastDeny = timestampPreciseToDb(TalerPreciseTimestamp.now()); + rec.kycPaytoHash = kycPaytoHash; + rec.status = PeerPullPaymentCreditStatus.PendingMergeKycRequired; + applyNotifyBalanceEffect(tx.notify, ctx.transactionId, BalanceEffect.Flags); + return TransitionResultType.Transition; }); - - 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 recordTransition(ctx, {}, async (rec, tx) => { - rec.kycPaytoHash = kycPaytoHash; - logger.info( - `setting peer-pull-credit kyc payto hash to ${kycPaytoHash}`, - ); - rec.kycAccessToken = res.body.access_token; - rec.status = PeerPullPaymentCreditStatus.PendingMergeKycRequired; - applyNotifyBalanceEffect( - tx.notify, - ctx.transactionId, - BalanceEffect.Flags, - ); - return TransitionResultType.Transition; - }); - return TaskRunResult.progress(); - } - default: - throw Error(`unexpected response from kyc-check (${res.case})`); - } + return TaskRunResult.progress(); } /** diff --git a/packages/taler-wallet-core/src/pay-peer-push-credit.ts b/packages/taler-wallet-core/src/pay-peer-push-credit.ts @@ -42,6 +42,7 @@ import { WalletNotification, assertUnreachable, checkDbInvariant, + checkProtocolInvariant, codecForPeerContractTerms, decodeCrock, eddsaGetPublic, @@ -83,8 +84,11 @@ import { handleStartExchangeWalletKyc, } from "./exchanges.js"; import { + GenericKycStatusReq, checkPeerCreditHardLimitExceeded, getPeerCreditLimitInfo, + isKycOperationDue, + runKycCheckAlgo, } from "./kyc.js"; import { getMergeReserveInfo, @@ -94,7 +98,6 @@ import { recordTransition, recordTransitionStatus, recordUpdateMeta, - waitForKycCompletion, } from "./pay-peer-common.js"; import { BalanceEffect, @@ -613,94 +616,103 @@ export async function preparePeerPushCredit( }; } -async function longpollKycStatus( - wex: WalletExecutionContext, - peerPushCreditId: string, - exchangeUrl: string, - kycPaytoHash: string, -): Promise<TaskRunResult> { - const done = await waitForKycCompletion(wex, exchangeUrl, kycPaytoHash); - if (done) { - const ctx = new PeerPushCreditTransactionContext(wex, peerPushCreditId); - await recordTransitionStatus( - ctx, - PeerPushCreditStatus.PendingMergeKycRequired, - PeerPushCreditStatus.PendingMerge, - ); - return TaskRunResult.progress(); - } else { - return TaskRunResult.longpollReturnedPending(); - } -} - -async function processPeerPushCreditKycRequired( +async function processPeerPushDebitMergeKyc( wex: WalletExecutionContext, peerInc: PeerPushPaymentIncomingRecord, - kycPending: LegitimizationNeededResponse, + contractTerms: PeerContractTerms, ): Promise<TaskRunResult> { const ctx = new PeerPushCreditTransactionContext( wex, peerInc.peerPushCreditId, ); - + const { exchangeBaseUrl } = peerInc; // FIXME: What if this changes? Should be part of the p2p record const mergeReserveInfo = await getMergeReserveInfo(wex, { - exchangeBaseUrl: peerInc.exchangeBaseUrl, + exchangeBaseUrl, }); - const sigResp = await wex.cryptoApi.signWalletKycAuth({ - accountPriv: mergeReserveInfo.reservePriv, - accountPub: mergeReserveInfo.reservePub, - }); + const accountPub = mergeReserveInfo.reservePub; + const accountPriv = mergeReserveInfo.reservePriv; - const exchangeClient = walletExchangeClient(peerInc.exchangeBaseUrl, wex); - const resp = await exchangeClient.checkKycStatus({ - accountPub: mergeReserveInfo.reservePub, - accountSig: sigResp.sig, - paytoHash: kycPending.h_payto, - }); + let myKycState: GenericKycStatusReq | undefined; - logger.info(`kyc-check response case: ${resp.case}`); + if (peerInc.kycPaytoHash) { + myKycState = { + accountPriv, + accountPub, + // FIXME: Is this the correct amount? + amount: peerInc.estimatedAmountEffective, + exchangeBaseUrl, + operation: "MERGE", + paytoHash: peerInc.kycPaytoHash, + lastAmlReview: peerInc.kycLastAmlReview, + lastCheckCode: peerInc.kycLastCheckCode, + lastCheckStatus: peerInc.kycLastCheckStatus, + lastDeny: peerInc.kycLastDeny, + lastRuleGen: peerInc.kycLastRuleGen, + }; + } - switch (resp.case) { - case "ok": - case HttpStatusCode.Ok: - logger.warn("kyc requested, but already fulfilled"); - return TaskRunResult.finished(); - case HttpStatusCode.Accepted: - logger.info(`kyc-check response body: ${j2s(resp.body)}`); - return await wex.db.runReadWriteTx( - { storeNames: ["peerPushCredit", "transactionsMeta"] }, - async (tx) => { - const peerInc = await tx.peerPushCredit.get(ctx.peerPushCreditId); - if (!peerInc) { - return TaskRunResult.finished(); - } - const oldTxState = computePeerPushCreditTransactionState(peerInc); - peerInc.kycPaytoHash = kycPending.h_payto; - peerInc.kycAccessToken = resp.body.access_token; - peerInc.status = PeerPushCreditStatus.PendingMergeKycRequired; - const newTxState = computePeerPushCreditTransactionState(peerInc); - await tx.peerPushCredit.put(peerInc); - await ctx.updateTransactionMeta(tx); - applyNotifyTransition(tx.notify, ctx.transactionId, { - oldTxState, - newTxState, - balanceEffect: BalanceEffect.Flags, - }); - return TaskRunResult.progress(); - }, - ); - case HttpStatusCode.Conflict: - case HttpStatusCode.Forbidden: - case HttpStatusCode.NotFound: - throw Error(`unexpected kyc status response ${resp.case}`); - default: - assertUnreachable(resp); + if (myKycState == null || isKycOperationDue(myKycState)) { + return processPendingMerge(wex, peerInc, contractTerms); } + + const algoRes = await runKycCheckAlgo(wex, myKycState); + + if (!algoRes.updatedStatus) { + return algoRes.taskResult; + } + + const updatedStatus = algoRes.updatedStatus; + + checkProtocolInvariant(algoRes.requiresAuth != true); + + recordTransition(ctx, {}, async (rec) => { + rec.kycLastAmlReview = updatedStatus.lastAmlReview; + rec.kycLastCheckStatus = updatedStatus.lastCheckStatus; + rec.kycLastCheckCode = updatedStatus.lastCheckCode; + rec.kycLastDeny = updatedStatus.lastDeny; + rec.kycLastRuleGen = updatedStatus.lastRuleGen; + return TransitionResultType.Transition; + }); + return algoRes.taskResult; +} + +async function transitionPeerPushCreditKycRequired( + wex: WalletExecutionContext, + peerInc: PeerPushPaymentIncomingRecord, + kycPending: LegitimizationNeededResponse, +): Promise<TaskRunResult> { + const ctx = new PeerPushCreditTransactionContext( + wex, + peerInc.peerPushCreditId, + ); + + return await wex.db.runReadWriteTx( + { storeNames: ["peerPushCredit", "transactionsMeta"] }, + async (tx) => { + const peerInc = await tx.peerPushCredit.get(ctx.peerPushCreditId); + if (!peerInc) { + return TaskRunResult.finished(); + } + const oldTxState = computePeerPushCreditTransactionState(peerInc); + peerInc.kycPaytoHash = kycPending.h_payto; + peerInc.status = PeerPushCreditStatus.PendingMergeKycRequired; + peerInc.kycLastDeny = timestampPreciseToDb(TalerPreciseTimestamp.now()); + const newTxState = computePeerPushCreditTransactionState(peerInc); + await tx.peerPushCredit.put(peerInc); + await ctx.updateTransactionMeta(tx); + applyNotifyTransition(tx.notify, ctx.transactionId, { + oldTxState, + newTxState, + balanceEffect: BalanceEffect.Flags, + }); + return TaskRunResult.progress(); + }, + ); } -async function handlePendingMerge( +async function processPendingMerge( wex: WalletExecutionContext, peerInc: PeerPushPaymentIncomingRecord, contractTerms: PeerContractTerms, @@ -782,7 +794,11 @@ async function handlePendingMerge( case HttpStatusCode.UnavailableForLegalReasons: const kycLegiNeededResp = mergeResp.body; logger.info(`kyc legitimization needed response: ${j2s(mergeResp.body)}`); - return processPeerPushCreditKycRequired(wex, peerInc, kycLegiNeededResp); + return transitionPeerPushCreditKycRequired( + wex, + peerInc, + kycLegiNeededResp, + ); case HttpStatusCode.Conflict: // FIXME: Check signature. // FIXME: status completed by other @@ -865,7 +881,7 @@ async function handlePendingMerge( return TaskRunResult.backoff(); } -async function handlePendingWithdrawing( +async function processPendingWithdrawing( wex: WalletExecutionContext, peerInc: PeerPushPaymentIncomingRecord, ): Promise<TaskRunResult> { @@ -1017,16 +1033,11 @@ export async function processPeerPushCredit( if (!peerInc.kycPaytoHash) { throw Error("invalid state, kycPaytoHash required"); } - return longpollKycStatus( - wex, - peerPushCreditId, - peerInc.exchangeBaseUrl, - peerInc.kycPaytoHash, - ); + return processPeerPushDebitMergeKyc(wex, peerInc, contractTerms); case PeerPushCreditStatus.PendingMerge: - return handlePendingMerge(wex, peerInc, contractTerms); + return processPendingMerge(wex, peerInc, contractTerms); case PeerPushCreditStatus.PendingWithdrawing: - return handlePendingWithdrawing(wex, peerInc); + return processPendingWithdrawing(wex, peerInc); case PeerPushCreditStatus.PendingBalanceKycInit: case PeerPushCreditStatus.PendingBalanceKycRequired: return processPeerPushCreditBalanceKyc(ctx, peerInc);