gnunet-svn
[Top][All Lists]
Advanced

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

[taler-wallet-core] 01/02: use /paid API for proof of purchase


From: gnunet
Subject: [taler-wallet-core] 01/02: use /paid API for proof of purchase
Date: Wed, 19 Aug 2020 17:26:49 +0200

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

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

commit 082498b20d2a73009bb4193cd95ab409159fb972
Author: Florian Dold <florian.dold@gmail.com>
AuthorDate: Wed Aug 19 20:55:38 2020 +0530

    use /paid API for proof of purchase
---
 packages/taler-wallet-core/src/operations/pay.ts | 197 ++++++++++++++++-------
 packages/taler-wallet-core/src/types/dbTypes.ts  |   2 +
 packages/taler-wallet-core/src/util/http.ts      |   9 ++
 3 files changed, 151 insertions(+), 57 deletions(-)

diff --git a/packages/taler-wallet-core/src/operations/pay.ts 
b/packages/taler-wallet-core/src/operations/pay.ts
index 996e1c1e..327a6c80 100644
--- a/packages/taler-wallet-core/src/operations/pay.ts
+++ b/packages/taler-wallet-core/src/operations/pay.ts
@@ -60,7 +60,11 @@ import { createRefreshGroup, getTotalRefreshCost } from 
"./refresh";
 import { InternalWalletState, EXCHANGE_COINS_LOCK } from "./state";
 import { getTimestampNow, timestampAddDuration } from "../util/time";
 import { strcmp, canonicalJson } from "../util/helpers";
-import { readSuccessResponseJsonOrThrow } from "../util/http";
+import {
+  readSuccessResponseJsonOrThrow,
+  throwUnexpectedRequestError,
+  getHttpResponseErrorDetails,
+} from "../util/http";
 import { TalerErrorCode } from "../TalerErrorCode";
 import { URL } from "../util/url";
 
@@ -457,6 +461,7 @@ async function recordConfirmPay(
     autoRefundDeadline: undefined,
     paymentSubmitPending: true,
     refunds: {},
+    merchantPaySig: undefined,
   };
 
   await ws.db.runWithWriteTransaction(
@@ -769,6 +774,89 @@ async function startDownloadProposal(
   return proposalId;
 }
 
+async function storeFirstPaySuccess(
+  ws: InternalWalletState,
+  proposalId: string,
+  sessionId: string | undefined,
+  paySig: string,
+): Promise<void> {
+  const now = getTimestampNow();
+  await ws.db.runWithWriteTransaction(
+    [Stores.purchases, Stores.payEvents],
+    async (tx) => {
+      const purchase = await tx.get(Stores.purchases, proposalId);
+
+      if (!purchase) {
+        logger.warn("purchase does not exist anymore");
+        return;
+      }
+      const isFirst = purchase.timestampFirstSuccessfulPay === undefined;
+      if (!isFirst) {
+        logger.warn("payment success already stored");
+        return;
+      }
+      purchase.timestampFirstSuccessfulPay = now;
+      purchase.paymentSubmitPending = false;
+      purchase.lastPayError = undefined;
+      purchase.lastSessionId = sessionId;
+      purchase.payRetryInfo = initRetryInfo(false);
+      purchase.merchantPaySig = paySig;
+      if (isFirst) {
+        const ar = purchase.contractData.autoRefund;
+        if (ar) {
+          logger.info("auto_refund present");
+          purchase.refundStatusRequested = true;
+          purchase.refundStatusRetryInfo = initRetryInfo();
+          purchase.lastRefundStatusError = undefined;
+          purchase.autoRefundDeadline = timestampAddDuration(now, ar);
+        }
+      }
+
+      await tx.put(Stores.purchases, purchase);
+      const payEvent: PayEventRecord = {
+        proposalId,
+        sessionId,
+        timestamp: now,
+        isReplay: !isFirst,
+      };
+      await tx.put(Stores.payEvents, payEvent);
+    },
+  );
+}
+
+async function storePayReplaySuccess(
+  ws: InternalWalletState,
+  proposalId: string,
+  sessionId: string | undefined,
+): Promise<void> {
+  await ws.db.runWithWriteTransaction(
+    [Stores.purchases, Stores.payEvents],
+    async (tx) => {
+      const purchase = await tx.get(Stores.purchases, proposalId);
+
+      if (!purchase) {
+        logger.warn("purchase does not exist anymore");
+        return;
+      }
+      const isFirst = purchase.timestampFirstSuccessfulPay === undefined;
+      if (isFirst) {
+        throw Error("invalid payment state");
+      }
+      purchase.paymentSubmitPending = false;
+      purchase.lastPayError = undefined;
+      purchase.payRetryInfo = initRetryInfo(false);
+      purchase.lastSessionId = sessionId;
+      await tx.put(Stores.purchases, purchase);
+    },
+  );
+}
+
+/**
+ * Submit a payment to the merchant.
+ *
+ * If the wallet has previously paid, it just transmits the merchant's
+ * own signature certifying that the wallet has previously paid.
+ */
 export async function submitPay(
   ws: InternalWalletState,
   proposalId: string,
@@ -784,71 +872,66 @@ export async function submitPay(
 
   logger.trace("paying with session ID", sessionId);
 
-  const payUrl = new URL(
-    `orders/${purchase.contractData.orderId}/pay`,
-    purchase.contractData.merchantBaseUrl,
-  ).href;
+  if (!purchase.merchantPaySig) {
+    const payUrl = new URL(
+      `orders/${purchase.contractData.orderId}/pay`,
+      purchase.contractData.merchantBaseUrl,
+    ).href;
 
-  const reqBody = {
-    coins: purchase.coinDepositPermissions,
-    session_id: purchase.lastSessionId,
-  };
+    const reqBody = {
+      coins: purchase.coinDepositPermissions,
+      session_id: purchase.lastSessionId,
+    };
 
-  logger.trace("making pay request", JSON.stringify(reqBody, undefined, 2));
+    logger.trace("making pay request", JSON.stringify(reqBody, undefined, 2));
 
-  const resp = await ws.runSequentialized([EXCHANGE_COINS_LOCK], () =>
-    ws.http.postJson(payUrl, reqBody),
-  );
+    const resp = await ws.runSequentialized([EXCHANGE_COINS_LOCK], () =>
+      ws.http.postJson(payUrl, reqBody),
+    );
 
-  const merchantResp = await readSuccessResponseJsonOrThrow(
-    resp,
-    codecForMerchantPayResponse(),
-  );
+    const merchantResp = await readSuccessResponseJsonOrThrow(
+      resp,
+      codecForMerchantPayResponse(),
+    );
 
-  logger.trace("got success from pay URL", merchantResp);
+    logger.trace("got success from pay URL", merchantResp);
 
-  const now = getTimestampNow();
+    const merchantPub = purchase.contractData.merchantPub;
+    const valid: boolean = await ws.cryptoApi.isValidPaymentSignature(
+      merchantResp.sig,
+      purchase.contractData.contractTermsHash,
+      merchantPub,
+    );
 
-  const merchantPub = purchase.contractData.merchantPub;
-  const valid: boolean = await ws.cryptoApi.isValidPaymentSignature(
-    merchantResp.sig,
-    purchase.contractData.contractTermsHash,
-    merchantPub,
-  );
-  if (!valid) {
-    logger.error("merchant payment signature invalid");
-    // FIXME: properly display error
-    throw Error("merchant payment signature invalid");
-  }
-  const isFirst = purchase.timestampFirstSuccessfulPay === undefined;
-  purchase.timestampFirstSuccessfulPay = now;
-  purchase.paymentSubmitPending = false;
-  purchase.lastPayError = undefined;
-  purchase.payRetryInfo = initRetryInfo(false);
-  if (isFirst) {
-    const ar = purchase.contractData.autoRefund;
-    if (ar) {
-      logger.info("auto_refund present");
-      purchase.refundStatusRequested = true;
-      purchase.refundStatusRetryInfo = initRetryInfo();
-      purchase.lastRefundStatusError = undefined;
-      purchase.autoRefundDeadline = timestampAddDuration(now, ar);
+    if (!valid) {
+      logger.error("merchant payment signature invalid");
+      // FIXME: properly display error
+      throw Error("merchant payment signature invalid");
     }
-  }
 
-  await ws.db.runWithWriteTransaction(
-    [Stores.purchases, Stores.payEvents],
-    async (tx) => {
-      await tx.put(Stores.purchases, purchase);
-      const payEvent: PayEventRecord = {
-        proposalId,
-        sessionId,
-        timestamp: now,
-        isReplay: !isFirst,
-      };
-      await tx.put(Stores.payEvents, payEvent);
-    },
-  );
+    await storeFirstPaySuccess(ws, proposalId, sessionId, merchantResp.sig);
+  } else {
+    const payAgainUrl = new URL(
+      `orders/${purchase.contractData.orderId}/paid`,
+      purchase.contractData.merchantBaseUrl,
+    ).href;
+    const reqBody = {
+      sig: purchase.merchantPaySig,
+      h_contract: purchase.contractData.contractTermsHash,
+      session_id: sessionId ?? "",
+    };
+    const resp = await ws.runSequentialized([EXCHANGE_COINS_LOCK], () =>
+      ws.http.postJson(payAgainUrl, reqBody),
+    );
+    if (resp.status !== 204) {
+      throw OperationFailedError.fromCode(
+        TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR,
+        "/paid failed",
+        getHttpResponseErrorDetails(resp),
+      );
+    }
+    await storePayReplaySuccess(ws, proposalId, sessionId);
+  }
 
   const nextUrl = getNextUrl(purchase.contractData);
   ws.cachedNextUrl[purchase.contractData.fulfillmentUrl] = {
diff --git a/packages/taler-wallet-core/src/types/dbTypes.ts 
b/packages/taler-wallet-core/src/types/dbTypes.ts
index 26e636e4..42192dd9 100644
--- a/packages/taler-wallet-core/src/types/dbTypes.ts
+++ b/packages/taler-wallet-core/src/types/dbTypes.ts
@@ -1309,6 +1309,8 @@ export interface PurchaseRecord {
    */
   timestampFirstSuccessfulPay: Timestamp | undefined;
 
+  merchantPaySig: string | undefined;
+
   /**
    * When was the purchase made?
    * Refers to the time that the user accepted.
diff --git a/packages/taler-wallet-core/src/util/http.ts 
b/packages/taler-wallet-core/src/util/http.ts
index 72de2ed1..22566daa 100644
--- a/packages/taler-wallet-core/src/util/http.ts
+++ b/packages/taler-wallet-core/src/util/http.ts
@@ -151,6 +151,15 @@ export async function 
readSuccessResponseJsonOrErrorCode<T>(
   };
 }
 
+export function getHttpResponseErrorDetails(
+  httpResponse: HttpResponse,
+): Record<string, unknown> {
+  return {
+    requestUrl: httpResponse.requestUrl,
+    httpStatusCode: httpResponse.status,
+  };
+}
+
 export function throwUnexpectedRequestError(
   httpResponse: HttpResponse,
   talerErrorResponse: TalerErrorResponse,

-- 
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]