adopt new merchant refund API

This commit is contained in:
Florian Dold 2020-04-27 21:11:20 +05:30
parent e404f5e6d3
commit 5be0708a10
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
8 changed files with 225 additions and 486 deletions

View File

@ -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;
}

View File

@ -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,

View File

@ -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,
});
}
}
});
}

View File

@ -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 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,
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);
}
}
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) {
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();
p.lastRefundApplyError = undefined;
p.refundApplyRetryInfo = initRetryInfo();
p.refundState.refundGroups.push({
timestampQueried: now,
reason,
});
}
await tx.put(Stores.purchases, p);
});
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,
});
}

View File

@ -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.
*/
refundState: PurchaseRefundState;
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.
*/
refundsFailed: { [refundKey: string]: RefundInfo };
/**
* When was the last refund made?
@ -1280,16 +1274,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.
*/

View File

@ -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;

View File

@ -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;
exchange_pub?: string;
/**
* Refund fee associated with the given coin.
* must be smaller than the refund amount.
* Exchange's signature to confirm the refund.
*/
refund_fee: string;
exchange_sig?: string;
/**
* SHA-512 hash of the contact of the merchant with the customer.
* Error replay from the exchange (if any).
*/
h_contract_terms: string;
exchange_reply?: any;
/**
* coin's public key, both ECDHE and EdDSA.
* Error code from the exchange (if any).
*/
coin_pub: string;
exchange_code?: number;
/**
* 64-bit transaction id of the refund transaction between merchant and customer
* HTTP status code of the exchange's response
* to the merchant's refund request.
*/
rtransaction_id: number;
/**
* EdDSA public key of the merchant.
*/
merchant_pub: string;
/**
* EdDSA signature of the merchant affirming the refund.
*/
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");

View File

@ -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,