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:
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);