simplify /pay, add pay event
This commit is contained in:
parent
59bd755f7d
commit
1b9c5855a8
@ -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,
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
@ -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();
|
||||
|
@ -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 */
|
||||
|
@ -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[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
Loading…
Reference in New Issue
Block a user