From 7ba1d1f3351e58a331e99337afea0fbedb6eb828 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Wed, 6 Mar 2024 21:15:30 +0100 Subject: refactor coin selection, report maxEffectiveSpendAmount --- .../src/integrationtests/test-forced-selection.ts | 2 +- packages/taler-util/src/wallet-types.ts | 39 +-- .../taler-wallet-core/src/coinSelection.test.ts | 2 +- packages/taler-wallet-core/src/coinSelection.ts | 366 ++++++++++----------- packages/taler-wallet-core/src/db.ts | 14 +- packages/taler-wallet-core/src/deposits.ts | 31 +- .../src/instructedAmountConversion.test.ts | 4 +- .../src/instructedAmountConversion.ts | 17 +- packages/taler-wallet-core/src/pay-merchant.ts | 63 ++-- packages/taler-wallet-core/src/pay-peer-common.ts | 8 +- .../taler-wallet-core/src/pay-peer-pull-debit.ts | 14 +- .../taler-wallet-core/src/pay-peer-push-debit.ts | 14 +- packages/taler-wallet-core/src/testing.ts | 10 +- .../src/components/PaymentButtons.tsx | 22 +- .../src/cta/Payment/stories.tsx | 12 +- .../src/cta/TransferCreate/state.ts | 18 +- 16 files changed, 307 insertions(+), 329 deletions(-) (limited to 'packages') diff --git a/packages/taler-harness/src/integrationtests/test-forced-selection.ts b/packages/taler-harness/src/integrationtests/test-forced-selection.ts index 752810703..839ddd927 100644 --- a/packages/taler-harness/src/integrationtests/test-forced-selection.ts +++ b/packages/taler-harness/src/integrationtests/test-forced-selection.ts @@ -80,7 +80,7 @@ export async function runForcedSelectionTest(t: GlobalTestState) { console.log(j2s(payResp)); // Without forced selection, we would only use 2 coins. - t.assertDeepEqual(payResp.payCoinSelection.coinContributions.length, 3); + t.assertDeepEqual(payResp.numCoins, 3); } runForcedSelectionTest.suites = ["wallet"]; diff --git a/packages/taler-util/src/wallet-types.ts b/packages/taler-util/src/wallet-types.ts index 9fe114b3d..cb4374648 100644 --- a/packages/taler-util/src/wallet-types.ts +++ b/packages/taler-util/src/wallet-types.ts @@ -868,21 +868,15 @@ export interface PayMerchantInsufficientBalanceDetails { balanceMerchantDepositable: AmountString; /** - * If the payment would succeed without fees - * (i.e. balanceMerchantDepositable >= 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. + * Maximum effective amount that the wallet can spend, + * when all fees are paid by the wallet. */ - feeGapEstimate: AmountString; + maxEffectiveSpendAmount: AmountString; perExchange: { [url: string]: { balanceAvailable: AmountString; balanceMaterial: AmountString; - feeGapEstimate: AmountString; }; }; } @@ -896,8 +890,8 @@ export const codecForPayMerchantInsufficientBalanceDetails = .property("balanceMaterial", codecForAmountString()) .property("balanceMerchantAcceptable", codecForAmountString()) .property("balanceMerchantDepositable", codecForAmountString()) - .property("feeGapEstimate", codecForAmountString()) .property("perExchange", codecForAny()) + .property("maxEffectiveSpendAmount", codecForAmountString()) .build("PayMerchantInsufficientBalanceDetails"); export const codecForPreparePayResultInsufficientBalance = @@ -2623,7 +2617,15 @@ export interface ForcedCoinSel { } export interface TestPayResult { - payCoinSelection: PayCoinSelection; + /** + * Number of coins used for the payment. + */ + numCoins: number; +} + +export interface SelectedCoin { + coinPub: string; + contribution: AmountString; } /** @@ -2631,20 +2633,7 @@ export interface TestPayResult { * coins with their denomination. */ export interface PayCoinSelection { - /** - * Amount requested by the merchant. - */ - paymentAmount: AmountString; - - /** - * Public keys of the coins that were selected. - */ - coinPubs: string[]; - - /** - * Amount that each coin contributes. - */ - coinContributions: AmountString[]; + coins: SelectedCoin[]; /** * How much of the wire fees is the customer paying? diff --git a/packages/taler-wallet-core/src/coinSelection.test.ts b/packages/taler-wallet-core/src/coinSelection.test.ts index 6eae9deaa..3d8e24b0c 100644 --- a/packages/taler-wallet-core/src/coinSelection.test.ts +++ b/packages/taler-wallet-core/src/coinSelection.test.ts @@ -179,7 +179,7 @@ test("pay: select one coin to pay with fee", (t) => { amountWireFeeLimitRemaining: zero, amountDepositFeeLimitRemaining: zero, customerDepositFees: Amounts.parse("LOCAL:0.1"), - customerWireFees: zero, + customerWireFees: Amounts.parse("LOCAL:0.1"), wireFeeCoveredForExchange: new Set(["http://exchange.localhost/"]), lastDepositFee: Amounts.parse("LOCAL:0.1"), }); diff --git a/packages/taler-wallet-core/src/coinSelection.ts b/packages/taler-wallet-core/src/coinSelection.ts index 1208e7c37..5ac52e1d3 100644 --- a/packages/taler-wallet-core/src/coinSelection.ts +++ b/packages/taler-wallet-core/src/coinSelection.ts @@ -27,19 +27,15 @@ import { GlobalIDB } from "@gnu-taler/idb-bridge"; import { AbsoluteTime, AccountRestriction, - AgeCommitmentProof, AgeRestriction, AllowedAuditorInfo, AllowedExchangeInfo, AmountJson, Amounts, - AmountString, checkDbInvariant, checkLogicInvariant, - CoinPublicKeyString, CoinStatus, DenominationInfo, - Duration, ForcedCoinSel, InternationalizedString, j2s, @@ -47,9 +43,9 @@ import { parsePaytoUri, PayCoinSelection, PayMerchantInsufficientBalanceDetails, + SelectedCoin, strcmp, TalerProtocolTimestamp, - UnblindedSignature, } from "@gnu-taler/taler-util"; import { getExchangePaymentBalanceDetailsInTx, @@ -68,8 +64,6 @@ const logger = new Logger("coinSelection.ts"); export type PreviousPayCoins = { coinPub: string; contribution: AmountJson; - feeDeposit: AmountJson; - exchangeBaseUrl: string; }[]; export interface ExchangeRestrictionSpec { @@ -192,12 +186,7 @@ export async function selectPayCoins( wex: WalletExecutionContext, req: SelectPayCoinRequestNg, ): Promise { - const { - contractTermsAmount, - depositFeeLimit, - wireFeeLimit, - wireFeeAmortization, - } = req; + const { contractTermsAmount, depositFeeLimit, wireFeeLimit } = req; return await wex.db.runReadOnlyTx( [ @@ -221,8 +210,7 @@ export async function selectPayCoins( }, ); - const coinPubs: string[] = []; - const coinContributions: AmountJson[] = []; + const coinRes: SelectedCoin[] = []; const currency = contractTermsAmount.currency; let tally: CoinSelectionTally = { @@ -235,25 +223,17 @@ export async function selectPayCoins( lastDepositFee: Amounts.zeroOfCurrency(currency), }; - const prevPayCoins = req.prevPayCoins ?? []; - - // Look at existing pay coin selection and tally up - for (const prev of prevPayCoins) { - tallyFees( - tally, - wireFeesPerExchange, - wireFeeAmortization, - prev.exchangeBaseUrl, - prev.feeDeposit, - ); - tally.amountPayRemaining = Amounts.sub( - tally.amountPayRemaining, - prev.contribution, - ).amount; - - coinPubs.push(prev.coinPub); - coinContributions.push(prev.contribution); - } + await maybeRepairCoinSelection( + wex, + tx, + req.prevPayCoins ?? [], + coinRes, + tally, + { + wireFeeAmortization: req.wireFeeAmortization, + wireFeesPerExchange: wireFeesPerExchange, + }, + ); let selectedDenom: SelResult | undefined; if (req.forcedSelection) { @@ -292,8 +272,7 @@ export async function selectPayCoins( const coinSel = await assembleSelectPayCoinsSuccessResult( tx, selectedDenom, - coinPubs, - coinContributions, + coinRes, req.contractTermsAmount, tally, ); @@ -306,11 +285,55 @@ export async function selectPayCoins( ); } +async function maybeRepairCoinSelection( + wex: WalletExecutionContext, + tx: WalletDbReadOnlyTransaction<["coins", "denominations"]>, + prevPayCoins: PreviousPayCoins, + coinRes: SelectedCoin[], + tally: CoinSelectionTally, + feeInfo: { + wireFeeAmortization: number; + wireFeesPerExchange: Record; + }, +): Promise { + // Look at existing pay coin selection and tally up + for (const prev of prevPayCoins) { + const coin = await tx.coins.get(prev.coinPub); + if (!coin) { + continue; + } + const denom = await getDenomInfo( + wex, + tx, + coin.exchangeBaseUrl, + coin.denomPubHash, + ); + if (!denom) { + continue; + } + tallyFees( + tally, + feeInfo.wireFeesPerExchange, + feeInfo.wireFeeAmortization, + coin.exchangeBaseUrl, + Amounts.parseOrThrow(denom.feeDeposit), + ); + tally.amountPayRemaining = Amounts.sub( + tally.amountPayRemaining, + prev.contribution, + ).amount; + + coinRes.push({ + coinPub: prev.coinPub, + contribution: Amounts.stringify(prev.contribution), + }); + } +} + async function assembleSelectPayCoinsSuccessResult( tx: WalletDbReadOnlyTransaction<["coins"]>, finalSel: SelResult, - coinPubs: string[], - coinContributions: AmountJson[], + coinRes: SelectedCoin[], contractTermsAmount: AmountJson, tally: CoinSelectionTally, ): Promise { @@ -334,15 +357,17 @@ async function assembleSelectPayCoinsSuccessResult( `coin selection failed (not available anymore, got only ${coins.length}/${numRequested})`, ); } - coinPubs.push(...coins.map((x) => x.coinPub)); - coinContributions.push(...selInfo.contributions); + + for (let i = 0; i < selInfo.contributions.length; i++) { + coinRes.push({ + coinPub: coins[i].coinPub, + contribution: Amounts.stringify(selInfo.contributions[i]), + }); + } } return { - // FIXME: Why do we return this?! - paymentAmount: Amounts.stringify(contractTermsAmount), - coinContributions: coinContributions.map((x) => Amounts.stringify(x)), - coinPubs, + coins: coinRes, customerDepositFees: Amounts.stringify(tally.customerDepositFees), customerWireFees: Amounts.stringify(tally.customerWireFees), }; @@ -358,7 +383,13 @@ interface ReportInsufficientBalanceRequest { export async function reportInsufficientBalanceDetails( wex: WalletExecutionContext, tx: WalletDbReadOnlyTransaction< - ["coinAvailability", "exchanges", "exchangeDetails", "refreshGroups"] + [ + "coinAvailability", + "exchanges", + "exchangeDetails", + "refreshGroups", + "denominations", + ] >, req: ReportInsufficientBalanceRequest, ): Promise { @@ -384,10 +415,52 @@ export async function reportInsufficientBalanceDetails( const exchanges = await tx.exchanges.iter().toArray(); + let maxEffectiveSpendAmount = Amounts.zeroOfAmount(req.instructedAmount); + for (const exch of exchanges) { if (exch.detailsPointer?.currency !== currency) { continue; } + + // We now see how much we could spend if we paid all the fees ourselves + // in a worst-case estimate. + + const exchangeBaseUrl = exch.baseUrl; + let ageLower = 0; + let ageUpper = AgeRestriction.AGE_UNRESTRICTED; + if (req.requiredMinimumAge) { + ageLower = req.requiredMinimumAge; + } + + const myExchangeCoins = + await tx.coinAvailability.indexes.byExchangeAgeAvailability.getAll( + GlobalIDB.KeyRange.bound( + [exchangeBaseUrl, ageLower, 1], + [exchangeBaseUrl, ageUpper, Number.MAX_SAFE_INTEGER], + ), + ); + + for (const ec of myExchangeCoins) { + maxEffectiveSpendAmount = Amounts.add( + maxEffectiveSpendAmount, + Amounts.mult(ec.value, ec.freshCoinCount).amount, + ).amount; + + const denom = await getDenomInfo( + wex, + tx, + exchangeBaseUrl, + ec.denomPubHash, + ); + if (!denom) { + continue; + } + maxEffectiveSpendAmount = Amounts.sub( + maxEffectiveSpendAmount, + Amounts.mult(denom.feeDeposit, ec.freshCoinCount).amount, + ).amount; + } + const infoExchange = await getExchangePaymentBalanceDetailsInTx(wex, tx, { currency, restrictExchangeTo: exch.baseUrl, @@ -395,7 +468,6 @@ export async function reportInsufficientBalanceDetails( perExchange[exch.baseUrl] = { balanceAvailable: Amounts.stringify(infoExchange.balanceAvailable), balanceMaterial: Amounts.stringify(infoExchange.balanceMaterial), - feeGapEstimate: Amounts.stringify(Amounts.zeroOfCurrency(currency)), }; } @@ -410,7 +482,7 @@ export async function reportInsufficientBalanceDetails( balanceMerchantDepositable: Amounts.stringify( details.balanceMerchantDepositable, ), - feeGapEstimate: Amounts.stringify(feeGapEstimate), + maxEffectiveSpendAmount: Amounts.stringify(maxEffectiveSpendAmount), perExchange, }; } @@ -434,8 +506,6 @@ interface SelResult { [avKey: string]: { exchangeBaseUrl: string; denomPubHash: string; - expireWithdraw: TalerProtocolTimestamp; - expireDeposit: TalerProtocolTimestamp; maxAge: number; contributions: AmountJson[]; }; @@ -508,8 +578,6 @@ function selectGreedy( denomPubHash: denom.denomPubHash, exchangeBaseUrl: denom.exchangeBaseUrl, maxAge: denom.maxAge, - expireDeposit: denom.stampExpireDeposit, - expireWithdraw: denom.stampExpireWithdraw, }; } sd.contributions.push(...contributions); @@ -549,8 +617,6 @@ function selectForced( denomPubHash: aci.denomPubHash, exchangeBaseUrl: aci.exchangeBaseUrl, maxAge: aci.maxAge, - expireDeposit: aci.stampExpireDeposit, - expireWithdraw: aci.stampExpireWithdraw, }; } sd.contributions.push(Amounts.parseOrThrow(forcedCoin.value)); @@ -563,7 +629,6 @@ function selectForced( throw Error("can't find coin for forced coin selection"); } } - return selectedDenom; } @@ -696,12 +761,7 @@ interface SelectPayCandidatesRequest { instructedAmount: AmountJson; restrictWireMethod: string | undefined; depositPaytoUri?: string; - restrictExchanges: - | { - exchanges: AllowedExchangeInfo[]; - auditors: AllowedAuditorInfo[]; - } - | undefined; + restrictExchanges: ExchangeRestrictionSpec | undefined; requiredMinimumAge?: number; } @@ -796,36 +856,13 @@ async function selectPayCandidates( return [denoms, wfPerExchange]; } -export interface CoinInfo { - id: string; - value: AmountJson; - denomDeposit: AmountJson; - denomWithdraw: AmountJson; - denomRefresh: AmountJson; - totalAvailable: number | undefined; - exchangeWire: AmountJson | undefined; - exchangePurse: AmountJson | undefined; - duration: Duration; - exchangeBaseUrl: string; - maxAge: number; -} - -export interface SelectedPeerCoin { - coinPub: string; - coinPriv: string; - contribution: AmountString; - denomPubHash: string; - denomSig: UnblindedSignature; - ageCommitmentProof: AgeCommitmentProof | undefined; -} - export interface PeerCoinSelectionDetails { exchangeBaseUrl: string; /** * Info of Coins that were selected. */ - coins: SelectedPeerCoin[]; + coins: SelectedCoin[]; /** * How much of the deposit fees is the customer paying? @@ -842,12 +879,6 @@ export type SelectPeerCoinsResult = insufficientBalanceDetails: PayMerchantInsufficientBalanceDetails; }; -export interface PeerCoinRepair { - exchangeBaseUrl: string; - coinPubs: CoinPublicKeyString[]; - contribs: AmountJson[]; -} - export interface PeerCoinSelectionRequest { instructedAmount: AmountJson; @@ -855,121 +886,39 @@ export interface PeerCoinSelectionRequest { * Instruct the coin selection to repair this coin * selection instead of selecting completely new coins. */ - repair?: PeerCoinRepair; + repair?: PreviousPayCoins; } -async function assemblePeerCoinSelectionDetails( - tx: WalletDbReadOnlyTransaction<["coins"]>, - exchangeBaseUrl: string, +export async function computeCoinSelMaxExpirationDate( + wex: WalletExecutionContext, + tx: WalletDbReadOnlyTransaction<["coins", "denominations"]>, selectedDenom: SelResult, - resCoins: ResCoin[], - tally: CoinSelectionTally, -): Promise { +): Promise { let minAutorefreshExecuteThreshold = TalerProtocolTimestamp.never(); for (const dph of Object.keys(selectedDenom)) { const selInfo = selectedDenom[dph]; + const denom = await getDenomInfo( + wex, + tx, + selInfo.exchangeBaseUrl, + selInfo.denomPubHash, + ); + if (!denom) { + continue; + } // Compute earliest time that a selected denom // would have its coins auto-refreshed. minAutorefreshExecuteThreshold = TalerProtocolTimestamp.min( minAutorefreshExecuteThreshold, AbsoluteTime.toProtocolTimestamp( getAutoRefreshExecuteThreshold({ - stampExpireDeposit: selInfo.expireDeposit, - stampExpireWithdraw: selInfo.expireWithdraw, + stampExpireDeposit: denom.stampExpireDeposit, + stampExpireWithdraw: denom.stampExpireWithdraw, }), ), ); - const numRequested = selInfo.contributions.length; - const query = [ - selInfo.exchangeBaseUrl, - selInfo.denomPubHash, - selInfo.maxAge, - CoinStatus.Fresh, - ]; - logger.info(`query: ${j2s(query)}`); - const coins = - await tx.coins.indexes.byExchangeDenomPubHashAndAgeAndStatus.getAll( - query, - numRequested, - ); - if (coins.length != numRequested) { - throw Error( - `coin selection failed (not available anymore, got only ${coins.length}/${numRequested})`, - ); - } - for (let i = 0; i < selInfo.contributions.length; i++) { - resCoins.push({ - coinPriv: coins[i].coinPriv, - coinPub: coins[i].coinPub, - contribution: Amounts.stringify(selInfo.contributions[i]), - ageCommitmentProof: coins[i].ageCommitmentProof, - denomPubHash: selInfo.denomPubHash, - denomSig: coins[i].denomSig, - }); - } } - - return { - exchangeBaseUrl, - coins: resCoins, - depositFees: tally.customerDepositFees, - maxExpirationDate: minAutorefreshExecuteThreshold, - }; -} - -async function maybeRepairPeerCoinSelection( - wex: WalletExecutionContext, - tx: WalletDbReadOnlyTransaction<["coins", "denominations"]>, - exchangeBaseUrl: string, - tally: CoinSelectionTally, - repair: PeerCoinRepair | undefined, -): Promise { - const resCoins: ResCoin[] = []; - - if (repair && repair.exchangeBaseUrl === exchangeBaseUrl) { - for (let i = 0; i < repair.coinPubs.length; i++) { - const contrib = repair.contribs[i]; - const coin = await tx.coins.get(repair.coinPubs[i]); - if (!coin) { - throw Error("repair not possible, coin not found"); - } - const denom = await getDenomInfo( - wex, - tx, - coin.exchangeBaseUrl, - coin.denomPubHash, - ); - checkDbInvariant(!!denom); - resCoins.push({ - coinPriv: coin.coinPriv, - coinPub: coin.coinPub, - contribution: Amounts.stringify(contrib), - denomPubHash: coin.denomPubHash, - denomSig: coin.denomSig, - ageCommitmentProof: coin.ageCommitmentProof, - }); - const depositFee = Amounts.parseOrThrow(denom.feeDeposit); - tally.lastDepositFee = depositFee; - tally.amountPayRemaining = Amounts.sub( - tally.amountPayRemaining, - Amounts.sub(contrib, depositFee).amount, - ).amount; - tally.customerDepositFees = Amounts.add( - tally.customerDepositFees, - depositFee, - ).amount; - } - } - return resCoins; -} - -interface ResCoin { - coinPub: string; - coinPriv: string; - contribution: AmountString; - denomPubHash: string; - denomSig: UnblindedSignature; - ageCommitmentProof: AgeCommitmentProof | undefined; + return minAutorefreshExecuteThreshold; } export function emptyTallyForPeerPayment( @@ -1034,12 +983,18 @@ export async function selectPeerCoins( logger.trace(`peer payment candidate coins: ${j2s(candidates)}`); } const tally = emptyTallyForPeerPayment(req.instructedAmount); - const resCoins: ResCoin[] = await maybeRepairPeerCoinSelection( + const resCoins: SelectedCoin[] = []; + + await maybeRepairCoinSelection( wex, tx, - exch.baseUrl, + req.repair ?? [], + resCoins, tally, - req.repair, + { + wireFeeAmortization: 1, + wireFeesPerExchange: {}, + }, ); if (logger.shouldLogTrace()) { @@ -1058,15 +1013,28 @@ export async function selectPeerCoins( ); if (selectedDenom) { + const r = await assembleSelectPayCoinsSuccessResult( + tx, + selectedDenom, + resCoins, + req.instructedAmount, + tally, + ); + + const maxExpirationDate = await computeCoinSelMaxExpirationDate( + wex, + tx, + selectedDenom, + ); + return { type: "success", - result: await assemblePeerCoinSelectionDetails( - tx, - exch.baseUrl, - selectedDenom, - resCoins, - tally, - ), + result: { + coins: r.coins, + depositFees: Amounts.parseOrThrow(r.customerDepositFees), + exchangeBaseUrl: exch.baseUrl, + maxExpirationDate, + }, }; } } diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts index dabc6393d..14621c2d5 100644 --- a/packages/taler-wallet-core/src/db.ts +++ b/packages/taler-wallet-core/src/db.ts @@ -48,7 +48,6 @@ import { ExchangeGlobalFees, HashCodeString, Logger, - PayCoinSelection, RefreshReason, TalerErrorDetail, TalerPreciseTimestamp, @@ -1207,8 +1206,13 @@ export interface ProposalDownloadInfo { contractTermsMerchantSig: string; } +export interface DbCoinSelection { + coinPubs: string[]; + coinContributions: AmountString[]; +} + export interface PurchasePayInfo { - payCoinSelection: PayCoinSelection; + payCoinSelection: DbCoinSelection; totalPayCost: AmountString; payCoinSelectionUid: string; } @@ -1769,7 +1773,7 @@ export interface DepositGroupRecord { contractTermsHash: string; - payCoinSelection: PayCoinSelection; + payCoinSelection: DbCoinSelection; payCoinSelectionUid: string; @@ -1847,7 +1851,7 @@ export enum PeerPushDebitStatus { Expired = 0x0502_0000, } -export interface PeerPushPaymentCoinSelection { +export interface DbPeerPushPaymentCoinSelection { contributions: AmountString[]; coinPubs: CoinPublicKeyString[]; } @@ -1868,7 +1872,7 @@ export interface PeerPushDebitRecord { totalCost: AmountString; - coinSel: PeerPushPaymentCoinSelection; + coinSel: DbPeerPushPaymentCoinSelection; contractTermsHash: HashCodeString; diff --git a/packages/taler-wallet-core/src/deposits.ts b/packages/taler-wallet-core/src/deposits.ts index 2e28ba9b7..2c7ee3596 100644 --- a/packages/taler-wallet-core/src/deposits.ts +++ b/packages/taler-wallet-core/src/deposits.ts @@ -1378,8 +1378,8 @@ export async function createDepositGroup( const infoPerExchange: Record = {}; await wex.db.runReadOnlyTx(["coins"], async (tx) => { - for (let i = 0; i < payCoinSel.coinSel.coinPubs.length; i++) { - const coin = await tx.coins.get(payCoinSel.coinSel.coinPubs[i]); + for (let i = 0; i < payCoinSel.coinSel.coins.length; i++) { + const coin = await tx.coins.get(payCoinSel.coinSel.coins[i].coinPub); if (!coin) { logger.error("coin not found anymore"); continue; @@ -1392,7 +1392,7 @@ export async function createDepositGroup( ), }; } - const contrib = payCoinSel.coinSel.coinContributions[i]; + const contrib = payCoinSel.coinSel.coins[i].contribution; depPerExchange.amountEffective = Amounts.stringify( Amounts.add(depPerExchange.amountEffective, contrib).amount, ); @@ -1417,10 +1417,13 @@ export async function createDepositGroup( AbsoluteTime.toPreciseTimestamp(now), ), timestampFinished: undefined, - statusPerCoin: payCoinSel.coinSel.coinPubs.map( + statusPerCoin: payCoinSel.coinSel.coins.map( () => DepositElementStatus.DepositPending, ), - payCoinSelection: payCoinSel.coinSel, + payCoinSelection: { + coinContributions: payCoinSel.coinSel.coins.map((x) => x.contribution), + coinPubs: payCoinSel.coinSel.coins.map((x) => x.coinPub), + }, payCoinSelectionUid: encodeCrock(getRandomBytes(32)), merchantPriv: merchantPair.priv, merchantPub: merchantPair.pub, @@ -1455,9 +1458,9 @@ export async function createDepositGroup( async (tx) => { await spendCoins(wex, tx, { allocationId: transactionId, - coinPubs: payCoinSel.coinSel.coinPubs, - contributions: payCoinSel.coinSel.coinContributions.map((x) => - Amounts.parseOrThrow(x), + coinPubs: payCoinSel.coinSel.coins.map((x) => x.coinPub), + contributions: payCoinSel.coinSel.coins.map((x) => + Amounts.parseOrThrow(x.contribution), ), refreshReason: RefreshReason.PayDeposit, }); @@ -1508,8 +1511,8 @@ export async function getCounterpartyEffectiveDepositAmount( await wex.db.runReadOnlyTx( ["coins", "denominations", "exchangeDetails", "exchanges"], async (tx) => { - for (let i = 0; i < pcs.coinPubs.length; i++) { - const coin = await tx.coins.get(pcs.coinPubs[i]); + for (let i = 0; i < pcs.coins.length; i++) { + const coin = await tx.coins.get(pcs.coins[i].coinPub); if (!coin) { throw Error("can't calculate deposit amount, coin not found"); } @@ -1522,7 +1525,7 @@ export async function getCounterpartyEffectiveDepositAmount( if (!denom) { throw Error("can't find denomination to calculate deposit amount"); } - amt.push(Amounts.parseOrThrow(pcs.coinContributions[i])); + amt.push(Amounts.parseOrThrow(pcs.coins[i].contribution)); fees.push(Amounts.parseOrThrow(denom.feeDeposit)); exchangeSet.add(coin.exchangeBaseUrl); } @@ -1574,8 +1577,8 @@ async function getTotalFeesForDepositAmount( await wex.db.runReadOnlyTx( ["coins", "denominations", "exchanges", "exchangeDetails"], async (tx) => { - for (let i = 0; i < pcs.coinPubs.length; i++) { - const coin = await tx.coins.get(pcs.coinPubs[i]); + for (let i = 0; i < pcs.coins.length; i++) { + const coin = await tx.coins.get(pcs.coins[i].coinPub); if (!coin) { throw Error("can't calculate deposit amount, coin not found"); } @@ -1599,7 +1602,7 @@ async function getTotalFeesForDepositAmount( ); const amountLeft = Amounts.sub( denom.value, - pcs.coinContributions[i], + pcs.coins[i].contribution, ).amount; const refreshCost = getTotalRefreshCost( allDenoms, diff --git a/packages/taler-wallet-core/src/instructedAmountConversion.test.ts b/packages/taler-wallet-core/src/instructedAmountConversion.test.ts index 3b618f797..03e702568 100644 --- a/packages/taler-wallet-core/src/instructedAmountConversion.test.ts +++ b/packages/taler-wallet-core/src/instructedAmountConversion.test.ts @@ -22,11 +22,11 @@ import { TransactionAmountMode, } from "@gnu-taler/taler-util"; import test, { ExecutionContext } from "ava"; -import { CoinInfo } from "./coinSelection.js"; import { + CoinInfo, convertDepositAmountForAvailableCoins, - getMaxDepositAmountForAvailableCoins, convertWithdrawalAmountFromAvailableCoins, + getMaxDepositAmountForAvailableCoins, } from "./instructedAmountConversion.js"; function makeCurrencyHelper(currency: string) { diff --git a/packages/taler-wallet-core/src/instructedAmountConversion.ts b/packages/taler-wallet-core/src/instructedAmountConversion.ts index ccad050bf..63ccb8b56 100644 --- a/packages/taler-wallet-core/src/instructedAmountConversion.ts +++ b/packages/taler-wallet-core/src/instructedAmountConversion.ts @@ -31,10 +31,23 @@ import { parsePaytoUri, strcmp, } from "@gnu-taler/taler-util"; -import { CoinInfo } from "./coinSelection.js"; import { DenominationRecord, timestampProtocolFromDb } from "./db.js"; import { getExchangeWireDetailsInTx } from "./exchanges.js"; -import { InternalWalletState, WalletExecutionContext } from "./wallet.js"; +import { WalletExecutionContext } from "./wallet.js"; + +export interface CoinInfo { + id: string; + value: AmountJson; + denomDeposit: AmountJson; + denomWithdraw: AmountJson; + denomRefresh: AmountJson; + totalAvailable: number | undefined; + exchangeWire: AmountJson | undefined; + exchangePurse: AmountJson | undefined; + duration: Duration; + exchangeBaseUrl: string; + maxAge: number; +} /** * If the operation going to be plan subtracts diff --git a/packages/taler-wallet-core/src/pay-merchant.ts b/packages/taler-wallet-core/src/pay-merchant.ts index ed58dc404..a155d6298 100644 --- a/packages/taler-wallet-core/src/pay-merchant.ts +++ b/packages/taler-wallet-core/src/pay-merchant.ts @@ -111,6 +111,7 @@ import { import { EddsaKeypair } from "./crypto/cryptoImplementation.js"; import { CoinRecord, + DbCoinSelection, DenominationRecord, PurchaseRecord, PurchaseStatus, @@ -445,11 +446,11 @@ export async function getTotalPaymentCost( wex: WalletExecutionContext, pcs: PayCoinSelection, ): Promise { - const currency = Amounts.currencyOf(pcs.paymentAmount); + const currency = Amounts.currencyOf(pcs.customerDepositFees); return wex.db.runReadOnlyTx(["coins", "denominations"], async (tx) => { const costs: AmountJson[] = []; - for (let i = 0; i < pcs.coinPubs.length; i++) { - const coin = await tx.coins.get(pcs.coinPubs[i]); + for (let i = 0; i < pcs.coins.length; i++) { + const coin = await tx.coins.get(pcs.coins[i].coinPub); if (!coin) { throw Error("can't calculate payment cost, coin not found"); } @@ -470,7 +471,7 @@ export async function getTotalPaymentCost( ); const amountLeft = Amounts.sub( denom.value, - pcs.coinContributions[i], + pcs.coins[i].contribution, ).amount; const refreshCost = getTotalRefreshCost( allDenoms, @@ -478,10 +479,10 @@ export async function getTotalPaymentCost( amountLeft, wex.ws.config.testing.denomselAllowLate, ); - costs.push(Amounts.parseOrThrow(pcs.coinContributions[i])); + costs.push(Amounts.parseOrThrow(pcs.coins[i].contribution)); costs.push(refreshCost); } - const zero = Amounts.zeroOfAmount(pcs.paymentAmount); + const zero = Amounts.zeroOfAmount(pcs.customerDepositFees); return Amounts.sum([zero, ...costs]).amount; }); } @@ -617,7 +618,8 @@ async function processDownloadProposal( if (proposal.purchaseStatus != PurchaseStatus.PendingDownloadingProposal) { logger.error( - `unexpected state ${proposal.purchaseStatus}/${PurchaseStatus[proposal.purchaseStatus] + `unexpected state ${proposal.purchaseStatus}/${ + PurchaseStatus[proposal.purchaseStatus] } for ${ctx.transactionId} in processDownloadProposal`, ); return TaskRunResult.finished(); @@ -873,7 +875,8 @@ async function createOrReusePurchase( oldProposal.claimToken === claimToken ) { logger.info( - `Found old proposal (status=${PurchaseStatus[oldProposal.purchaseStatus] + `Found old proposal (status=${ + PurchaseStatus[oldProposal.purchaseStatus] }) for order ${orderId} at ${merchantBaseUrl}`, ); if (oldProposal.purchaseStatus === PurchaseStatus.DialogShared) { @@ -1137,26 +1140,10 @@ async function handleInsufficientFunds( await wex.db.runReadOnlyTx(["coins", "denominations"], async (tx) => { for (let i = 0; i < payCoinSelection.coinPubs.length; i++) { const coinPub = payCoinSelection.coinPubs[i]; - if (coinPub === brokenCoinPub) { - continue; - } const contrib = payCoinSelection.coinContributions[i]; - const coin = await tx.coins.get(coinPub); - if (!coin) { - continue; - } - const denom = await tx.denominations.get([ - coin.exchangeBaseUrl, - coin.denomPubHash, - ]); - if (!denom) { - continue; - } prevPayCoins.push({ coinPub, contribution: Amounts.parseOrThrow(contrib), - exchangeBaseUrl: coin.exchangeBaseUrl, - feeDeposit: Amounts.parseOrThrow(denom.fees.feeDeposit), }); } }); @@ -1199,7 +1186,11 @@ async function handleInsufficientFunds( if (!payInfo) { return; } - payInfo.payCoinSelection = res.coinSel; + // Convert to DB format + payInfo.payCoinSelection = { + coinContributions: res.coinSel.coins.map((x) => x.contribution), + coinPubs: res.coinSel.coins.map((x) => x.coinPub), + }; payInfo.payCoinSelectionUid = encodeCrock(getRandomBytes(32)); await tx.purchases.put(p); await spendCoins(wex, tx, { @@ -1286,13 +1277,14 @@ async function checkPaymentByProposalId( purchase.purchaseStatus === PurchaseStatus.DialogProposed || purchase.purchaseStatus === PurchaseStatus.DialogShared ) { + const instructedAmount = Amounts.parseOrThrow(contractData.amount); // If not already paid, check if we could pay for it. const res = await selectPayCoins(wex, { restrictExchanges: { auditors: [], exchanges: contractData.allowedExchanges, }, - contractTermsAmount: Amounts.parseOrThrow(contractData.amount), + contractTermsAmount: instructedAmount, depositFeeLimit: Amounts.parseOrThrow(contractData.maxDepositFee), wireFeeAmortization: contractData.wireFeeAmortization ?? 1, wireFeeLimit: Amounts.parseOrThrow(contractData.maxWireFee), @@ -1327,7 +1319,7 @@ async function checkPaymentByProposalId( transactionId, proposalId: proposal.proposalId, amountEffective: Amounts.stringify(totalCost), - amountRaw: Amounts.stringify(res.coinSel.paymentAmount), + amountRaw: Amounts.stringify(instructedAmount), contractTermsHash: d.contractData.contractTermsHash, talerUri, }; @@ -1599,7 +1591,7 @@ export async function preparePayForTemplate( */ export async function generateDepositPermissions( wex: WalletExecutionContext, - payCoinSel: PayCoinSelection, + payCoinSel: DbCoinSelection, contractData: WalletContractData, ): Promise { const depositPermissions: CoinDepositPermission[] = []; @@ -1608,7 +1600,7 @@ export async function generateDepositPermissions( denom: DenominationRecord; }> = []; await wex.db.runReadOnlyTx(["coins", "denominations"], async (tx) => { - for (let i = 0; i < payCoinSel.coinPubs.length; i++) { + for (let i = 0; i < payCoinSel.coinContributions.length; i++) { const coin = await tx.coins.get(payCoinSel.coinPubs[i]); if (!coin) { throw Error("can't pay, allocated coin not found anymore"); @@ -1626,7 +1618,7 @@ export async function generateDepositPermissions( } }); - for (let i = 0; i < payCoinSel.coinPubs.length; i++) { + for (let i = 0; i < payCoinSel.coinContributions.length; i++) { const { coin, denom } = coinWithDenom[i]; let wireInfoHash: string; wireInfoHash = contractData.wireInfoHash; @@ -1881,7 +1873,10 @@ export async function confirmPay( case PurchaseStatus.DialogShared: case PurchaseStatus.DialogProposed: p.payInfo = { - payCoinSelection: coinSelection, + payCoinSelection: { + coinContributions: coinSelection.coins.map((x) => x.contribution), + coinPubs: coinSelection.coins.map((x) => x.coinPub), + }, payCoinSelectionUid: encodeCrock(getRandomBytes(16)), totalPayCost: Amounts.stringify(payCostInfo), }; @@ -1895,9 +1890,9 @@ export async function confirmPay( tag: TransactionType.Payment, proposalId: proposalId, }), - coinPubs: coinSelection.coinPubs, - contributions: coinSelection.coinContributions.map((x) => - Amounts.parseOrThrow(x), + coinPubs: coinSelection.coins.map((x) => x.coinPub), + contributions: coinSelection.coins.map((x) => + Amounts.parseOrThrow(x.contribution), ), refreshReason: RefreshReason.PayMerchant, }); diff --git a/packages/taler-wallet-core/src/pay-peer-common.ts b/packages/taler-wallet-core/src/pay-peer-common.ts index ff035d5e5..599010c1d 100644 --- a/packages/taler-wallet-core/src/pay-peer-common.ts +++ b/packages/taler-wallet-core/src/pay-peer-common.ts @@ -22,6 +22,7 @@ import { AmountString, Amounts, Codec, + SelectedCoin, TalerProtocolTimestamp, buildCodecForObject, checkDbInvariant, @@ -29,9 +30,8 @@ import { codecForTimestamp, codecOptional, } from "@gnu-taler/taler-util"; -import type { SelectedPeerCoin } from "./coinSelection.js"; import { SpendCoinDetails } from "./crypto/cryptoImplementation.js"; -import { PeerPushPaymentCoinSelection, ReserveRecord } from "./db.js"; +import { DbPeerPushPaymentCoinSelection, ReserveRecord } from "./db.js"; import { getTotalRefreshCost } from "./refresh.js"; import { WalletExecutionContext, getDenomInfo } from "./wallet.js"; import { getCandidateWithdrawalDenomsTx } from "./withdraw.js"; @@ -41,7 +41,7 @@ import { getCandidateWithdrawalDenomsTx } from "./withdraw.js"; */ export async function queryCoinInfosForSelection( wex: WalletExecutionContext, - csel: PeerPushPaymentCoinSelection, + csel: DbPeerPushPaymentCoinSelection, ): Promise { let infos: SpendCoinDetails[] = []; await wex.db.runReadOnlyTx(["coins", "denominations"], async (tx) => { @@ -74,7 +74,7 @@ export async function queryCoinInfosForSelection( export async function getTotalPeerPaymentCost( wex: WalletExecutionContext, - pcs: SelectedPeerCoin[], + pcs: SelectedCoin[], ): Promise { const currency = Amounts.currencyOf(pcs[0].contribution); return wex.db.runReadOnlyTx(["coins", "denominations"], async (tx) => { diff --git a/packages/taler-wallet-core/src/pay-peer-pull-debit.ts b/packages/taler-wallet-core/src/pay-peer-pull-debit.ts index 2418f08da..0ccca82a2 100644 --- a/packages/taler-wallet-core/src/pay-peer-pull-debit.ts +++ b/packages/taler-wallet-core/src/pay-peer-pull-debit.ts @@ -64,7 +64,7 @@ import { readSuccessResponseJsonOrThrow, readTalerErrorResponse, } from "@gnu-taler/taler-util/http"; -import { PeerCoinRepair, selectPeerCoins } from "./coinSelection.js"; +import { PreviousPayCoins, selectPeerCoins } from "./coinSelection.js"; import { PendingTaskType, TaskIdStr, @@ -358,16 +358,14 @@ async function handlePurseCreationConflict( throw Error("invalid state (coin selection expected)"); } - const repair: PeerCoinRepair = { - coinPubs: [], - contribs: [], - exchangeBaseUrl: peerPullInc.exchangeBaseUrl, - }; + const repair: PreviousPayCoins = []; for (let i = 0; i < sel.coinPubs.length; i++) { if (sel.coinPubs[i] != brokenCoinPub) { - repair.coinPubs.push(sel.coinPubs[i]); - repair.contribs.push(Amounts.parseOrThrow(sel.contributions[i])); + repair.push({ + coinPub: sel.coinPubs[i], + contribution: Amounts.parseOrThrow(sel.contributions[i]), + }); } } diff --git a/packages/taler-wallet-core/src/pay-peer-push-debit.ts b/packages/taler-wallet-core/src/pay-peer-push-debit.ts index b621b9e0e..cf4e7b619 100644 --- a/packages/taler-wallet-core/src/pay-peer-push-debit.ts +++ b/packages/taler-wallet-core/src/pay-peer-push-debit.ts @@ -48,7 +48,7 @@ import { readSuccessResponseJsonOrThrow, readTalerErrorResponse, } from "@gnu-taler/taler-util/http"; -import { PeerCoinRepair, selectPeerCoins } from "./coinSelection.js"; +import { PreviousPayCoins, selectPeerCoins } from "./coinSelection.js"; import { PendingTaskType, TaskIdStr, @@ -391,16 +391,14 @@ async function handlePurseCreationConflict( const instructedAmount = Amounts.parseOrThrow(peerPushInitiation.amount); const sel = peerPushInitiation.coinSel; - const repair: PeerCoinRepair = { - coinPubs: [], - contribs: [], - exchangeBaseUrl: peerPushInitiation.exchangeBaseUrl, - }; + const repair: PreviousPayCoins = []; for (let i = 0; i < sel.coinPubs.length; i++) { if (sel.coinPubs[i] != brokenCoinPub) { - repair.coinPubs.push(sel.coinPubs[i]); - repair.contribs.push(Amounts.parseOrThrow(sel.contributions[i])); + repair.push({ + coinPub: sel.coinPubs[i], + contribution: Amounts.parseOrThrow(sel.contributions[i]), + }); } } diff --git a/packages/taler-wallet-core/src/testing.ts b/packages/taler-wallet-core/src/testing.ts index 45a29a6e3..795f963d0 100644 --- a/packages/taler-wallet-core/src/testing.ts +++ b/packages/taler-wallet-core/src/testing.ts @@ -77,7 +77,7 @@ import { import { initiatePeerPushDebit } from "./pay-peer-push-debit.js"; import { getRefreshesForTransaction } from "./refresh.js"; import { getTransactionById, getTransactions } from "./transactions.js"; -import type { InternalWalletState, WalletExecutionContext } from "./wallet.js"; +import type { WalletExecutionContext } from "./wallet.js"; import { acceptWithdrawalFromUri } from "./withdraw.js"; const logger = new Logger("operations/testing.ts"); @@ -883,7 +883,11 @@ export async function testPay( "taler://fulfillment-success/thank+you", ); logger.trace("created new order with order ID", orderResp.orderId); - const checkPayResp = await checkPayment(wex.http, merchant, orderResp.orderId); + const checkPayResp = await checkPayment( + wex.http, + merchant, + orderResp.orderId, + ); const talerPayUri = checkPayResp.taler_pay_uri; if (!talerPayUri) { console.error("fatal: no taler pay URI received from backend"); @@ -908,6 +912,6 @@ export async function testPay( }); checkLogicInvariant(!!purchase); return { - payCoinSelection: purchase.payInfo?.payCoinSelection!, + numCoins: purchase.payInfo?.payCoinSelection.coinContributions.length ?? 0, }; } diff --git a/packages/taler-wallet-webextension/src/components/PaymentButtons.tsx b/packages/taler-wallet-webextension/src/components/PaymentButtons.tsx index 8cb1c49dd..731bcfed9 100644 --- a/packages/taler-wallet-webextension/src/components/PaymentButtons.tsx +++ b/packages/taler-wallet-webextension/src/components/PaymentButtons.tsx @@ -22,20 +22,19 @@ import { PreparePayResultType, TranslatedString, parsePayUri, - stringifyPayUri, } from "@gnu-taler/taler-util"; -import { Fragment, h, VNode } from "preact"; +import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; +import { useBackendContext } from "../context/backend.js"; +import { Button } from "../mui/Button.js"; +import { ButtonHandler } from "../mui/handlers.js"; +import { assertUnreachable } from "../utils/index.js"; import { Amount } from "./Amount.js"; import { Part } from "./Part.js"; import { QR } from "./QR.js"; import { LinkSuccess, WarningBox } from "./styled/index.js"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Button } from "../mui/Button.js"; -import { ButtonHandler } from "../mui/handlers.js"; -import { assertUnreachable } from "../utils/index.js"; -import { useBackendContext } from "../context/backend.js"; -import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; interface Props { payStatus: PreparePayResult; @@ -118,7 +117,12 @@ export function PaymentButtons({ } case "fee-gap": { BalanceMessage = i18n.str`Balance looks like it should be enough, but doesn't cover all fees requested by the merchant and payment processor. Please ensure there is at least ${Amounts.stringifyValue( - payStatus.balanceDetails.feeGapEstimate, + Amounts.stringify( + Amounts.sub( + amount, + payStatus.balanceDetails.maxEffectiveSpendAmount, + ).amount, + ), )} ${ amount.currency } more balance in your wallet or ask your merchant to cover more of the fees.`; diff --git a/packages/taler-wallet-webextension/src/cta/Payment/stories.tsx b/packages/taler-wallet-webextension/src/cta/Payment/stories.tsx index a3f00f164..567b5c177 100644 --- a/packages/taler-wallet-webextension/src/cta/Payment/stories.tsx +++ b/packages/taler-wallet-webextension/src/cta/Payment/stories.tsx @@ -59,7 +59,7 @@ export const NoEnoughBalanceAvailable = tests.createExample(BaseView, { balanceAgeAcceptable: "USD:9" as AmountString, balanceMerchantAcceptable: "USD:9" as AmountString, balanceMerchantDepositable: "USD:9" as AmountString, - feeGapEstimate: "USD:1" as AmountString, + maxEffectiveSpendAmount: "USD:9.5" as AmountString, perExchange: {}, }, talerUri: "taler://pay/..", @@ -100,7 +100,7 @@ export const NoEnoughBalanceMaterial = tests.createExample(BaseView, { balanceAgeAcceptable: "USD:9" as AmountString, balanceMerchantAcceptable: "USD:9" as AmountString, balanceMerchantDepositable: "USD:0" as AmountString, - feeGapEstimate: "USD:1" as AmountString, + maxEffectiveSpendAmount: "USD:9.5" as AmountString, perExchange: {}, }, talerUri: "taler://pay/..", @@ -141,7 +141,7 @@ export const NoEnoughBalanceAgeAcceptable = tests.createExample(BaseView, { balanceAgeAcceptable: "USD:9" as AmountString, balanceMerchantAcceptable: "USD:9" as AmountString, balanceMerchantDepositable: "USD:9" as AmountString, - feeGapEstimate: "USD:1" as AmountString, + maxEffectiveSpendAmount: "USD:9.5" as AmountString, perExchange: {}, }, talerUri: "taler://pay/..", @@ -183,7 +183,7 @@ export const NoEnoughBalanceMerchantAcceptable = tests.createExample(BaseView, { balanceAgeAcceptable: "USD:10" as AmountString, balanceMerchantAcceptable: "USD:9" as AmountString, balanceMerchantDepositable: "USD:9" as AmountString, - feeGapEstimate: "USD:1" as AmountString, + maxEffectiveSpendAmount: "USD:9.5" as AmountString, perExchange: {}, }, talerUri: "taler://pay/..", @@ -226,7 +226,7 @@ export const NoEnoughBalanceMerchantDepositable = tests.createExample( balanceAgeAcceptable: "USD:10" as AmountString, balanceMerchantAcceptable: "USD:10" as AmountString, balanceMerchantDepositable: "USD:9" as AmountString, - feeGapEstimate: "USD:1" as AmountString, + maxEffectiveSpendAmount: "USD:9.5" as AmountString, perExchange: {}, }, talerUri: "taler://pay/..", @@ -268,7 +268,7 @@ export const NoEnoughBalanceFeeGap = tests.createExample(BaseView, { balanceAgeAcceptable: "USD:10" as AmountString, balanceMerchantAcceptable: "USD:10" as AmountString, balanceMerchantDepositable: "USD:10" as AmountString, - feeGapEstimate: "USD:1" as AmountString, + maxEffectiveSpendAmount: "USD:9.5" as AmountString, perExchange: {}, }, talerUri: "taler://pay/..", diff --git a/packages/taler-wallet-webextension/src/cta/TransferCreate/state.ts b/packages/taler-wallet-webextension/src/cta/TransferCreate/state.ts index d3cbc66a0..f092801ed 100644 --- a/packages/taler-wallet-webextension/src/cta/TransferCreate/state.ts +++ b/packages/taler-wallet-webextension/src/cta/TransferCreate/state.ts @@ -17,19 +17,18 @@ import { AmountString, Amounts, - TalerError, TalerErrorCode, TalerProtocolTimestamp, } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { isFuture, parse } from "date-fns"; -import { useEffect, useState } from "preact/hooks"; +import { useState } from "preact/hooks"; import { alertFromError, useAlertContext } from "../../context/alert.js"; import { useBackendContext } from "../../context/backend.js"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js"; -import { Props, State } from "./index.js"; import { BackgroundError, WxApiType } from "../../wxApi.js"; +import { Props, State } from "./index.js"; export function useComponentState({ amount: amountStr, @@ -164,11 +163,14 @@ async function checkPeerPushDebitAndCheckMax( const material = Amounts.parseOrThrow( e.errorDetail.insufficientBalanceDetails.balanceMaterial, ); - const gap = Amounts.parseOrThrow( - e.errorDetail.insufficientBalanceDetails.feeGapEstimate, - ); - const newAmount = Amounts.sub(material, gap).amount; const amount = Amounts.parseOrThrow(amountState); + const gap = Amounts.sub( + amount, + Amounts.parseOrThrow( + e.errorDetail.insufficientBalanceDetails.maxEffectiveSpendAmount, + ), + ).amount; + const newAmount = Amounts.sub(material, gap).amount; if (Amounts.cmp(newAmount, amount) === 0) { //insufficient balance and the exception didn't give //a good response that allow us to try again -- cgit v1.2.3