aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSebastian <sebasjm@gmail.com>2023-06-20 14:30:02 -0300
committerSebastian <sebasjm@gmail.com>2023-06-20 14:30:02 -0300
commit1e9f1fb7a9451ad8fae6474cc831596a9e9a3f2f (patch)
treec1d3eaaf7bf4faab622ca138c47fee7b4d6ec5a6
parentd79155b634b2bdca48faa6ac3b25e21c3c30a062 (diff)
downloadwallet-core-1e9f1fb7a9451ad8fae6474cc831596a9e9a3f2f.tar.xz
remove calculate plan (for now) implemented simpler API
-rw-r--r--packages/taler-wallet-core/src/util/coinSelection.test.ts614
-rw-r--r--packages/taler-wallet-core/src/util/coinSelection.ts906
-rw-r--r--packages/taler-wallet-core/src/wallet-api-types.ts39
-rw-r--r--packages/taler-wallet-core/src/wallet.ts36
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);