gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[taler-wallet-core] branch master updated: wallet-core: fix withdrawal K


From: gnunet
Subject: [taler-wallet-core] branch master updated: wallet-core: fix withdrawal KYC transitions and use long-polling
Date: Wed, 21 Jun 2023 12:21:52 +0200

This is an automated email from the git hooks/post-receive script.

dold pushed a commit to branch master
in repository wallet-core.

The following commit(s) were added to refs/heads/master by this push:
     new 5eb339b83 wallet-core: fix withdrawal KYC transitions and use 
long-polling
5eb339b83 is described below

commit 5eb339b836b250891f00d8287781175b50788eb7
Author: Florian Dold <florian@dold.me>
AuthorDate: Wed Jun 21 12:21:48 2023 +0200

    wallet-core: fix withdrawal KYC transitions and use long-polling
---
 packages/taler-wallet-core/src/db.ts               |   2 +
 .../src/operations/transactions.ts                 |   1 +
 .../taler-wallet-core/src/operations/withdraw.ts   | 334 +++++++++++++--------
 3 files changed, 210 insertions(+), 127 deletions(-)

diff --git a/packages/taler-wallet-core/src/db.ts 
b/packages/taler-wallet-core/src/db.ts
index 3bf28aa94..9d0efbc6a 100644
--- a/packages/taler-wallet-core/src/db.ts
+++ b/packages/taler-wallet-core/src/db.ts
@@ -1416,6 +1416,8 @@ export interface WithdrawalGroupRecord {
 
   kycPending?: KycPendingInfo;
 
+  kycUrl?: string;
+
   /**
    * Secret seed used to derive planchets.
    * Stored since planchets are created lazily.
diff --git a/packages/taler-wallet-core/src/operations/transactions.ts 
b/packages/taler-wallet-core/src/operations/transactions.ts
index 82b7cea64..b4791e7c3 100644
--- a/packages/taler-wallet-core/src/operations/transactions.ts
+++ b/packages/taler-wallet-core/src/operations/transactions.ts
@@ -655,6 +655,7 @@ function buildTransactionForBankIntegratedWithdraw(
         wgRecord.status === WithdrawalGroupStatus.Finished ||
         wgRecord.status === WithdrawalGroupStatus.PendingReady,
     },
+    kycUrl: wgRecord.kycUrl,
     exchangeBaseUrl: wgRecord.exchangeBaseUrl,
     timestamp: wgRecord.timestampStart,
     transactionId: constructTransactionIdentifier({
diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts 
b/packages/taler-wallet-core/src/operations/withdraw.ts
index ed9522c0f..28f4eeebb 100644
--- a/packages/taler-wallet-core/src/operations/withdraw.ts
+++ b/packages/taler-wallet-core/src/operations/withdraw.ts
@@ -731,6 +731,96 @@ interface WithdrawalBatchResult {
   batchResp: ExchangeWithdrawBatchResponse;
 }
 
+async function handleKycRequired(
+  ws: InternalWalletState,
+  withdrawalGroup: WithdrawalGroupRecord,
+  resp: HttpResponse,
+  startIdx: number,
+  requestCoinIdxs: number[],
+): Promise<void> {
+  logger.info("withdrawal requires KYC");
+  const respJson = await resp.json();
+  const uuidResp = codecForWalletKycUuid().decode(respJson);
+  const withdrawalGroupId = withdrawalGroup.withdrawalGroupId;
+  const transactionId = constructTransactionIdentifier({
+    tag: TransactionType.Withdrawal,
+    withdrawalGroupId,
+  });
+  logger.info(`kyc uuid response: ${j2s(uuidResp)}`);
+  const exchangeUrl = withdrawalGroup.exchangeBaseUrl;
+  const userType = "individual";
+  const kycInfo: KycPendingInfo = {
+    paytoHash: uuidResp.h_payto,
+    requirementRow: uuidResp.requirement_row,
+  };
+  const url = new URL(
+    `kyc-check/${kycInfo.requirementRow}/${kycInfo.paytoHash}/${userType}`,
+    exchangeUrl,
+  );
+  logger.info(`kyc url ${url.href}`);
+  const kycStatusRes = await ws.http.fetch(url.href, {
+    method: "GET",
+  });
+  let kycUrl: string;
+  if (
+    kycStatusRes.status === HttpStatusCode.Ok ||
+    //FIXME: NoContent is not expected 
https://docs.taler.net/core/api-exchange.html#post--purses-$PURSE_PUB-merge
+    // remove after the exchange is fixed or clarified
+    kycStatusRes.status === HttpStatusCode.NoContent
+  ) {
+    logger.warn("kyc requested, but already fulfilled");
+    return;
+  } else if (kycStatusRes.status === HttpStatusCode.Accepted) {
+    const kycStatus = await kycStatusRes.json();
+    logger.info(`kyc status: ${j2s(kycStatus)}`);
+    kycUrl = kycStatus.kyc_url;
+  } else {
+    throw Error(`unexpected response from kyc-check (${kycStatusRes.status})`);
+  }
+
+  const transitionInfo = await ws.db
+    .mktx((x) => [x.planchets, x.withdrawalGroups])
+    .runReadWrite(async (tx) => {
+      for (let i = startIdx; i < requestCoinIdxs.length; i++) {
+        let planchet = await tx.planchets.indexes.byGroupAndIndex.get([
+          withdrawalGroup.withdrawalGroupId,
+          requestCoinIdxs[i],
+        ]);
+        if (!planchet) {
+          continue;
+        }
+        planchet.planchetStatus = PlanchetStatus.KycRequired;
+        await tx.planchets.put(planchet);
+      }
+      const wg2 = await tx.withdrawalGroups.get(
+        withdrawalGroup.withdrawalGroupId,
+      );
+      if (!wg2) {
+        return;
+      }
+      const oldTxState = computeWithdrawalTransactionStatus(wg2);
+      switch (wg2.status) {
+        case WithdrawalGroupStatus.PendingReady: {
+          wg2.kycPending = {
+            paytoHash: uuidResp.h_payto,
+            requirementRow: uuidResp.requirement_row,
+          };
+          wg2.kycUrl = kycUrl;
+          wg2.status = WithdrawalGroupStatus.PendingKyc;
+          await tx.withdrawalGroups.put(wg2);
+          const newTxState = computeWithdrawalTransactionStatus(wg2);
+          return {
+            oldTxState,
+            newTxState,
+          };
+        }
+        default:
+          return undefined;
+      }
+    });
+  notifyTransition(ws, transactionId, transitionInfo);
+}
+
 /**
  * Send the withdrawal request for a generated planchet to the exchange.
  *
@@ -805,43 +895,6 @@ async function processPlanchetExchangeBatchRequest(
     };
   }
 
-  async function handleKycRequired(
-    resp: HttpResponse,
-    startIdx: number,
-  ): Promise<void> {
-    logger.info("withdrawal requires KYC");
-    const respJson = await resp.json();
-    const uuidResp = codecForWalletKycUuid().decode(respJson);
-    logger.info(`kyc uuid response: ${j2s(uuidResp)}`);
-    await ws.db
-      .mktx((x) => [x.planchets, x.withdrawalGroups])
-      .runReadWrite(async (tx) => {
-        for (let i = startIdx; i < requestCoinIdxs.length; i++) {
-          let planchet = await tx.planchets.indexes.byGroupAndIndex.get([
-            withdrawalGroup.withdrawalGroupId,
-            requestCoinIdxs[i],
-          ]);
-          if (!planchet) {
-            continue;
-          }
-          planchet.planchetStatus = PlanchetStatus.KycRequired;
-          await tx.planchets.put(planchet);
-        }
-        const wg2 = await tx.withdrawalGroups.get(
-          withdrawalGroup.withdrawalGroupId,
-        );
-        if (!wg2) {
-          return;
-        }
-        wg2.kycPending = {
-          paytoHash: uuidResp.h_payto,
-          requirementRow: uuidResp.requirement_row,
-        };
-        await tx.withdrawalGroups.put(wg2);
-      });
-    return;
-  }
-
   async function storeCoinError(e: any, coinIdx: number): Promise<void> {
     const errDetail = getErrorDetailFromException(e);
     logger.trace("withdrawal request failed", e);
@@ -872,7 +925,7 @@ async function processPlanchetExchangeBatchRequest(
     try {
       const resp = await ws.http.postJson(reqUrl, batchReq);
       if (resp.status === HttpStatusCode.UnavailableForLegalReasons) {
-        await handleKycRequired(resp, 0);
+        await handleKycRequired(ws, withdrawalGroup, resp, 0, requestCoinIdxs);
       }
       const r = await readSuccessResponseJsonOrThrow(
         resp,
@@ -902,9 +955,15 @@ async function processPlanchetExchangeBatchRequest(
           `reserves/${withdrawalGroup.reservePub}/withdraw`,
           withdrawalGroup.exchangeBaseUrl,
         ).href;
-        const resp = await ws.http.postJson(reqUrl, p);
+        const resp = await ws.http.fetch(reqUrl, { method: "POST", body: p });
         if (resp.status === HttpStatusCode.UnavailableForLegalReasons) {
-          await handleKycRequired(resp, i);
+          await handleKycRequired(
+            ws,
+            withdrawalGroup,
+            resp,
+            i,
+            requestCoinIdxs,
+          );
           // We still return blinded coins that we could actually withdraw.
           return {
             coinIdxs: responseCoinIdxs,
@@ -1321,6 +1380,96 @@ async function processWithdrawalGroupAbortingBank(
   };
 }
 
+/**
+ * Store in the database that the KYC for a withdrawal is now
+ * satisfied.
+ */
+async function transitionKycSatisfied(
+  ws: InternalWalletState,
+  withdrawalGroup: WithdrawalGroupRecord,
+): Promise<void> {
+  const transactionId = constructTransactionIdentifier({
+    tag: TransactionType.Withdrawal,
+    withdrawalGroupId: withdrawalGroup.withdrawalGroupId,
+  });
+  const transitionInfo = await ws.db
+    .mktx((x) => [x.withdrawalGroups])
+    .runReadWrite(async (tx) => {
+      const wg2 = await tx.withdrawalGroups.get(
+        withdrawalGroup.withdrawalGroupId,
+      );
+      if (!wg2) {
+        return;
+      }
+      const oldTxState = computeWithdrawalTransactionStatus(wg2);
+      switch (wg2.status) {
+        case WithdrawalGroupStatus.PendingKyc: {
+          delete wg2.kycPending;
+          delete wg2.kycUrl;
+          wg2.status = WithdrawalGroupStatus.PendingReady;
+          await tx.withdrawalGroups.put(wg2);
+          const newTxState = computeWithdrawalTransactionStatus(wg2);
+          return {
+            oldTxState,
+            newTxState,
+          };
+        }
+        default:
+          return undefined;
+      }
+    });
+  notifyTransition(ws, transactionId, transitionInfo);
+}
+
+async function processWithdrawalGroupPendingKyc(
+  ws: InternalWalletState,
+  withdrawalGroup: WithdrawalGroupRecord,
+): Promise<OperationAttemptResult> {
+  const userType = "individual";
+  const kycInfo = withdrawalGroup.kycPending;
+  if (!kycInfo) {
+    throw Error("no kyc info available in pending(kyc)");
+  }
+  const exchangeUrl = withdrawalGroup.exchangeBaseUrl;
+  const url = new URL(
+    `kyc-check/${kycInfo.requirementRow}/${kycInfo.paytoHash}/${userType}`,
+    exchangeUrl,
+  );
+  url.searchParams.set("timeout_ms", "30000");
+
+  const retryTag = TaskIdentifiers.forWithdrawal(withdrawalGroup);
+
+  runLongpollAsync(ws, retryTag, async (cancellationToken) => {
+    logger.info(`long-polling for withdrawal KYC status via ${url.href}`);
+    const kycStatusRes = await ws.http.fetch(url.href, {
+      method: "GET",
+      cancellationToken,
+    });
+    logger.info(
+      `kyc long-polling response status: HTTP ${kycStatusRes.status}`,
+    );
+    if (
+      kycStatusRes.status === HttpStatusCode.Ok ||
+      //FIXME: NoContent is not expected 
https://docs.taler.net/core/api-exchange.html#post--purses-$PURSE_PUB-merge
+      // remove after the exchange is fixed or clarified
+      kycStatusRes.status === HttpStatusCode.NoContent
+    ) {
+      await transitionKycSatisfied(ws, withdrawalGroup);
+      return { ready: true };
+    } else if (kycStatusRes.status === HttpStatusCode.Accepted) {
+      const kycStatus = await kycStatusRes.json();
+      logger.info(`kyc status: ${j2s(kycStatus)}`);
+      // FIXME: do we need to update the KYC url, or does it always stay 
constant?
+      return { ready: false };
+    } else {
+      throw Error(
+        `unexpected response from kyc-check (${kycStatusRes.status})`,
+      );
+    }
+  });
+  return OperationAttemptResult.longpoll();
+}
+
 async function processWithdrawalGroupPendingReady(
   ws: InternalWalletState,
   withdrawalGroup: WithdrawalGroupRecord,
@@ -1419,8 +1568,6 @@ async function processWithdrawalGroupPendingReady(
   }
 
   let numFinished = 0;
-  let numKycRequired = 0;
-  let finishedForFirstTime = false;
   const errorsPerCoin: Record<number, TalerErrorDetail> = {};
   let numPlanchetErrors = 0;
   const maxReportedErrors = 5;
@@ -1439,9 +1586,6 @@ async function processWithdrawalGroupPendingReady(
           if (x.planchetStatus === PlanchetStatus.WithdrawalDone) {
             numFinished++;
           }
-          if (x.planchetStatus === PlanchetStatus.KycRequired) {
-            numKycRequired++;
-          }
           if (x.lastError) {
             numPlanchetErrors++;
             if (numPlanchetErrors < maxReportedErrors) {
@@ -1452,7 +1596,6 @@ async function processWithdrawalGroupPendingReady(
       const oldTxState = computeWithdrawalTransactionStatus(wg);
       logger.info(`now withdrawn ${numFinished} of ${numTotalCoins} coins`);
       if (wg.timestampFinish === undefined && numFinished === numTotalCoins) {
-        finishedForFirstTime = true;
         wg.timestampFinish = TalerPreciseTimestamp.now();
         wg.status = WithdrawalGroupStatus.Finished;
       }
@@ -1475,46 +1618,6 @@ async function processWithdrawalGroupPendingReady(
 
   notifyTransition(ws, transactionId, res.transitionInfo);
 
-  const { kycInfo } = res;
-
-  if (numKycRequired > 0) {
-    if (kycInfo) {
-      const txId = constructTransactionIdentifier({
-        tag: TransactionType.Withdrawal,
-        withdrawalGroupId: withdrawalGroup.withdrawalGroupId,
-      });
-      await checkWithdrawalKycStatus(
-        ws,
-        withdrawalGroup.exchangeBaseUrl,
-        txId,
-        kycInfo,
-        "individual",
-      );
-      return {
-        type: OperationAttemptResultType.Pending,
-        result: undefined,
-      };
-    } else {
-      throw TalerError.fromDetail(
-        TalerErrorCode.WALLET_WITHDRAWAL_KYC_REQUIRED,
-        {
-          //FIXME we can't rise KYC error here since we don't have the url
-        } as any,
-        `KYC check required for withdrawal (not yet implemented in 
wallet-core)`,
-      );
-    }
-  }
-  if (numFinished != numTotalCoins) {
-    throw TalerError.fromDetail(
-      TalerErrorCode.WALLET_WITHDRAWAL_GROUP_INCOMPLETE,
-      {
-        numErrors: numPlanchetErrors,
-        errorsPerCoin,
-      },
-      `withdrawal did not finish (${numFinished} / ${numTotalCoins} coins 
withdrawn)`,
-    );
-  }
-
   return {
     type: OperationAttemptResultType.Finished,
     result: undefined,
@@ -1588,53 +1691,30 @@ export async function processWithdrawalGroup(
         result: undefined,
       };
     }
+    case WithdrawalGroupStatus.PendingAml:
+      // FIXME: Handle this case, withdrawal doesn't support AML yet.
+      return OperationAttemptResult.pendingEmpty();
+    case WithdrawalGroupStatus.PendingKyc:
+      return processWithdrawalGroupPendingKyc(ws, withdrawalGroup);
     case WithdrawalGroupStatus.PendingReady:
       // Continue with the actual withdrawal!
       return await processWithdrawalGroupPendingReady(ws, withdrawalGroup);
     case WithdrawalGroupStatus.AbortingBank:
       return await processWithdrawalGroupAbortingBank(ws, withdrawalGroup);
+    case WithdrawalGroupStatus.AbortedBank:
+    case WithdrawalGroupStatus.AbortedExchange:
+    case WithdrawalGroupStatus.FailedAbortingBank:
+    case WithdrawalGroupStatus.SuspendedAbortingBank:
+    case WithdrawalGroupStatus.SuspendedAml:
+    case WithdrawalGroupStatus.SuspendedKyc:
+    case WithdrawalGroupStatus.SuspendedQueryingStatus:
+    case WithdrawalGroupStatus.SuspendedReady:
+    case WithdrawalGroupStatus.SuspendedRegisteringBank:
+    case WithdrawalGroupStatus.SuspendedWaitConfirmBank:
+      // Nothing to do.
+      return OperationAttemptResult.finishedEmpty();
     default:
-      throw new InvariantViolatedError(
-        `unknown withdrawal group status: ${withdrawalGroup.status}`,
-      );
-  }
-}
-
-export async function checkWithdrawalKycStatus(
-  ws: InternalWalletState,
-  exchangeUrl: string,
-  txId: string,
-  kycInfo: KycPendingInfo,
-  userType: KycUserType,
-): Promise<void> {
-  const url = new URL(
-    `kyc-check/${kycInfo.requirementRow}/${kycInfo.paytoHash}/${userType}`,
-    exchangeUrl,
-  );
-  logger.info(`kyc url ${url.href}`);
-  const kycStatusRes = await ws.http.fetch(url.href, {
-    method: "GET",
-  });
-  if (
-    kycStatusRes.status === HttpStatusCode.Ok ||
-    //FIXME: NoContent is not expected 
https://docs.taler.net/core/api-exchange.html#post--purses-$PURSE_PUB-merge
-    // remove after the exchange is fixed or clarified
-    kycStatusRes.status === HttpStatusCode.NoContent
-  ) {
-    logger.warn("kyc requested, but already fulfilled");
-    return;
-  } else if (kycStatusRes.status === HttpStatusCode.Accepted) {
-    const kycStatus = await kycStatusRes.json();
-    logger.info(`kyc status: ${j2s(kycStatus)}`);
-    throw TalerError.fromDetail(
-      TalerErrorCode.WALLET_WITHDRAWAL_KYC_REQUIRED, //FIXME: another error 
code or rename for merge
-      {
-        kycUrl: kycStatus.kyc_url,
-      },
-      `KYC check required for transfer`,
-    );
-  } else {
-    throw Error(`unexpected response from kyc-check (${kycStatusRes.status})`);
+      assertUnreachable(withdrawalGroup.status);
   }
 }
 

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]