aboutsummaryrefslogtreecommitdiff
path: root/src/wallet-impl/pay.ts
diff options
context:
space:
mode:
authorFlorian Dold <florian.dold@gmail.com>2019-12-06 00:24:34 +0100
committerFlorian Dold <florian.dold@gmail.com>2019-12-06 00:24:34 +0100
commit65bccbd139c53a2baccec442a680373125488102 (patch)
tree216860ec3523af33091b8fb52193787034c667f8 /src/wallet-impl/pay.ts
parent7b54439fd62bd2a5e15b3068a8fbaffeb0a57468 (diff)
downloadwallet-core-65bccbd139c53a2baccec442a680373125488102.tar.xz
separate operations for pay, refund status query and refund submission
Diffstat (limited to 'src/wallet-impl/pay.ts')
-rw-r--r--src/wallet-impl/pay.ts418
1 files changed, 238 insertions, 180 deletions
diff --git a/src/wallet-impl/pay.ts b/src/wallet-impl/pay.ts
index f07b0328c..cec1b6bc5 100644
--- a/src/wallet-impl/pay.ts
+++ b/src/wallet-impl/pay.ts
@@ -55,7 +55,6 @@ import {
ProposalStatus,
initRetryInfo,
updateRetryInfoTimeout,
- PurchaseStatus,
} from "../dbTypes";
import * as Amounts from "../util/amounts";
import {
@@ -344,18 +343,22 @@ async function recordConfirmPay(
abortRequested: false,
contractTerms: d.contractTerms,
contractTermsHash: d.contractTermsHash,
- finished: false,
+ payFinished: false,
lastSessionId: undefined,
merchantSig: d.merchantSig,
payReq,
refundsDone: {},
refundsPending: {},
acceptTimestamp: getTimestampNow(),
- lastRefundTimestamp: undefined,
+ lastRefundStatusTimestamp: undefined,
proposalId: proposal.proposalId,
- retryInfo: initRetryInfo(),
- lastError: undefined,
- status: PurchaseStatus.SubmitPay,
+ lastPayError: undefined,
+ lastRefundStatusError: undefined,
+ payRetryInfo: initRetryInfo(),
+ refundStatusRetryInfo: initRetryInfo(),
+ refundStatusRequested: false,
+ lastRefundApplyError: undefined,
+ refundApplyRetryInfo: initRetryInfo(),
};
await runWithWriteTransaction(
@@ -402,7 +405,7 @@ export async function abortFailedPayment(
if (!purchase) {
throw Error("Purchase not found, unable to abort with refund");
}
- if (purchase.finished) {
+ if (purchase.payFinished) {
throw Error("Purchase already finished, not aborting");
}
if (purchase.abortDone) {
@@ -464,23 +467,65 @@ async function incrementProposalRetry(
});
}
-async function incrementPurchaseRetry(
+async function incrementPurchasePayRetry(
ws: InternalWalletState,
proposalId: string,
err: OperationError | undefined,
): Promise<void> {
- console.log("incrementing purchase retry with error", err);
+ console.log("incrementing purchase pay retry with error", err);
await runWithWriteTransaction(ws.db, [Stores.purchases], async tx => {
const pr = await tx.get(Stores.purchases, proposalId);
if (!pr) {
return;
}
- if (!pr.retryInfo) {
+ if (!pr.payRetryInfo) {
return;
}
- pr.retryInfo.retryCounter++;
- updateRetryInfoTimeout(pr.retryInfo);
- pr.lastError = err;
+ pr.payRetryInfo.retryCounter++;
+ updateRetryInfoTimeout(pr.payRetryInfo);
+ pr.lastPayError = err;
+ await tx.put(Stores.purchases, pr);
+ });
+}
+
+async function incrementPurchaseQueryRefundRetry(
+ ws: InternalWalletState,
+ proposalId: string,
+ err: OperationError | undefined,
+): Promise<void> {
+ console.log("incrementing purchase refund query retry with error", err);
+ await runWithWriteTransaction(ws.db, [Stores.purchases], async tx => {
+ const pr = await tx.get(Stores.purchases, proposalId);
+ if (!pr) {
+ return;
+ }
+ if (!pr.refundStatusRetryInfo) {
+ return;
+ }
+ pr.refundStatusRetryInfo.retryCounter++;
+ updateRetryInfoTimeout(pr.refundStatusRetryInfo);
+ pr.lastRefundStatusError = err;
+ await tx.put(Stores.purchases, pr);
+ });
+}
+
+async function incrementPurchaseApplyRefundRetry(
+ ws: InternalWalletState,
+ proposalId: string,
+ err: OperationError | undefined,
+): Promise<void> {
+ console.log("incrementing purchase refund apply retry with error", err);
+ await runWithWriteTransaction(ws.db, [Stores.purchases], async tx => {
+ const pr = await tx.get(Stores.purchases, proposalId);
+ if (!pr) {
+ return;
+ }
+ if (!pr.refundApplyRetryInfo) {
+ return;
+ }
+ pr.refundApplyRetryInfo.retryCounter++;
+ updateRetryInfoTimeout(pr.refundStatusRetryInfo);
+ pr.lastRefundApplyError = err;
await tx.put(Stores.purchases, pr);
});
}
@@ -652,10 +697,9 @@ export async function submitPay(
// FIXME: properly display error
throw Error("merchant payment signature invalid");
}
- purchase.finished = true;
- purchase.status = PurchaseStatus.Dormant;
- purchase.lastError = undefined;
- purchase.retryInfo = initRetryInfo(false);
+ purchase.payFinished = true;
+ purchase.lastPayError = undefined;
+ purchase.payRetryInfo = initRetryInfo(false);
const modifiedCoins: CoinRecord[] = [];
for (const pc of purchase.payReq.coins) {
const c = await oneShotGet(ws.db, Stores.coins, pc.coin_pub);
@@ -986,90 +1030,6 @@ export async function getFullRefundFees(
return feeAcc;
}
-async function submitRefundsToExchange(
- ws: InternalWalletState,
- proposalId: string,
-): Promise<void> {
- const purchase = await oneShotGet(ws.db, Stores.purchases, proposalId);
- if (!purchase) {
- console.error("not submitting refunds, payment not found:");
- return;
- }
- const pendingKeys = Object.keys(purchase.refundsPending);
- if (pendingKeys.length === 0) {
- console.log("no pending refunds");
- return;
- }
- for (const pk of pendingKeys) {
- const perm = purchase.refundsPending[pk];
- const req: RefundRequest = {
- coin_pub: perm.coin_pub,
- h_contract_terms: purchase.contractTermsHash,
- merchant_pub: purchase.contractTerms.merchant_pub,
- merchant_sig: perm.merchant_sig,
- refund_amount: perm.refund_amount,
- refund_fee: perm.refund_fee,
- rtransaction_id: perm.rtransaction_id,
- };
- console.log("sending refund permission", perm);
- // FIXME: not correct once we support multiple exchanges per payment
- const exchangeUrl = purchase.payReq.coins[0].exchange_url;
- const reqUrl = new URL("refund", exchangeUrl);
- const resp = await ws.http.postJson(reqUrl.href, req);
- console.log("sent refund permission");
- if (resp.status !== 200) {
- console.error("refund failed", resp);
- continue;
- }
-
- let allRefundsProcessed = false;
-
- await runWithWriteTransaction(
- ws.db,
- [Stores.purchases, Stores.coins],
- async tx => {
- const p = await tx.get(Stores.purchases, proposalId);
- if (!p) {
- return;
- }
- if (p.refundsPending[pk]) {
- p.refundsDone[pk] = p.refundsPending[pk];
- delete p.refundsPending[pk];
- }
- if (Object.keys(p.refundsPending).length === 0) {
- p.retryInfo = initRetryInfo();
- p.lastError = undefined;
- p.status = PurchaseStatus.Dormant;
- allRefundsProcessed = true;
- }
- await tx.put(Stores.purchases, p);
- const c = await tx.get(Stores.coins, perm.coin_pub);
- if (!c) {
- console.warn("coin not found, can't apply refund");
- return;
- }
- const refundAmount = Amounts.parseOrThrow(perm.refund_amount);
- const refundFee = Amounts.parseOrThrow(perm.refund_fee);
- c.status = CoinStatus.Dirty;
- c.currentAmount = Amounts.add(c.currentAmount, refundAmount).amount;
- c.currentAmount = Amounts.sub(c.currentAmount, refundFee).amount;
- await tx.put(Stores.coins, c);
- },
- );
- if (allRefundsProcessed) {
- ws.notify({
- type: NotificationType.RefundFinished,
- })
- }
- await refresh(ws, perm.coin_pub);
- }
-
- ws.notify({
- type: NotificationType.RefundsSubmitted,
- proposalId,
- });
-}
-
async function acceptRefundResponse(
ws: InternalWalletState,
proposalId: string,
@@ -1082,60 +1042,45 @@ async function acceptRefundResponse(
throw Error("empty refund");
}
- /**
- * Add refund to purchase if not already added.
- */
- function f(t: PurchaseRecord | undefined): PurchaseRecord | undefined {
- if (!t) {
+
+ let numNewRefunds = 0;
+
+ await runWithWriteTransaction(ws.db, [Stores.purchases], async (tx) => {
+ const p = await tx.get(Stores.purchases, proposalId);
+ if (!p) {
console.error("purchase not found, not adding refunds");
return;
}
- t.lastRefundTimestamp = getTimestampNow();
- t.status = PurchaseStatus.ProcessRefund;
- t.lastError = undefined;
- t.retryInfo = initRetryInfo();
+ if (!p.refundStatusRequested) {
+ return;
+ }
+
+ p.lastRefundStatusTimestamp = getTimestampNow();
+ p.lastRefundStatusError = undefined;
+ p.refundStatusRetryInfo = initRetryInfo();
+ p.refundStatusRequested = false;
for (const perm of refundPermissions) {
if (
- !t.refundsPending[perm.merchant_sig] &&
- !t.refundsDone[perm.merchant_sig]
+ !p.refundsPending[perm.merchant_sig] &&
+ !p.refundsDone[perm.merchant_sig]
) {
- t.refundsPending[perm.merchant_sig] = perm;
+ p.refundsPending[perm.merchant_sig] = perm;
+ numNewRefunds++;
}
}
- return t;
- }
- // Add the refund permissions to the purchase within a DB transaction
- await oneShotMutate(ws.db, Stores.purchases, proposalId, f);
- await submitRefundsToExchange(ws, proposalId);
-}
-async function queryRefund(
- ws: InternalWalletState,
- proposalId: string,
-): Promise<void> {
- const purchase = await oneShotGet(ws.db, Stores.purchases, proposalId);
- if (purchase?.status !== PurchaseStatus.QueryRefund) {
- return;
- }
+ if (numNewRefunds) {
+ p.lastRefundApplyError = undefined;
+ p.refundApplyRetryInfo = initRetryInfo();
+ }
- const refundUrlObj = new URL(
- "refund",
- purchase.contractTerms.merchant_base_url,
- );
- refundUrlObj.searchParams.set("order_id", purchase.contractTerms.order_id);
- const refundUrl = refundUrlObj.href;
- let resp;
- try {
- resp = await ws.http.get(refundUrl);
- } catch (e) {
- console.error("error downloading refund permission", e);
- throw e;
+ await tx.put(Stores.purchases, p);
+ });
+ if (numNewRefunds > 0) {
+ await processPurchaseApplyRefund(ws, proposalId);
}
-
- const refundResponse = MerchantRefundResponse.checked(resp.responseJson);
- await acceptRefundResponse(ws, proposalId, refundResponse);
}
async function startRefundQuery(
@@ -1151,21 +1096,12 @@ async function startRefundQuery(
console.log("no purchase found for refund URL");
return false;
}
- if (p.status === PurchaseStatus.QueryRefund) {
- return true;
- }
- if (p.status === PurchaseStatus.ProcessRefund) {
- return true;
- }
- if (p.status !== PurchaseStatus.Dormant) {
- console.log(
- `can't apply refund, as payment isn't done (status ${p.status})`,
- );
- return false;
+ if (p.refundStatusRequested) {
+
}
- p.lastError = undefined;
- p.status = PurchaseStatus.QueryRefund;
- p.retryInfo = initRetryInfo();
+ p.refundStatusRequested = true;
+ p.lastRefundStatusError = undefined;
+ p.refundStatusRetryInfo = initRetryInfo();
await tx.put(Stores.purchases, p);
return true;
},
@@ -1175,7 +1111,7 @@ async function startRefundQuery(
return;
}
- await processPurchase(ws, proposalId);
+ await processPurchaseQueryRefund(ws, proposalId);
}
/**
@@ -1210,19 +1146,19 @@ export async function applyRefund(
return purchase.contractTermsHash;
}
-export async function processPurchase(
+export async function processPurchasePay(
ws: InternalWalletState,
proposalId: string,
): Promise<void> {
const onOpErr = (e: OperationError) =>
- incrementPurchaseRetry(ws, proposalId, e);
+ incrementPurchasePayRetry(ws, proposalId, e);
await guardOperationException(
- () => processPurchaseImpl(ws, proposalId),
+ () => processPurchasePayImpl(ws, proposalId),
onOpErr,
);
}
-async function processPurchaseImpl(
+async function processPurchasePayImpl(
ws: InternalWalletState,
proposalId: string,
): Promise<void> {
@@ -1230,24 +1166,146 @@ async function processPurchaseImpl(
if (!purchase) {
return;
}
- logger.trace(`processing purchase ${proposalId}`);
- switch (purchase.status) {
- case PurchaseStatus.Dormant:
- return;
- case PurchaseStatus.Abort:
- // FIXME
- break;
- case PurchaseStatus.SubmitPay:
- break;
- case PurchaseStatus.QueryRefund:
- await queryRefund(ws, proposalId);
- break;
- case PurchaseStatus.ProcessRefund:
- console.log("submitting refunds to exchange (toplvl)");
- await submitRefundsToExchange(ws, proposalId);
- console.log("after submitting refunds to exchange (toplvl)");
- break;
- default:
- throw assertUnreachable(purchase.status);
+ logger.trace(`processing purchase pay ${proposalId}`);
+ if (purchase.payFinished) {
+ return;
+ }
+ await submitPay(ws, proposalId, purchase.lastSessionId);
+}
+
+export async function processPurchaseQueryRefund(
+ ws: InternalWalletState,
+ proposalId: string,
+): Promise<void> {
+ const onOpErr = (e: OperationError) =>
+ incrementPurchaseQueryRefundRetry(ws, proposalId, e);
+ await guardOperationException(
+ () => processPurchaseQueryRefundImpl(ws, proposalId),
+ onOpErr,
+ );
+}
+
+async function processPurchaseQueryRefundImpl(
+ ws: InternalWalletState,
+ proposalId: string,
+): Promise<void> {
+ const purchase = await oneShotGet(ws.db, Stores.purchases, proposalId);
+ if (!purchase) {
+ return;
+ }
+ if (!purchase.refundStatusRequested) {
+ return;
+ }
+
+ const refundUrlObj = new URL(
+ "refund",
+ purchase.contractTerms.merchant_base_url,
+ );
+ refundUrlObj.searchParams.set("order_id", purchase.contractTerms.order_id);
+ const refundUrl = refundUrlObj.href;
+ let resp;
+ try {
+ resp = await ws.http.get(refundUrl);
+ } catch (e) {
+ console.error("error downloading refund permission", e);
+ throw e;
+ }
+
+ const refundResponse = MerchantRefundResponse.checked(resp.responseJson);
+ await acceptRefundResponse(ws, proposalId, refundResponse);
+}
+
+export async function processPurchaseApplyRefund(
+ ws: InternalWalletState,
+ proposalId: string,
+): Promise<void> {
+ const onOpErr = (e: OperationError) =>
+ incrementPurchaseApplyRefundRetry(ws, proposalId, e);
+ await guardOperationException(
+ () => processPurchaseApplyRefundImpl(ws, proposalId),
+ onOpErr,
+ );
+}
+
+async function processPurchaseApplyRefundImpl(
+ ws: InternalWalletState,
+ proposalId: string,
+): Promise<void> {
+ const purchase = await oneShotGet(ws.db, Stores.purchases, proposalId);
+ if (!purchase) {
+ console.error("not submitting refunds, payment not found:");
+ return;
+ }
+ const pendingKeys = Object.keys(purchase.refundsPending);
+ if (pendingKeys.length === 0) {
+ console.log("no pending refunds");
+ return;
+ }
+ for (const pk of pendingKeys) {
+ const perm = purchase.refundsPending[pk];
+ const req: RefundRequest = {
+ coin_pub: perm.coin_pub,
+ h_contract_terms: purchase.contractTermsHash,
+ merchant_pub: purchase.contractTerms.merchant_pub,
+ merchant_sig: perm.merchant_sig,
+ refund_amount: perm.refund_amount,
+ refund_fee: perm.refund_fee,
+ rtransaction_id: perm.rtransaction_id,
+ };
+ console.log("sending refund permission", perm);
+ // FIXME: not correct once we support multiple exchanges per payment
+ const exchangeUrl = purchase.payReq.coins[0].exchange_url;
+ const reqUrl = new URL("refund", exchangeUrl);
+ const resp = await ws.http.postJson(reqUrl.href, req);
+ console.log("sent refund permission");
+ if (resp.status !== 200) {
+ console.error("refund failed", resp);
+ continue;
+ }
+
+ let allRefundsProcessed = false;
+
+ await runWithWriteTransaction(
+ ws.db,
+ [Stores.purchases, Stores.coins],
+ async tx => {
+ const p = await tx.get(Stores.purchases, proposalId);
+ if (!p) {
+ return;
+ }
+ if (p.refundsPending[pk]) {
+ p.refundsDone[pk] = p.refundsPending[pk];
+ delete p.refundsPending[pk];
+ }
+ if (Object.keys(p.refundsPending).length === 0) {
+ p.refundStatusRetryInfo = initRetryInfo();
+ p.lastRefundStatusError = undefined;
+ allRefundsProcessed = true;
+ }
+ await tx.put(Stores.purchases, p);
+ const c = await tx.get(Stores.coins, perm.coin_pub);
+ if (!c) {
+ console.warn("coin not found, can't apply refund");
+ return;
+ }
+ const refundAmount = Amounts.parseOrThrow(perm.refund_amount);
+ const refundFee = Amounts.parseOrThrow(perm.refund_fee);
+ c.status = CoinStatus.Dirty;
+ c.currentAmount = Amounts.add(c.currentAmount, refundAmount).amount;
+ c.currentAmount = Amounts.sub(c.currentAmount, refundFee).amount;
+ await tx.put(Stores.coins, c);
+ },
+ );
+ if (allRefundsProcessed) {
+ ws.notify({
+ type: NotificationType.RefundFinished,
+ });
+ }
+ await refresh(ws, perm.coin_pub);
}
+
+ ws.notify({
+ type: NotificationType.RefundsSubmitted,
+ proposalId,
+ });
}