wallet-core: split coin selection and instructed amount conversion

This commit is contained in:
Florian Dold 2023-08-29 18:33:51 +02:00
parent 5852b5cf2e
commit a386de8a9c
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
8 changed files with 1865 additions and 1798 deletions

View File

@ -43,8 +43,6 @@ import {
import { SpendCoinDetails } from "../crypto/cryptoImplementation.js"; import { SpendCoinDetails } from "../crypto/cryptoImplementation.js";
import { import {
DenominationRecord, DenominationRecord,
KycPendingInfo,
KycUserType,
PeerPushPaymentCoinSelection, PeerPushPaymentCoinSelection,
ReserveRecord, ReserveRecord,
} from "../db.js"; } from "../db.js";
@ -52,68 +50,13 @@ import { InternalWalletState } from "../internal-wallet-state.js";
import { checkDbInvariant } from "../util/invariants.js"; import { checkDbInvariant } from "../util/invariants.js";
import { getPeerPaymentBalanceDetailsInTx } from "./balance.js"; import { getPeerPaymentBalanceDetailsInTx } from "./balance.js";
import { getTotalRefreshCost } from "./refresh.js"; import { getTotalRefreshCost } from "./refresh.js";
import type { PeerCoinInfo, PeerCoinSelectionRequest, SelectPeerCoinsResult, SelectedPeerCoin } from "../util/coinSelection.js";
const logger = new Logger("operations/peer-to-peer.ts"); const logger = new Logger("operations/peer-to-peer.ts");
interface SelectedPeerCoin {
coinPub: string;
coinPriv: string;
contribution: AmountString;
denomPubHash: string;
denomSig: UnblindedSignature;
ageCommitmentProof: AgeCommitmentProof | undefined;
}
interface PeerCoinSelectionDetails {
exchangeBaseUrl: string;
/**
* Info of Coins that were selected.
*/
coins: SelectedPeerCoin[];
/**
* How much of the deposit fees is the customer paying?
*/
depositFees: AmountJson;
}
/**
* Information about a selected coin for peer to peer payments.
*/
interface CoinInfo {
/**
* Public key of the coin.
*/
coinPub: string;
coinPriv: string;
/**
* Deposit fee for the coin.
*/
feeDeposit: AmountJson;
value: AmountJson;
denomPubHash: string;
denomSig: UnblindedSignature;
maxAge: number;
ageCommitmentProof?: AgeCommitmentProof;
}
export type SelectPeerCoinsResult =
| { type: "success"; result: PeerCoinSelectionDetails }
| {
type: "failure";
insufficientBalanceDetails: PayPeerInsufficientBalanceDetails;
};
/** /**
* Get information about the coin selected for signatures * Get information about the coin selected for signatures
*
* @param ws * @param ws
* @param csel * @param csel
* @returns * @returns
@ -153,211 +96,7 @@ export async function queryCoinInfosForSelection(
return infos; return infos;
} }
export interface PeerCoinRepair {
exchangeBaseUrl: string;
coinPubs: CoinPublicKeyString[];
contribs: AmountJson[];
}
export interface PeerCoinSelectionRequest {
instructedAmount: AmountJson;
/**
* Instruct the coin selection to repair this coin
* selection instead of selecting completely new coins.
*/
repair?: PeerCoinRepair;
}
export async function selectPeerCoins(
ws: InternalWalletState,
req: PeerCoinSelectionRequest,
): Promise<SelectPeerCoinsResult> {
const instructedAmount = req.instructedAmount;
if (Amounts.isZero(instructedAmount)) {
// Other parts of the code assume that we have at least
// one coin to spend.
throw new Error("amount of zero not allowed");
}
return await ws.db
.mktx((x) => [
x.exchanges,
x.contractTerms,
x.coins,
x.coinAvailability,
x.denominations,
x.refreshGroups,
x.peerPushPaymentInitiations,
])
.runReadWrite(async (tx) => {
const exchanges = await tx.exchanges.iter().toArray();
const exchangeFeeGap: { [url: string]: AmountJson } = {};
const currency = Amounts.currencyOf(instructedAmount);
for (const exch of exchanges) {
if (exch.detailsPointer?.currency !== currency) {
continue;
}
// FIXME: Can't we do this faster by using coinAvailability?
const coins = (
await tx.coins.indexes.byBaseUrl.getAll(exch.baseUrl)
).filter((x) => x.status === CoinStatus.Fresh);
const coinInfos: CoinInfo[] = [];
for (const coin of coins) {
const denom = await ws.getDenomInfo(
ws,
tx,
coin.exchangeBaseUrl,
coin.denomPubHash,
);
if (!denom) {
throw Error("denom not found");
}
coinInfos.push({
coinPub: coin.coinPub,
feeDeposit: Amounts.parseOrThrow(denom.feeDeposit),
value: Amounts.parseOrThrow(denom.value),
denomPubHash: denom.denomPubHash,
coinPriv: coin.coinPriv,
denomSig: coin.denomSig,
maxAge: coin.maxAge,
ageCommitmentProof: coin.ageCommitmentProof,
});
}
if (coinInfos.length === 0) {
continue;
}
coinInfos.sort(
(o1, o2) =>
-Amounts.cmp(o1.value, o2.value) ||
strcmp(o1.denomPubHash, o2.denomPubHash),
);
let amountAcc = Amounts.zeroOfCurrency(currency);
let depositFeesAcc = Amounts.zeroOfCurrency(currency);
const resCoins: {
coinPub: string;
coinPriv: string;
contribution: AmountString;
denomPubHash: string;
denomSig: UnblindedSignature;
ageCommitmentProof: AgeCommitmentProof | undefined;
}[] = [];
let lastDepositFee = Amounts.zeroOfCurrency(currency);
if (req.repair) {
for (let i = 0; i < req.repair.coinPubs.length; i++) {
const contrib = req.repair.contribs[i];
const coin = await tx.coins.get(req.repair.coinPubs[i]);
if (!coin) {
throw Error("repair not possible, coin not found");
}
const denom = await ws.getDenomInfo(
ws,
tx,
coin.exchangeBaseUrl,
coin.denomPubHash,
);
checkDbInvariant(!!denom);
resCoins.push({
coinPriv: coin.coinPriv,
coinPub: coin.coinPub,
contribution: Amounts.stringify(contrib),
denomPubHash: coin.denomPubHash,
denomSig: coin.denomSig,
ageCommitmentProof: coin.ageCommitmentProof,
});
const depositFee = Amounts.parseOrThrow(denom.feeDeposit);
lastDepositFee = depositFee;
amountAcc = Amounts.add(
amountAcc,
Amounts.sub(contrib, depositFee).amount,
).amount;
depositFeesAcc = Amounts.add(depositFeesAcc, depositFee).amount;
}
}
for (const coin of coinInfos) {
if (Amounts.cmp(amountAcc, instructedAmount) >= 0) {
break;
}
const gap = Amounts.add(
coin.feeDeposit,
Amounts.sub(instructedAmount, amountAcc).amount,
).amount;
const contrib = Amounts.min(gap, coin.value);
amountAcc = Amounts.add(
amountAcc,
Amounts.sub(contrib, coin.feeDeposit).amount,
).amount;
depositFeesAcc = Amounts.add(depositFeesAcc, coin.feeDeposit).amount;
resCoins.push({
coinPriv: coin.coinPriv,
coinPub: coin.coinPub,
contribution: Amounts.stringify(contrib),
denomPubHash: coin.denomPubHash,
denomSig: coin.denomSig,
ageCommitmentProof: coin.ageCommitmentProof,
});
lastDepositFee = coin.feeDeposit;
}
if (Amounts.cmp(amountAcc, instructedAmount) >= 0) {
const res: PeerCoinSelectionDetails = {
exchangeBaseUrl: exch.baseUrl,
coins: resCoins,
depositFees: depositFeesAcc,
};
return { type: "success", result: res };
}
const diff = Amounts.sub(instructedAmount, amountAcc).amount;
exchangeFeeGap[exch.baseUrl] = Amounts.add(lastDepositFee, diff).amount;
continue;
}
// We were unable to select coins.
// Now we need to produce error details.
const infoGeneral = await getPeerPaymentBalanceDetailsInTx(ws, tx, {
currency,
});
const perExchange: PayPeerInsufficientBalanceDetails["perExchange"] = {};
let maxFeeGapEstimate = Amounts.zeroOfCurrency(currency);
for (const exch of exchanges) {
if (exch.detailsPointer?.currency !== currency) {
continue;
}
const infoExchange = await getPeerPaymentBalanceDetailsInTx(ws, tx, {
currency,
restrictExchangeTo: exch.baseUrl,
});
let gap =
exchangeFeeGap[exch.baseUrl] ?? Amounts.zeroOfCurrency(currency);
if (Amounts.cmp(infoExchange.balanceMaterial, instructedAmount) < 0) {
// Show fee gap only if we should've been able to pay with the material amount
gap = Amounts.zeroOfCurrency(currency);
}
perExchange[exch.baseUrl] = {
balanceAvailable: Amounts.stringify(infoExchange.balanceAvailable),
balanceMaterial: Amounts.stringify(infoExchange.balanceMaterial),
feeGapEstimate: Amounts.stringify(gap),
};
maxFeeGapEstimate = Amounts.max(maxFeeGapEstimate, gap);
}
const errDetails: PayPeerInsufficientBalanceDetails = {
amountRequested: Amounts.stringify(instructedAmount),
balanceAvailable: Amounts.stringify(infoGeneral.balanceAvailable),
balanceMaterial: Amounts.stringify(infoGeneral.balanceMaterial),
feeGapEstimate: Amounts.stringify(maxFeeGapEstimate),
perExchange,
};
return { type: "failure", insufficientBalanceDetails: errDetails };
});
}
export async function getTotalPeerPaymentCost( export async function getTotalPeerPaymentCost(
ws: InternalWalletState, ws: InternalWalletState,

View File

@ -68,11 +68,9 @@ import {
spendCoins, spendCoins,
} from "./common.js"; } from "./common.js";
import { import {
PeerCoinRepair,
codecForExchangePurseStatus, codecForExchangePurseStatus,
getTotalPeerPaymentCost, getTotalPeerPaymentCost,
queryCoinInfosForSelection, queryCoinInfosForSelection,
selectPeerCoins,
} from "./pay-peer-common.js"; } from "./pay-peer-common.js";
import { import {
constructTransactionIdentifier, constructTransactionIdentifier,
@ -80,6 +78,7 @@ import {
parseTransactionIdentifier, parseTransactionIdentifier,
stopLongpolling, stopLongpolling,
} from "./transactions.js"; } from "./transactions.js";
import { PeerCoinRepair, selectPeerCoins } from "../util/coinSelection.js";
const logger = new Logger("pay-peer-pull-debit.ts"); const logger = new Logger("pay-peer-pull-debit.ts");

View File

@ -68,17 +68,16 @@ import {
spendCoins, spendCoins,
} from "./common.js"; } from "./common.js";
import { import {
PeerCoinRepair,
codecForExchangePurseStatus, codecForExchangePurseStatus,
getTotalPeerPaymentCost, getTotalPeerPaymentCost,
queryCoinInfosForSelection, queryCoinInfosForSelection,
selectPeerCoins,
} from "./pay-peer-common.js"; } from "./pay-peer-common.js";
import { import {
constructTransactionIdentifier, constructTransactionIdentifier,
notifyTransition, notifyTransition,
stopLongpolling, stopLongpolling,
} from "./transactions.js"; } from "./transactions.js";
import { PeerCoinRepair, selectPeerCoins } from "../util/coinSelection.js";
const logger = new Logger("pay-peer-push-debit.ts"); const logger = new Logger("pay-peer-push-debit.ts");

View File

@ -22,746 +22,4 @@ import {
TransactionAmountMode, TransactionAmountMode,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import test, { ExecutionContext } from "ava"; import test, { ExecutionContext } from "ava";
import {
CoinInfo,
convertDepositAmountForAvailableCoins,
convertWithdrawalAmountFromAvailableCoins,
getMaxDepositAmountForAvailableCoins,
} from "./coinSelection.js";
function makeCurrencyHelper(currency: string) {
return (sx: TemplateStringsArray, ...vx: any[]) => {
const s = String.raw({ raw: sx }, ...vx);
return Amounts.parseOrThrow(`${currency}:${s}`);
};
}
const kudos = makeCurrencyHelper("kudos");
function defaultFeeConfig(value: AmountJson, totalAvailable: number): CoinInfo {
return {
id: Amounts.stringify(value),
denomDeposit: kudos`0.01`,
denomRefresh: kudos`0.01`,
denomWithdraw: kudos`0.01`,
exchangeBaseUrl: "1",
duration: Duration.getForever(),
exchangePurse: undefined,
exchangeWire: undefined,
maxAge: AgeRestriction.AGE_UNRESTRICTED,
totalAvailable,
value,
};
}
type Coin = [AmountJson, number];
/**
* Making a deposit with effective amount
*
*/
test("deposit effective 2", (t) => {
const coinList: Coin[] = [
[kudos`2`, 5],
[kudos`5`, 5],
];
const result = convertDepositAmountForAvailableCoins(
{
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), "1.99");
});
test("deposit 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`10`,
TransactionAmountMode.Effective,
);
t.is(Amounts.stringifyValue(result.effective), "10");
t.is(Amounts.stringifyValue(result.raw), "9.98");
});
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("deposit 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`40`,
TransactionAmountMode.Effective,
);
t.is(Amounts.stringifyValue(result.effective), "35");
t.is(Amounts.stringifyValue(result.raw), "34.9");
});
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");
});
/**
* 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],
];
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,
);
t.is(Amounts.stringifyValue(result.effective), "2");
t.is(Amounts.stringifyValue(result.raw), "1.89");
});
/**
* Calculating the max amount possible to deposit
*
*/
test("deposit max 35", (t) => {
const coinList: Coin[] = [
[kudos`2`, 5],
[kudos`5`, 5],
];
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(),
},
},
},
"KUDOS",
);
t.is(Amounts.stringifyValue(result.raw), "34.9");
t.is(Amounts.stringifyValue(result.effective), "35");
});
test("deposit max 35 with wirefee", (t) => {
const coinList: Coin[] = [
[kudos`2`, 5],
[kudos`5`, 5],
];
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(),
},
},
},
"KUDOS",
);
t.is(Amounts.stringifyValue(result.raw), "33.9");
t.is(Amounts.stringifyValue(result.effective), "35");
});
test("deposit max repeated denom", (t) => {
const coinList: Coin[] = [
[kudos`2`, 1],
[kudos`2`, 1],
[kudos`5`, 1],
];
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(),
},
},
},
"KUDOS",
);
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];
});
}
/**
* regression tests
*/
test("demo: withdraw raw 25", (t) => {
const coinList: Coin[] = [
[kudos`0.1`, 0],
[kudos`1`, 0],
[kudos`2`, 0],
[kudos`5`, 0],
[kudos`10`, 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.92");
// coins received
// 8 x 0.1
// 2 x 0.2
// 2 x 10.0
// total effective 24.8
// fee 12 x 0.01 = 0.12
// total raw 24.92
// left in reserve 25 - 24.92 == 0.08
//current wallet impl: hides the left in reserve fee
//shows fee = 0.2
});
test("demo: deposit max after withdraw raw 25", (t) => {
const coinList: Coin[] = [
[kudos`0.1`, 8],
[kudos`1`, 0],
[kudos`2`, 2],
[kudos`5`, 0],
[kudos`10`, 2],
];
const result = getMaxDepositAmountForAvailableCoins(
{
list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
exchanges: {
one: {
wireFee: kudos`0.01`,
purseFee: kudos`0.00`,
creditDeadline: AbsoluteTime.never(),
debitDeadline: AbsoluteTime.never(),
},
},
},
"KUDOS",
);
t.is(Amounts.stringifyValue(result.effective), "24.8");
t.is(Amounts.stringifyValue(result.raw), "24.67");
// 8 x 0.1
// 2 x 0.2
// 2 x 10.0
// total effective 24.8
// deposit fee 12 x 0.01 = 0.12
// wire fee 0.01
// total raw: 24.8 - 0.13 = 24.67
// current wallet impl fee 0.14
});
test("demo: withdraw raw 13", (t) => {
const coinList: Coin[] = [
[kudos`0.1`, 0],
[kudos`1`, 0],
[kudos`2`, 0],
[kudos`5`, 0],
[kudos`10`, 0],
];
const result = convertWithdrawalAmountFromAvailableCoins(
{
list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
exchanges: {},
},
kudos`13`,
TransactionAmountMode.Raw,
);
t.is(Amounts.stringifyValue(result.effective), "12.8");
t.is(Amounts.stringifyValue(result.raw), "12.9");
// coins received
// 8 x 0.1
// 1 x 0.2
// 1 x 10.0
// total effective 12.8
// fee 10 x 0.01 = 0.10
// total raw 12.9
// left in reserve 13 - 12.9 == 0.1
//current wallet impl: hides the left in reserve fee
//shows fee = 0.2
});
test("demo: deposit max after withdraw raw 13", (t) => {
const coinList: Coin[] = [
[kudos`0.1`, 8],
[kudos`1`, 0],
[kudos`2`, 1],
[kudos`5`, 0],
[kudos`10`, 1],
];
const result = getMaxDepositAmountForAvailableCoins(
{
list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
exchanges: {
one: {
wireFee: kudos`0.01`,
purseFee: kudos`0.00`,
creditDeadline: AbsoluteTime.never(),
debitDeadline: AbsoluteTime.never(),
},
},
},
"KUDOS",
);
t.is(Amounts.stringifyValue(result.effective), "12.8");
t.is(Amounts.stringifyValue(result.raw), "12.69");
// 8 x 0.1
// 1 x 0.2
// 1 x 10.0
// total effective 12.8
// deposit fee 10 x 0.01 = 0.10
// wire fee 0.01
// total raw: 12.8 - 0.11 = 12.69
// current wallet impl fee 0.14
});

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,763 @@
/*
This file is part of GNU Taler
(C) 2022 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
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 {
AbsoluteTime,
AgeRestriction,
AmountJson,
Amounts,
Duration,
TransactionAmountMode,
} from "@gnu-taler/taler-util";
import test, { ExecutionContext } from "ava";
import { CoinInfo } from "./coinSelection.js";
import { convertDepositAmountForAvailableCoins, getMaxDepositAmountForAvailableCoins, convertWithdrawalAmountFromAvailableCoins } from "./instructedAmountConversion.js";
function makeCurrencyHelper(currency: string) {
return (sx: TemplateStringsArray, ...vx: any[]) => {
const s = String.raw({ raw: sx }, ...vx);
return Amounts.parseOrThrow(`${currency}:${s}`);
};
}
const kudos = makeCurrencyHelper("kudos");
function defaultFeeConfig(value: AmountJson, totalAvailable: number): CoinInfo {
return {
id: Amounts.stringify(value),
denomDeposit: kudos`0.01`,
denomRefresh: kudos`0.01`,
denomWithdraw: kudos`0.01`,
exchangeBaseUrl: "1",
duration: Duration.getForever(),
exchangePurse: undefined,
exchangeWire: undefined,
maxAge: AgeRestriction.AGE_UNRESTRICTED,
totalAvailable,
value,
};
}
type Coin = [AmountJson, number];
/**
* Making a deposit with effective amount
*
*/
test("deposit effective 2", (t) => {
const coinList: Coin[] = [
[kudos`2`, 5],
[kudos`5`, 5],
];
const result = convertDepositAmountForAvailableCoins(
{
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), "1.99");
});
test("deposit 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`10`,
TransactionAmountMode.Effective,
);
t.is(Amounts.stringifyValue(result.effective), "10");
t.is(Amounts.stringifyValue(result.raw), "9.98");
});
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("deposit 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`40`,
TransactionAmountMode.Effective,
);
t.is(Amounts.stringifyValue(result.effective), "35");
t.is(Amounts.stringifyValue(result.raw), "34.9");
});
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");
});
/**
* 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],
];
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,
);
t.is(Amounts.stringifyValue(result.effective), "2");
t.is(Amounts.stringifyValue(result.raw), "1.89");
});
/**
* Calculating the max amount possible to deposit
*
*/
test("deposit max 35", (t) => {
const coinList: Coin[] = [
[kudos`2`, 5],
[kudos`5`, 5],
];
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(),
},
},
},
"KUDOS",
);
t.is(Amounts.stringifyValue(result.raw), "34.9");
t.is(Amounts.stringifyValue(result.effective), "35");
});
test("deposit max 35 with wirefee", (t) => {
const coinList: Coin[] = [
[kudos`2`, 5],
[kudos`5`, 5],
];
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(),
},
},
},
"KUDOS",
);
t.is(Amounts.stringifyValue(result.raw), "33.9");
t.is(Amounts.stringifyValue(result.effective), "35");
});
test("deposit max repeated denom", (t) => {
const coinList: Coin[] = [
[kudos`2`, 1],
[kudos`2`, 1],
[kudos`5`, 1],
];
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(),
},
},
},
"KUDOS",
);
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];
});
}
/**
* regression tests
*/
test("demo: withdraw raw 25", (t) => {
const coinList: Coin[] = [
[kudos`0.1`, 0],
[kudos`1`, 0],
[kudos`2`, 0],
[kudos`5`, 0],
[kudos`10`, 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.92");
// coins received
// 8 x 0.1
// 2 x 0.2
// 2 x 10.0
// total effective 24.8
// fee 12 x 0.01 = 0.12
// total raw 24.92
// left in reserve 25 - 24.92 == 0.08
//current wallet impl: hides the left in reserve fee
//shows fee = 0.2
});
test("demo: deposit max after withdraw raw 25", (t) => {
const coinList: Coin[] = [
[kudos`0.1`, 8],
[kudos`1`, 0],
[kudos`2`, 2],
[kudos`5`, 0],
[kudos`10`, 2],
];
const result = getMaxDepositAmountForAvailableCoins(
{
list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
exchanges: {
one: {
wireFee: kudos`0.01`,
purseFee: kudos`0.00`,
creditDeadline: AbsoluteTime.never(),
debitDeadline: AbsoluteTime.never(),
},
},
},
"KUDOS",
);
t.is(Amounts.stringifyValue(result.effective), "24.8");
t.is(Amounts.stringifyValue(result.raw), "24.67");
// 8 x 0.1
// 2 x 0.2
// 2 x 10.0
// total effective 24.8
// deposit fee 12 x 0.01 = 0.12
// wire fee 0.01
// total raw: 24.8 - 0.13 = 24.67
// current wallet impl fee 0.14
});
test("demo: withdraw raw 13", (t) => {
const coinList: Coin[] = [
[kudos`0.1`, 0],
[kudos`1`, 0],
[kudos`2`, 0],
[kudos`5`, 0],
[kudos`10`, 0],
];
const result = convertWithdrawalAmountFromAvailableCoins(
{
list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
exchanges: {},
},
kudos`13`,
TransactionAmountMode.Raw,
);
t.is(Amounts.stringifyValue(result.effective), "12.8");
t.is(Amounts.stringifyValue(result.raw), "12.9");
// coins received
// 8 x 0.1
// 1 x 0.2
// 1 x 10.0
// total effective 12.8
// fee 10 x 0.01 = 0.10
// total raw 12.9
// left in reserve 13 - 12.9 == 0.1
//current wallet impl: hides the left in reserve fee
//shows fee = 0.2
});
test("demo: deposit max after withdraw raw 13", (t) => {
const coinList: Coin[] = [
[kudos`0.1`, 8],
[kudos`1`, 0],
[kudos`2`, 1],
[kudos`5`, 0],
[kudos`10`, 1],
];
const result = getMaxDepositAmountForAvailableCoins(
{
list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
exchanges: {
one: {
wireFee: kudos`0.01`,
purseFee: kudos`0.00`,
creditDeadline: AbsoluteTime.never(),
debitDeadline: AbsoluteTime.never(),
},
},
},
"KUDOS",
);
t.is(Amounts.stringifyValue(result.effective), "12.8");
t.is(Amounts.stringifyValue(result.raw), "12.69");
// 8 x 0.1
// 1 x 0.2
// 1 x 10.0
// total effective 12.8
// deposit fee 10 x 0.01 = 0.10
// wire fee 0.01
// total raw: 12.8 - 0.11 = 12.69
// current wallet impl fee 0.14
});

View File

@ -0,0 +1,849 @@
/*
This file is part of GNU Taler
(C) 2023 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
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 {
AbsoluteTime,
AgeRestriction,
AmountJson,
AmountResponse,
Amounts,
ConvertAmountRequest,
Duration,
GetAmountRequest,
GetPlanForOperationRequest,
TransactionAmountMode,
TransactionType,
parsePaytoUri,
strcmp,
} from "@gnu-taler/taler-util";
import { checkDbInvariant } from "./invariants.js";
import {
DenominationRecord,
InternalWalletState,
getExchangeDetails,
} from "../index.js";
import { CoinInfo } from "./coinSelection.js";
import { GlobalIDB } from "@gnu-taler/idb-bridge";
/**
* If the operation going to be plan subtracts
* or adds amount in the wallet db
*/
export enum OperationType {
Credit = "credit",
Debit = "debit",
}
// FIXME: Name conflict ...
interface ExchangeInfo {
wireFee: AmountJson | undefined;
purseFee: AmountJson | undefined;
creditDeadline: AbsoluteTime;
debitDeadline: AbsoluteTime;
}
function getOperationType(txType: TransactionType): OperationType {
const operationType =
txType === TransactionType.Withdrawal
? OperationType.Credit
: txType === TransactionType.Deposit
? OperationType.Debit
: undefined;
if (!operationType) {
throw Error(`operation type ${txType} not yet supported`);
}
return operationType;
}
interface SelectedCoins {
totalValue: AmountJson;
coins: { info: CoinInfo; size: number }[];
refresh?: RefreshChoice;
}
function getCoinsFilter(req: GetPlanForOperationRequest): CoinsFilter {
switch (req.type) {
case TransactionType.Withdrawal: {
return {
exchanges:
req.exchangeUrl === undefined ? undefined : [req.exchangeUrl],
};
}
case TransactionType.Deposit: {
const payto = parsePaytoUri(req.account);
if (!payto) {
throw Error(`wrong payto ${req.account}`);
}
return {
wireMethod: payto.targetType,
};
}
}
}
interface RefreshChoice {
/**
* Amount that need to be covered
*/
gap: AmountJson;
totalFee: AmountJson;
selected: CoinInfo;
totalChangeValue: AmountJson;
refreshEffective: AmountJson;
coins: { info: CoinInfo; size: number }[];
// totalValue: AmountJson;
// totalDepositFee: AmountJson;
// totalRefreshFee: AmountJson;
// totalChangeContribution: AmountJson;
// totalChangeWithdrawalFee: AmountJson;
}
interface CoinsFilter {
shouldCalculatePurseFee?: boolean;
exchanges?: string[];
wireMethod?: string;
ageRestricted?: number;
}
interface AvailableCoins {
list: CoinInfo[];
exchanges: Record<string, ExchangeInfo>;
}
/**
* Get all the denoms that can be used for a operation that is limited
* by the following restrictions.
* This function is costly (by the database access) but with high chances
* of being cached
*/
async function getAvailableDenoms(
ws: InternalWalletState,
op: TransactionType,
currency: string,
filters: CoinsFilter = {},
): Promise<AvailableCoins> {
const operationType = getOperationType(TransactionType.Deposit);
return await ws.db
.mktx((x) => [
x.exchanges,
x.exchangeDetails,
x.denominations,
x.coinAvailability,
])
.runReadOnly(async (tx) => {
const list: CoinInfo[] = [];
const exchanges: Record<string, ExchangeInfo> = {};
const databaseExchanges = await tx.exchanges.iter().toArray();
const filteredExchanges =
filters.exchanges ?? databaseExchanges.map((e) => e.baseUrl);
for (const exchangeBaseUrl of filteredExchanges) {
const exchangeDetails = await getExchangeDetails(tx, exchangeBaseUrl);
// 1.- exchange has same currency
if (exchangeDetails?.currency !== currency) {
continue;
}
let deadline = AbsoluteTime.never();
// 2.- exchange supports wire method
let wireFee: AmountJson | undefined;
if (filters.wireMethod) {
const wireMethodWithDates =
exchangeDetails.wireInfo.feesForType[filters.wireMethod];
if (!wireMethodWithDates) {
throw Error(
`exchange ${exchangeBaseUrl} doesn't have wire method ${filters.wireMethod}`,
);
}
const wireMethodFee = wireMethodWithDates.find((x) => {
return AbsoluteTime.isBetween(
AbsoluteTime.now(),
AbsoluteTime.fromProtocolTimestamp(x.startStamp),
AbsoluteTime.fromProtocolTimestamp(x.endStamp),
);
});
if (!wireMethodFee) {
throw Error(
`exchange ${exchangeBaseUrl} doesn't have wire fee defined for this period`,
);
}
wireFee = Amounts.parseOrThrow(wireMethodFee.wireFee);
deadline = AbsoluteTime.min(
deadline,
AbsoluteTime.fromProtocolTimestamp(wireMethodFee.endStamp),
);
}
// exchanges[exchangeBaseUrl].wireFee = wireMethodFee;
// 3.- exchange supports wire method
let purseFee: AmountJson | undefined;
if (filters.shouldCalculatePurseFee) {
const purseFeeFound = exchangeDetails.globalFees.find((x) => {
return AbsoluteTime.isBetween(
AbsoluteTime.now(),
AbsoluteTime.fromProtocolTimestamp(x.startDate),
AbsoluteTime.fromProtocolTimestamp(x.endDate),
);
});
if (!purseFeeFound) {
throw Error(
`exchange ${exchangeBaseUrl} doesn't have purse fee defined for this period`,
);
}
purseFee = Amounts.parseOrThrow(purseFeeFound.purseFee);
deadline = AbsoluteTime.min(
deadline,
AbsoluteTime.fromProtocolTimestamp(purseFeeFound.endDate),
);
}
let creditDeadline = AbsoluteTime.never();
let debitDeadline = AbsoluteTime.never();
//4.- filter coins restricted by age
if (operationType === OperationType.Credit) {
const ds = await tx.denominations.indexes.byExchangeBaseUrl.getAll(
exchangeBaseUrl,
);
for (const denom of ds) {
const expiresWithdraw = AbsoluteTime.fromProtocolTimestamp(
denom.stampExpireWithdraw,
);
const expiresDeposit = AbsoluteTime.fromProtocolTimestamp(
denom.stampExpireDeposit,
);
creditDeadline = AbsoluteTime.min(deadline, expiresWithdraw);
debitDeadline = AbsoluteTime.min(deadline, expiresDeposit);
list.push(
buildCoinInfoFromDenom(
denom,
purseFee,
wireFee,
AgeRestriction.AGE_UNRESTRICTED,
Number.MAX_SAFE_INTEGER, // Max withdrawable from single denom
),
);
}
} else {
const ageLower = filters.ageRestricted ?? 0;
const ageUpper = AgeRestriction.AGE_UNRESTRICTED;
const myExchangeCoins =
await tx.coinAvailability.indexes.byExchangeAgeAvailability.getAll(
GlobalIDB.KeyRange.bound(
[exchangeDetails.exchangeBaseUrl, ageLower, 1],
[
exchangeDetails.exchangeBaseUrl,
ageUpper,
Number.MAX_SAFE_INTEGER,
],
),
);
//5.- save denoms with how many coins are available
// FIXME: Check that the individual denomination is audited!
// FIXME: Should we exclude denominations that are
// not spendable anymore?
for (const coinAvail of myExchangeCoins) {
const denom = await tx.denominations.get([
coinAvail.exchangeBaseUrl,
coinAvail.denomPubHash,
]);
checkDbInvariant(!!denom);
if (denom.isRevoked || !denom.isOffered) {
continue;
}
const expiresWithdraw = AbsoluteTime.fromProtocolTimestamp(
denom.stampExpireWithdraw,
);
const expiresDeposit = AbsoluteTime.fromProtocolTimestamp(
denom.stampExpireDeposit,
);
creditDeadline = AbsoluteTime.min(deadline, expiresWithdraw);
debitDeadline = AbsoluteTime.min(deadline, expiresDeposit);
list.push(
buildCoinInfoFromDenom(
denom,
purseFee,
wireFee,
coinAvail.maxAge,
coinAvail.freshCoinCount,
),
);
}
}
exchanges[exchangeBaseUrl] = {
purseFee,
wireFee,
debitDeadline,
creditDeadline,
};
}
return { list, exchanges };
});
}
function buildCoinInfoFromDenom(
denom: DenominationRecord,
purseFee: AmountJson | undefined,
wireFee: AmountJson | undefined,
maxAge: number,
total: number,
): CoinInfo {
return {
id: denom.denomPubHash,
denomWithdraw: Amounts.parseOrThrow(denom.fees.feeWithdraw),
denomDeposit: Amounts.parseOrThrow(denom.fees.feeDeposit),
denomRefresh: Amounts.parseOrThrow(denom.fees.feeRefresh),
exchangePurse: purseFee,
exchangeWire: wireFee,
exchangeBaseUrl: denom.exchangeBaseUrl,
duration: AbsoluteTime.difference(
AbsoluteTime.now(),
AbsoluteTime.fromProtocolTimestamp(denom.stampExpireDeposit),
),
totalAvailable: total,
value: DenominationRecord.getValue(denom),
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

@ -276,13 +276,6 @@ import {
} from "./operations/withdraw.js"; } from "./operations/withdraw.js";
import { PendingTaskInfo, PendingTaskType } from "./pending-types.js"; import { PendingTaskInfo, PendingTaskType } from "./pending-types.js";
import { assertUnreachable } from "./util/assertUnreachable.js"; import { assertUnreachable } from "./util/assertUnreachable.js";
import {
convertDepositAmount,
convertPeerPushAmount,
convertWithdrawalAmount,
getMaxDepositAmount,
getMaxPeerPushAmount,
} from "./util/coinSelection.js";
import { import {
createTimeline, createTimeline,
selectBestForOverlappingDenominations, selectBestForOverlappingDenominations,
@ -313,6 +306,13 @@ import {
WalletCoreApiClient, WalletCoreApiClient,
WalletCoreResponseType, WalletCoreResponseType,
} from "./wallet-api-types.js"; } from "./wallet-api-types.js";
import {
convertDepositAmount,
getMaxDepositAmount,
convertPeerPushAmount,
getMaxPeerPushAmount,
convertWithdrawalAmount,
} from "./util/instructedAmountConversion.js";
const logger = new Logger("wallet.ts"); const logger = new Logger("wallet.ts");