aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorFlorian Dold <florian.dold@gmail.com>2019-12-05 22:17:01 +0100
committerFlorian Dold <florian.dold@gmail.com>2019-12-05 22:17:01 +0100
commit8115ac660cd9d12ef69ca80fc2e4cf8eec6b1ba1 (patch)
tree1190c1e16fb620d7812b1f26b03f20ed9615e795 /src
parentf67d7f54f9d0fed97446898942e3dfee67ee2985 (diff)
downloadwallet-core-8115ac660cd9d12ef69ca80fc2e4cf8eec6b1ba1.tar.xz
fix refunds
Diffstat (limited to 'src')
-rw-r--r--src/dbTypes.ts2
-rw-r--r--src/headless/merchant.ts3
-rw-r--r--src/headless/taler-wallet-cli.ts2
-rw-r--r--src/wallet-impl/errors.ts5
-rw-r--r--src/wallet-impl/pay.ts143
-rw-r--r--src/wallet-impl/pending.ts7
-rw-r--r--src/wallet-impl/withdraw.ts1
-rw-r--r--src/wallet.ts5
-rw-r--r--src/walletTypes.ts14
9 files changed, 125 insertions, 57 deletions
diff --git a/src/dbTypes.ts b/src/dbTypes.ts
index 16edbf31a..096c3f04e 100644
--- a/src/dbTypes.ts
+++ b/src/dbTypes.ts
@@ -980,7 +980,7 @@ export enum PurchaseStatus {
QueryRefund = "query-refund",
ProcessRefund = "process-refund",
Abort = "abort",
- Done = "done",
+ Dormant = "dormant",
}
/**
diff --git a/src/headless/merchant.ts b/src/headless/merchant.ts
index 1b9630732..5ce50cb53 100644
--- a/src/headless/merchant.ts
+++ b/src/headless/merchant.ts
@@ -89,12 +89,15 @@ export class MerchantBackendConnection {
summary: string,
fulfillmentUrl: string,
): Promise<{ orderId: string }> {
+ const t = Math.floor(new Date().getTime() / 1000) + 15 * 60;
const reqUrl = new URL("order", this.merchantBaseUrl).href;
const orderReq = {
order: {
amount,
summary,
fulfillment_url: fulfillmentUrl,
+ refund_deadline: `/Date(${t})/`,
+ wire_transfer_deadline: `/Date(${t})/`,
},
};
const resp = await axios({
diff --git a/src/headless/taler-wallet-cli.ts b/src/headless/taler-wallet-cli.ts
index 931cac087..71bccadef 100644
--- a/src/headless/taler-wallet-cli.ts
+++ b/src/headless/taler-wallet-cli.ts
@@ -137,7 +137,7 @@ async function withWallet<T>(
console.error("Operation failed: " + e.message);
console.log("Hint: check pending operations for details.");
} else {
- console.error("caught exception:", e);
+ console.error("caught unhandled exception (bug?):", e);
}
process.exit(1);
} finally {
diff --git a/src/wallet-impl/errors.ts b/src/wallet-impl/errors.ts
index 5df99b7d3..803497e66 100644
--- a/src/wallet-impl/errors.ts
+++ b/src/wallet-impl/errors.ts
@@ -52,8 +52,9 @@ export async function guardOperationException<T>(
onOpError: (e: OperationError) => Promise<void>,
): Promise<T> {
try {
- return op();
+ return await op();
} catch (e) {
+ console.log("guard: caught exception");
if (e instanceof OperationFailedAndReportedError) {
throw e;
}
@@ -62,6 +63,7 @@ export async function guardOperationException<T>(
throw new OperationFailedAndReportedError(e.message);
}
if (e instanceof Error) {
+ console.log("guard: caught Error");
await onOpError({
type: "exception",
message: e.message,
@@ -69,6 +71,7 @@ export async function guardOperationException<T>(
});
throw new OperationFailedAndReportedError(e.message);
}
+ console.log("guard: caught something else");
await onOpError({
type: "exception",
message: "non-error exception thrown",
diff --git a/src/wallet-impl/pay.ts b/src/wallet-impl/pay.ts
index 9b2da9c7d..f07b0328c 100644
--- a/src/wallet-impl/pay.ts
+++ b/src/wallet-impl/pay.ts
@@ -365,6 +365,8 @@ async function recordConfirmPay(
const p = await tx.get(Stores.proposals, proposal.proposalId);
if (p) {
p.proposalStatus = ProposalStatus.ACCEPTED;
+ p.lastError = undefined;
+ p.retryInfo = initRetryInfo(false);
await tx.put(Stores.proposals, p);
}
await tx.put(Stores.purchases, t);
@@ -467,6 +469,7 @@ async function incrementPurchaseRetry(
proposalId: string,
err: OperationError | undefined,
): Promise<void> {
+ console.log("incrementing purchase retry with error", err);
await runWithWriteTransaction(ws.db, [Stores.purchases], async tx => {
const pr = await tx.get(Stores.purchases, proposalId);
if (!pr) {
@@ -650,6 +653,8 @@ export async function submitPay(
throw Error("merchant payment signature invalid");
}
purchase.finished = true;
+ purchase.status = PurchaseStatus.Dormant;
+ purchase.lastError = undefined;
purchase.retryInfo = initRetryInfo(false);
const modifiedCoins: CoinRecord[] = [];
for (const pc of purchase.payReq.coins) {
@@ -992,6 +997,7 @@ async function submitRefundsToExchange(
}
const pendingKeys = Object.keys(purchase.refundsPending);
if (pendingKeys.length === 0) {
+ console.log("no pending refunds");
return;
}
for (const pk of pendingKeys) {
@@ -1010,50 +1016,52 @@ async function submitRefundsToExchange(
const exchangeUrl = purchase.payReq.coins[0].exchange_url;
const reqUrl = new URL("refund", exchangeUrl);
const resp = await ws.http.postJson(reqUrl.href, req);
+ console.log("sent refund permission");
if (resp.status !== 200) {
console.error("refund failed", resp);
continue;
}
- // Transactionally mark successful refunds as done
- const transformPurchase = (
- t: PurchaseRecord | undefined,
- ): PurchaseRecord | undefined => {
- if (!t) {
- console.warn("purchase not found, not updating refund");
- return;
- }
- if (t.refundsPending[pk]) {
- t.refundsDone[pk] = t.refundsPending[pk];
- delete t.refundsPending[pk];
- }
- return t;
- };
- const transformCoin = (
- c: CoinRecord | undefined,
- ): CoinRecord | undefined => {
- if (!c) {
- console.warn("coin not found, can't apply refund");
- return;
- }
- const refundAmount = Amounts.parseOrThrow(perm.refund_amount);
- const refundFee = Amounts.parseOrThrow(perm.refund_fee);
- c.status = CoinStatus.Dirty;
- c.currentAmount = Amounts.add(c.currentAmount, refundAmount).amount;
- c.currentAmount = Amounts.sub(c.currentAmount, refundFee).amount;
-
- return c;
- };
+ let allRefundsProcessed = false;
await runWithWriteTransaction(
ws.db,
[Stores.purchases, Stores.coins],
async tx => {
- await tx.mutate(Stores.purchases, proposalId, transformPurchase);
- await tx.mutate(Stores.coins, perm.coin_pub, transformCoin);
+ const p = await tx.get(Stores.purchases, proposalId);
+ if (!p) {
+ return;
+ }
+ if (p.refundsPending[pk]) {
+ p.refundsDone[pk] = p.refundsPending[pk];
+ delete p.refundsPending[pk];
+ }
+ if (Object.keys(p.refundsPending).length === 0) {
+ p.retryInfo = initRetryInfo();
+ p.lastError = undefined;
+ p.status = PurchaseStatus.Dormant;
+ allRefundsProcessed = true;
+ }
+ await tx.put(Stores.purchases, p);
+ const c = await tx.get(Stores.coins, perm.coin_pub);
+ if (!c) {
+ console.warn("coin not found, can't apply refund");
+ return;
+ }
+ const refundAmount = Amounts.parseOrThrow(perm.refund_amount);
+ const refundFee = Amounts.parseOrThrow(perm.refund_fee);
+ c.status = CoinStatus.Dirty;
+ c.currentAmount = Amounts.add(c.currentAmount, refundAmount).amount;
+ c.currentAmount = Amounts.sub(c.currentAmount, refundFee).amount;
+ await tx.put(Stores.coins, c);
},
);
- refresh(ws, perm.coin_pub);
+ if (allRefundsProcessed) {
+ ws.notify({
+ type: NotificationType.RefundFinished,
+ })
+ }
+ await refresh(ws, perm.coin_pub);
}
ws.notify({
@@ -1062,7 +1070,6 @@ async function submitRefundsToExchange(
});
}
-
async function acceptRefundResponse(
ws: InternalWalletState,
proposalId: string,
@@ -1086,6 +1093,8 @@ async function acceptRefundResponse(
t.lastRefundTimestamp = getTimestampNow();
t.status = PurchaseStatus.ProcessRefund;
+ t.lastError = undefined;
+ t.retryInfo = initRetryInfo();
for (const perm of refundPermissions) {
if (
@@ -1102,14 +1111,21 @@ async function acceptRefundResponse(
await submitRefundsToExchange(ws, proposalId);
}
-
-async function queryRefund(ws: InternalWalletState, proposalId: string): Promise<void> {
+async function queryRefund(
+ ws: InternalWalletState,
+ proposalId: string,
+): Promise<void> {
const purchase = await oneShotGet(ws.db, Stores.purchases, proposalId);
if (purchase?.status !== PurchaseStatus.QueryRefund) {
return;
}
- const refundUrl = new URL("refund", purchase.contractTerms.merchant_base_url).href
+ const refundUrlObj = new URL(
+ "refund",
+ purchase.contractTerms.merchant_base_url,
+ );
+ refundUrlObj.searchParams.set("order_id", purchase.contractTerms.order_id);
+ const refundUrl = refundUrlObj.href;
let resp;
try {
resp = await ws.http.get(refundUrl);
@@ -1122,22 +1138,45 @@ async function queryRefund(ws: InternalWalletState, proposalId: string): Promise
await acceptRefundResponse(ws, proposalId, refundResponse);
}
-async function startRefundQuery(ws: InternalWalletState, proposalId: string): Promise<void> {
- const success = await runWithWriteTransaction(ws.db, [Stores.purchases], async (tx) => {
- const p = await tx.get(Stores.purchases, proposalId);
- if (p?.status !== PurchaseStatus.Done) {
- return false;
- }
- p.status = PurchaseStatus.QueryRefund;
- return true;
- });
+async function startRefundQuery(
+ ws: InternalWalletState,
+ proposalId: string,
+): Promise<void> {
+ const success = await runWithWriteTransaction(
+ ws.db,
+ [Stores.purchases],
+ async tx => {
+ const p = await tx.get(Stores.purchases, proposalId);
+ if (!p) {
+ console.log("no purchase found for refund URL");
+ return false;
+ }
+ if (p.status === PurchaseStatus.QueryRefund) {
+ return true;
+ }
+ if (p.status === PurchaseStatus.ProcessRefund) {
+ return true;
+ }
+ if (p.status !== PurchaseStatus.Dormant) {
+ console.log(
+ `can't apply refund, as payment isn't done (status ${p.status})`,
+ );
+ return false;
+ }
+ p.lastError = undefined;
+ p.status = PurchaseStatus.QueryRefund;
+ p.retryInfo = initRetryInfo();
+ await tx.put(Stores.purchases, p);
+ return true;
+ },
+ );
if (!success) {
return;
}
- await queryRefund(ws, proposalId);
-}
+ await processPurchase(ws, proposalId);
+}
/**
* Accept a refund, return the contract hash for the contract
@@ -1149,6 +1188,8 @@ export async function applyRefund(
): Promise<string> {
const parseResult = parseRefundUri(talerRefundUri);
+ console.log("applying refund");
+
if (!parseResult) {
throw Error("invalid refund URI");
}
@@ -1163,6 +1204,7 @@ export async function applyRefund(
throw Error("no purchase for the taler://refund/ URI was found");
}
+ console.log("processing purchase for refund");
await startRefundQuery(ws, purchase.proposalId);
return purchase.contractTermsHash;
@@ -1180,7 +1222,7 @@ export async function processPurchase(
);
}
-export async function processPurchaseImpl(
+async function processPurchaseImpl(
ws: InternalWalletState,
proposalId: string,
): Promise<void> {
@@ -1188,8 +1230,9 @@ export async function processPurchaseImpl(
if (!purchase) {
return;
}
+ logger.trace(`processing purchase ${proposalId}`);
switch (purchase.status) {
- case PurchaseStatus.Done:
+ case PurchaseStatus.Dormant:
return;
case PurchaseStatus.Abort:
// FIXME
@@ -1200,7 +1243,9 @@ export async function processPurchaseImpl(
await queryRefund(ws, proposalId);
break;
case PurchaseStatus.ProcessRefund:
+ console.log("submitting refunds to exchange (toplvl)");
await submitRefundsToExchange(ws, proposalId);
+ console.log("after submitting refunds to exchange (toplvl)");
break;
default:
throw assertUnreachable(purchase.status);
diff --git a/src/wallet-impl/pending.ts b/src/wallet-impl/pending.ts
index bd10538af..c86ed6959 100644
--- a/src/wallet-impl/pending.ts
+++ b/src/wallet-impl/pending.ts
@@ -32,7 +32,9 @@ import {
ReserveRecordStatus,
CoinStatus,
ProposalStatus,
+ PurchaseStatus,
} from "../dbTypes";
+import { assertUnreachable } from "../util/assertUnreachable";
function updateRetryDelay(
oldDelay: Duration,
@@ -353,7 +355,7 @@ async function gatherPurchasePending(
onlyDue: boolean = false,
): Promise<void> {
await tx.iter(Stores.purchases).forEach((pr) => {
- if (pr.finished) {
+ if (pr.status === PurchaseStatus.Dormant) {
return;
}
resp.nextRetryDelay = updateRetryDelay(
@@ -369,6 +371,9 @@ async function gatherPurchasePending(
givesLifeness: true,
isReplay: false,
proposalId: pr.proposalId,
+ status: pr.status,
+ retryInfo: pr.retryInfo,
+ lastError: pr.lastError,
});
});
diff --git a/src/wallet-impl/withdraw.ts b/src/wallet-impl/withdraw.ts
index 7b7d0f640..3122a463c 100644
--- a/src/wallet-impl/withdraw.ts
+++ b/src/wallet-impl/withdraw.ts
@@ -282,6 +282,7 @@ async function processPlanchet(
}
if (numDone === ws.denoms.length) {
ws.finishTimestamp = getTimestampNow();
+ ws.lastError = undefined;
ws.retryInfo = initRetryInfo(false);
withdrawSessionFinished = true;
}
diff --git a/src/wallet.ts b/src/wallet.ts
index 86b3085f4..489bb2af8 100644
--- a/src/wallet.ts
+++ b/src/wallet.ts
@@ -49,7 +49,7 @@ import {
processDownloadProposal,
applyRefund,
getFullRefundFees,
- processPurchaseImpl,
+ processPurchase,
} from "./wallet-impl/pay";
import {
@@ -180,6 +180,7 @@ export class Wallet {
pending: PendingOperationInfo,
forceNow: boolean = false,
): Promise<void> {
+ console.log("running pending", pending);
switch (pending.type) {
case "bug":
// Nothing to do, will just be displayed to the user
@@ -209,7 +210,7 @@ export class Wallet {
await processTip(this.ws, pending.tipId);
break;
case "pay":
- await processPurchaseImpl(this.ws, pending.proposalId);
+ await processPurchase(this.ws, pending.proposalId);
break;
default:
assertUnreachable(pending);
diff --git a/src/walletTypes.ts b/src/walletTypes.ts
index d78fc8126..2413234eb 100644
--- a/src/walletTypes.ts
+++ b/src/walletTypes.ts
@@ -37,6 +37,7 @@ import {
ExchangeWireInfo,
WithdrawalSource,
RetryInfo,
+ PurchaseStatus,
} from "./dbTypes";
import { CoinPaySig, ContractTerms, PayReq } from "./talerTypes";
@@ -520,6 +521,7 @@ export const enum NotificationType {
ReserveDepleted = "reserve-depleted",
WithdrawSessionFinished = "withdraw-session-finished",
WaitingForRetry = "waiting-for-retry",
+ RefundFinished = "refund-finished",
}
export interface ProposalAcceptedNotification {
@@ -585,6 +587,10 @@ export interface WaitingForRetryNotification {
numGivingLiveness: number;
}
+export interface RefundFinishedNotification {
+ type: NotificationType.RefundFinished;
+}
+
export type WalletNotification =
| ProposalAcceptedNotification
| ProposalDownloadedNotification
@@ -599,7 +605,8 @@ export type WalletNotification =
| ReserveConfirmedNotification
| WithdrawSessionFinishedNotification
| ReserveDepletedNotification
- | WaitingForRetryNotification;
+ | WaitingForRetryNotification
+ | RefundFinishedNotification;
export interface OperationError {
type: string;
@@ -612,7 +619,7 @@ export interface PendingExchangeUpdateOperation {
stage: string;
reason: string;
exchangeBaseUrl: string;
- lastError?: OperationError;
+ lastError: OperationError | undefined;
}
export interface PendingBugOperation {
@@ -674,6 +681,9 @@ export interface PendingPayOperation {
type: "pay";
proposalId: string;
isReplay: boolean;
+ status: PurchaseStatus;
+ retryInfo: RetryInfo,
+ lastError: OperationError | undefined;
}
export interface PendingOperationInfoCommon {