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);
|
console.log(resp);
|
||||||
|
|
||||||
}
|
}
|
||||||
const resp = await wallet1.client.call(
|
const resp = await wallet1.client.call(
|
||||||
WalletApiOperation.InitiatePeerPushPayment,
|
WalletApiOperation.InitiatePeerPushPayment,
|
||||||
@ -114,6 +113,21 @@ export async function runPeerToPeerPushTest(t: GlobalTestState) {
|
|||||||
|
|
||||||
console.log(`txn1: ${j2s(txn1)}`);
|
console.log(`txn1: ${j2s(txn1)}`);
|
||||||
console.log(`txn2: ${j2s(txn2)}`);
|
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"];
|
runPeerToPeerPushTest.suites = ["wallet"];
|
||||||
|
@ -3248,6 +3248,14 @@ export enum TalerErrorCode {
|
|||||||
WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE = 7027,
|
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.
|
* We encountered a timeout with our payment backend.
|
||||||
* Returned with an HTTP status code of #MHD_HTTP_GATEWAY_TIMEOUT (504).
|
* Returned with an HTTP status code of #MHD_HTTP_GATEWAY_TIMEOUT (504).
|
||||||
|
@ -2164,20 +2164,11 @@ export interface PayPeerInsufficientBalanceDetails {
|
|||||||
*/
|
*/
|
||||||
balanceMaterial: AmountString;
|
balanceMaterial: AmountString;
|
||||||
|
|
||||||
/**
|
perExchange: {
|
||||||
* Acceptable balance based on restrictions on which
|
[url: string]: {
|
||||||
* exchange can be used.
|
balanceAvailable: AmountString;
|
||||||
*/
|
balanceMaterial: AmountString;
|
||||||
balanceExchangeAcceptable: AmountString
|
feeGapEstimate: 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;
|
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,7 @@
|
|||||||
*/
|
*/
|
||||||
import {
|
import {
|
||||||
PayMerchantInsufficientBalanceDetails,
|
PayMerchantInsufficientBalanceDetails,
|
||||||
|
PayPeerInsufficientBalanceDetails,
|
||||||
TalerErrorCode,
|
TalerErrorCode,
|
||||||
TalerErrorDetail,
|
TalerErrorDetail,
|
||||||
TransactionType,
|
TransactionType,
|
||||||
@ -87,6 +88,9 @@ export interface DetailsMap {
|
|||||||
[TalerErrorCode.WALLET_DEPOSIT_GROUP_INSUFFICIENT_BALANCE]: {
|
[TalerErrorCode.WALLET_DEPOSIT_GROUP_INSUFFICIENT_BALANCE]: {
|
||||||
insufficientBalanceDetails: PayMerchantInsufficientBalanceDetails;
|
insufficientBalanceDetails: PayMerchantInsufficientBalanceDetails;
|
||||||
};
|
};
|
||||||
|
[TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE]: {
|
||||||
|
insufficientBalanceDetails: PayPeerInsufficientBalanceDetails;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
type ErrBody<Y> = Y extends keyof DetailsMap ? DetailsMap[Y] : never;
|
type ErrBody<Y> = Y extends keyof DetailsMap ? DetailsMap[Y] : never;
|
||||||
|
@ -56,7 +56,12 @@ import {
|
|||||||
canonicalizeBaseUrl,
|
canonicalizeBaseUrl,
|
||||||
parsePaytoUri,
|
parsePaytoUri,
|
||||||
} from "@gnu-taler/taler-util";
|
} 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 { GetReadOnlyAccess } from "../util/query.js";
|
||||||
import { InternalWalletState } from "../internal-wallet-state.js";
|
import { InternalWalletState } from "../internal-wallet-state.js";
|
||||||
import { getExchangeDetails } from "./exchanges.js";
|
import { getExchangeDetails } from "./exchanges.js";
|
||||||
@ -362,7 +367,7 @@ export async function getMerchantPaymentBalanceDetails(
|
|||||||
balanceMerchantDepositable: Amounts.zeroOfCurrency(req.currency),
|
balanceMerchantDepositable: Amounts.zeroOfCurrency(req.currency),
|
||||||
};
|
};
|
||||||
|
|
||||||
const wbal = await ws.db
|
await ws.db
|
||||||
.mktx((x) => [
|
.mktx((x) => [
|
||||||
x.coins,
|
x.coins,
|
||||||
x.coinAvailability,
|
x.coinAvailability,
|
||||||
@ -415,3 +420,67 @@ export async function getMerchantPaymentBalanceDetails(
|
|||||||
|
|
||||||
return d;
|
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,
|
PreparePeerPushPaymentResponse,
|
||||||
RefreshReason,
|
RefreshReason,
|
||||||
strcmp,
|
strcmp,
|
||||||
|
TalerErrorCode,
|
||||||
TalerProtocolTimestamp,
|
TalerProtocolTimestamp,
|
||||||
TransactionType,
|
TransactionType,
|
||||||
UnblindedSignature,
|
UnblindedSignature,
|
||||||
@ -77,11 +78,13 @@ import {
|
|||||||
WithdrawalGroupStatus,
|
WithdrawalGroupStatus,
|
||||||
WithdrawalRecordType,
|
WithdrawalRecordType,
|
||||||
} from "../db.js";
|
} from "../db.js";
|
||||||
|
import { TalerError } from "../errors.js";
|
||||||
import { InternalWalletState } from "../internal-wallet-state.js";
|
import { InternalWalletState } from "../internal-wallet-state.js";
|
||||||
import { makeTransactionId, spendCoins } from "../operations/common.js";
|
import { makeTransactionId, spendCoins } from "../operations/common.js";
|
||||||
import { readSuccessResponseJsonOrThrow } from "../util/http.js";
|
import { readSuccessResponseJsonOrThrow } from "../util/http.js";
|
||||||
import { checkDbInvariant } from "../util/invariants.js";
|
import { checkDbInvariant } from "../util/invariants.js";
|
||||||
import { GetReadOnlyAccess } from "../util/query.js";
|
import { GetReadOnlyAccess } from "../util/query.js";
|
||||||
|
import { getPeerPaymentBalanceDetailsInTx } from "./balance.js";
|
||||||
import { updateExchangeFromUrl } from "./exchanges.js";
|
import { updateExchangeFromUrl } from "./exchanges.js";
|
||||||
import { internalCreateWithdrawalGroup } from "./withdraw.js";
|
import { internalCreateWithdrawalGroup } from "./withdraw.js";
|
||||||
|
|
||||||
@ -135,6 +138,7 @@ export type SelectPeerCoinsResult =
|
|||||||
| { type: "success"; result: PeerCoinSelection }
|
| { type: "success"; result: PeerCoinSelection }
|
||||||
| {
|
| {
|
||||||
type: "failure";
|
type: "failure";
|
||||||
|
insufficientBalanceDetails: PayPeerInsufficientBalanceDetails;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function selectPeerCoins(
|
export async function selectPeerCoins(
|
||||||
@ -143,12 +147,16 @@ export async function selectPeerCoins(
|
|||||||
exchanges: typeof WalletStoresV1.exchanges;
|
exchanges: typeof WalletStoresV1.exchanges;
|
||||||
denominations: typeof WalletStoresV1.denominations;
|
denominations: typeof WalletStoresV1.denominations;
|
||||||
coins: typeof WalletStoresV1.coins;
|
coins: typeof WalletStoresV1.coins;
|
||||||
|
coinAvailability: typeof WalletStoresV1.coinAvailability;
|
||||||
|
refreshGroups: typeof WalletStoresV1.refreshGroups;
|
||||||
}>,
|
}>,
|
||||||
instructedAmount: AmountJson,
|
instructedAmount: AmountJson,
|
||||||
): Promise<SelectPeerCoinsResult> {
|
): Promise<SelectPeerCoinsResult> {
|
||||||
const exchanges = await tx.exchanges.iter().toArray();
|
const exchanges = await tx.exchanges.iter().toArray();
|
||||||
|
const exchangeFeeGap: { [url: string]: AmountJson } = {};
|
||||||
|
const currency = Amounts.currencyOf(instructedAmount);
|
||||||
for (const exch of exchanges) {
|
for (const exch of exchanges) {
|
||||||
if (exch.detailsPointer?.currency !== instructedAmount.currency) {
|
if (exch.detailsPointer?.currency !== currency) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const coins = (
|
const coins = (
|
||||||
@ -184,8 +192,8 @@ export async function selectPeerCoins(
|
|||||||
-Amounts.cmp(o1.value, o2.value) ||
|
-Amounts.cmp(o1.value, o2.value) ||
|
||||||
strcmp(o1.denomPubHash, o2.denomPubHash),
|
strcmp(o1.denomPubHash, o2.denomPubHash),
|
||||||
);
|
);
|
||||||
let amountAcc = Amounts.zeroOfCurrency(instructedAmount.currency);
|
let amountAcc = Amounts.zeroOfCurrency(currency);
|
||||||
let depositFeesAcc = Amounts.zeroOfCurrency(instructedAmount.currency);
|
let depositFeesAcc = Amounts.zeroOfCurrency(currency);
|
||||||
const resCoins: {
|
const resCoins: {
|
||||||
coinPub: string;
|
coinPub: string;
|
||||||
coinPriv: string;
|
coinPriv: string;
|
||||||
@ -194,6 +202,7 @@ export async function selectPeerCoins(
|
|||||||
denomSig: UnblindedSignature;
|
denomSig: UnblindedSignature;
|
||||||
ageCommitmentProof: AgeCommitmentProof | undefined;
|
ageCommitmentProof: AgeCommitmentProof | undefined;
|
||||||
}[] = [];
|
}[] = [];
|
||||||
|
let lastDepositFee = Amounts.zeroOfCurrency(currency);
|
||||||
for (const coin of coinInfos) {
|
for (const coin of coinInfos) {
|
||||||
if (Amounts.cmp(amountAcc, instructedAmount) >= 0) {
|
if (Amounts.cmp(amountAcc, instructedAmount) >= 0) {
|
||||||
break;
|
break;
|
||||||
@ -216,6 +225,7 @@ export async function selectPeerCoins(
|
|||||||
denomSig: coin.denomSig,
|
denomSig: coin.denomSig,
|
||||||
ageCommitmentProof: coin.ageCommitmentProof,
|
ageCommitmentProof: coin.ageCommitmentProof,
|
||||||
});
|
});
|
||||||
|
lastDepositFee = coin.feeDeposit;
|
||||||
}
|
}
|
||||||
if (Amounts.cmp(amountAcc, instructedAmount) >= 0) {
|
if (Amounts.cmp(amountAcc, instructedAmount) >= 0) {
|
||||||
const res: PeerCoinSelection = {
|
const res: PeerCoinSelection = {
|
||||||
@ -225,9 +235,48 @@ export async function selectPeerCoins(
|
|||||||
};
|
};
|
||||||
return { type: "success", result: res };
|
return { type: "success", result: res };
|
||||||
}
|
}
|
||||||
|
const diff = Amounts.sub(instructedAmount, amountAcc).amount;
|
||||||
|
exchangeFeeGap[exch.baseUrl] = Amounts.add(lastDepositFee, diff).amount;
|
||||||
|
|
||||||
continue;
|
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(
|
export async function preparePeerPushPayment(
|
||||||
@ -316,8 +365,12 @@ export async function initiatePeerToPeerPush(
|
|||||||
logger.info(`selected p2p coins (push): ${j2s(coinSelRes)}`);
|
logger.info(`selected p2p coins (push): ${j2s(coinSelRes)}`);
|
||||||
|
|
||||||
if (coinSelRes.type !== "success") {
|
if (coinSelRes.type !== "success") {
|
||||||
// FIXME: use error code with details here
|
throw TalerError.fromDetail(
|
||||||
throw Error("insufficient balance");
|
TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE,
|
||||||
|
{
|
||||||
|
insufficientBalanceDetails: coinSelRes.insufficientBalanceDetails,
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const purseSigResp = await ws.cryptoApi.signPurseCreation({
|
const purseSigResp = await ws.cryptoApi.signPurseCreation({
|
||||||
@ -675,7 +728,12 @@ export async function acceptPeerPullPayment(
|
|||||||
logger.info(`selected p2p coins (pull): ${j2s(coinSelRes)}`);
|
logger.info(`selected p2p coins (pull): ${j2s(coinSelRes)}`);
|
||||||
|
|
||||||
if (coinSelRes.type !== "success") {
|
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;
|
const pursePub = peerPullInc.pursePub;
|
||||||
|
Loading…
Reference in New Issue
Block a user