gnunet-svn
[Top][All Lists]
Advanced

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

[taler-wallet-core] branch master updated: towards refunds with updated


From: gnunet
Subject: [taler-wallet-core] branch master updated: towards refunds with updated protocol
Date: Thu, 23 Jul 2020 14:05:38 +0200

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 d88829cf towards refunds with updated protocol
d88829cf is described below

commit d88829cfa8dc7bf2967fb494af0290e068466828
Author: Florian Dold <florian.dold@gmail.com>
AuthorDate: Thu Jul 23 17:35:17 2020 +0530

    towards refunds with updated protocol
---
 src/operations/history.ts      | 120 +++++++--------
 src/operations/pay.ts          |   9 +-
 src/operations/recoup.ts       |   1 +
 src/operations/refresh.ts      |  12 ++
 src/operations/refund.ts       | 340 +++++++++++++++++++----------------------
 src/operations/transactions.ts | 121 ++++-----------
 src/types/dbTypes.ts           |  76 +++++----
 src/types/talerTypes.ts        | 185 +++++++++++++++++++++-
 src/util/codec.ts              |  56 +++++++
 src/util/taleruri.ts           |   2 +-
 src/wallet.ts                  |  27 ++--
 11 files changed, 563 insertions(+), 386 deletions(-)

diff --git a/src/operations/history.ts b/src/operations/history.ts
index 9cbbd516..8fff4f88 100644
--- a/src/operations/history.ts
+++ b/src/operations/history.ts
@@ -421,66 +421,66 @@ export async function getHistory(
         }
       });
 
-      tx.iter(Stores.refundEvents).forEachAsync(async (re) => {
-        const proposal = await tx.get(Stores.proposals, re.proposalId);
-        if (!proposal) {
-          return;
-        }
-        const purchase = await tx.get(Stores.purchases, re.proposalId);
-        if (!purchase) {
-          return;
-        }
-        const orderShortInfo = getOrderShortInfo(proposal);
-        if (!orderShortInfo) {
-          return;
-        }
-        const purchaseAmount = purchase.contractData.amount;
-        let amountRefundedRaw = Amounts.getZero(purchaseAmount.currency);
-        let amountRefundedInvalid = Amounts.getZero(purchaseAmount.currency);
-        let amountRefundedEffective = Amounts.getZero(purchaseAmount.currency);
-        Object.keys(purchase.refundsDone).forEach((x, i) => {
-          const r = purchase.refundsDone[x];
-          if (r.refundGroupId !== re.refundGroupId) {
-            return;
-          }
-          const refundAmount = Amounts.parseOrThrow(r.perm.refund_amount);
-          const refundFee = Amounts.parseOrThrow(r.perm.refund_fee);
-          amountRefundedRaw = Amounts.add(amountRefundedRaw, refundAmount)
-            .amount;
-          amountRefundedEffective = Amounts.add(
-            amountRefundedEffective,
-            refundAmount,
-          ).amount;
-          amountRefundedEffective = Amounts.sub(
-            amountRefundedEffective,
-            refundFee,
-          ).amount;
-        });
-        Object.keys(purchase.refundsFailed).forEach((x, i) => {
-          const r = purchase.refundsFailed[x];
-          if (r.refundGroupId !== re.refundGroupId) {
-            return;
-          }
-          const ra = Amounts.parseOrThrow(r.perm.refund_amount);
-          const refundFee = Amounts.parseOrThrow(r.perm.refund_fee);
-          amountRefundedRaw = Amounts.add(amountRefundedRaw, ra).amount;
-          amountRefundedInvalid = Amounts.add(amountRefundedInvalid, 
ra).amount;
-          amountRefundedEffective = Amounts.sub(
-            amountRefundedEffective,
-            refundFee,
-          ).amount;
-        });
-        history.push({
-          type: HistoryEventType.Refund,
-          eventId: makeEventId(HistoryEventType.Refund, re.refundGroupId),
-          refundGroupId: re.refundGroupId,
-          orderShortInfo,
-          timestamp: re.timestamp,
-          amountRefundedEffective: Amounts.stringify(amountRefundedEffective),
-          amountRefundedRaw: Amounts.stringify(amountRefundedRaw),
-          amountRefundedInvalid: Amounts.stringify(amountRefundedInvalid),
-        });
-      });
+      // tx.iter(Stores.refundEvents).forEachAsync(async (re) => {
+      //   const proposal = await tx.get(Stores.proposals, re.proposalId);
+      //   if (!proposal) {
+      //     return;
+      //   }
+      //   const purchase = await tx.get(Stores.purchases, re.proposalId);
+      //   if (!purchase) {
+      //     return;
+      //   }
+      //   const orderShortInfo = getOrderShortInfo(proposal);
+      //   if (!orderShortInfo) {
+      //     return;
+      //   }
+      //   const purchaseAmount = purchase.contractData.amount;
+      //   let amountRefundedRaw = Amounts.getZero(purchaseAmount.currency);
+      //   let amountRefundedInvalid = 
Amounts.getZero(purchaseAmount.currency);
+      //   let amountRefundedEffective = 
Amounts.getZero(purchaseAmount.currency);
+      //   Object.keys(purchase.refundsDone).forEach((x, i) => {
+      //     const r = purchase.refundsDone[x];
+      //     if (r.refundGroupId !== re.refundGroupId) {
+      //       return;
+      //     }
+      //     const refundAmount = Amounts.parseOrThrow(r.perm.refund_amount);
+      //     const refundFee = Amounts.parseOrThrow(r.perm.refund_fee);
+      //     amountRefundedRaw = Amounts.add(amountRefundedRaw, refundAmount)
+      //       .amount;
+      //     amountRefundedEffective = Amounts.add(
+      //       amountRefundedEffective,
+      //       refundAmount,
+      //     ).amount;
+      //     amountRefundedEffective = Amounts.sub(
+      //       amountRefundedEffective,
+      //       refundFee,
+      //     ).amount;
+      //   });
+      //   Object.keys(purchase.refundsFailed).forEach((x, i) => {
+      //     const r = purchase.refundsFailed[x];
+      //     if (r.refundGroupId !== re.refundGroupId) {
+      //       return;
+      //     }
+      //     const ra = Amounts.parseOrThrow(r.perm.refund_amount);
+      //     const refundFee = Amounts.parseOrThrow(r.perm.refund_fee);
+      //     amountRefundedRaw = Amounts.add(amountRefundedRaw, ra).amount;
+      //     amountRefundedInvalid = Amounts.add(amountRefundedInvalid, 
ra).amount;
+      //     amountRefundedEffective = Amounts.sub(
+      //       amountRefundedEffective,
+      //       refundFee,
+      //     ).amount;
+      //   });
+      //   history.push({
+      //     type: HistoryEventType.Refund,
+      //     eventId: makeEventId(HistoryEventType.Refund, re.refundGroupId),
+      //     refundGroupId: re.refundGroupId,
+      //     orderShortInfo,
+      //     timestamp: re.timestamp,
+      //     amountRefundedEffective: 
Amounts.stringify(amountRefundedEffective),
+      //     amountRefundedRaw: Amounts.stringify(amountRefundedRaw),
+      //     amountRefundedInvalid: Amounts.stringify(amountRefundedInvalid),
+      //   });
+      // });
 
       tx.iter(Stores.recoupGroups).forEach((rg) => {
         if (rg.timestampFinished) {
diff --git a/src/operations/pay.ts b/src/operations/pay.ts
index 29b69783..0027bf0f 100644
--- a/src/operations/pay.ts
+++ b/src/operations/pay.ts
@@ -59,7 +59,6 @@ import { InternalWalletState } from "./state";
 import { getTimestampNow, timestampAddDuration } from "../util/time";
 import { strcmp, canonicalJson } from "../util/helpers";
 import {
-  readSuccessResponseJsonOrErrorCode,
   readSuccessResponseJsonOrThrow,
 } from "../util/http";
 
@@ -455,11 +454,7 @@ async function recordConfirmPay(
     timestampFirstSuccessfulPay: undefined,
     autoRefundDeadline: undefined,
     paymentSubmitPending: true,
-    refundGroups: [],
-    refundsDone: {},
-    refundsFailed: {},
-    refundsPending: {},
-    refundsRefreshCost: {},
+    refunds: {},
   };
 
   await ws.db.runWithWriteTransaction(
@@ -492,7 +487,7 @@ async function recordConfirmPay(
       const refreshCoinPubs = coinSelection.coinPubs.map((x) => ({
         coinPub: x,
       }));
-      await createRefreshGroup(tx, refreshCoinPubs, RefreshReason.Pay);
+      await createRefreshGroup(ws, tx, refreshCoinPubs, RefreshReason.Pay);
     },
   );
 
diff --git a/src/operations/recoup.ts b/src/operations/recoup.ts
index 445d029c..e5f14c6e 100644
--- a/src/operations/recoup.ts
+++ b/src/operations/recoup.ts
@@ -96,6 +96,7 @@ async function putGroupAsFinished(
     recoupGroup.lastError = undefined;
     if (recoupGroup.scheduleRefreshCoins.length > 0) {
       const refreshGroupId = await createRefreshGroup(
+        ws,
         tx,
         recoupGroup.scheduleRefreshCoins.map((x) => ({ coinPub: x })),
         RefreshReason.Recoup,
diff --git a/src/operations/refresh.ts b/src/operations/refresh.ts
index 4d477d64..74b032b9 100644
--- a/src/operations/refresh.ts
+++ b/src/operations/refresh.ts
@@ -535,6 +535,7 @@ async function processRefreshSession(
  * Create a refresh group for a list of coins.
  */
 export async function createRefreshGroup(
+  ws: InternalWalletState,
   tx: TransactionHandle,
   oldCoinPubs: CoinPublicKey[],
   reason: RefreshReason,
@@ -554,6 +555,17 @@ export async function createRefreshGroup(
   };
 
   await tx.put(Stores.refreshGroups, refreshGroup);
+
+  const processAsync = async (): Promise<void> => {
+    try {
+      await processRefreshGroup(ws, refreshGroupId);
+    } catch (e) {
+      logger.trace(`Error during refresh: ${e}`)
+    }
+  };
+
+  processAsync();
+
   return {
     refreshGroupId,
   };
diff --git a/src/operations/refund.ts b/src/operations/refund.ts
index 1d6561bd..af3325cf 100644
--- a/src/operations/refund.ts
+++ b/src/operations/refund.ts
@@ -36,23 +36,24 @@ import {
   CoinStatus,
   RefundReason,
   RefundEventRecord,
+  RefundState,
+  PurchaseRecord,
 } from "../types/dbTypes";
 import { NotificationType } from "../types/notifications";
 import { parseRefundUri } from "../util/taleruri";
 import { createRefreshGroup, getTotalRefreshCost } from "./refresh";
 import { Amounts } from "../util/amounts";
 import {
-  MerchantRefundDetails,
-  MerchantRefundResponse,
-  codecForMerchantRefundResponse,
+  codecForMerchantOrderStatus,
+  MerchantCoinRefundStatus,
+  MerchantCoinRefundSuccessStatus,
+  MerchantCoinRefundFailureStatus,
 } from "../types/talerTypes";
-import { AmountJson } from "../util/amounts";
 import { guardOperationException } from "./errors";
-import { randomBytes } from "../crypto/primitives/nacl-fast";
-import { encodeCrock } from "../crypto/talerCrypto";
 import { getTimestampNow } from "../util/time";
 import { Logger } from "../util/logging";
 import { readSuccessResponseJsonOrThrow } from "../util/http";
+import { TransactionHandle } from "../util/query";
 
 const logger = new Logger("refund.ts");
 
@@ -85,80 +86,122 @@ async function incrementPurchaseQueryRefundRetry(
   }
 }
 
-function getRefundKey(d: MerchantRefundDetails): string {
+function getRefundKey(d: MerchantCoinRefundStatus): string {
   return `${d.coin_pub}-${d.rtransaction_id}`;
 }
 
-async function acceptRefundResponse(
-  ws: InternalWalletState,
-  proposalId: string,
-  refundResponse: MerchantRefundResponse,
-  reason: RefundReason,
+async function applySuccessfulRefund(
+  tx: TransactionHandle,
+  p: PurchaseRecord,
+  refreshCoinsMap: Record<string, { coinPub: string }>,
+  r: MerchantCoinRefundSuccessStatus,
 ): Promise<void> {
-  const refunds = refundResponse.refunds;
-
-  const refundGroupId = encodeCrock(randomBytes(32));
+  // FIXME: check signature before storing it as valid!
 
-  let numNewRefunds = 0;
-
-  const finishedRefunds: MerchantRefundDetails[] = [];
-  const unfinishedRefunds: MerchantRefundDetails[] = [];
-  const failedRefunds: MerchantRefundDetails[] = [];
+  const refundKey = getRefundKey(r);
+  const coin = await tx.get(Stores.coins, r.coin_pub);
+  if (!coin) {
+    console.warn("coin not found, can't apply refund");
+    return;
+  }
+  const denom = await tx.getIndexed(
+    Stores.denominations.denomPubHashIndex,
+    coin.denomPubHash,
+  );
+  if (!denom) {
+    throw Error("inconsistent database");
+  }
+  refreshCoinsMap[coin.coinPub] = { coinPub: coin.coinPub };
+  const refundAmount = Amounts.parseOrThrow(r.refund_amount);
+  const refundFee = denom.feeRefund;
+  coin.status = CoinStatus.Dormant;
+  coin.currentAmount = Amounts.add(coin.currentAmount, refundAmount).amount;
+  coin.currentAmount = Amounts.sub(coin.currentAmount, refundFee).amount;
+  logger.trace(`coin amount after is 
${Amounts.stringify(coin.currentAmount)}`);
+  await tx.put(Stores.coins, coin);
+
+  const allDenoms = await tx
+    .iterIndexed(Stores.denominations.exchangeBaseUrlIndex, 
coin.exchangeBaseUrl)
+    .toArray();
+
+  const amountLeft = Amounts.sub(
+    Amounts.add(coin.currentAmount, Amounts.parseOrThrow(r.refund_amount))
+      .amount,
+    denom.feeRefund,
+  ).amount;
+
+  const totalRefreshCostBound = getTotalRefreshCost(
+    allDenoms,
+    denom,
+    amountLeft,
+  );
 
-  console.log("handling refund response", refundResponse);
+  p.refunds[refundKey] = {
+    type: RefundState.Applied,
+    executionTime: r.execution_time,
+    refundAmount: Amounts.parseOrThrow(r.refund_amount),
+    refundFee: denom.feeRefund,
+    totalRefreshCostBound,
+  };
+}
 
-  const refundsRefreshCost: { [refundKey: string]: AmountJson } = {};
+async function storePendingRefund(
+  tx: TransactionHandle,
+  p: PurchaseRecord,
+  r: MerchantCoinRefundFailureStatus,
+): Promise<void> {
+  const refundKey = getRefundKey(r);
 
-  for (const rd of refunds) {
-    logger.trace(
-      `Refund ${rd.rtransaction_id} has HTTP status 
${rd.exchange_http_status}`,
-    );
-    if (rd.exchange_http_status === 200) {
-      // FIXME: also verify signature if necessary.
-      finishedRefunds.push(rd);
-    } else if (
-      rd.exchange_http_status >= 400 &&
-      rd.exchange_http_status < 400
-    ) {
-      failedRefunds.push(rd);
-    } else {
-      unfinishedRefunds.push(rd);
-    }
+  const coin = await tx.get(Stores.coins, r.coin_pub);
+  if (!coin) {
+    console.warn("coin not found, can't apply refund");
+    return;
   }
+  const denom = await tx.getIndexed(
+    Stores.denominations.denomPubHashIndex,
+    coin.denomPubHash,
+  );
 
-  // Compute cost.
-  // FIXME: Optimize, don't always recompute.
-  for (const rd of [...finishedRefunds, ...unfinishedRefunds]) {
-    const key = getRefundKey(rd);
-    const coin = await ws.db.get(Stores.coins, rd.coin_pub);
-    if (!coin) {
-      continue;
-    }
-    const denom = await ws.db.getIndexed(
-      Stores.denominations.denomPubHashIndex,
-      coin.denomPubHash,
-    );
-    if (!denom) {
-      throw Error("inconsistent database");
-    }
-    const amountLeft = Amounts.sub(
-      Amounts.add(coin.currentAmount, Amounts.parseOrThrow(rd.refund_amount))
-        .amount,
-      Amounts.parseOrThrow(rd.refund_fee),
-    ).amount;
-    const allDenoms = await ws.db
-      .iterIndex(
-        Stores.denominations.exchangeBaseUrlIndex,
-        coin.exchangeBaseUrl,
-      )
-      .toArray();
-    refundsRefreshCost[key] = getTotalRefreshCost(allDenoms, denom, 
amountLeft);
+  if (!denom) {
+    throw Error("inconsistent database");
   }
 
+  const allDenoms = await tx
+    .iterIndexed(Stores.denominations.exchangeBaseUrlIndex, 
coin.exchangeBaseUrl)
+    .toArray();
+
+  const amountLeft = Amounts.sub(
+    Amounts.add(coin.currentAmount, Amounts.parseOrThrow(r.refund_amount))
+      .amount,
+    denom.feeRefund,
+  ).amount;
+
+  const totalRefreshCostBound = getTotalRefreshCost(
+    allDenoms,
+    denom,
+    amountLeft,
+  );
+
+  p.refunds[refundKey] = {
+    type: RefundState.Pending,
+    executionTime: r.execution_time,
+    refundAmount: Amounts.parseOrThrow(r.refund_amount),
+    refundFee: denom.feeRefund,
+    totalRefreshCostBound,
+  };
+}
+
+async function acceptRefunds(
+  ws: InternalWalletState,
+  proposalId: string,
+  refunds: MerchantCoinRefundStatus[],
+  reason: RefundReason,
+): Promise<void> {
+  console.log("handling refunds", refunds);
   const now = getTimestampNow();
 
   await ws.db.runWithWriteTransaction(
-    [Stores.purchases, Stores.coins, Stores.refreshGroups, 
Stores.refundEvents],
+    [Stores.purchases, Stores.coins, Stores.denominations, 
Stores.refreshGroups, Stores.refundEvents],
     async (tx) => {
       const p = await tx.get(Stores.purchases, proposalId);
       if (!p) {
@@ -166,103 +209,60 @@ async function acceptRefundResponse(
         return;
       }
 
-      // Groups that newly failed/succeeded
-      const changedGroups: { [refundGroupId: string]: boolean } = {};
+      const refreshCoinsMap: Record<string, CoinPublicKey> = {};
 
-      for (const rd of failedRefunds) {
-        const refundKey = getRefundKey(rd);
-        if (p.refundsFailed[refundKey]) {
+      for (const refundStatus of refunds) {
+        const refundKey = getRefundKey(refundStatus);
+        const existingRefundInfo = p.refunds[refundKey];
+
+        // Already failed.
+        if (existingRefundInfo?.type === RefundState.Failed) {
           continue;
         }
-        if (!p.refundsFailed[refundKey]) {
-          p.refundsFailed[refundKey] = {
-            perm: rd,
-            refundGroupId,
-          };
-          numNewRefunds++;
-          changedGroups[refundGroupId] = true;
-        }
-        const oldPending = p.refundsPending[refundKey];
-        if (oldPending) {
-          delete p.refundsPending[refundKey];
-          changedGroups[oldPending.refundGroupId] = true;
-        }
-      }
 
-      for (const rd of unfinishedRefunds) {
-        const refundKey = getRefundKey(rd);
-        if (!p.refundsPending[refundKey]) {
-          p.refundsPending[refundKey] = {
-            perm: rd,
-            refundGroupId,
-          };
-          numNewRefunds++;
+        // Already applied.
+        if (existingRefundInfo?.type === RefundState.Applied) {
+          continue;
         }
-      }
 
-      // Avoid duplicates
-      const refreshCoinsMap: { [coinPub: string]: CoinPublicKey } = {};
-
-      for (const rd of finishedRefunds) {
-        const refundKey = getRefundKey(rd);
-        if (p.refundsDone[refundKey]) {
+        // Still pending.
+        if (
+          refundStatus.success === false &&
+          existingRefundInfo?.type === RefundState.Pending
+        ) {
           continue;
         }
-        p.refundsDone[refundKey] = {
-          perm: rd,
-          refundGroupId,
-        };
-        const oldPending = p.refundsPending[refundKey];
-        if (oldPending) {
-          delete p.refundsPending[refundKey];
-          changedGroups[oldPending.refundGroupId] = true;
-        } else {
-          numNewRefunds++;
-        }
 
-        const c = await tx.get(Stores.coins, rd.coin_pub);
+        // Invariant: (!existingRefundInfo) || (existingRefundInfo === Pending)
 
-        if (!c) {
-          console.warn("coin not found, can't apply refund");
-          return;
+        if (refundStatus.success === true) {
+          await applySuccessfulRefund(tx, p, refreshCoinsMap, refundStatus);
+        } else {
+          await storePendingRefund(tx, p, refundStatus);
         }
-        refreshCoinsMap[c.coinPub] = { coinPub: c.coinPub };
-        logger.trace(`commiting refund ${refundKey} to coin ${c.coinPub}`);
-        logger.trace(
-          `coin amount before is ${Amounts.stringify(c.currentAmount)}`,
-        );
-        logger.trace(`refund amount (via merchant) is ${refundKey}`);
-        logger.trace(`refund fee (via merchant) is ${refundKey}`);
-        const refundAmount = Amounts.parseOrThrow(rd.refund_amount);
-        const refundFee = Amounts.parseOrThrow(rd.refund_fee);
-        c.status = CoinStatus.Dormant;
-        c.currentAmount = Amounts.add(c.currentAmount, refundAmount).amount;
-        c.currentAmount = Amounts.sub(c.currentAmount, refundFee).amount;
-        logger.trace(
-          `coin amount after is ${Amounts.stringify(c.currentAmount)}`,
-        );
-        await tx.put(Stores.coins, c);
       }
 
+      const refreshCoinsPubs = Object.values(refreshCoinsMap);
+      await createRefreshGroup(ws, tx, refreshCoinsPubs, RefreshReason.Refund);
+
       // Are we done with querying yet, or do we need to do another round
       // after a retry delay?
       let queryDone = true;
 
-      logger.trace(`got ${numNewRefunds} new refund permissions`);
+      if (p.autoRefundDeadline && p.autoRefundDeadline.t_ms > now.t_ms) {
+        queryDone = false;
+      }
 
-      if (numNewRefunds === 0) {
-        if (p.autoRefundDeadline && p.autoRefundDeadline.t_ms > now.t_ms) {
-          queryDone = false;
+      let numPendingRefunds = 0;
+      for (const ri of Object.values(p.refunds)) {
+        switch (ri.type) {
+          case RefundState.Pending:
+            numPendingRefunds++;
+            break;
         }
-      } else {
-        p.refundGroups.push({
-          reason: RefundReason.NormalRefund,
-          refundGroupId,
-          timestampQueried: getTimestampNow(),
-        });
       }
 
-      if (Object.keys(unfinishedRefunds).length != 0) {
+      if (numPendingRefunds > 0) {
         queryDone = false;
       }
 
@@ -281,38 +281,7 @@ async function acceptRefundResponse(
         logger.trace("refund query not done");
       }
 
-      p.refundsRefreshCost = { ...p.refundsRefreshCost, ...refundsRefreshCost 
};
-
       await tx.put(Stores.purchases, p);
-
-      const coinsPubsToBeRefreshed = Object.values(refreshCoinsMap);
-      if (coinsPubsToBeRefreshed.length > 0) {
-        await createRefreshGroup(
-          tx,
-          coinsPubsToBeRefreshed,
-          RefreshReason.Refund,
-        );
-      }
-
-      // Check if any of the refund groups are done, and we
-      // can emit an corresponding event.
-      for (const g of Object.keys(changedGroups)) {
-        let groupDone = true;
-        for (const pk of Object.keys(p.refundsPending)) {
-          const r = p.refundsPending[pk];
-          if (r.refundGroupId == g) {
-            groupDone = false;
-          }
-        }
-        if (groupDone) {
-          const refundEvent: RefundEventRecord = {
-            proposalId,
-            refundGroupId: g,
-            timestamp: now,
-          };
-          await tx.put(Stores.refundEvents, refundEvent);
-        }
-      }
     },
   );
 
@@ -430,22 +399,33 @@ async function processPurchaseQueryRefundImpl(
     return;
   }
 
-  const request = await ws.http.get(
-    new URL(
-      `orders/${purchase.contractData.orderId}`,
-      purchase.contractData.merchantBaseUrl,
-    ).href,
+  const requestUrl = new URL(
+    `orders/${purchase.contractData.orderId}`,
+    purchase.contractData.merchantBaseUrl,
+  );
+  requestUrl.searchParams.set(
+    "h_contract",
+    purchase.contractData.contractTermsHash,
   );
 
+  const request = await ws.http.get(requestUrl.href);
+
+  console.log("got json", JSON.stringify(await request.json(), undefined, 2));
+
   const refundResponse = await readSuccessResponseJsonOrThrow(
     request,
-    codecForMerchantRefundResponse(),
+    codecForMerchantOrderStatus(),
   );
 
-  await acceptRefundResponse(
+  if (!refundResponse.paid) {
+    logger.error("can't refund unpaid order");
+    return;
+  }
+
+  await acceptRefunds(
     ws,
     proposalId,
-    refundResponse,
+    refundResponse.refunds,
     RefundReason.NormalRefund,
   );
 }
diff --git a/src/operations/transactions.ts b/src/operations/transactions.ts
index f104f107..fb062966 100644
--- a/src/operations/transactions.ts
+++ b/src/operations/transactions.ts
@@ -44,68 +44,6 @@ function makeEventId(type: TransactionType, ...args: 
string[]): string {
   return type + ";" + args.map((x) => encodeURIComponent(x)).join(";");
 }
 
-interface RefundStats {
-  amountInvalid: AmountJson;
-  amountEffective: AmountJson;
-  amountRaw: AmountJson;
-}
-
-function getRefundStats(
-  pr: PurchaseRecord,
-  refundGroupId: string,
-): RefundStats {
-  let amountEffective = Amounts.getZero(pr.contractData.amount.currency);
-  let amountInvalid = Amounts.getZero(pr.contractData.amount.currency);
-  let amountRaw = Amounts.getZero(pr.contractData.amount.currency);
-
-  for (const rk of Object.keys(pr.refundsDone)) {
-    const perm = pr.refundsDone[rk].perm;
-    if (pr.refundsDone[rk].refundGroupId !== refundGroupId) {
-      continue;
-    }
-    amountEffective = Amounts.add(
-      amountEffective,
-      Amounts.parseOrThrow(perm.refund_amount),
-    ).amount;
-    amountRaw = Amounts.add(amountRaw, 
Amounts.parseOrThrow(perm.refund_amount))
-      .amount;
-  }
-
-  // Subtract fees from effective refund amount
-
-  for (const rk of Object.keys(pr.refundsDone)) {
-    const perm = pr.refundsDone[rk].perm;
-    if (pr.refundsDone[rk].refundGroupId !== refundGroupId) {
-      continue;
-    }
-    amountEffective = Amounts.sub(
-      amountEffective,
-      Amounts.parseOrThrow(perm.refund_fee),
-    ).amount;
-    if (pr.refundsRefreshCost[rk]) {
-      amountEffective = Amounts.sub(amountEffective, pr.refundsRefreshCost[rk])
-        .amount;
-    }
-  }
-
-  for (const rk of Object.keys(pr.refundsFailed)) {
-    const perm = pr.refundsDone[rk].perm;
-    if (pr.refundsDone[rk].refundGroupId !== refundGroupId) {
-      continue;
-    }
-    amountInvalid = Amounts.add(
-      amountInvalid,
-      Amounts.parseOrThrow(perm.refund_fee),
-    ).amount;
-  }
-
-  return {
-    amountEffective,
-    amountInvalid,
-    amountRaw,
-  };
-}
-
 function shouldSkipCurrency(
   transactionsRequest: TransactionsRequest | undefined,
   currency: string,
@@ -319,36 +257,37 @@ export async function getTransactions(
           },
         });
 
-        for (const rg of pr.refundGroups) {
-          const pending = Object.keys(pr.refundsPending).length > 0;
-          const stats = getRefundStats(pr, rg.refundGroupId);
+        // for (const rg of pr.refundGroups) {
+        //   const pending = Object.keys(pr.refundsPending).length > 0;
+        //   const stats = getRefundStats(pr, rg.refundGroupId);
+
+        //   transactions.push({
+        //     type: TransactionType.Refund,
+        //     pending,
+        //     info: {
+        //       fulfillmentUrl: pr.contractData.fulfillmentUrl,
+        //       merchant: pr.contractData.merchant,
+        //       orderId: pr.contractData.orderId,
+        //       products: pr.contractData.products,
+        //       summary: pr.contractData.summary,
+        //       summary_i18n: pr.contractData.summaryI18n,
+        //     },
+        //     timestamp: rg.timestampQueried,
+        //     transactionId: makeEventId(
+        //       TransactionType.Refund,
+        //       pr.proposalId,
+        //       `${rg.timestampQueried.t_ms}`,
+        //     ),
+        //     refundedTransactionId: makeEventId(
+        //       TransactionType.Payment,
+        //       pr.proposalId,
+        //     ),
+        //     amountEffective: Amounts.stringify(stats.amountEffective),
+        //     amountInvalid: Amounts.stringify(stats.amountInvalid),
+        //     amountRaw: Amounts.stringify(stats.amountRaw),
+        //   });
+        // }
 
-          transactions.push({
-            type: TransactionType.Refund,
-            pending,
-            info: {
-              fulfillmentUrl: pr.contractData.fulfillmentUrl,
-              merchant: pr.contractData.merchant,
-              orderId: pr.contractData.orderId,
-              products: pr.contractData.products,
-              summary: pr.contractData.summary,
-              summary_i18n: pr.contractData.summaryI18n,
-            },
-            timestamp: rg.timestampQueried,
-            transactionId: makeEventId(
-              TransactionType.Refund,
-              pr.proposalId,
-              `${rg.timestampQueried.t_ms}`,
-            ),
-            refundedTransactionId: makeEventId(
-              TransactionType.Payment,
-              pr.proposalId,
-            ),
-            amountEffective: Amounts.stringify(stats.amountEffective),
-            amountInvalid: Amounts.stringify(stats.amountInvalid),
-            amountRaw: Amounts.stringify(stats.amountRaw),
-          });
-        }
       });
     },
   );
diff --git a/src/types/dbTypes.ts b/src/types/dbTypes.ts
index 252649b0..f75d5bab 100644
--- a/src/types/dbTypes.ts
+++ b/src/types/dbTypes.ts
@@ -27,7 +27,6 @@ import { AmountJson } from "../util/amounts";
 import {
   Auditor,
   CoinDepositPermission,
-  MerchantRefundDetails,
   TipResponse,
   ExchangeSignKeyJson,
   MerchantInfo,
@@ -1140,13 +1139,54 @@ export interface WireFee {
  */
 export interface RefundEventRecord {
   timestamp: Timestamp;
+  merchantExecutionTimestamp: Timestamp;
   refundGroupId: string;
   proposalId: string;
 }
 
-export interface RefundInfo {
-  refundGroupId: string;
-  perm: MerchantRefundDetails;
+export const enum RefundState {
+  Failed = "failed",
+  Applied = "applied",
+  Pending = "pending",
+}
+
+/**
+ * State of one refund from the merchant, maintained by the wallet.
+ */
+export type WalletRefundItem =
+  | WalletRefundFailedItem
+  | WalletRefundPendingItem
+  | WalletRefundAppliedItem;
+
+export interface WalletRefundItemCommon {
+  executionTime: Timestamp;
+  refundAmount: AmountJson;
+  refundFee: AmountJson;
+
+  /**
+   * Upper bound on the refresh cost incurred by
+   * applying this refund.
+   * 
+   * Might be lower in practice when two refunds on the same
+   * coin are refreshed in the same refresh operation.
+   */
+  totalRefreshCostBound: AmountJson;
+}
+
+/**
+ * Failed refund, either because the merchant did
+ * something wrong or it expired.
+ */
+export interface WalletRefundFailedItem extends WalletRefundItemCommon {
+  type: RefundState.Failed;
+}
+
+export interface WalletRefundPendingItem extends WalletRefundItemCommon {
+  type: RefundState.Pending;
+}
+
+export interface WalletRefundAppliedItem extends WalletRefundItemCommon {
+  type: RefundState.Applied;
 }
 
 export const enum RefundReason {
@@ -1160,12 +1200,6 @@ export const enum RefundReason {
   AbortRefund = "abort-pay-refund",
 }
 
-export interface RefundGroupInfo {
-  refundGroupId: string;
-  timestampQueried: Timestamp;
-  reason: RefundReason;
-}
-
 /**
  * Record stored for every time we successfully submitted
  * a payment to the merchant (both first time and re-play).
@@ -1269,31 +1303,11 @@ export interface PurchaseRecord {
    */
   timestampAccept: Timestamp;
 
-  /**
-   * Information regarding each group of refunds we receive at once.
-   */
-  refundGroups: RefundGroupInfo[];
-
   /**
    * Pending refunds for the purchase.  A refund is pending
    * when the merchant reports a transient error from the exchange.
    */
-  refundsPending: { [refundKey: string]: RefundInfo };
-
-  /**
-   * Applied refunds for the purchase.
-   */
-  refundsDone: { [refundKey: string]: RefundInfo };
-
-  /**
-   * Refunds that permanently failed.
-   */
-  refundsFailed: { [refundKey: string]: RefundInfo };
-
-  /**
-   * Refresh cost for each refund permission.
-   */
-  refundsRefreshCost: { [refundKey: string]: AmountJson };
+  refunds: { [refundKey: string]: WalletRefundItem };
 
   /**
    * When was the last refund made?
diff --git a/src/types/talerTypes.ts b/src/types/talerTypes.ts
index ef14684f..b2d8f6a3 100644
--- a/src/types/talerTypes.ts
+++ b/src/types/talerTypes.ts
@@ -37,6 +37,10 @@ import {
   codecForBoolean,
   makeCodecForMap,
   Codec,
+  makeCodecForConstNumber,
+  makeCodecForUnion,
+  makeCodecForConstTrue,
+  makeCodecForConstFalse,
 } from "../util/codec";
 import {
   Timestamp,
@@ -436,7 +440,7 @@ export class ContractTerms {
 /**
  * Refund permission in the format that the merchant gives it to us.
  */
-export class MerchantRefundDetails {
+export class MerchantAbortPayRefundDetails {
   /**
    * Amount to be refunded.
    */
@@ -502,7 +506,7 @@ export class MerchantRefundResponse {
   /**
    * The signed refund permissions, to be sent to the exchange.
    */
-  refunds: MerchantRefundDetails[];
+  refunds: MerchantAbortPayRefundDetails[];
 }
 
 /**
@@ -834,6 +838,115 @@ export interface ExchangeRevealResponse {
   ev_sigs: ExchangeRevealItem[];
 }
 
+export type MerchantOrderStatus =
+  | MerchantOrderStatusPaid
+  | MerchantOrderStatusUnpaid;
+
+interface MerchantOrderStatusPaid {
+  /**
+   * Has the payment for this order (ever) been completed?
+   */
+  paid: true;
+
+  /**
+   * Was the payment refunded (even partially, via refund or abort)?
+   */
+  refunded: boolean;
+
+  /**
+   * Amount that was refunded in total.
+   */
+  refund_amount: AmountString;
+
+  /**
+   * Successful refunds for this payment, empty array for none.
+   */
+  refunds: MerchantCoinRefundStatus[];
+
+  /**
+   * Public key of the merchant.
+   */
+  merchant_pub: EddsaPublicKeyString;
+}
+
+export type MerchantCoinRefundStatus =
+  | MerchantCoinRefundSuccessStatus
+  | MerchantCoinRefundFailureStatus;
+
+export interface MerchantCoinRefundSuccessStatus {
+  success: true;
+
+  // 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: EddsaSignatureString;
+
+  // 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: EddsaPublicKeyString;
+
+  // Refund transaction ID.
+  rtransaction_id: number;
+
+  // public key of a coin that was refunded
+  coin_pub: EddsaPublicKeyString;
+
+  // Amount that was refunded, including refund fee charged by the exchange
+  // to the customer.
+  refund_amount: AmountString;
+
+  execution_time: Timestamp;
+}
+
+export interface MerchantCoinRefundFailureStatus {
+  success: false;
+
+  // 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?: any;
+
+  // Refund transaction ID.
+  rtransaction_id: number;
+
+  // public key of a coin that was refunded
+  coin_pub: EddsaPublicKeyString;
+
+  // Amount that was refunded, including refund fee charged by the exchange
+  // to the customer.
+  refund_amount: AmountString;
+
+  execution_time: Timestamp;
+}
+
+export interface MerchantOrderStatusUnpaid {
+  /**
+   * Has the payment for this order (ever) been completed?
+   */
+  paid: false;
+
+  /**
+   * URI that the wallet must process to complete the payment.
+   */
+  taler_pay_uri: string;
+
+  /**
+   * Alternative order ID which was paid for already in the same session.
+   *
+   * Only given if the same product was purchased before in the same session.
+   */
+  already_paid_order_id?: string;
+}
+
 export type AmountString = string;
 export type Base32String = string;
 export type EddsaSignatureString = string;
@@ -940,9 +1053,9 @@ export const codecForContractTerms = (): 
Codec<ContractTerms> =>
     .build("ContractTerms");
 
 export const codecForMerchantRefundPermission = (): Codec<
-  MerchantRefundDetails
+  MerchantAbortPayRefundDetails
 > =>
-  makeCodecForObject<MerchantRefundDetails>()
+  makeCodecForObject<MerchantAbortPayRefundDetails>()
     .property("refund_amount", codecForString)
     .property("refund_fee", codecForString)
     .property("coin_pub", codecForString)
@@ -1094,3 +1207,67 @@ export const codecForExchangeRevealResponse = (): Codec<
   makeCodecForObject<ExchangeRevealResponse>()
     .property("ev_sigs", makeCodecForList(codecForExchangeRevealItem()))
     .build("ExchangeRevealResponse");
+
+export const codecForMerchantCoinRefundSuccessStatus = (): Codec<
+  MerchantCoinRefundSuccessStatus
+> =>
+  makeCodecForObject<MerchantCoinRefundSuccessStatus>()
+    .property("success", makeCodecForConstTrue())
+    .property("coin_pub", codecForString)
+    .property("exchange_status", makeCodecForConstNumber(200))
+    .property("exchange_sig", codecForString)
+    .property("rtransaction_id", codecForNumber)
+    .property("refund_amount", codecForString)
+    .property("exchange_pub", codecForString)
+    .property("execution_time", codecForTimestamp)
+    .build("MerchantCoinRefundSuccessStatus");
+
+export const codecForMerchantCoinRefundFailureStatus = (): Codec<
+  MerchantCoinRefundFailureStatus
+> =>
+  makeCodecForObject<MerchantCoinRefundFailureStatus>()
+    .property("success", makeCodecForConstFalse())
+    .property("coin_pub", codecForString)
+    .property("exchange_status", makeCodecForConstNumber(200))
+    .property("rtransaction_id", codecForNumber)
+    .property("refund_amount", codecForString)
+    .property("exchange_code", makeCodecOptional(codecForNumber))
+    .property("exchange_reply", makeCodecOptional(codecForAny))
+    .property("execution_time", codecForTimestamp)
+    .build("MerchantCoinRefundSuccessStatus");
+
+export const codecForMerchantCoinRefundStatus = (): Codec<
+  MerchantCoinRefundStatus
+> =>
+  makeCodecForUnion<MerchantCoinRefundStatus>()
+    .discriminateOn("success")
+    .alternative(true, codecForMerchantCoinRefundSuccessStatus())
+    .alternative(false, codecForMerchantCoinRefundFailureStatus())
+    .build("MerchantCoinRefundStatus");
+
+export const codecForMerchantOrderStatusPaid = (): Codec<
+  MerchantOrderStatusPaid
+> =>
+  makeCodecForObject<MerchantOrderStatusPaid>()
+    .property("paid", makeCodecForConstTrue())
+    .property("merchant_pub", codecForString)
+    .property("refund_amount", codecForString)
+    .property("refunded", codecForBoolean)
+    .property("refunds", makeCodecForList(codecForMerchantCoinRefundStatus()))
+    .build("MerchantOrderStatusPaid");
+
+export const codecForMerchantOrderStatusUnpaid = (): Codec<
+  MerchantOrderStatusUnpaid
+> =>
+  makeCodecForObject<MerchantOrderStatusUnpaid>()
+    .property("paid", makeCodecForConstFalse())
+    .property("taler_pay_uri", codecForString)
+    .property("already_paid_order_id", makeCodecOptional(codecForString))
+    .build("MerchantOrderStatusUnpaid");
+
+export const codecForMerchantOrderStatus = (): Codec<MerchantOrderStatus> =>
+  makeCodecForUnion<MerchantOrderStatus>()
+    .discriminateOn("paid")
+    .alternative(true, codecForMerchantOrderStatusPaid())
+    .alternative(false, codecForMerchantOrderStatusUnpaid())
+    .build("MerchantOrderStatus");
diff --git a/src/util/codec.ts b/src/util/codec.ts
index 136c5b05..c468704b 100644
--- a/src/util/codec.ts
+++ b/src/util/codec.ts
@@ -18,6 +18,8 @@
  * Type-safe codecs for converting from/to JSON.
  */
 
+ /* eslint-disable @typescript-eslint/ban-types */
+
 /**
  * Error thrown when decoding fails.
  */
@@ -335,6 +337,60 @@ export function makeCodecForConstString<V extends 
string>(s: V): Codec<V> {
   };
 }
 
+/**
+ * Return a codec for a boolean true constant.
+ */
+export function makeCodecForConstTrue(): Codec<true> {
+  return {
+    decode(x: any, c?: Context): true {
+      if (x === true) {
+        return x;
+      }
+      throw new DecodingError(
+        `expected boolean true at ${renderContext(
+          c,
+        )} but got ${typeof x}`,
+      );
+    },
+  };
+}
+
+/**
+ * Return a codec for a boolean true constant.
+ */
+export function makeCodecForConstFalse(): Codec<false> {
+  return {
+    decode(x: any, c?: Context): false {
+      if (x === false) {
+        return x;
+      }
+      throw new DecodingError(
+        `expected boolean false at ${renderContext(
+          c,
+        )} but got ${typeof x}`,
+      );
+    },
+  };
+}
+
+/**
+ * Return a codec for a value that must be a constant number.
+ */
+export function makeCodecForConstNumber<V extends number>(n: V): Codec<V> {
+  return {
+    decode(x: any, c?: Context): V {
+      if (x === n) {
+        return x;
+      }
+      throw new DecodingError(
+        `expected number constant "${n}" at ${renderContext(
+          c,
+        )}  but got ${typeof x}`,
+      );
+    },
+  };
+}
+
 export function makeCodecOptional<V>(
   innerCodec: Codec<V>,
 ): Codec<V | undefined> {
diff --git a/src/util/taleruri.ts b/src/util/taleruri.ts
index 30209d48..73280b6c 100644
--- a/src/util/taleruri.ts
+++ b/src/util/taleruri.ts
@@ -220,7 +220,7 @@ export function parseRefundUri(s: string): RefundUriResult 
| undefined {
   }
 
   if (maybePath === "-") {
-    maybePath = "public/";
+    maybePath = "";
   } else {
     maybePath = decodeURIComponent(maybePath) + "/";
   }
diff --git a/src/wallet.ts b/src/wallet.ts
index ff72f3c7..60ed695f 100644
--- a/src/wallet.ts
+++ b/src/wallet.ts
@@ -51,6 +51,7 @@ import {
   Stores,
   ReserveRecordStatus,
   CoinSourceType,
+  RefundState,
 } from "./types/dbTypes";
 import { CoinDumpJson } from "./types/talerTypes";
 import {
@@ -534,6 +535,7 @@ export class Wallet {
         [Stores.refreshGroups],
         async (tx) => {
           return await createRefreshGroup(
+            this.ws,
             tx,
             [{ coinPub: oldCoinPub }],
             RefreshReason.Manual,
@@ -785,22 +787,23 @@ export class Wallet {
     if (!purchase) {
       throw Error("unknown purchase");
     }
-    const refundsDoneAmounts = Object.values(purchase.refundsDone).map((x) =>
-      Amounts.parseOrThrow(x.perm.refund_amount),
-    );
-    const refundsPendingAmounts = Object.values(
-      purchase.refundsPending,
-    ).map((x) => Amounts.parseOrThrow(x.perm.refund_amount));
+    const refundsDoneAmounts = Object.values(purchase.refunds)
+      .filter((x) => x.type === RefundState.Applied)
+      .map((x) => x.refundAmount);
+
+    const refundsPendingAmounts = Object.values(purchase.refunds)
+      .filter((x) => x.type === RefundState.Pending)
+      .map((x) => x.refundAmount);
     const totalRefundAmount = Amounts.sum([
       ...refundsDoneAmounts,
       ...refundsPendingAmounts,
     ]).amount;
-    const refundsDoneFees = Object.values(purchase.refundsDone).map((x) =>
-      Amounts.parseOrThrow(x.perm.refund_amount),
-    );
-    const refundsPendingFees = Object.values(purchase.refundsPending).map((x) 
=>
-      Amounts.parseOrThrow(x.perm.refund_amount),
-    );
+    const refundsDoneFees = Object.values(purchase.refunds)
+      .filter((x) => x.type === RefundState.Applied)
+      .map((x) => x.refundFee);
+    const refundsPendingFees = Object.values(purchase.refunds)
+      .filter((x) => x.type === RefundState.Pending)
+      .map((x) => x.refundFee);
     const totalRefundFees = Amounts.sum([
       ...refundsDoneFees,
       ...refundsPendingFees,

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

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