diff options
author | Florian Dold <florian.dold@gmail.com> | 2019-12-03 14:40:05 +0100 |
---|---|---|
committer | Florian Dold <florian.dold@gmail.com> | 2019-12-03 14:40:05 +0100 |
commit | 829acdd3d98f1014747f15ecb619b6fbaa06b640 (patch) | |
tree | 2b95c7ee2df1d3cc8277d0b684300f79d05c2264 /src/wallet-impl/pay.ts | |
parent | 8683c93613caa4047c4fd874aefb0b7d35fdc038 (diff) | |
download | wallet-core-829acdd3d98f1014747f15ecb619b6fbaa06b640.tar.xz |
android
Diffstat (limited to 'src/wallet-impl/pay.ts')
-rw-r--r-- | src/wallet-impl/pay.ts | 215 |
1 files changed, 213 insertions, 2 deletions
diff --git a/src/wallet-impl/pay.ts b/src/wallet-impl/pay.ts index 69144d2d6..9942139a6 100644 --- a/src/wallet-impl/pay.ts +++ b/src/wallet-impl/pay.ts @@ -22,6 +22,8 @@ import { PayReq, Proposal, ContractTerms, + MerchantRefundPermission, + RefundRequest, } from "../talerTypes"; import { Timestamp, @@ -39,6 +41,7 @@ import { runWithWriteTransaction, oneShotPut, oneShotGetIndexed, + oneShotMutate, } from "../util/query"; import { Stores, @@ -59,9 +62,8 @@ import { } from "../util/helpers"; import { Logger } from "../util/logging"; import { InternalWalletState } from "./state"; -import { parsePayUri } from "../util/taleruri"; +import { parsePayUri, parseRefundUri } from "../util/taleruri"; import { getTotalRefreshCost, refresh } from "./refresh"; -import { acceptRefundResponse } from "./refund"; import { encodeCrock, getRandomBytes } from "../crypto/talerCrypto"; export interface SpeculativePayData { @@ -856,3 +858,212 @@ export async function confirmPay( return submitPay(ws, proposalId, sessionId); } + + + +export async function getFullRefundFees( + ws: InternalWalletState, + refundPermissions: MerchantRefundPermission[], +): Promise<AmountJson> { + if (refundPermissions.length === 0) { + throw Error("no refunds given"); + } + const coin0 = await oneShotGet( + ws.db, + Stores.coins, + refundPermissions[0].coin_pub, + ); + if (!coin0) { + throw Error("coin not found"); + } + let feeAcc = Amounts.getZero( + Amounts.parseOrThrow(refundPermissions[0].refund_amount).currency, + ); + + const denoms = await oneShotIterIndex( + ws.db, + Stores.denominations.exchangeBaseUrlIndex, + coin0.exchangeBaseUrl, + ).toArray(); + + for (const rp of refundPermissions) { + const coin = await oneShotGet(ws.db, Stores.coins, rp.coin_pub); + if (!coin) { + throw Error("coin not found"); + } + const denom = await oneShotGet(ws.db, Stores.denominations, [ + coin0.exchangeBaseUrl, + coin.denomPub, + ]); + if (!denom) { + throw Error(`denom not found (${coin.denomPub})`); + } + // FIXME: this assumes that the refund already happened. + // When it hasn't, the refresh cost is inaccurate. To fix this, + // we need introduce a flag to tell if a coin was refunded or + // refreshed normally (and what about incremental refunds?) + const refundAmount = Amounts.parseOrThrow(rp.refund_amount); + const refundFee = Amounts.parseOrThrow(rp.refund_fee); + const refreshCost = getTotalRefreshCost( + denoms, + denom, + Amounts.sub(refundAmount, refundFee).amount, + ); + feeAcc = Amounts.add(feeAcc, refreshCost, refundFee).amount; + } + return feeAcc; +} + +async function submitRefunds( + 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) { + 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); + if (resp.status !== 200) { + console.error("refund failed", resp); + continue; + } + + // Transactionally mark successful refunds as done + const transformPurchase = ( + t: PurchaseRecord | undefined, + ): PurchaseRecord | undefined => { + if (!t) { + console.warn("purchase not found, not updating refund"); + return; + } + if (t.refundsPending[pk]) { + t.refundsDone[pk] = t.refundsPending[pk]; + delete t.refundsPending[pk]; + } + return t; + }; + const transformCoin = ( + c: CoinRecord | undefined, + ): CoinRecord | undefined => { + 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; + + return c; + }; + + await runWithWriteTransaction( + ws.db, + [Stores.purchases, Stores.coins], + async tx => { + await tx.mutate(Stores.purchases, proposalId, transformPurchase); + await tx.mutate(Stores.coins, perm.coin_pub, transformCoin); + }, + ); + refresh(ws, perm.coin_pub); + } + + ws.badge.showNotification(); + ws.notifier.notify(); +} + +export async function acceptRefundResponse( + ws: InternalWalletState, + refundResponse: MerchantRefundResponse, +): Promise<string> { + const refundPermissions = refundResponse.refund_permissions; + + if (!refundPermissions.length) { + console.warn("got empty refund list"); + throw Error("empty refund"); + } + + /** + * Add refund to purchase if not already added. + */ + function f(t: PurchaseRecord | undefined): PurchaseRecord | undefined { + if (!t) { + console.error("purchase not found, not adding refunds"); + return; + } + + t.timestamp_refund = getTimestampNow(); + + for (const perm of refundPermissions) { + if ( + !t.refundsPending[perm.merchant_sig] && + !t.refundsDone[perm.merchant_sig] + ) { + t.refundsPending[perm.merchant_sig] = perm; + } + } + return t; + } + + const hc = refundResponse.h_contract_terms; + + // Add the refund permissions to the purchase within a DB transaction + await oneShotMutate(ws.db, Stores.purchases, hc, f); + ws.notifier.notify(); + + await submitRefunds(ws, hc); + + return hc; +} + +/** + * Accept a refund, return the contract hash for the contract + * that was involved in the refund. + */ +export async function applyRefund( + ws: InternalWalletState, + talerRefundUri: string, +): Promise<string> { + const parseResult = parseRefundUri(talerRefundUri); + + if (!parseResult) { + throw Error("invalid refund URI"); + } + + const refundUrl = parseResult.refundUrl; + + logger.trace("processing refund"); + 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); + return acceptRefundResponse(ws, refundResponse); +} |