wallet-core: insufficient balance details for p2p payments
This commit is contained in:
parent
c2c35925bb
commit
417c07f3f4
@ -67,7 +67,6 @@ export async function runPeerToPeerPushTest(t: GlobalTestState) {
|
||||
);
|
||||
|
||||
console.log(resp);
|
||||
|
||||
}
|
||||
const resp = await wallet1.client.call(
|
||||
WalletApiOperation.InitiatePeerPushPayment,
|
||||
@ -114,6 +113,21 @@ export async function runPeerToPeerPushTest(t: GlobalTestState) {
|
||||
|
||||
console.log(`txn1: ${j2s(txn1)}`);
|
||||
console.log(`txn2: ${j2s(txn2)}`);
|
||||
|
||||
const ex1 = await t.assertThrowsTalerErrorAsync(async () => {
|
||||
await wallet1.client.call(
|
||||
WalletApiOperation.InitiatePeerPushPayment,
|
||||
{
|
||||
partialContractTerms: {
|
||||
summary: "(this will fail)",
|
||||
amount: "TESTKUDOS:15",
|
||||
purse_expiration
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
console.log("got expected exception detail", j2s(ex1.errorDetail));
|
||||
}
|
||||
|
||||
runPeerToPeerPushTest.suites = ["wallet"];
|
||||
|
@ -3248,6 +3248,14 @@ export enum TalerErrorCode {
|
||||
WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE = 7027,
|
||||
|
||||
|
||||
/**
|
||||
* The wallet does not have sufficient balance to pay for an invoice.
|
||||
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
|
||||
* (A value of 0 indicates that the error is generated client-side).
|
||||
*/
|
||||
WALLET_PEER_PULL_PAYMENT_INSUFFICIENT_BALANCE = 7028,
|
||||
|
||||
|
||||
/**
|
||||
* We encountered a timeout with our payment backend.
|
||||
* Returned with an HTTP status code of #MHD_HTTP_GATEWAY_TIMEOUT (504).
|
||||
|
@ -2164,20 +2164,11 @@ export interface PayPeerInsufficientBalanceDetails {
|
||||
*/
|
||||
balanceMaterial: AmountString;
|
||||
|
||||
/**
|
||||
* Acceptable balance based on restrictions on which
|
||||
* exchange can be used.
|
||||
*/
|
||||
balanceExchangeAcceptable: AmountString
|
||||
|
||||
/**
|
||||
* If the payment would succeed without fees
|
||||
* (i.e. balanceExchangeAcceptable >= amountRequested),
|
||||
* this field contains an estimate of the amount that would additionally
|
||||
* be required to cover the fees.
|
||||
*
|
||||
* 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: AmountString;
|
||||
perExchange: {
|
||||
[url: string]: {
|
||||
balanceAvailable: AmountString;
|
||||
balanceMaterial: AmountString;
|
||||
feeGapEstimate: AmountString;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
@ -25,6 +25,7 @@
|
||||
*/
|
||||
import {
|
||||
PayMerchantInsufficientBalanceDetails,
|
||||
PayPeerInsufficientBalanceDetails,
|
||||
TalerErrorCode,
|
||||
TalerErrorDetail,
|
||||
TransactionType,
|
||||
@ -87,6 +88,9 @@ export interface DetailsMap {
|
||||
[TalerErrorCode.WALLET_DEPOSIT_GROUP_INSUFFICIENT_BALANCE]: {
|
||||
insufficientBalanceDetails: PayMerchantInsufficientBalanceDetails;
|
||||
};
|
||||
[TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE]: {
|
||||
insufficientBalanceDetails: PayPeerInsufficientBalanceDetails;
|
||||
};
|
||||
}
|
||||
|
||||
type ErrBody<Y> = Y extends keyof DetailsMap ? DetailsMap[Y] : never;
|
||||
|
@ -56,7 +56,12 @@ import {
|
||||
canonicalizeBaseUrl,
|
||||
parsePaytoUri,
|
||||
} from "@gnu-taler/taler-util";
|
||||
import { AllowedAuditorInfo, AllowedExchangeInfo, RefreshGroupRecord, 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";
|
||||
@ -362,7 +367,7 @@ export async function getMerchantPaymentBalanceDetails(
|
||||
balanceMerchantDepositable: Amounts.zeroOfCurrency(req.currency),
|
||||
};
|
||||
|
||||
const wbal = await ws.db
|
||||
await ws.db
|
||||
.mktx((x) => [
|
||||
x.coins,
|
||||
x.coinAvailability,
|
||||
@ -415,3 +420,67 @@ export async function getMerchantPaymentBalanceDetails(
|
||||
|
||||
return d;
|
||||
}
|
||||
|
||||
export interface PeerPaymentRestrictionsForBalance {
|
||||
currency: string;
|
||||
restrictExchangeTo?: string;
|
||||
}
|
||||
|
||||
export interface PeerPaymentBalanceDetails {
|
||||
/**
|
||||
* Balance of type "available" (see balance.ts for definition).
|
||||
*/
|
||||
balanceAvailable: AmountJson;
|
||||
|
||||
/**
|
||||
* Balance of type "material" (see balance.ts for definition).
|
||||
*/
|
||||
balanceMaterial: AmountJson;
|
||||
}
|
||||
|
||||
export async function getPeerPaymentBalanceDetailsInTx(
|
||||
ws: InternalWalletState,
|
||||
tx: GetReadOnlyAccess<{
|
||||
coinAvailability: typeof WalletStoresV1.coinAvailability;
|
||||
refreshGroups: typeof WalletStoresV1.refreshGroups;
|
||||
}>,
|
||||
req: PeerPaymentRestrictionsForBalance,
|
||||
): Promise<PeerPaymentBalanceDetails> {
|
||||
let balanceAvailable = Amounts.zeroOfCurrency(req.currency);
|
||||
let balanceMaterial = Amounts.zeroOfCurrency(req.currency);
|
||||
|
||||
await tx.coinAvailability.iter().forEach((ca) => {
|
||||
if (ca.currency != req.currency) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
req.restrictExchangeTo &&
|
||||
req.restrictExchangeTo !== ca.exchangeBaseUrl
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const singleCoinAmount: AmountJson = {
|
||||
currency: ca.currency,
|
||||
fraction: ca.amountFrac,
|
||||
value: ca.amountVal,
|
||||
};
|
||||
const coinAmount: AmountJson = Amounts.mult(
|
||||
singleCoinAmount,
|
||||
ca.freshCoinCount,
|
||||
).amount;
|
||||
balanceAvailable = Amounts.add(balanceAvailable, coinAmount).amount;
|
||||
balanceMaterial = Amounts.add(balanceMaterial, coinAmount).amount;
|
||||
});
|
||||
|
||||
await tx.refreshGroups.iter().forEach((r) => {
|
||||
balanceAvailable = Amounts.add(
|
||||
balanceAvailable,
|
||||
computeRefreshGroupAvailableAmount(r),
|
||||
).amount;
|
||||
});
|
||||
|
||||
return {
|
||||
balanceAvailable,
|
||||
balanceMaterial,
|
||||
};
|
||||
}
|
||||
|
@ -62,6 +62,7 @@ import {
|
||||
PreparePeerPushPaymentResponse,
|
||||
RefreshReason,
|
||||
strcmp,
|
||||
TalerErrorCode,
|
||||
TalerProtocolTimestamp,
|
||||
TransactionType,
|
||||
UnblindedSignature,
|
||||
@ -77,11 +78,13 @@ import {
|
||||
WithdrawalGroupStatus,
|
||||
WithdrawalRecordType,
|
||||
} from "../db.js";
|
||||
import { TalerError } from "../errors.js";
|
||||
import { InternalWalletState } from "../internal-wallet-state.js";
|
||||
import { makeTransactionId, spendCoins } from "../operations/common.js";
|
||||
import { readSuccessResponseJsonOrThrow } from "../util/http.js";
|
||||
import { checkDbInvariant } from "../util/invariants.js";
|
||||
import { GetReadOnlyAccess } from "../util/query.js";
|
||||
import { getPeerPaymentBalanceDetailsInTx } from "./balance.js";
|
||||
import { updateExchangeFromUrl } from "./exchanges.js";
|
||||
import { internalCreateWithdrawalGroup } from "./withdraw.js";
|
||||
|
||||
@ -135,6 +138,7 @@ export type SelectPeerCoinsResult =
|
||||
| { type: "success"; result: PeerCoinSelection }
|
||||
| {
|
||||
type: "failure";
|
||||
insufficientBalanceDetails: PayPeerInsufficientBalanceDetails;
|
||||
};
|
||||
|
||||
export async function selectPeerCoins(
|
||||
@ -143,12 +147,16 @@ export async function selectPeerCoins(
|
||||
exchanges: typeof WalletStoresV1.exchanges;
|
||||
denominations: typeof WalletStoresV1.denominations;
|
||||
coins: typeof WalletStoresV1.coins;
|
||||
coinAvailability: typeof WalletStoresV1.coinAvailability;
|
||||
refreshGroups: typeof WalletStoresV1.refreshGroups;
|
||||
}>,
|
||||
instructedAmount: AmountJson,
|
||||
): Promise<SelectPeerCoinsResult> {
|
||||
const exchanges = await tx.exchanges.iter().toArray();
|
||||
const exchangeFeeGap: { [url: string]: AmountJson } = {};
|
||||
const currency = Amounts.currencyOf(instructedAmount);
|
||||
for (const exch of exchanges) {
|
||||
if (exch.detailsPointer?.currency !== instructedAmount.currency) {
|
||||
if (exch.detailsPointer?.currency !== currency) {
|
||||
continue;
|
||||
}
|
||||
const coins = (
|
||||
@ -184,8 +192,8 @@ export async function selectPeerCoins(
|
||||
-Amounts.cmp(o1.value, o2.value) ||
|
||||
strcmp(o1.denomPubHash, o2.denomPubHash),
|
||||
);
|
||||
let amountAcc = Amounts.zeroOfCurrency(instructedAmount.currency);
|
||||
let depositFeesAcc = Amounts.zeroOfCurrency(instructedAmount.currency);
|
||||
let amountAcc = Amounts.zeroOfCurrency(currency);
|
||||
let depositFeesAcc = Amounts.zeroOfCurrency(currency);
|
||||
const resCoins: {
|
||||
coinPub: string;
|
||||
coinPriv: string;
|
||||
@ -194,6 +202,7 @@ export async function selectPeerCoins(
|
||||
denomSig: UnblindedSignature;
|
||||
ageCommitmentProof: AgeCommitmentProof | undefined;
|
||||
}[] = [];
|
||||
let lastDepositFee = Amounts.zeroOfCurrency(currency);
|
||||
for (const coin of coinInfos) {
|
||||
if (Amounts.cmp(amountAcc, instructedAmount) >= 0) {
|
||||
break;
|
||||
@ -216,6 +225,7 @@ export async function selectPeerCoins(
|
||||
denomSig: coin.denomSig,
|
||||
ageCommitmentProof: coin.ageCommitmentProof,
|
||||
});
|
||||
lastDepositFee = coin.feeDeposit;
|
||||
}
|
||||
if (Amounts.cmp(amountAcc, instructedAmount) >= 0) {
|
||||
const res: PeerCoinSelection = {
|
||||
@ -225,9 +235,48 @@ export async function selectPeerCoins(
|
||||
};
|
||||
return { type: "success", result: res };
|
||||
}
|
||||
const diff = Amounts.sub(instructedAmount, amountAcc).amount;
|
||||
exchangeFeeGap[exch.baseUrl] = Amounts.add(lastDepositFee, diff).amount;
|
||||
|
||||
continue;
|
||||
}
|
||||
return { type: "failure" };
|
||||
// We were unable to select coins.
|
||||
// Now we need to produce error details.
|
||||
|
||||
const infoGeneral = await getPeerPaymentBalanceDetailsInTx(ws, tx, {
|
||||
currency,
|
||||
});
|
||||
|
||||
const perExchange: PayPeerInsufficientBalanceDetails["perExchange"] = {};
|
||||
|
||||
for (const exch of exchanges) {
|
||||
if (exch.detailsPointer?.currency !== currency) {
|
||||
continue;
|
||||
}
|
||||
const infoExchange = await getPeerPaymentBalanceDetailsInTx(ws, tx, {
|
||||
currency,
|
||||
restrictExchangeTo: exch.baseUrl,
|
||||
});
|
||||
let gap = exchangeFeeGap[exch.baseUrl] ?? Amounts.zeroOfCurrency(currency);
|
||||
if (Amounts.cmp(infoExchange.balanceMaterial, instructedAmount) < 0) {
|
||||
// Show fee gap only if we should've been able to pay with the material amount
|
||||
gap = Amounts.zeroOfAmount(currency);
|
||||
}
|
||||
perExchange[exch.baseUrl] = {
|
||||
balanceAvailable: Amounts.stringify(infoExchange.balanceAvailable),
|
||||
balanceMaterial: Amounts.stringify(infoExchange.balanceMaterial),
|
||||
feeGapEstimate: Amounts.stringify(gap),
|
||||
};
|
||||
}
|
||||
|
||||
const errDetails: PayPeerInsufficientBalanceDetails = {
|
||||
amountRequested: Amounts.stringify(instructedAmount),
|
||||
balanceAvailable: Amounts.stringify(infoGeneral.balanceAvailable),
|
||||
balanceMaterial: Amounts.stringify(infoGeneral.balanceMaterial),
|
||||
perExchange,
|
||||
};
|
||||
|
||||
return { type: "failure", insufficientBalanceDetails: errDetails };
|
||||
}
|
||||
|
||||
export async function preparePeerPushPayment(
|
||||
@ -316,8 +365,12 @@ export async function initiatePeerToPeerPush(
|
||||
logger.info(`selected p2p coins (push): ${j2s(coinSelRes)}`);
|
||||
|
||||
if (coinSelRes.type !== "success") {
|
||||
// FIXME: use error code with details here
|
||||
throw Error("insufficient balance");
|
||||
throw TalerError.fromDetail(
|
||||
TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE,
|
||||
{
|
||||
insufficientBalanceDetails: coinSelRes.insufficientBalanceDetails,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const purseSigResp = await ws.cryptoApi.signPurseCreation({
|
||||
@ -675,7 +728,12 @@ export async function acceptPeerPullPayment(
|
||||
logger.info(`selected p2p coins (pull): ${j2s(coinSelRes)}`);
|
||||
|
||||
if (coinSelRes.type !== "success") {
|
||||
throw Error("insufficient balance");
|
||||
throw TalerError.fromDetail(
|
||||
TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE,
|
||||
{
|
||||
insufficientBalanceDetails: coinSelRes.insufficientBalanceDetails,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const pursePub = peerPullInc.pursePub;
|
||||
|
Loading…
Reference in New Issue
Block a user