get operation plan impl, no test
This commit is contained in:
parent
671342818f
commit
8b74bda065
@ -52,7 +52,11 @@ 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 { OperationAttemptLongpollResult, OperationAttemptResult, OperationAttemptResultType } from "../util/retries.js";
|
import {
|
||||||
|
OperationAttemptLongpollResult,
|
||||||
|
OperationAttemptResult,
|
||||||
|
OperationAttemptResultType,
|
||||||
|
} from "../util/retries.js";
|
||||||
|
|
||||||
const logger = new Logger("operations/peer-to-peer.ts");
|
const logger = new Logger("operations/peer-to-peer.ts");
|
||||||
|
|
||||||
@ -113,6 +117,12 @@ export type SelectPeerCoinsResult =
|
|||||||
insufficientBalanceDetails: PayPeerInsufficientBalanceDetails;
|
insufficientBalanceDetails: PayPeerInsufficientBalanceDetails;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get information about the coin selected for signatures
|
||||||
|
* @param ws
|
||||||
|
* @param csel
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
export async function queryCoinInfosForSelection(
|
export async function queryCoinInfosForSelection(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
csel: PeerPushPaymentCoinSelection,
|
csel: PeerPushPaymentCoinSelection,
|
||||||
|
@ -30,29 +30,29 @@ import {
|
|||||||
AgeRestriction,
|
AgeRestriction,
|
||||||
AmountJson,
|
AmountJson,
|
||||||
Amounts,
|
Amounts,
|
||||||
|
AmountString,
|
||||||
CoinStatus,
|
CoinStatus,
|
||||||
DenominationInfo,
|
DenominationInfo,
|
||||||
DenominationPubKey,
|
DenominationPubKey,
|
||||||
DenomSelectionState,
|
DenomSelectionState,
|
||||||
ForcedCoinSel,
|
ForcedCoinSel,
|
||||||
ForcedDenomSel,
|
ForcedDenomSel,
|
||||||
|
GetPlanForOperationRequest,
|
||||||
|
GetPlanForOperationResponse,
|
||||||
j2s,
|
j2s,
|
||||||
Logger,
|
Logger,
|
||||||
parsePaytoUri,
|
parsePaytoUri,
|
||||||
PayCoinSelection,
|
PayCoinSelection,
|
||||||
PayMerchantInsufficientBalanceDetails,
|
PayMerchantInsufficientBalanceDetails,
|
||||||
strcmp,
|
strcmp,
|
||||||
|
TransactionType,
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import {
|
import {
|
||||||
AllowedAuditorInfo,
|
AllowedAuditorInfo,
|
||||||
AllowedExchangeInfo,
|
AllowedExchangeInfo,
|
||||||
DenominationRecord,
|
DenominationRecord,
|
||||||
} from "../db.js";
|
} from "../db.js";
|
||||||
import {
|
import { getExchangeDetails, isWithdrawableDenom } from "../index.js";
|
||||||
getExchangeDetails,
|
|
||||||
isWithdrawableDenom,
|
|
||||||
WalletConfig,
|
|
||||||
} 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";
|
||||||
@ -150,7 +150,7 @@ export interface CoinSelectionTally {
|
|||||||
/**
|
/**
|
||||||
* Account for the fees of spending a coin.
|
* Account for the fees of spending a coin.
|
||||||
*/
|
*/
|
||||||
export function tallyFees(
|
function tallyFees(
|
||||||
tally: Readonly<CoinSelectionTally>,
|
tally: Readonly<CoinSelectionTally>,
|
||||||
wireFeesPerExchange: Record<string, AmountJson>,
|
wireFeesPerExchange: Record<string, AmountJson>,
|
||||||
wireFeeAmortization: number,
|
wireFeeAmortization: number,
|
||||||
@ -542,7 +542,7 @@ export type AvailableDenom = DenominationInfo & {
|
|||||||
numAvailable: number;
|
numAvailable: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function selectCandidates(
|
async function selectCandidates(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
req: SelectPayCoinRequestNg,
|
req: SelectPayCoinRequestNg,
|
||||||
): Promise<[AvailableDenom[], Record<string, AmountJson>]> {
|
): Promise<[AvailableDenom[], Record<string, AmountJson>]> {
|
||||||
@ -789,3 +789,501 @@ export function selectForcedWithdrawalDenominations(
|
|||||||
totalWithdrawCost: Amounts.stringify(totalWithdrawCost),
|
totalWithdrawCost: Amounts.stringify(totalWithdrawCost),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* simulate a coin selection and return the amount
|
||||||
|
* that will effectively change the wallet balance and
|
||||||
|
* the raw amount of the operation
|
||||||
|
*
|
||||||
|
* @param ws
|
||||||
|
* @param br
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export async function getPlanForOperation(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
req: GetPlanForOperationRequest,
|
||||||
|
): Promise<GetPlanForOperationResponse> {
|
||||||
|
const amount = Amounts.parseOrThrow(req.instructedAmount);
|
||||||
|
|
||||||
|
switch (req.type) {
|
||||||
|
case TransactionType.Withdrawal: {
|
||||||
|
const availableCoins = await getAvailableCoins(
|
||||||
|
ws,
|
||||||
|
"credit",
|
||||||
|
amount.currency,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
const usableCoins = selectCoinForOperation(
|
||||||
|
"credit",
|
||||||
|
amount,
|
||||||
|
req.mode === "effective" ? "net" : "gross",
|
||||||
|
availableCoins.denoms,
|
||||||
|
);
|
||||||
|
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param op defined which fee are we taking into consideration: deposits or withdraw
|
||||||
|
* @param limit the total amount limit of the operation
|
||||||
|
* @param mode if the total amount is includes the fees or just the contribution
|
||||||
|
* @param denoms list of available denomination for the operation
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
function selectCoinForOperation(
|
||||||
|
op: "debit" | "credit",
|
||||||
|
limit: AmountJson,
|
||||||
|
mode: "net" | "gross",
|
||||||
|
denoms: AvailableDenom[],
|
||||||
|
): 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),
|
||||||
|
),
|
||||||
|
coins: [],
|
||||||
|
};
|
||||||
|
if (!denoms.length) return result;
|
||||||
|
/**
|
||||||
|
* We can make this faster. We should prevent sorting and
|
||||||
|
* keep the information ready for multiple calls since this
|
||||||
|
* function is expected to work on embedded devices and
|
||||||
|
* create a response on key press
|
||||||
|
*/
|
||||||
|
|
||||||
|
//rank coins
|
||||||
|
denoms.sort(
|
||||||
|
op === "credit"
|
||||||
|
? denomsByDescendingWithdrawContribution
|
||||||
|
: denomsByDescendingDepositContribution,
|
||||||
|
);
|
||||||
|
|
||||||
|
//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;
|
||||||
|
|
||||||
|
if (Amounts.isZero(contribution)) {
|
||||||
|
// 0 contribution denoms should be the last
|
||||||
|
break iterateDenoms;
|
||||||
|
}
|
||||||
|
iterateCoins: while (total > 0) {
|
||||||
|
const nextValue = Amounts.add(result.totalValue, cur.value).amount;
|
||||||
|
|
||||||
|
const nextContribution = Amounts.add(
|
||||||
|
result.totalContribution,
|
||||||
|
contribution,
|
||||||
|
).amount;
|
||||||
|
|
||||||
|
const progress = mode === "gross" ? nextValue : nextContribution;
|
||||||
|
|
||||||
|
if (Amounts.cmp(progress, limit) === 1) {
|
||||||
|
//the current coin is more than we need, try next denom
|
||||||
|
break iterateCoins;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.totalValue = Amounts.stringify(nextValue);
|
||||||
|
result.totalContribution = Amounts.stringify(nextContribution);
|
||||||
|
|
||||||
|
result.totalDepositFee = Amounts.stringify(
|
||||||
|
Amounts.add(result.totalDepositFee, cur.feeDeposit).amount,
|
||||||
|
);
|
||||||
|
|
||||||
|
result.totalWithdrawalFee = Amounts.stringify(
|
||||||
|
Amounts.add(result.totalWithdrawalFee, cur.feeWithdraw).amount,
|
||||||
|
);
|
||||||
|
|
||||||
|
result.coins.push(cur.denomPubHash);
|
||||||
|
|
||||||
|
if (Amounts.cmp(progress, limit) === 0) {
|
||||||
|
selectedCoinsAreEnough = true;
|
||||||
|
// we have just enough coins, complete
|
||||||
|
break iterateDenoms;
|
||||||
|
}
|
||||||
|
|
||||||
|
//go next coin
|
||||||
|
total--;
|
||||||
|
}
|
||||||
|
//go next denom
|
||||||
|
denomIdx++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedCoinsAreEnough) {
|
||||||
|
// we made it
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
if (op === "credit") {
|
||||||
|
//doing withdraw there is no way to cover the gap
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
//tried all the coins but there is a gap
|
||||||
|
//doing deposit we can try refreshing coins
|
||||||
|
|
||||||
|
const total = mode === "gross" ? result.totalValue : result.totalContribution;
|
||||||
|
const gap = Amounts.sub(limit, total).amount;
|
||||||
|
|
||||||
|
//about recursive calls
|
||||||
|
//the only way to get here is by doing a deposit (that will do a refresh)
|
||||||
|
//and now we are calculating fee for credit (which does not need to calculate refresh)
|
||||||
|
|
||||||
|
let refreshIdx = 0;
|
||||||
|
let choice: RefreshChoice | undefined = undefined;
|
||||||
|
refreshIteration: while (refreshIdx < denoms.length) {
|
||||||
|
const d = denoms[refreshIdx];
|
||||||
|
const denomContribution =
|
||||||
|
mode === "gross"
|
||||||
|
? Amounts.sub(d.value, d.feeRefresh).amount
|
||||||
|
: Amounts.sub(d.value, d.feeDeposit, d.feeRefresh).amount;
|
||||||
|
|
||||||
|
const changeAfterDeposit = Amounts.sub(denomContribution, gap).amount;
|
||||||
|
if (Amounts.isZero(changeAfterDeposit)) {
|
||||||
|
//the rest of the coins are very small
|
||||||
|
break refreshIteration;
|
||||||
|
}
|
||||||
|
|
||||||
|
const changeCost = selectCoinForOperation(
|
||||||
|
"credit",
|
||||||
|
changeAfterDeposit,
|
||||||
|
mode,
|
||||||
|
denoms,
|
||||||
|
);
|
||||||
|
const totalFee = Amounts.add(
|
||||||
|
d.feeDeposit,
|
||||||
|
d.feeRefresh,
|
||||||
|
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,
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
change: changeCost.coins,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
refreshIdx++;
|
||||||
|
}
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
result.totalContribution = Amounts.stringify(
|
||||||
|
Amounts.add(result.totalContribution, gap).amount,
|
||||||
|
);
|
||||||
|
result.totalValue = Amounts.stringify(
|
||||||
|
Amounts.add(result.totalValue, gap, choice.totalFee).amount,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// console.log("gap", Amounts.stringify(limit), Amounts.stringify(gap), choice);
|
||||||
|
result.refresh = choice;
|
||||||
|
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;
|
||||||
|
return (
|
||||||
|
Amounts.cmp(contrib2, contrib1) || strcmp(d1.denomPubHash, d2.denomPubHash)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RefreshChoice {
|
||||||
|
gap: AmountString;
|
||||||
|
totalFee: AmountString;
|
||||||
|
selected: string;
|
||||||
|
|
||||||
|
totalValue: AmountString;
|
||||||
|
totalDepositFee: AmountString;
|
||||||
|
totalRefreshFee: AmountString;
|
||||||
|
totalChangeValue: AmountString;
|
||||||
|
totalChangeContribution: AmountString;
|
||||||
|
totalChangeWithdrawalFee: AmountString;
|
||||||
|
change: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SelectedCoins {
|
||||||
|
totalValue: AmountString;
|
||||||
|
totalContribution: AmountString;
|
||||||
|
totalWithdrawalFee: AmountString;
|
||||||
|
totalDepositFee: AmountString;
|
||||||
|
coins: string[];
|
||||||
|
refresh?: RefreshChoice;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 getAvailableCoins(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
op: "credit" | "debit",
|
||||||
|
currency: string,
|
||||||
|
shouldCalculateWireFee: boolean,
|
||||||
|
shouldCalculatePurseFee: boolean,
|
||||||
|
exchangeFilter: string[] | undefined,
|
||||||
|
wireMethodFilter: string[] | undefined,
|
||||||
|
ageRestrictedFilter: number | undefined,
|
||||||
|
) {
|
||||||
|
return await ws.db
|
||||||
|
.mktx((x) => [
|
||||||
|
x.exchanges,
|
||||||
|
x.exchangeDetails,
|
||||||
|
x.denominations,
|
||||||
|
x.coinAvailability,
|
||||||
|
])
|
||||||
|
.runReadOnly(async (tx) => {
|
||||||
|
const denoms: AvailableDenom[] = [];
|
||||||
|
const wfPerExchange: Record<string, Record<string, AmountJson>> = {};
|
||||||
|
const pfPerExchange: Record<string, AmountJson> = {};
|
||||||
|
|
||||||
|
const databaseExchanges = await tx.exchanges.iter().toArray();
|
||||||
|
const exchanges =
|
||||||
|
exchangeFilter === undefined
|
||||||
|
? databaseExchanges.map((e) => e.baseUrl)
|
||||||
|
: exchangeFilter;
|
||||||
|
|
||||||
|
for (const exchangeBaseUrl of exchanges) {
|
||||||
|
const exchangeDetails = await getExchangeDetails(tx, exchangeBaseUrl);
|
||||||
|
// 1.- exchange has same currency
|
||||||
|
if (exchangeDetails?.currency !== currency) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const wireMethodFee: Record<string, AmountJson> = {};
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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) {
|
||||||
|
wireMethodFee[pp.targetType] = Amounts.parseOrThrow(wireFeeStr);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (Object.keys(wireMethodFee).length === 0) {
|
||||||
|
throw Error(
|
||||||
|
`exchange ${exchangeBaseUrl} doesn't have wire fee defined for this period`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wfPerExchange[exchangeBaseUrl] = wireMethodFee;
|
||||||
|
|
||||||
|
// 3.- exchange supports wire method
|
||||||
|
if (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;
|
||||||
|
}
|
||||||
|
|
||||||
|
//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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const ageLower = !ageRestrictedFilter ? 0 : ageRestrictedFilter;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
denoms.push({
|
||||||
|
...DenominationRecord.toDenomInfo(denom),
|
||||||
|
numAvailable: coinAvail.freshCoinCount ?? 0,
|
||||||
|
maxAge: coinAvail.maxAge,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
denoms,
|
||||||
|
wfPerExchange,
|
||||||
|
pfPerExchange,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
@ -58,6 +58,8 @@ import {
|
|||||||
GetContractTermsDetailsRequest,
|
GetContractTermsDetailsRequest,
|
||||||
GetExchangeTosRequest,
|
GetExchangeTosRequest,
|
||||||
GetExchangeTosResult,
|
GetExchangeTosResult,
|
||||||
|
GetPlanForOperationRequest,
|
||||||
|
GetPlanForOperationResponse,
|
||||||
GetWithdrawalDetailsForAmountRequest,
|
GetWithdrawalDetailsForAmountRequest,
|
||||||
GetWithdrawalDetailsForUriRequest,
|
GetWithdrawalDetailsForUriRequest,
|
||||||
InitRequest,
|
InitRequest,
|
||||||
@ -143,6 +145,7 @@ export enum WalletApiOperation {
|
|||||||
AcceptManualWithdrawal = "acceptManualWithdrawal",
|
AcceptManualWithdrawal = "acceptManualWithdrawal",
|
||||||
GetBalances = "getBalances",
|
GetBalances = "getBalances",
|
||||||
GetBalanceDetail = "getBalanceDetail",
|
GetBalanceDetail = "getBalanceDetail",
|
||||||
|
GetPlanForOperation = "getPlanForOperation",
|
||||||
GetUserAttentionRequests = "getUserAttentionRequests",
|
GetUserAttentionRequests = "getUserAttentionRequests",
|
||||||
GetUserAttentionUnreadCount = "getUserAttentionUnreadCount",
|
GetUserAttentionUnreadCount = "getUserAttentionUnreadCount",
|
||||||
MarkAttentionRequestAsRead = "markAttentionRequestAsRead",
|
MarkAttentionRequestAsRead = "markAttentionRequestAsRead",
|
||||||
@ -275,6 +278,12 @@ export type GetBalancesDetailOp = {
|
|||||||
response: MerchantPaymentBalanceDetails;
|
response: MerchantPaymentBalanceDetails;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type GetPlanForOperationOp = {
|
||||||
|
op: WalletApiOperation.GetPlanForOperation;
|
||||||
|
request: GetPlanForOperationRequest;
|
||||||
|
response: GetPlanForOperationResponse;
|
||||||
|
};
|
||||||
|
|
||||||
// group: Managing Transactions
|
// group: Managing Transactions
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -940,6 +949,7 @@ export type WalletOperations = {
|
|||||||
[WalletApiOperation.SuspendTransaction]: SuspendTransactionOp;
|
[WalletApiOperation.SuspendTransaction]: SuspendTransactionOp;
|
||||||
[WalletApiOperation.ResumeTransaction]: ResumeTransactionOp;
|
[WalletApiOperation.ResumeTransaction]: ResumeTransactionOp;
|
||||||
[WalletApiOperation.GetBalances]: GetBalancesOp;
|
[WalletApiOperation.GetBalances]: GetBalancesOp;
|
||||||
|
[WalletApiOperation.GetPlanForOperation]: GetPlanForOperationOp;
|
||||||
[WalletApiOperation.GetBalanceDetail]: GetBalancesDetailOp;
|
[WalletApiOperation.GetBalanceDetail]: GetBalancesDetailOp;
|
||||||
[WalletApiOperation.GetTransactions]: GetTransactionsOp;
|
[WalletApiOperation.GetTransactions]: GetTransactionsOp;
|
||||||
[WalletApiOperation.TestingGetSampleTransactions]: TestingGetSampleTransactionsOp;
|
[WalletApiOperation.TestingGetSampleTransactions]: TestingGetSampleTransactionsOp;
|
||||||
|
@ -75,6 +75,7 @@ import {
|
|||||||
codecForGetBalanceDetailRequest,
|
codecForGetBalanceDetailRequest,
|
||||||
codecForGetContractTermsDetails,
|
codecForGetContractTermsDetails,
|
||||||
codecForGetExchangeTosRequest,
|
codecForGetExchangeTosRequest,
|
||||||
|
codecForGetPlanForOperationRequest,
|
||||||
codecForGetWithdrawalDetailsForAmountRequest,
|
codecForGetWithdrawalDetailsForAmountRequest,
|
||||||
codecForGetWithdrawalDetailsForUri,
|
codecForGetWithdrawalDetailsForUri,
|
||||||
codecForImportDbRequest,
|
codecForImportDbRequest,
|
||||||
@ -218,9 +219,7 @@ import {
|
|||||||
processPeerPushDebit,
|
processPeerPushDebit,
|
||||||
} from "./operations/pay-peer-push-debit.js";
|
} from "./operations/pay-peer-push-debit.js";
|
||||||
import { getPendingOperations } from "./operations/pending.js";
|
import { getPendingOperations } from "./operations/pending.js";
|
||||||
import {
|
import { createRecoupGroup, processRecoupGroup } from "./operations/recoup.js";
|
||||||
createRecoupGroup, processRecoupGroup,
|
|
||||||
} from "./operations/recoup.js";
|
|
||||||
import {
|
import {
|
||||||
autoRefresh,
|
autoRefresh,
|
||||||
createRefreshGroup,
|
createRefreshGroup,
|
||||||
@ -283,6 +282,7 @@ import {
|
|||||||
WalletCoreApiClient,
|
WalletCoreApiClient,
|
||||||
WalletCoreResponseType,
|
WalletCoreResponseType,
|
||||||
} from "./wallet-api-types.js";
|
} from "./wallet-api-types.js";
|
||||||
|
import { getPlanForOperation } from "./util/coinSelection.js";
|
||||||
|
|
||||||
const logger = new Logger("wallet.ts");
|
const logger = new Logger("wallet.ts");
|
||||||
|
|
||||||
@ -331,9 +331,7 @@ async function callOperationHandler(
|
|||||||
/**
|
/**
|
||||||
* Process pending operations.
|
* Process pending operations.
|
||||||
*/
|
*/
|
||||||
export async function runPending(
|
export async function runPending(ws: InternalWalletState): Promise<void> {
|
||||||
ws: InternalWalletState,
|
|
||||||
): Promise<void> {
|
|
||||||
const pendingOpsResponse = await getPendingOperations(ws);
|
const pendingOpsResponse = await getPendingOperations(ws);
|
||||||
for (const p of pendingOpsResponse.pendingOperations) {
|
for (const p of pendingOpsResponse.pendingOperations) {
|
||||||
if (!AbsoluteTime.isExpired(p.timestampDue)) {
|
if (!AbsoluteTime.isExpired(p.timestampDue)) {
|
||||||
@ -1336,6 +1334,10 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>(
|
|||||||
await loadBackupRecovery(ws, req);
|
await loadBackupRecovery(ws, req);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
case WalletApiOperation.GetPlanForOperation: {
|
||||||
|
const req = codecForGetPlanForOperationRequest().decode(payload);
|
||||||
|
return await getPlanForOperation(ws, req);
|
||||||
|
}
|
||||||
case WalletApiOperation.GetBackupInfo: {
|
case WalletApiOperation.GetBackupInfo: {
|
||||||
const resp = await getBackupInfo(ws);
|
const resp = await getBackupInfo(ws);
|
||||||
return resp;
|
return resp;
|
||||||
|
Loading…
Reference in New Issue
Block a user