wallet-core: implement insufficient balance details
For now, only for merchant payments
This commit is contained in:
parent
44aaa7a636
commit
92f1b5928c
@ -419,6 +419,7 @@ export const codecForPreparePayResultInsufficientBalance =
|
||||
"status",
|
||||
codecForConstString(PreparePayResultType.InsufficientBalance),
|
||||
)
|
||||
.property("balanceDetails", codecForPayMerchantInsufficientBalanceDetails())
|
||||
.build("PreparePayResultInsufficientBalance");
|
||||
|
||||
export const codecForPreparePayResultAlreadyConfirmed =
|
||||
@ -483,6 +484,7 @@ export interface PreparePayResultInsufficientBalance {
|
||||
amountRaw: string;
|
||||
noncePriv: string;
|
||||
talerUri: string;
|
||||
balanceDetails: PayMerchantInsufficientBalanceDetails;
|
||||
}
|
||||
|
||||
export interface PreparePayResultAlreadyConfirmed {
|
||||
@ -2090,32 +2092,32 @@ export interface PayMerchantInsufficientBalanceDetails {
|
||||
/**
|
||||
* Amount requested by the merchant.
|
||||
*/
|
||||
amountRequested: AmountJson;
|
||||
amountRequested: AmountString;
|
||||
|
||||
/**
|
||||
* Balance of type "available" (see balance.ts for definition).
|
||||
*/
|
||||
balanceAvailable: AmountJson;
|
||||
balanceAvailable: AmountString;
|
||||
|
||||
/**
|
||||
* Balance of type "material" (see balance.ts for definition).
|
||||
*/
|
||||
balanceMaterial: AmountJson;
|
||||
balanceMaterial: AmountString;
|
||||
|
||||
/**
|
||||
* Balance of type "age-acceptable" (see balance.ts for definition).
|
||||
*/
|
||||
balanceAgeAcceptable: AmountJson;
|
||||
balanceAgeAcceptable: AmountString;
|
||||
|
||||
/**
|
||||
* Balance of type "merchant-acceptable" (see balance.ts for definition).
|
||||
*/
|
||||
balanceMechantAcceptable: AmountJson;
|
||||
balanceMerchantAcceptable: AmountString;
|
||||
|
||||
/**
|
||||
* Balance of type "merchant-depositable" (see balance.ts for definition).
|
||||
*/
|
||||
balanceMechantDepositable: AmountJson;
|
||||
balanceMerchantDepositable: AmountString;
|
||||
|
||||
/**
|
||||
* If the payment would succeed without fees
|
||||
@ -2126,5 +2128,17 @@ export interface PayMerchantInsufficientBalanceDetails {
|
||||
* It is not possible to give an exact value here, since it depends
|
||||
* on the coin selection for the amount that would be additionally withdrawn.
|
||||
*/
|
||||
feeGapEstimate: AmountJson;
|
||||
}
|
||||
feeGapEstimate: AmountString;
|
||||
}
|
||||
|
||||
const codecForPayMerchantInsufficientBalanceDetails =
|
||||
(): Codec<PayMerchantInsufficientBalanceDetails> =>
|
||||
buildCodecForObject<PayMerchantInsufficientBalanceDetails>()
|
||||
.property("amountRequested", codecForAmountString())
|
||||
.property("balanceAgeAcceptable", codecForAmountString())
|
||||
.property("balanceAvailable", codecForAmountString())
|
||||
.property("balanceMaterial", codecForAmountString())
|
||||
.property("balanceMerchantAcceptable", codecForAmountString())
|
||||
.property("balanceMerchantDepositable", codecForAmountString())
|
||||
.property("feeGapEstimate", codecForAmountString())
|
||||
.build("PayMerchantInsufficientBalanceDetails");
|
@ -835,6 +835,14 @@ export enum RefreshOperationStatus {
|
||||
FinishedWithError = 51 /* DORMANT_START + 1 */,
|
||||
}
|
||||
|
||||
/**
|
||||
* Group of refresh operations. The refreshed coins do not
|
||||
* have to belong to the same exchange, but must have the same
|
||||
* currency.
|
||||
*
|
||||
* FIXME: Should include the currency as a top-level field,
|
||||
* but we need to write a migration for that.
|
||||
*/
|
||||
export interface RefreshGroupRecord {
|
||||
operationStatus: RefreshOperationStatus;
|
||||
|
||||
@ -847,6 +855,13 @@ export interface RefreshGroupRecord {
|
||||
*/
|
||||
refreshGroupId: string;
|
||||
|
||||
/**
|
||||
* Currency of this refresh group.
|
||||
*
|
||||
* FIXME: Write a migration to add this to earlier DB versions.
|
||||
*/
|
||||
currency: string;
|
||||
|
||||
/**
|
||||
* Reason why this refresh group has been created.
|
||||
*/
|
||||
|
@ -86,6 +86,7 @@ export interface RefreshOperations {
|
||||
refreshGroups: typeof WalletStoresV1.refreshGroups;
|
||||
coinAvailability: typeof WalletStoresV1.coinAvailability;
|
||||
}>,
|
||||
currency: string,
|
||||
oldCoinPubs: CoinRefreshRequest[],
|
||||
reason: RefreshReason,
|
||||
): Promise<RefreshGroupId>;
|
||||
|
@ -778,6 +778,7 @@ export async function importBackup(
|
||||
timestampFinished: backupRefreshGroup.timestamp_finish,
|
||||
timestampCreated: backupRefreshGroup.timestamp_created,
|
||||
refreshGroupId: backupRefreshGroup.refresh_group_id,
|
||||
currency: Amounts.currencyOf(backupRefreshGroup.old_coins[0].input_amount),
|
||||
reason,
|
||||
lastErrorPerCoin: {},
|
||||
oldCoinPubs: backupRefreshGroup.old_coins.map((x) => x.coin_pub),
|
||||
|
@ -16,15 +16,15 @@
|
||||
|
||||
/**
|
||||
* Functions to compute the wallet's balance.
|
||||
*
|
||||
*
|
||||
* There are multiple definition of the wallet's balance.
|
||||
* We use the following terminology:
|
||||
*
|
||||
*
|
||||
* - "available": Balance that the wallet believes will certainly be available
|
||||
* for spending, modulo any failures of the exchange or double spending issues.
|
||||
* This includes available coins *not* allocated to any
|
||||
* spending/refresh/... operation. Pending withdrawals are *not* counted
|
||||
* towards this balance, because they are not certain to succeed.
|
||||
* towards this balance, because they are not certain to succeed.
|
||||
* Pending refreshes *are* counted towards this balance.
|
||||
* This balance type is nice to show to the user, because it does not
|
||||
* temporarily decrease after payment when we are waiting for refreshes
|
||||
@ -38,12 +38,11 @@
|
||||
*
|
||||
* - "merchant-acceptable": Subset of the material balance that can be spent with a particular
|
||||
* merchant (restricted via min age, exchange, auditor, wire_method).
|
||||
*
|
||||
*
|
||||
* - "merchant-depositable": Subset of the merchant-acceptable balance that the merchant
|
||||
* can accept via their supported wire methods.
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* Imports.
|
||||
*/
|
||||
@ -52,10 +51,16 @@ import {
|
||||
BalancesResponse,
|
||||
Amounts,
|
||||
Logger,
|
||||
AuditorHandle,
|
||||
ExchangeHandle,
|
||||
canonicalizeBaseUrl,
|
||||
parsePaytoUri,
|
||||
} from "@gnu-taler/taler-util";
|
||||
import { WalletStoresV1 } from "../db.js";
|
||||
import { AllowedAuditorInfo, AllowedExchangeInfo, RefreshGroupRecord, WalletStoresV1 } from "../db.js";
|
||||
import { GetReadOnlyAccess } from "../util/query.js";
|
||||
import { InternalWalletState } from "../internal-wallet-state.js";
|
||||
import { getExchangeDetails } from "./exchanges.js";
|
||||
import { checkLogicInvariant } from "../util/invariants.js";
|
||||
|
||||
/**
|
||||
* Logger.
|
||||
@ -68,6 +73,30 @@ interface WalletBalance {
|
||||
pendingOutgoing: AmountJson;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the available amount that the wallet expects to get
|
||||
* out of a refresh group.
|
||||
*/
|
||||
function computeRefreshGroupAvailableAmount(r: RefreshGroupRecord): AmountJson {
|
||||
// Don't count finished refreshes, since the refresh already resulted
|
||||
// in coins being added to the wallet.
|
||||
let available = Amounts.zeroOfCurrency(r.currency);
|
||||
if (r.timestampFinished) {
|
||||
return available;
|
||||
}
|
||||
for (let i = 0; i < r.oldCoinPubs.length; i++) {
|
||||
const session = r.refreshSessionPerCoin[i];
|
||||
if (session) {
|
||||
// We are always assuming the refresh will succeed, thus we
|
||||
// report the output as available balance.
|
||||
available = Amounts.add(available, session.amountRefreshOutput).amount;
|
||||
} else {
|
||||
available = Amounts.add(available, r.estimatedOutputPerCoin[i]).amount;
|
||||
}
|
||||
}
|
||||
return available;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get balance information.
|
||||
*/
|
||||
@ -110,33 +139,11 @@ export async function getBalancesInsideTransaction(
|
||||
});
|
||||
|
||||
await tx.refreshGroups.iter().forEach((r) => {
|
||||
// Don't count finished refreshes, since the refresh already resulted
|
||||
// in coins being added to the wallet.
|
||||
if (r.timestampFinished) {
|
||||
return;
|
||||
}
|
||||
for (let i = 0; i < r.oldCoinPubs.length; i++) {
|
||||
const session = r.refreshSessionPerCoin[i];
|
||||
if (session) {
|
||||
const currency = Amounts.parseOrThrow(
|
||||
session.amountRefreshOutput,
|
||||
).currency;
|
||||
const b = initBalance(currency);
|
||||
// We are always assuming the refresh will succeed, thus we
|
||||
// report the output as available balance.
|
||||
b.available = Amounts.add(
|
||||
b.available,
|
||||
session.amountRefreshOutput,
|
||||
).amount;
|
||||
} else {
|
||||
const currency = Amounts.parseOrThrow(r.inputPerCoin[i]).currency;
|
||||
const b = initBalance(currency);
|
||||
b.available = Amounts.add(
|
||||
b.available,
|
||||
r.estimatedOutputPerCoin[i],
|
||||
).amount;
|
||||
}
|
||||
}
|
||||
const b = initBalance(r.currency);
|
||||
b.available = Amounts.add(
|
||||
b.available,
|
||||
computeRefreshGroupAvailableAmount(r),
|
||||
).amount;
|
||||
});
|
||||
|
||||
await tx.withdrawalGroups.iter().forEach((wds) => {
|
||||
@ -194,3 +201,217 @@ export async function getBalances(
|
||||
|
||||
return wbal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Information about the balance for a particular payment to a particular
|
||||
* merchant.
|
||||
*/
|
||||
export interface MerchantPaymentBalanceDetails {
|
||||
balanceAvailable: AmountJson;
|
||||
}
|
||||
|
||||
export interface MerchantPaymentRestrictionsForBalance {
|
||||
currency: string;
|
||||
minAge: number;
|
||||
acceptedExchanges: AllowedExchangeInfo[];
|
||||
acceptedAuditors: AllowedAuditorInfo[];
|
||||
acceptedWireMethods: string[];
|
||||
}
|
||||
|
||||
export interface AcceptableExchanges {
|
||||
/**
|
||||
* Exchanges accepted by the merchant, but wire method might not match.
|
||||
*/
|
||||
acceptableExchanges: string[];
|
||||
|
||||
/**
|
||||
* Exchanges accepted by the merchant, including a matching
|
||||
* wire method, i.e. the merchant can deposit coins there.
|
||||
*/
|
||||
depositableExchanges: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all exchanges that are acceptable for a particular payment.
|
||||
*/
|
||||
export async function getAcceptableExchangeBaseUrls(
|
||||
ws: InternalWalletState,
|
||||
req: MerchantPaymentRestrictionsForBalance,
|
||||
): Promise<AcceptableExchanges> {
|
||||
const acceptableExchangeUrls = new Set<string>();
|
||||
const depositableExchangeUrls = new Set<string>();
|
||||
await ws.db
|
||||
.mktx((x) => [x.exchanges, x.exchangeDetails, x.auditorTrust])
|
||||
.runReadOnly(async (tx) => {
|
||||
// FIXME: We should have a DB index to look up all exchanges
|
||||
// for a particular auditor ...
|
||||
|
||||
const canonExchanges = new Set<string>();
|
||||
const canonAuditors = new Set<string>();
|
||||
|
||||
for (const exchangeHandle of req.acceptedExchanges) {
|
||||
const normUrl = canonicalizeBaseUrl(exchangeHandle.exchangeBaseUrl);
|
||||
canonExchanges.add(normUrl);
|
||||
}
|
||||
|
||||
for (const auditorHandle of req.acceptedAuditors) {
|
||||
const normUrl = canonicalizeBaseUrl(auditorHandle.auditorBaseUrl);
|
||||
canonAuditors.add(normUrl);
|
||||
}
|
||||
|
||||
await tx.exchanges.iter().forEachAsync(async (exchange) => {
|
||||
const dp = exchange.detailsPointer;
|
||||
if (!dp) {
|
||||
return;
|
||||
}
|
||||
const { currency, masterPublicKey } = dp;
|
||||
const exchangeDetails = await tx.exchangeDetails.indexes.byPointer.get([
|
||||
exchange.baseUrl,
|
||||
currency,
|
||||
masterPublicKey,
|
||||
]);
|
||||
if (!exchangeDetails) {
|
||||
return;
|
||||
}
|
||||
|
||||
let acceptable = false;
|
||||
|
||||
if (canonExchanges.has(exchange.baseUrl)) {
|
||||
acceptableExchangeUrls.add(exchange.baseUrl);
|
||||
acceptable = true;
|
||||
}
|
||||
for (const exchangeAuditor of exchangeDetails.auditors) {
|
||||
if (canonAuditors.has(exchangeAuditor.auditor_url)) {
|
||||
acceptableExchangeUrls.add(exchange.baseUrl);
|
||||
acceptable = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!acceptable) {
|
||||
return;
|
||||
}
|
||||
// FIXME: Also consider exchange and auditor public key
|
||||
// instead of just base URLs?
|
||||
|
||||
let wireMethodSupported = false;
|
||||
for (const acc of exchangeDetails.wireInfo.accounts) {
|
||||
const pp = parsePaytoUri(acc.payto_uri);
|
||||
checkLogicInvariant(!!pp);
|
||||
for (const wm of req.acceptedWireMethods) {
|
||||
if (pp.targetType === wm) {
|
||||
wireMethodSupported = true;
|
||||
break;
|
||||
}
|
||||
if (wireMethodSupported) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
acceptableExchangeUrls.add(exchange.baseUrl);
|
||||
if (wireMethodSupported) {
|
||||
depositableExchangeUrls.add(exchange.baseUrl);
|
||||
}
|
||||
});
|
||||
});
|
||||
return {
|
||||
acceptableExchanges: [...acceptableExchangeUrls],
|
||||
depositableExchanges: [...depositableExchangeUrls],
|
||||
};
|
||||
}
|
||||
|
||||
export interface MerchantPaymentBalanceDetails {
|
||||
/**
|
||||
* Balance of type "available" (see balance.ts for definition).
|
||||
*/
|
||||
balanceAvailable: AmountJson;
|
||||
|
||||
/**
|
||||
* Balance of type "material" (see balance.ts for definition).
|
||||
*/
|
||||
balanceMaterial: AmountJson;
|
||||
|
||||
/**
|
||||
* Balance of type "age-acceptable" (see balance.ts for definition).
|
||||
*/
|
||||
balanceAgeAcceptable: AmountJson;
|
||||
|
||||
/**
|
||||
* Balance of type "merchant-acceptable" (see balance.ts for definition).
|
||||
*/
|
||||
balanceMerchantAcceptable: AmountJson;
|
||||
|
||||
/**
|
||||
* Balance of type "merchant-depositable" (see balance.ts for definition).
|
||||
*/
|
||||
balanceMerchantDepositable: AmountJson;
|
||||
}
|
||||
|
||||
export async function getMerchantPaymentBalanceDetails(
|
||||
ws: InternalWalletState,
|
||||
req: MerchantPaymentRestrictionsForBalance,
|
||||
): Promise<MerchantPaymentBalanceDetails> {
|
||||
const acceptability = await getAcceptableExchangeBaseUrls(ws, req);
|
||||
|
||||
const d: MerchantPaymentBalanceDetails = {
|
||||
balanceAvailable: Amounts.zeroOfCurrency(req.currency),
|
||||
balanceMaterial: Amounts.zeroOfCurrency(req.currency),
|
||||
balanceAgeAcceptable: Amounts.zeroOfCurrency(req.currency),
|
||||
balanceMerchantAcceptable: Amounts.zeroOfCurrency(req.currency),
|
||||
balanceMerchantDepositable: Amounts.zeroOfCurrency(req.currency),
|
||||
};
|
||||
|
||||
const wbal = await ws.db
|
||||
.mktx((x) => [
|
||||
x.coins,
|
||||
x.coinAvailability,
|
||||
x.refreshGroups,
|
||||
x.purchases,
|
||||
x.withdrawalGroups,
|
||||
])
|
||||
.runReadOnly(async (tx) => {
|
||||
await tx.coinAvailability.iter().forEach((ca) => {
|
||||
const singleCoinAmount: AmountJson = {
|
||||
currency: ca.currency,
|
||||
fraction: ca.amountFrac,
|
||||
value: ca.amountVal,
|
||||
};
|
||||
const coinAmount: AmountJson = Amounts.mult(
|
||||
singleCoinAmount,
|
||||
ca.freshCoinCount,
|
||||
).amount;
|
||||
d.balanceAvailable = Amounts.add(d.balanceAvailable, coinAmount).amount;
|
||||
d.balanceMaterial = Amounts.add(d.balanceMaterial, coinAmount).amount;
|
||||
if (ca.maxAge === 0 || ca.maxAge > req.minAge) {
|
||||
d.balanceAgeAcceptable = Amounts.add(
|
||||
d.balanceAgeAcceptable,
|
||||
coinAmount,
|
||||
).amount;
|
||||
if (acceptability.acceptableExchanges.includes(ca.exchangeBaseUrl)) {
|
||||
d.balanceMerchantAcceptable = Amounts.add(
|
||||
d.balanceMerchantAcceptable,
|
||||
coinAmount,
|
||||
).amount;
|
||||
if (
|
||||
acceptability.depositableExchanges.includes(ca.exchangeBaseUrl)
|
||||
) {
|
||||
d.balanceMerchantDepositable = Amounts.add(
|
||||
d.balanceMerchantDepositable,
|
||||
coinAmount,
|
||||
).amount;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await tx.refreshGroups.iter().forEach((r) => {
|
||||
d.balanceAvailable = Amounts.add(
|
||||
d.balanceAvailable,
|
||||
computeRefreshGroupAvailableAmount(r),
|
||||
).amount;
|
||||
});
|
||||
});
|
||||
|
||||
return d;
|
||||
}
|
||||
|
@ -175,9 +175,11 @@ export async function spendCoins(
|
||||
await tx.coins.put(coin);
|
||||
await tx.coinAvailability.put(coinAvailability);
|
||||
}
|
||||
|
||||
await ws.refreshOps.createRefreshGroup(
|
||||
ws,
|
||||
tx,
|
||||
Amounts.currencyOf(csi.contributions[0]),
|
||||
refreshCoinPubs,
|
||||
RefreshReason.PayMerchant,
|
||||
);
|
||||
|
@ -268,7 +268,7 @@ export async function getFeeForDeposit(
|
||||
prevPayCoins: [],
|
||||
});
|
||||
|
||||
if (!payCoinSel) {
|
||||
if (payCoinSel.type !== "success") {
|
||||
throw Error("insufficient funds");
|
||||
}
|
||||
|
||||
@ -276,7 +276,7 @@ export async function getFeeForDeposit(
|
||||
ws,
|
||||
p.targetType,
|
||||
amount,
|
||||
payCoinSel,
|
||||
payCoinSel.coinSel,
|
||||
);
|
||||
}
|
||||
|
||||
@ -355,16 +355,16 @@ export async function prepareDepositGroup(
|
||||
prevPayCoins: [],
|
||||
});
|
||||
|
||||
if (!payCoinSel) {
|
||||
if (payCoinSel.type !== "success") {
|
||||
throw Error("insufficient funds");
|
||||
}
|
||||
|
||||
const totalDepositCost = await getTotalPaymentCost(ws, payCoinSel);
|
||||
const totalDepositCost = await getTotalPaymentCost(ws, payCoinSel.coinSel);
|
||||
|
||||
const effectiveDepositAmount = await getEffectiveDepositAmount(
|
||||
ws,
|
||||
p.targetType,
|
||||
payCoinSel,
|
||||
payCoinSel.coinSel,
|
||||
);
|
||||
|
||||
return {
|
||||
@ -452,18 +452,18 @@ export async function createDepositGroup(
|
||||
prevPayCoins: [],
|
||||
});
|
||||
|
||||
if (!payCoinSel) {
|
||||
if (payCoinSel.type !== "success") {
|
||||
throw Error("insufficient funds");
|
||||
}
|
||||
|
||||
const totalDepositCost = await getTotalPaymentCost(ws, payCoinSel);
|
||||
const totalDepositCost = await getTotalPaymentCost(ws, payCoinSel.coinSel);
|
||||
|
||||
const depositGroupId = encodeCrock(getRandomBytes(32));
|
||||
|
||||
const effectiveDepositAmount = await getEffectiveDepositAmount(
|
||||
ws,
|
||||
p.targetType,
|
||||
payCoinSel,
|
||||
payCoinSel.coinSel,
|
||||
);
|
||||
|
||||
const depositGroup: DepositGroupRecord = {
|
||||
@ -474,9 +474,9 @@ export async function createDepositGroup(
|
||||
noncePub: noncePair.pub,
|
||||
timestampCreated: AbsoluteTime.toTimestamp(now),
|
||||
timestampFinished: undefined,
|
||||
payCoinSelection: payCoinSel,
|
||||
payCoinSelection: payCoinSel.coinSel,
|
||||
payCoinSelectionUid: encodeCrock(getRandomBytes(32)),
|
||||
depositedPerCoin: payCoinSel.coinPubs.map(() => false),
|
||||
depositedPerCoin: payCoinSel.coinSel.coinPubs.map(() => false),
|
||||
merchantPriv: merchantPair.priv,
|
||||
merchantPub: merchantPair.pub,
|
||||
totalPayCost: Amounts.stringify(totalDepositCost),
|
||||
@ -500,8 +500,8 @@ export async function createDepositGroup(
|
||||
.runReadWrite(async (tx) => {
|
||||
await spendCoins(ws, tx, {
|
||||
allocationId: `txn:deposit:${depositGroup.depositGroupId}`,
|
||||
coinPubs: payCoinSel.coinPubs,
|
||||
contributions: payCoinSel.coinContributions.map((x) =>
|
||||
coinPubs: payCoinSel.coinSel.coinPubs,
|
||||
contributions: payCoinSel.coinSel.coinContributions.map((x) =>
|
||||
Amounts.parseOrThrow(x),
|
||||
),
|
||||
refreshReason: RefreshReason.PayDeposit,
|
||||
|
@ -73,6 +73,7 @@ import {
|
||||
TransactionType,
|
||||
URL,
|
||||
constructPayUri,
|
||||
PayMerchantInsufficientBalanceDetails,
|
||||
} from "@gnu-taler/taler-util";
|
||||
import { EddsaKeypair } from "../crypto/cryptoImplementation.js";
|
||||
import {
|
||||
@ -131,11 +132,12 @@ import {
|
||||
import { getExchangeDetails } from "./exchanges.js";
|
||||
import { createRefreshGroup, getTotalRefreshCost } from "./refresh.js";
|
||||
import { GetReadOnlyAccess } from "../util/query.js";
|
||||
import { getMerchantPaymentBalanceDetails } from "./balance.js";
|
||||
|
||||
/**
|
||||
* Logger.
|
||||
*/
|
||||
const logger = new Logger("pay.ts");
|
||||
const logger = new Logger("pay-merchant.ts");
|
||||
|
||||
/**
|
||||
* Compute the total cost of a payment to the customer.
|
||||
@ -817,7 +819,7 @@ async function handleInsufficientFunds(
|
||||
requiredMinimumAge: contractData.minimumAge,
|
||||
});
|
||||
|
||||
if (!res) {
|
||||
if (res.type !== "success") {
|
||||
logger.trace("insufficient funds for coin re-selection");
|
||||
return;
|
||||
}
|
||||
@ -841,8 +843,7 @@ async function handleInsufficientFunds(
|
||||
if (!payInfo) {
|
||||
return;
|
||||
}
|
||||
payInfo.payCoinSelection = res;
|
||||
payInfo.payCoinSelection = res;
|
||||
payInfo.payCoinSelection = res.coinSel;
|
||||
payInfo.payCoinSelectionUid = encodeCrock(getRandomBytes(32));
|
||||
await tx.purchases.put(p);
|
||||
await spendCoins(ws, tx, {
|
||||
@ -905,6 +906,8 @@ export async function selectCandidates(
|
||||
x.coinAvailability,
|
||||
])
|
||||
.runReadOnly(async (tx) => {
|
||||
// FIXME: Use the existing helper (from balance.ts) to
|
||||
// get acceptable exchanges.
|
||||
const denoms: AvailableDenom[] = [];
|
||||
const exchanges = await tx.exchanges.iter().toArray();
|
||||
const wfPerExchange: Record<string, AmountJson> = {};
|
||||
@ -1030,6 +1033,7 @@ export function selectGreedy(
|
||||
// Don't use this coin if depositing it is more expensive than
|
||||
// the amount it would give the merchant.
|
||||
if (Amounts.cmp(aci.feeDeposit, aci.value) > 0) {
|
||||
tally.lastDepositFee = Amounts.parseOrThrow(aci.feeDeposit);
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -1129,6 +1133,13 @@ export function selectForced(
|
||||
return selectedDenom;
|
||||
}
|
||||
|
||||
export type SelectPayCoinsResult =
|
||||
| {
|
||||
type: "failure";
|
||||
insufficientBalanceDetails: PayMerchantInsufficientBalanceDetails;
|
||||
}
|
||||
| { type: "success"; coinSel: PayCoinSelection };
|
||||
|
||||
/**
|
||||
* Given a list of candidate coins, select coins to spend under the merchant's
|
||||
* constraints.
|
||||
@ -1142,7 +1153,7 @@ export function selectForced(
|
||||
export async function selectPayCoinsNew(
|
||||
ws: InternalWalletState,
|
||||
req: SelectPayCoinRequestNg,
|
||||
): Promise<PayCoinSelection | undefined> {
|
||||
): Promise<SelectPayCoinsResult> {
|
||||
const {
|
||||
contractTermsAmount,
|
||||
depositFeeLimit,
|
||||
@ -1168,6 +1179,7 @@ export async function selectPayCoinsNew(
|
||||
customerDepositFees: Amounts.zeroOfCurrency(currency),
|
||||
customerWireFees: Amounts.zeroOfCurrency(currency),
|
||||
wireFeeCoveredForExchange: new Set(),
|
||||
lastDepositFee: Amounts.zeroOfCurrency(currency),
|
||||
};
|
||||
|
||||
const prevPayCoins = req.prevPayCoins ?? [];
|
||||
@ -1207,7 +1219,44 @@ export async function selectPayCoinsNew(
|
||||
}
|
||||
|
||||
if (!selectedDenom) {
|
||||
return undefined;
|
||||
const details = await getMerchantPaymentBalanceDetails(ws, {
|
||||
acceptedAuditors: req.auditors,
|
||||
acceptedExchanges: req.exchanges,
|
||||
acceptedWireMethods: [req.wireMethod],
|
||||
currency: Amounts.currencyOf(req.contractTermsAmount),
|
||||
minAge: req.requiredMinimumAge ?? 0,
|
||||
});
|
||||
let feeGapEstimate: AmountJson;
|
||||
if (
|
||||
Amounts.cmp(
|
||||
details.balanceMerchantDepositable,
|
||||
req.contractTermsAmount,
|
||||
) >= 0
|
||||
) {
|
||||
// FIXME: We can probably give a better estimate.
|
||||
feeGapEstimate = Amounts.add(
|
||||
tally.amountPayRemaining,
|
||||
tally.lastDepositFee,
|
||||
).amount;
|
||||
} else {
|
||||
feeGapEstimate = Amounts.zeroOfAmount(req.contractTermsAmount);
|
||||
}
|
||||
return {
|
||||
type: "failure",
|
||||
insufficientBalanceDetails: {
|
||||
amountRequested: Amounts.stringify(req.contractTermsAmount),
|
||||
balanceAgeAcceptable: Amounts.stringify(details.balanceAgeAcceptable),
|
||||
balanceAvailable: Amounts.stringify(details.balanceAvailable),
|
||||
balanceMaterial: Amounts.stringify(details.balanceMaterial),
|
||||
balanceMerchantAcceptable: Amounts.stringify(
|
||||
details.balanceMerchantAcceptable,
|
||||
),
|
||||
balanceMerchantDepositable: Amounts.stringify(
|
||||
details.balanceMerchantDepositable,
|
||||
),
|
||||
feeGapEstimate: Amounts.stringify(feeGapEstimate),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const finalSel = selectedDenom;
|
||||
@ -1244,11 +1293,14 @@ export async function selectPayCoinsNew(
|
||||
});
|
||||
|
||||
return {
|
||||
paymentAmount: Amounts.stringify(contractTermsAmount),
|
||||
coinContributions: coinContributions.map((x) => Amounts.stringify(x)),
|
||||
coinPubs,
|
||||
customerDepositFees: Amounts.stringify(tally.customerDepositFees),
|
||||
customerWireFees: Amounts.stringify(tally.customerWireFees),
|
||||
type: "success",
|
||||
coinSel: {
|
||||
paymentAmount: Amounts.stringify(contractTermsAmount),
|
||||
coinContributions: coinContributions.map((x) => Amounts.stringify(x)),
|
||||
coinPubs,
|
||||
customerDepositFees: Amounts.stringify(tally.customerDepositFees),
|
||||
customerWireFees: Amounts.stringify(tally.customerWireFees),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@ -1318,7 +1370,7 @@ export async function checkPaymentByProposalId(
|
||||
wireMethod: contractData.wireMethod,
|
||||
});
|
||||
|
||||
if (!res) {
|
||||
if (res.type !== "success") {
|
||||
logger.info("not allowing payment, insufficient coins");
|
||||
return {
|
||||
status: PreparePayResultType.InsufficientBalance,
|
||||
@ -1327,10 +1379,11 @@ export async function checkPaymentByProposalId(
|
||||
noncePriv: proposal.noncePriv,
|
||||
amountRaw: Amounts.stringify(d.contractData.amount),
|
||||
talerUri,
|
||||
balanceDetails: res.insufficientBalanceDetails,
|
||||
};
|
||||
}
|
||||
|
||||
const totalCost = await getTotalPaymentCost(ws, res);
|
||||
const totalCost = await getTotalPaymentCost(ws, res.coinSel);
|
||||
logger.trace("costInfo", totalCost);
|
||||
logger.trace("coinsForPayment", res);
|
||||
|
||||
@ -1340,7 +1393,7 @@ export async function checkPaymentByProposalId(
|
||||
proposalId: proposal.proposalId,
|
||||
noncePriv: proposal.noncePriv,
|
||||
amountEffective: Amounts.stringify(totalCost),
|
||||
amountRaw: Amounts.stringify(res.paymentAmount),
|
||||
amountRaw: Amounts.stringify(res.coinSel.paymentAmount),
|
||||
contractTermsHash: d.contractData.contractTermsHash,
|
||||
talerUri,
|
||||
};
|
||||
@ -1666,9 +1719,9 @@ export async function confirmPay(
|
||||
|
||||
const contractData = d.contractData;
|
||||
|
||||
let maybeCoinSelection: PayCoinSelection | undefined = undefined;
|
||||
let selectCoinsResult: SelectPayCoinsResult | undefined = undefined;
|
||||
|
||||
maybeCoinSelection = await selectPayCoinsNew(ws, {
|
||||
selectCoinsResult = await selectPayCoinsNew(ws, {
|
||||
auditors: contractData.allowedAuditors,
|
||||
exchanges: contractData.allowedExchanges,
|
||||
wireMethod: contractData.wireMethod,
|
||||
@ -1681,9 +1734,9 @@ export async function confirmPay(
|
||||
forcedSelection: forcedCoinSel,
|
||||
});
|
||||
|
||||
logger.trace("coin selection result", maybeCoinSelection);
|
||||
logger.trace("coin selection result", selectCoinsResult);
|
||||
|
||||
if (!maybeCoinSelection) {
|
||||
if (selectCoinsResult.type === "failure") {
|
||||
// Should not happen, since checkPay should be called first
|
||||
// FIXME: Actually, this should be handled gracefully,
|
||||
// and the status should be stored in the DB.
|
||||
@ -1691,14 +1744,7 @@ export async function confirmPay(
|
||||
throw Error("insufficient balance");
|
||||
}
|
||||
|
||||
const coinSelection = maybeCoinSelection;
|
||||
|
||||
const depositPermissions = await generateDepositPermissions(
|
||||
ws,
|
||||
coinSelection,
|
||||
d.contractData,
|
||||
);
|
||||
|
||||
const coinSelection = selectCoinsResult.coinSel;
|
||||
const payCostInfo = await getTotalPaymentCost(ws, coinSelection);
|
||||
|
||||
let sessionId: string | undefined;
|
||||
@ -2373,6 +2419,7 @@ async function acceptRefunds(
|
||||
await createRefreshGroup(
|
||||
ws,
|
||||
tx,
|
||||
Amounts.currencyOf(refreshCoinsPubs[0].amount),
|
||||
refreshCoinsPubs,
|
||||
RefreshReason.Refund,
|
||||
);
|
||||
|
@ -429,6 +429,7 @@ export async function processRecoupGroupHandler(
|
||||
const refreshGroupId = await createRefreshGroup(
|
||||
ws,
|
||||
tx,
|
||||
Amounts.currencyOf(rg2.scheduleRefreshCoins[0].amount),
|
||||
rg2.scheduleRefreshCoins,
|
||||
RefreshReason.Recoup,
|
||||
);
|
||||
|
@ -850,6 +850,7 @@ export async function createRefreshGroup(
|
||||
refreshGroups: typeof WalletStoresV1.refreshGroups;
|
||||
coinAvailability: typeof WalletStoresV1.coinAvailability;
|
||||
}>,
|
||||
currency: string,
|
||||
oldCoinPubs: CoinRefreshRequest[],
|
||||
reason: RefreshReason,
|
||||
): Promise<RefreshGroupId> {
|
||||
@ -934,6 +935,7 @@ export async function createRefreshGroup(
|
||||
|
||||
const refreshGroup: RefreshGroupRecord = {
|
||||
operationStatus: RefreshOperationStatus.Pending,
|
||||
currency,
|
||||
timestampFinished: undefined,
|
||||
statusPerCoin: oldCoinPubs.map(() => RefreshCoinStatus.Pending),
|
||||
oldCoinPubs: oldCoinPubs.map((x) => x.coinPub),
|
||||
@ -1018,7 +1020,7 @@ export async function autoRefresh(
|
||||
])
|
||||
.runReadWrite(async (tx) => {
|
||||
const exchange = await tx.exchanges.get(exchangeBaseUrl);
|
||||
if (!exchange) {
|
||||
if (!exchange || !exchange.detailsPointer) {
|
||||
return;
|
||||
}
|
||||
const coins = await tx.coins.indexes.byBaseUrl
|
||||
@ -1059,6 +1061,7 @@ export async function autoRefresh(
|
||||
const res = await createRefreshGroup(
|
||||
ws,
|
||||
tx,
|
||||
exchange.detailsPointer?.currency,
|
||||
refreshCoins,
|
||||
RefreshReason.Scheduled,
|
||||
);
|
||||
|
@ -117,6 +117,8 @@ export interface CoinSelectionTally {
|
||||
customerWireFees: AmountJson;
|
||||
|
||||
wireFeeCoveredForExchange: Set<string>;
|
||||
|
||||
lastDepositFee: AmountJson;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -188,5 +190,6 @@ export function tallyFees(
|
||||
customerDepositFees,
|
||||
customerWireFees,
|
||||
wireFeeCoveredForExchange,
|
||||
lastDepositFee: feeDeposit,
|
||||
};
|
||||
}
|
||||
|
@ -1178,6 +1178,9 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>(
|
||||
}
|
||||
case WalletApiOperation.ForceRefresh: {
|
||||
const req = codecForForceRefreshRequest().decode(payload);
|
||||
if (req.coinPubList.length == 0) {
|
||||
throw Error("refusing to create empty refresh group");
|
||||
}
|
||||
const refreshGroupId = await ws.db
|
||||
.mktx((x) => [
|
||||
x.refreshGroups,
|
||||
@ -1207,6 +1210,7 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>(
|
||||
return await createRefreshGroup(
|
||||
ws,
|
||||
tx,
|
||||
Amounts.currencyOf(coinPubs[0].amount),
|
||||
coinPubs,
|
||||
RefreshReason.Manual,
|
||||
);
|
||||
|
Loading…
Reference in New Issue
Block a user