wallet-core: split coin selection and instructed amount conversion
This commit is contained in:
parent
5852b5cf2e
commit
a386de8a9c
@ -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,
|
||||||
|
@ -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");
|
||||||
|
|
||||||
|
@ -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");
|
||||||
|
|
||||||
|
@ -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
@ -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
|
||||||
|
});
|
@ -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);
|
||||||
|
}
|
@ -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");
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user