diff options
-rw-r--r-- | packages/taler-harness/src/integrationtests/test-peer-to-peer-push.ts | 16 | ||||
-rw-r--r-- | packages/taler-util/src/taler-error-codes.ts | 8 | ||||
-rw-r--r-- | packages/taler-util/src/wallet-types.ts | 23 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/errors.ts | 4 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/operations/balance.ts | 73 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/operations/pay-peer.ts | 72 |
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; |