implement payment aborts with integration test
This commit is contained in:
parent
68ca4600e0
commit
67df550b4f
@ -80,7 +80,7 @@ export class FaultProxy {
|
|||||||
start() {
|
start() {
|
||||||
const server = http.createServer((req, res) => {
|
const server = http.createServer((req, res) => {
|
||||||
const requestChunks: Buffer[] = [];
|
const requestChunks: Buffer[] = [];
|
||||||
const requestUrl = `http://locahost:${this.faultProxyConfig.inboundPort}${req.url}`;
|
const requestUrl = `http://localhost:${this.faultProxyConfig.inboundPort}${req.url}`;
|
||||||
console.log("request for", new URL(requestUrl));
|
console.log("request for", new URL(requestUrl));
|
||||||
req.on("data", (chunk) => {
|
req.on("data", (chunk) => {
|
||||||
requestChunks.push(chunk);
|
requestChunks.push(chunk);
|
||||||
|
@ -76,6 +76,7 @@ import {
|
|||||||
PrepareTipRequest,
|
PrepareTipRequest,
|
||||||
codecForPrepareTipResult,
|
codecForPrepareTipResult,
|
||||||
AcceptTipRequest,
|
AcceptTipRequest,
|
||||||
|
AbortPayWithRefundRequest,
|
||||||
} from "taler-wallet-core";
|
} from "taler-wallet-core";
|
||||||
import { URL } from "url";
|
import { URL } from "url";
|
||||||
import axios, { AxiosError } from "axios";
|
import axios, { AxiosError } from "axios";
|
||||||
@ -1538,6 +1539,15 @@ export class WalletCli {
|
|||||||
throw new OperationFailedError(resp.error);
|
throw new OperationFailedError(resp.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async abortFailedPayWithRefund(req: AbortPayWithRefundRequest): Promise<void> {
|
||||||
|
const resp = await this.apiRequest("abortFailedPayWithRefund", req);
|
||||||
|
if (resp.type === "response") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new OperationFailedError(resp.error);
|
||||||
|
}
|
||||||
|
|
||||||
async confirmPay(req: ConfirmPayRequest): Promise<ConfirmPayResult> {
|
async confirmPay(req: ConfirmPayRequest): Promise<ConfirmPayResult> {
|
||||||
const resp = await this.apiRequest("confirmPay", req);
|
const resp = await this.apiRequest("confirmPay", req);
|
||||||
if (resp.type === "response") {
|
if (resp.type === "response") {
|
||||||
|
@ -36,6 +36,7 @@ import {
|
|||||||
BankApi,
|
BankApi,
|
||||||
BankAccessApi,
|
BankAccessApi,
|
||||||
MerchantPrivateApi,
|
MerchantPrivateApi,
|
||||||
|
ExchangeServiceInterface,
|
||||||
} from "./harness";
|
} from "./harness";
|
||||||
import {
|
import {
|
||||||
AmountString,
|
AmountString,
|
||||||
@ -233,7 +234,7 @@ export async function startWithdrawViaBank(
|
|||||||
p: {
|
p: {
|
||||||
wallet: WalletCli;
|
wallet: WalletCli;
|
||||||
bank: BankService;
|
bank: BankService;
|
||||||
exchange: ExchangeService;
|
exchange: ExchangeServiceInterface;
|
||||||
amount: AmountString;
|
amount: AmountString;
|
||||||
},
|
},
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
@ -272,7 +273,7 @@ export async function withdrawViaBank(
|
|||||||
p: {
|
p: {
|
||||||
wallet: WalletCli;
|
wallet: WalletCli;
|
||||||
bank: BankService;
|
bank: BankService;
|
||||||
exchange: ExchangeService;
|
exchange: ExchangeServiceInterface;
|
||||||
amount: AmountString;
|
amount: AmountString;
|
||||||
},
|
},
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
@ -21,7 +21,6 @@ import {
|
|||||||
runTest,
|
runTest,
|
||||||
GlobalTestState,
|
GlobalTestState,
|
||||||
MerchantPrivateApi,
|
MerchantPrivateApi,
|
||||||
BankAccessApi,
|
|
||||||
BankApi,
|
BankApi,
|
||||||
} from "./harness";
|
} from "./harness";
|
||||||
import { createSimpleTestkudosEnvironment, withdrawViaBank } from "./helpers";
|
import { createSimpleTestkudosEnvironment, withdrawViaBank } from "./helpers";
|
||||||
|
@ -35,6 +35,7 @@ import {
|
|||||||
CoinRecord,
|
CoinRecord,
|
||||||
DenominationRecord,
|
DenominationRecord,
|
||||||
PayCoinSelection,
|
PayCoinSelection,
|
||||||
|
AbortStatus,
|
||||||
} from "../types/dbTypes";
|
} from "../types/dbTypes";
|
||||||
import { NotificationType } from "../types/notifications";
|
import { NotificationType } from "../types/notifications";
|
||||||
import {
|
import {
|
||||||
@ -77,7 +78,11 @@ import {
|
|||||||
} from "../util/http";
|
} from "../util/http";
|
||||||
import { TalerErrorCode } from "../TalerErrorCode";
|
import { TalerErrorCode } from "../TalerErrorCode";
|
||||||
import { URL } from "../util/url";
|
import { URL } from "../util/url";
|
||||||
import { initRetryInfo, updateRetryInfoTimeout, getRetryDuration } from "../util/retries";
|
import {
|
||||||
|
initRetryInfo,
|
||||||
|
updateRetryInfoTimeout,
|
||||||
|
getRetryDuration,
|
||||||
|
} from "../util/retries";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logger.
|
* Logger.
|
||||||
@ -111,7 +116,6 @@ export interface AvailableCoinInfo {
|
|||||||
feeDeposit: AmountJson;
|
feeDeposit: AmountJson;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compute the total cost of a payment to the customer.
|
* Compute the total cost of a payment to the customer.
|
||||||
*
|
*
|
||||||
@ -429,8 +433,7 @@ async function recordConfirmPay(
|
|||||||
logger.trace(`recording payment with session ID ${sessionId}`);
|
logger.trace(`recording payment with session ID ${sessionId}`);
|
||||||
const payCostInfo = await getTotalPaymentCost(ws, coinSelection);
|
const payCostInfo = await getTotalPaymentCost(ws, coinSelection);
|
||||||
const t: PurchaseRecord = {
|
const t: PurchaseRecord = {
|
||||||
abortDone: false,
|
abortStatus: AbortStatus.None,
|
||||||
abortRequested: false,
|
|
||||||
contractTermsRaw: d.contractTermsRaw,
|
contractTermsRaw: d.contractTermsRaw,
|
||||||
contractData: d.contractData,
|
contractData: d.contractData,
|
||||||
lastSessionId: sessionId,
|
lastSessionId: sessionId,
|
||||||
@ -444,7 +447,7 @@ async function recordConfirmPay(
|
|||||||
lastRefundStatusError: undefined,
|
lastRefundStatusError: undefined,
|
||||||
payRetryInfo: initRetryInfo(),
|
payRetryInfo: initRetryInfo(),
|
||||||
refundStatusRetryInfo: initRetryInfo(),
|
refundStatusRetryInfo: initRetryInfo(),
|
||||||
refundStatusRequested: false,
|
refundQueryRequested: false,
|
||||||
timestampFirstSuccessfulPay: undefined,
|
timestampFirstSuccessfulPay: undefined,
|
||||||
autoRefundDeadline: undefined,
|
autoRefundDeadline: undefined,
|
||||||
paymentSubmitPending: true,
|
paymentSubmitPending: true,
|
||||||
@ -522,6 +525,10 @@ async function incrementProposalRetry(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FIXME: currently pay operations aren't ever automatically retried.
|
||||||
|
* But we still keep a payRetryInfo around in the database.
|
||||||
|
*/
|
||||||
async function incrementPurchasePayRetry(
|
async function incrementPurchasePayRetry(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
proposalId: string,
|
proposalId: string,
|
||||||
@ -579,7 +586,10 @@ function getProposalRequestTimeout(proposal: ProposalRecord): Duration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getPayRequestTimeout(purchase: PurchaseRecord): Duration {
|
function getPayRequestTimeout(purchase: PurchaseRecord): Duration {
|
||||||
return durationMul({ d_ms: 5000 }, 1 + purchase.payCoinSelection.coinPubs.length / 20);
|
return durationMul(
|
||||||
|
{ d_ms: 5000 },
|
||||||
|
1 + purchase.payCoinSelection.coinPubs.length / 20,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function processDownloadProposalImpl(
|
async function processDownloadProposalImpl(
|
||||||
@ -794,40 +804,37 @@ async function storeFirstPaySuccess(
|
|||||||
paySig: string,
|
paySig: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const now = getTimestampNow();
|
const now = getTimestampNow();
|
||||||
await ws.db.runWithWriteTransaction(
|
await ws.db.runWithWriteTransaction([Stores.purchases], async (tx) => {
|
||||||
[Stores.purchases],
|
const purchase = await tx.get(Stores.purchases, proposalId);
|
||||||
async (tx) => {
|
|
||||||
const purchase = await tx.get(Stores.purchases, proposalId);
|
|
||||||
|
|
||||||
if (!purchase) {
|
if (!purchase) {
|
||||||
logger.warn("purchase does not exist anymore");
|
logger.warn("purchase does not exist anymore");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const isFirst = purchase.timestampFirstSuccessfulPay === undefined;
|
const isFirst = purchase.timestampFirstSuccessfulPay === undefined;
|
||||||
if (!isFirst) {
|
if (!isFirst) {
|
||||||
logger.warn("payment success already stored");
|
logger.warn("payment success already stored");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
purchase.timestampFirstSuccessfulPay = now;
|
purchase.timestampFirstSuccessfulPay = now;
|
||||||
purchase.paymentSubmitPending = false;
|
purchase.paymentSubmitPending = false;
|
||||||
purchase.lastPayError = undefined;
|
purchase.lastPayError = undefined;
|
||||||
purchase.lastSessionId = sessionId;
|
purchase.lastSessionId = sessionId;
|
||||||
purchase.payRetryInfo = initRetryInfo(false);
|
purchase.payRetryInfo = initRetryInfo(false);
|
||||||
purchase.merchantPaySig = paySig;
|
purchase.merchantPaySig = paySig;
|
||||||
if (isFirst) {
|
if (isFirst) {
|
||||||
const ar = purchase.contractData.autoRefund;
|
const ar = purchase.contractData.autoRefund;
|
||||||
if (ar) {
|
if (ar) {
|
||||||
logger.info("auto_refund present");
|
logger.info("auto_refund present");
|
||||||
purchase.refundStatusRequested = true;
|
purchase.refundQueryRequested = true;
|
||||||
purchase.refundStatusRetryInfo = initRetryInfo();
|
purchase.refundStatusRetryInfo = initRetryInfo();
|
||||||
purchase.lastRefundStatusError = undefined;
|
purchase.lastRefundStatusError = undefined;
|
||||||
purchase.autoRefundDeadline = timestampAddDuration(now, ar);
|
purchase.autoRefundDeadline = timestampAddDuration(now, ar);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await tx.put(Stores.purchases, purchase);
|
await tx.put(Stores.purchases, purchase);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function storePayReplaySuccess(
|
async function storePayReplaySuccess(
|
||||||
@ -835,26 +842,23 @@ async function storePayReplaySuccess(
|
|||||||
proposalId: string,
|
proposalId: string,
|
||||||
sessionId: string | undefined,
|
sessionId: string | undefined,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await ws.db.runWithWriteTransaction(
|
await ws.db.runWithWriteTransaction([Stores.purchases], async (tx) => {
|
||||||
[Stores.purchases],
|
const purchase = await tx.get(Stores.purchases, proposalId);
|
||||||
async (tx) => {
|
|
||||||
const purchase = await tx.get(Stores.purchases, proposalId);
|
|
||||||
|
|
||||||
if (!purchase) {
|
if (!purchase) {
|
||||||
logger.warn("purchase does not exist anymore");
|
logger.warn("purchase does not exist anymore");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const isFirst = purchase.timestampFirstSuccessfulPay === undefined;
|
const isFirst = purchase.timestampFirstSuccessfulPay === undefined;
|
||||||
if (isFirst) {
|
if (isFirst) {
|
||||||
throw Error("invalid payment state");
|
throw Error("invalid payment state");
|
||||||
}
|
}
|
||||||
purchase.paymentSubmitPending = false;
|
purchase.paymentSubmitPending = false;
|
||||||
purchase.lastPayError = undefined;
|
purchase.lastPayError = undefined;
|
||||||
purchase.payRetryInfo = initRetryInfo(false);
|
purchase.payRetryInfo = initRetryInfo(false);
|
||||||
purchase.lastSessionId = sessionId;
|
purchase.lastSessionId = sessionId;
|
||||||
await tx.put(Stores.purchases, purchase);
|
await tx.put(Stores.purchases, purchase);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -863,7 +867,7 @@ async function storePayReplaySuccess(
|
|||||||
* If the wallet has previously paid, it just transmits the merchant's
|
* If the wallet has previously paid, it just transmits the merchant's
|
||||||
* own signature certifying that the wallet has previously paid.
|
* own signature certifying that the wallet has previously paid.
|
||||||
*/
|
*/
|
||||||
export async function submitPay(
|
async function submitPay(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
proposalId: string,
|
proposalId: string,
|
||||||
): Promise<ConfirmPayResult> {
|
): Promise<ConfirmPayResult> {
|
||||||
@ -871,7 +875,7 @@ export async function submitPay(
|
|||||||
if (!purchase) {
|
if (!purchase) {
|
||||||
throw Error("Purchase not found: " + proposalId);
|
throw Error("Purchase not found: " + proposalId);
|
||||||
}
|
}
|
||||||
if (purchase.abortRequested) {
|
if (purchase.abortStatus !== AbortStatus.None) {
|
||||||
throw Error("not submitting payment for aborted purchase");
|
throw Error("not submitting payment for aborted purchase");
|
||||||
}
|
}
|
||||||
const sessionId = purchase.lastSessionId;
|
const sessionId = purchase.lastSessionId;
|
||||||
@ -1047,7 +1051,11 @@ export async function preparePayForUri(
|
|||||||
p.lastSessionId = uriResult.sessionId;
|
p.lastSessionId = uriResult.sessionId;
|
||||||
await tx.put(Stores.purchases, p);
|
await tx.put(Stores.purchases, p);
|
||||||
});
|
});
|
||||||
const r = await submitPay(ws, proposalId);
|
const r = await guardOperationException(
|
||||||
|
() => submitPay(ws, proposalId),
|
||||||
|
(e: TalerErrorDetails): Promise<void> =>
|
||||||
|
incrementPurchasePayRetry(ws, proposalId, e),
|
||||||
|
);
|
||||||
if (r.type !== ConfirmPayResultType.Done) {
|
if (r.type !== ConfirmPayResultType.Done) {
|
||||||
throw Error("submitting pay failed");
|
throw Error("submitting pay failed");
|
||||||
}
|
}
|
||||||
@ -1125,7 +1133,11 @@ export async function confirmPay(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
logger.trace("confirmPay: submitting payment for existing purchase");
|
logger.trace("confirmPay: submitting payment for existing purchase");
|
||||||
return submitPay(ws, proposalId);
|
return await guardOperationException(
|
||||||
|
() => submitPay(ws, proposalId),
|
||||||
|
(e: TalerErrorDetails): Promise<void> =>
|
||||||
|
incrementPurchasePayRetry(ws, proposalId, e),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.trace("confirmPay: purchase record does not exist yet");
|
logger.trace("confirmPay: purchase record does not exist yet");
|
||||||
@ -1179,7 +1191,11 @@ export async function confirmPay(
|
|||||||
sessionIdOverride,
|
sessionIdOverride,
|
||||||
);
|
);
|
||||||
|
|
||||||
return submitPay(ws, proposalId);
|
return await guardOperationException(
|
||||||
|
() => submitPay(ws, proposalId),
|
||||||
|
(e: TalerErrorDetails): Promise<void> =>
|
||||||
|
incrementPurchasePayRetry(ws, proposalId, e),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function processPurchasePay(
|
export async function processPurchasePay(
|
||||||
|
@ -22,6 +22,7 @@ import {
|
|||||||
ProposalStatus,
|
ProposalStatus,
|
||||||
ReserveRecordStatus,
|
ReserveRecordStatus,
|
||||||
Stores,
|
Stores,
|
||||||
|
AbortStatus,
|
||||||
} from "../types/dbTypes";
|
} from "../types/dbTypes";
|
||||||
import {
|
import {
|
||||||
PendingOperationsResponse,
|
PendingOperationsResponse,
|
||||||
@ -381,7 +382,7 @@ async function gatherPurchasePending(
|
|||||||
onlyDue = false,
|
onlyDue = false,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await tx.iter(Stores.purchases).forEach((pr) => {
|
await tx.iter(Stores.purchases).forEach((pr) => {
|
||||||
if (pr.paymentSubmitPending) {
|
if (pr.paymentSubmitPending && pr.abortStatus === AbortStatus.None) {
|
||||||
resp.nextRetryDelay = updateRetryDelay(
|
resp.nextRetryDelay = updateRetryDelay(
|
||||||
resp.nextRetryDelay,
|
resp.nextRetryDelay,
|
||||||
now,
|
now,
|
||||||
@ -398,7 +399,7 @@ async function gatherPurchasePending(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (pr.refundStatusRequested) {
|
if (pr.refundQueryRequested) {
|
||||||
resp.nextRetryDelay = updateRetryDelay(
|
resp.nextRetryDelay = updateRetryDelay(
|
||||||
resp.nextRetryDelay,
|
resp.nextRetryDelay,
|
||||||
now,
|
now,
|
||||||
|
@ -36,6 +36,7 @@ import {
|
|||||||
RefundReason,
|
RefundReason,
|
||||||
RefundState,
|
RefundState,
|
||||||
PurchaseRecord,
|
PurchaseRecord,
|
||||||
|
AbortStatus,
|
||||||
} from "../types/dbTypes";
|
} from "../types/dbTypes";
|
||||||
import { NotificationType } from "../types/notifications";
|
import { NotificationType } from "../types/notifications";
|
||||||
import { parseRefundUri } from "../util/taleruri";
|
import { parseRefundUri } from "../util/taleruri";
|
||||||
@ -46,14 +47,25 @@ import {
|
|||||||
MerchantCoinRefundSuccessStatus,
|
MerchantCoinRefundSuccessStatus,
|
||||||
MerchantCoinRefundFailureStatus,
|
MerchantCoinRefundFailureStatus,
|
||||||
codecForMerchantOrderRefundPickupResponse,
|
codecForMerchantOrderRefundPickupResponse,
|
||||||
|
AbortRequest,
|
||||||
|
AbortingCoin,
|
||||||
|
codecForMerchantAbortPayRefundStatus,
|
||||||
|
codecForAbortResponse,
|
||||||
} from "../types/talerTypes";
|
} from "../types/talerTypes";
|
||||||
import { guardOperationException } from "./errors";
|
import { guardOperationException } from "./errors";
|
||||||
import { getTimestampNow, Timestamp } from "../util/time";
|
import {
|
||||||
|
getTimestampNow,
|
||||||
|
Timestamp,
|
||||||
|
durationAdd,
|
||||||
|
timestampAddDuration,
|
||||||
|
} from "../util/time";
|
||||||
import { Logger } from "../util/logging";
|
import { Logger } from "../util/logging";
|
||||||
import { readSuccessResponseJsonOrThrow } from "../util/http";
|
import { readSuccessResponseJsonOrThrow } from "../util/http";
|
||||||
import { TransactionHandle } from "../util/query";
|
import { TransactionHandle } from "../util/query";
|
||||||
import { URL } from "../util/url";
|
import { URL } from "../util/url";
|
||||||
import { updateRetryInfoTimeout, initRetryInfo } from "../util/retries";
|
import { updateRetryInfoTimeout, initRetryInfo } from "../util/retries";
|
||||||
|
import { checkDbInvariant } from "../util/invariants";
|
||||||
|
import { TalerErrorCode } from "../TalerErrorCode";
|
||||||
|
|
||||||
const logger = new Logger("refund.ts");
|
const logger = new Logger("refund.ts");
|
||||||
|
|
||||||
@ -101,7 +113,7 @@ async function applySuccessfulRefund(
|
|||||||
const refundKey = getRefundKey(r);
|
const refundKey = getRefundKey(r);
|
||||||
const coin = await tx.get(Stores.coins, r.coin_pub);
|
const coin = await tx.get(Stores.coins, r.coin_pub);
|
||||||
if (!coin) {
|
if (!coin) {
|
||||||
console.warn("coin not found, can't apply refund");
|
logger.warn("coin not found, can't apply refund");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const denom = await tx.get(Stores.denominations, [
|
const denom = await tx.get(Stores.denominations, [
|
||||||
@ -158,7 +170,7 @@ async function storePendingRefund(
|
|||||||
|
|
||||||
const coin = await tx.get(Stores.coins, r.coin_pub);
|
const coin = await tx.get(Stores.coins, r.coin_pub);
|
||||||
if (!coin) {
|
if (!coin) {
|
||||||
console.warn("coin not found, can't apply refund");
|
logger.warn("coin not found, can't apply refund");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const denom = await tx.get(Stores.denominations, [
|
const denom = await tx.get(Stores.denominations, [
|
||||||
@ -202,13 +214,14 @@ async function storePendingRefund(
|
|||||||
async function storeFailedRefund(
|
async function storeFailedRefund(
|
||||||
tx: TransactionHandle,
|
tx: TransactionHandle,
|
||||||
p: PurchaseRecord,
|
p: PurchaseRecord,
|
||||||
|
refreshCoinsMap: Record<string, { coinPub: string }>,
|
||||||
r: MerchantCoinRefundFailureStatus,
|
r: MerchantCoinRefundFailureStatus,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const refundKey = getRefundKey(r);
|
const refundKey = getRefundKey(r);
|
||||||
|
|
||||||
const coin = await tx.get(Stores.coins, r.coin_pub);
|
const coin = await tx.get(Stores.coins, r.coin_pub);
|
||||||
if (!coin) {
|
if (!coin) {
|
||||||
console.warn("coin not found, can't apply refund");
|
logger.warn("coin not found, can't apply refund");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const denom = await tx.get(Stores.denominations, [
|
const denom = await tx.get(Stores.denominations, [
|
||||||
@ -247,6 +260,38 @@ async function storeFailedRefund(
|
|||||||
refundFee: denom.feeRefund,
|
refundFee: denom.feeRefund,
|
||||||
totalRefreshCostBound,
|
totalRefreshCostBound,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (p.abortStatus === AbortStatus.AbortRefund) {
|
||||||
|
// Refund failed because the merchant didn't even try to deposit
|
||||||
|
// the coin yet, so we try to refresh.
|
||||||
|
if (r.exchange_code === TalerErrorCode.REFUND_DEPOSIT_NOT_FOUND) {
|
||||||
|
const coin = await tx.get(Stores.coins, r.coin_pub);
|
||||||
|
if (!coin) {
|
||||||
|
logger.warn("coin not found, can't apply refund");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const denom = await tx.get(Stores.denominations, [
|
||||||
|
coin.exchangeBaseUrl,
|
||||||
|
coin.denomPubHash,
|
||||||
|
]);
|
||||||
|
if (!denom) {
|
||||||
|
logger.warn("denomination for coin missing");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let contrib: AmountJson | undefined;
|
||||||
|
for (let i = 0; i < p.payCoinSelection.coinPubs.length; i++) {
|
||||||
|
if (p.payCoinSelection.coinPubs[i] === r.coin_pub) {
|
||||||
|
contrib = p.payCoinSelection.coinContributions[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (contrib) {
|
||||||
|
coin.currentAmount = Amounts.add(coin.currentAmount, contrib).amount;
|
||||||
|
coin.currentAmount = Amounts.sub(coin.currentAmount, denom.feeRefund).amount;
|
||||||
|
}
|
||||||
|
refreshCoinsMap[coin.coinPub] = { coinPub: coin.coinPub };
|
||||||
|
await tx.put(Stores.coins, coin);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function acceptRefunds(
|
async function acceptRefunds(
|
||||||
@ -268,7 +313,7 @@ async function acceptRefunds(
|
|||||||
async (tx) => {
|
async (tx) => {
|
||||||
const p = await tx.get(Stores.purchases, proposalId);
|
const p = await tx.get(Stores.purchases, proposalId);
|
||||||
if (!p) {
|
if (!p) {
|
||||||
console.error("purchase not found, not adding refunds");
|
logger.error("purchase not found, not adding refunds");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -280,7 +325,7 @@ async function acceptRefunds(
|
|||||||
|
|
||||||
const isPermanentFailure =
|
const isPermanentFailure =
|
||||||
refundStatus.type === "failure" &&
|
refundStatus.type === "failure" &&
|
||||||
refundStatus.exchange_status === 410;
|
refundStatus.exchange_status >= 400 && refundStatus.exchange_status < 500 ;
|
||||||
|
|
||||||
// Already failed.
|
// Already failed.
|
||||||
if (existingRefundInfo?.type === RefundState.Failed) {
|
if (existingRefundInfo?.type === RefundState.Failed) {
|
||||||
@ -306,7 +351,7 @@ async function acceptRefunds(
|
|||||||
if (refundStatus.type === "success") {
|
if (refundStatus.type === "success") {
|
||||||
await applySuccessfulRefund(tx, p, refreshCoinsMap, refundStatus);
|
await applySuccessfulRefund(tx, p, refreshCoinsMap, refundStatus);
|
||||||
} else if (isPermanentFailure) {
|
} else if (isPermanentFailure) {
|
||||||
await storeFailedRefund(tx, p, refundStatus);
|
await storeFailedRefund(tx, p, refreshCoinsMap, refundStatus);
|
||||||
} else {
|
} else {
|
||||||
await storePendingRefund(tx, p, refundStatus);
|
await storePendingRefund(tx, p, refundStatus);
|
||||||
}
|
}
|
||||||
@ -326,7 +371,11 @@ async function acceptRefunds(
|
|||||||
// after a retry delay?
|
// after a retry delay?
|
||||||
let queryDone = true;
|
let queryDone = true;
|
||||||
|
|
||||||
if (p.autoRefundDeadline && p.autoRefundDeadline.t_ms > now.t_ms) {
|
if (
|
||||||
|
p.timestampFirstSuccessfulPay &&
|
||||||
|
p.autoRefundDeadline &&
|
||||||
|
p.autoRefundDeadline.t_ms > now.t_ms
|
||||||
|
) {
|
||||||
queryDone = false;
|
queryDone = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -347,7 +396,10 @@ async function acceptRefunds(
|
|||||||
p.timestampLastRefundStatus = now;
|
p.timestampLastRefundStatus = now;
|
||||||
p.lastRefundStatusError = undefined;
|
p.lastRefundStatusError = undefined;
|
||||||
p.refundStatusRetryInfo = initRetryInfo(false);
|
p.refundStatusRetryInfo = initRetryInfo(false);
|
||||||
p.refundStatusRequested = false;
|
p.refundQueryRequested = false;
|
||||||
|
if (p.abortStatus === AbortStatus.AbortRefund) {
|
||||||
|
p.abortStatus = AbortStatus.AbortFinished;
|
||||||
|
}
|
||||||
logger.trace("refund query done");
|
logger.trace("refund query done");
|
||||||
} else {
|
} else {
|
||||||
// No error, but we need to try again!
|
// No error, but we need to try again!
|
||||||
@ -415,7 +467,7 @@ export async function applyRefund(
|
|||||||
logger.error("no purchase found for refund URL");
|
logger.error("no purchase found for refund URL");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
p.refundStatusRequested = true;
|
p.refundQueryRequested = true;
|
||||||
p.lastRefundStatusError = undefined;
|
p.lastRefundStatusError = undefined;
|
||||||
p.refundStatusRetryInfo = initRetryInfo();
|
p.refundStatusRetryInfo = initRetryInfo();
|
||||||
await tx.put(Stores.purchases, p);
|
await tx.put(Stores.purchases, p);
|
||||||
@ -516,32 +568,121 @@ async function processPurchaseQueryRefundImpl(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!purchase.refundStatusRequested) {
|
if (!purchase.refundQueryRequested) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const requestUrl = new URL(
|
if (purchase.timestampFirstSuccessfulPay) {
|
||||||
`orders/${purchase.contractData.orderId}/refund`,
|
const requestUrl = new URL(
|
||||||
purchase.contractData.merchantBaseUrl,
|
`orders/${purchase.contractData.orderId}/refund`,
|
||||||
);
|
purchase.contractData.merchantBaseUrl,
|
||||||
|
);
|
||||||
|
|
||||||
logger.trace(`making refund request to ${requestUrl.href}`);
|
logger.trace(`making refund request to ${requestUrl.href}`);
|
||||||
|
|
||||||
const request = await ws.http.postJson(requestUrl.href, {
|
const request = await ws.http.postJson(requestUrl.href, {
|
||||||
h_contract: purchase.contractData.contractTermsHash,
|
h_contract: purchase.contractData.contractTermsHash,
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.trace("got json", JSON.stringify(await request.json(), undefined, 2));
|
logger.trace(
|
||||||
|
"got json",
|
||||||
|
JSON.stringify(await request.json(), undefined, 2),
|
||||||
|
);
|
||||||
|
|
||||||
const refundResponse = await readSuccessResponseJsonOrThrow(
|
const refundResponse = await readSuccessResponseJsonOrThrow(
|
||||||
request,
|
request,
|
||||||
codecForMerchantOrderRefundPickupResponse(),
|
codecForMerchantOrderRefundPickupResponse(),
|
||||||
);
|
);
|
||||||
|
|
||||||
await acceptRefunds(
|
await acceptRefunds(
|
||||||
ws,
|
ws,
|
||||||
proposalId,
|
proposalId,
|
||||||
refundResponse.refunds,
|
refundResponse.refunds,
|
||||||
RefundReason.NormalRefund,
|
RefundReason.NormalRefund,
|
||||||
);
|
);
|
||||||
|
} else if (purchase.abortStatus === AbortStatus.AbortRefund) {
|
||||||
|
const requestUrl = new URL(
|
||||||
|
`orders/${purchase.contractData.orderId}/abort`,
|
||||||
|
purchase.contractData.merchantBaseUrl,
|
||||||
|
);
|
||||||
|
|
||||||
|
const abortingCoins: AbortingCoin[] = [];
|
||||||
|
for (let i = 0; i < purchase.payCoinSelection.coinPubs.length; i++) {
|
||||||
|
const coinPub = purchase.payCoinSelection.coinPubs[i];
|
||||||
|
const coin = await ws.db.get(Stores.coins, coinPub);
|
||||||
|
checkDbInvariant(!!coin, "expected coin to be present");
|
||||||
|
abortingCoins.push({
|
||||||
|
coin_pub: coinPub,
|
||||||
|
contribution: Amounts.stringify(
|
||||||
|
purchase.payCoinSelection.coinContributions[i],
|
||||||
|
),
|
||||||
|
exchange_url: coin.exchangeBaseUrl,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const abortReq: AbortRequest = {
|
||||||
|
h_contract: purchase.contractData.contractTermsHash,
|
||||||
|
coins: abortingCoins,
|
||||||
|
};
|
||||||
|
|
||||||
|
logger.trace(`making order abort request to ${requestUrl.href}`);
|
||||||
|
|
||||||
|
const request = await ws.http.postJson(requestUrl.href, abortReq);
|
||||||
|
const abortResp = await readSuccessResponseJsonOrThrow(
|
||||||
|
request,
|
||||||
|
codecForAbortResponse(),
|
||||||
|
);
|
||||||
|
|
||||||
|
const refunds: MerchantCoinRefundStatus[] = [];
|
||||||
|
|
||||||
|
if (abortResp.refunds.length != abortingCoins.length) {
|
||||||
|
// FIXME: define error code!
|
||||||
|
throw Error("invalid order abort response");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < abortResp.refunds.length; i++) {
|
||||||
|
const r = abortResp.refunds[i];
|
||||||
|
refunds.push({
|
||||||
|
...r,
|
||||||
|
coin_pub: purchase.payCoinSelection.coinPubs[i],
|
||||||
|
refund_amount: Amounts.stringify(
|
||||||
|
purchase.payCoinSelection.coinContributions[i],
|
||||||
|
),
|
||||||
|
rtransaction_id: 0,
|
||||||
|
execution_time: timestampAddDuration(purchase.contractData.timestamp, {
|
||||||
|
d_ms: 1000,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await acceptRefunds(ws, proposalId, refunds, RefundReason.AbortRefund);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function abortFailedPayWithRefund(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
proposalId: string,
|
||||||
|
): Promise<void> {
|
||||||
|
await ws.db.runWithWriteTransaction([Stores.purchases], async (tx) => {
|
||||||
|
const purchase = await tx.get(Stores.purchases, proposalId);
|
||||||
|
if (!purchase) {
|
||||||
|
throw Error("purchase not found");
|
||||||
|
}
|
||||||
|
if (purchase.timestampFirstSuccessfulPay) {
|
||||||
|
// No point in aborting it. We don't even report an error.
|
||||||
|
logger.warn(`tried to abort successful payment`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (purchase.abortStatus !== AbortStatus.None) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
purchase.refundQueryRequested = true;
|
||||||
|
purchase.paymentSubmitPending = false;
|
||||||
|
purchase.abortStatus = AbortStatus.AbortRefund;
|
||||||
|
purchase.lastPayError = undefined;
|
||||||
|
purchase.payRetryInfo = initRetryInfo(false);
|
||||||
|
await tx.put(Stores.purchases, purchase);
|
||||||
|
});
|
||||||
|
processPurchaseQueryRefund(ws, proposalId, true).catch((e) => {
|
||||||
|
logger.trace(`error during refund processing after abort pay: ${e}`);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,7 @@ import {
|
|||||||
WalletRefundItem,
|
WalletRefundItem,
|
||||||
RefundState,
|
RefundState,
|
||||||
ReserveRecordStatus,
|
ReserveRecordStatus,
|
||||||
|
AbortStatus,
|
||||||
} from "../types/dbTypes";
|
} from "../types/dbTypes";
|
||||||
import { Amounts, AmountJson } from "../util/amounts";
|
import { Amounts, AmountJson } from "../util/amounts";
|
||||||
import { timestampCmp } from "../util/time";
|
import { timestampCmp } from "../util/time";
|
||||||
@ -242,7 +243,9 @@ export async function getTransactions(
|
|||||||
status: pr.timestampFirstSuccessfulPay
|
status: pr.timestampFirstSuccessfulPay
|
||||||
? PaymentStatus.Paid
|
? PaymentStatus.Paid
|
||||||
: PaymentStatus.Accepted,
|
: PaymentStatus.Accepted,
|
||||||
pending: !pr.timestampFirstSuccessfulPay,
|
pending:
|
||||||
|
!pr.timestampFirstSuccessfulPay &&
|
||||||
|
pr.abortStatus === AbortStatus.None,
|
||||||
timestamp: pr.timestampAccept,
|
timestamp: pr.timestampAccept,
|
||||||
transactionId: paymentTransactionId,
|
transactionId: paymentTransactionId,
|
||||||
info: info,
|
info: info,
|
||||||
@ -324,7 +327,10 @@ export async function getTransactions(
|
|||||||
amountRaw: Amounts.stringify(tipRecord.tipAmountRaw),
|
amountRaw: Amounts.stringify(tipRecord.tipAmountRaw),
|
||||||
pending: !tipRecord.pickedUpTimestamp,
|
pending: !tipRecord.pickedUpTimestamp,
|
||||||
timestamp: tipRecord.acceptedTimestamp,
|
timestamp: tipRecord.acceptedTimestamp,
|
||||||
transactionId: makeEventId(TransactionType.Tip, tipRecord.walletTipId),
|
transactionId: makeEventId(
|
||||||
|
TransactionType.Tip,
|
||||||
|
tipRecord.walletTipId,
|
||||||
|
),
|
||||||
error: tipRecord.lastError,
|
error: tipRecord.lastError,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -337,5 +343,5 @@ export async function getTransactions(
|
|||||||
txPending.sort((h1, h2) => timestampCmp(h1.timestamp, h2.timestamp));
|
txPending.sort((h1, h2) => timestampCmp(h1.timestamp, h2.timestamp));
|
||||||
txNotPending.sort((h1, h2) => timestampCmp(h1.timestamp, h2.timestamp));
|
txNotPending.sort((h1, h2) => timestampCmp(h1.timestamp, h2.timestamp));
|
||||||
|
|
||||||
return { transactions: [...txPending, ...txNotPending] };
|
return { transactions: [...txNotPending, ...txPending] };
|
||||||
}
|
}
|
||||||
|
@ -1285,6 +1285,12 @@ export interface PayCoinSelection {
|
|||||||
customerDepositFees: AmountJson;
|
customerDepositFees: AmountJson;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum AbortStatus {
|
||||||
|
None = "none",
|
||||||
|
AbortRefund = "abort-refund",
|
||||||
|
AbortFinished = "abort-finished",
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Record that stores status information about one purchase, starting from when
|
* Record that stores status information about one purchase, starting from when
|
||||||
* the customer accepts a proposal. Includes refund status if applicable.
|
* the customer accepts a proposal. Includes refund status if applicable.
|
||||||
@ -1352,17 +1358,9 @@ export interface PurchaseRecord {
|
|||||||
* Do we need to query the merchant for the refund status
|
* Do we need to query the merchant for the refund status
|
||||||
* of the payment?
|
* of the payment?
|
||||||
*/
|
*/
|
||||||
refundStatusRequested: boolean;
|
refundQueryRequested: boolean;
|
||||||
|
|
||||||
/**
|
abortStatus: AbortStatus;
|
||||||
* An abort (with refund) was requested for this (incomplete!) purchase.
|
|
||||||
*/
|
|
||||||
abortRequested: boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The abort (with refund) was completed for this (incomplete!) purchase.
|
|
||||||
*/
|
|
||||||
abortDone: boolean;
|
|
||||||
|
|
||||||
payRetryInfo: RetryInfo;
|
payRetryInfo: RetryInfo;
|
||||||
|
|
||||||
|
@ -1059,7 +1059,6 @@ export const codecForAuditorHandle = (): Codec<AuditorHandle> =>
|
|||||||
.property("url", codecForString())
|
.property("url", codecForString())
|
||||||
.build("AuditorHandle");
|
.build("AuditorHandle");
|
||||||
|
|
||||||
|
|
||||||
export const codecForLocation = (): Codec<Location> =>
|
export const codecForLocation = (): Codec<Location> =>
|
||||||
buildCodecForObject<Location>()
|
buildCodecForObject<Location>()
|
||||||
.property("country", codecOptional(codecForString()))
|
.property("country", codecOptional(codecForString()))
|
||||||
@ -1351,3 +1350,108 @@ export const codecForMerchantOrderStatusUnpaid = (): Codec<
|
|||||||
.property("taler_pay_uri", codecForString())
|
.property("taler_pay_uri", codecForString())
|
||||||
.property("already_paid_order_id", codecOptional(codecForString()))
|
.property("already_paid_order_id", codecOptional(codecForString()))
|
||||||
.build("MerchantOrderStatusUnpaid");
|
.build("MerchantOrderStatusUnpaid");
|
||||||
|
|
||||||
|
export interface AbortRequest {
|
||||||
|
// hash of the order's contract terms (this is used to authenticate the
|
||||||
|
// wallet/customer in case $ORDER_ID is guessable).
|
||||||
|
h_contract: string;
|
||||||
|
|
||||||
|
// List of coins the wallet would like to see refunds for.
|
||||||
|
// (Should be limited to the coins for which the original
|
||||||
|
// payment succeeded, as far as the wallet knows.)
|
||||||
|
coins: AbortingCoin[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AbortingCoin {
|
||||||
|
// Public key of a coin for which the wallet is requesting an abort-related refund.
|
||||||
|
coin_pub: EddsaPublicKeyString;
|
||||||
|
|
||||||
|
// The amount to be refunded (matches the original contribution)
|
||||||
|
contribution: AmountString;
|
||||||
|
|
||||||
|
// URL of the exchange this coin was withdrawn from.
|
||||||
|
exchange_url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AbortResponse {
|
||||||
|
// List of refund responses about the coins that the wallet
|
||||||
|
// requested an abort for. In the same order as the 'coins'
|
||||||
|
// from the original request.
|
||||||
|
// The rtransaction_id is implied to be 0.
|
||||||
|
refunds: MerchantAbortPayRefundStatus[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const codecForAbortResponse = (): Codec<AbortResponse> =>
|
||||||
|
buildCodecForObject<AbortResponse>()
|
||||||
|
.property("refunds", codecForList(codecForMerchantAbortPayRefundStatus()))
|
||||||
|
.build("AbortResponse");
|
||||||
|
|
||||||
|
export type MerchantAbortPayRefundStatus =
|
||||||
|
| MerchantAbortPayRefundSuccessStatus
|
||||||
|
| MerchantAbortPayRefundFailureStatus;
|
||||||
|
|
||||||
|
// Details about why a refund failed.
|
||||||
|
export interface MerchantAbortPayRefundFailureStatus {
|
||||||
|
// Used as tag for the sum type RefundStatus sum type.
|
||||||
|
type: "failure";
|
||||||
|
|
||||||
|
// HTTP status of the exchange request, must NOT be 200.
|
||||||
|
exchange_status: number;
|
||||||
|
|
||||||
|
// Taler error code from the exchange reply, if available.
|
||||||
|
exchange_code?: number;
|
||||||
|
|
||||||
|
// If available, HTTP reply from the exchange.
|
||||||
|
exchange_reply?: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Additional details needed to verify the refund confirmation signature
|
||||||
|
// (h_contract_terms and merchant_pub) are already known
|
||||||
|
// to the wallet and thus not included.
|
||||||
|
export interface MerchantAbortPayRefundSuccessStatus {
|
||||||
|
// Used as tag for the sum type MerchantCoinRefundStatus sum type.
|
||||||
|
type: "success";
|
||||||
|
|
||||||
|
// HTTP status of the exchange request, 200 (integer) required for refund confirmations.
|
||||||
|
exchange_status: 200;
|
||||||
|
|
||||||
|
// the EdDSA :ref:signature (binary-only) with purpose
|
||||||
|
// TALER_SIGNATURE_EXCHANGE_CONFIRM_REFUND using a current signing key of the
|
||||||
|
// exchange affirming the successful refund
|
||||||
|
exchange_sig: string;
|
||||||
|
|
||||||
|
// public EdDSA key of the exchange that was used to generate the signature.
|
||||||
|
// Should match one of the exchange's signing keys from /keys. It is given
|
||||||
|
// explicitly as the client might otherwise be confused by clock skew as to
|
||||||
|
// which signing key was used.
|
||||||
|
exchange_pub: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const codecForMerchantAbortPayRefundSuccessStatus = (): Codec<
|
||||||
|
MerchantAbortPayRefundSuccessStatus
|
||||||
|
> =>
|
||||||
|
buildCodecForObject<MerchantAbortPayRefundSuccessStatus>()
|
||||||
|
.property("exchange_pub", codecForString())
|
||||||
|
.property("exchange_sig", codecForString())
|
||||||
|
.property("exchange_status", codecForConstNumber(200))
|
||||||
|
.property("type", codecForConstString("success"))
|
||||||
|
.build("MerchantAbortPayRefundSuccessStatus");
|
||||||
|
|
||||||
|
export const codecForMerchantAbortPayRefundFailureStatus = (): Codec<
|
||||||
|
MerchantAbortPayRefundFailureStatus
|
||||||
|
> =>
|
||||||
|
buildCodecForObject<MerchantAbortPayRefundFailureStatus>()
|
||||||
|
.property("exchange_code", codecForNumber())
|
||||||
|
.property("exchange_reply", codecForAny())
|
||||||
|
.property("exchange_status", codecForNumber())
|
||||||
|
.property("type", codecForConstString("failure"))
|
||||||
|
.build("MerchantAbortPayRefundFailureStatus");
|
||||||
|
|
||||||
|
export const codecForMerchantAbortPayRefundStatus = (): Codec<
|
||||||
|
MerchantAbortPayRefundStatus
|
||||||
|
> =>
|
||||||
|
buildCodecForUnion<MerchantAbortPayRefundStatus>()
|
||||||
|
.discriminateOn("type")
|
||||||
|
.alternative("success", codecForMerchantAbortPayRefundSuccessStatus())
|
||||||
|
.alternative("failure", codecForMerchantAbortPayRefundFailureStatus())
|
||||||
|
.build("MerchantAbortPayRefundStatus");
|
||||||
|
@ -932,3 +932,11 @@ export const codecForAcceptTipRequest = (): Codec<AcceptTipRequest> =>
|
|||||||
.property("walletTipId", codecForString())
|
.property("walletTipId", codecForString())
|
||||||
.build("AcceptTipRequest");
|
.build("AcceptTipRequest");
|
||||||
|
|
||||||
|
export interface AbortPayWithRefundRequest {
|
||||||
|
proposalId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const codecForAbortPayWithRefundRequest = (): Codec<AbortPayWithRefundRequest> =>
|
||||||
|
buildCodecForObject<AbortPayWithRefundRequest>()
|
||||||
|
.property("proposalId", codecForString())
|
||||||
|
.build("AbortPayWithRefundRequest");
|
||||||
|
@ -94,6 +94,7 @@ import {
|
|||||||
PrepareTipResult,
|
PrepareTipResult,
|
||||||
codecForPrepareTipRequest,
|
codecForPrepareTipRequest,
|
||||||
codecForAcceptTipRequest,
|
codecForAcceptTipRequest,
|
||||||
|
codecForAbortPayWithRefundRequest,
|
||||||
} from "./types/walletTypes";
|
} from "./types/walletTypes";
|
||||||
import { Logger } from "./util/logging";
|
import { Logger } from "./util/logging";
|
||||||
|
|
||||||
@ -132,7 +133,7 @@ import {
|
|||||||
PendingOperationType,
|
PendingOperationType,
|
||||||
} from "./types/pending";
|
} from "./types/pending";
|
||||||
import { WalletNotification, NotificationType } from "./types/notifications";
|
import { WalletNotification, NotificationType } from "./types/notifications";
|
||||||
import { processPurchaseQueryRefund, applyRefund } from "./operations/refund";
|
import { processPurchaseQueryRefund, applyRefund, abortFailedPayWithRefund } from "./operations/refund";
|
||||||
import { durationMin, Duration } from "./util/time";
|
import { durationMin, Duration } from "./util/time";
|
||||||
import { processRecoupGroup } from "./operations/recoup";
|
import { processRecoupGroup } from "./operations/recoup";
|
||||||
import {
|
import {
|
||||||
@ -744,8 +745,8 @@ export class Wallet {
|
|||||||
return prepareTip(this.ws, talerTipUri);
|
return prepareTip(this.ws, talerTipUri);
|
||||||
}
|
}
|
||||||
|
|
||||||
async abortFailedPayment(contractTermsHash: string): Promise<void> {
|
async abortFailedPayWithRefund(proposalId: string): Promise<void> {
|
||||||
throw Error("not implemented");
|
return abortFailedPayWithRefund(this.ws, proposalId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1022,11 +1023,6 @@ export class Wallet {
|
|||||||
const req = codecForGetExchangeTosRequest().decode(payload);
|
const req = codecForGetExchangeTosRequest().decode(payload);
|
||||||
return this.getExchangeTos(req.exchangeBaseUrl);
|
return this.getExchangeTos(req.exchangeBaseUrl);
|
||||||
}
|
}
|
||||||
case "abortProposal": {
|
|
||||||
const req = codecForAbortProposalRequest().decode(payload);
|
|
||||||
await this.refuseProposal(req.proposalId);
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
case "retryPendingNow": {
|
case "retryPendingNow": {
|
||||||
await this.runPending(true);
|
await this.runPending(true);
|
||||||
return {};
|
return {};
|
||||||
@ -1039,6 +1035,11 @@ export class Wallet {
|
|||||||
const req = codecForConfirmPayRequest().decode(payload);
|
const req = codecForConfirmPayRequest().decode(payload);
|
||||||
return await this.confirmPay(req.proposalId, req.sessionId);
|
return await this.confirmPay(req.proposalId, req.sessionId);
|
||||||
}
|
}
|
||||||
|
case "abortFailedPayWithRefund": {
|
||||||
|
const req = codecForAbortPayWithRefundRequest().decode(payload);
|
||||||
|
await this.abortFailedPayWithRefund(req.proposalId);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
case "dumpCoins": {
|
case "dumpCoins": {
|
||||||
return await this.dumpCoins();
|
return await this.dumpCoins();
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user