gnunet-svn
[Top][All Lists]
Advanced

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

[taler-wallet-core] branch master updated: remove calculate plan (for no


From: gnunet
Subject: [taler-wallet-core] branch master updated: remove calculate plan (for now) implemented simpler API
Date: Tue, 20 Jun 2023 19:30:11 +0200

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

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

The following commit(s) were added to refs/heads/master by this push:
     new 1e9f1fb7a9 remove calculate plan (for now) implemented simpler API
1e9f1fb7a9 is described below

commit 1e9f1fb7a9451ad8fae6474cc831596a9e9a3f2f
Author: Sebastian <sebasjm@gmail.com>
AuthorDate: Tue Jun 20 14:30:02 2023 -0300

    remove calculate plan (for now) implemented simpler API
---
 .../src/util/coinSelection.test.ts                 | 614 ++++++++++----
 .../taler-wallet-core/src/util/coinSelection.ts    | 906 +++++++++++++--------
 packages/taler-wallet-core/src/wallet-api-types.ts |  39 +
 packages/taler-wallet-core/src/wallet.ts           |  36 +-
 4 files changed, 1092 insertions(+), 503 deletions(-)

diff --git a/packages/taler-wallet-core/src/util/coinSelection.test.ts 
b/packages/taler-wallet-core/src/util/coinSelection.test.ts
index ab3b2c4f8..3073b69c7 100644
--- a/packages/taler-wallet-core/src/util/coinSelection.test.ts
+++ b/packages/taler-wallet-core/src/util/coinSelection.test.ts
@@ -13,13 +13,6 @@
  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/>
  */
-import test, { ExecutionContext } from "ava";
-import {
-  AmountMode,
-  OperationType,
-  calculatePlanFormAvailableCoins,
-  selectCoinForOperation,
-} from "./coinSelection.js";
 import {
   AbsoluteTime,
   AgeRestriction,
@@ -27,28 +20,31 @@ import {
   Amounts,
   Duration,
   TransactionAmountMode,
-  TransactionType,
 } from "@gnu-taler/taler-util";
+import test, { ExecutionContext } from "ava";
+import {
+  CoinInfo,
+  convertDepositAmountForAvailableCoins,
+  convertWithdrawalAmountFromAvailableCoins,
+  getMaxDepositAmountForAvailableCoins,
+} from "./coinSelection.js";
 
-function expect(t: ExecutionContext, thing: any): any {
-  return {
-    deep: {
-      equal: (another: any) => t.deepEqual(thing, another),
-      equals: (another: any) => t.deepEqual(thing, another),
-    },
+function makeCurrencyHelper(currency: string) {
+  return (sx: TemplateStringsArray, ...vx: any[]) => {
+    const s = String.raw({ raw: sx }, ...vx);
+    return Amounts.parseOrThrow(`${currency}:${s}`);
   };
 }
 
-function kudos(v: number): AmountJson {
-  return Amounts.fromFloat(v, "KUDOS");
-}
+const kudos = makeCurrencyHelper("kudos");
 
-function defaultFeeConfig(value: AmountJson, totalAvailable: number) {
+function defaultFeeConfig(value: AmountJson, totalAvailable: number): CoinInfo 
{
   return {
     id: Amounts.stringify(value),
-    denomDeposit: kudos(0.01),
-    denomRefresh: kudos(0.01),
-    denomWithdraw: kudos(0.01),
+    denomDeposit: kudos`0.01`,
+    denomRefresh: kudos`0.01`,
+    denomWithdraw: kudos`0.01`,
+    exchangeBaseUrl: "1",
     duration: Duration.getForever(),
     exchangePurse: undefined,
     exchangeWire: undefined,
@@ -60,242 +56,574 @@ function defaultFeeConfig(value: AmountJson, 
totalAvailable: number) {
 type Coin = [AmountJson, number];
 
 /**
- * selectCoinForOperation test
+ * Making a deposit with effective amount
  *
- * Test here should check that the correct coins are selected
  */
 
-test("get effective 2", (t) => {
+test("deposit effective 2", (t) => {
   const coinList: Coin[] = [
-    [kudos(2), 5],
-    [kudos(5), 5],
+    [kudos`2`, 5],
+    [kudos`5`, 5],
   ];
-  const result = selectCoinForOperation(
-    OperationType.Credit,
-    kudos(2),
-    AmountMode.Net,
+  const result = convertDepositAmountForAvailableCoins(
     {
       list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
       exchanges: {},
     },
+    kudos`2`,
+    TransactionAmountMode.Effective,
   );
-  expect(t, result.coins).deep.equal(["KUDOS:2"]);
-  t.assert(result.refresh === undefined);
+  t.is(Amounts.stringifyValue(result.effective), "2");
+  t.is(Amounts.stringifyValue(result.raw), "1.99");
 });
 
-test("get raw 4", (t) => {
+test("deposit effective 10", (t) => {
   const coinList: Coin[] = [
-    [kudos(2), 5],
-    [kudos(5), 5],
+    [kudos`2`, 5],
+    [kudos`5`, 5],
   ];
-  const result = selectCoinForOperation(
-    OperationType.Credit,
-    kudos(4),
-    AmountMode.Gross,
+  const result = convertDepositAmountForAvailableCoins(
     {
       list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
       exchanges: {},
     },
+    kudos`10`,
+    TransactionAmountMode.Effective,
   );
+  t.is(Amounts.stringifyValue(result.effective), "10");
+  t.is(Amounts.stringifyValue(result.raw), "9.98");
+});
 
-  expect(t, result.coins).deep.equal(["KUDOS:2", "KUDOS:2"]);
-  t.assert(result.refresh === undefined);
+test("deposit effective 24", (t) => {
+  const coinList: Coin[] = [
+    [kudos`2`, 5],
+    [kudos`5`, 5],
+  ];
+  const result = convertDepositAmountForAvailableCoins(
+    {
+      list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+      exchanges: {},
+    },
+    kudos`24`,
+    TransactionAmountMode.Effective,
+  );
+  t.is(Amounts.stringifyValue(result.effective), "24");
+  t.is(Amounts.stringifyValue(result.raw), "23.94");
 });
 
-test("get raw 25, diff with demo ", (t) => {
+test("deposit effective 40", (t) => {
   const coinList: Coin[] = [
-    [kudos(0.1), 0],
-    [kudos(1), 0],
-    [kudos(2), 0],
-    [kudos(5), 0],
-    [kudos(10), 0],
+    [kudos`2`, 5],
+    [kudos`5`, 5],
   ];
-  const result = selectCoinForOperation(
-    OperationType.Credit,
-    kudos(25),
-    AmountMode.Gross,
+  const result = convertDepositAmountForAvailableCoins(
     {
       list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
       exchanges: {},
     },
+    kudos`40`,
+    TransactionAmountMode.Effective,
   );
+  t.is(Amounts.stringifyValue(result.effective), "35");
+  t.is(Amounts.stringifyValue(result.raw), "34.9");
+});
 
-  expect(t, result.coins).deep.equal(["KUDOS:10", "KUDOS:10", "KUDOS:5"]);
-  t.assert(result.refresh === undefined);
+test("deposit with wire fee effective 2", (t) => {
+  const coinList: Coin[] = [
+    [kudos`2`, 5],
+    [kudos`5`, 5],
+  ];
+  const result = convertDepositAmountForAvailableCoins(
+    {
+      list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+      exchanges: {
+        one: {
+          wireFee: kudos`0.1`,
+          purseFee: kudos`0.00`,
+          creditDeadline: AbsoluteTime.never(),
+          debitDeadline: AbsoluteTime.never(),
+        },
+      },
+    },
+    kudos`2`,
+    TransactionAmountMode.Effective,
+  );
+  t.is(Amounts.stringifyValue(result.effective), "2");
+  t.is(Amounts.stringifyValue(result.raw), "1.89");
 });
 
-test("send effective 6", (t) => {
+/**
+ * Making a deposit with raw amount, using the result from effective
+ *
+ */
+
+test("deposit raw 1.99 (effective 2)", (t) => {
   const coinList: Coin[] = [
-    [kudos(2), 5],
-    [kudos(5), 5],
+    [kudos`2`, 5],
+    [kudos`5`, 5],
   ];
-  const result = selectCoinForOperation(
-    OperationType.Debit,
-    kudos(6),
-    AmountMode.Gross,
+  const result = convertDepositAmountForAvailableCoins(
     {
       list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
       exchanges: {},
     },
+    kudos`1.99`,
+    TransactionAmountMode.Raw,
   );
-
-  expect(t, result.coins).deep.equal(["KUDOS:5"]);
-  t.assert(result.refresh?.selected === "KUDOS:2");
+  t.is(Amounts.stringifyValue(result.effective), "2");
+  t.is(Amounts.stringifyValue(result.raw), "1.99");
 });
 
-test("send raw 6", (t) => {
+test("deposit raw 9.98 (effective 10)", (t) => {
   const coinList: Coin[] = [
-    [kudos(2), 5],
-    [kudos(5), 5],
+    [kudos`2`, 5],
+    [kudos`5`, 5],
   ];
-  const result = selectCoinForOperation(
-    OperationType.Debit,
-    kudos(6),
-    AmountMode.Gross,
+  const result = convertDepositAmountForAvailableCoins(
     {
       list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
       exchanges: {},
     },
+    kudos`9.98`,
+    TransactionAmountMode.Raw,
   );
+  t.is(Amounts.stringifyValue(result.effective), "10");
+  t.is(Amounts.stringifyValue(result.raw), "9.98");
+});
 
-  expect(t, result.coins).deep.equal(["KUDOS:5"]);
-  t.assert(result.refresh?.selected === "KUDOS:2");
+test("deposit raw 23.94 (effective 24)", (t) => {
+  const coinList: Coin[] = [
+    [kudos`2`, 5],
+    [kudos`5`, 5],
+  ];
+  const result = convertDepositAmountForAvailableCoins(
+    {
+      list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+      exchanges: {},
+    },
+    kudos`23.94`,
+    TransactionAmountMode.Raw,
+  );
+  t.is(Amounts.stringifyValue(result.effective), "24");
+  t.is(Amounts.stringifyValue(result.raw), "23.94");
 });
 
-test("send raw 20 (not enough)", (t) => {
+test("deposit raw 34.9 (effective 40)", (t) => {
   const coinList: Coin[] = [
-    [kudos(2), 1],
-    [kudos(5), 2],
+    [kudos`2`, 5],
+    [kudos`5`, 5],
   ];
-  const result = selectCoinForOperation(
-    OperationType.Debit,
-    kudos(20),
-    AmountMode.Gross,
+  const result = convertDepositAmountForAvailableCoins(
     {
       list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
       exchanges: {},
     },
+    kudos`34.9`,
+    TransactionAmountMode.Raw,
   );
+  t.is(Amounts.stringifyValue(result.effective), "35");
+  t.is(Amounts.stringifyValue(result.raw), "34.9");
+});
 
-  expect(t, result.coins).deep.equal(["KUDOS:5", "KUDOS:5", "KUDOS:2"]);
-  t.assert(result.refresh === undefined);
+test("deposit with wire fee raw 2", (t) => {
+  const coinList: Coin[] = [
+    [kudos`2`, 5],
+    [kudos`5`, 5],
+  ];
+  const result = convertDepositAmountForAvailableCoins(
+    {
+      list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+      exchanges: {
+        one: {
+          wireFee: kudos`0.1`,
+          purseFee: kudos`0.00`,
+          creditDeadline: AbsoluteTime.never(),
+          debitDeadline: AbsoluteTime.never(),
+        },
+      },
+    },
+    kudos`2`,
+    TransactionAmountMode.Effective,
+  );
+  t.is(Amounts.stringifyValue(result.effective), "2");
+  t.is(Amounts.stringifyValue(result.raw), "1.89");
 });
 
 /**
- * calculatePlanFormAvailableCoins test
+ * Calculating the max amount possible to deposit
  *
- * Test here should check that the plan summary for a transaction is correct
- *  * effective amount
- *  * raw amount
  */
 
-test("deposit effective 2 ", (t) => {
+test("deposit max 35", (t) => {
   const coinList: Coin[] = [
-    [kudos(2), 1],
-    [kudos(5), 2],
+    [kudos`2`, 5],
+    [kudos`5`, 5],
   ];
-  const result = calculatePlanFormAvailableCoins(
-    TransactionType.Deposit,
-    kudos(2),
-    TransactionAmountMode.Effective,
+  const result = getMaxDepositAmountForAvailableCoins(
     {
       list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
       exchanges: {
         "2": {
+          wireFee: kudos`0.00`,
+          purseFee: kudos`0.00`,
           creditDeadline: AbsoluteTime.never(),
           debitDeadline: AbsoluteTime.never(),
-          wireFee: kudos(0.01),
-          purseFee: kudos(0.01),
         },
       },
     },
+    "KUDOS",
   );
-
-  t.deepEqual(result.rawAmount, "KUDOS:1.98");
-  t.deepEqual(result.effectiveAmount, "KUDOS:2");
+  t.is(Amounts.stringifyValue(result.raw), "34.9");
+  t.is(Amounts.stringifyValue(result.effective), "35");
 });
 
-test("deposit raw 2 ", (t) => {
+test("deposit max 35 with wirefee", (t) => {
   const coinList: Coin[] = [
-    [kudos(2), 1],
-    [kudos(5), 2],
+    [kudos`2`, 5],
+    [kudos`5`, 5],
   ];
-  const result = calculatePlanFormAvailableCoins(
-    TransactionType.Deposit,
-    kudos(2),
-    TransactionAmountMode.Raw,
+  const result = getMaxDepositAmountForAvailableCoins(
     {
       list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
       exchanges: {
         "2": {
+          wireFee: kudos`1`,
+          purseFee: kudos`0.00`,
           creditDeadline: AbsoluteTime.never(),
           debitDeadline: AbsoluteTime.never(),
-          wireFee: kudos(0.01),
-          purseFee: kudos(0.01),
         },
       },
     },
+    "KUDOS",
   );
-
-  t.deepEqual(result.rawAmount, "KUDOS:2");
-  t.deepEqual(result.effectiveAmount, "KUDOS:2.04");
+  t.is(Amounts.stringifyValue(result.raw), "33.9");
+  t.is(Amounts.stringifyValue(result.effective), "35");
 });
 
-test("withdraw raw 21 ", (t) => {
+test("deposit max repeated denom", (t) => {
   const coinList: Coin[] = [
-    [kudos(2), 1],
-    [kudos(5), 2],
+    [kudos`2`, 1],
+    [kudos`2`, 1],
+    [kudos`5`, 1],
   ];
-  const result = calculatePlanFormAvailableCoins(
-    TransactionType.Withdrawal,
-    kudos(21),
-    TransactionAmountMode.Raw,
+  const result = getMaxDepositAmountForAvailableCoins(
     {
       list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
       exchanges: {
         "2": {
+          wireFee: kudos`0.00`,
+          purseFee: kudos`0.00`,
           creditDeadline: AbsoluteTime.never(),
           debitDeadline: AbsoluteTime.never(),
-          wireFee: kudos(0.01),
-          purseFee: kudos(0.01),
         },
       },
     },
+    "KUDOS",
   );
+  t.is(Amounts.stringifyValue(result.raw), "8.97");
+  t.is(Amounts.stringifyValue(result.effective), "9");
+});
 
-  // denominations configuration is not suitable
-  // for greedy algorithm
-  t.deepEqual(result.rawAmount, "KUDOS:20");
-  t.deepEqual(result.effectiveAmount, "KUDOS:19.96");
+/**
+ * Making a withdrawal with effective amount
+ *
+ */
+
+test("withdraw effective 2", (t) => {
+  const coinList: Coin[] = [
+    [kudos`2`, 5],
+    [kudos`5`, 5],
+  ];
+  const result = convertWithdrawalAmountFromAvailableCoins(
+    {
+      list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+      exchanges: {},
+    },
+    kudos`2`,
+    TransactionAmountMode.Effective,
+  );
+  t.is(Amounts.stringifyValue(result.effective), "2");
+  t.is(Amounts.stringifyValue(result.raw), "2.01");
+});
+
+test("withdraw effective 10", (t) => {
+  const coinList: Coin[] = [
+    [kudos`2`, 5],
+    [kudos`5`, 5],
+  ];
+  const result = convertWithdrawalAmountFromAvailableCoins(
+    {
+      list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+      exchanges: {},
+    },
+    kudos`10`,
+    TransactionAmountMode.Effective,
+  );
+  t.is(Amounts.stringifyValue(result.effective), "10");
+  t.is(Amounts.stringifyValue(result.raw), "10.02");
+});
+
+test("withdraw effective 24", (t) => {
+  const coinList: Coin[] = [
+    [kudos`2`, 5],
+    [kudos`5`, 5],
+  ];
+  const result = convertWithdrawalAmountFromAvailableCoins(
+    {
+      list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+      exchanges: {},
+    },
+    kudos`24`,
+    TransactionAmountMode.Effective,
+  );
+  t.is(Amounts.stringifyValue(result.effective), "24");
+  t.is(Amounts.stringifyValue(result.raw), "24.06");
 });
 
-test("withdraw raw 25, diff with demo ", (t) => {
+test("withdraw effective 40", (t) => {
   const coinList: Coin[] = [
-    [kudos(0.1), 0],
-    [kudos(1), 0],
-    [kudos(2), 0],
-    [kudos(5), 0],
-    [kudos(10), 0],
+    [kudos`2`, 5],
+    [kudos`5`, 5],
   ];
-  const result = calculatePlanFormAvailableCoins(
-    TransactionType.Withdrawal,
-    kudos(25),
+  const result = convertWithdrawalAmountFromAvailableCoins(
+    {
+      list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+      exchanges: {},
+    },
+    kudos`40`,
+    TransactionAmountMode.Effective,
+  );
+  t.is(Amounts.stringifyValue(result.effective), "40");
+  t.is(Amounts.stringifyValue(result.raw), "40.08");
+});
+
+/**
+ * Making a deposit with raw amount, using the result from effective
+ *
+ */
+
+test("withdraw raw 2.01 (effective 2)", (t) => {
+  const coinList: Coin[] = [
+    [kudos`2`, 5],
+    [kudos`5`, 5],
+  ];
+  const result = convertWithdrawalAmountFromAvailableCoins(
+    {
+      list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+      exchanges: {},
+    },
+    kudos`2.01`,
     TransactionAmountMode.Raw,
+  );
+  t.is(Amounts.stringifyValue(result.effective), "2");
+  t.is(Amounts.stringifyValue(result.raw), "2.01");
+});
+
+test("withdraw raw 10.02 (effective 10)", (t) => {
+  const coinList: Coin[] = [
+    [kudos`2`, 5],
+    [kudos`5`, 5],
+  ];
+  const result = convertWithdrawalAmountFromAvailableCoins(
     {
       list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
-      exchanges: {
-        "2": {
-          creditDeadline: AbsoluteTime.never(),
-          debitDeadline: AbsoluteTime.never(),
-          wireFee: kudos(0.01),
-          purseFee: kudos(0.01),
-        },
-      },
+      exchanges: {},
+    },
+    kudos`10.02`,
+    TransactionAmountMode.Raw,
+  );
+  t.is(Amounts.stringifyValue(result.effective), "10");
+  t.is(Amounts.stringifyValue(result.raw), "10.02");
+});
+
+test("withdraw raw 24.06 (effective 24)", (t) => {
+  const coinList: Coin[] = [
+    [kudos`2`, 5],
+    [kudos`5`, 5],
+  ];
+  const result = convertWithdrawalAmountFromAvailableCoins(
+    {
+      list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+      exchanges: {},
+    },
+    kudos`24.06`,
+    TransactionAmountMode.Raw,
+  );
+  t.is(Amounts.stringifyValue(result.effective), "24");
+  t.is(Amounts.stringifyValue(result.raw), "24.06");
+});
+
+test("withdraw raw 40.08 (effective 40)", (t) => {
+  const coinList: Coin[] = [
+    [kudos`2`, 5],
+    [kudos`5`, 5],
+  ];
+  const result = convertWithdrawalAmountFromAvailableCoins(
+    {
+      list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+      exchanges: {},
+    },
+    kudos`40.08`,
+    TransactionAmountMode.Raw,
+  );
+  t.is(Amounts.stringifyValue(result.effective), "40");
+  t.is(Amounts.stringifyValue(result.raw), "40.08");
+});
+
+test("withdraw raw 25", (t) => {
+  const coinList: Coin[] = [
+    [kudos`0.1`, 0],
+    [kudos`1`, 0],
+    [kudos`2`, 0],
+    [kudos`5`, 0],
+  ];
+  const result = convertWithdrawalAmountFromAvailableCoins(
+    {
+      list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+      exchanges: {},
+    },
+    kudos`25`,
+    TransactionAmountMode.Raw,
+  );
+  t.is(Amounts.stringifyValue(result.effective), "24.8");
+  t.is(Amounts.stringifyValue(result.raw), "24.94");
+});
+
+test("withdraw effective 24.8 (raw 25)", (t) => {
+  const coinList: Coin[] = [
+    [kudos`0.1`, 0],
+    [kudos`1`, 0],
+    [kudos`2`, 0],
+    [kudos`5`, 0],
+  ];
+  const result = convertWithdrawalAmountFromAvailableCoins(
+    {
+      list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+      exchanges: {},
     },
+    kudos`24.8`,
+    TransactionAmountMode.Effective,
+  );
+  t.is(Amounts.stringifyValue(result.effective), "24.8");
+  t.is(Amounts.stringifyValue(result.raw), "24.94");
+});
+
+/**
+ * Making a deposit with refresh
+ *
+ */
+
+test("deposit with refresh: effective 3", (t) => {
+  const coinList: Coin[] = [
+    [kudos`0.1`, 0],
+    [kudos`1`, 0],
+    [kudos`2`, 5],
+    [kudos`5`, 5],
+  ];
+  const result = convertDepositAmountForAvailableCoins(
+    {
+      list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+      exchanges: {},
+    },
+    kudos`3`,
+    TransactionAmountMode.Effective,
   );
+  t.is(Amounts.stringifyValue(result.effective), "3.1");
+  t.is(Amounts.stringifyValue(result.raw), "2.98");
+  expectDefined(t, result.refresh);
+  //FEES
+  //deposit  2 x 0.01
+  //refresh  1 x 0.01
+  //withdraw 9 x 0.01
+  //-----------------
+  //op           0.12
 
-  t.deepEqual(result.rawAmount, "KUDOS:25");
-  // here demo report KUDOS:0.2 fee
-  // t.deepEqual(result.effectiveAmount, "KUDOS:24.80");
-  t.deepEqual(result.effectiveAmount, "KUDOS:24.97");
+  //coins sent  2 x 2.0
+  //coins recv  9 x 0.1
+  //-------------------
+  //effective       3.10
+  //raw             2.98
+  t.is(Amounts.stringifyValue(result.refresh.selected.id), "2");
+  t.deepEqual(asCoinList(result.refresh.coins), [[kudos`0.1`, 9]]);
 });
+
+test("deposit with refresh: raw 2.98 (effective 3)", (t) => {
+  const coinList: Coin[] = [
+    [kudos`0.1`, 0],
+    [kudos`1`, 0],
+    [kudos`2`, 5],
+    [kudos`5`, 5],
+  ];
+  const result = convertDepositAmountForAvailableCoins(
+    {
+      list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+      exchanges: {},
+    },
+    kudos`2.98`,
+    TransactionAmountMode.Raw,
+  );
+  t.is(Amounts.stringifyValue(result.effective), "3.2");
+  t.is(Amounts.stringifyValue(result.raw), "3.09");
+  expectDefined(t, result.refresh);
+  //FEES
+  //deposit  1 x 0.01
+  //refresh  1 x 0.01
+  //withdraw 8 x 0.01
+  //-----------------
+  //op           0.10
+
+  //coins sent  1 x 2.0
+  //coins recv  8 x 0.1
+  //-------------------
+  //effective       3.20
+  //raw             3.09
+  t.is(Amounts.stringifyValue(result.refresh.selected.id), "2");
+  t.deepEqual(asCoinList(result.refresh.coins), [[kudos`0.1`, 8]]);
+});
+
+test("deposit with refresh: effective 3.2 (raw 2.98)", (t) => {
+  const coinList: Coin[] = [
+    [kudos`0.1`, 0],
+    [kudos`1`, 0],
+    [kudos`2`, 5],
+    [kudos`5`, 5],
+  ];
+  const result = convertDepositAmountForAvailableCoins(
+    {
+      list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
+      exchanges: {},
+    },
+    kudos`3.2`,
+    TransactionAmountMode.Effective,
+  );
+  t.is(Amounts.stringifyValue(result.effective), "3.3");
+  t.is(Amounts.stringifyValue(result.raw), "3.2");
+  expectDefined(t, result.refresh);
+  //FEES
+  //deposit  2 x 0.01
+  //refresh  1 x 0.01
+  //withdraw 7 x 0.01
+  //-----------------
+  //op           0.10
+
+  //coins sent  2 x 2.0
+  //coins recv  7 x 0.1
+  //-------------------
+  //effective       3.30
+  //raw             3.20
+  t.is(Amounts.stringifyValue(result.refresh.selected.id), "2");
+  t.deepEqual(asCoinList(result.refresh.coins), [[kudos`0.1`, 7]]);
+});
+
+function expectDefined<T>(
+  t: ExecutionContext,
+  v: T | undefined,
+): asserts v is T {
+  t.assert(v !== undefined);
+}
+
+function asCoinList(v: { info: CoinInfo; size: number }[]): any {
+  return v.map((c) => {
+    return [c.info.value, c.size];
+  });
+}
diff --git a/packages/taler-wallet-core/src/util/coinSelection.ts 
b/packages/taler-wallet-core/src/util/coinSelection.ts
index c5a810c4f..26dc0dedc 100644
--- a/packages/taler-wallet-core/src/util/coinSelection.ts
+++ b/packages/taler-wallet-core/src/util/coinSelection.ts
@@ -29,15 +29,18 @@ import {
   AgeCommitmentProof,
   AgeRestriction,
   AmountJson,
+  AmountResponse,
   Amounts,
   AmountString,
   CoinStatus,
+  ConvertAmountRequest,
   DenominationInfo,
   DenominationPubKey,
   DenomSelectionState,
   Duration,
   ForcedCoinSel,
   ForcedDenomSel,
+  GetAmountRequest,
   GetPlanForOperationRequest,
   GetPlanForOperationResponse,
   j2s,
@@ -816,106 +819,6 @@ function getCoinsFilter(req: GetPlanForOperationRequest): 
CoinsFilter {
   }
 }
 
-export function calculatePlanFormAvailableCoins(
-  transactionType: TransactionType,
-  amount: AmountJson,
-  mode: TransactionAmountMode,
-  availableCoins: AvailableCoins,
-) {
-  const operationType = getOperationType(transactionType);
-  let usableCoins;
-  switch (transactionType) {
-    case TransactionType.Withdrawal: {
-      usableCoins = selectCoinForOperation(
-        operationType,
-        amount,
-        mode === TransactionAmountMode.Effective
-          ? AmountMode.Net
-          : AmountMode.Gross,
-        availableCoins,
-      );
-      break;
-    }
-    case TransactionType.Deposit: {
-      //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!;
-
-      if (mode === TransactionAmountMode.Effective) {
-        usableCoins = selectCoinForOperation(
-          operationType,
-          amount,
-          AmountMode.Gross,
-          availableCoins,
-        );
-
-        usableCoins.totalContribution = Amounts.sub(
-          usableCoins.totalContribution,
-          wireFee,
-        ).amount;
-      } else {
-        const adjustedAmount = Amounts.add(amount, wireFee).amount;
-
-        usableCoins = selectCoinForOperation(
-          operationType,
-          adjustedAmount,
-          AmountMode.Net,
-          availableCoins,
-        );
-
-        usableCoins.totalContribution = Amounts.sub(
-          usableCoins.totalContribution,
-          wireFee,
-        ).amount;
-      }
-      break;
-    }
-    default: {
-      throw Error("operation not supported");
-    }
-  }
-
-  return getAmountsWithFee(
-    operationType,
-    usableCoins!.totalValue,
-    usableCoins!.totalContribution,
-    usableCoins,
-  );
-}
-
-/**
- * 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,
-  );
-}
-
 /**
  * If the operation going to be plan subtracts
  * or adds amount in the wallet db
@@ -925,225 +828,6 @@ export enum OperationType {
   Debit = "debit",
 }
 
-/**
- * How the amount should be interpreted
- * net = without fee
- * gross = with fee
- *
- * Net value is always lower than gross
- */
-export enum AmountMode {
-  Net = "net",
-  Gross = "gross",
-}
-
-/**
- *
- * @param op defined which fee are we taking into consideration: deposits or 
withdraw
- * @param limit the total amount limit of the operation
- * @param mode if the total amount is includes the fees or just the 
contribution
- * @param denoms list of available denomination for the operation
- * @returns
- */
-export function selectCoinForOperation(
-  op: OperationType,
-  limit: AmountJson,
-  mode: AmountMode,
-  coins: AvailableCoins,
-): SelectedCoins {
-  const result: SelectedCoins = {
-    totalValue: Amounts.zeroOfCurrency(limit.currency),
-    totalWithdrawalFee: Amounts.zeroOfCurrency(limit.currency),
-    totalDepositFee: Amounts.zeroOfCurrency(limit.currency),
-    totalContribution: Amounts.zeroOfCurrency(limit.currency),
-    coins: [],
-  };
-  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
-   * function is expected to work on embedded devices and
-   * create a response on key press
-   */
-
-  //rank coins
-  coins.list.sort(buildRankingForCoins(op));
-
-  //take coins in order until amount
-  let selectedCoinsAreEnough = false;
-  let denomIdx = 0;
-  iterateDenoms: while (denomIdx < coins.list.length) {
-    const denom = coins.list[denomIdx];
-    let total =
-      op === OperationType.Credit
-        ? Number.MAX_SAFE_INTEGER
-        : denom.totalAvailable ?? 0;
-    const opFee =
-      op === OperationType.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, denom.value).amount;
-
-      const nextContribution = Amounts.add(
-        result.totalContribution,
-        contribution,
-      ).amount;
-
-      const progress = mode === AmountMode.Gross ? nextValue : 
nextContribution;
-
-      if (Amounts.cmp(progress, limit) === 1) {
-        //the current coin is more than we need, try next denom
-        break iterateCoins;
-      }
-
-      result.totalValue = nextValue;
-      result.totalContribution = nextContribution;
-
-      result.totalDepositFee = Amounts.add(
-        result.totalDepositFee,
-        denom.denomDeposit,
-      ).amount;
-
-      result.totalWithdrawalFee = Amounts.add(
-        result.totalWithdrawalFee,
-        denom.denomWithdraw,
-      ).amount;
-
-      result.coins.push(denom.id);
-
-      if (Amounts.cmp(progress, limit) === 0) {
-        selectedCoinsAreEnough = true;
-        // we have just enough coins, complete
-        break iterateDenoms;
-      }
-
-      //go next coin
-      total--;
-    }
-    //go next denom
-    denomIdx++;
-  }
-
-  if (selectedCoinsAreEnough) {
-    // we made it
-    return result;
-  }
-  if (op === OperationType.Credit) {
-    //doing withdraw there is no way to cover the gap
-    return result;
-  }
-  //tried all the coins but there is a gap
-  //doing deposit we can try refreshing coins
-
-  const total =
-    mode === AmountMode.Gross ? result.totalValue : result.totalContribution;
-  const gap = Amounts.sub(limit, total).amount;
-
-  //about recursive calls
-  //the only way to get here is by doing a deposit (that will do a refresh)
-  //and now we are calculating fee for credit (which does not need to 
calculate refresh)
-
-  let refreshIdx = 0;
-  let choice: RefreshChoice | undefined = undefined;
-  refreshIteration: while (refreshIdx < coins.list.length) {
-    const d = coins.list[refreshIdx];
-    const denomContribution =
-      mode === AmountMode.Gross
-        ? 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)) {
-      //the rest of the coins are very small
-      break refreshIteration;
-    }
-
-    const changeCost = selectCoinForOperation(
-      OperationType.Credit,
-      changeAfterDeposit,
-      mode,
-      coins,
-    );
-    const totalFee = Amounts.add(
-      d.denomDeposit,
-      d.denomRefresh,
-      changeCost.totalWithdrawalFee,
-    ).amount;
-
-    if (!choice || Amounts.cmp(totalFee, choice.totalFee) === -1) {
-      //found cheaper change
-      choice = {
-        gap: gap,
-        totalFee: totalFee,
-        selected: d.id,
-        totalValue: d.value,
-        totalRefreshFee: d.denomRefresh,
-        totalDepositFee: d.denomDeposit,
-        totalChangeValue: changeCost.totalValue,
-        totalChangeContribution: changeCost.totalContribution,
-        totalChangeWithdrawalFee: changeCost.totalWithdrawalFee,
-        change: changeCost.coins,
-      };
-    }
-    refreshIdx++;
-  }
-  if (choice) {
-    if (mode === AmountMode.Gross) {
-      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.add(
-        result.totalContribution,
-        gap,
-      ).amount;
-      result.totalValue = Amounts.add(
-        result.totalValue,
-        gap,
-        choice.totalFee,
-      ).amount;
-    }
-  }
-
-  // console.log("gap", Amounts.stringify(limit), Amounts.stringify(gap), 
choice);
-  result.refresh = choice;
-  return result;
-}
-
-type CompareCoinsFunction = (d1: CoinInfo, d2: CoinInfo) => -1 | 0 | 1;
-function buildRankingForCoins(op: OperationType): CompareCoinsFunction {
-  function getFee(d: CoinInfo) {
-    return op === OperationType.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 getOperationType(txType: TransactionType): OperationType {
   const operationType =
     txType === TransactionType.Withdrawal
@@ -1157,51 +841,35 @@ function getOperationType(txType: TransactionType): 
OperationType {
   return operationType;
 }
 
-function getAmountsWithFee(
-  op: OperationType,
-  value: AmountJson,
-  contribution: AmountJson,
-  details: any,
-): GetPlanForOperationResponse {
-  return {
-    rawAmount: Amounts.stringify(
-      op === OperationType.Credit ? value : contribution,
-    ),
-    effectiveAmount: Amounts.stringify(
-      op === OperationType.Credit ? contribution : value,
-    ),
-    details,
-  };
-}
-
 interface RefreshChoice {
+  /**
+   * Amount that need to be covered
+   */
   gap: AmountJson;
   totalFee: AmountJson;
-  selected: string;
-
-  totalValue: AmountJson;
-  totalDepositFee: AmountJson;
-  totalRefreshFee: AmountJson;
+  selected: CoinInfo;
   totalChangeValue: AmountJson;
-  totalChangeContribution: AmountJson;
-  totalChangeWithdrawalFee: AmountJson;
-  change: string[];
+  refreshEffective: AmountJson;
+  coins: { info: CoinInfo; size: number }[];
+
+  // totalValue: AmountJson;
+  // totalDepositFee: AmountJson;
+  // totalRefreshFee: AmountJson;
+  // totalChangeContribution: AmountJson;
+  // totalChangeWithdrawalFee: AmountJson;
 }
 
+interface AvailableCoins {
+  list: CoinInfo[];
+  exchanges: Record<string, ExchangeInfo>;
+}
 interface SelectedCoins {
   totalValue: AmountJson;
-  totalContribution: AmountJson;
-  totalWithdrawalFee: AmountJson;
-  totalDepositFee: AmountJson;
-  coins: string[];
+  coins: { info: CoinInfo; size: number }[];
   refresh?: RefreshChoice;
 }
 
-interface AvailableCoins {
-  list: CoinInfo[];
-  exchanges: Record<string, ExchangeInfo>;
-}
-interface CoinInfo {
+export interface CoinInfo {
   id: string;
   value: AmountJson;
   denomDeposit: AmountJson;
@@ -1211,6 +879,7 @@ interface CoinInfo {
   exchangeWire: AmountJson | undefined;
   exchangePurse: AmountJson | undefined;
   duration: Duration;
+  exchangeBaseUrl: string;
   maxAge: number;
 }
 interface ExchangeInfo {
@@ -1232,12 +901,14 @@ interface CoinsFilter {
  * This function is costly (by the database access) but with high chances
  * of being cached
  */
-async function getAvailableCoins(
+async function getAvailableDenoms(
   ws: InternalWalletState,
-  op: OperationType,
+  op: TransactionType,
   currency: string,
   filters: CoinsFilter = {},
 ): Promise<AvailableCoins> {
+  const operationType = getOperationType(TransactionType.Deposit);
+
   return await ws.db
     .mktx((x) => [
       x.exchanges,
@@ -1318,7 +989,7 @@ async function getAvailableCoins(
         let creditDeadline = AbsoluteTime.never();
         let debitDeadline = AbsoluteTime.never();
         //4.- filter coins restricted by age
-        if (op === OperationType.Credit) {
+        if (operationType === OperationType.Credit) {
           const ds = await tx.denominations.indexes.byExchangeBaseUrl.getAll(
             exchangeBaseUrl,
           );
@@ -1415,6 +1086,7 @@ function buildCoinInfoFromDenom(
     denomRefresh: Amounts.parseOrThrow(denom.fees.feeRefresh),
     exchangePurse: purseFee,
     exchangeWire: wireFee,
+    exchangeBaseUrl: denom.exchangeBaseUrl,
     duration: AbsoluteTime.difference(
       AbsoluteTime.now(),
       AbsoluteTime.fromProtocolTimestamp(denom.stampExpireDeposit),
@@ -1424,3 +1096,525 @@ function buildCoinInfoFromDenom(
     maxAge,
   };
 }
+
+export async function convertDepositAmount(
+  ws: InternalWalletState,
+  req: ConvertAmountRequest,
+): Promise<AmountResponse> {
+  const amount = Amounts.parseOrThrow(req.amount);
+  // const filter = getCoinsFilter(req);
+
+  const denoms = await getAvailableDenoms(
+    ws,
+    TransactionType.Deposit,
+    amount.currency,
+    {},
+  );
+  const result = convertDepositAmountForAvailableCoins(
+    denoms,
+    amount,
+    req.type,
+  );
+
+  return {
+    effectiveAmount: Amounts.stringify(result.effective),
+    rawAmount: Amounts.stringify(result.raw),
+  };
+}
+
+const LOG_REFRESH = false;
+const LOG_DEPOSIT = false;
+export function convertDepositAmountForAvailableCoins(
+  denoms: AvailableCoins,
+  amount: AmountJson,
+  mode: TransactionAmountMode,
+): AmountAndRefresh {
+  const zero = Amounts.zeroOfCurrency(amount.currency);
+  if (!denoms.list.length) {
+    // no coins in the database
+    return { effective: zero, raw: zero };
+  }
+  const depositDenoms = rankDenominationForDeposit(denoms.list, mode);
+
+  //FIXME: we are not taking into account
+  // * exchanges with multiple accounts
+  // * wallet with multiple exchanges
+  const wireFee = Object.values(denoms.exchanges)[0]?.wireFee ?? zero;
+  const adjustedAmount = Amounts.add(amount, wireFee).amount;
+
+  const selected = selectGreedyCoins(depositDenoms, adjustedAmount);
+
+  const gap = Amounts.sub(amount, selected.totalValue).amount;
+
+  const result = getTotalEffectiveAndRawForDeposit(
+    selected.coins,
+    amount.currency,
+  );
+  result.raw = Amounts.sub(result.raw, wireFee).amount;
+
+  if (Amounts.isZero(gap)) {
+    // exact amount founds
+    return result;
+  }
+
+  if (LOG_DEPOSIT) {
+    const logInfo = selected.coins.map((c) => {
+      return `${Amounts.stringifyValue(c.info.id)} x ${c.size}`;
+    });
+    console.log(
+      "deposit used:",
+      logInfo.join(", "),
+      "gap:",
+      Amounts.stringifyValue(gap),
+    );
+  }
+
+  const refreshDenoms = rankDenominationForRefresh(denoms.list);
+  /**
+   * FIXME: looking for refresh AFTER selecting greedy is not optimal
+   */
+  const refreshCoin = searchBestRefreshCoin(
+    depositDenoms,
+    refreshDenoms,
+    gap,
+    mode,
+  );
+
+  if (refreshCoin) {
+    const fee = Amounts.sub(result.effective, result.raw).amount;
+    const effective = Amounts.add(
+      result.effective,
+      refreshCoin.refreshEffective,
+    ).amount;
+    const raw = Amounts.sub(effective, fee, refreshCoin.totalFee).amount;
+    //found with change
+    return {
+      effective,
+      raw,
+      refresh: refreshCoin,
+    };
+  }
+
+  // there is a gap, but no refresh coin was found
+  return result;
+}
+
+export async function getMaxDepositAmount(
+  ws: InternalWalletState,
+  req: GetAmountRequest,
+): Promise<AmountResponse> {
+  // const filter = getCoinsFilter(req);
+
+  const denoms = await getAvailableDenoms(
+    ws,
+    TransactionType.Deposit,
+    req.currency,
+    {},
+  );
+
+  const result = getMaxDepositAmountForAvailableCoins(denoms, req.currency);
+  return {
+    effectiveAmount: Amounts.stringify(result.effective),
+    rawAmount: Amounts.stringify(result.raw),
+  };
+}
+
+export function getMaxDepositAmountForAvailableCoins(
+  denoms: AvailableCoins,
+  currency: string,
+) {
+  const zero = Amounts.zeroOfCurrency(currency);
+  if (!denoms.list.length) {
+    // no coins in the database
+    return { effective: zero, raw: zero };
+  }
+
+  const result = getTotalEffectiveAndRawForDeposit(
+    denoms.list.map((info) => {
+      return { info, size: info.totalAvailable ?? 0 };
+    }),
+    currency,
+  );
+
+  const wireFee = Object.values(denoms.exchanges)[0]?.wireFee ?? zero;
+  result.raw = Amounts.sub(result.raw, wireFee).amount;
+
+  return result;
+}
+
+export async function convertPeerPushAmount(
+  ws: InternalWalletState,
+  req: ConvertAmountRequest,
+): Promise<AmountResponse> {
+  throw Error("to be implemented after 1.0");
+}
+export async function getMaxPeerPushAmount(
+  ws: InternalWalletState,
+  req: GetAmountRequest,
+): Promise<AmountResponse> {
+  throw Error("to be implemented after 1.0");
+}
+export async function convertWithdrawalAmount(
+  ws: InternalWalletState,
+  req: ConvertAmountRequest,
+): Promise<AmountResponse> {
+  const amount = Amounts.parseOrThrow(req.amount);
+
+  const denoms = await getAvailableDenoms(
+    ws,
+    TransactionType.Withdrawal,
+    amount.currency,
+    {},
+  );
+
+  const result = convertWithdrawalAmountFromAvailableCoins(
+    denoms,
+    amount,
+    req.type,
+  );
+
+  return {
+    effectiveAmount: Amounts.stringify(result.effective),
+    rawAmount: Amounts.stringify(result.raw),
+  };
+}
+
+export function convertWithdrawalAmountFromAvailableCoins(
+  denoms: AvailableCoins,
+  amount: AmountJson,
+  mode: TransactionAmountMode,
+) {
+  const zero = Amounts.zeroOfCurrency(amount.currency);
+  if (!denoms.list.length) {
+    // no coins in the database
+    return { effective: zero, raw: zero };
+  }
+  const withdrawDenoms = rankDenominationForWithdrawals(denoms.list, mode);
+
+  const selected = selectGreedyCoins(withdrawDenoms, amount);
+
+  return getTotalEffectiveAndRawForWithdrawal(selected.coins, amount.currency);
+}
+
+/** *****************************************************
+ * HELPERS
+ * *****************************************************
+ */
+
+/**
+ *
+ * @param depositDenoms
+ * @param refreshDenoms
+ * @param amount
+ * @param mode
+ * @returns
+ */
+function searchBestRefreshCoin(
+  depositDenoms: SelectableElement[],
+  refreshDenoms: Record<string, SelectableElement[]>,
+  amount: AmountJson,
+  mode: TransactionAmountMode,
+): RefreshChoice | undefined {
+  let choice: RefreshChoice | undefined = undefined;
+  let refreshIdx = 0;
+  refreshIteration: while (refreshIdx < depositDenoms.length) {
+    const d = depositDenoms[refreshIdx];
+
+    const denomContribution =
+      mode === TransactionAmountMode.Effective
+        ? d.value
+        : Amounts.sub(d.value, d.info.denomRefresh, 
d.info.denomDeposit).amount;
+
+    const changeAfterDeposit = Amounts.sub(denomContribution, amount).amount;
+    if (Amounts.isZero(changeAfterDeposit)) {
+      //this coin is not big enough to use for refresh
+      //since the list is sorted, we can break here
+      break refreshIteration;
+    }
+
+    const withdrawDenoms = refreshDenoms[d.info.exchangeBaseUrl];
+    const change = selectGreedyCoins(withdrawDenoms, changeAfterDeposit);
+
+    const zero = Amounts.zeroOfCurrency(amount.currency);
+    const withdrawChangeFee = change.coins.reduce((cur, prev) => {
+      return Amounts.add(
+        cur,
+        Amounts.mult(prev.info.denomWithdraw, prev.size).amount,
+      ).amount;
+    }, zero);
+
+    const withdrawChangeValue = change.coins.reduce((cur, prev) => {
+      return Amounts.add(cur, Amounts.mult(prev.info.value, prev.size).amount)
+        .amount;
+    }, zero);
+
+    const totalFee = Amounts.add(
+      d.info.denomDeposit,
+      d.info.denomRefresh,
+      withdrawChangeFee,
+    ).amount;
+
+    if (!choice || Amounts.cmp(totalFee, choice.totalFee) === -1) {
+      //found cheaper change
+      choice = {
+        gap: amount,
+        totalFee: totalFee,
+        totalChangeValue: change.totalValue, //change after refresh
+        refreshEffective: Amounts.sub(d.info.value, 
withdrawChangeValue).amount, // what of the denom used is not recovered
+        selected: d.info,
+        coins: change.coins,
+      };
+    }
+    refreshIdx++;
+  }
+  if (choice) {
+    if (LOG_REFRESH) {
+      const logInfo = choice.coins.map((c) => {
+        return `${Amounts.stringifyValue(c.info.id)} x ${c.size}`;
+      });
+      console.log(
+        "refresh used:",
+        Amounts.stringifyValue(choice.selected.value),
+        "change:",
+        logInfo.join(", "),
+        "fee:",
+        Amounts.stringifyValue(choice.totalFee),
+        "refreshEffective:",
+        Amounts.stringifyValue(choice.refreshEffective),
+        "totalChangeValue:",
+        Amounts.stringifyValue(choice.totalChangeValue),
+      );
+    }
+  }
+  return choice;
+}
+
+/**
+ * Returns a copy of the list sorted for the best denom to withdraw first
+ *
+ * @param denoms
+ * @returns
+ */
+function rankDenominationForWithdrawals(
+  denoms: CoinInfo[],
+  mode: TransactionAmountMode,
+): SelectableElement[] {
+  const copyList = [...denoms];
+  /**
+   * Rank coins
+   */
+  copyList.sort((d1, d2) => {
+    // the best coin to use is
+    // 1.- the one that contrib more and pay less fee
+    // 2.- it takes more time before expires
+
+    //different exchanges may have different wireFee
+    //ranking should take the relative contribution in the exchange
+    //which is (value - denomFee / fixedFee)
+    const rate1 = Amounts.divmod(d1.value, d1.denomWithdraw).quotient;
+    const rate2 = Amounts.divmod(d2.value, d2.denomWithdraw).quotient;
+    const contribCmp = rate1 === rate2 ? 0 : rate1 < rate2 ? 1 : -1;
+    return (
+      contribCmp ||
+      Duration.cmp(d1.duration, d2.duration) ||
+      strcmp(d1.id, d2.id)
+    );
+  });
+
+  return copyList.map((info) => {
+    switch (mode) {
+      case TransactionAmountMode.Effective: {
+        //if the user instructed "effective" then we need to selected
+        //greedy total coin value
+        return {
+          info,
+          value: info.value,
+          total: Number.MAX_SAFE_INTEGER,
+        };
+      }
+      case TransactionAmountMode.Raw: {
+        //if the user instructed "raw" then we need to selected
+        //greedy total coin raw amount (without fee)
+        return {
+          info,
+          value: Amounts.add(info.value, info.denomWithdraw).amount,
+          total: Number.MAX_SAFE_INTEGER,
+        };
+      }
+    }
+  });
+}
+
+/**
+ * Returns a copy of the list sorted for the best denom to deposit first
+ *
+ * @param denoms
+ * @returns
+ */
+function rankDenominationForDeposit(
+  denoms: CoinInfo[],
+  mode: TransactionAmountMode,
+): SelectableElement[] {
+  const copyList = [...denoms];
+  /**
+   * Rank coins
+   */
+  copyList.sort((d1, d2) => {
+    // the best coin to use is
+    // 1.- the one that contrib more and pay less fee
+    // 2.- it takes more time before expires
+
+    //different exchanges may have different wireFee
+    //ranking should take the relative contribution in the exchange
+    //which is (value - denomFee / fixedFee)
+    const rate1 = Amounts.divmod(d1.value, d1.denomDeposit).quotient;
+    const rate2 = Amounts.divmod(d2.value, d2.denomDeposit).quotient;
+    const contribCmp = rate1 === rate2 ? 0 : rate1 < rate2 ? 1 : -1;
+    return (
+      contribCmp ||
+      Duration.cmp(d1.duration, d2.duration) ||
+      strcmp(d1.id, d2.id)
+    );
+  });
+
+  return copyList.map((info) => {
+    switch (mode) {
+      case TransactionAmountMode.Effective: {
+        //if the user instructed "effective" then we need to selected
+        //greedy total coin value
+        return {
+          info,
+          value: info.value,
+          total: info.totalAvailable ?? 0,
+        };
+      }
+      case TransactionAmountMode.Raw: {
+        //if the user instructed "raw" then we need to selected
+        //greedy total coin raw amount (without fee)
+        return {
+          info,
+          value: Amounts.sub(info.value, info.denomDeposit).amount,
+          total: info.totalAvailable ?? 0,
+        };
+      }
+    }
+  });
+}
+
+/**
+ * Returns a copy of the list sorted for the best denom to withdraw first
+ *
+ * @param denoms
+ * @returns
+ */
+function rankDenominationForRefresh(
+  denoms: CoinInfo[],
+): Record<string, SelectableElement[]> {
+  const groupByExchange: Record<string, CoinInfo[]> = {};
+  for (const d of denoms) {
+    if (!groupByExchange[d.exchangeBaseUrl]) {
+      groupByExchange[d.exchangeBaseUrl] = [];
+    }
+    groupByExchange[d.exchangeBaseUrl].push(d);
+  }
+
+  const result: Record<string, SelectableElement[]> = {};
+  for (const d of denoms) {
+    result[d.exchangeBaseUrl] = rankDenominationForWithdrawals(
+      groupByExchange[d.exchangeBaseUrl],
+      TransactionAmountMode.Raw,
+    );
+  }
+  return result;
+}
+
+interface SelectableElement {
+  total: number;
+  value: AmountJson;
+  info: CoinInfo;
+}
+
+function selectGreedyCoins(
+  coins: SelectableElement[],
+  limit: AmountJson,
+): SelectedCoins {
+  const result: SelectedCoins = {
+    totalValue: Amounts.zeroOfCurrency(limit.currency),
+    coins: [],
+  };
+  if (!coins.length) return result;
+
+  let denomIdx = 0;
+  iterateDenoms: while (denomIdx < coins.length) {
+    const denom = coins[denomIdx];
+    // let total = denom.total;
+    const left = Amounts.sub(limit, result.totalValue).amount;
+
+    if (Amounts.isZero(denom.value)) {
+      // 0 contribution denoms should be the last
+      break iterateDenoms;
+    }
+
+    //use Amounts.divmod instead of iterate
+    const div = Amounts.divmod(left, denom.value);
+    const size = Math.min(div.quotient, denom.total);
+    if (size > 0) {
+      const mul = Amounts.mult(denom.value, size).amount;
+      const progress = Amounts.add(result.totalValue, mul).amount;
+
+      result.totalValue = progress;
+      result.coins.push({ info: denom.info, size });
+      denom.total = denom.total - size;
+    }
+
+    //go next denom
+    denomIdx++;
+  }
+
+  return result;
+}
+
+type AmountWithFee = { raw: AmountJson; effective: AmountJson };
+type AmountAndRefresh = AmountWithFee & { refresh?: RefreshChoice };
+
+export function getTotalEffectiveAndRawForDeposit(
+  list: { info: CoinInfo; size: number }[],
+  currency: string,
+): AmountWithFee {
+  const init = {
+    raw: Amounts.zeroOfCurrency(currency),
+    effective: Amounts.zeroOfCurrency(currency),
+  };
+  return list.reduce((prev, cur) => {
+    const ef = Amounts.mult(cur.info.value, cur.size).amount;
+    const rw = Amounts.mult(
+      Amounts.sub(cur.info.value, cur.info.denomDeposit).amount,
+      cur.size,
+    ).amount;
+
+    prev.effective = Amounts.add(prev.effective, ef).amount;
+    prev.raw = Amounts.add(prev.raw, rw).amount;
+    return prev;
+  }, init);
+}
+
+function getTotalEffectiveAndRawForWithdrawal(
+  list: { info: CoinInfo; size: number }[],
+  currency: string,
+): AmountWithFee {
+  const init = {
+    raw: Amounts.zeroOfCurrency(currency),
+    effective: Amounts.zeroOfCurrency(currency),
+  };
+  return list.reduce((prev, cur) => {
+    const ef = Amounts.mult(cur.info.value, cur.size).amount;
+    const rw = Amounts.mult(
+      Amounts.add(cur.info.value, cur.info.denomWithdraw).amount,
+      cur.size,
+    ).amount;
+
+    prev.effective = Amounts.add(prev.effective, ef).amount;
+    prev.raw = Amounts.add(prev.raw, rw).amount;
+    return prev;
+  }, init);
+}
diff --git a/packages/taler-wallet-core/src/wallet-api-types.ts 
b/packages/taler-wallet-core/src/wallet-api-types.ts
index 3b0d11039..c58ced045 100644
--- a/packages/taler-wallet-core/src/wallet-api-types.ts
+++ b/packages/taler-wallet-core/src/wallet-api-types.ts
@@ -34,6 +34,7 @@ import {
   AcceptWithdrawalResponse,
   AddExchangeRequest,
   AddKnownBankAccountsRequest,
+  AmountResponse,
   ApplyDevExperimentRequest,
   BackupRecovery,
   BalancesResponse,
@@ -47,6 +48,7 @@ import {
   ConfirmPayResult,
   ConfirmPeerPullDebitRequest,
   ConfirmPeerPushCreditRequest,
+  ConvertAmountRequest,
   CreateDepositGroupRequest,
   CreateDepositGroupResponse,
   DeleteTransactionRequest,
@@ -54,6 +56,7 @@ import {
   ExchangesListResponse,
   ForceRefreshRequest,
   ForgetKnownBankAccountsRequest,
+  GetAmountRequest,
   GetBalanceDetailRequest,
   GetContractTermsDetailsRequest,
   GetExchangeTosRequest,
@@ -146,6 +149,11 @@ export enum WalletApiOperation {
   GetBalances = "getBalances",
   GetBalanceDetail = "getBalanceDetail",
   GetPlanForOperation = "getPlanForOperation",
+  ConvertDepositAmount = "ConvertDepositAmount",
+  GetMaxDepositAmount = "GetMaxDepositAmount",
+  ConvertPeerPushAmount = "ConvertPeerPushAmount",
+  GetMaxPeerPushAmount = "GetMaxPeerPushAmount",
+  ConvertWithdrawalAmount = "ConvertWithdrawalAmount",
   GetUserAttentionRequests = "getUserAttentionRequests",
   GetUserAttentionUnreadCount = "getUserAttentionUnreadCount",
   MarkAttentionRequestAsRead = "markAttentionRequestAsRead",
@@ -284,6 +292,32 @@ export type GetPlanForOperationOp = {
   response: GetPlanForOperationResponse;
 };
 
+export type ConvertDepositAmountOp = {
+  op: WalletApiOperation.ConvertDepositAmount;
+  request: ConvertAmountRequest;
+  response: AmountResponse;
+};
+export type GetMaxDepositAmountOp = {
+  op: WalletApiOperation.GetMaxDepositAmount;
+  request: GetAmountRequest;
+  response: AmountResponse;
+};
+export type ConvertPeerPushAmountOp = {
+  op: WalletApiOperation.ConvertPeerPushAmount;
+  request: ConvertAmountRequest;
+  response: AmountResponse;
+};
+export type GetMaxPeerPushAmountOp = {
+  op: WalletApiOperation.GetMaxPeerPushAmount;
+  request: GetAmountRequest;
+  response: AmountResponse;
+};
+export type ConvertWithdrawalAmountOp = {
+  op: WalletApiOperation.ConvertWithdrawalAmount;
+  request: ConvertAmountRequest;
+  response: AmountResponse;
+};
+
 // group: Managing Transactions
 
 /**
@@ -949,6 +983,11 @@ export type WalletOperations = {
   [WalletApiOperation.SuspendTransaction]: SuspendTransactionOp;
   [WalletApiOperation.ResumeTransaction]: ResumeTransactionOp;
   [WalletApiOperation.GetBalances]: GetBalancesOp;
+  [WalletApiOperation.ConvertDepositAmount]: ConvertDepositAmountOp;
+  [WalletApiOperation.GetMaxDepositAmount]: GetMaxDepositAmountOp;
+  [WalletApiOperation.ConvertPeerPushAmount]: ConvertPeerPushAmountOp;
+  [WalletApiOperation.GetMaxPeerPushAmount]: GetMaxPeerPushAmountOp;
+  [WalletApiOperation.ConvertWithdrawalAmount]: ConvertWithdrawalAmountOp;
   [WalletApiOperation.GetPlanForOperation]: GetPlanForOperationOp;
   [WalletApiOperation.GetBalanceDetail]: GetBalancesDetailOp;
   [WalletApiOperation.GetTransactions]: GetTransactionsOp;
diff --git a/packages/taler-wallet-core/src/wallet.ts 
b/packages/taler-wallet-core/src/wallet.ts
index e5cd713b8..af6bb4d62 100644
--- a/packages/taler-wallet-core/src/wallet.ts
+++ b/packages/taler-wallet-core/src/wallet.ts
@@ -69,10 +69,12 @@ import {
   codecForCheckPeerPushDebitRequest,
   codecForConfirmPayRequest,
   codecForConfirmPeerPushPaymentRequest,
+  codecForConvertAmountRequest,
   codecForCreateDepositGroupRequest,
   codecForDeleteTransactionRequest,
   codecForForceRefreshRequest,
   codecForForgetKnownBankAccounts,
+  codecForGetAmountRequest,
   codecForGetBalanceDetailRequest,
   codecForGetContractTermsDetails,
   codecForGetExchangeTosRequest,
@@ -293,7 +295,13 @@ import {
   WalletCoreApiClient,
   WalletCoreResponseType,
 } from "./wallet-api-types.js";
-import { getPlanForOperation } from "./util/coinSelection.js";
+import {
+  convertDepositAmount,
+  convertPeerPushAmount,
+  convertWithdrawalAmount,
+  getMaxDepositAmount,
+  getMaxPeerPushAmount,
+} from "./util/coinSelection.js";
 
 const logger = new Logger("wallet.ts");
 
@@ -1345,9 +1353,29 @@ async function dispatchRequestInternal<Op extends 
WalletApiOperation>(
       await loadBackupRecovery(ws, req);
       return {};
     }
-    case WalletApiOperation.GetPlanForOperation: {
-      const req = codecForGetPlanForOperationRequest().decode(payload);
-      return await getPlanForOperation(ws, req);
+    // case WalletApiOperation.GetPlanForOperation: {
+    //   const req = codecForGetPlanForOperationRequest().decode(payload);
+    //   return await getPlanForOperation(ws, req);
+    // }
+    case WalletApiOperation.ConvertDepositAmount: {
+      const req = codecForConvertAmountRequest.decode(payload);
+      return await convertDepositAmount(ws, req);
+    }
+    case WalletApiOperation.GetMaxDepositAmount: {
+      const req = codecForGetAmountRequest.decode(payload);
+      return await getMaxDepositAmount(ws, req);
+    }
+    case WalletApiOperation.ConvertPeerPushAmount: {
+      const req = codecForConvertAmountRequest.decode(payload);
+      return await convertPeerPushAmount(ws, req);
+    }
+    case WalletApiOperation.GetMaxPeerPushAmount: {
+      const req = codecForGetAmountRequest.decode(payload);
+      return await getMaxPeerPushAmount(ws, req);
+    }
+    case WalletApiOperation.ConvertWithdrawalAmount: {
+      const req = codecForConvertAmountRequest.decode(payload);
+      return await convertWithdrawalAmount(ws, req);
     }
     case WalletApiOperation.GetBackupInfo: {
       const resp = await getBackupInfo(ws);

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