remove calculate plan (for now) implemented simpler API

This commit is contained in:
Sebastian 2023-06-20 14:30:02 -03:00
parent d79155b634
commit 1e9f1fb7a9
No known key found for this signature in database
GPG Key ID: 173909D1A5F66069
4 changed files with 1111 additions and 522 deletions

View File

@ -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,
);
expect(t, result.coins).deep.equal(["KUDOS:2", "KUDOS:2"]);
t.assert(result.refresh === undefined);
t.is(Amounts.stringifyValue(result.effective), "10");
t.is(Amounts.stringifyValue(result.raw), "9.98");
});
test("get raw 25, diff with demo ", (t) => {
test("deposit effective 24", (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`24`,
TransactionAmountMode.Effective,
);
expect(t, result.coins).deep.equal(["KUDOS:10", "KUDOS:10", "KUDOS:5"]);
t.assert(result.refresh === undefined);
t.is(Amounts.stringifyValue(result.effective), "24");
t.is(Amounts.stringifyValue(result.raw), "23.94");
});
test("send effective 6", (t) => {
test("deposit effective 40", (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`40`,
TransactionAmountMode.Effective,
);
expect(t, result.coins).deep.equal(["KUDOS:5"]);
t.assert(result.refresh?.selected === "KUDOS:2");
t.is(Amounts.stringifyValue(result.effective), "35");
t.is(Amounts.stringifyValue(result.raw), "34.9");
});
test("send raw 6", (t) => {
test("deposit with wire fee 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: {},
exchanges: {
one: {
wireFee: kudos`0.1`,
purseFee: kudos`0.00`,
creditDeadline: AbsoluteTime.never(),
debitDeadline: AbsoluteTime.never(),
},
},
},
kudos`2`,
TransactionAmountMode.Effective,
);
expect(t, result.coins).deep.equal(["KUDOS:5"]);
t.assert(result.refresh?.selected === "KUDOS:2");
});
test("send raw 20 (not enough)", (t) => {
const coinList: Coin[] = [
[kudos(2), 1],
[kudos(5), 2],
];
const result = selectCoinForOperation(
OperationType.Debit,
kudos(20),
AmountMode.Gross,
{
list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
exchanges: {},
},
);
expect(t, result.coins).deep.equal(["KUDOS:5", "KUDOS:5", "KUDOS:2"]);
t.assert(result.refresh === undefined);
t.is(Amounts.stringifyValue(result.effective), "2");
t.is(Amounts.stringifyValue(result.raw), "1.89");
});
/**
* calculatePlanFormAvailableCoins test
* Making a deposit with raw amount, using the result from effective
*
* Test here should check that the plan summary for a transaction is correct
* * effective amount
* * raw amount
*/
test("deposit effective 2 ", (t) => {
test("deposit raw 1.99 (effective 2)", (t) => {
const coinList: Coin[] = [
[kudos(2), 1],
[kudos(5), 2],
[kudos`2`, 5],
[kudos`5`, 5],
];
const result = calculatePlanFormAvailableCoins(
TransactionType.Deposit,
kudos(2),
const result = convertDepositAmountForAvailableCoins(
{
list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
exchanges: {},
},
kudos`1.99`,
TransactionAmountMode.Raw,
);
t.is(Amounts.stringifyValue(result.effective), "2");
t.is(Amounts.stringifyValue(result.raw), "1.99");
});
test("deposit raw 9.98 (effective 10)", (t) => {
const coinList: Coin[] = [
[kudos`2`, 5],
[kudos`5`, 5],
];
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");
});
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("deposit raw 34.9 (effective 40)", (t) => {
const coinList: Coin[] = [
[kudos`2`, 5],
[kudos`5`, 5],
];
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");
});
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,
{
list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
exchanges: {
"2": {
creditDeadline: AbsoluteTime.never(),
debitDeadline: AbsoluteTime.never(),
wireFee: kudos(0.01),
purseFee: kudos(0.01),
},
},
},
);
t.deepEqual(result.rawAmount, "KUDOS:1.98");
t.deepEqual(result.effectiveAmount, "KUDOS:2");
t.is(Amounts.stringifyValue(result.effective), "2");
t.is(Amounts.stringifyValue(result.raw), "1.89");
});
test("deposit raw 2 ", (t) => {
/**
* Calculating the max amount possible to deposit
*
*/
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.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.deepEqual(result.rawAmount, "KUDOS:2");
t.deepEqual(result.effectiveAmount, "KUDOS:2.04");
t.is(Amounts.stringifyValue(result.raw), "34.9");
t.is(Amounts.stringifyValue(result.effective), "35");
});
test("withdraw raw 21 ", (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.Withdrawal,
kudos(21),
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",
);
// denominations configuration is not suitable
// for greedy algorithm
t.deepEqual(result.rawAmount, "KUDOS:20");
t.deepEqual(result.effectiveAmount, "KUDOS:19.96");
t.is(Amounts.stringifyValue(result.raw), "33.9");
t.is(Amounts.stringifyValue(result.effective), "35");
});
test("withdraw raw 25, diff with demo ", (t) => {
test("deposit max repeated denom", (t) => {
const coinList: Coin[] = [
[kudos(0.1), 0],
[kudos(1), 0],
[kudos(2), 0],
[kudos(5), 0],
[kudos(10), 0],
[kudos`2`, 1],
[kudos`2`, 1],
[kudos`5`, 1],
];
const result = calculatePlanFormAvailableCoins(
TransactionType.Withdrawal,
kudos(25),
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.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");
t.is(Amounts.stringifyValue(result.raw), "8.97");
t.is(Amounts.stringifyValue(result.effective), "9");
});
/**
* 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 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`,
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: {},
},
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
//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];
});
}

View File

@ -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 }[];
interface SelectedCoins {
totalValue: AmountJson;
totalContribution: AmountJson;
totalWithdrawalFee: AmountJson;
totalDepositFee: AmountJson;
coins: string[];
refresh?: RefreshChoice;
// totalValue: AmountJson;
// totalDepositFee: AmountJson;
// totalRefreshFee: AmountJson;
// totalChangeContribution: AmountJson;
// totalChangeWithdrawalFee: AmountJson;
}
interface AvailableCoins {
list: CoinInfo[];
exchanges: Record<string, ExchangeInfo>;
}
interface CoinInfo {
interface SelectedCoins {
totalValue: AmountJson;
coins: { info: CoinInfo; size: number }[];
refresh?: RefreshChoice;
}
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);
}

View File

@ -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;

View File

@ -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);