From 43655adff0f8f1db082e1f0c18f1271a29ab8905 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Tue, 28 Jul 2020 15:18:01 +0530 Subject: [PATCH] updated preparePay API according to spec --- src/TalerErrorCode.ts | 23 ++++++++++++- src/headless/taler-wallet-cli.ts | 32 ++++++++++--------- src/operations/pay.ts | 55 +++++++++++++++++++++----------- src/types/walletTypes.ts | 26 ++++++++------- src/webex/pages/pay.tsx | 8 ++--- 5 files changed, 91 insertions(+), 53 deletions(-) diff --git a/src/TalerErrorCode.ts b/src/TalerErrorCode.ts index f338f1ad4..56c28ee20 100644 --- a/src/TalerErrorCode.ts +++ b/src/TalerErrorCode.ts @@ -550,7 +550,7 @@ export enum TalerErrorCode { DEPOSIT_INVALID_WIRE_FORMAT_JSON = 1210, /** - * The hash of the given wire address does not match the hash specified in the proposal data. + * The hash of the given wire address does not match the wire hash specified in the proposal data. * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). * (A value of 0 indicates that the error is generated client-side). */ @@ -1403,6 +1403,13 @@ export enum TalerErrorCode { */ PAY_EXCHANGE_FAILED = 2135, + /** + * The merchant backend couldn't verify the order payment because of a database failure. + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + PAID_DB_ERROR = 2146, + /** * The order is not known. * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). @@ -2677,6 +2684,13 @@ export enum TalerErrorCode { */ MERCHANT_ORDER_GET_REPLY_MALFORMED = 2922, + /** + * The token used to authenticate the client is invalid for this order. + * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_GET_ORDER_INVALID_TOKEN = 2923, + /** * The signature from the exchange on the deposit confirmation is invalid. Returned with a "400 Bad Request" status code. * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). @@ -3062,6 +3076,13 @@ export enum TalerErrorCode { */ WALLET_CORE_API_OPERATION_UNKNOWN = 7007, + /** + * The given taler://pay URI is invalid. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + WALLET_INVALID_TALER_PAY_URI = 7008, + /** * The exchange does not know about the reserve (yet), and thus withdrawal can't progress. * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). diff --git a/src/headless/taler-wallet-cli.ts b/src/headless/taler-wallet-cli.ts index 4bae298a0..ca0f0f5d1 100644 --- a/src/headless/taler-wallet-cli.ts +++ b/src/headless/taler-wallet-cli.ts @@ -43,6 +43,7 @@ import { NodeHttpLib } from "./NodeHttpLib"; import * as nacl from "../crypto/primitives/nacl-fast"; import { addPaytoQueryParams } from "../util/payto"; import { handleCoreApiRequest } from "../walletCoreApiHandler"; +import { PreparePayResultType } from "../types/walletTypes"; const logger = new Logger("taler-wallet-cli.ts"); @@ -58,19 +59,19 @@ async function doPay( options: { alwaysYes: boolean } = { alwaysYes: true }, ): Promise { const result = await wallet.preparePayForUri(payUrl); - if (result.status === "error") { - console.error("Could not pay:", result.error); - process.exit(1); - return; - } - if (result.status === "insufficient-balance") { + if (result.status === PreparePayResultType.InsufficientBalance) { console.log("contract", result.contractTermsRaw); console.error("insufficient balance"); process.exit(1); return; } - if (result.status === "paid") { - console.log("already paid!"); + if (result.status === PreparePayResultType.AlreadyConfirmed) { + if (result.paid) { + console.log("already paid!"); + } else { + console.log("payment already in progress"); + } + process.exit(0); return; } @@ -502,16 +503,17 @@ advancedCli await withWallet(args, async (wallet) => { const res = await wallet.preparePayForUri(args.payPrepare.url); switch (res.status) { - case "error": - console.log("error:", res.error); - break; - case "insufficient-balance": + case PreparePayResultType.InsufficientBalance: console.log("insufficient balance"); break; - case "paid": - console.log("already paid"); + case PreparePayResultType.AlreadyConfirmed: + if (res.paid) { + console.log("already paid!"); + } else { + console.log("payment in progress"); + } break; - case "payment-possible": + case PreparePayResultType.PaymentPossible: console.log("payment possible"); break; default: diff --git a/src/operations/pay.ts b/src/operations/pay.ts index 58911b4bf..08bbe43a1 100644 --- a/src/operations/pay.ts +++ b/src/operations/pay.ts @@ -48,19 +48,19 @@ import { OperationErrorDetails, PreparePayResult, RefreshReason, + PreparePayResultType, } from "../types/walletTypes"; import * as Amounts from "../util/amounts"; import { AmountJson } from "../util/amounts"; import { Logger } from "../util/logging"; import { parsePayUri } from "../util/taleruri"; -import { guardOperationException } from "./errors"; +import { guardOperationException, OperationFailedError } from "./errors"; import { createRefreshGroup, getTotalRefreshCost } from "./refresh"; import { InternalWalletState } from "./state"; import { getTimestampNow, timestampAddDuration } from "../util/time"; import { strcmp, canonicalJson } from "../util/helpers"; -import { - readSuccessResponseJsonOrThrow, -} from "../util/http"; +import { readSuccessResponseJsonOrThrow } from "../util/http"; +import { TalerErrorCode } from "../TalerErrorCode"; /** * Logger. @@ -783,7 +783,7 @@ export async function submitPay( coins: purchase.coinDepositPermissions, session_id: purchase.lastSessionId, }; - + logger.trace("making pay request", JSON.stringify(reqBody, undefined, 2)); const resp = await ws.http.postJson(payUrl, reqBody); @@ -860,10 +860,13 @@ export async function preparePayForUri( const uriResult = parsePayUri(talerPayUri); if (!uriResult) { - return { - status: "error", - error: "URI not supported", - }; + throw OperationFailedError.fromCode( + TalerErrorCode.WALLET_INVALID_TALER_PAY_URI, + `invalid taler://pay URI (${talerPayUri})`, + { + talerPayUri, + } + ); } let proposalId = await startDownloadProposal( @@ -911,7 +914,7 @@ export async function preparePayForUri( if (!res) { console.log("not confirming payment, insufficient coins"); return { - status: "insufficient-balance", + status: PreparePayResultType.InsufficientBalance, contractTermsRaw: d.contractTermsRaw, proposalId: proposal.proposalId, }; @@ -923,14 +926,14 @@ export async function preparePayForUri( const totalFees = Amounts.sub(costInfo.totalCost, res.paymentAmount).amount; return { - status: "payment-possible", + status: PreparePayResultType.PaymentPossible, contractTermsRaw: d.contractTermsRaw, proposalId: proposal.proposalId, totalFees, }; } - if (uriResult.sessionId && purchase.lastSessionId !== uriResult.sessionId) { + if (purchase.lastSessionId !== uriResult.sessionId) { console.log( "automatically re-submitting payment with different session ID", ); @@ -942,14 +945,28 @@ export async function preparePayForUri( p.lastSessionId = uriResult.sessionId; await tx.put(Stores.purchases, p); }); - await submitPay(ws, proposalId); + const r = await submitPay(ws, proposalId); + return { + status: PreparePayResultType.AlreadyConfirmed, + contractTermsRaw: purchase.contractTermsRaw, + paid: true, + nextUrl: r.nextUrl, + }; + } else if (!purchase.timestampFirstSuccessfulPay) { + return { + status: PreparePayResultType.AlreadyConfirmed, + contractTermsRaw: purchase.contractTermsRaw, + paid: false, + }; + } else if (purchase.paymentSubmitPending) { + return { + status: PreparePayResultType.AlreadyConfirmed, + contractTermsRaw: purchase.contractTermsRaw, + paid: false, + }; } - - return { - status: "paid", - contractTermsRaw: purchase.contractTermsRaw, - nextUrl: getNextUrl(purchase.contractData), - }; + // FIXME: we don't handle aborted payments correctly here. + throw Error("BUG: invariant violation (purchase status)"); } /** diff --git a/src/types/walletTypes.ts b/src/types/walletTypes.ts index 2ca95bf48..d68f41564 100644 --- a/src/types/walletTypes.ts +++ b/src/types/walletTypes.ts @@ -337,34 +337,36 @@ export interface NextUrlResult { lastSessionId: string | undefined; } +export const enum PreparePayResultType { + PaymentPossible = "payment-possible", + InsufficientBalance = "insufficient-balance", + AlreadyConfirmed = "already-confirmed", +} + export type PreparePayResult = - | PreparePayResultError | PreparePayResultInsufficientBalance - | PreparePayResultPaid + | PreparePayResultAlreadyConfirmed | PreparePayResultPaymentPossible; export interface PreparePayResultPaymentPossible { - status: "payment-possible"; + status: PreparePayResultType.PaymentPossible; proposalId: string; contractTermsRaw: string; totalFees: AmountJson; } export interface PreparePayResultInsufficientBalance { - status: "insufficient-balance"; + status: PreparePayResultType.InsufficientBalance; proposalId: string; contractTermsRaw: any; } -export interface PreparePayResultError { - status: "error"; - error: string; -} - -export interface PreparePayResultPaid { - status: "paid"; +export interface PreparePayResultAlreadyConfirmed { + status: PreparePayResultType.AlreadyConfirmed; contractTermsRaw: any; - nextUrl: string; + paid: boolean; + // Only specified if paid. + nextUrl?: string; } export interface BankWithdrawDetails { diff --git a/src/webex/pages/pay.tsx b/src/webex/pages/pay.tsx index a69b6b766..87f3c83d3 100644 --- a/src/webex/pages/pay.tsx +++ b/src/webex/pages/pay.tsx @@ -24,7 +24,7 @@ */ import * as i18n from "../i18n"; -import { PreparePayResult } from "../../types/walletTypes"; +import { PreparePayResult, PreparePayResultType } from "../../types/walletTypes"; import { renderAmount, ProgressButton } from "../renderHtml"; import * as wxApi from "../wxApi"; @@ -58,15 +58,11 @@ function TalerPayDialog({ talerPayUri }: { talerPayUri: string }): JSX.Element { insufficientBalance = true; } - if (payStatus.status === "error") { - return Error: {payStatus.error}; - } - if (payStatus.status === "payment-possible") { totalFees = payStatus.totalFees; } - if (payStatus.status === "paid" && numTries === 0) { + if (payStatus.status === PreparePayResultType.AlreadyConfirmed && numTries === 0) { return ( You have already paid for this article. Click{" "}