summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Dold <florian@dold.me>2024-03-06 21:15:30 +0100
committerFlorian Dold <florian@dold.me>2024-03-07 00:03:59 +0100
commit7ba1d1f3351e58a331e99337afea0fbedb6eb828 (patch)
tree60b7a485cd317c1fe55276acdc0e055cd9353bfb
parent618caa117111b9fed6a792b6816fc724483eb349 (diff)
downloadwallet-core-7ba1d1f3351e58a331e99337afea0fbedb6eb828.tar.gz
wallet-core-7ba1d1f3351e58a331e99337afea0fbedb6eb828.tar.bz2
wallet-core-7ba1d1f3351e58a331e99337afea0fbedb6eb828.zip
refactor coin selection, report maxEffectiveSpendAmount
-rw-r--r--packages/taler-harness/src/integrationtests/test-forced-selection.ts2
-rw-r--r--packages/taler-util/src/wallet-types.ts39
-rw-r--r--packages/taler-wallet-core/src/coinSelection.test.ts2
-rw-r--r--packages/taler-wallet-core/src/coinSelection.ts366
-rw-r--r--packages/taler-wallet-core/src/db.ts14
-rw-r--r--packages/taler-wallet-core/src/deposits.ts31
-rw-r--r--packages/taler-wallet-core/src/instructedAmountConversion.test.ts4
-rw-r--r--packages/taler-wallet-core/src/instructedAmountConversion.ts17
-rw-r--r--packages/taler-wallet-core/src/pay-merchant.ts63
-rw-r--r--packages/taler-wallet-core/src/pay-peer-common.ts8
-rw-r--r--packages/taler-wallet-core/src/pay-peer-pull-debit.ts14
-rw-r--r--packages/taler-wallet-core/src/pay-peer-push-debit.ts14
-rw-r--r--packages/taler-wallet-core/src/testing.ts10
-rw-r--r--packages/taler-wallet-webextension/src/components/PaymentButtons.tsx22
-rw-r--r--packages/taler-wallet-webextension/src/cta/Payment/stories.tsx12
-rw-r--r--packages/taler-wallet-webextension/src/cta/TransferCreate/state.ts18
16 files changed, 307 insertions, 329 deletions
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<SelectPayCoinsResult> {
- 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<string, AmountJson>;
+ },
+): Promise<void> {
+ // 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<PayCoinSelection> {
@@ -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<PayMerchantInsufficientBalanceDetails> {
@@ -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<PeerCoinSelectionDetails> {
+): Promise<TalerProtocolTimestamp> {
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<ResCoin[]> {
- 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<string, DepositInfoPerExchange> = {};
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<AmountJson> {
- 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<CoinDepositPermission[]> {
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<SpendCoinDetails[]> {
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<AmountJson> {
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