gnunet-svn
[Top][All Lists]
Advanced

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

[taler-wallet-core] branch master updated: oops, missing file


From: gnunet
Subject: [taler-wallet-core] branch master updated: oops, missing file
Date: Sun, 15 Dec 2019 19:08:16 +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 59bd755f oops, missing file
59bd755f is described below

commit 59bd755f7d6a3451859ca08084df83d465cd8500
Author: Florian Dold <address@hidden>
AuthorDate: Sun Dec 15 19:08:07 2019 +0100

    oops, missing file
---
 src/operations/pay.ts     |  56 +++---
 src/operations/pending.ts |  20 +-
 src/operations/refund.ts  | 502 ++++++++++++++++++++++++++++++++++++++++++++++
 src/wallet.ts             |   1 -
 4 files changed, 536 insertions(+), 43 deletions(-)

diff --git a/src/operations/pay.ts b/src/operations/pay.ts
index 388db94b..5ed29350 100644
--- a/src/operations/pay.ts
+++ b/src/operations/pay.ts
@@ -24,60 +24,54 @@
 /**
  * Imports.
  */
-import { AmountJson } from "../util/amounts";
+import { encodeCrock, getRandomBytes } from "../crypto/talerCrypto";
+import {
+  CoinRecord,
+  CoinStatus,
+  DenominationRecord,
+  initRetryInfo,
+  ProposalRecord,
+  ProposalStatus,
+  PurchaseRecord,
+  RefundReason,
+  Stores,
+  updateRetryInfoTimeout,
+} from "../types/dbTypes";
+import { NotificationType } from "../types/notifications";
 import {
   Auditor,
+  ContractTerms,
   ExchangeHandle,
   MerchantRefundResponse,
   PayReq,
   Proposal,
-  ContractTerms,
-  MerchantRefundPermission,
-  RefundRequest,
 } from "../types/talerTypes";
 import {
-  Timestamp,
   CoinSelectionResult,
   CoinWithDenom,
-  PayCoinInfo,
-  getTimestampNow,
-  PreparePayResult,
   ConfirmPayResult,
+  getTimestampNow,
   OperationError,
+  PayCoinInfo,
+  PreparePayResult,
   RefreshReason,
+  Timestamp,
 } from "../types/walletTypes";
-import {
-  Stores,
-  CoinStatus,
-  DenominationRecord,
-  ProposalRecord,
-  PurchaseRecord,
-  CoinRecord,
-  ProposalStatus,
-  initRetryInfo,
-  updateRetryInfoTimeout,
-  RefundReason,
-} from "../types/dbTypes";
 import * as Amounts from "../util/amounts";
+import { AmountJson } from "../util/amounts";
 import {
   amountToPretty,
-  strcmp,
   canonicalJson,
-  extractTalerStampOrThrow,
   extractTalerDuration,
+  extractTalerStampOrThrow,
+  strcmp,
 } from "../util/helpers";
 import { Logger } from "../util/logging";
-import { InternalWalletState } from "./state";
-import {
-  parsePayUri,
-  parseRefundUri,
-  getOrderDownloadUrl,
-} from "../util/taleruri";
-import { getTotalRefreshCost, createRefreshGroup } from "./refresh";
-import { encodeCrock, getRandomBytes } from "../crypto/talerCrypto";
+import { getOrderDownloadUrl, parsePayUri } from "../util/taleruri";
 import { guardOperationException } from "./errors";
-import { NotificationType } from "../types/notifications";
+import { createRefreshGroup, getTotalRefreshCost } from "./refresh";
 import { acceptRefundResponse } from "./refund";
+import { InternalWalletState } from "./state";
 
 export interface SpeculativePayData {
   payCoinInfo: PayCoinInfo;
diff --git a/src/operations/pending.ts b/src/operations/pending.ts
index f0b29792..b9b2c664 100644
--- a/src/operations/pending.ts
+++ b/src/operations/pending.ts
@@ -18,20 +18,18 @@
  * Imports.
  */
 import {
-  getTimestampNow,
-  Timestamp,
-  Duration,
-} from "../types/walletTypes";
-import { Database, TransactionHandle } from "../util/query";
-import { InternalWalletState } from "./state";
-import {
-  Stores,
   ExchangeUpdateStatus,
-  ReserveRecordStatus,
-  CoinStatus,
   ProposalStatus,
+  ReserveRecordStatus,
+  Stores,
 } from "../types/dbTypes";
-import { PendingOperationsResponse, PendingOperationType } from 
"../types/pending";
+import {
+  PendingOperationsResponse,
+  PendingOperationType,
+} from "../types/pending";
+import { Duration, getTimestampNow, Timestamp } from "../types/walletTypes";
+import { TransactionHandle } from "../util/query";
+import { InternalWalletState } from "./state";
 
 function updateRetryDelay(
   oldDelay: Duration,
diff --git a/src/operations/refund.ts b/src/operations/refund.ts
new file mode 100644
index 00000000..a2b4dbe2
--- /dev/null
+++ b/src/operations/refund.ts
@@ -0,0 +1,502 @@
+/*
+ This file is part of GNU Taler
+ (C) 2019 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Implementation of the refund operation.
+ *
+ * @author Florian Dold
+ */
+
+/**
+ * Imports.
+ */
+import { InternalWalletState } from "./state";
+import {
+  OperationError,
+  getTimestampNow,
+  RefreshReason,
+} from "../types/walletTypes";
+import {
+  Stores,
+  updateRetryInfoTimeout,
+  initRetryInfo,
+  CoinStatus,
+  RefundReason,
+  RefundEventRecord,
+} from "../types/dbTypes";
+import { NotificationType } from "../types/notifications";
+import { parseRefundUri } from "../util/taleruri";
+import { createRefreshGroup, getTotalRefreshCost } from "./refresh";
+import * as Amounts from "../util/amounts";
+import {
+  MerchantRefundPermission,
+  MerchantRefundResponse,
+  RefundRequest,
+} 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";
+
+async function incrementPurchaseQueryRefundRetry(
+  ws: InternalWalletState,
+  proposalId: string,
+  err: OperationError | undefined,
+): Promise<void> {
+  console.log("incrementing purchase refund query 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.refundStatusRetryInfo) {
+      return;
+    }
+    pr.refundStatusRetryInfo.retryCounter++;
+    updateRetryInfoTimeout(pr.refundStatusRetryInfo);
+    pr.lastRefundStatusError = err;
+    await tx.put(Stores.purchases, pr);
+  });
+  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.refundStatusRetryInfo);
+    pr.lastRefundApplyError = err;
+    await tx.put(Stores.purchases, pr);
+  });
+  ws.notify({ type: NotificationType.RefundApplyOperationError });
+}
+
+export async function getFullRefundFees(
+  ws: InternalWalletState,
+  refundPermissions: MerchantRefundPermission[],
+): Promise<AmountJson> {
+  if (refundPermissions.length === 0) {
+    throw Error("no refunds given");
+  }
+  const coin0 = await ws.db.get(Stores.coins, refundPermissions[0].coin_pub);
+  if (!coin0) {
+    throw Error("coin not found");
+  }
+  let feeAcc = Amounts.getZero(
+    Amounts.parseOrThrow(refundPermissions[0].refund_amount).currency,
+  );
+
+  const denoms = await ws.db
+    .iterIndex(Stores.denominations.exchangeBaseUrlIndex, 
coin0.exchangeBaseUrl)
+    .toArray();
+
+  for (const rp of refundPermissions) {
+    const coin = await ws.db.get(Stores.coins, rp.coin_pub);
+    if (!coin) {
+      throw Error("coin not found");
+    }
+    const denom = await ws.db.get(Stores.denominations, [
+      coin0.exchangeBaseUrl,
+      coin.denomPub,
+    ]);
+    if (!denom) {
+      throw Error(`denom not found (${coin.denomPub})`);
+    }
+    // FIXME:  this assumes that the refund already happened.
+    // When it hasn't, the refresh cost is inaccurate.  To fix this,
+    // we need introduce a flag to tell if a coin was refunded or
+    // refreshed normally (and what about incremental refunds?)
+    const refundAmount = Amounts.parseOrThrow(rp.refund_amount);
+    const refundFee = Amounts.parseOrThrow(rp.refund_fee);
+    const refreshCost = getTotalRefreshCost(
+      denoms,
+      denom,
+      Amounts.sub(refundAmount, refundFee).amount,
+    );
+    feeAcc = Amounts.add(feeAcc, refreshCost, refundFee).amount;
+  }
+  return feeAcc;
+}
+
+export async function acceptRefundResponse(
+  ws: InternalWalletState,
+  proposalId: string,
+  refundResponse: MerchantRefundResponse,
+  reason: RefundReason,
+): Promise<void> {
+  const refundPermissions = refundResponse.refund_permissions;
+
+  let numNewRefunds = 0;
+
+  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;
+    }
+
+    if (!p.refundStatusRequested) {
+      return;
+    }
+
+    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,
+          refundGroupId,
+        };
+        numNewRefunds++;
+      }
+    }
+
+    // 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
+      ) {
+        queryDone = false;
+      }
+    }
+
+    if (queryDone) {
+      p.lastRefundStatusTimestamp = 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.lastRefundStatusTimestamp = getTimestampNow();
+      p.refundStatusRetryInfo.retryCounter++;
+      updateRetryInfoTimeout(p.refundStatusRetryInfo);
+      p.lastRefundStatusError = undefined;
+      console.log("refund query not done");
+    }
+
+    if (numNewRefunds > 0) {
+      const now = getTimestampNow();
+      p.lastRefundApplyError = undefined;
+      p.refundApplyRetryInfo = initRetryInfo();
+      p.refundState.refundGroups.push({
+        timestampQueried: now,
+        reason,
+      });
+
+      const refundEvent: RefundEventRecord = {
+        proposalId,
+        refundGroupId,
+        timestamp: now,
+      };
+      await tx.put(Stores.refundEvents, refundEvent);
+    }
+
+    await tx.put(Stores.purchases, p);
+  });
+
+  ws.notify({
+    type: NotificationType.RefundQueried,
+  });
+  if (numNewRefunds > 0) {
+    await processPurchaseApplyRefund(ws, proposalId);
+  }
+}
+
+async function startRefundQuery(
+  ws: InternalWalletState,
+  proposalId: string,
+): Promise<void> {
+  const success = await ws.db.runWithWriteTransaction(
+    [Stores.purchases],
+    async tx => {
+      const p = await tx.get(Stores.purchases, proposalId);
+      if (!p) {
+        console.log("no purchase found for refund URL");
+        return false;
+      }
+      p.refundStatusRequested = true;
+      p.lastRefundStatusError = undefined;
+      p.refundStatusRetryInfo = initRetryInfo();
+      await tx.put(Stores.purchases, p);
+      return true;
+    },
+  );
+
+  if (!success) {
+    return;
+  }
+
+  ws.notify({
+    type: NotificationType.RefundStarted,
+  });
+
+  await processPurchaseQueryRefund(ws, proposalId);
+}
+
+/**
+ * Accept a refund, return the contract hash for the contract
+ * that was involved in the refund.
+ */
+export async function applyRefund(
+  ws: InternalWalletState,
+  talerRefundUri: string,
+): Promise<string> {
+  const parseResult = parseRefundUri(talerRefundUri);
+
+  console.log("applying refund");
+
+  if (!parseResult) {
+    throw Error("invalid refund URI");
+  }
+
+  const purchase = await ws.db.getIndexed(Stores.purchases.orderIdIndex, [
+    parseResult.merchantBaseUrl,
+    parseResult.orderId,
+  ]);
+
+  if (!purchase) {
+    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;
+}
+
+export async function processPurchaseQueryRefund(
+  ws: InternalWalletState,
+  proposalId: string,
+  forceNow: boolean = false,
+): Promise<void> {
+  const onOpErr = (e: OperationError) =>
+    incrementPurchaseQueryRefundRetry(ws, proposalId, e);
+  await guardOperationException(
+    () => processPurchaseQueryRefundImpl(ws, proposalId, forceNow),
+    onOpErr,
+  );
+}
+
+async function resetPurchaseQueryRefundRetry(
+  ws: InternalWalletState,
+  proposalId: string,
+) {
+  await ws.db.mutate(Stores.purchases, proposalId, x => {
+    if (x.refundStatusRetryInfo.active) {
+      x.refundStatusRetryInfo = initRetryInfo();
+    }
+    return x;
+  });
+}
+
+async function processPurchaseQueryRefundImpl(
+  ws: InternalWalletState,
+  proposalId: string,
+  forceNow: boolean,
+): Promise<void> {
+  if (forceNow) {
+    await resetPurchaseQueryRefundRetry(ws, proposalId);
+  }
+  const purchase = await ws.db.get(Stores.purchases, proposalId);
+  if (!purchase) {
+    return;
+  }
+  if (!purchase.refundStatusRequested) {
+    return;
+  }
+
+  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);
+  } catch (e) {
+    console.error("error downloading refund permission", e);
+    throw e;
+  }
+  if (resp.status !== 200) {
+    throw Error(`unexpected status code (${resp.status}) for /refund`);
+  }
+
+  const refundResponse = MerchantRefundResponse.checked(await resp.json());
+  await acceptRefundResponse(
+    ws,
+    proposalId,
+    refundResponse,
+    RefundReason.NormalRefund,
+  );
+}
+
+export async function processPurchaseApplyRefund(
+  ws: InternalWalletState,
+  proposalId: string,
+  forceNow: boolean = false,
+): Promise<void> {
+  const onOpErr = (e: OperationError) =>
+    incrementPurchaseApplyRefundRetry(ws, proposalId, e);
+  await guardOperationException(
+    () => processPurchaseApplyRefundImpl(ws, proposalId, forceNow),
+    onOpErr,
+  );
+}
+
+async function resetPurchaseApplyRefundRetry(
+  ws: InternalWalletState,
+  proposalId: string,
+) {
+  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;
+  }
+  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.contractTermsHash,
+      merchant_pub: purchase.contractTerms.merchant_pub,
+      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("refund", exchangeUrl);
+    const resp = await ws.http.postJson(reqUrl.href, req);
+    console.log("sent refund permission");
+    let refundGone = false;
+    switch (resp.status) {
+      case HttpResponseStatus.Ok:
+        break;
+      case HttpResponseStatus.Gone:
+        // We're too late, refund is expired.
+        refundGone = true;
+        break;
+      default:
+        let body: string | null = null;
+        try {
+          body = await resp.json();
+        } catch {}
+        const m = "refund request (at exchange) failed";
+        throw new OperationFailedError(m, {
+          message: m,
+          type: "network",
+          details: {
+            body,
+          },
+        });
+    }
+
+    let allRefundsProcessed = false;
+
+    await ws.db.runWithWriteTransaction(
+      [Stores.purchases, Stores.coins, Stores.refreshGroups],
+      async tx => {
+        const p = await tx.get(Stores.purchases, proposalId);
+        if (!p) {
+          return;
+        }
+        if (p.refundState.refundsPending[pk]) {
+          if (refundGone) {
+            p.refundState.refundsFailed[pk] = p.refundState.refundsPending[pk];
+          } else {
+            p.refundState.refundsDone[pk] = p.refundState.refundsPending[pk];
+          }
+          delete p.refundState.refundsPending[pk];
+        }
+        if (Object.keys(p.refundState.refundsPending).length === 0) {
+          p.refundStatusRetryInfo = initRetryInfo();
+          p.lastRefundStatusError = undefined;
+          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.Dormant;
+        c.currentAmount = Amounts.add(c.currentAmount, refundAmount).amount;
+        c.currentAmount = Amounts.sub(c.currentAmount, refundFee).amount;
+        await tx.put(Stores.coins, c);
+        await createRefreshGroup(
+          tx,
+          [{ coinPub: perm.coin_pub }],
+          RefreshReason.Refund,
+        );
+      },
+    );
+    if (allRefundsProcessed) {
+      ws.notify({
+        type: NotificationType.RefundFinished,
+      });
+    }
+  }
+
+  ws.notify({
+    type: NotificationType.RefundsSubmitted,
+    proposalId,
+  });
+}
diff --git a/src/wallet.ts b/src/wallet.ts
index a4e20107..aca8a18a 100644
--- a/src/wallet.ts
+++ b/src/wallet.ts
@@ -47,7 +47,6 @@ import {
 
 import {
   CoinRecord,
-  CoinStatus,
   CurrencyRecord,
   DenominationRecord,
   ExchangeRecord,

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



reply via email to

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