add test to coin selection algorithm

This commit is contained in:
Sebastian 2023-06-15 13:07:31 -03:00
parent f7058a86c9
commit d0d7685f16
No known key found for this signature in database
GPG Key ID: 173909D1A5F66069
2 changed files with 553 additions and 257 deletions

View File

@ -14,6 +14,18 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import test, { ExecutionContext } from "ava";
import {
calculatePlanFormAvailableCoins,
selectCoinForOperation,
} from "./coinSelection.js";
import {
AbsoluteTime,
AgeRestriction,
AmountJson,
Amounts,
Duration,
TransactionType,
} from "@gnu-taler/taler-util";
function expect(t: ExecutionContext, thing: any): any {
return {
@ -24,6 +36,185 @@ function expect(t: ExecutionContext, thing: any): any {
};
}
test("should have a test", (t) => {
expect(t, true).deep.equal(true);
function kudos(v: number): AmountJson {
return Amounts.fromFloat(v, "KUDOS");
}
function defaultFeeConfig(value: AmountJson, totalAvailable: number) {
return {
id: Amounts.stringify(value),
denomDeposit: kudos(0.01),
denomRefresh: kudos(0.01),
denomWithdraw: kudos(0.01),
duration: Duration.getForever(),
exchangePurse: undefined,
exchangeWire: undefined,
maxAge: AgeRestriction.AGE_UNRESTRICTED,
totalAvailable,
value,
};
}
type Coin = [AmountJson, number];
/**
* selectCoinForOperation test
*
* Test here should check that the correct coins are selected
*/
test("get effective 2", (t) => {
const coinList: Coin[] = [
[kudos(2), 5],
[kudos(5), 5],
];
const result = selectCoinForOperation("credit", kudos(2), "net", {
list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
exchanges: {},
});
expect(t, result.coins).deep.equal(["KUDOS:2"]);
t.assert(result.refresh === undefined);
});
test("get raw 4", (t) => {
const coinList: Coin[] = [
[kudos(2), 5],
[kudos(5), 5],
];
const result = selectCoinForOperation("credit", kudos(4), "gross", {
list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
exchanges: {},
});
expect(t, result.coins).deep.equal(["KUDOS:2", "KUDOS:2"]);
t.assert(result.refresh === undefined);
});
test("send effective 6", (t) => {
const coinList: Coin[] = [
[kudos(2), 5],
[kudos(5), 5],
];
const result = selectCoinForOperation("debit", kudos(6), "gross", {
list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
exchanges: {},
});
expect(t, result.coins).deep.equal(["KUDOS:5"]);
t.assert(result.refresh?.selected === "KUDOS:2");
});
test("send raw 6", (t) => {
const coinList: Coin[] = [
[kudos(2), 5],
[kudos(5), 5],
];
const result = selectCoinForOperation("debit", kudos(6), "gross", {
list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
exchanges: {},
});
expect(t, result.coins).deep.equal(["KUDOS:5"]);
t.assert(result.refresh?.selected === "KUDOS:2");
});
test("send raw 20 (not enough)", (t) => {
const coinList: Coin[] = [
[kudos(2), 1],
[kudos(5), 2],
];
const result = selectCoinForOperation("debit", kudos(20), "gross", {
list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
exchanges: {},
});
expect(t, result.coins).deep.equal(["KUDOS:5", "KUDOS:5", "KUDOS:2"]);
t.assert(result.refresh === undefined);
});
/**
* calculatePlanFormAvailableCoins test
*
* Test here should check that the plan summary for a transaction is correct
* * effective amount
* * raw amount
*/
test("deposit effective 2 ", (t) => {
const coinList: Coin[] = [
[kudos(2), 1],
[kudos(5), 2],
];
const result = calculatePlanFormAvailableCoins(
TransactionType.Deposit,
kudos(2),
"effective",
{
list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
exchanges: {
"2": {
creditDeadline: AbsoluteTime.never(),
debitDeadline: AbsoluteTime.never(),
wireFee: kudos(0.01),
purseFee: kudos(0.01),
},
},
},
);
t.deepEqual(result.rawAmount, "KUDOS:1.98");
t.deepEqual(result.effectiveAmount, "KUDOS:2");
});
test("deposit raw 2 ", (t) => {
const coinList: Coin[] = [
[kudos(2), 1],
[kudos(5), 2],
];
const result = calculatePlanFormAvailableCoins(
TransactionType.Deposit,
kudos(2),
"raw",
{
list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
exchanges: {
"2": {
creditDeadline: AbsoluteTime.never(),
debitDeadline: AbsoluteTime.never(),
wireFee: kudos(0.01),
purseFee: kudos(0.01),
},
},
},
);
t.deepEqual(result.rawAmount, "KUDOS:2");
t.deepEqual(result.effectiveAmount, "KUDOS:2.04");
});
test("withdraw raw 21 ", (t) => {
const coinList: Coin[] = [
[kudos(2), 1],
[kudos(5), 2],
];
const result = calculatePlanFormAvailableCoins(
TransactionType.Withdrawal,
kudos(21),
"raw",
{
list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
exchanges: {
"2": {
creditDeadline: AbsoluteTime.never(),
debitDeadline: AbsoluteTime.never(),
wireFee: kudos(0.01),
purseFee: kudos(0.01),
},
},
},
);
// denominations configuration is not suitable
// for greedy algorithm
t.deepEqual(result.rawAmount, "KUDOS:20");
t.deepEqual(result.effectiveAmount, "KUDOS:19.96");
});

View File

@ -35,6 +35,7 @@ import {
DenominationInfo,
DenominationPubKey,
DenomSelectionState,
Duration,
ForcedCoinSel,
ForcedDenomSel,
GetPlanForOperationRequest,
@ -52,7 +53,11 @@ import {
AllowedExchangeInfo,
DenominationRecord,
} from "../db.js";
import { getExchangeDetails, isWithdrawableDenom } from "../index.js";
import {
CoinAvailabilityRecord,
getExchangeDetails,
isWithdrawableDenom,
} from "../index.js";
import { InternalWalletState } from "../internal-wallet-state.js";
import { getMerchantPaymentBalanceDetails } from "../operations/balance.js";
import { checkDbInvariant, checkLogicInvariant } from "./invariants.js";
@ -790,6 +795,92 @@ export function selectForcedWithdrawalDenominations(
};
}
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,
};
}
}
}
export function calculatePlanFormAvailableCoins(
transactionType: TransactionType,
amount: AmountJson,
mode: "effective" | "raw",
availableCoins: AvailableCoins,
) {
const operationType = getOperationType(transactionType);
let usableCoins;
switch (transactionType) {
case TransactionType.Withdrawal: {
usableCoins = selectCoinForOperation(
operationType,
amount,
mode === "effective" ? "net" : "gross",
availableCoins,
);
break;
}
case TransactionType.Deposit: {
//FIXME: just doing for 1 exchange now
//assuming that the wallet has one exchange and all the coins available
//are from that exchange
const wireFee = Object.values(availableCoins.exchanges)[0].wireFee!;
if (mode === "effective") {
usableCoins = selectCoinForOperation(
operationType,
amount,
"gross",
availableCoins,
);
usableCoins.totalContribution = Amounts.sub(
usableCoins.totalContribution,
wireFee,
).amount;
} else {
const adjustedAmount = Amounts.add(amount, wireFee).amount;
usableCoins = selectCoinForOperation(
operationType,
adjustedAmount,
"net",
availableCoins,
);
usableCoins.totalContribution = Amounts.sub(
usableCoins.totalContribution,
wireFee,
).amount;
}
break;
}
default: {
throw Error("operation not supported");
}
}
return getAmountsWithFee(
operationType,
usableCoins!.totalValue,
usableCoins!.totalContribution,
usableCoins,
);
}
/**
* simulate a coin selection and return the amount
* that will effectively change the wallet balance and
@ -804,106 +895,22 @@ export async function getPlanForOperation(
req: GetPlanForOperationRequest,
): Promise<GetPlanForOperationResponse> {
const amount = Amounts.parseOrThrow(req.instructedAmount);
const operationType = getOperationType(req.type);
const filter = getCoinsFilter(req);
switch (req.type) {
case TransactionType.Withdrawal: {
const availableCoins = await getAvailableCoins(
ws,
"credit",
operationType,
amount.currency,
false,
false,
undefined,
undefined,
undefined,
filter,
);
const usableCoins = selectCoinForOperation(
"credit",
return calculatePlanFormAvailableCoins(
req.type,
amount,
req.mode === "effective" ? "net" : "gross",
availableCoins.denoms,
req.mode,
availableCoins,
);
return getAmountsWithFee(
"credit",
usableCoins.totalValue,
usableCoins.totalContribution,
usableCoins,
);
}
case TransactionType.Deposit: {
const payto = parsePaytoUri(req.account)!;
const availableCoins = await getAvailableCoins(
ws,
"debit",
amount.currency,
true,
false,
undefined,
[payto.targetType],
undefined,
);
//FIXME: just doing for 1 exchange now
//assuming that the wallet has one exchange and all the coins available
//are from that exchange
const wireFee = Object.entries(availableCoins.wfPerExchange)[0][1][
payto.targetType
];
let usableCoins;
if (req.mode === "effective") {
usableCoins = selectCoinForOperation(
"debit",
amount,
"gross",
availableCoins.denoms,
);
usableCoins.totalContribution = Amounts.stringify(
Amounts.sub(usableCoins.totalContribution, wireFee).amount,
);
} else {
const adjustedAmount = Amounts.add(amount, wireFee).amount;
usableCoins = selectCoinForOperation(
"debit",
adjustedAmount,
// amount,
"net",
availableCoins.denoms,
);
usableCoins.totalContribution = Amounts.stringify(
Amounts.sub(usableCoins.totalContribution, wireFee).amount,
);
}
return getAmountsWithFee(
"debit",
usableCoins.totalValue,
usableCoins.totalContribution,
usableCoins,
);
}
default: {
throw Error("operation not supported");
}
}
}
function getAmountsWithFee(
op: "debit" | "credit",
value: AmountString,
contribution: AmountString,
details: any,
): GetPlanForOperationResponse {
return {
rawAmount: op === "credit" ? value : contribution,
effectiveAmount: op === "credit" ? contribution : value,
details,
};
}
/**
@ -914,24 +921,20 @@ function getAmountsWithFee(
* @param denoms list of available denomination for the operation
* @returns
*/
function selectCoinForOperation(
export function selectCoinForOperation(
op: "debit" | "credit",
limit: AmountJson,
mode: "net" | "gross",
denoms: AvailableDenom[],
coins: AvailableCoins,
): SelectedCoins {
const result: SelectedCoins = {
totalValue: Amounts.stringify(Amounts.zeroOfCurrency(limit.currency)),
totalWithdrawalFee: Amounts.stringify(
Amounts.zeroOfCurrency(limit.currency),
),
totalDepositFee: Amounts.stringify(Amounts.zeroOfCurrency(limit.currency)),
totalContribution: Amounts.stringify(
Amounts.zeroOfCurrency(limit.currency),
),
totalValue: Amounts.zeroOfCurrency(limit.currency),
totalWithdrawalFee: Amounts.zeroOfCurrency(limit.currency),
totalDepositFee: Amounts.zeroOfCurrency(limit.currency),
totalContribution: Amounts.zeroOfCurrency(limit.currency),
coins: [],
};
if (!denoms.length) return result;
if (!coins.list.length) return result;
/**
* We can make this faster. We should prevent sorting and
* keep the information ready for multiple calls since this
@ -940,28 +943,26 @@ function selectCoinForOperation(
*/
//rank coins
denoms.sort(
op === "credit"
? denomsByDescendingWithdrawContribution
: denomsByDescendingDepositContribution,
);
coins.list.sort(buildRankingForCoins(op));
//take coins in order until amount
let selectedCoinsAreEnough = false;
let denomIdx = 0;
iterateDenoms: while (denomIdx < denoms.length) {
const cur = denoms[denomIdx];
// for (const cur of denoms) {
let total = op === "credit" ? Number.MAX_SAFE_INTEGER : cur.numAvailable;
const opFee = op === "credit" ? cur.feeWithdraw : cur.feeDeposit;
const contribution = Amounts.sub(cur.value, opFee).amount;
iterateDenoms: while (denomIdx < coins.list.length) {
const denom = coins.list[denomIdx];
let total =
op === "credit" ? Number.MAX_SAFE_INTEGER : denom.totalAvailable ?? 0;
const opFee = op === "credit" ? denom.denomWithdraw : denom.denomDeposit;
const contribution = Amounts.sub(denom.value, opFee).amount;
if (Amounts.isZero(contribution)) {
// 0 contribution denoms should be the last
break iterateDenoms;
}
//use Amounts.divmod instead of iterate
iterateCoins: while (total > 0) {
const nextValue = Amounts.add(result.totalValue, cur.value).amount;
const nextValue = Amounts.add(result.totalValue, denom.value).amount;
const nextContribution = Amounts.add(
result.totalContribution,
@ -975,18 +976,20 @@ function selectCoinForOperation(
break iterateCoins;
}
result.totalValue = Amounts.stringify(nextValue);
result.totalContribution = Amounts.stringify(nextContribution);
result.totalValue = nextValue;
result.totalContribution = nextContribution;
result.totalDepositFee = Amounts.stringify(
Amounts.add(result.totalDepositFee, cur.feeDeposit).amount,
);
result.totalDepositFee = Amounts.add(
result.totalDepositFee,
denom.denomDeposit,
).amount;
result.totalWithdrawalFee = Amounts.stringify(
Amounts.add(result.totalWithdrawalFee, cur.feeWithdraw).amount,
);
result.totalWithdrawalFee = Amounts.add(
result.totalWithdrawalFee,
denom.denomWithdraw,
).amount;
result.coins.push(cur.denomPubHash);
result.coins.push(denom.id);
if (Amounts.cmp(progress, limit) === 0) {
selectedCoinsAreEnough = true;
@ -1021,12 +1024,12 @@ function selectCoinForOperation(
let refreshIdx = 0;
let choice: RefreshChoice | undefined = undefined;
refreshIteration: while (refreshIdx < denoms.length) {
const d = denoms[refreshIdx];
refreshIteration: while (refreshIdx < coins.list.length) {
const d = coins.list[refreshIdx];
const denomContribution =
mode === "gross"
? Amounts.sub(d.value, d.feeRefresh).amount
: Amounts.sub(d.value, d.feeDeposit, d.feeRefresh).amount;
? Amounts.sub(d.value, d.denomRefresh).amount
: Amounts.sub(d.value, d.denomDeposit, d.denomRefresh).amount;
const changeAfterDeposit = Amounts.sub(denomContribution, gap).amount;
if (Amounts.isZero(changeAfterDeposit)) {
@ -1038,30 +1041,26 @@ function selectCoinForOperation(
"credit",
changeAfterDeposit,
mode,
denoms,
coins,
);
const totalFee = Amounts.add(
d.feeDeposit,
d.feeRefresh,
d.denomDeposit,
d.denomRefresh,
changeCost.totalWithdrawalFee,
).amount;
if (!choice || Amounts.cmp(totalFee, choice.totalFee) === -1) {
//found cheaper change
choice = {
gap: Amounts.stringify(gap),
totalFee: Amounts.stringify(totalFee),
selected: d.denomPubHash,
gap: gap,
totalFee: totalFee,
selected: d.id,
totalValue: d.value,
totalRefreshFee: Amounts.stringify(d.feeRefresh),
totalDepositFee: d.feeDeposit,
totalChangeValue: Amounts.stringify(changeCost.totalValue),
totalChangeContribution: Amounts.stringify(
changeCost.totalContribution,
),
totalChangeWithdrawalFee: Amounts.stringify(
changeCost.totalWithdrawalFee,
),
totalRefreshFee: d.denomRefresh,
totalDepositFee: d.denomDeposit,
totalChangeValue: changeCost.totalValue,
totalChangeContribution: changeCost.totalContribution,
totalChangeWithdrawalFee: changeCost.totalWithdrawalFee,
change: changeCost.coins,
};
}
@ -1069,22 +1068,25 @@ function selectCoinForOperation(
}
if (choice) {
if (mode === "gross") {
result.totalValue = Amounts.stringify(
Amounts.add(result.totalValue, gap).amount,
);
result.totalContribution = Amounts.stringify(
Amounts.add(result.totalContribution, gap).amount,
);
result.totalContribution = Amounts.stringify(
Amounts.sub(result.totalContribution, choice.totalFee).amount,
);
result.totalValue = Amounts.add(result.totalValue, gap).amount;
result.totalContribution = Amounts.add(
result.totalContribution,
gap,
).amount;
result.totalContribution = Amounts.sub(
result.totalContribution,
choice.totalFee,
).amount;
} else {
result.totalContribution = Amounts.stringify(
Amounts.add(result.totalContribution, gap).amount,
);
result.totalValue = Amounts.stringify(
Amounts.add(result.totalValue, gap, choice.totalFee).amount,
);
result.totalContribution = Amounts.add(
result.totalContribution,
gap,
).amount;
result.totalValue = Amounts.add(
result.totalValue,
gap,
choice.totalFee,
).amount;
}
}
@ -1093,50 +1095,105 @@ function selectCoinForOperation(
return result;
}
function denomsByDescendingDepositContribution(
d1: AvailableDenom,
d2: AvailableDenom,
) {
const contrib1 = Amounts.sub(d1.value, d1.feeDeposit).amount;
const contrib2 = Amounts.sub(d2.value, d2.feeDeposit).amount;
type CompareCoinsFunction = (d1: CoinInfo, d2: CoinInfo) => -1 | 0 | 1;
function buildRankingForCoins(op: "debit" | "credit"): CompareCoinsFunction {
function getFee(d: CoinInfo) {
return op === "credit" ? d.denomWithdraw : d.denomDeposit;
}
//different exchanges may have different wireFee
//ranking should take the relative contribution in the exchange
//which is (value - denomFee / fixedFee)
// where denomFee is withdraw or deposit
// and fixedFee can be purse or wire
return function rank(d1: CoinInfo, d2: CoinInfo) {
const contrib1 = Amounts.sub(d1.value, getFee(d1)).amount;
const contrib2 = Amounts.sub(d2.value, getFee(d2)).amount;
return (
Amounts.cmp(contrib2, contrib1) || strcmp(d1.denomPubHash, d2.denomPubHash)
Amounts.cmp(contrib2, contrib1) ||
Duration.cmp(d1.duration, d2.duration) ||
strcmp(d1.id, d2.id)
);
};
}
function denomsByDescendingWithdrawContribution(
d1: AvailableDenom,
d2: AvailableDenom,
) {
const contrib1 = Amounts.sub(d1.value, d1.feeWithdraw).amount;
const contrib2 = Amounts.sub(d2.value, d2.feeWithdraw).amount;
return (
Amounts.cmp(contrib2, contrib1) || strcmp(d1.denomPubHash, d2.denomPubHash)
);
function getOperationType(txType: TransactionType): "credit" | "debit" {
const operationType =
txType === TransactionType.Withdrawal
? ("credit" as const)
: txType === TransactionType.Deposit
? ("debit" as const)
: undefined;
if (!operationType) {
throw Error(`operation type ${txType} not supported`);
}
return operationType;
}
function getAmountsWithFee(
op: "debit" | "credit",
value: AmountJson,
contribution: AmountJson,
details: any,
): GetPlanForOperationResponse {
return {
rawAmount: Amounts.stringify(op === "credit" ? value : contribution),
effectiveAmount: Amounts.stringify(op === "credit" ? contribution : value),
details,
};
}
interface RefreshChoice {
gap: AmountString;
totalFee: AmountString;
gap: AmountJson;
totalFee: AmountJson;
selected: string;
totalValue: AmountString;
totalDepositFee: AmountString;
totalRefreshFee: AmountString;
totalChangeValue: AmountString;
totalChangeContribution: AmountString;
totalChangeWithdrawalFee: AmountString;
totalValue: AmountJson;
totalDepositFee: AmountJson;
totalRefreshFee: AmountJson;
totalChangeValue: AmountJson;
totalChangeContribution: AmountJson;
totalChangeWithdrawalFee: AmountJson;
change: string[];
}
interface SelectedCoins {
totalValue: AmountString;
totalContribution: AmountString;
totalWithdrawalFee: AmountString;
totalDepositFee: AmountString;
totalValue: AmountJson;
totalContribution: AmountJson;
totalWithdrawalFee: AmountJson;
totalDepositFee: AmountJson;
coins: string[];
refresh?: RefreshChoice;
}
interface AvailableCoins {
list: CoinInfo[];
exchanges: Record<string, ExchangeInfo>;
}
interface CoinInfo {
id: string;
value: AmountJson;
denomDeposit: AmountJson;
denomWithdraw: AmountJson;
denomRefresh: AmountJson;
totalAvailable: number | undefined;
exchangeWire: AmountJson | undefined;
exchangePurse: AmountJson | undefined;
duration: Duration;
maxAge: number;
}
interface ExchangeInfo {
wireFee: AmountJson | undefined;
purseFee: AmountJson | undefined;
creditDeadline: AbsoluteTime;
debitDeadline: AbsoluteTime;
}
interface CoinsFilter {
shouldCalculatePurseFee?: boolean;
exchanges?: string[];
wireMethod?: string;
ageRestricted?: number;
}
/**
* Get all the denoms that can be used for a operation that is limited
* by the following restrictions.
@ -1147,12 +1204,8 @@ async function getAvailableCoins(
ws: InternalWalletState,
op: "credit" | "debit",
currency: string,
shouldCalculateWireFee: boolean,
shouldCalculatePurseFee: boolean,
exchangeFilter: string[] | undefined,
wireMethodFilter: string[] | undefined,
ageRestrictedFilter: number | undefined,
) {
filters: CoinsFilter = {},
): Promise<AvailableCoins> {
return await ws.db
.mktx((x) => [
x.exchanges,
@ -1161,90 +1214,103 @@ async function getAvailableCoins(
x.coinAvailability,
])
.runReadOnly(async (tx) => {
const denoms: AvailableDenom[] = [];
const wfPerExchange: Record<string, Record<string, AmountJson>> = {};
const pfPerExchange: Record<string, AmountJson> = {};
const list: CoinInfo[] = [];
const exchanges: Record<string, ExchangeInfo> = {};
const databaseExchanges = await tx.exchanges.iter().toArray();
const exchanges =
exchangeFilter === undefined
? databaseExchanges.map((e) => e.baseUrl)
: exchangeFilter;
const filteredExchanges =
filters.exchanges ?? databaseExchanges.map((e) => e.baseUrl);
for (const exchangeBaseUrl of exchanges) {
for (const exchangeBaseUrl of filteredExchanges) {
const exchangeDetails = await getExchangeDetails(tx, exchangeBaseUrl);
// 1.- exchange has same currency
if (exchangeDetails?.currency !== currency) {
continue;
}
const wireMethodFee: Record<string, AmountJson> = {};
let deadline = AbsoluteTime.never();
// 2.- exchange supports wire method
if (shouldCalculateWireFee) {
for (const acc of exchangeDetails.wireInfo.accounts) {
const pp = parsePaytoUri(acc.payto_uri);
checkLogicInvariant(!!pp);
// also check that wire method is supported now
if (wireMethodFilter !== undefined) {
if (wireMethodFilter.indexOf(pp.targetType) === -1) {
continue;
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 wireFeeStr = exchangeDetails.wireInfo.feesForType[
pp.targetType
]?.find((x) => {
const wireMethodFee = wireMethodWithDates.find((x) => {
return AbsoluteTime.isBetween(
AbsoluteTime.now(),
AbsoluteTime.fromProtocolTimestamp(x.startStamp),
AbsoluteTime.fromProtocolTimestamp(x.endStamp),
);
})?.wireFee;
});
if (wireFeeStr) {
wireMethodFee[pp.targetType] = Amounts.parseOrThrow(wireFeeStr);
}
break;
}
if (Object.keys(wireMethodFee).length === 0) {
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),
);
}
wfPerExchange[exchangeBaseUrl] = wireMethodFee;
// exchanges[exchangeBaseUrl].wireFee = wireMethodFee;
// 3.- exchange supports wire method
if (shouldCalculatePurseFee) {
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),
);
})?.purseFee;
});
if (!purseFeeFound) {
throw Error(
`exchange ${exchangeBaseUrl} doesn't have purse fee defined for this period`,
);
}
const purseFee = Amounts.parseOrThrow(purseFeeFound);
pfPerExchange[exchangeBaseUrl] = purseFee;
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 (op === "credit") {
const ds = await tx.denominations.indexes.byExchangeBaseUrl.getAll(
exchangeBaseUrl,
);
for (const denom of ds) {
denoms.push({
...DenominationRecord.toDenomInfo(denom),
numAvailable: Number.MAX_SAFE_INTEGER,
maxAge: AgeRestriction.AGE_UNRESTRICTED,
});
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 = !ageRestrictedFilter ? 0 : ageRestrictedFilter;
const ageLower = filters.ageRestricted ?? 0;
const ageUpper = AgeRestriction.AGE_UNRESTRICTED;
const myExchangeCoins =
@ -1271,19 +1337,58 @@ async function getAvailableCoins(
if (denom.isRevoked || !denom.isOffered) {
continue;
}
denoms.push({
...DenominationRecord.toDenomInfo(denom),
numAvailable: coinAvail.freshCoinCount ?? 0,
maxAge: coinAvail.maxAge,
});
}
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,
),
);
}
}
return {
denoms,
wfPerExchange,
pfPerExchange,
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,
duration: AbsoluteTime.difference(
AbsoluteTime.now(),
AbsoluteTime.fromProtocolTimestamp(denom.stampExpireDeposit),
),
totalAvailable: total,
value: DenominationRecord.getValue(denom),
maxAge,
};
}