gnunet-svn
[Top][All Lists]
Advanced

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

[taler-wallet-core] 03/03: add test to coin selection algorithm


From: gnunet
Subject: [taler-wallet-core] 03/03: add test to coin selection algorithm
Date: Thu, 15 Jun 2023 18:30:54 +0200

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

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

commit d0d7685f169ecad5ba29210973a9e59834c979c7
Author: Sebastian <sebasjm@gmail.com>
AuthorDate: Thu Jun 15 13:07:31 2023 -0300

    add test to coin selection algorithm
---
 .../src/util/coinSelection.test.ts                 | 195 ++++++-
 .../taler-wallet-core/src/util/coinSelection.ts    | 585 ++++++++++++---------
 2 files changed, 538 insertions(+), 242 deletions(-)

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,
+  };
+}

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