gnunet-svn
[Top][All Lists]
Advanced

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

[taler-wallet-core] 01/02: new transactions API: withdrawal


From: gnunet
Subject: [taler-wallet-core] 01/02: new transactions API: withdrawal
Date: Tue, 12 May 2020 12:14:57 +0200

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

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

commit 6206b418ff88a238762a18e7b6eeaceafc5de294
Author: Florian Dold <address@hidden>
AuthorDate: Tue May 12 14:08:58 2020 +0530

    new transactions API: withdrawal
---
 src/headless/taler-wallet-cli.ts |   9 ++
 src/operations/history.ts        |   4 +-
 src/operations/pending.ts        |   2 +-
 src/operations/reserves.ts       |  96 ++++++++++--------
 src/operations/transactions.ts   | 130 ++++++++++++++++++++++++
 src/types/dbTypes.ts             |  14 ++-
 src/types/transactions.ts        | 208 +++++++++++++++++++++++++++++++++++++++
 src/wallet.ts                    |   6 ++
 8 files changed, 421 insertions(+), 48 deletions(-)

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/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/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..8333b66c
--- /dev/null
+++ b/src/operations/transactions.ts
@@ -0,0 +1,130 @@
+/*
+ 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, ProposalRecord, ReserveRecordStatus } from "../types/dbTypes";
+import { Amounts } from "../util/amounts";
+import { timestampCmp } from "../util/time";
+import {
+  TransactionsRequest,
+  TransactionsResponse,
+  Transaction,
+  TransactionType,
+} from "../types/transactions";
+import { OrderShortInfo } from "../types/history";
+
+/**
+ * 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(";");
+}
+
+function getOrderShortInfo(
+  proposal: ProposalRecord,
+): OrderShortInfo | undefined {
+  const download = proposal.download;
+  if (!download) {
+    return undefined;
+  }
+  return {
+    amount: Amounts.stringify(download.contractData.amount),
+    fulfillmentUrl: download.contractData.fulfillmentUrl,
+    orderId: download.contractData.orderId,
+    merchantBaseUrl: download.contractData.merchantBaseUrl,
+    proposalId: proposal.proposalId,
+    summary: download.contractData.summary,
+  };
+}
+
+/**
+ * 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 (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 (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,
+          ),
+        });
+      });
+    },
+  );
+
+  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..07c59d4d 100644
--- a/src/types/dbTypes.ts
+++ b/src/types/dbTypes.ts
@@ -273,13 +273,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;
 
diff --git a/src/types/transactions.ts b/src/types/transactions.ts
new file mode 100644
index 00000000..d2f0f6cb
--- /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: TransactionInfo;
+
+  // 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 TransactionInfo {
+  // 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: TransactionInfo;
+
+  // 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]