gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[taler-wallet-core] branch master updated: fix refunds


From: gnunet
Subject: [taler-wallet-core] branch master updated: fix refunds
Date: Thu, 05 Dec 2019 22:17:09 +0100

This is an automated email from the git hooks/post-receive script.

dold pushed a commit to branch master
in repository wallet-core.

The following commit(s) were added to refs/heads/master by this push:
     new 8115ac66 fix refunds
8115ac66 is described below

commit 8115ac660cd9d12ef69ca80fc2e4cf8eec6b1ba1
Author: Florian Dold <address@hidden>
AuthorDate: Thu Dec 5 22:17:01 2019 +0100

    fix refunds
---
 src/dbTypes.ts                   |   2 +-
 src/headless/merchant.ts         |   3 +
 src/headless/taler-wallet-cli.ts |   2 +-
 src/wallet-impl/errors.ts        |   5 +-
 src/wallet-impl/pay.ts           | 143 +++++++++++++++++++++++++--------------
 src/wallet-impl/pending.ts       |   7 +-
 src/wallet-impl/withdraw.ts      |   1 +
 src/wallet.ts                    |   5 +-
 src/walletTypes.ts               |  14 +++-
 9 files changed, 125 insertions(+), 57 deletions(-)

diff --git a/src/dbTypes.ts b/src/dbTypes.ts
index 16edbf31..096c3f04 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 1b963073..5ce50cb5 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 931cac08..71bccade 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 5df99b7d..803497e6 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 9b2da9c7..f07b0328 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 bd10538a..c86ed695 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 7b7d0f64..3122a463 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 86b3085f..489bb2af 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 d78fc812..2413234e 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 {

-- 
To stop receiving notification emails like this one, please contact
address@hidden.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]