gnunet-svn
[Top][All Lists]
Advanced

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

[taler-wallet-core] branch master updated (857a2b9d -> 67dd0eb0)


From: gnunet
Subject: [taler-wallet-core] branch master updated (857a2b9d -> 67dd0eb0)
Date: Tue, 12 May 2020 12:14:56 +0200

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

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

    from 857a2b9d perf: reserve history in separate object store
     new 6206b418 new transactions API: withdrawal
     new 67dd0eb0 new transactions API: purchases and refunds

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 src/headless/taler-wallet-cli.ts |   9 ++
 src/operations/history.ts        |   4 +-
 src/operations/pay.ts            |  15 ++-
 src/operations/pending.ts        |   2 +-
 src/operations/refund.ts         |  18 ++-
 src/operations/reserves.ts       |  96 +++++++++-------
 src/operations/transactions.ts   | 230 +++++++++++++++++++++++++++++++++++++++
 src/types/dbTypes.ts             |  18 ++-
 src/types/transactions.ts        | 208 +++++++++++++++++++++++++++++++++++
 src/wallet.ts                    |   6 +
 10 files changed, 548 insertions(+), 58 deletions(-)
 create mode 100644 src/operations/transactions.ts
 create mode 100644 src/types/transactions.ts

diff --git a/src/headless/taler-wallet-cli.ts b/src/headless/taler-wallet-cli.ts
index 483a9e7c..3e9d993d 100644
--- a/src/headless/taler-wallet-cli.ts
+++ b/src/headless/taler-wallet-cli.ts
@@ -231,6 +231,15 @@ walletCli
     });
   });
 
+walletCli
+  .subcommand("", "transactions", { help: "Show transactions." })
+  .action(async (args) => {
+    await withWallet(args, async (wallet) => {
+      const pending = await wallet.getTransactions({});
+      console.log(JSON.stringify(pending, undefined, 2));
+    });
+  });
+
 async function asyncSleep(milliSeconds: number): Promise<void> {
   return new Promise<void>((resolve, reject) => {
     setTimeout(() => resolve(), milliSeconds);
diff --git a/src/operations/history.ts b/src/operations/history.ts
index 1271c56e..4e43596f 100644
--- a/src/operations/history.ts
+++ b/src/operations/history.ts
@@ -375,10 +375,10 @@ export async function getHistory(
           return;
         }
         let reserveCreationDetail: ReserveCreationDetail;
-        if (reserve.bankWithdrawStatusUrl) {
+        if (reserve.bankInfo) {
           reserveCreationDetail = {
             type: ReserveType.TalerBankWithdraw,
-            bankUrl: reserve.bankWithdrawStatusUrl,
+            bankUrl: reserve.bankInfo.statusUrl,
           };
         } else {
           reserveCreationDetail = {
diff --git a/src/operations/pay.ts b/src/operations/pay.ts
index a7528439..30ccb56c 100644
--- a/src/operations/pay.ts
+++ b/src/operations/pay.ts
@@ -122,6 +122,10 @@ export interface AvailableCoinInfo {
   feeDeposit: AmountJson;
 }
 
+export interface PayCostInfo {
+  totalCost: AmountJson;
+}
+
 /**
  * Compute the total cost of a payment to the customer.
  *
@@ -132,7 +136,7 @@ export interface AvailableCoinInfo {
 export async function getTotalPaymentCost(
   ws: InternalWalletState,
   pcs: PayCoinSelection,
-): Promise<AmountJson> {
+): Promise<PayCostInfo> {
   const costs = [
     pcs.paymentAmount,
     pcs.customerDepositFees,
@@ -163,7 +167,9 @@ export async function getTotalPaymentCost(
     const refreshCost = getTotalRefreshCost(allDenoms, denom, amountLeft);
     costs.push(refreshCost);
   }
-  return Amounts.sum(costs).amount;
+  return {
+    totalCost: Amounts.sum(costs).amount
+  };
 }
 
 /**
@@ -434,6 +440,7 @@ async function recordConfirmPay(
     contractTermsRaw: d.contractTermsRaw,
     contractData: d.contractData,
     lastSessionId: sessionId,
+    payCoinSelection: coinSelection,
     payReq,
     timestampAccept: getTimestampNow(),
     timestampLastRefundStatus: undefined,
@@ -903,8 +910,8 @@ export async function preparePayForUri(
       };
     }
 
-    const totalCost = await getTotalPaymentCost(ws, res);
-    const totalFees = Amounts.sub(totalCost, res.paymentAmount).amount;
+    const costInfo = await getTotalPaymentCost(ws, res);
+    const totalFees = Amounts.sub(costInfo.totalCost, 
res.paymentAmount).amount;
 
     return {
       status: "payment-possible",
diff --git a/src/operations/pending.ts b/src/operations/pending.ts
index 14072633..c793f5f0 100644
--- a/src/operations/pending.ts
+++ b/src/operations/pending.ts
@@ -150,7 +150,7 @@ async function gatherReservePending(
 ): Promise<void> {
   // FIXME: this should be optimized by using an index for "onlyDue==true".
   await tx.iter(Stores.reserves).forEach((reserve) => {
-    const reserveType = reserve.bankWithdrawStatusUrl
+    const reserveType = reserve.bankInfo
       ? ReserveType.TalerBankWithdraw
       : ReserveType.Manual;
     if (!reserve.retryInfo.active) {
diff --git a/src/operations/refund.ts b/src/operations/refund.ts
index 9b18cafd..1ffcd2da 100644
--- a/src/operations/refund.ts
+++ b/src/operations/refund.ts
@@ -36,7 +36,6 @@ import {
   CoinStatus,
   RefundReason,
   RefundEventRecord,
-  RefundInfo,
 } from "../types/dbTypes";
 import { NotificationType } from "../types/notifications";
 import { parseRefundUri } from "../util/taleruri";
@@ -48,7 +47,7 @@ import {
   codecForMerchantRefundResponse,
 } from "../types/talerTypes";
 import { AmountJson } from "../util/amounts";
-import { guardOperationException, OperationFailedError } from "./errors";
+import { guardOperationException } from "./errors";
 import { randomBytes } from "../crypto/primitives/nacl-fast";
 import { encodeCrock } from "../crypto/talerCrypto";
 import { getTimestampNow } from "../util/time";
@@ -159,6 +158,8 @@ async function acceptRefundResponse(
     }
   }
 
+  const now = getTimestampNow();
+
   await ws.db.runWithWriteTransaction(
     [Stores.purchases, Stores.coins, Stores.refreshGroups, 
Stores.refundEvents],
     async (tx) => {
@@ -253,10 +254,16 @@ async function acceptRefundResponse(
       if (numNewRefunds === 0) {
         if (
           p.autoRefundDeadline &&
-          p.autoRefundDeadline.t_ms > getTimestampNow().t_ms
+          p.autoRefundDeadline.t_ms > now.t_ms
         ) {
           queryDone = false;
         }
+      } else {
+        p.refundGroups.push({
+          reason: RefundReason.NormalRefund,
+          refundGroupId,
+          timestampQueried: getTimestampNow(),
+        });
       }
 
       if (Object.keys(unfinishedRefunds).length != 0) {
@@ -264,14 +271,14 @@ async function acceptRefundResponse(
       }
 
       if (queryDone) {
-        p.timestampLastRefundStatus = getTimestampNow();
+        p.timestampLastRefundStatus = now;
         p.lastRefundStatusError = undefined;
         p.refundStatusRetryInfo = initRetryInfo(false);
         p.refundStatusRequested = false;
         console.log("refund query done");
       } else {
         // No error, but we need to try again!
-        p.timestampLastRefundStatus = getTimestampNow();
+        p.timestampLastRefundStatus = now;
         p.refundStatusRetryInfo.retryCounter++;
         updateRetryInfoTimeout(p.refundStatusRetryInfo);
         p.lastRefundStatusError = undefined;
@@ -291,7 +298,6 @@ async function acceptRefundResponse(
 
       // Check if any of the refund groups are done, and we
       // can emit an corresponding event.
-      const now = getTimestampNow();
       for (const g of Object.keys(changedGroups)) {
         let groupDone = true;
         for (const pk of Object.keys(p.refundsPending)) {
diff --git a/src/operations/reserves.ts b/src/operations/reserves.ts
index 2bbb085d..347f6e89 100644
--- a/src/operations/reserves.ts
+++ b/src/operations/reserves.ts
@@ -108,7 +108,14 @@ export async function createReserve(
     senderWire: req.senderWire,
     timestampConfirmed: undefined,
     timestampReserveInfoPosted: undefined,
-    bankWithdrawStatusUrl: req.bankWithdrawStatusUrl,
+    bankInfo: req.bankWithdrawStatusUrl
+      ? {
+          statusUrl: req.bankWithdrawStatusUrl,
+          amount: req.amount,
+          bankWithdrawalGroupId: encodeCrock(getRandomBytes(32)),
+          withdrawalStarted: false,
+        }
+      : undefined,
     exchangeWire: req.exchangeWire,
     reserveStatus,
     lastSuccessfulStatusQuery: undefined,
@@ -173,10 +180,10 @@ export async function createReserve(
     ],
     async (tx) => {
       // Check if we have already created a reserve for that 
bankWithdrawStatusUrl
-      if (reserveRecord.bankWithdrawStatusUrl) {
+      if (reserveRecord.bankInfo?.statusUrl) {
         const bwi = await tx.get(
           Stores.bankWithdrawUris,
-          reserveRecord.bankWithdrawStatusUrl,
+          reserveRecord.bankInfo.statusUrl,
         );
         if (bwi) {
           const otherReserve = await tx.get(Stores.reserves, bwi.reservePub);
@@ -192,7 +199,7 @@ export async function createReserve(
         }
         await tx.put(Stores.bankWithdrawUris, {
           reservePub: reserveRecord.reservePub,
-          talerWithdrawUri: reserveRecord.bankWithdrawStatusUrl,
+          talerWithdrawUri: reserveRecord.bankInfo.statusUrl,
         });
       }
       await tx.put(Stores.currencies, cr);
@@ -279,7 +286,7 @@ async function registerReserveWithBank(
     default:
       return;
   }
-  const bankStatusUrl = reserve.bankWithdrawStatusUrl;
+  const bankStatusUrl = reserve.bankInfo?.statusUrl;
   if (!bankStatusUrl) {
     return;
   }
@@ -333,7 +340,7 @@ async function processReserveBankStatusImpl(
     default:
       return;
   }
-  const bankStatusUrl = reserve.bankWithdrawStatusUrl;
+  const bankStatusUrl = reserve.bankInfo?.statusUrl;
   if (!bankStatusUrl) {
     return;
   }
@@ -382,7 +389,9 @@ async function processReserveBankStatusImpl(
         default:
           return;
       }
-      r.bankWithdrawConfirmUrl = status.confirm_transfer_url;
+      if (r.bankInfo) {
+        r.bankInfo.confirmUrl = status.confirm_transfer_url;
+      }
       return r;
     });
     await incrementReserveRetry(ws, reservePub, undefined);
@@ -673,35 +682,7 @@ async function depleteReserve(
 
   logger.trace("selected denominations");
 
-  const withdrawalGroupId = encodeCrock(randomBytes(32));
-
-  logger.trace("created plachets");
-
-  const withdrawalRecord: WithdrawalGroupRecord = {
-    withdrawalGroupId: withdrawalGroupId,
-    exchangeBaseUrl: reserve.exchangeBaseUrl,
-    source: {
-      type: WithdrawalSourceType.Reserve,
-      reservePub: reserve.reservePub,
-    },
-    rawWithdrawalAmount: withdrawAmount,
-    timestampStart: getTimestampNow(),
-    retryInfo: initRetryInfo(),
-    lastErrorPerCoin: {},
-    lastError: undefined,
-    denomsSel: {
-      totalCoinValue: denomsForWithdraw.totalCoinValue,
-      totalWithdrawCost: denomsForWithdraw.totalWithdrawCost,
-      selectedDenoms: denomsForWithdraw.selectedDenoms.map((x) => {
-        return {
-          count: x.count,
-          denomPubHash: x.denom.denomPubHash,
-        };
-      }),
-    },
-  };
-
-  const success = await ws.db.runWithWriteTransaction(
+  const newWithdrawalGroup = await ws.db.runWithWriteTransaction(
     [
       Stores.withdrawalGroups,
       Stores.reserves,
@@ -748,20 +729,55 @@ async function depleteReserve(
       }
       newReserve.reserveStatus = ReserveRecordStatus.DORMANT;
       newReserve.retryInfo = initRetryInfo(false);
+
+      let withdrawalGroupId: string;
+
+      const bankInfo = newReserve.bankInfo;
+      if (bankInfo && !bankInfo.withdrawalStarted) {
+        withdrawalGroupId = bankInfo.bankWithdrawalGroupId;
+        bankInfo.withdrawalStarted = true;
+      } else {
+        withdrawalGroupId = encodeCrock(randomBytes(32));
+      }
+
+      const withdrawalRecord: WithdrawalGroupRecord = {
+        withdrawalGroupId: withdrawalGroupId,
+        exchangeBaseUrl: newReserve.exchangeBaseUrl,
+        source: {
+          type: WithdrawalSourceType.Reserve,
+          reservePub: newReserve.reservePub,
+        },
+        rawWithdrawalAmount: withdrawAmount,
+        timestampStart: getTimestampNow(),
+        retryInfo: initRetryInfo(),
+        lastErrorPerCoin: {},
+        lastError: undefined,
+        denomsSel: {
+          totalCoinValue: denomsForWithdraw.totalCoinValue,
+          totalWithdrawCost: denomsForWithdraw.totalWithdrawCost,
+          selectedDenoms: denomsForWithdraw.selectedDenoms.map((x) => {
+            return {
+              count: x.count,
+              denomPubHash: x.denom.denomPubHash,
+            };
+          }),
+        },
+      };    
+
       await tx.put(Stores.reserves, newReserve);
       await tx.put(Stores.reserveHistory, newHist);
       await tx.put(Stores.withdrawalGroups, withdrawalRecord);
-      return true;
+      return withdrawalRecord;
     },
   );
 
-  if (success) {
+  if (newWithdrawalGroup) {
     console.log("processing new withdraw group");
     ws.notify({
       type: NotificationType.WithdrawGroupCreated,
-      withdrawalGroupId: withdrawalGroupId,
+      withdrawalGroupId: newWithdrawalGroup.withdrawalGroupId,
     });
-    await processWithdrawGroup(ws, withdrawalGroupId);
+    await processWithdrawGroup(ws, newWithdrawalGroup.withdrawalGroupId);
   } else {
     console.trace("withdraw session already existed");
   }
diff --git a/src/operations/transactions.ts b/src/operations/transactions.ts
new file mode 100644
index 00000000..e5c704b0
--- /dev/null
+++ b/src/operations/transactions.ts
@@ -0,0 +1,230 @@
+/*
+ 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/>
+ */
+
+/**
+ * Imports.
+ */
+import { InternalWalletState } from "./state";
+import { Stores, ReserveRecordStatus, PurchaseRecord } from "../types/dbTypes";
+import { Amounts, AmountJson } from "../util/amounts";
+import { timestampCmp } from "../util/time";
+import {
+  TransactionsRequest,
+  TransactionsResponse,
+  Transaction,
+  TransactionType,
+} from "../types/transactions";
+import { getTotalPaymentCost } from "./pay";
+
+/**
+ * Create an event ID from the type and the primary key for the event.
+ */
+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;
+  }
+
+  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;
+  }
+
+  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,
+  }
+
+}
+
+/**
+ * Retrive the full event history for this wallet.
+ */
+export async function getTransactions(
+  ws: InternalWalletState,
+  transactionsRequest?: TransactionsRequest,
+): Promise<TransactionsResponse> {
+  const transactions: Transaction[] = [];
+
+  await ws.db.runWithReadTransaction(
+    [
+      Stores.currencies,
+      Stores.coins,
+      Stores.denominations,
+      Stores.proposals,
+      Stores.purchases,
+      Stores.refreshGroups,
+      Stores.reserves,
+      Stores.reserveHistory,
+      Stores.tips,
+      Stores.withdrawalGroups,
+      Stores.payEvents,
+      Stores.planchets,
+      Stores.refundEvents,
+      Stores.reserveUpdatedEvents,
+      Stores.recoupGroups,
+    ],
+    async (tx) => {
+      tx.iter(Stores.withdrawalGroups).forEach((wsr) => {
+        if (
+          transactionsRequest?.currency &&
+          wsr.rawWithdrawalAmount.currency != transactionsRequest.currency
+        ) {
+          return;
+        }
+        if (wsr.rawWithdrawalAmount.currency)
+          if (wsr.timestampFinish) {
+            transactions.push({
+              type: TransactionType.Withdrawal,
+              amountEffective: Amounts.stringify(
+                wsr.denomsSel.totalWithdrawCost,
+              ),
+              amountRaw: Amounts.stringify(wsr.denomsSel.totalCoinValue),
+              confirmed: true,
+              exchangeBaseUrl: wsr.exchangeBaseUrl,
+              pending: !wsr.timestampFinish,
+              timestamp: wsr.timestampStart,
+              transactionId: makeEventId(
+                TransactionType.Withdrawal,
+                wsr.withdrawalGroupId,
+              ),
+            });
+          }
+      });
+
+      tx.iter(Stores.reserves).forEach((r) => {
+        if (
+          transactionsRequest?.currency &&
+          r.currency != transactionsRequest.currency
+        ) {
+          return;
+        }
+        if (r.reserveStatus !== ReserveRecordStatus.WAIT_CONFIRM_BANK) {
+          return;
+        }
+        if (!r.bankInfo) {
+          return;
+        }
+        transactions.push({
+          type: TransactionType.Withdrawal,
+          confirmed: false,
+          amountRaw: Amounts.stringify(r.bankInfo.amount),
+          amountEffective: undefined,
+          exchangeBaseUrl: undefined,
+          pending: true,
+          timestamp: r.timestampCreated,
+          bankConfirmationUrl: r.bankInfo.confirmUrl,
+          transactionId: makeEventId(
+            TransactionType.Withdrawal,
+            r.bankInfo.bankWithdrawalGroupId,
+          ),
+        });
+      });
+
+      tx.iter(Stores.purchases).forEachAsync(async (pr) => {
+        if (
+          transactionsRequest?.currency &&
+          pr.contractData.amount.currency != transactionsRequest.currency
+        ) {
+          return;
+        }
+        const proposal = await tx.get(Stores.proposals, pr.proposalId);
+        if (!proposal) {
+          return;
+        }
+        const cost = await getTotalPaymentCost(ws, pr.payCoinSelection);
+        transactions.push({
+          type: TransactionType.Payment,
+          amountRaw: Amounts.stringify(pr.contractData.amount),
+          amountEffective: Amounts.stringify(cost.totalCost),
+          failed: false,
+          pending: !pr.timestampFirstSuccessfulPay,
+          timestamp: pr.timestampAccept,
+          transactionId: makeEventId(TransactionType.Payment, pr.proposalId),
+          info: {
+            fulfillmentUrl: pr.contractData.fulfillmentUrl,
+            merchant: {},
+            orderId: pr.contractData.orderId,
+            products: [],
+            summary: pr.contractData.summary,
+            summary_i18n: {},
+          },
+        });
+
+        for (const rg of pr.refundGroups) {
+          const pending = Object.keys(pr.refundsDone).length > 0;
+
+          const stats = getRefundStats(pr, rg.refundGroupId);
+          
+          transactions.push({
+            type: TransactionType.Refund,
+            pending,
+            info: {
+              fulfillmentUrl: pr.contractData.fulfillmentUrl,
+              merchant: {},
+              orderId: pr.contractData.orderId,
+              products: [],
+              summary: pr.contractData.summary,
+              summary_i18n: {},
+            },
+            timestamp: rg.timestampQueried,
+            transactionId: makeEventId(TransactionType.Refund, 
`{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.sort((h1, h2) => timestampCmp(h1.timestamp, h2.timestamp));
+
+  return { transactions };
+}
diff --git a/src/types/dbTypes.ts b/src/types/dbTypes.ts
index 4cf19a56..eae39fff 100644
--- a/src/types/dbTypes.ts
+++ b/src/types/dbTypes.ts
@@ -43,6 +43,7 @@ import {
   ReserveRecoupTransaction,
 } from "./ReserveTransaction";
 import { Timestamp, Duration, getTimestampNow } from "../util/time";
+import { PayCoinSelection } from "../operations/pay";
 
 export enum ReserveRecordStatus {
   /**
@@ -273,13 +274,17 @@ export interface ReserveRecord {
    */
   exchangeWire: string;
 
-  bankWithdrawStatusUrl?: string;
-
   /**
-   * URL that the bank gave us to redirect the customer
-   * to in order to confirm a withdrawal.
+   * Extra state for when this is a withdrawal involving
+   * a Taler-integrated bank.
    */
-  bankWithdrawConfirmUrl?: string;
+  bankInfo?: {
+    statusUrl: string;
+    confirmUrl?: string;
+    amount: AmountJson;
+    bankWithdrawalGroupId: string;
+    withdrawalStarted: boolean;
+  };
 
   reserveStatus: ReserveRecordStatus;
 
@@ -1129,6 +1134,7 @@ export const enum RefundReason {
 }
 
 export interface RefundGroupInfo {
+  refundGroupId: string;
   timestampQueried: Timestamp;
   reason: RefundReason;
 }
@@ -1218,6 +1224,8 @@ export interface PurchaseRecord {
    */
   payReq: PayReq;
 
+  payCoinSelection: PayCoinSelection;
+
   /**
    * Timestamp of the first time that sending a payment to the merchant
    * for this purchase was successful.
diff --git a/src/types/transactions.ts b/src/types/transactions.ts
new file mode 100644
index 00000000..7dda46f7
--- /dev/null
+++ b/src/types/transactions.ts
@@ -0,0 +1,208 @@
+/*
+ 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/>
+ */
+
+/**
+ * Type and schema definitions for the wallet's transaction list.
+ */
+
+/**
+ * Imports.
+ */
+import { Timestamp } from "../util/time";
+import { AmountString } from "./talerTypes";
+
+export interface TransactionsRequest {
+  /**
+   * return only transactions in the given currency
+   */
+  currency?: string;
+
+  /**
+   * if present, results will be limited to transactions related to the given 
search string
+   */
+  search?: string;
+}
+
+export interface TransactionsResponse {
+  // a list of past and pending transactions sorted by pending, timestamp and 
transactionId.
+  // In case two events are both pending and have the same timestamp,
+  // they are sorted by the transactionId
+  // (lexically ascending and locale-independent comparison).
+  transactions: Transaction[];
+}
+
+export interface TransactionCommon {
+  // opaque unique ID for the transaction, used as a starting point for 
paginating queries
+  // and for invoking actions on the transaction (e.g. deleting/hiding it from 
the history)
+  transactionId: string;
+
+  // the type of the transaction; different types might provide additional 
information
+  type: TransactionType;
+
+  // main timestamp of the transaction
+  timestamp: Timestamp;
+
+  // true if the transaction is still pending, false otherwise
+  // If a transaction is not longer pending, its timestamp will be updated,
+  // but its transactionId will remain unchanged
+  pending: boolean;
+
+  // Raw amount of the transaction (exclusive of fees or other extra costs)
+  amountRaw: AmountString;
+
+  // Amount added or removed from the wallet's balance (including all fees and 
other costs)
+  amountEffective?: AmountString;
+}
+
+export type Transaction = (
+  TransactionWithdrawal |
+  TransactionPayment |
+  TransactionRefund |
+  TransactionTip |
+  TransactionRefresh
+)
+
+export const enum TransactionType {
+  Withdrawal = "withdrawal",
+  Payment = "payment",
+  Refund = "refund",
+  Refresh = "refresh",
+  Tip = "tip",
+}
+
+// This should only be used for actual withdrawals
+// and not for tips that have their own transactions type.
+interface TransactionWithdrawal extends TransactionCommon {
+  type: TransactionType.Withdrawal;
+
+  /**
+   * Exchange of the withdrawal.
+   */
+  exchangeBaseUrl?: string;
+
+  // true if the bank has confirmed the withdrawal, false if not.
+  // An unconfirmed withdrawal usually requires user-input and should be 
highlighted in the UI.
+  // See also bankConfirmationUrl below.
+  confirmed: boolean;
+
+  // If the withdrawal is unconfirmed, this can include a URL for user 
initiated confirmation.
+  bankConfirmationUrl?: string;
+
+  // Amount that has been subtracted from the reserve's balance for this 
withdrawal.
+  amountRaw: AmountString;
+
+  /**
+   * Amount that actually was (or will be) added to the wallet's balance.
+   * Only present if an exchange has already been selected.
+   */
+  amountEffective?: AmountString;
+}
+
+interface TransactionPayment extends TransactionCommon {
+  type: TransactionType.Payment;
+
+  // Additional information about the payment.
+  info: PaymentShortInfo;
+
+  // true if the payment failed, false otherwise.
+  // Note that failed payments with zero effective amount will not be returned 
by the API.
+  failed: boolean;
+
+  // Amount that must be paid for the contract
+  amountRaw: AmountString;
+
+  // Amount that was paid, including deposit, wire and refresh fees.
+  amountEffective?: AmountString;
+}
+
+
+interface PaymentShortInfo {
+  // Order ID, uniquely identifies the order within a merchant instance
+  orderId: string;
+
+  // More information about the merchant
+  merchant: any;
+
+  // Summary of the order, given by the merchant
+  summary: string;
+
+  // Map from IETF BCP 47 language tags to localized summaries
+  summary_i18n?: { [lang_tag: string]: string };
+
+  // List of products that are part of the order
+  products: any[];
+
+  // URL of the fulfillment, given by the merchant
+  fulfillmentUrl: string;
+}
+
+
+interface TransactionRefund extends TransactionCommon {
+  type: TransactionType.Refund;
+
+  // ID for the transaction that is refunded
+  refundedTransactionId: string;
+
+  // Additional information about the refunded payment
+  info: PaymentShortInfo;
+
+  // Part of the refund that couldn't be applied because the refund 
permissions were expired
+  amountInvalid: AmountString;
+
+  // Amount that has been refunded by the merchant
+  amountRaw: AmountString;
+
+  // Amount will be added to the wallet's balance after fees and refreshing
+  amountEffective: AmountString;
+}
+
+interface TransactionTip extends TransactionCommon {
+  type: TransactionType.Tip;
+
+  // true if the user still needs to accept/decline this tip
+  waiting: boolean;
+
+  // true if the user has accepted this top, false otherwise
+  accepted: boolean;
+
+  // Exchange that the tip will be (or was) withdrawn from
+  exchangeBaseUrl: string;
+
+  // More information about the merchant that sent the tip
+  merchant: any;
+
+  // Raw amount of the tip, without extra fees that apply
+  amountRaw: AmountString;
+
+  // Amount will be (or was) added to the wallet's balance after fees and 
refreshing
+  amountEffective: AmountString;
+}
+
+// A transaction shown for refreshes that are not associated to other 
transactions
+// such as a refresh necessary before coin expiration.
+// It should only be returned by the API if the effective amount is different 
from zero.
+interface TransactionRefresh extends TransactionCommon {
+  type: TransactionType.Refresh;
+
+  // Exchange that the coins are refreshed with
+  exchangeBaseUrl: string;
+
+  // Raw amount that is refreshed
+  amountRaw: AmountString;
+
+  // Amount that will be paid as fees for the refresh
+  amountEffective: AmountString;
+}
\ No newline at end of file
diff --git a/src/wallet.ts b/src/wallet.ts
index 3558e102..2d63e229 100644
--- a/src/wallet.ts
+++ b/src/wallet.ts
@@ -112,6 +112,8 @@ import {
 import { durationMin, Duration } from "./util/time";
 import { processRecoupGroup } from "./operations/recoup";
 import { OperationFailedAndReportedError } from "./operations/errors";
+import { TransactionsRequest, TransactionsResponse } from 
"./types/transactions";
+import { getTransactions } from "./operations/transactions";
 
 const builtinCurrencies: CurrencyRecord[] = [
   {
@@ -815,4 +817,8 @@ export class Wallet {
     }
     return coinsJson;
   }
+
+  async getTransactions(request: TransactionsRequest): 
Promise<TransactionsResponse> {
+    return getTransactions(this.ws, request);
+  }
 }

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



reply via email to

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