summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/operations/history.ts8
-rw-r--r--src/operations/pay.ts76
-rw-r--r--src/operations/pending.ts20
-rw-r--r--src/operations/refund.ts448
-rw-r--r--src/types/dbTypes.ts58
-rw-r--r--src/types/pending.ts16
-rw-r--r--src/types/talerTypes.ts60
-rw-r--r--src/wallet.ts23
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,