From 1b9c5855a8afb6833ff7a706f5bed5650e1191ad Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Sun, 15 Dec 2019 21:40:06 +0100 Subject: [PATCH] simplify /pay, add pay event --- src/crypto/workers/cryptoApi.ts | 6 +- src/crypto/workers/cryptoImplementation.ts | 22 +-- src/operations/pay.ts | 160 ++++++--------------- src/operations/return.ts | 15 +- src/operations/state.ts | 2 - src/types/dbTypes.ts | 17 +++ src/types/walletTypes.ts | 24 +++- 7 files changed, 105 insertions(+), 141 deletions(-) diff --git a/src/crypto/workers/cryptoApi.ts b/src/crypto/workers/cryptoApi.ts index 3c6758670..da807cce0 100644 --- a/src/crypto/workers/cryptoApi.ts +++ b/src/crypto/workers/cryptoApi.ts @@ -39,7 +39,7 @@ import { ContractTerms, PaybackRequest } from "../../types/talerTypes"; import { BenchmarkResult, CoinWithDenom, - PayCoinInfo, + PaySigInfo, PlanchetCreationResult, PlanchetCreationRequest, } from "../../types/walletTypes"; @@ -387,8 +387,8 @@ export class CryptoApi { contractTerms: ContractTerms, cds: CoinWithDenom[], totalAmount: AmountJson, - ): Promise { - return this.doRpc( + ): Promise { + return this.doRpc( "signDeposit", 3, contractTerms, diff --git a/src/crypto/workers/cryptoImplementation.ts b/src/crypto/workers/cryptoImplementation.ts index 01cd797b9..0049a1222 100644 --- a/src/crypto/workers/cryptoImplementation.ts +++ b/src/crypto/workers/cryptoImplementation.ts @@ -39,11 +39,12 @@ import { CoinPaySig, ContractTerms, PaybackRequest } from "../../types/talerType import { BenchmarkResult, CoinWithDenom, - PayCoinInfo, + PaySigInfo, Timestamp, PlanchetCreationResult, PlanchetCreationRequest, getTimestampNow, + CoinPayInfo, } from "../../types/walletTypes"; import { canonicalJson, getTalerStampSec } from "../../util/helpers"; import { AmountJson } from "../../util/amounts"; @@ -348,11 +349,9 @@ export class CryptoImplementation { contractTerms: ContractTerms, cds: CoinWithDenom[], totalAmount: AmountJson, - ): PayCoinInfo { - const ret: PayCoinInfo = { - originalCoins: [], - sigs: [], - updatedCoins: [], + ): PaySigInfo { + const ret: PaySigInfo = { + coinInfo: [], }; const contractTermsHash = this.hashString(canonicalJson(contractTerms)); @@ -369,8 +368,6 @@ export class CryptoImplementation { let amountRemaining = total; for (const cd of cds) { - const originalCoin = { ...cd.coin }; - if (amountRemaining.value === 0 && amountRemaining.fraction === 0) { break; } @@ -416,9 +413,12 @@ export class CryptoImplementation { exchange_url: cd.denom.exchangeBaseUrl, ub_sig: cd.coin.denomSig, }; - ret.sigs.push(s); - ret.updatedCoins.push(cd.coin); - ret.originalCoins.push(originalCoin); + const coinInfo: CoinPayInfo = { + sig: s, + coinPub: cd.coin.coinPub, + subtractedAmount: coinSpend, + }; + ret.coinInfo.push(coinInfo); } return ret; } diff --git a/src/operations/pay.ts b/src/operations/pay.ts index 5ed293505..363688dbd 100644 --- a/src/operations/pay.ts +++ b/src/operations/pay.ts @@ -36,6 +36,7 @@ import { RefundReason, Stores, updateRetryInfoTimeout, + PayEventRecord, } from "../types/dbTypes"; import { NotificationType } from "../types/notifications"; import { @@ -52,7 +53,7 @@ import { ConfirmPayResult, getTimestampNow, OperationError, - PayCoinInfo, + PaySigInfo, PreparePayResult, RefreshReason, Timestamp, @@ -73,13 +74,6 @@ import { createRefreshGroup, getTotalRefreshCost } from "./refresh"; import { acceptRefundResponse } from "./refund"; import { InternalWalletState } from "./state"; -export interface SpeculativePayData { - payCoinInfo: PayCoinInfo; - exchangeUrl: string; - orderDownloadId: string; - proposal: ProposalRecord; -} - interface CoinsForPaymentArgs { allowedAuditors: Auditor[]; allowedExchanges: ExchangeHandle[]; @@ -323,8 +317,7 @@ async function getCoinsForPayment( async function recordConfirmPay( ws: InternalWalletState, proposal: ProposalRecord, - payCoinInfo: PayCoinInfo, - chosenExchange: string, + payCoinInfo: PaySigInfo, sessionIdOverride: string | undefined, ): Promise { const d = proposal.download; @@ -339,7 +332,7 @@ async function recordConfirmPay( } logger.trace(`recording payment with session ID ${sessionId}`); const payReq: PayReq = { - coins: payCoinInfo.sigs, + coins: payCoinInfo.coinInfo.map((x) => x.sig), merchant_pub: d.contractTerms.merchant_pub, mode: "pay", order_id: d.contractTerms.order_id, @@ -374,7 +367,7 @@ async function recordConfirmPay( }; await ws.db.runWithWriteTransaction( - [Stores.coins, Stores.purchases, Stores.proposals], + [Stores.coins, Stores.purchases, Stores.proposals, Stores.refreshGroups], async tx => { const p = await tx.get(Stores.proposals, proposal.proposalId); if (p) { @@ -384,9 +377,21 @@ async function recordConfirmPay( await tx.put(Stores.proposals, p); } await tx.put(Stores.purchases, t); - for (let c of payCoinInfo.updatedCoins) { - await tx.put(Stores.coins, c); + for (let coinInfo of payCoinInfo.coinInfo) { + const coin = await tx.get(Stores.coins, coinInfo.coinPub); + if (!coin) { + throw Error("coin allocated for payment doesn't exist anymore"); + } + coin.status = CoinStatus.Dormant; + const remaining = Amounts.sub(coin.currentAmount, coinInfo.subtractedAmount); + if (remaining.saturated) { + throw Error("not enough remaining balance on coin for payment"); + } + coin.currentAmount = remaining.amount; + await tx.put(Stores.coins, coin); } + const refreshCoinPubs = payCoinInfo.coinInfo.map((x) => ({coinPub: x.coinPub})); + await createRefreshGroup(tx, refreshCoinPubs, RefreshReason.Pay); }, ); @@ -707,6 +712,8 @@ export async function submitPay( const merchantResp = await resp.json(); console.log("got success from pay URL", merchantResp); + const now = getTimestampNow(); + const merchantPub = purchase.contractTerms.merchant_pub; const valid: boolean = await ws.cryptoApi.isValidPaymentSignature( merchantResp.sig, @@ -719,7 +726,7 @@ export async function submitPay( throw Error("merchant payment signature invalid"); } const isFirst = purchase.firstSuccessfulPayTimestamp === undefined; - purchase.firstSuccessfulPayTimestamp = getTimestampNow(); + purchase.firstSuccessfulPayTimestamp = now; purchase.paymentSubmitPending = false; purchase.lastPayError = undefined; purchase.payRetryInfo = initRetryInfo(false); @@ -734,35 +741,22 @@ export async function submitPay( purchase.refundStatusRetryInfo = initRetryInfo(); purchase.lastRefundStatusError = undefined; purchase.autoRefundDeadline = { - t_ms: getTimestampNow().t_ms + autoRefundDelay.d_ms, + t_ms: now.t_ms + autoRefundDelay.d_ms, }; } } } - const modifiedCoins: CoinRecord[] = []; - for (const pc of purchase.payReq.coins) { - const c = await ws.db.get(Stores.coins, pc.coin_pub); - if (!c) { - console.error("coin not found"); - throw Error("coin used in payment not found"); - } - c.status = CoinStatus.Dormant; - modifiedCoins.push(c); - } - await ws.db.runWithWriteTransaction( - [Stores.coins, Stores.purchases, Stores.refreshGroups], + [Stores.purchases, Stores.payEvents], async tx => { - for (let c of modifiedCoins) { - await tx.put(Stores.coins, c); - } - await createRefreshGroup( - tx, - modifiedCoins.map(x => ({ coinPub: x.coinPub })), - RefreshReason.Pay, - ); await tx.put(Stores.purchases, purchase); + const payEvent: PayEventRecord = { + proposalId, + sessionId, + timestamp: now, + }; + await tx.put(Stores.payEvents, payEvent); }, ); @@ -861,27 +855,6 @@ export async function preparePay( }; } - // Only create speculative signature if we don't already have one for this proposal - if ( - !ws.speculativePayData || - (ws.speculativePayData && - ws.speculativePayData.orderDownloadId !== proposalId) - ) { - const { exchangeUrl, cds, totalAmount } = res; - const payCoinInfo = await ws.cryptoApi.signDeposit( - contractTerms, - cds, - totalAmount, - ); - ws.speculativePayData = { - exchangeUrl, - payCoinInfo, - proposal, - orderDownloadId: proposalId, - }; - logger.trace("created speculative pay data for payment"); - } - return { status: "payment-possible", contractTerms: contractTerms, @@ -901,43 +874,6 @@ export async function preparePay( }; } -/** - * Get the speculative pay data, but only if coins have not changed in between. - */ -async function getSpeculativePayData( - ws: InternalWalletState, - proposalId: string, -): Promise { - const sp = ws.speculativePayData; - if (!sp) { - return; - } - if (sp.orderDownloadId !== proposalId) { - return; - } - const coinKeys = sp.payCoinInfo.updatedCoins.map(x => x.coinPub); - const coins: CoinRecord[] = []; - for (let coinKey of coinKeys) { - const cc = await ws.db.get(Stores.coins, coinKey); - if (cc) { - coins.push(cc); - } - } - for (let i = 0; i < coins.length; i++) { - const specCoin = sp.payCoinInfo.originalCoins[i]; - const currentCoin = coins[i]; - - // Coin does not exist anymore! - if (!currentCoin) { - return; - } - if (Amounts.cmp(specCoin.currentAmount, currentCoin.currentAmount) !== 0) { - return; - } - } - return sp; -} - /** * Add a contract to the wallet and sign coins, and send them. */ @@ -1008,30 +944,18 @@ export async function confirmPay( throw Error("insufficient balance"); } - const sd = await getSpeculativePayData(ws, proposalId); - if (!sd) { - const { exchangeUrl, cds, totalAmount } = res; - const payCoinInfo = await ws.cryptoApi.signDeposit( - d.contractTerms, - cds, - totalAmount, - ); - purchase = await recordConfirmPay( - ws, - proposal, - payCoinInfo, - exchangeUrl, - sessionIdOverride, - ); - } else { - purchase = await recordConfirmPay( - ws, - sd.proposal, - sd.payCoinInfo, - sd.exchangeUrl, - sessionIdOverride, - ); - } + const { cds, totalAmount } = res; + const payCoinInfo = await ws.cryptoApi.signDeposit( + d.contractTerms, + cds, + totalAmount, + ); + purchase = await recordConfirmPay( + ws, + proposal, + payCoinInfo, + sessionIdOverride + ); logger.trace("confirmPay: submitting payment after creating purchase record"); return submitPay(ws, proposalId); diff --git a/src/operations/return.ts b/src/operations/return.ts index 01d2802d9..4238f6cd2 100644 --- a/src/operations/return.ts +++ b/src/operations/return.ts @@ -176,7 +176,7 @@ export async function returnCoins( logger.trace("pci", payCoinInfo); - const coins = payCoinInfo.sigs.map(s => ({ coinPaySig: s })); + const coins = payCoinInfo.coinInfo.map(s => ({ coinPaySig: s.sig })); const coinsReturnRecord: CoinsReturnRecord = { coins, @@ -191,8 +191,17 @@ export async function returnCoins( [Stores.coinsReturns, Stores.coins], async tx => { await tx.put(Stores.coinsReturns, coinsReturnRecord); - for (let c of payCoinInfo.updatedCoins) { - await tx.put(Stores.coins, c); + for (let coinInfo of payCoinInfo.coinInfo) { + const coin = await tx.get(Stores.coins, coinInfo.coinPub); + if (!coin) { + throw Error("coin allocated for deposit not in database anymore"); + } + const remaining = Amounts.sub(coin.currentAmount, coinInfo.subtractedAmount); + if (remaining.saturated) { + throw Error("coin allocated for deposit does not have enough balance"); + } + coin.currentAmount = remaining.amount; + await tx.put(Stores.coins, coin); } }, ); diff --git a/src/operations/state.ts b/src/operations/state.ts index 1e4b90360..3e4936c98 100644 --- a/src/operations/state.ts +++ b/src/operations/state.ts @@ -19,7 +19,6 @@ import { NextUrlResult, WalletBalance, } from "../types/walletTypes"; -import { SpeculativePayData } from "./pay"; import { CryptoApi, CryptoWorkerFactory } from "../crypto/workers/cryptoApi"; import { AsyncOpMemoMap, AsyncOpMemoSingle } from "../util/asyncMemo"; import { Logger } from "../util/logging"; @@ -32,7 +31,6 @@ type NotificationListener = (n: WalletNotification) => void; const logger = new Logger("state.ts"); export class InternalWalletState { - speculativePayData: SpeculativePayData | undefined = undefined; cachedNextUrl: { [fulfillmentUrl: string]: NextUrlResult } = {}; memoProcessReserve: AsyncOpMemoMap = new AsyncOpMemoMap(); memoMakePlanchet: AsyncOpMemoMap = new AsyncOpMemoMap(); diff --git a/src/types/dbTypes.ts b/src/types/dbTypes.ts index 9d2f6fe5d..7447fc546 100644 --- a/src/types/dbTypes.ts +++ b/src/types/dbTypes.ts @@ -1059,6 +1059,16 @@ export interface PurchaseRefundState { refundsFailed: { [refundSig: string]: RefundInfo }; } +/** + * Record stored for every time we successfully submitted + * a payment to the merchant (both first time and re-play). + */ +export interface PayEventRecord { + proposalId: string; + sessionId: string | undefined; + timestamp: Timestamp; +} + /** * Record that stores status information about one purchase, starting from when * the customer accepts a proposal. Includes refund status if applicable. @@ -1432,6 +1442,12 @@ export namespace Stores { } } + class PayEventsStore extends Store { + constructor() { + super("payEvents", { keyPath: "proposalId" }); + } + } + class BankWithdrawUrisStore extends Store { constructor() { super("bankWithdrawUris", { keyPath: "talerWithdrawUri" }); @@ -1457,6 +1473,7 @@ export namespace Stores { export const withdrawalSession = new WithdrawalSessionsStore(); export const bankWithdrawUris = new BankWithdrawUrisStore(); export const refundEvents = new RefundEventsStore(); + export const payEvents = new PayEventsStore(); } /* tslint:enable:completed-docs */ diff --git a/src/types/walletTypes.ts b/src/types/walletTypes.ts index eedae6f2c..df19d8dc2 100644 --- a/src/types/walletTypes.ts +++ b/src/types/walletTypes.ts @@ -195,14 +195,30 @@ export interface WalletBalanceEntry { pendingIncomingDirty: AmountJson; } +export interface CoinPayInfo { + /** + * Amount that will be subtracted from the coin when the payment is finalized. + */ + subtractedAmount: AmountJson; + + /** + * Public key of the coin that is being spent. + */ + coinPub: string; + + /** + * Signature together with the other information needed by the merchant, + * directly in the format expected by the merchant. + */ + sig: CoinPaySig; +} + /** * Coins used for a payment, with signatures authorizing the payment and the * coins with remaining value updated to accomodate for a payment. */ -export interface PayCoinInfo { - originalCoins: CoinRecord[]; - updatedCoins: CoinRecord[]; - sigs: CoinPaySig[]; +export interface PaySigInfo { + coinInfo: CoinPayInfo[]; } /**