gnunet-svn
[Top][All Lists]
Advanced

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

[taler-wallet-core] branch master updated (4e7967dba -> d0d7685f1)


From: gnunet
Subject: [taler-wallet-core] branch master updated (4e7967dba -> d0d7685f1)
Date: Thu, 15 Jun 2023 18:30:51 +0200

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

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

    from 4e7967dba showing off information about operation plan
     new 08746f5bf compare duration
     new f7058a86c watch test
     new d0d7685f1 add test to coin selection algorithm

The 3 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:
 packages/taler-util/src/time.ts                    |  23 +
 packages/taler-wallet-core/package.json            |   1 +
 .../src/util/coinSelection.test.ts                 | 195 ++++++-
 .../taler-wallet-core/src/util/coinSelection.ts    | 585 ++++++++++++---------
 packages/taler-wallet-core/watch_test.sh           |   3 +
 5 files changed, 565 insertions(+), 242 deletions(-)
 create mode 100755 packages/taler-wallet-core/watch_test.sh

diff --git a/packages/taler-util/src/time.ts b/packages/taler-util/src/time.ts
index 3d7d80485..cf370fdad 100644
--- a/packages/taler-util/src/time.ts
+++ b/packages/taler-util/src/time.ts
@@ -216,6 +216,29 @@ export namespace Duration {
     };
   }
 
+  /**
+   * Compare two durations.  Returns 0 when equal, -1 when a < b
+   * and +1 when a > b.
+   */
+  export function cmp(d1: Duration, d2: Duration): 1 | 0 | -1 {
+    if (d1.d_ms === "forever") {
+      if (d2.d_ms === "forever") {
+        return 0;
+      }
+      return 1;
+    }
+    if (d2.d_ms === "forever") {
+      return -1;
+    }
+    if (d1.d_ms == d2.d_ms) {
+      return 0;
+    }
+    if (d1.d_ms > d2.d_ms) {
+      return 1;
+    }
+    return -1;
+  }
+
   export function max(d1: Duration, d2: Duration): Duration {
     return durationMax(d1, d2);
   }
diff --git a/packages/taler-wallet-core/package.json 
b/packages/taler-wallet-core/package.json
index b32f797d0..ee1c63996 100644
--- a/packages/taler-wallet-core/package.json
+++ b/packages/taler-wallet-core/package.json
@@ -78,6 +78,7 @@
     "tslib": "^2.5.3"
   },
   "ava": {
+    "ignoredByWatcher": ["src/**/*"],
     "files": [
       "lib/**/*test.*"
     ]
diff --git a/packages/taler-wallet-core/src/util/coinSelection.test.ts 
b/packages/taler-wallet-core/src/util/coinSelection.test.ts
index c0edc4cc1..7f4164aa9 100644
--- a/packages/taler-wallet-core/src/util/coinSelection.test.ts
+++ b/packages/taler-wallet-core/src/util/coinSelection.test.ts
@@ -14,6 +14,18 @@
  GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 import test, { ExecutionContext } from "ava";
+import {
+  calculatePlanFormAvailableCoins,
+  selectCoinForOperation,
+} from "./coinSelection.js";
+import {
+  AbsoluteTime,
+  AgeRestriction,
+  AmountJson,
+  Amounts,
+  Duration,
+  TransactionType,
+} from "@gnu-taler/taler-util";
 
 function expect(t: ExecutionContext, thing: any): any {
   return {
@@ -24,6 +36,185 @@ function expect(t: ExecutionContext, thing: any): any {
   };
 }
 
-test("should have a test", (t) => {
-  expect(t, true).deep.equal(true);
+function kudos(v: number): AmountJson {
+  return Amounts.fromFloat(v, "KUDOS");
+}
+
+function defaultFeeConfig(value: AmountJson, totalAvailable: number) {
+  return {
+    id: Amounts.stringify(value),
+    denomDeposit: kudos(0.01),
+    denomRefresh: kudos(0.01),
+    denomWithdraw: kudos(0.01),
+    duration: Duration.getForever(),
+    exchangePurse: undefined,
+    exchangeWire: undefined,
+    maxAge: AgeRestriction.AGE_UNRESTRICTED,
+    totalAvailable,
+    value,
+  };
+}
+type Coin = [AmountJson, number];
+
+/**
+ * selectCoinForOperation test
+ *
+ * Test here should check that the correct coins are selected
+ */
+
+test("get effective 2", (t) => {
+  const coinList: Coin[] = [
+    [kudos(2), 5],
+    [kudos(5), 5],
+  ];
+  const result = selectCoinForOperation("credit", kudos(2), "net", {
+    list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+    exchanges: {},
+  });
+  expect(t, result.coins).deep.equal(["KUDOS:2"]);
+  t.assert(result.refresh === undefined);
+});
+
+test("get raw 4", (t) => {
+  const coinList: Coin[] = [
+    [kudos(2), 5],
+    [kudos(5), 5],
+  ];
+  const result = selectCoinForOperation("credit", kudos(4), "gross", {
+    list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+    exchanges: {},
+  });
+
+  expect(t, result.coins).deep.equal(["KUDOS:2", "KUDOS:2"]);
+  t.assert(result.refresh === undefined);
+});
+
+test("send effective 6", (t) => {
+  const coinList: Coin[] = [
+    [kudos(2), 5],
+    [kudos(5), 5],
+  ];
+  const result = selectCoinForOperation("debit", kudos(6), "gross", {
+    list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+    exchanges: {},
+  });
+
+  expect(t, result.coins).deep.equal(["KUDOS:5"]);
+  t.assert(result.refresh?.selected === "KUDOS:2");
+});
+
+test("send raw 6", (t) => {
+  const coinList: Coin[] = [
+    [kudos(2), 5],
+    [kudos(5), 5],
+  ];
+  const result = selectCoinForOperation("debit", kudos(6), "gross", {
+    list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+    exchanges: {},
+  });
+
+  expect(t, result.coins).deep.equal(["KUDOS:5"]);
+  t.assert(result.refresh?.selected === "KUDOS:2");
+});
+
+test("send raw 20 (not enough)", (t) => {
+  const coinList: Coin[] = [
+    [kudos(2), 1],
+    [kudos(5), 2],
+  ];
+  const result = selectCoinForOperation("debit", kudos(20), "gross", {
+    list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+    exchanges: {},
+  });
+
+  expect(t, result.coins).deep.equal(["KUDOS:5", "KUDOS:5", "KUDOS:2"]);
+  t.assert(result.refresh === undefined);
+});
+
+/**
+ * calculatePlanFormAvailableCoins test
+ *
+ * Test here should check that the plan summary for a transaction is correct
+ *  * effective amount
+ *  * raw amount
+ */
+
+test("deposit effective 2 ", (t) => {
+  const coinList: Coin[] = [
+    [kudos(2), 1],
+    [kudos(5), 2],
+  ];
+  const result = calculatePlanFormAvailableCoins(
+    TransactionType.Deposit,
+    kudos(2),
+    "effective",
+    {
+      list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+      exchanges: {
+        "2": {
+          creditDeadline: AbsoluteTime.never(),
+          debitDeadline: AbsoluteTime.never(),
+          wireFee: kudos(0.01),
+          purseFee: kudos(0.01),
+        },
+      },
+    },
+  );
+
+  t.deepEqual(result.rawAmount, "KUDOS:1.98");
+  t.deepEqual(result.effectiveAmount, "KUDOS:2");
+});
+
+test("deposit raw 2 ", (t) => {
+  const coinList: Coin[] = [
+    [kudos(2), 1],
+    [kudos(5), 2],
+  ];
+  const result = calculatePlanFormAvailableCoins(
+    TransactionType.Deposit,
+    kudos(2),
+    "raw",
+    {
+      list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+      exchanges: {
+        "2": {
+          creditDeadline: AbsoluteTime.never(),
+          debitDeadline: AbsoluteTime.never(),
+          wireFee: kudos(0.01),
+          purseFee: kudos(0.01),
+        },
+      },
+    },
+  );
+
+  t.deepEqual(result.rawAmount, "KUDOS:2");
+  t.deepEqual(result.effectiveAmount, "KUDOS:2.04");
+});
+
+test("withdraw raw 21 ", (t) => {
+  const coinList: Coin[] = [
+    [kudos(2), 1],
+    [kudos(5), 2],
+  ];
+  const result = calculatePlanFormAvailableCoins(
+    TransactionType.Withdrawal,
+    kudos(21),
+    "raw",
+    {
+      list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+      exchanges: {
+        "2": {
+          creditDeadline: AbsoluteTime.never(),
+          debitDeadline: AbsoluteTime.never(),
+          wireFee: kudos(0.01),
+          purseFee: kudos(0.01),
+        },
+      },
+    },
+  );
+
+  // denominations configuration is not suitable
+  // for greedy algorithm
+  t.deepEqual(result.rawAmount, "KUDOS:20");
+  t.deepEqual(result.effectiveAmount, "KUDOS:19.96");
 });
diff --git a/packages/taler-wallet-core/src/util/coinSelection.ts 
b/packages/taler-wallet-core/src/util/coinSelection.ts
index 8fd09ea2b..f6d8abcd4 100644
--- a/packages/taler-wallet-core/src/util/coinSelection.ts
+++ b/packages/taler-wallet-core/src/util/coinSelection.ts
@@ -35,6 +35,7 @@ import {
   DenominationInfo,
   DenominationPubKey,
   DenomSelectionState,
+  Duration,
   ForcedCoinSel,
   ForcedDenomSel,
   GetPlanForOperationRequest,
@@ -52,7 +53,11 @@ import {
   AllowedExchangeInfo,
   DenominationRecord,
 } from "../db.js";
-import { getExchangeDetails, isWithdrawableDenom } from "../index.js";
+import {
+  CoinAvailabilityRecord,
+  getExchangeDetails,
+  isWithdrawableDenom,
+} from "../index.js";
 import { InternalWalletState } from "../internal-wallet-state.js";
 import { getMerchantPaymentBalanceDetails } from "../operations/balance.js";
 import { checkDbInvariant, checkLogicInvariant } from "./invariants.js";
@@ -790,120 +795,122 @@ export function selectForcedWithdrawalDenominations(
   };
 }
 
-/**
- * simulate a coin selection and return the amount
- * that will effectively change the wallet balance and
- * the raw amount of the operation
- *
- * @param ws
- * @param br
- * @returns
- */
-export async function getPlanForOperation(
-  ws: InternalWalletState,
-  req: GetPlanForOperationRequest,
-): Promise<GetPlanForOperationResponse> {
-  const amount = Amounts.parseOrThrow(req.instructedAmount);
-
+function getCoinsFilter(req: GetPlanForOperationRequest): CoinsFilter {
   switch (req.type) {
     case TransactionType.Withdrawal: {
-      const availableCoins = await getAvailableCoins(
-        ws,
-        "credit",
-        amount.currency,
-        false,
-        false,
-        undefined,
-        undefined,
-        undefined,
-      );
-      const usableCoins = selectCoinForOperation(
-        "credit",
-        amount,
-        req.mode === "effective" ? "net" : "gross",
-        availableCoins.denoms,
-      );
+      return {
+        exchanges:
+          req.exchangeUrl === undefined ? undefined : [req.exchangeUrl],
+      };
+    }
+    case TransactionType.Deposit: {
+      const payto = parsePaytoUri(req.account);
+      if (!payto) {
+        throw Error(`wrong payto ${req.account}`);
+      }
+      return {
+        wireMethod: payto.targetType,
+      };
+    }
+  }
+}
 
-      return getAmountsWithFee(
-        "credit",
-        usableCoins.totalValue,
-        usableCoins.totalContribution,
-        usableCoins,
+export function calculatePlanFormAvailableCoins(
+  transactionType: TransactionType,
+  amount: AmountJson,
+  mode: "effective" | "raw",
+  availableCoins: AvailableCoins,
+) {
+  const operationType = getOperationType(transactionType);
+  let usableCoins;
+  switch (transactionType) {
+    case TransactionType.Withdrawal: {
+      usableCoins = selectCoinForOperation(
+        operationType,
+        amount,
+        mode === "effective" ? "net" : "gross",
+        availableCoins,
       );
+      break;
     }
     case TransactionType.Deposit: {
-      const payto = parsePaytoUri(req.account)!;
-      const availableCoins = await getAvailableCoins(
-        ws,
-        "debit",
-        amount.currency,
-        true,
-        false,
-        undefined,
-        [payto.targetType],
-        undefined,
-      );
       //FIXME: just doing for 1 exchange now
       //assuming that the wallet has one exchange and all the coins available
       //are from that exchange
+      const wireFee = Object.values(availableCoins.exchanges)[0].wireFee!;
 
-      const wireFee = Object.entries(availableCoins.wfPerExchange)[0][1][
-        payto.targetType
-      ];
-
-      let usableCoins;
-
-      if (req.mode === "effective") {
+      if (mode === "effective") {
         usableCoins = selectCoinForOperation(
-          "debit",
+          operationType,
           amount,
           "gross",
-          availableCoins.denoms,
+          availableCoins,
         );
 
-        usableCoins.totalContribution = Amounts.stringify(
-          Amounts.sub(usableCoins.totalContribution, wireFee).amount,
-        );
+        usableCoins.totalContribution = Amounts.sub(
+          usableCoins.totalContribution,
+          wireFee,
+        ).amount;
       } else {
         const adjustedAmount = Amounts.add(amount, wireFee).amount;
 
         usableCoins = selectCoinForOperation(
-          "debit",
+          operationType,
           adjustedAmount,
-          // amount,
           "net",
-          availableCoins.denoms,
+          availableCoins,
         );
 
-        usableCoins.totalContribution = Amounts.stringify(
-          Amounts.sub(usableCoins.totalContribution, wireFee).amount,
-        );
+        usableCoins.totalContribution = Amounts.sub(
+          usableCoins.totalContribution,
+          wireFee,
+        ).amount;
       }
-
-      return getAmountsWithFee(
-        "debit",
-        usableCoins.totalValue,
-        usableCoins.totalContribution,
-        usableCoins,
-      );
+      break;
     }
     default: {
       throw Error("operation not supported");
     }
   }
+
+  return getAmountsWithFee(
+    operationType,
+    usableCoins!.totalValue,
+    usableCoins!.totalContribution,
+    usableCoins,
+  );
 }
 
-function getAmountsWithFee(
-  op: "debit" | "credit",
-  value: AmountString,
-  contribution: AmountString,
-  details: any,
-): GetPlanForOperationResponse {
-  return {
-    rawAmount: op === "credit" ? value : contribution,
-    effectiveAmount: op === "credit" ? contribution : value,
-    details,
-  };
+/**
+ * simulate a coin selection and return the amount
+ * that will effectively change the wallet balance and
+ * the raw amount of the operation
+ *
+ * @param ws
+ * @param br
+ * @returns
+ */
+export async function getPlanForOperation(
+  ws: InternalWalletState,
+  req: GetPlanForOperationRequest,
+): Promise<GetPlanForOperationResponse> {
+  const amount = Amounts.parseOrThrow(req.instructedAmount);
+  const operationType = getOperationType(req.type);
+  const filter = getCoinsFilter(req);
+
+  const availableCoins = await getAvailableCoins(
+    ws,
+    operationType,
+    amount.currency,
+    filter,
+  );
+
+  return calculatePlanFormAvailableCoins(
+    req.type,
+    amount,
+    req.mode,
+    availableCoins,
+  );
 }
 
 /**
@@ -914,24 +921,20 @@ function getAmountsWithFee(
  * @param denoms list of available denomination for the operation
  * @returns
  */
-function selectCoinForOperation(
+export function selectCoinForOperation(
   op: "debit" | "credit",
   limit: AmountJson,
   mode: "net" | "gross",
-  denoms: AvailableDenom[],
+  coins: AvailableCoins,
 ): SelectedCoins {
   const result: SelectedCoins = {
-    totalValue: Amounts.stringify(Amounts.zeroOfCurrency(limit.currency)),
-    totalWithdrawalFee: Amounts.stringify(
-      Amounts.zeroOfCurrency(limit.currency),
-    ),
-    totalDepositFee: Amounts.stringify(Amounts.zeroOfCurrency(limit.currency)),
-    totalContribution: Amounts.stringify(
-      Amounts.zeroOfCurrency(limit.currency),
-    ),
+    totalValue: Amounts.zeroOfCurrency(limit.currency),
+    totalWithdrawalFee: Amounts.zeroOfCurrency(limit.currency),
+    totalDepositFee: Amounts.zeroOfCurrency(limit.currency),
+    totalContribution: Amounts.zeroOfCurrency(limit.currency),
     coins: [],
   };
-  if (!denoms.length) return result;
+  if (!coins.list.length) return result;
   /**
    * We can make this faster. We should prevent sorting and
    * keep the information ready for multiple calls since this
@@ -940,28 +943,26 @@ function selectCoinForOperation(
    */
 
   //rank coins
-  denoms.sort(
-    op === "credit"
-      ? denomsByDescendingWithdrawContribution
-      : denomsByDescendingDepositContribution,
-  );
+  coins.list.sort(buildRankingForCoins(op));
 
   //take coins in order until amount
   let selectedCoinsAreEnough = false;
   let denomIdx = 0;
-  iterateDenoms: while (denomIdx < denoms.length) {
-    const cur = denoms[denomIdx];
-    // for (const cur of denoms) {
-    let total = op === "credit" ? Number.MAX_SAFE_INTEGER : cur.numAvailable;
-    const opFee = op === "credit" ? cur.feeWithdraw : cur.feeDeposit;
-    const contribution = Amounts.sub(cur.value, opFee).amount;
+  iterateDenoms: while (denomIdx < coins.list.length) {
+    const denom = coins.list[denomIdx];
+    let total =
+      op === "credit" ? Number.MAX_SAFE_INTEGER : denom.totalAvailable ?? 0;
+    const opFee = op === "credit" ? denom.denomWithdraw : denom.denomDeposit;
+    const contribution = Amounts.sub(denom.value, opFee).amount;
 
     if (Amounts.isZero(contribution)) {
       // 0 contribution denoms should be the last
       break iterateDenoms;
     }
+
+    //use Amounts.divmod instead of iterate
     iterateCoins: while (total > 0) {
-      const nextValue = Amounts.add(result.totalValue, cur.value).amount;
+      const nextValue = Amounts.add(result.totalValue, denom.value).amount;
 
       const nextContribution = Amounts.add(
         result.totalContribution,
@@ -975,18 +976,20 @@ function selectCoinForOperation(
         break iterateCoins;
       }
 
-      result.totalValue = Amounts.stringify(nextValue);
-      result.totalContribution = Amounts.stringify(nextContribution);
+      result.totalValue = nextValue;
+      result.totalContribution = nextContribution;
 
-      result.totalDepositFee = Amounts.stringify(
-        Amounts.add(result.totalDepositFee, cur.feeDeposit).amount,
-      );
+      result.totalDepositFee = Amounts.add(
+        result.totalDepositFee,
+        denom.denomDeposit,
+      ).amount;
 
-      result.totalWithdrawalFee = Amounts.stringify(
-        Amounts.add(result.totalWithdrawalFee, cur.feeWithdraw).amount,
-      );
+      result.totalWithdrawalFee = Amounts.add(
+        result.totalWithdrawalFee,
+        denom.denomWithdraw,
+      ).amount;
 
-      result.coins.push(cur.denomPubHash);
+      result.coins.push(denom.id);
 
       if (Amounts.cmp(progress, limit) === 0) {
         selectedCoinsAreEnough = true;
@@ -1021,12 +1024,12 @@ function selectCoinForOperation(
 
   let refreshIdx = 0;
   let choice: RefreshChoice | undefined = undefined;
-  refreshIteration: while (refreshIdx < denoms.length) {
-    const d = denoms[refreshIdx];
+  refreshIteration: while (refreshIdx < coins.list.length) {
+    const d = coins.list[refreshIdx];
     const denomContribution =
       mode === "gross"
-        ? Amounts.sub(d.value, d.feeRefresh).amount
-        : Amounts.sub(d.value, d.feeDeposit, d.feeRefresh).amount;
+        ? Amounts.sub(d.value, d.denomRefresh).amount
+        : Amounts.sub(d.value, d.denomDeposit, d.denomRefresh).amount;
 
     const changeAfterDeposit = Amounts.sub(denomContribution, gap).amount;
     if (Amounts.isZero(changeAfterDeposit)) {
@@ -1038,30 +1041,26 @@ function selectCoinForOperation(
       "credit",
       changeAfterDeposit,
       mode,
-      denoms,
+      coins,
     );
     const totalFee = Amounts.add(
-      d.feeDeposit,
-      d.feeRefresh,
+      d.denomDeposit,
+      d.denomRefresh,
       changeCost.totalWithdrawalFee,
     ).amount;
 
     if (!choice || Amounts.cmp(totalFee, choice.totalFee) === -1) {
       //found cheaper change
       choice = {
-        gap: Amounts.stringify(gap),
-        totalFee: Amounts.stringify(totalFee),
-        selected: d.denomPubHash,
+        gap: gap,
+        totalFee: totalFee,
+        selected: d.id,
         totalValue: d.value,
-        totalRefreshFee: Amounts.stringify(d.feeRefresh),
-        totalDepositFee: d.feeDeposit,
-        totalChangeValue: Amounts.stringify(changeCost.totalValue),
-        totalChangeContribution: Amounts.stringify(
-          changeCost.totalContribution,
-        ),
-        totalChangeWithdrawalFee: Amounts.stringify(
-          changeCost.totalWithdrawalFee,
-        ),
+        totalRefreshFee: d.denomRefresh,
+        totalDepositFee: d.denomDeposit,
+        totalChangeValue: changeCost.totalValue,
+        totalChangeContribution: changeCost.totalContribution,
+        totalChangeWithdrawalFee: changeCost.totalWithdrawalFee,
         change: changeCost.coins,
       };
     }
@@ -1069,22 +1068,25 @@ function selectCoinForOperation(
   }
   if (choice) {
     if (mode === "gross") {
-      result.totalValue = Amounts.stringify(
-        Amounts.add(result.totalValue, gap).amount,
-      );
-      result.totalContribution = Amounts.stringify(
-        Amounts.add(result.totalContribution, gap).amount,
-      );
-      result.totalContribution = Amounts.stringify(
-        Amounts.sub(result.totalContribution, choice.totalFee).amount,
-      );
+      result.totalValue = Amounts.add(result.totalValue, gap).amount;
+      result.totalContribution = Amounts.add(
+        result.totalContribution,
+        gap,
+      ).amount;
+      result.totalContribution = Amounts.sub(
+        result.totalContribution,
+        choice.totalFee,
+      ).amount;
     } else {
-      result.totalContribution = Amounts.stringify(
-        Amounts.add(result.totalContribution, gap).amount,
-      );
-      result.totalValue = Amounts.stringify(
-        Amounts.add(result.totalValue, gap, choice.totalFee).amount,
-      );
+      result.totalContribution = Amounts.add(
+        result.totalContribution,
+        gap,
+      ).amount;
+      result.totalValue = Amounts.add(
+        result.totalValue,
+        gap,
+        choice.totalFee,
+      ).amount;
     }
   }
 
@@ -1093,50 +1095,105 @@ function selectCoinForOperation(
   return result;
 }
 
-function denomsByDescendingDepositContribution(
-  d1: AvailableDenom,
-  d2: AvailableDenom,
-) {
-  const contrib1 = Amounts.sub(d1.value, d1.feeDeposit).amount;
-  const contrib2 = Amounts.sub(d2.value, d2.feeDeposit).amount;
-  return (
-    Amounts.cmp(contrib2, contrib1) || strcmp(d1.denomPubHash, d2.denomPubHash)
-  );
+type CompareCoinsFunction = (d1: CoinInfo, d2: CoinInfo) => -1 | 0 | 1;
+function buildRankingForCoins(op: "debit" | "credit"): CompareCoinsFunction {
+  function getFee(d: CoinInfo) {
+    return op === "credit" ? d.denomWithdraw : d.denomDeposit;
+  }
+  //different exchanges may have different wireFee
+  //ranking should take the relative contribution in the exchange
+  //which is (value - denomFee / fixedFee)
+  // where denomFee is withdraw or deposit
+  // and fixedFee can be purse or wire
+  return function rank(d1: CoinInfo, d2: CoinInfo) {
+    const contrib1 = Amounts.sub(d1.value, getFee(d1)).amount;
+    const contrib2 = Amounts.sub(d2.value, getFee(d2)).amount;
+    return (
+      Amounts.cmp(contrib2, contrib1) ||
+      Duration.cmp(d1.duration, d2.duration) ||
+      strcmp(d1.id, d2.id)
+    );
+  };
 }
-function denomsByDescendingWithdrawContribution(
-  d1: AvailableDenom,
-  d2: AvailableDenom,
-) {
-  const contrib1 = Amounts.sub(d1.value, d1.feeWithdraw).amount;
-  const contrib2 = Amounts.sub(d2.value, d2.feeWithdraw).amount;
-  return (
-    Amounts.cmp(contrib2, contrib1) || strcmp(d1.denomPubHash, d2.denomPubHash)
-  );
+
+function getOperationType(txType: TransactionType): "credit" | "debit" {
+  const operationType =
+    txType === TransactionType.Withdrawal
+      ? ("credit" as const)
+      : txType === TransactionType.Deposit
+      ? ("debit" as const)
+      : undefined;
+  if (!operationType) {
+    throw Error(`operation type ${txType} not supported`);
+  }
+  return operationType;
+}
+
+function getAmountsWithFee(
+  op: "debit" | "credit",
+  value: AmountJson,
+  contribution: AmountJson,
+  details: any,
+): GetPlanForOperationResponse {
+  return {
+    rawAmount: Amounts.stringify(op === "credit" ? value : contribution),
+    effectiveAmount: Amounts.stringify(op === "credit" ? contribution : value),
+    details,
+  };
 }
 
 interface RefreshChoice {
-  gap: AmountString;
-  totalFee: AmountString;
+  gap: AmountJson;
+  totalFee: AmountJson;
   selected: string;
 
-  totalValue: AmountString;
-  totalDepositFee: AmountString;
-  totalRefreshFee: AmountString;
-  totalChangeValue: AmountString;
-  totalChangeContribution: AmountString;
-  totalChangeWithdrawalFee: AmountString;
+  totalValue: AmountJson;
+  totalDepositFee: AmountJson;
+  totalRefreshFee: AmountJson;
+  totalChangeValue: AmountJson;
+  totalChangeContribution: AmountJson;
+  totalChangeWithdrawalFee: AmountJson;
   change: string[];
 }
 
 interface SelectedCoins {
-  totalValue: AmountString;
-  totalContribution: AmountString;
-  totalWithdrawalFee: AmountString;
-  totalDepositFee: AmountString;
+  totalValue: AmountJson;
+  totalContribution: AmountJson;
+  totalWithdrawalFee: AmountJson;
+  totalDepositFee: AmountJson;
   coins: string[];
   refresh?: RefreshChoice;
 }
 
+interface AvailableCoins {
+  list: CoinInfo[];
+  exchanges: Record<string, ExchangeInfo>;
+}
+interface CoinInfo {
+  id: string;
+  value: AmountJson;
+  denomDeposit: AmountJson;
+  denomWithdraw: AmountJson;
+  denomRefresh: AmountJson;
+  totalAvailable: number | undefined;
+  exchangeWire: AmountJson | undefined;
+  exchangePurse: AmountJson | undefined;
+  duration: Duration;
+  maxAge: number;
+}
+interface ExchangeInfo {
+  wireFee: AmountJson | undefined;
+  purseFee: AmountJson | undefined;
+  creditDeadline: AbsoluteTime;
+  debitDeadline: AbsoluteTime;
+}
+
+interface CoinsFilter {
+  shouldCalculatePurseFee?: boolean;
+  exchanges?: string[];
+  wireMethod?: string;
+  ageRestricted?: number;
+}
 /**
  * Get all the denoms that can be used for a operation that is limited
  * by the following restrictions.
@@ -1147,12 +1204,8 @@ async function getAvailableCoins(
   ws: InternalWalletState,
   op: "credit" | "debit",
   currency: string,
-  shouldCalculateWireFee: boolean,
-  shouldCalculatePurseFee: boolean,
-  exchangeFilter: string[] | undefined,
-  wireMethodFilter: string[] | undefined,
-  ageRestrictedFilter: number | undefined,
-) {
+  filters: CoinsFilter = {},
+): Promise<AvailableCoins> {
   return await ws.db
     .mktx((x) => [
       x.exchanges,
@@ -1161,90 +1214,103 @@ async function getAvailableCoins(
       x.coinAvailability,
     ])
     .runReadOnly(async (tx) => {
-      const denoms: AvailableDenom[] = [];
-      const wfPerExchange: Record<string, Record<string, AmountJson>> = {};
-      const pfPerExchange: Record<string, AmountJson> = {};
+      const list: CoinInfo[] = [];
+      const exchanges: Record<string, ExchangeInfo> = {};
 
       const databaseExchanges = await tx.exchanges.iter().toArray();
-      const exchanges =
-        exchangeFilter === undefined
-          ? databaseExchanges.map((e) => e.baseUrl)
-          : exchangeFilter;
+      const filteredExchanges =
+        filters.exchanges ?? databaseExchanges.map((e) => e.baseUrl);
 
-      for (const exchangeBaseUrl of exchanges) {
+      for (const exchangeBaseUrl of filteredExchanges) {
         const exchangeDetails = await getExchangeDetails(tx, exchangeBaseUrl);
         // 1.- exchange has same currency
         if (exchangeDetails?.currency !== currency) {
           continue;
         }
 
-        const wireMethodFee: Record<string, AmountJson> = {};
+        let deadline = AbsoluteTime.never();
         // 2.- exchange supports wire method
-        if (shouldCalculateWireFee) {
-          for (const acc of exchangeDetails.wireInfo.accounts) {
-            const pp = parsePaytoUri(acc.payto_uri);
-            checkLogicInvariant(!!pp);
-            // also check that wire method is supported now
-            if (wireMethodFilter !== undefined) {
-              if (wireMethodFilter.indexOf(pp.targetType) === -1) {
-                continue;
-              }
-            }
-            const wireFeeStr = exchangeDetails.wireInfo.feesForType[
-              pp.targetType
-            ]?.find((x) => {
-              return AbsoluteTime.isBetween(
-                AbsoluteTime.now(),
-                AbsoluteTime.fromProtocolTimestamp(x.startStamp),
-                AbsoluteTime.fromProtocolTimestamp(x.endStamp),
-              );
-            })?.wireFee;
+        let wireFee: AmountJson | undefined;
+        if (filters.wireMethod) {
+          const wireMethodWithDates =
+            exchangeDetails.wireInfo.feesForType[filters.wireMethod];
 
-            if (wireFeeStr) {
-              wireMethodFee[pp.targetType] = Amounts.parseOrThrow(wireFeeStr);
-            }
-            break;
+          if (!wireMethodWithDates) {
+            throw Error(
+              `exchange ${exchangeBaseUrl} doesn't have wire method 
${filters.wireMethod}`,
+            );
           }
-          if (Object.keys(wireMethodFee).length === 0) {
+          const wireMethodFee = wireMethodWithDates.find((x) => {
+            return AbsoluteTime.isBetween(
+              AbsoluteTime.now(),
+              AbsoluteTime.fromProtocolTimestamp(x.startStamp),
+              AbsoluteTime.fromProtocolTimestamp(x.endStamp),
+            );
+          });
+
+          if (!wireMethodFee) {
             throw Error(
               `exchange ${exchangeBaseUrl} doesn't have wire fee defined for 
this period`,
             );
           }
+          wireFee = Amounts.parseOrThrow(wireMethodFee.wireFee);
+          deadline = AbsoluteTime.min(
+            deadline,
+            AbsoluteTime.fromProtocolTimestamp(wireMethodFee.endStamp),
+          );
         }
-        wfPerExchange[exchangeBaseUrl] = wireMethodFee;
+        // exchanges[exchangeBaseUrl].wireFee = wireMethodFee;
 
         // 3.- exchange supports wire method
-        if (shouldCalculatePurseFee) {
+        let purseFee: AmountJson | undefined;
+        if (filters.shouldCalculatePurseFee) {
           const purseFeeFound = exchangeDetails.globalFees.find((x) => {
             return AbsoluteTime.isBetween(
               AbsoluteTime.now(),
               AbsoluteTime.fromProtocolTimestamp(x.startDate),
               AbsoluteTime.fromProtocolTimestamp(x.endDate),
             );
-          })?.purseFee;
+          });
           if (!purseFeeFound) {
             throw Error(
               `exchange ${exchangeBaseUrl} doesn't have purse fee defined for 
this period`,
             );
           }
-          const purseFee = Amounts.parseOrThrow(purseFeeFound);
-          pfPerExchange[exchangeBaseUrl] = purseFee;
+          purseFee = Amounts.parseOrThrow(purseFeeFound.purseFee);
+          deadline = AbsoluteTime.min(
+            deadline,
+            AbsoluteTime.fromProtocolTimestamp(purseFeeFound.endDate),
+          );
         }
 
+        let creditDeadline = AbsoluteTime.never();
+        let debitDeadline = AbsoluteTime.never();
         //4.- filter coins restricted by age
         if (op === "credit") {
           const ds = await tx.denominations.indexes.byExchangeBaseUrl.getAll(
             exchangeBaseUrl,
           );
           for (const denom of ds) {
-            denoms.push({
-              ...DenominationRecord.toDenomInfo(denom),
-              numAvailable: Number.MAX_SAFE_INTEGER,
-              maxAge: AgeRestriction.AGE_UNRESTRICTED,
-            });
+            const expiresWithdraw = AbsoluteTime.fromProtocolTimestamp(
+              denom.stampExpireWithdraw,
+            );
+            const expiresDeposit = AbsoluteTime.fromProtocolTimestamp(
+              denom.stampExpireDeposit,
+            );
+            creditDeadline = AbsoluteTime.min(deadline, expiresWithdraw);
+            debitDeadline = AbsoluteTime.min(deadline, expiresDeposit);
+            list.push(
+              buildCoinInfoFromDenom(
+                denom,
+                purseFee,
+                wireFee,
+                AgeRestriction.AGE_UNRESTRICTED,
+                Number.MAX_SAFE_INTEGER, // Max withdrawable from single denom
+              ),
+            );
           }
         } else {
-          const ageLower = !ageRestrictedFilter ? 0 : ageRestrictedFilter;
+          const ageLower = filters.ageRestricted ?? 0;
           const ageUpper = AgeRestriction.AGE_UNRESTRICTED;
 
           const myExchangeCoins =
@@ -1271,19 +1337,58 @@ async function getAvailableCoins(
             if (denom.isRevoked || !denom.isOffered) {
               continue;
             }
-            denoms.push({
-              ...DenominationRecord.toDenomInfo(denom),
-              numAvailable: coinAvail.freshCoinCount ?? 0,
-              maxAge: coinAvail.maxAge,
-            });
+            const expiresWithdraw = AbsoluteTime.fromProtocolTimestamp(
+              denom.stampExpireWithdraw,
+            );
+            const expiresDeposit = AbsoluteTime.fromProtocolTimestamp(
+              denom.stampExpireDeposit,
+            );
+            creditDeadline = AbsoluteTime.min(deadline, expiresWithdraw);
+            debitDeadline = AbsoluteTime.min(deadline, expiresDeposit);
+            list.push(
+              buildCoinInfoFromDenom(
+                denom,
+                purseFee,
+                wireFee,
+                coinAvail.maxAge,
+                coinAvail.freshCoinCount,
+              ),
+            );
           }
         }
+
+        exchanges[exchangeBaseUrl] = {
+          purseFee,
+          wireFee,
+          debitDeadline,
+          creditDeadline,
+        };
       }
 
-      return {
-        denoms,
-        wfPerExchange,
-        pfPerExchange,
-      };
+      return { list, exchanges };
     });
 }
+
+function buildCoinInfoFromDenom(
+  denom: DenominationRecord,
+  purseFee: AmountJson | undefined,
+  wireFee: AmountJson | undefined,
+  maxAge: number,
+  total: number,
+): CoinInfo {
+  return {
+    id: denom.denomPubHash,
+    denomWithdraw: Amounts.parseOrThrow(denom.fees.feeWithdraw),
+    denomDeposit: Amounts.parseOrThrow(denom.fees.feeDeposit),
+    denomRefresh: Amounts.parseOrThrow(denom.fees.feeRefresh),
+    exchangePurse: purseFee,
+    exchangeWire: wireFee,
+    duration: AbsoluteTime.difference(
+      AbsoluteTime.now(),
+      AbsoluteTime.fromProtocolTimestamp(denom.stampExpireDeposit),
+    ),
+    totalAvailable: total,
+    value: DenominationRecord.getValue(denom),
+    maxAge,
+  };
+}
diff --git a/packages/taler-wallet-core/watch_test.sh 
b/packages/taler-wallet-core/watch_test.sh
new file mode 100755
index 000000000..124e18e21
--- /dev/null
+++ b/packages/taler-wallet-core/watch_test.sh
@@ -0,0 +1,3 @@
+#!/bin/bash
+./node_modules/typescript/bin/tsc --watch &
+./node_modules/ava/entrypoints/cli.mjs -w "$@"

-- 
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]