aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/crypto/workers/cryptoApi.ts6
-rw-r--r--src/crypto/workers/cryptoImplementation.ts22
-rw-r--r--src/operations/pay.ts160
-rw-r--r--src/operations/return.ts15
-rw-r--r--src/operations/state.ts2
-rw-r--r--src/types/dbTypes.ts17
-rw-r--r--src/types/walletTypes.ts24
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<PayCoinInfo> {
- return this.doRpc<PayCoinInfo>(
+ ): Promise<PaySigInfo> {
+ return this.doRpc<PaySigInfo>(
"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<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,
@@ -902,43 +875,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.
*/
export async function confirmPay(
@@ -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<void> = new AsyncOpMemoMap();
memoMakePlanchet: AsyncOpMemoMap<void> = 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
@@ -1060,6 +1060,16 @@ export interface PurchaseRefundState {
}
/**
+ * 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 */
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[];
}
/**