use /paid API for proof of purchase

This commit is contained in:
Florian Dold 2020-08-19 20:55:38 +05:30
parent f7299a1aa0
commit 082498b20d
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
3 changed files with 151 additions and 57 deletions

View File

@ -60,7 +60,11 @@ import { createRefreshGroup, getTotalRefreshCost } from "./refresh";
import { InternalWalletState, EXCHANGE_COINS_LOCK } from "./state"; import { InternalWalletState, EXCHANGE_COINS_LOCK } from "./state";
import { getTimestampNow, timestampAddDuration } from "../util/time"; import { getTimestampNow, timestampAddDuration } from "../util/time";
import { strcmp, canonicalJson } from "../util/helpers"; import { strcmp, canonicalJson } from "../util/helpers";
import { readSuccessResponseJsonOrThrow } from "../util/http"; import {
readSuccessResponseJsonOrThrow,
throwUnexpectedRequestError,
getHttpResponseErrorDetails,
} from "../util/http";
import { TalerErrorCode } from "../TalerErrorCode"; import { TalerErrorCode } from "../TalerErrorCode";
import { URL } from "../util/url"; import { URL } from "../util/url";
@ -457,6 +461,7 @@ async function recordConfirmPay(
autoRefundDeadline: undefined, autoRefundDeadline: undefined,
paymentSubmitPending: true, paymentSubmitPending: true,
refunds: {}, refunds: {},
merchantPaySig: undefined,
}; };
await ws.db.runWithWriteTransaction( await ws.db.runWithWriteTransaction(
@ -769,6 +774,89 @@ async function startDownloadProposal(
return proposalId; 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( export async function submitPay(
ws: InternalWalletState, ws: InternalWalletState,
proposalId: string, proposalId: string,
@ -784,71 +872,66 @@ export async function submitPay(
logger.trace("paying with session ID", sessionId); logger.trace("paying with session ID", sessionId);
const payUrl = new URL( if (!purchase.merchantPaySig) {
`orders/${purchase.contractData.orderId}/pay`, const payUrl = new URL(
purchase.contractData.merchantBaseUrl, `orders/${purchase.contractData.orderId}/pay`,
).href; purchase.contractData.merchantBaseUrl,
).href;
const reqBody = { const reqBody = {
coins: purchase.coinDepositPermissions, coins: purchase.coinDepositPermissions,
session_id: purchase.lastSessionId, 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], () => const resp = await ws.runSequentialized([EXCHANGE_COINS_LOCK], () =>
ws.http.postJson(payUrl, reqBody), ws.http.postJson(payUrl, reqBody),
); );
const merchantResp = await readSuccessResponseJsonOrThrow( const merchantResp = await readSuccessResponseJsonOrThrow(
resp, resp,
codecForMerchantPayResponse(), 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; if (!valid) {
const valid: boolean = await ws.cryptoApi.isValidPaymentSignature( logger.error("merchant payment signature invalid");
merchantResp.sig, // FIXME: properly display error
purchase.contractData.contractTermsHash, throw Error("merchant payment signature invalid");
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);
} }
}
await ws.db.runWithWriteTransaction( await storeFirstPaySuccess(ws, proposalId, sessionId, merchantResp.sig);
[Stores.purchases, Stores.payEvents], } else {
async (tx) => { const payAgainUrl = new URL(
await tx.put(Stores.purchases, purchase); `orders/${purchase.contractData.orderId}/paid`,
const payEvent: PayEventRecord = { purchase.contractData.merchantBaseUrl,
proposalId, ).href;
sessionId, const reqBody = {
timestamp: now, sig: purchase.merchantPaySig,
isReplay: !isFirst, h_contract: purchase.contractData.contractTermsHash,
}; session_id: sessionId ?? "",
await tx.put(Stores.payEvents, payEvent); };
}, 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); const nextUrl = getNextUrl(purchase.contractData);
ws.cachedNextUrl[purchase.contractData.fulfillmentUrl] = { ws.cachedNextUrl[purchase.contractData.fulfillmentUrl] = {

View File

@ -1309,6 +1309,8 @@ export interface PurchaseRecord {
*/ */
timestampFirstSuccessfulPay: Timestamp | undefined; timestampFirstSuccessfulPay: Timestamp | undefined;
merchantPaySig: string | undefined;
/** /**
* When was the purchase made? * When was the purchase made?
* Refers to the time that the user accepted. * Refers to the time that the user accepted.

View File

@ -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( export function throwUnexpectedRequestError(
httpResponse: HttpResponse, httpResponse: HttpResponse,
talerErrorResponse: TalerErrorResponse, talerErrorResponse: TalerErrorResponse,