diff options
author | Florian Dold <florian.dold@gmail.com> | 2020-04-27 21:11:20 +0530 |
---|---|---|
committer | Florian Dold <florian.dold@gmail.com> | 2020-04-27 21:11:20 +0530 |
commit | 5be0708a10874be939e63ca82082cd665165823c (patch) | |
tree | d67531e204513905ffe90b7874baead3afe50305 | |
parent | e404f5e6d3001906c11b82a532db518720caebfd (diff) |
adopt new merchant refund API
-rw-r--r-- | src/operations/history.ts | 8 | ||||
-rw-r--r-- | src/operations/pay.ts | 76 | ||||
-rw-r--r-- | src/operations/pending.ts | 20 | ||||
-rw-r--r-- | src/operations/refund.ts | 448 | ||||
-rw-r--r-- | src/types/dbTypes.ts | 58 | ||||
-rw-r--r-- | src/types/pending.ts | 16 | ||||
-rw-r--r-- | src/types/talerTypes.ts | 60 | ||||
-rw-r--r-- | src/wallet.ts | 23 |
8 files changed, 224 insertions, 485 deletions
diff --git a/src/operations/history.ts b/src/operations/history.ts index efbfbf377..f32dbbe2d 100644 --- a/src/operations/history.ts +++ b/src/operations/history.ts @@ -453,8 +453,8 @@ export async function getHistory( let amountRefundedRaw = Amounts.getZero(purchaseAmount.currency); let amountRefundedInvalid = Amounts.getZero(purchaseAmount.currency); let amountRefundedEffective = Amounts.getZero(purchaseAmount.currency); - Object.keys(purchase.refundState.refundsDone).forEach((x, i) => { - const r = purchase.refundState.refundsDone[x]; + Object.keys(purchase.refundsDone).forEach((x, i) => { + const r = purchase.refundsDone[x]; if (r.refundGroupId !== re.refundGroupId) { return; } @@ -471,8 +471,8 @@ export async function getHistory( refundFee, ).amount; }); - Object.keys(purchase.refundState.refundsFailed).forEach((x, i) => { - const r = purchase.refundState.refundsFailed[x]; + Object.keys(purchase.refundsFailed).forEach((x, i) => { + const r = purchase.refundsFailed[x]; if (r.refundGroupId !== re.refundGroupId) { return; } diff --git a/src/operations/pay.ts b/src/operations/pay.ts index 337068b55..a75284393 100644 --- a/src/operations/pay.ts +++ b/src/operations/pay.ts @@ -31,7 +31,6 @@ import { ProposalRecord, ProposalStatus, PurchaseRecord, - RefundReason, Stores, updateRetryInfoTimeout, PayEventRecord, @@ -40,7 +39,6 @@ import { import { NotificationType } from "../types/notifications"; import { PayReq, - codecForMerchantRefundResponse, codecForProposal, codecForContractTerms, CoinDepositPermission, @@ -57,7 +55,6 @@ import { Logger } from "../util/logging"; import { getOrderDownloadUrl, parsePayUri } from "../util/taleruri"; import { guardOperationException } from "./errors"; import { createRefreshGroup, getTotalRefreshCost } from "./refresh"; -import { acceptRefundResponse } from "./refund"; import { InternalWalletState } from "./state"; import { getTimestampNow, timestampAddDuration } from "../util/time"; import { strcmp, canonicalJson } from "../util/helpers"; @@ -446,17 +443,13 @@ async function recordConfirmPay( payRetryInfo: initRetryInfo(), refundStatusRetryInfo: initRetryInfo(), refundStatusRequested: false, - lastRefundApplyError: undefined, - refundApplyRetryInfo: initRetryInfo(), timestampFirstSuccessfulPay: undefined, autoRefundDeadline: undefined, paymentSubmitPending: true, - refundState: { - refundGroups: [], - refundsDone: {}, - refundsFailed: {}, - refundsPending: {}, - }, + refundGroups: [], + refundsDone: {}, + refundsFailed: {}, + refundsPending: {}, }; await ws.db.runWithWriteTransaction( @@ -511,67 +504,6 @@ function getNextUrl(contractData: WalletContractData): string { } } -export async function abortFailedPayment( - ws: InternalWalletState, - proposalId: string, -): Promise<void> { - const purchase = await ws.db.get(Stores.purchases, proposalId); - if (!purchase) { - throw Error("Purchase not found, unable to abort with refund"); - } - if (purchase.timestampFirstSuccessfulPay) { - throw Error("Purchase already finished, not aborting"); - } - if (purchase.abortDone) { - console.warn("abort requested on already aborted purchase"); - return; - } - - purchase.abortRequested = true; - - // From now on, we can't retry payment anymore, - // so mark this in the DB in case the /pay abort - // does not complete on the first try. - await ws.db.put(Stores.purchases, purchase); - - let resp; - - const abortReq = { ...purchase.payReq, mode: "abort-refund" }; - - const payUrl = new URL("pay", purchase.contractData.merchantBaseUrl).href; - - try { - resp = await ws.http.postJson(payUrl, abortReq); - } catch (e) { - // Gives the user the option to retry / abort and refresh - console.log("aborting payment failed", e); - throw e; - } - - if (resp.status !== 200) { - throw Error(`unexpected status for /pay (${resp.status})`); - } - - const refundResponse = codecForMerchantRefundResponse().decode( - await resp.json(), - ); - await acceptRefundResponse( - ws, - purchase.proposalId, - refundResponse, - RefundReason.AbortRefund, - ); - - await ws.db.runWithWriteTransaction([Stores.purchases], async (tx) => { - const p = await tx.get(Stores.purchases, proposalId); - if (!p) { - return; - } - p.abortDone = true; - await tx.put(Stores.purchases, p); - }); -} - async function incrementProposalRetry( ws: InternalWalletState, proposalId: string, diff --git a/src/operations/pending.ts b/src/operations/pending.ts index 3e548a27f..a797763bf 100644 --- a/src/operations/pending.ts +++ b/src/operations/pending.ts @@ -396,26 +396,6 @@ async function gatherPurchasePending( }); } } - const numRefundsPending = Object.keys(pr.refundState.refundsPending).length; - if (numRefundsPending > 0) { - const numRefundsDone = Object.keys(pr.refundState.refundsDone).length; - resp.nextRetryDelay = updateRetryDelay( - resp.nextRetryDelay, - now, - pr.refundApplyRetryInfo.nextRetry, - ); - if (!onlyDue || pr.refundApplyRetryInfo.nextRetry.t_ms <= now.t_ms) { - resp.pendingOperations.push({ - type: PendingOperationType.RefundApply, - numRefundsDone, - numRefundsPending, - givesLifeness: true, - proposalId: pr.proposalId, - retryInfo: pr.refundApplyRetryInfo, - lastError: pr.lastRefundApplyError, - }); - } - } }); } diff --git a/src/operations/refund.ts b/src/operations/refund.ts index 8feb2baea..9b18cafd4 100644 --- a/src/operations/refund.ts +++ b/src/operations/refund.ts @@ -43,16 +43,14 @@ import { parseRefundUri } from "../util/taleruri"; import { createRefreshGroup, getTotalRefreshCost } from "./refresh"; import { Amounts } from "../util/amounts"; import { - MerchantRefundPermission, + MerchantRefundDetails, MerchantRefundResponse, - RefundRequest, codecForMerchantRefundResponse, } from "../types/talerTypes"; import { AmountJson } from "../util/amounts"; import { guardOperationException, OperationFailedError } from "./errors"; import { randomBytes } from "../crypto/primitives/nacl-fast"; import { encodeCrock } from "../crypto/talerCrypto"; -import { HttpResponseStatus } from "../util/http"; import { getTimestampNow } from "../util/time"; import { Logger } from "../util/logging"; @@ -80,31 +78,9 @@ async function incrementPurchaseQueryRefundRetry( ws.notify({ type: NotificationType.RefundStatusOperationError }); } -async function incrementPurchaseApplyRefundRetry( - ws: InternalWalletState, - proposalId: string, - err: OperationError | undefined, -): Promise<void> { - console.log("incrementing purchase refund apply retry with error", err); - await ws.db.runWithWriteTransaction([Stores.purchases], async (tx) => { - const pr = await tx.get(Stores.purchases, proposalId); - if (!pr) { - return; - } - if (!pr.refundApplyRetryInfo) { - return; - } - pr.refundApplyRetryInfo.retryCounter++; - updateRetryInfoTimeout(pr.refundApplyRetryInfo); - pr.lastRefundApplyError = err; - await tx.put(Stores.purchases, pr); - }); - ws.notify({ type: NotificationType.RefundApplyOperationError }); -} - export async function getFullRefundFees( ws: InternalWalletState, - refundPermissions: MerchantRefundPermission[], + refundPermissions: MerchantRefundDetails[], ): Promise<AmountJson> { if (refundPermissions.length === 0) { throw Error("no refunds given"); @@ -149,88 +125,196 @@ export async function getFullRefundFees( return feeAcc; } -export async function acceptRefundResponse( +function getRefundKey(d: MerchantRefundDetails): string { + return `{d.coin_pub}-{d.rtransaction_id}`; +} + +async function acceptRefundResponse( ws: InternalWalletState, proposalId: string, refundResponse: MerchantRefundResponse, reason: RefundReason, ): Promise<void> { - const refundPermissions = refundResponse.refund_permissions; - - let numNewRefunds = 0; + const refunds = refundResponse.refunds; const refundGroupId = encodeCrock(randomBytes(32)); - await ws.db.runWithWriteTransaction([Stores.purchases], async (tx) => { - const p = await tx.get(Stores.purchases, proposalId); - if (!p) { - console.error("purchase not found, not adding refunds"); - return; - } + let numNewRefunds = 0; - if (!p.refundStatusRequested) { - return; + const finishedRefunds: MerchantRefundDetails[] = []; + const unfinishedRefunds: MerchantRefundDetails[] = []; + const failedRefunds: MerchantRefundDetails[] = []; + + for (const rd of refunds) { + if (rd.exchange_http_status === 200) { + // FIXME: also verify signature if necessary. + finishedRefunds.push(rd); + } else if ( + rd.exchange_http_status >= 400 && + rd.exchange_http_status < 400 + ) { + failedRefunds.push(rd); + } else { + unfinishedRefunds.push(rd); } + } - for (const perm of refundPermissions) { - const isDone = p.refundState.refundsDone[perm.merchant_sig]; - const isPending = p.refundState.refundsPending[perm.merchant_sig]; - if (!isDone && !isPending) { - p.refundState.refundsPending[perm.merchant_sig] = { - perm, + await ws.db.runWithWriteTransaction( + [Stores.purchases, Stores.coins, Stores.refreshGroups, Stores.refundEvents], + async (tx) => { + const p = await tx.get(Stores.purchases, proposalId); + if (!p) { + console.error("purchase not found, not adding refunds"); + return; + } + + // Groups that newly failed/succeeded + const changedGroups: { [refundGroupId: string]: boolean } = {}; + + for (const rd of failedRefunds) { + const refundKey = getRefundKey(rd); + if (p.refundsFailed[refundKey]) { + continue; + } + if (!p.refundsFailed[refundKey]) { + p.refundsFailed[refundKey] = { + perm: rd, + refundGroupId, + }; + numNewRefunds++; + changedGroups[refundGroupId] = true; + } + const oldPending = p.refundsPending[refundKey]; + if (oldPending) { + delete p.refundsPending[refundKey]; + changedGroups[oldPending.refundGroupId] = true; + } + } + + for (const rd of unfinishedRefunds) { + const refundKey = getRefundKey(rd); + if (!p.refundsPending[refundKey]) { + p.refundsPending[refundKey] = { + perm: rd, + refundGroupId, + }; + numNewRefunds++; + } + } + + // Avoid duplicates + const refreshCoinsMap: { [coinPub: string]: CoinPublicKey } = {}; + + for (const rd of finishedRefunds) { + const refundKey = getRefundKey(rd); + if (p.refundsDone[refundKey]) { + continue; + } + p.refundsDone[refundKey] = { + perm: rd, refundGroupId, }; - numNewRefunds++; + const oldPending = p.refundsPending[refundKey]; + if (oldPending) { + delete p.refundsPending[refundKey]; + changedGroups[oldPending.refundGroupId] = true; + } else { + numNewRefunds++; + } + + const c = await tx.get(Stores.coins, rd.coin_pub); + + if (!c) { + console.warn("coin not found, can't apply refund"); + return; + } + refreshCoinsMap[c.coinPub] = { coinPub: c.coinPub }; + logger.trace(`commiting refund ${refundKey} to coin ${c.coinPub}`); + logger.trace( + `coin amount before is ${Amounts.stringify(c.currentAmount)}`, + ); + logger.trace(`refund amount (via merchant) is ${refundKey}`); + logger.trace(`refund fee (via merchant) is ${refundKey}`); + const refundAmount = Amounts.parseOrThrow(rd.refund_amount); + const refundFee = Amounts.parseOrThrow(rd.refund_fee); + c.status = CoinStatus.Dormant; + c.currentAmount = Amounts.add(c.currentAmount, refundAmount).amount; + c.currentAmount = Amounts.sub(c.currentAmount, refundFee).amount; + logger.trace( + `coin amount after is ${Amounts.stringify(c.currentAmount)}`, + ); + await tx.put(Stores.coins, c); } - } - // Are we done with querying yet, or do we need to do another round - // after a retry delay? - let queryDone = true; + // Are we done with querying yet, or do we need to do another round + // after a retry delay? + let queryDone = true; - if (numNewRefunds === 0) { - if ( - p.autoRefundDeadline && - p.autoRefundDeadline.t_ms > getTimestampNow().t_ms - ) { + if (numNewRefunds === 0) { + if ( + p.autoRefundDeadline && + p.autoRefundDeadline.t_ms > getTimestampNow().t_ms + ) { + queryDone = false; + } + } + + if (Object.keys(unfinishedRefunds).length != 0) { queryDone = false; } - } - if (queryDone) { - p.timestampLastRefundStatus = getTimestampNow(); - p.lastRefundStatusError = undefined; - p.refundStatusRetryInfo = initRetryInfo(); - p.refundStatusRequested = false; - console.log("refund query done"); - } else { - // No error, but we need to try again! - p.timestampLastRefundStatus = getTimestampNow(); - p.refundStatusRetryInfo.retryCounter++; - updateRetryInfoTimeout(p.refundStatusRetryInfo); - p.lastRefundStatusError = undefined; - console.log("refund query not done"); - } + if (queryDone) { + p.timestampLastRefundStatus = getTimestampNow(); + p.lastRefundStatusError = undefined; + p.refundStatusRetryInfo = initRetryInfo(false); + p.refundStatusRequested = false; + console.log("refund query done"); + } else { + // No error, but we need to try again! + p.timestampLastRefundStatus = getTimestampNow(); + p.refundStatusRetryInfo.retryCounter++; + updateRetryInfoTimeout(p.refundStatusRetryInfo); + p.lastRefundStatusError = undefined; + console.log("refund query not done"); + } - if (numNewRefunds > 0) { - const now = getTimestampNow(); - p.lastRefundApplyError = undefined; - p.refundApplyRetryInfo = initRetryInfo(); - p.refundState.refundGroups.push({ - timestampQueried: now, - reason, - }); - } + await tx.put(Stores.purchases, p); - await tx.put(Stores.purchases, p); - }); + const coinsPubsToBeRefreshed = Object.values(refreshCoinsMap); + if (coinsPubsToBeRefreshed.length > 0) { + await createRefreshGroup( + tx, + coinsPubsToBeRefreshed, + RefreshReason.Refund, + ); + } + + // Check if any of the refund groups are done, and we + // can emit an corresponding event. + const now = getTimestampNow(); + for (const g of Object.keys(changedGroups)) { + let groupDone = true; + for (const pk of Object.keys(p.refundsPending)) { + const r = p.refundsPending[pk]; + if (r.refundGroupId == g) { + groupDone = false; + } + } + if (groupDone) { + const refundEvent: RefundEventRecord = { + proposalId, + refundGroupId: g, + timestamp: now, + }; + await tx.put(Stores.refundEvents, refundEvent); + } + } + }, + ); ws.notify({ type: NotificationType.RefundQueried, }); - if (numNewRefunds > 0) { - await processPurchaseApplyRefund(ws, proposalId); - } } async function startRefundQuery( @@ -362,201 +446,3 @@ async function processPurchaseQueryRefundImpl( RefundReason.NormalRefund, ); } - -export async function processPurchaseApplyRefund( - ws: InternalWalletState, - proposalId: string, - forceNow = false, -): Promise<void> { - const onOpErr = (e: OperationError): Promise<void> => - incrementPurchaseApplyRefundRetry(ws, proposalId, e); - await guardOperationException( - () => processPurchaseApplyRefundImpl(ws, proposalId, forceNow), - onOpErr, - ); -} - -async function resetPurchaseApplyRefundRetry( - ws: InternalWalletState, - proposalId: string, -): Promise<void> { - await ws.db.mutate(Stores.purchases, proposalId, (x) => { - if (x.refundApplyRetryInfo.active) { - x.refundApplyRetryInfo = initRetryInfo(); - } - return x; - }); -} - -async function processPurchaseApplyRefundImpl( - ws: InternalWalletState, - proposalId: string, - forceNow: boolean, -): Promise<void> { - if (forceNow) { - await resetPurchaseApplyRefundRetry(ws, proposalId); - } - const purchase = await ws.db.get(Stores.purchases, proposalId); - if (!purchase) { - console.error("not submitting refunds, payment not found:"); - return; - } - const pendingKeys = Object.keys(purchase.refundState.refundsPending); - if (pendingKeys.length === 0) { - console.log("no pending refunds"); - return; - } - - const newRefundsDone: { [sig: string]: RefundInfo } = {}; - const newRefundsFailed: { [sig: string]: RefundInfo } = {}; - for (const pk of pendingKeys) { - const info = purchase.refundState.refundsPending[pk]; - const perm = info.perm; - const req: RefundRequest = { - coin_pub: perm.coin_pub, - h_contract_terms: purchase.contractData.contractTermsHash, - merchant_pub: purchase.contractData.merchantPub, - 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(`coins/${perm.coin_pub}/refund`, exchangeUrl); - const resp = await ws.http.postJson(reqUrl.href, req); - console.log("sent refund permission"); - switch (resp.status) { - case HttpResponseStatus.Ok: - newRefundsDone[pk] = info; - break; - case HttpResponseStatus.Gone: - // We're too late, refund is expired. - newRefundsFailed[pk] = info; - break; - default: { - let body: string | null = null; - // FIXME: error handling! - body = await resp.json(); - const m = "refund request (at exchange) failed"; - throw new OperationFailedError({ - message: m, - type: "network", - details: { - body, - }, - }); - } - } - } - let allRefundsProcessed = false; - await ws.db.runWithWriteTransaction( - [Stores.purchases, Stores.coins, Stores.refreshGroups, Stores.refundEvents], - async (tx) => { - const p = await tx.get(Stores.purchases, proposalId); - if (!p) { - return; - } - - // Groups that failed/succeeded - const groups: { [refundGroupId: string]: boolean } = {}; - - // Avoid duplicates - const refreshCoinsMap: { [coinPub: string]: CoinPublicKey } = {}; - - const modCoin = async (perm: MerchantRefundPermission): Promise<void> => { - const c = await tx.get(Stores.coins, perm.coin_pub); - if (!c) { - console.warn("coin not found, can't apply refund"); - return; - } - refreshCoinsMap[c.coinPub] = { coinPub: c.coinPub }; - logger.trace( - `commiting refund ${perm.merchant_sig} to coin ${c.coinPub}`, - ); - logger.trace( - `coin amount before is ${Amounts.stringify(c.currentAmount)}`, - ); - logger.trace(`refund amount (via merchant) is ${perm.refund_amount}`); - logger.trace(`refund fee (via merchant) is ${perm.refund_fee}`); - const refundAmount = Amounts.parseOrThrow(perm.refund_amount); - const refundFee = Amounts.parseOrThrow(perm.refund_fee); - c.status = CoinStatus.Dormant; - c.currentAmount = Amounts.add(c.currentAmount, refundAmount).amount; - c.currentAmount = Amounts.sub(c.currentAmount, refundFee).amount; - logger.trace( - `coin amount after is ${Amounts.stringify(c.currentAmount)}`, - ); - await tx.put(Stores.coins, c); - }; - - for (const pk of Object.keys(newRefundsFailed)) { - if (p.refundState.refundsDone[pk]) { - // We already processed this one. - break; - } - const r = newRefundsFailed[pk]; - groups[r.refundGroupId] = true; - delete p.refundState.refundsPending[pk]; - p.refundState.refundsFailed[pk] = r; - } - - for (const pk of Object.keys(newRefundsDone)) { - if (p.refundState.refundsDone[pk]) { - // We already processed this one. - break; - } - const r = newRefundsDone[pk]; - groups[r.refundGroupId] = true; - delete p.refundState.refundsPending[pk]; - p.refundState.refundsDone[pk] = r; - await modCoin(r.perm); - } - - const now = getTimestampNow(); - for (const g of Object.keys(groups)) { - let groupDone = true; - for (const pk of Object.keys(p.refundState.refundsPending)) { - const r = p.refundState.refundsPending[pk]; - if (r.refundGroupId == g) { - groupDone = false; - } - } - if (groupDone) { - const refundEvent: RefundEventRecord = { - proposalId, - refundGroupId: g, - timestamp: now, - }; - await tx.put(Stores.refundEvents, refundEvent); - } - } - - if (Object.keys(p.refundState.refundsPending).length === 0) { - p.refundStatusRetryInfo = initRetryInfo(); - p.lastRefundStatusError = undefined; - allRefundsProcessed = true; - } - await tx.put(Stores.purchases, p); - const coinsPubsToBeRefreshed = Object.values(refreshCoinsMap); - if (coinsPubsToBeRefreshed.length > 0) { - await createRefreshGroup( - tx, - coinsPubsToBeRefreshed, - RefreshReason.Refund, - ); - } - }, - ); - if (allRefundsProcessed) { - ws.notify({ - type: NotificationType.RefundFinished, - }); - } - - ws.notify({ - type: NotificationType.RefundsSubmitted, - proposalId, - }); -} diff --git a/src/types/dbTypes.ts b/src/types/dbTypes.ts index a9344c045..158d438cf 100644 --- a/src/types/dbTypes.ts +++ b/src/types/dbTypes.ts @@ -27,7 +27,7 @@ import { AmountJson } from "../util/amounts"; import { Auditor, CoinDepositPermission, - MerchantRefundPermission, + MerchantRefundDetails, PayReq, TipResponse, ExchangeSignKeyJson, @@ -1091,7 +1091,7 @@ export interface RefundEventRecord { export interface RefundInfo { refundGroupId: string; - perm: MerchantRefundPermission; + perm: MerchantRefundDetails; } export const enum RefundReason { @@ -1102,7 +1102,7 @@ export const enum RefundReason { /** * Refund from an aborted payment. */ - AbortRefund = "abort-refund", + AbortRefund = "abort-pay-refund", } export interface RefundGroupInfo { @@ -1110,28 +1110,6 @@ export interface RefundGroupInfo { reason: RefundReason; } -export interface PurchaseRefundState { - /** - * Information regarding each group of refunds we receive at once. - */ - refundGroups: RefundGroupInfo[]; - - /** - * Pending refunds for the purchase. - */ - refundsPending: { [refundSig: string]: RefundInfo }; - - /** - * Applied refunds for the purchase. - */ - refundsDone: { [refundSig: string]: RefundInfo }; - - /** - * Submitted refunds for the purchase. - */ - refundsFailed: { [refundSig: string]: RefundInfo }; -} - /** * Record stored for every time we successfully submitted * a payment to the merchant (both first time and re-play). @@ -1230,9 +1208,25 @@ export interface PurchaseRecord { timestampAccept: Timestamp; /** - * State of refunds for this proposal. + * Information regarding each group of refunds we receive at once. + */ + refundGroups: RefundGroupInfo[]; + + /** + * Pending refunds for the purchase. A refund is pending + * when the merchant reports a transient error from the exchange. + */ + refundsPending: { [refundKey: string]: RefundInfo }; + + /** + * Applied refunds for the purchase. + */ + refundsDone: { [refundKey: string]: RefundInfo }; + + /** + * Refunds that permanently failed. */ - refundState: PurchaseRefundState; + refundsFailed: { [refundKey: string]: RefundInfo }; /** * When was the last refund made? @@ -1281,16 +1275,6 @@ export interface PurchaseRecord { lastRefundStatusError: OperationError | undefined; /** - * Retry information for querying the refund status with the merchant. - */ - refundApplyRetryInfo: RetryInfo; - - /** - * Last error (or undefined) for querying the refund status with the merchant. - */ - lastRefundApplyError: OperationError | undefined; - - /** * Continue querying the refund status until this deadline has expired. */ autoRefundDeadline: Timestamp | undefined; diff --git a/src/types/pending.ts b/src/types/pending.ts index 4ff82f55e..f949b7c16 100644 --- a/src/types/pending.ts +++ b/src/types/pending.ts @@ -35,7 +35,6 @@ export const enum PendingOperationType { Refresh = "refresh", Reserve = "reserve", Recoup = "recoup", - RefundApply = "refund-apply", RefundQuery = "refund-query", TipChoice = "tip-choice", TipPickup = "tip-pickup", @@ -53,7 +52,6 @@ export type PendingOperationInfo = PendingOperationInfoCommon & | PendingProposalChoiceOperation | PendingProposalDownloadOperation | PendingRefreshOperation - | PendingRefundApplyOperation | PendingRefundQueryOperation | PendingReserveOperation | PendingTipChoiceOperation @@ -188,20 +186,6 @@ export interface PendingRefundQueryOperation { lastError: OperationError | undefined; } -/** - * The wallet is processing refunds that it received from a merchant. - * During this operation, the wallet checks the refund permissions and sends - * them to the exchange to obtain a refund on a coin. - */ -export interface PendingRefundApplyOperation { - type: PendingOperationType.RefundApply; - proposalId: string; - retryInfo: RetryInfo; - lastError: OperationError | undefined; - numRefundsPending: number; - numRefundsDone: number; -} - export interface PendingRecoupOperation { type: PendingOperationType.Recoup; recoupGroupId: string; diff --git a/src/types/talerTypes.ts b/src/types/talerTypes.ts index 799c84dc5..17d11eea8 100644 --- a/src/types/talerTypes.ts +++ b/src/types/talerTypes.ts @@ -411,7 +411,7 @@ export interface PayReq { /** * Refund permission in the format that the merchant gives it to us. */ -export class MerchantRefundPermission { +export class MerchantRefundDetails { /** * Amount to be refunded. */ @@ -433,52 +433,30 @@ export class MerchantRefundPermission { rtransaction_id: number; /** - * Signature made by the merchant over the refund permission. + * Exchange's key used for the signature. */ - merchant_sig: string; -} - -/** - * Refund request sent to the exchange. - */ -export interface RefundRequest { - /** - * Amount to be refunded, can be a fraction of the - * coin's total deposit value (including deposit fee); - * must be larger than the refund fee. - */ - refund_amount: string; - - /** - * Refund fee associated with the given coin. - * must be smaller than the refund amount. - */ - refund_fee: string; + exchange_pub?: string; /** - * SHA-512 hash of the contact of the merchant with the customer. + * Exchange's signature to confirm the refund. */ - h_contract_terms: string; + exchange_sig?: string; /** - * coin's public key, both ECDHE and EdDSA. + * Error replay from the exchange (if any). */ - coin_pub: string; + exchange_reply?: any; /** - * 64-bit transaction id of the refund transaction between merchant and customer + * Error code from the exchange (if any). */ - rtransaction_id: number; - - /** - * EdDSA public key of the merchant. - */ - merchant_pub: string; + exchange_code?: number; /** - * EdDSA signature of the merchant affirming the refund. + * HTTP status code of the exchange's response + * to the merchant's refund request. */ - merchant_sig: string; + exchange_http_status: number; } /** @@ -499,7 +477,7 @@ export class MerchantRefundResponse { /** * The signed refund permissions, to be sent to the exchange. */ - refund_permissions: MerchantRefundPermission[]; + refunds: MerchantRefundDetails[]; } /** @@ -854,14 +832,18 @@ export const codecForContractTerms = (): Codec<ContractTerms> => .build("ContractTerms"); export const codecForMerchantRefundPermission = (): Codec< - MerchantRefundPermission + MerchantRefundDetails > => - makeCodecForObject<MerchantRefundPermission>() + makeCodecForObject<MerchantRefundDetails>() .property("refund_amount", codecForString) .property("refund_fee", codecForString) .property("coin_pub", codecForString) .property("rtransaction_id", codecForNumber) - .property("merchant_sig", codecForString) + .property("exchange_http_status", codecForNumber) + .property("exchange_code", makeCodecOptional(codecForNumber)) + .property("exchange_reply", makeCodecOptional(codecForAny)) + .property("exchange_sig", makeCodecOptional(codecForString)) + .property("exchange_pub", makeCodecOptional(codecForString)) .build("MerchantRefundPermission"); export const codecForMerchantRefundResponse = (): Codec< @@ -871,7 +853,7 @@ export const codecForMerchantRefundResponse = (): Codec< .property("merchant_pub", codecForString) .property("h_contract_terms", codecForString) .property( - "refund_permissions", + "refunds", makeCodecForList(codecForMerchantRefundPermission()), ) .build("MerchantRefundResponse"); diff --git a/src/wallet.ts b/src/wallet.ts index 273a9f875..41569a44f 100644 --- a/src/wallet.ts +++ b/src/wallet.ts @@ -34,7 +34,6 @@ import { } from "./operations/withdraw"; import { - abortFailedPayment, preparePayForUri, refuseProposal, confirmPay, @@ -53,7 +52,7 @@ import { ReserveRecordStatus, CoinSourceType, } from "./types/dbTypes"; -import { MerchantRefundPermission, CoinDumpJson } from "./types/talerTypes"; +import { MerchantRefundDetails, CoinDumpJson } from "./types/talerTypes"; import { BenchmarkResult, ConfirmPayResult, @@ -107,7 +106,6 @@ import { WalletNotification, NotificationType } from "./types/notifications"; import { HistoryQuery, HistoryEvent } from "./types/history"; import { processPurchaseQueryRefund, - processPurchaseApplyRefund, getFullRefundFees, applyRefund, } from "./operations/refund"; @@ -218,9 +216,6 @@ export class Wallet { case PendingOperationType.RefundQuery: await processPurchaseQueryRefund(this.ws, pending.proposalId, forceNow); break; - case PendingOperationType.RefundApply: - await processPurchaseApplyRefund(this.ws, pending.proposalId, forceNow); - break; case PendingOperationType.Recoup: await processRecoupGroup(this.ws, pending.recoupGroupId, forceNow); break; @@ -658,7 +653,7 @@ export class Wallet { } async getFullRefundFees( - refundPermissions: MerchantRefundPermission[], + refundPermissions: MerchantRefundDetails[], ): Promise<AmountJson> { return getFullRefundFees(this.ws, refundPermissions); } @@ -676,11 +671,7 @@ export class Wallet { } async abortFailedPayment(contractTermsHash: string): Promise<void> { - try { - return abortFailedPayment(this.ws, contractTermsHash); - } finally { - this.latch.trigger(); - } + throw Error("not implemented"); } /** @@ -745,20 +736,20 @@ export class Wallet { throw Error("unknown purchase"); } const refundsDoneAmounts = Object.values( - purchase.refundState.refundsDone, + purchase.refundsDone, ).map((x) => Amounts.parseOrThrow(x.perm.refund_amount)); const refundsPendingAmounts = Object.values( - purchase.refundState.refundsPending, + purchase.refundsPending, ).map((x) => Amounts.parseOrThrow(x.perm.refund_amount)); const totalRefundAmount = Amounts.sum([ ...refundsDoneAmounts, ...refundsPendingAmounts, ]).amount; const refundsDoneFees = Object.values( - purchase.refundState.refundsDone, + purchase.refundsDone, ).map((x) => Amounts.parseOrThrow(x.perm.refund_amount)); const refundsPendingFees = Object.values( - purchase.refundState.refundsPending, + purchase.refundsPending, ).map((x) => Amounts.parseOrThrow(x.perm.refund_amount)); const totalRefundFees = Amounts.sum([ ...refundsDoneFees, |