diff --git a/src/operations/pay.ts b/src/operations/pay.ts
index ccb55305d..388db94ba 100644
--- a/src/operations/pay.ts
+++ b/src/operations/pay.ts
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2019 GNUnet e.V.
+ (C) Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -14,6 +14,16 @@
GNU Taler; see the file COPYING. If not, see
*/
+/**
+ * Implementation of the payment operation, including downloading and
+ * claiming of proposals.
+ *
+ * @author Florian Dold
+ */
+
+/**
+ * Imports.
+ */
import { AmountJson } from "../util/amounts";
import {
Auditor,
@@ -36,9 +46,6 @@ import {
OperationError,
RefreshReason,
} from "../types/walletTypes";
-import {
- Database
-} from "../util/query";
import {
Stores,
CoinStatus,
@@ -49,6 +56,7 @@ import {
ProposalStatus,
initRetryInfo,
updateRetryInfoTimeout,
+ RefundReason,
} from "../types/dbTypes";
import * as Amounts from "../util/amounts";
import {
@@ -56,7 +64,6 @@ import {
strcmp,
canonicalJson,
extractTalerStampOrThrow,
- extractTalerDurationOrThrow,
extractTalerDuration,
} from "../util/helpers";
import { Logger } from "../util/logging";
@@ -69,8 +76,8 @@ import {
import { getTotalRefreshCost, createRefreshGroup } from "./refresh";
import { encodeCrock, getRandomBytes } from "../crypto/talerCrypto";
import { guardOperationException } from "./errors";
-import { assertUnreachable } from "../util/assertUnreachable";
import { NotificationType } from "../types/notifications";
+import { acceptRefundResponse } from "./refund";
export interface SpeculativePayData {
payCoinInfo: PayCoinInfo;
@@ -237,15 +244,13 @@ async function getCoinsForPayment(
continue;
}
- const coins = await ws.db.iterIndex(
- Stores.coins.exchangeBaseUrlIndex,
- exchange.baseUrl,
- ).toArray();
+ const coins = await ws.db
+ .iterIndex(Stores.coins.exchangeBaseUrlIndex, exchange.baseUrl)
+ .toArray();
- const denoms = await ws.db.iterIndex(
- Stores.denominations.exchangeBaseUrlIndex,
- exchange.baseUrl,
- ).toArray();
+ const denoms = await ws.db
+ .iterIndex(Stores.denominations.exchangeBaseUrlIndex, exchange.baseUrl)
+ .toArray();
if (!coins || coins.length === 0) {
continue;
@@ -353,8 +358,6 @@ async function recordConfirmPay(
lastSessionId: sessionId,
merchantSig: d.merchantSig,
payReq,
- refundsDone: {},
- refundsPending: {},
acceptTimestamp: getTimestampNow(),
lastRefundStatusTimestamp: undefined,
proposalId: proposal.proposalId,
@@ -368,6 +371,12 @@ async function recordConfirmPay(
firstSuccessfulPayTimestamp: undefined,
autoRefundDeadline: undefined,
paymentSubmitPending: true,
+ refundState: {
+ refundGroups: [],
+ refundsDone: {},
+ refundsFailed: {},
+ refundsPending: {},
+ },
};
await ws.db.runWithWriteTransaction(
@@ -447,7 +456,12 @@ export async function abortFailedPayment(
}
const refundResponse = MerchantRefundResponse.checked(await resp.json());
- await acceptRefundResponse(ws, purchase.proposalId, refundResponse);
+ await acceptRefundResponse(
+ ws,
+ purchase.proposalId,
+ refundResponse,
+ RefundReason.AbortRefund,
+ );
await ws.db.runWithWriteTransaction([Stores.purchases], async tx => {
const p = await tx.get(Stores.purchases, proposalId);
@@ -502,50 +516,6 @@ async function incrementPurchasePayRetry(
ws.notify({ type: NotificationType.PayOperationError });
}
-async function incrementPurchaseQueryRefundRetry(
- ws: InternalWalletState,
- proposalId: string,
- err: OperationError | undefined,
-): Promise {
- console.log("incrementing purchase refund query 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.refundStatusRetryInfo) {
- return;
- }
- pr.refundStatusRetryInfo.retryCounter++;
- updateRetryInfoTimeout(pr.refundStatusRetryInfo);
- pr.lastRefundStatusError = err;
- await tx.put(Stores.purchases, pr);
- });
- ws.notify({ type: NotificationType.RefundStatusOperationError });
-}
-
-async function incrementPurchaseApplyRefundRetry(
- ws: InternalWalletState,
- proposalId: string,
- err: OperationError | undefined,
-): Promise {
- 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.refundStatusRetryInfo);
- pr.lastRefundApplyError = err;
- await tx.put(Stores.purchases, pr);
- });
- ws.notify({ type: NotificationType.RefundApplyOperationError });
-}
-
export async function processDownloadProposal(
ws: InternalWalletState,
proposalId: string,
@@ -695,11 +665,11 @@ async function startDownloadProposal(
downloadSessionId: sessionId,
};
- await ws.db.runWithWriteTransaction([Stores.proposals], async (tx) => {
- const existingRecord = await tx.getIndexed(Stores.proposals.urlAndOrderIdIndex, [
- merchantBaseUrl,
- orderId,
- ]);
+ await ws.db.runWithWriteTransaction([Stores.proposals], async tx => {
+ const existingRecord = await tx.getIndexed(
+ Stores.proposals.urlAndOrderIdIndex,
+ [merchantBaseUrl, orderId],
+ );
if (existingRecord) {
// Created concurrently
return;
@@ -793,7 +763,11 @@ export async function submitPay(
for (let c of modifiedCoins) {
await tx.put(Stores.coins, c);
}
- await createRefreshGroup(tx, modifiedCoins.map((x) => ({ coinPub: x.coinPub })), RefreshReason.Pay);
+ await createRefreshGroup(
+ tx,
+ modifiedCoins.map(x => ({ coinPub: x.coinPub })),
+ RefreshReason.Pay,
+ );
await tx.put(Stores.purchases, purchase);
},
);
@@ -1069,192 +1043,6 @@ export async function confirmPay(
return submitPay(ws, proposalId);
}
-export async function getFullRefundFees(
- ws: InternalWalletState,
- refundPermissions: MerchantRefundPermission[],
-): Promise {
- if (refundPermissions.length === 0) {
- throw Error("no refunds given");
- }
- const coin0 = await ws.db.get(
- 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 ws.db.iterIndex(
- Stores.denominations.exchangeBaseUrlIndex,
- coin0.exchangeBaseUrl,
- ).toArray();
-
- for (const rp of refundPermissions) {
- const coin = await ws.db.get(Stores.coins, rp.coin_pub);
- if (!coin) {
- throw Error("coin not found");
- }
- const denom = await ws.db.get(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 acceptRefundResponse(
- ws: InternalWalletState,
- proposalId: string,
- refundResponse: MerchantRefundResponse,
-): Promise {
- const refundPermissions = refundResponse.refund_permissions;
-
- let numNewRefunds = 0;
-
- 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;
- }
-
- if (!p.refundStatusRequested) {
- return;
- }
-
- for (const perm of refundPermissions) {
- if (
- !p.refundsPending[perm.merchant_sig] &&
- !p.refundsDone[perm.merchant_sig]
- ) {
- p.refundsPending[perm.merchant_sig] = perm;
- numNewRefunds++;
- }
- }
-
- // 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
- ) {
- queryDone = false;
- }
- }
-
- if (queryDone) {
- p.lastRefundStatusTimestamp = 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.lastRefundStatusTimestamp = getTimestampNow();
- p.refundStatusRetryInfo.retryCounter++;
- updateRetryInfoTimeout(p.refundStatusRetryInfo);
- p.lastRefundStatusError = undefined;
- console.log("refund query not done");
- }
-
- if (numNewRefunds) {
- p.lastRefundApplyError = undefined;
- p.refundApplyRetryInfo = initRetryInfo();
- }
-
- await tx.put(Stores.purchases, p);
- });
- ws.notify({
- type: NotificationType.RefundQueried,
- });
- if (numNewRefunds > 0) {
- await processPurchaseApplyRefund(ws, proposalId);
- }
-}
-
-async function startRefundQuery(
- ws: InternalWalletState,
- proposalId: string,
-): Promise {
- const success = await ws.db.runWithWriteTransaction(
- [Stores.purchases],
- async tx => {
- const p = await tx.get(Stores.purchases, proposalId);
- if (!p) {
- console.log("no purchase found for refund URL");
- return false;
- }
- p.refundStatusRequested = true;
- p.lastRefundStatusError = undefined;
- p.refundStatusRetryInfo = initRetryInfo();
- await tx.put(Stores.purchases, p);
- return true;
- },
- );
-
- if (!success) {
- return;
- }
-
- ws.notify({
- type: NotificationType.RefundStarted,
- });
-
- await processPurchaseQueryRefund(ws, proposalId);
-}
-
-/**
- * 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 {
- const parseResult = parseRefundUri(talerRefundUri);
-
- console.log("applying refund");
-
- if (!parseResult) {
- throw Error("invalid refund URI");
- }
-
- const purchase = await ws.db.getIndexed(
- Stores.purchases.orderIdIndex,
- [parseResult.merchantBaseUrl, parseResult.orderId],
- );
-
- if (!purchase) {
- throw Error("no purchase for the taler://refund/ URI was found");
- }
-
- console.log("processing purchase for refund");
- await startRefundQuery(ws, purchase.proposalId);
-
- return purchase.contractTermsHash;
-}
-
export async function processPurchasePay(
ws: InternalWalletState,
proposalId: string,
@@ -1298,176 +1086,3 @@ async function processPurchasePayImpl(
logger.trace(`processing purchase pay ${proposalId}`);
await submitPay(ws, proposalId);
}
-
-export async function processPurchaseQueryRefund(
- ws: InternalWalletState,
- proposalId: string,
- forceNow: boolean = false,
-): Promise {
- const onOpErr = (e: OperationError) =>
- incrementPurchaseQueryRefundRetry(ws, proposalId, e);
- await guardOperationException(
- () => processPurchaseQueryRefundImpl(ws, proposalId, forceNow),
- onOpErr,
- );
-}
-
-async function resetPurchaseQueryRefundRetry(
- ws: InternalWalletState,
- proposalId: string,
-) {
- await ws.db.mutate(Stores.purchases, proposalId, x => {
- if (x.refundStatusRetryInfo.active) {
- x.refundStatusRetryInfo = initRetryInfo();
- }
- return x;
- });
-}
-
-async function processPurchaseQueryRefundImpl(
- ws: InternalWalletState,
- proposalId: string,
- forceNow: boolean,
-): Promise {
- if (forceNow) {
- await resetPurchaseQueryRefundRetry(ws, proposalId);
- }
- const purchase = await ws.db.get(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;
- }
- if (resp.status !== 200) {
- throw Error(`unexpected status code (${resp.status}) for /refund`);
- }
-
- const refundResponse = MerchantRefundResponse.checked(await resp.json());
- await acceptRefundResponse(ws, proposalId, refundResponse);
-}
-
-export async function processPurchaseApplyRefund(
- ws: InternalWalletState,
- proposalId: string,
- forceNow: boolean = false,
-): Promise {
- const onOpErr = (e: OperationError) =>
- incrementPurchaseApplyRefundRetry(ws, proposalId, e);
- await guardOperationException(
- () => processPurchaseApplyRefundImpl(ws, proposalId, forceNow),
- onOpErr,
- );
-}
-
-async function resetPurchaseApplyRefundRetry(
- ws: InternalWalletState,
- proposalId: string,
-) {
- 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 {
- 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.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 ws.db.runWithWriteTransaction(
- [Stores.purchases, Stores.coins, Stores.refreshGroups],
- 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.Dormant;
- c.currentAmount = Amounts.add(c.currentAmount, refundAmount).amount;
- c.currentAmount = Amounts.sub(c.currentAmount, refundFee).amount;
- await tx.put(Stores.coins, c);
- await createRefreshGroup(tx, [{ coinPub: perm.coin_pub }], RefreshReason.Refund);
- },
- );
- if (allRefundsProcessed) {
- ws.notify({
- type: NotificationType.RefundFinished,
- });
- }
- }
-
- ws.notify({
- type: NotificationType.RefundsSubmitted,
- proposalId,
- });
-}
diff --git a/src/operations/pending.ts b/src/operations/pending.ts
index 27892df06..f0b29792d 100644
--- a/src/operations/pending.ts
+++ b/src/operations/pending.ts
@@ -365,9 +365,9 @@ async function gatherPurchasePending(
});
}
}
- const numRefundsPending = Object.keys(pr.refundsPending).length;
+ const numRefundsPending = Object.keys(pr.refundState.refundsPending).length;
if (numRefundsPending > 0) {
- const numRefundsDone = Object.keys(pr.refundsDone).length;
+ const numRefundsDone = Object.keys(pr.refundState.refundsDone).length;
resp.nextRetryDelay = updateRetryDelay(
resp.nextRetryDelay,
now,
diff --git a/src/types/dbTypes.ts b/src/types/dbTypes.ts
index c05aa68d7..9d2f6fe5d 100644
--- a/src/types/dbTypes.ts
+++ b/src/types/dbTypes.ts
@@ -1002,6 +1002,63 @@ export interface WireFee {
sig: string;
}
+/**
+ * Record to store information about a refund event.
+ *
+ * All information about a refund is stored with the purchase,
+ * this event is just for the history.
+ *
+ * The event is only present for completed refunds.
+ */
+export interface RefundEventRecord {
+ timestamp: Timestamp;
+ refundGroupId: string;
+ proposalId: string;
+}
+
+export interface RefundInfo {
+ refundGroupId: string;
+ perm: MerchantRefundPermission;
+}
+
+export const enum RefundReason {
+ /**
+ * Normal refund given by the merchant.
+ */
+ NormalRefund = "normal-refund",
+ /**
+ * Refund from an aborted payment.
+ */
+ AbortRefund = "abort-refund",
+}
+
+export interface RefundGroupInfo {
+ timestampQueried: Timestamp;
+ 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 that stores status information about one purchase, starting from when
* the customer accepts a proposal. Includes refund status if applicable.
@@ -1034,24 +1091,23 @@ export interface PurchaseRecord {
*/
merchantSig: string;
+ /**
+ * Timestamp of the first time that sending a payment to the merchant
+ * for this purchase was successful.
+ */
firstSuccessfulPayTimestamp: Timestamp | undefined;
- /**
- * Pending refunds for the purchase.
- */
- refundsPending: { [refundSig: string]: MerchantRefundPermission };
-
- /**
- * Submitted refunds for the purchase.
- */
- refundsDone: { [refundSig: string]: MerchantRefundPermission };
-
/**
* When was the purchase made?
* Refers to the time that the user accepted.
*/
acceptTimestamp: Timestamp;
+ /**
+ * State of refunds for this proposal.
+ */
+ refundState: PurchaseRefundState;
+
/**
* When was the last refund made?
* Set to 0 if no refund was made on the purchase.
@@ -1370,6 +1426,12 @@ export namespace Stores {
}
}
+ class RefundEventsStore extends Store {
+ constructor() {
+ super("refundEvents", { keyPath: "refundGroupId" });
+ }
+ }
+
class BankWithdrawUrisStore extends Store {
constructor() {
super("bankWithdrawUris", { keyPath: "talerWithdrawUri" });
@@ -1394,6 +1456,7 @@ export namespace Stores {
export const senderWires = new SenderWiresStore();
export const withdrawalSession = new WithdrawalSessionsStore();
export const bankWithdrawUris = new BankWithdrawUrisStore();
+ export const refundEvents = new RefundEventsStore();
}
/* tslint:enable:completed-docs */
diff --git a/src/util/http.ts b/src/util/http.ts
index 79039f516..93c748d79 100644
--- a/src/util/http.ts
+++ b/src/util/http.ts
@@ -33,6 +33,11 @@ export interface HttpRequestOptions {
headers?: { [name: string]: string };
}
+export enum HttpResponseStatus {
+ Ok = 200,
+ Gone = 210,
+}
+
/**
* Headers, roughly modeled after the fetch API's headers object.
*/
diff --git a/src/wallet.ts b/src/wallet.ts
index 163f3def9..a4e201074 100644
--- a/src/wallet.ts
+++ b/src/wallet.ts
@@ -42,11 +42,7 @@ import {
preparePay,
confirmPay,
processDownloadProposal,
- applyRefund,
- getFullRefundFees,
processPurchasePay,
- processPurchaseQueryRefund,
- processPurchaseApplyRefund,
} from "./operations/pay";
import {
@@ -107,6 +103,7 @@ import { AsyncOpMemoSingle } from "./util/asyncMemo";
import { PendingOperationInfo, PendingOperationsResponse, PendingOperationType } from "./types/pending";
import { WalletNotification, NotificationType } from "./types/notifications";
import { HistoryQuery, HistoryEvent } from "./types/history";
+import { processPurchaseQueryRefund, processPurchaseApplyRefund, getFullRefundFees, applyRefund } from "./operations/refund";
/**
* Wallet protocol version spoken with the exchange
@@ -695,21 +692,21 @@ export class Wallet {
if (!purchase) {
throw Error("unknown purchase");
}
- const refundsDoneAmounts = Object.values(purchase.refundsDone).map(x =>
- Amounts.parseOrThrow(x.refund_amount),
+ const refundsDoneAmounts = Object.values(purchase.refundState.refundsDone).map(x =>
+ Amounts.parseOrThrow(x.perm.refund_amount),
);
const refundsPendingAmounts = Object.values(
- purchase.refundsPending,
- ).map(x => Amounts.parseOrThrow(x.refund_amount));
+ purchase.refundState.refundsPending,
+ ).map(x => Amounts.parseOrThrow(x.perm.refund_amount));
const totalRefundAmount = Amounts.sum([
...refundsDoneAmounts,
...refundsPendingAmounts,
]).amount;
- const refundsDoneFees = Object.values(purchase.refundsDone).map(x =>
- Amounts.parseOrThrow(x.refund_amount),
+ const refundsDoneFees = Object.values(purchase.refundState.refundsDone).map(x =>
+ Amounts.parseOrThrow(x.perm.refund_amount),
);
- const refundsPendingFees = Object.values(purchase.refundsPending).map(x =>
- Amounts.parseOrThrow(x.refund_amount),
+ const refundsPendingFees = Object.values(purchase.refundState.refundsPending).map(x =>
+ Amounts.parseOrThrow(x.perm.refund_amount),
);
const totalRefundFees = Amounts.sum([
...refundsDoneFees,
diff --git a/tsconfig.json b/tsconfig.json
index 43ccd7e21..81e529fad 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -54,6 +54,7 @@
"src/operations/payback.ts",
"src/operations/pending.ts",
"src/operations/refresh.ts",
+ "src/operations/refund.ts",
"src/operations/reserves.ts",
"src/operations/return.ts",
"src/operations/state.ts",