commit b9ded40e6241eb3bbf706b40a9b8efbc52c13f2f
parent 3e909773dd1788b0d10f0745b213e8211f1c6b15
Author: Florian Dold <florian@dold.me>
Date: Mon, 15 Jun 2026 20:08:18 +0200
wallet-core: remove support for prospective coin selections
Diffstat:
7 files changed, 145 insertions(+), 158 deletions(-)
diff --git a/packages/taler-util/src/errors.ts b/packages/taler-util/src/errors.ts
@@ -155,6 +155,9 @@ export interface DetailsMap {
[TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE]: {
insufficientBalanceDetails: PaymentInsufficientBalanceDetails;
};
+ [TalerErrorCode.WALLET_PAY_MERCHANT_INSUFFICIENT_BALANCE]: {
+ insufficientBalanceDetails: PaymentInsufficientBalanceDetails;
+ };
[TalerErrorCode.WALLET_REFRESH_GROUP_INCOMPLETE]: {
numErrors: number;
/**
@@ -189,7 +192,7 @@ export interface DetailsMap {
[TalerErrorCode.GENERIC_CLIENT_UNSUPPORTED_PROTOCOL_VERSION]: {
client: string;
server: string;
- }
+ };
}
type ErrBody<Y> = Y extends keyof DetailsMap ? DetailsMap[Y] : empty;
diff --git a/packages/taler-util/src/taler-error-codes.ts b/packages/taler-util/src/taler-error-codes.ts
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- Copyright (C) 2012-2026 Taler Systems SA
+ Copyright (C) 2012-2020 Taler Systems SA
GNU Taler is free software: you can redistribute it and/or modify it
under the terms of the GNU Lesser General Public License as published
@@ -1697,6 +1697,14 @@ export enum TalerErrorCode {
/**
+ * A coin signature for a deposit to pay to open the reserve is invalid.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_RESERVES_OPEN_COIN_SIGNATURE_INVALID = 1790,
+
+
+ /**
* The auditor that was supposed to be disabled is unknown to this exchange.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
* (A value of 0 indicates that the error is generated client-side).
@@ -2081,6 +2089,14 @@ export enum TalerErrorCode {
/**
+ * The client submitted the wrong form for the request. This is some invalid use of the API. Please contact technical support.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_KYC_INVALID_FORM_SUBMITTED = 1917,
+
+
+ /**
* The exchange tried to run an AML program, but that program did not terminate on time. Contact the exchange operator to address the AML program bug or performance issue. If it is not a performance issue, the timeout might have to be increased (requires changes to the source code).
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
* (A value of 0 indicates that the error is generated client-side).
@@ -5041,6 +5057,14 @@ export enum TalerErrorCode {
/**
+ * The wallet's balance for paying a merchant is insufficient.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WALLET_PAY_MERCHANT_INSUFFICIENT_BALANCE = 7050,
+
+
+ /**
* 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).
@@ -5673,6 +5697,30 @@ export enum TalerErrorCode {
/**
+ * The donation unit has expired and cannot be used any longer.
+ * Returned with an HTTP status code of #MHD_HTTP_GONE (410).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ DONAU_GENERIC_DONATION_UNIT_EXPIRED = 8619,
+
+
+ /**
+ * The donation unit is not yet valid. The client should repeat the request in the future when it might succeed.
+ * Returned with an HTTP status code of #MHD_HTTP_TOO_EARLY (425).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ DONAU_GENERIC_DONATION_UNIT_TOO_EARLY = 8620,
+
+
+ /**
+ * The donation unit is not valid for the year specified by the client.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ DONAU_GENERIC_DONATION_UNIT_WRONG_YEAR = 8621,
+
+
+ /**
* A generic error happened in the LibEuFin nexus. See the enclose details JSON for more information.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
* (A value of 0 indicates that the error is generated client-side).
@@ -5730,7 +5778,7 @@ export enum TalerErrorCode {
/**
* The client is not authorized to use the given redirect URI.
- * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * Returned with an HTTP status code of #MHD_HTTP_UNAUTHORIZED (401).
* (A value of 0 indicates that the error is generated client-side).
*/
CHALLENGER_GENERIC_CLIENT_FORBIDDEN_BAD_REDIRECT_URI = 9751,
@@ -5738,7 +5786,7 @@ export enum TalerErrorCode {
/**
* The service failed to execute its helper process to send the challenge.
- * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
* (A value of 0 indicates that the error is generated client-side).
*/
CHALLENGER_HELPER_EXEC_FAILED = 9752,
@@ -5746,7 +5794,7 @@ export enum TalerErrorCode {
/**
* The grant is unknown to the service (it could also have expired).
- * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * Returned with an HTTP status code of #MHD_HTTP_UNAUTHORIZED (401).
* (A value of 0 indicates that the error is generated client-side).
*/
CHALLENGER_GRANT_UNKNOWN = 9753,
@@ -5754,7 +5802,7 @@ export enum TalerErrorCode {
/**
* The code given is not even well-formed.
- * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * Returned with an HTTP status code of #MHD_HTTP_UNAUTHORIZED (401).
* (A value of 0 indicates that the error is generated client-side).
*/
CHALLENGER_CLIENT_FORBIDDEN_BAD_CODE = 9754,
@@ -5762,7 +5810,7 @@ export enum TalerErrorCode {
/**
* The service is not aware of the referenced validation process.
- * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * Returned with an HTTP status code of #MHD_HTTP_UNAUTHORIZED (401).
* (A value of 0 indicates that the error is generated client-side).
*/
CHALLENGER_GENERIC_VALIDATION_UNKNOWN = 9755,
@@ -5809,6 +5857,78 @@ export enum TalerErrorCode {
/**
+ * The request is invalid as the specified order is unpaid. The client should only call this endpoint after paying the order.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ PAIVANA_PAYMENT_MISSING = 9800,
+
+
+ /**
+ * The merchant backend refused our request to check the payment status. The backend is either down or Paivana is configured badly. The system administrator should check the logs.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ PAIVANA_BACKEND_REFUSED = 9801,
+
+
+ /**
+ * The order specified in the request is unknown to the backend. The client must instantiate the Paivana template first and pay the order before calling this endpoint.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ PAIVANA_ORDER_UNKNOWN = 9802,
+
+
+ /**
+ * The merchant backend returned an unexpected status code. This is probably a protocol incompatibility that should be reported to the developers.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ PAIVANA_BACKEND_ERROR = 9803,
+
+
+ /**
+ * Paivana failed to initialize the request to check the payment status. This should never happen. The system administrator should consult the logs.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ PAIVANA_GET_ORDER_FAILED = 9804,
+
+
+ /**
+ * The specified order does not match the specified Website or nonce and thus the order is invalid for the given request. Clients should pay for the correct order for access to the resource.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ PAIVANA_WRONG_ORDER = 9805,
+
+
+ /**
+ * The specified expiration time for the cookie is longer than what the purchase allows. The request may succeed with a shorter expiration time. Alternatively, the user may need to purchase access again.
+ * Returned with an HTTP status code of #MHD_HTTP_GONE (410).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ PAIVANA_TOO_LATE = 9806,
+
+
+ /**
+ * The payment template specified in the request is unknown to the backend.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ PAIVANA_TEMPLATE_UNKNOWN = 9807,
+
+
+ /**
+ * The paywall functionality is currently disabled. Thus, proofs of payment are unnecessary and also not supported.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_IMPLEMENTED (501).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ PAIVANA_PAYWALL_DISABLED = 9808,
+
+
+ /**
* End of error code range.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
* (A value of 0 indicates that the error is generated client-side).
diff --git a/packages/taler-wallet-core/src/coinSelection.ts b/packages/taler-wallet-core/src/coinSelection.ts
@@ -50,7 +50,6 @@ import {
parsePaytoUri,
PayCoinSelection,
PaymentInsufficientBalanceDetails,
- ProspectivePayCoinSelection,
ScopeInfo,
ScopeType,
SelectedCoin,
@@ -185,7 +184,6 @@ export type SelectPayCoinsResult =
type: "failure";
insufficientBalanceDetails: PaymentInsufficientBalanceDetails;
}
- | { type: "prospective"; result: ProspectivePayCoinSelection }
| { type: "success"; coinSel: PayCoinSelection };
async function internalSelectPayCoins(
@@ -304,34 +302,6 @@ export async function selectPayCoinsInTx(
const materialAvSel = await internalSelectPayCoins(wex, tx, req, false);
if (!materialAvSel) {
- const prospectiveAvSel = await internalSelectPayCoins(wex, tx, req, true);
-
- if (prospectiveAvSel) {
- const prospectiveCoins: SelectedProspectiveCoin[] = [];
- for (const avKey of Object.keys(prospectiveAvSel.sel)) {
- const mySel = prospectiveAvSel.sel[avKey];
- for (const contrib of mySel.contributions) {
- prospectiveCoins.push({
- denomPubHash: mySel.denomPubHash,
- contribution: Amounts.stringify(contrib),
- exchangeBaseUrl: mySel.exchangeBaseUrl,
- });
- }
- }
- return {
- type: "prospective",
- result: {
- prospectiveCoins,
- customerDepositFees: Amounts.stringify(
- prospectiveAvSel.tally.customerDepositFees,
- ),
- customerWireFees: Amounts.stringify(
- prospectiveAvSel.tally.customerWireFees,
- ),
- },
- } satisfies SelectPayCoinsResult;
- }
-
return {
type: "failure",
insufficientBalanceDetails: await reportInsufficientBalanceDetails(
@@ -1133,25 +1103,8 @@ export interface PeerCoinSelectionDetails {
maxExpirationDate: TalerProtocolTimestamp;
}
-export interface ProspectivePeerCoinSelectionDetails {
- exchangeBaseUrl: string;
-
- prospectiveCoins: SelectedProspectiveCoin[];
-
- /**
- * How much of the deposit fees is the customer paying?
- */
- customerDepositFees: AmountJson;
-
- totalDepositFees: AmountJson;
-
- maxExpirationDate: TalerProtocolTimestamp;
-}
-
export type SelectPeerCoinsResult =
| { type: "success"; result: PeerCoinSelectionDetails }
- // Successful, but using coins that are not materially available yet.
- | { type: "prospective"; result: ProspectivePeerCoinSelectionDetails }
| {
type: "failure";
insufficientBalanceDetails: PaymentInsufficientBalanceDetails;
@@ -1345,44 +1298,7 @@ export async function selectPeerCoinsInTx(
const avRes = await internalSelectPeerCoins(wex, tx, req, exchWire, false);
- if (!avRes) {
- // Try to see if we can do a prospective selection
- const prospectiveAvRes = await internalSelectPeerCoins(
- wex,
- tx,
- req,
- exchWire,
- true,
- );
- if (prospectiveAvRes) {
- const prospectiveCoins: SelectedProspectiveCoin[] = [];
- for (const avKey of Object.keys(prospectiveAvRes.sel)) {
- const mySel = prospectiveAvRes.sel[avKey];
- for (const contrib of mySel.contributions) {
- prospectiveCoins.push({
- denomPubHash: mySel.denomPubHash,
- contribution: Amounts.stringify(contrib),
- exchangeBaseUrl: mySel.exchangeBaseUrl,
- });
- }
- }
- const maxExpirationDate = await computeCoinSelMaxExpirationDate(
- wex,
- tx,
- prospectiveAvRes.sel,
- );
- return {
- type: "prospective",
- result: {
- prospectiveCoins,
- customerDepositFees: prospectiveAvRes.tally.customerDepositFees,
- totalDepositFees: prospectiveAvRes.tally.totalDepositFees,
- exchangeBaseUrl: exch.baseUrl,
- maxExpirationDate,
- },
- };
- }
- } else if (avRes) {
+ if (avRes) {
const r = await assembleSelectPayCoinsSuccessResult(
tx,
avRes.sel,
diff --git a/packages/taler-wallet-core/src/deposits.ts b/packages/taler-wallet-core/src/deposits.ts
@@ -1588,9 +1588,6 @@ async function doCoinSelection(
insufficientBalanceDetails: payCoinSel.insufficientBalanceDetails,
},
);
- case "prospective":
- logger.info("coin selection prospective");
- throw Error("insufficient balance (waiting on pending refresh)");
default:
assertUnreachable(payCoinSel);
}
@@ -2007,9 +2004,6 @@ export async function internalCheckDepositGroup(
insufficientBalanceDetails: payCoinSel.insufficientBalanceDetails,
},
);
- case "prospective":
- selCoins = payCoinSel.result.prospectiveCoins;
- break;
case "success":
selCoins = payCoinSel.coinSel.coins;
break;
@@ -2106,9 +2100,6 @@ export async function createDepositGroup(
insufficientBalanceDetails: payCoinSel.insufficientBalanceDetails,
},
);
- case "prospective":
- coins = payCoinSel.result.prospectiveCoins;
- break;
default:
assertUnreachable(payCoinSel);
}
diff --git a/packages/taler-wallet-core/src/pay-merchant.ts b/packages/taler-wallet-core/src/pay-merchant.ts
@@ -1683,8 +1683,6 @@ async function reselectCoinsTx(
case "failure":
logger.trace("insufficient funds for coin re-selection");
return;
- case "prospective":
- return;
case "success":
break;
default:
@@ -1996,9 +1994,6 @@ async function checkPaymentByProposalId(
balanceDetails: res.insufficientBalanceDetails,
};
}
- case "prospective":
- coins = res.result.prospectiveCoins;
- break;
case "success":
coins = res.coinSel.coins;
break;
@@ -2706,18 +2701,6 @@ export async function getChoicesForPayment(
});
break;
}
- case "prospective": {
- amountEffective = await getTotalPaymentCostInTx(
- wex,
- tx,
- currency,
- selectCoinsResult.result.prospectiveCoins,
- );
- selectCoinsResult.result.prospectiveCoins.forEach((x) => {
- exchanges.add(x.exchangeBaseUrl);
- });
- break;
- }
case "failure": {
logger.info("choice not payable, insufficient coins");
balanceDetails = selectCoinsResult.insufficientBalanceDetails;
@@ -3020,15 +3003,14 @@ export async function confirmPay(
switch (selectCoinsResult.type) {
case "failure": {
- // Should not happen, since checkPay should be called first
- // FIXME: Actually, this should be handled gracefully,
- // and the status should be stored in the DB.
logger.warn("not confirming payment, insufficient coins");
- throw Error("insufficient balance");
- }
- case "prospective": {
- coins = selectCoinsResult.result.prospectiveCoins;
- break;
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_PAY_MERCHANT_INSUFFICIENT_BALANCE,
+ {
+ insufficientBalanceDetails:
+ selectCoinsResult.insufficientBalanceDetails,
+ },
+ );
}
case "success":
coins = selectCoinsResult.coinSel.coins;
@@ -3337,14 +3319,13 @@ async function processPurchasePay(
});
switch (selectCoinsResult.type) {
case "failure": {
- // Should not happen, since checkPay should be called first
- // FIXME: Actually, this should be handled gracefully,
- // and the status should be stored in the DB.
- logger.warn("not confirming payment, insufficient coins");
- throw Error("insufficient balance");
- }
- case "prospective": {
- throw Error("insufficient balance (pending refresh)");
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_PAY_MERCHANT_INSUFFICIENT_BALANCE,
+ {
+ insufficientBalanceDetails:
+ selectCoinsResult.insufficientBalanceDetails,
+ },
+ );
}
case "success":
break;
diff --git a/packages/taler-wallet-core/src/pay-peer-pull-debit.ts b/packages/taler-wallet-core/src/pay-peer-pull-debit.ts
@@ -403,10 +403,6 @@ async function handlePurseCreationConflict(
throw Error(
"insufficient balance to re-select coins to repair double spending",
);
- case "prospective":
- throw Error(
- "insufficient balance to re-select coins to repair double spending (blocked on refresh)",
- );
case "success":
break;
default:
@@ -538,10 +534,6 @@ async function processPeerPullDebitPendingDeposit(
insufficientBalanceDetails: coinSelRes.insufficientBalanceDetails,
},
);
- case "prospective":
- // Coin selection is *still* only prospective!
- // FIXME: Really report this as error?
- throw Error("insufficient balance (locked behind refresh)");
case "success":
coins = coinSelRes.result.coins;
break;
@@ -770,9 +762,6 @@ export async function confirmPeerPullDebit(
insufficientBalanceDetails: coinSelRes.insufficientBalanceDetails,
},
);
- case "prospective":
- coins = coinSelRes.result.prospectiveCoins;
- break;
case "success":
coins = coinSelRes.result.coins;
break;
@@ -987,9 +976,6 @@ export async function preparePeerPullDebit(
insufficientBalanceDetails: coinSelRes.insufficientBalanceDetails,
},
);
- case "prospective":
- coins = coinSelRes.result.prospectiveCoins;
- break;
case "success":
coins = coinSelRes.result.coins;
break;
diff --git a/packages/taler-wallet-core/src/pay-peer-push-debit.ts b/packages/taler-wallet-core/src/pay-peer-push-debit.ts
@@ -481,9 +481,6 @@ async function internalCheckPeerPushDebit(
type: "insufficient-balance",
insufficientBalanceDetails: coinSelRes.insufficientBalanceDetails,
};
- case "prospective":
- coins = coinSelRes.result.prospectiveCoins;
- break;
case "success":
coins = coinSelRes.result.coins;
break;
@@ -572,8 +569,6 @@ async function handlePurseCreationConflict(
switch (coinSelRes.type) {
case "failure":
- case "prospective":
- // FIXME: Details!
throw Error(
"insufficient balance to re-select coins to repair double spending",
);
@@ -645,8 +640,6 @@ async function processPeerPushDebitCreateReserve(
insufficientBalanceDetails: coinSelRes.insufficientBalanceDetails,
},
);
- case "prospective":
- throw Error("insufficient funds (blocked on refresh)");
case "success":
break;
default:
@@ -1106,9 +1099,6 @@ export async function initiatePeerPushDebit(
insufficientBalanceDetails: coinSelRes.insufficientBalanceDetails,
},
);
- case "prospective":
- coins = coinSelRes.result.prospectiveCoins;
- break;
case "success":
coins = coinSelRes.result.coins;
break;