aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Dold <florian.dold@gmail.com>2020-08-19 20:55:38 +0530
committerFlorian Dold <florian.dold@gmail.com>2020-08-19 20:55:38 +0530
commit082498b20d2a73009bb4193cd95ab409159fb972 (patch)
tree4416c1aa26fa5b3203fb5b0f1544dc7affe8dddf
parentf7299a1aa0952490629588aa5a853999684b3fb9 (diff)
use /paid API for proof of purchase
-rw-r--r--packages/taler-wallet-core/src/operations/pay.ts197
-rw-r--r--packages/taler-wallet-core/src/types/dbTypes.ts2
-rw-r--r--packages/taler-wallet-core/src/util/http.ts9
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 996e1c1ec..327a6c804 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 26e636e48..42192dd9a 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 72de2ed1d..22566daac 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,