diff options
author | Florian Dold <florian.dold@gmail.com> | 2019-12-06 00:24:34 +0100 |
---|---|---|
committer | Florian Dold <florian.dold@gmail.com> | 2019-12-06 00:24:34 +0100 |
commit | 65bccbd139c53a2baccec442a680373125488102 (patch) | |
tree | 216860ec3523af33091b8fb52193787034c667f8 /src/wallet-impl/pay.ts | |
parent | 7b54439fd62bd2a5e15b3068a8fbaffeb0a57468 (diff) | |
download | wallet-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.ts | 418 |
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, + }); } |