aboutsummaryrefslogtreecommitdiff
path: root/src/wallet-impl/pay.ts
diff options
context:
space:
mode:
Diffstat (limited to 'src/wallet-impl/pay.ts')
-rw-r--r--src/wallet-impl/pay.ts309
1 files changed, 170 insertions, 139 deletions
diff --git a/src/wallet-impl/pay.ts b/src/wallet-impl/pay.ts
index 31a1500ec..69144d2d6 100644
--- a/src/wallet-impl/pay.ts
+++ b/src/wallet-impl/pay.ts
@@ -55,6 +55,7 @@ import {
strcmp,
extractTalerStamp,
canonicalJson,
+ extractTalerStampOrThrow,
} from "../util/helpers";
import { Logger } from "../util/logging";
import { InternalWalletState } from "./state";
@@ -320,31 +321,41 @@ async function recordConfirmPay(
payCoinInfo: PayCoinInfo,
chosenExchange: string,
): Promise<PurchaseRecord> {
+ const d = proposal.download;
+ if (!d) {
+ throw Error("proposal is in invalid state");
+ }
const payReq: PayReq = {
coins: payCoinInfo.sigs,
- merchant_pub: proposal.contractTerms.merchant_pub,
+ merchant_pub: d.contractTerms.merchant_pub,
mode: "pay",
- order_id: proposal.contractTerms.order_id,
+ order_id: d.contractTerms.order_id,
};
const t: PurchaseRecord = {
abortDone: false,
abortRequested: false,
- contractTerms: proposal.contractTerms,
- contractTermsHash: proposal.contractTermsHash,
+ contractTerms: d.contractTerms,
+ contractTermsHash: d.contractTermsHash,
finished: false,
lastSessionId: undefined,
- merchantSig: proposal.merchantSig,
+ merchantSig: d.merchantSig,
payReq,
refundsDone: {},
refundsPending: {},
timestamp: getTimestampNow(),
timestamp_refund: undefined,
+ proposalId: proposal.proposalId,
};
await runWithWriteTransaction(
ws.db,
- [Stores.coins, Stores.purchases],
+ [Stores.coins, Stores.purchases, Stores.proposals],
async tx => {
+ const p = await tx.get(Stores.proposals, proposal.proposalId);
+ if (p) {
+ p.proposalStatus = ProposalStatus.ACCEPTED;
+ await tx.put(Stores.proposals, p);
+ }
await tx.put(Stores.purchases, t);
for (let c of payCoinInfo.updatedCoins) {
await tx.put(Stores.coins, c);
@@ -360,7 +371,7 @@ async function recordConfirmPay(
function getNextUrl(contractTerms: ContractTerms): string {
const f = contractTerms.fulfillment_url;
if (f.startsWith("http://") || f.startsWith("https://")) {
- const fu = new URL(contractTerms.fulfillment_url)
+ const fu = new URL(contractTerms.fulfillment_url);
fu.searchParams.set("order_id", contractTerms.order_id);
return fu.href;
} else {
@@ -370,9 +381,9 @@ function getNextUrl(contractTerms: ContractTerms): string {
export async function abortFailedPayment(
ws: InternalWalletState,
- contractTermsHash: string,
+ proposalId: string,
): Promise<void> {
- const purchase = await oneShotGet(ws.db, Stores.purchases, contractTermsHash);
+ const purchase = await oneShotGet(ws.db, Stores.purchases, proposalId);
if (!purchase) {
throw Error("Purchase not found, unable to abort with refund");
}
@@ -409,7 +420,7 @@ export async function abortFailedPayment(
await acceptRefundResponse(ws, refundResponse);
await runWithWriteTransaction(ws.db, [Stores.purchases], async tx => {
- const p = await tx.get(Stores.purchases, purchase.contractTermsHash);
+ const p = await tx.get(Stores.purchases, proposalId);
if (!p) {
return;
}
@@ -418,6 +429,76 @@ export async function abortFailedPayment(
});
}
+export async function processDownloadProposal(
+ ws: InternalWalletState,
+ proposalId: string,
+): Promise<void> {
+ const proposal = await oneShotGet(ws.db, Stores.proposals, proposalId);
+ if (!proposal) {
+ return;
+ }
+ if (proposal.proposalStatus != ProposalStatus.DOWNLOADING) {
+ return;
+ }
+ const parsed_url = new URL(proposal.url);
+ parsed_url.searchParams.set("nonce", proposal.noncePub);
+ const urlWithNonce = parsed_url.href;
+ console.log("downloading contract from '" + urlWithNonce + "'");
+ let resp;
+ try {
+ resp = await ws.http.get(urlWithNonce);
+ } catch (e) {
+ console.log("contract download failed", e);
+ throw e;
+ }
+
+ const proposalResp = Proposal.checked(resp.responseJson);
+
+ const contractTermsHash = await ws.cryptoApi.hashString(
+ canonicalJson(proposalResp.contract_terms),
+ );
+
+ const fulfillmentUrl = proposalResp.contract_terms.fulfillment_url;
+
+ await runWithWriteTransaction(
+ ws.db,
+ [Stores.proposals, Stores.purchases],
+ async tx => {
+ const p = await tx.get(Stores.proposals, proposalId);
+ if (!p) {
+ return;
+ }
+ if (p.proposalStatus !== ProposalStatus.DOWNLOADING) {
+ return;
+ }
+ if (
+ fulfillmentUrl.startsWith("http://") ||
+ fulfillmentUrl.startsWith("https://")
+ ) {
+ const differentPurchase = await tx.getIndexed(
+ Stores.purchases.fulfillmentUrlIndex,
+ fulfillmentUrl,
+ );
+ if (differentPurchase) {
+ p.proposalStatus = ProposalStatus.REPURCHASE;
+ p.repurchaseProposalId = differentPurchase.proposalId;
+ await tx.put(Stores.proposals, p);
+ return;
+ }
+ }
+ p.download = {
+ contractTerms: proposalResp.contract_terms,
+ merchantSig: proposalResp.sig,
+ contractTermsHash,
+ };
+ p.proposalStatus = ProposalStatus.PROPOSED;
+ await tx.put(Stores.proposals, p);
+ },
+ );
+
+ ws.notifier.notify();
+}
+
/**
* Download a proposal and store it in the database.
* Returns an id for it to retrieve it later.
@@ -425,7 +506,7 @@ export async function abortFailedPayment(
* @param sessionId Current session ID, if the proposal is being
* downloaded in the context of a session ID.
*/
-async function downloadProposal(
+async function startDownloadProposal(
ws: InternalWalletState,
url: string,
sessionId?: string,
@@ -436,55 +517,38 @@ async function downloadProposal(
url,
);
if (oldProposal) {
+ await processDownloadProposal(ws, oldProposal.proposalId);
return oldProposal.proposalId;
}
const { priv, pub } = await ws.cryptoApi.createEddsaKeypair();
- const parsed_url = new URL(url);
- parsed_url.searchParams.set("nonce", pub);
- const urlWithNonce = parsed_url.href;
- console.log("downloading contract from '" + urlWithNonce + "'");
- let resp;
- try {
- resp = await ws.http.get(urlWithNonce);
- } catch (e) {
- console.log("contract download failed", e);
- throw e;
- }
-
- const proposal = Proposal.checked(resp.responseJson);
-
- const contractTermsHash = await ws.cryptoApi.hashString(
- canonicalJson(proposal.contract_terms),
- );
-
const proposalId = encodeCrock(getRandomBytes(32));
const proposalRecord: ProposalRecord = {
- contractTerms: proposal.contract_terms,
- contractTermsHash,
- merchantSig: proposal.sig,
+ download: undefined,
noncePriv: priv,
+ noncePub: pub,
timestamp: getTimestampNow(),
url,
downloadSessionId: sessionId,
proposalId: proposalId,
- proposalStatus: ProposalStatus.PROPOSED,
+ proposalStatus: ProposalStatus.DOWNLOADING,
+ repurchaseProposalId: undefined,
};
- await oneShotPut(ws.db, Stores.proposals, proposalRecord);
- ws.notifier.notify();
+ await oneShotPut(ws.db, Stores.proposals, proposalRecord);
+ await processDownloadProposal(ws, proposalId);
return proposalId;
}
-async function submitPay(
+export async function submitPay(
ws: InternalWalletState,
- contractTermsHash: string,
+ proposalId: string,
sessionId: string | undefined,
): Promise<ConfirmPayResult> {
- const purchase = await oneShotGet(ws.db, Stores.purchases, contractTermsHash);
+ const purchase = await oneShotGet(ws.db, Stores.purchases, proposalId);
if (!purchase) {
- throw Error("Purchase not found: " + contractTermsHash);
+ throw Error("Purchase not found: " + proposalId);
}
if (purchase.abortRequested) {
throw Error("not submitting payment for aborted purchase");
@@ -507,7 +571,7 @@ async function submitPay(
const merchantPub = purchase.contractTerms.merchant_pub;
const valid: boolean = await ws.cryptoApi.isValidPaymentSignature(
merchantResp.sig,
- contractTermsHash,
+ purchase.contractTermsHash,
merchantPub,
);
if (!valid) {
@@ -532,14 +596,16 @@ async function submitPay(
[Stores.coins, Stores.purchases],
async tx => {
for (let c of modifiedCoins) {
- tx.put(Stores.coins, c);
+ await tx.put(Stores.coins, c);
}
- tx.put(Stores.purchases, purchase);
+ await tx.put(Stores.purchases, purchase);
},
);
for (const c of purchase.payReq.coins) {
- refresh(ws, c.coin_pub);
+ refresh(ws, c.coin_pub).catch(e => {
+ console.log("error in refreshing after payment:", e);
+ });
}
const nextUrl = getNextUrl(purchase.contractTerms);
@@ -570,100 +636,67 @@ export async function preparePay(
};
}
- let proposalId: string;
- try {
- proposalId = await downloadProposal(
- ws,
- uriResult.downloadUrl,
- uriResult.sessionId,
- );
- } catch (e) {
- return {
- status: "error",
- error: e.toString(),
- };
- }
- const proposal = await oneShotGet(ws.db, Stores.proposals, proposalId);
- if (!proposal) {
- throw Error(`could not get proposal ${proposalId}`);
- }
-
- console.log("proposal", proposal);
-
- const differentPurchase = await oneShotGetIndexed(
- ws.db,
- Stores.purchases.fulfillmentUrlIndex,
- proposal.contractTerms.fulfillment_url,
+ const proposalId = await startDownloadProposal(
+ ws,
+ uriResult.downloadUrl,
+ uriResult.sessionId,
);
- let fulfillmentUrl = proposal.contractTerms.fulfillment_url;
- let doublePurchaseDetection = false;
- if (fulfillmentUrl.startsWith("http")) {
- doublePurchaseDetection = true;
+ let proposal = await oneShotGet(ws.db, Stores.proposals, proposalId);
+ if (!proposal) {
+ throw Error(`could not get proposal ${proposalId}`);
}
-
- if (differentPurchase && doublePurchaseDetection) {
- // We do this check to prevent merchant B to find out if we bought a
- // digital product with merchant A by abusing the existing payment
- // redirect feature.
- if (
- differentPurchase.contractTerms.merchant_pub !=
- proposal.contractTerms.merchant_pub
- ) {
- console.warn(
- "merchant with different public key offered contract with same fulfillment URL as an existing purchase",
- );
- } else {
- if (uriResult.sessionId) {
- await submitPay(
- ws,
- differentPurchase.contractTermsHash,
- uriResult.sessionId,
- );
- }
- return {
- status: "paid",
- contractTerms: differentPurchase.contractTerms,
- nextUrl: getNextUrl(differentPurchase.contractTerms),
- };
+ if (proposal.proposalStatus === ProposalStatus.REPURCHASE) {
+ const existingProposalId = proposal.repurchaseProposalId;
+ if (!existingProposalId) {
+ throw Error("invalid proposal state");
}
+ proposal = await oneShotGet(ws.db, Stores.proposals, existingProposalId);
+ if (!proposal) {
+ throw Error("existing proposal is in wrong state");
+ }
+ }
+ const d = proposal.download;
+ if (!d) {
+ console.error("bad proposal", proposal);
+ throw Error("proposal is in invalid state");
+ }
+ const contractTerms = d.contractTerms;
+ const merchantSig = d.merchantSig;
+ if (!contractTerms || !merchantSig) {
+ throw Error("BUG: proposal is in invalid state");
}
+ console.log("proposal", proposal);
+
// First check if we already payed for it.
- const purchase = await oneShotGet(
- ws.db,
- Stores.purchases,
- proposal.contractTermsHash,
- );
+ const purchase = await oneShotGet(ws.db, Stores.purchases, proposalId);
if (!purchase) {
- const paymentAmount = Amounts.parseOrThrow(proposal.contractTerms.amount);
+ const paymentAmount = Amounts.parseOrThrow(contractTerms.amount);
let wireFeeLimit;
- if (proposal.contractTerms.max_wire_fee) {
- wireFeeLimit = Amounts.parseOrThrow(proposal.contractTerms.max_wire_fee);
+ if (contractTerms.max_wire_fee) {
+ wireFeeLimit = Amounts.parseOrThrow(contractTerms.max_wire_fee);
} else {
wireFeeLimit = Amounts.getZero(paymentAmount.currency);
}
// If not already payed, check if we could pay for it.
const res = await getCoinsForPayment(ws, {
- allowedAuditors: proposal.contractTerms.auditors,
- allowedExchanges: proposal.contractTerms.exchanges,
- depositFeeLimit: Amounts.parseOrThrow(proposal.contractTerms.max_fee),
+ allowedAuditors: contractTerms.auditors,
+ allowedExchanges: contractTerms.exchanges,
+ depositFeeLimit: Amounts.parseOrThrow(contractTerms.max_fee),
paymentAmount,
- wireFeeAmortization: proposal.contractTerms.wire_fee_amortization || 1,
+ wireFeeAmortization: contractTerms.wire_fee_amortization || 1,
wireFeeLimit,
- // FIXME: parse this properly
- wireFeeTime: extractTalerStamp(proposal.contractTerms.timestamp) || {
- t_ms: 0,
- },
- wireMethod: proposal.contractTerms.wire_method,
+ wireFeeTime: extractTalerStampOrThrow(contractTerms.timestamp),
+ wireMethod: contractTerms.wire_method,
});
if (!res) {
console.log("not confirming payment, insufficient coins");
return {
status: "insufficient-balance",
- contractTerms: proposal.contractTerms,
+ contractTerms: contractTerms,
proposalId: proposal.proposalId,
};
}
@@ -676,7 +709,7 @@ export async function preparePay(
) {
const { exchangeUrl, cds, totalAmount } = res;
const payCoinInfo = await ws.cryptoApi.signDeposit(
- proposal.contractTerms,
+ contractTerms,
cds,
totalAmount,
);
@@ -691,19 +724,19 @@ export async function preparePay(
return {
status: "payment-possible",
- contractTerms: proposal.contractTerms,
+ contractTerms: contractTerms,
proposalId: proposal.proposalId,
totalFees: res.totalFees,
};
}
if (uriResult.sessionId) {
- await submitPay(ws, purchase.contractTermsHash, uriResult.sessionId);
+ await submitPay(ws, proposalId, uriResult.sessionId);
}
return {
status: "paid",
- contractTerms: proposal.contractTerms,
+ contractTerms: purchase.contractTerms,
nextUrl: getNextUrl(purchase.contractTerms),
};
}
@@ -762,39 +795,37 @@ export async function confirmPay(
throw Error(`proposal with id ${proposalId} not found`);
}
+ const d = proposal.download;
+ if (!d) {
+ throw Error("proposal is in invalid state");
+ }
+
const sessionId = sessionIdOverride || proposal.downloadSessionId;
- let purchase = await oneShotGet(
- ws.db,
- Stores.purchases,
- proposal.contractTermsHash,
- );
+ let purchase = await oneShotGet(ws.db, Stores.purchases, d.contractTermsHash);
if (purchase) {
- return submitPay(ws, purchase.contractTermsHash, sessionId);
+ return submitPay(ws, proposalId, sessionId);
}
- const contractAmount = Amounts.parseOrThrow(proposal.contractTerms.amount);
+ const contractAmount = Amounts.parseOrThrow(d.contractTerms.amount);
let wireFeeLimit;
- if (!proposal.contractTerms.max_wire_fee) {
+ if (!d.contractTerms.max_wire_fee) {
wireFeeLimit = Amounts.getZero(contractAmount.currency);
} else {
- wireFeeLimit = Amounts.parseOrThrow(proposal.contractTerms.max_wire_fee);
+ wireFeeLimit = Amounts.parseOrThrow(d.contractTerms.max_wire_fee);
}
const res = await getCoinsForPayment(ws, {
- allowedAuditors: proposal.contractTerms.auditors,
- allowedExchanges: proposal.contractTerms.exchanges,
- depositFeeLimit: Amounts.parseOrThrow(proposal.contractTerms.max_fee),
- paymentAmount: Amounts.parseOrThrow(proposal.contractTerms.amount),
- wireFeeAmortization: proposal.contractTerms.wire_fee_amortization || 1,
+ allowedAuditors: d.contractTerms.auditors,
+ allowedExchanges: d.contractTerms.exchanges,
+ depositFeeLimit: Amounts.parseOrThrow(d.contractTerms.max_fee),
+ paymentAmount: Amounts.parseOrThrow(d.contractTerms.amount),
+ wireFeeAmortization: d.contractTerms.wire_fee_amortization || 1,
wireFeeLimit,
- // FIXME: parse this properly
- wireFeeTime: extractTalerStamp(proposal.contractTerms.timestamp) || {
- t_ms: 0,
- },
- wireMethod: proposal.contractTerms.wire_method,
+ wireFeeTime: extractTalerStampOrThrow(d.contractTerms.timestamp),
+ wireMethod: d.contractTerms.wire_method,
});
logger.trace("coin selection result", res);
@@ -809,7 +840,7 @@ export async function confirmPay(
if (!sd) {
const { exchangeUrl, cds, totalAmount } = res;
const payCoinInfo = await ws.cryptoApi.signDeposit(
- proposal.contractTerms,
+ d.contractTerms,
cds,
totalAmount,
);
@@ -823,5 +854,5 @@ export async function confirmPay(
);
}
- return submitPay(ws, purchase.contractTermsHash, sessionId);
+ return submitPay(ws, proposalId, sessionId);
}