simplify /pay, add pay event

This commit is contained in:
Florian Dold 2019-12-15 21:40:06 +01:00
parent 59bd755f7d
commit 1b9c5855a8
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
7 changed files with 105 additions and 141 deletions

View File

@ -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<PayCoinInfo> {
return this.doRpc<PayCoinInfo>(
): Promise<PaySigInfo> {
return this.doRpc<PaySigInfo>(
"signDeposit",
3,
contractTerms,

View File

@ -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;
}

View File

@ -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<PurchaseRecord> {
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<SpeculativePayData | undefined> {
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);

View File

@ -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);
}
},
);

View File

@ -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<void> = new AsyncOpMemoMap();
memoMakePlanchet: AsyncOpMemoMap<void> = new AsyncOpMemoMap();

View File

@ -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<PayEventRecord> {
constructor() {
super("payEvents", { keyPath: "proposalId" });
}
}
class BankWithdrawUrisStore extends Store<BankWithdrawUriRecord> {
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 */

View File

@ -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[];
}
/**