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