aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Dold <florian@dold.me>2023-01-06 13:55:08 +0100
committerFlorian Dold <florian@dold.me>2023-01-06 13:55:08 +0100
commit417c07f3f4866918e1aaa6d42b7d5ec0ca59dd51 (patch)
tree9966f647bb0779cf2de248b805f0ea13a24ddba6
parentc2c35925bb953bf07e32c005dbe312d220b45749 (diff)
wallet-core: insufficient balance details for p2p payments
-rw-r--r--packages/taler-harness/src/integrationtests/test-peer-to-peer-push.ts16
-rw-r--r--packages/taler-util/src/taler-error-codes.ts8
-rw-r--r--packages/taler-util/src/wallet-types.ts23
-rw-r--r--packages/taler-wallet-core/src/errors.ts4
-rw-r--r--packages/taler-wallet-core/src/operations/balance.ts73
-rw-r--r--packages/taler-wallet-core/src/operations/pay-peer.ts72
6 files changed, 170 insertions, 26 deletions
diff --git a/packages/taler-harness/src/integrationtests/test-peer-to-peer-push.ts b/packages/taler-harness/src/integrationtests/test-peer-to-peer-push.ts
index 2a93c3559..eb29a81c2 100644
--- a/packages/taler-harness/src/integrationtests/test-peer-to-peer-push.ts
+++ b/packages/taler-harness/src/integrationtests/test-peer-to-peer-push.ts
@@ -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"];
diff --git a/packages/taler-util/src/taler-error-codes.ts b/packages/taler-util/src/taler-error-codes.ts
index 5e3c8fdfb..9e735817d 100644
--- a/packages/taler-util/src/taler-error-codes.ts
+++ b/packages/taler-util/src/taler-error-codes.ts
@@ -3249,6 +3249,14 @@ export enum TalerErrorCode {
/**
+ * 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).
* (A value of 0 indicates that the error is generated client-side).
diff --git a/packages/taler-util/src/wallet-types.ts b/packages/taler-util/src/wallet-types.ts
index 3a1176021..06edfe285 100644
--- a/packages/taler-util/src/wallet-types.ts
+++ b/packages/taler-util/src/wallet-types.ts
@@ -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;
+ };
+ };
}
diff --git a/packages/taler-wallet-core/src/errors.ts b/packages/taler-wallet-core/src/errors.ts
index 68cd39b54..37a31a8aa 100644
--- a/packages/taler-wallet-core/src/errors.ts
+++ b/packages/taler-wallet-core/src/errors.ts
@@ -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;
diff --git a/packages/taler-wallet-core/src/operations/balance.ts b/packages/taler-wallet-core/src/operations/balance.ts
index f697679af..383aa5dc1 100644
--- a/packages/taler-wallet-core/src/operations/balance.ts
+++ b/packages/taler-wallet-core/src/operations/balance.ts
@@ -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,
+ };
+}
diff --git a/packages/taler-wallet-core/src/operations/pay-peer.ts b/packages/taler-wallet-core/src/operations/pay-peer.ts
index 3d03c46db..3ee1795b0 100644
--- a/packages/taler-wallet-core/src/operations/pay-peer.ts
+++ b/packages/taler-wallet-core/src/operations/pay-peer.ts
@@ -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;