commit fa530bdc65f7eb86796259314b6cb9455177cf32
parent 4adadc3477601be5548004a48f260d14b3ace069
Author: Antoine A <>
Date: Tue, 18 Feb 2025 10:52:01 +0100
common: wg /transfer wtid reuse error
Diffstat:
9 files changed, 88 insertions(+), 31 deletions(-)
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/api/WireGatewayApi.kt b/bank/src/main/kotlin/tech/libeufin/bank/api/WireGatewayApi.kt
@@ -56,17 +56,21 @@ fun Routing.wireGatewayApi(db: Database, cfg: BankConfig) {
timestamp = Instant.now()
)
when (res) {
- is TransferResult.UnknownExchange -> throw unknownAccount(call.username)
- is TransferResult.NotAnExchange -> throw notExchange(call.username)
- is TransferResult.BothPartyAreExchange -> throw conflict(
+ TransferResult.UnknownExchange -> throw unknownAccount(call.username)
+ TransferResult.NotAnExchange -> throw notExchange(call.username)
+ TransferResult.BothPartyAreExchange -> throw conflict(
"Wire transfer attempted with credit and debit party being both exchange account",
TalerErrorCode.BANK_ACCOUNT_IS_EXCHANGE
)
- is TransferResult.ReserveUidReuse -> throw conflict(
+ TransferResult.ReserveUidReuse -> throw conflict(
"request_uid used already",
TalerErrorCode.BANK_TRANSFER_REQUEST_UID_REUSED
)
- is TransferResult.BalanceInsufficient -> throw conflict(
+ TransferResult.WtidReuse -> throw conflict(
+ "wtid used already",
+ TalerErrorCode.BANK_TRANSFER_WTID_REUSED
+ )
+ TransferResult.BalanceInsufficient -> throw conflict(
"Insufficient balance for exchange",
TalerErrorCode.BANK_UNALLOWED_DEBIT
)
@@ -164,21 +168,21 @@ fun Routing.wireGatewayApi(db: Database, cfg: BankConfig) {
metadata = metadata
)
when (res) {
- is AddIncomingResult.UnknownExchange -> throw unknownAccount(username)
- is AddIncomingResult.NotAnExchange -> throw notExchange(username)
- is AddIncomingResult.UnknownDebtor -> throw conflict(
+ AddIncomingResult.UnknownExchange -> throw unknownAccount(username)
+ AddIncomingResult.NotAnExchange -> throw notExchange(username)
+ AddIncomingResult.UnknownDebtor -> throw conflict(
"Debtor account $debitAccount was not found",
TalerErrorCode.BANK_UNKNOWN_DEBTOR
)
- is AddIncomingResult.BothPartyAreExchange -> throw conflict(
+ AddIncomingResult.BothPartyAreExchange -> throw conflict(
"Wire transfer attempted with credit and debit party being both exchange account",
TalerErrorCode.BANK_ACCOUNT_IS_EXCHANGE
)
- is AddIncomingResult.ReservePubReuse -> throw conflict(
+ AddIncomingResult.ReservePubReuse -> throw conflict(
"reserve_pub used already",
TalerErrorCode.BANK_DUPLICATE_RESERVE_PUB_SUBJECT
)
- is AddIncomingResult.BalanceInsufficient -> throw conflict(
+ AddIncomingResult.BalanceInsufficient -> throw conflict(
"Insufficient balance for debitor",
TalerErrorCode.BANK_UNALLOWED_DEBIT
)
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/ExchangeDAO.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/ExchangeDAO.kt
@@ -105,6 +105,7 @@ class ExchangeDAO(private val db: Database) {
data object BothPartyAreExchange: TransferResult
data object BalanceInsufficient: TransferResult
data object ReserveUidReuse: TransferResult
+ data object WtidReuse: TransferResult
}
/** Perform a Taler transfer */
@@ -119,6 +120,7 @@ class ExchangeDAO(private val db: Database) {
,out_debtor_not_exchange
,out_both_exchanges
,out_request_uid_reuse
+ ,out_wtid_reuse
,out_exchange_balance_insufficient
,out_tx_row_id
,out_timestamp
@@ -149,6 +151,7 @@ class ExchangeDAO(private val db: Database) {
it.getBoolean("out_both_exchanges") -> TransferResult.BothPartyAreExchange
it.getBoolean("out_exchange_balance_insufficient") -> TransferResult.BalanceInsufficient
it.getBoolean("out_request_uid_reuse") -> TransferResult.ReserveUidReuse
+ it.getBoolean("out_wtid_reuse") -> TransferResult.WtidReuse
else -> TransferResult.Success(
id = it.getLong("out_tx_row_id"),
timestamp = it.getTalerTimestamp("out_timestamp")
diff --git a/bank/src/test/kotlin/WireGatewayApiTest.kt b/bank/src/test/kotlin/WireGatewayApiTest.kt
@@ -68,6 +68,13 @@ class WireGatewayApiTest {
"exchange_base_url" to "http://different-exchange.example.com/"
}
}.assertConflict(TalerErrorCode.BANK_TRANSFER_REQUEST_UID_REUSED)
+
+ // Trigger conflict due to reused wtid
+ client.postA("/accounts/exchange/taler-wire-gateway/transfer") {
+ json(valid_req) {
+ "request_uid" to HashCode.rand()
+ }
+ }.assertConflict(TalerErrorCode.BANK_TRANSFER_WTID_REUSED)
// Currency mismatch
client.postA("/accounts/exchange/taler-wire-gateway/transfer") {
diff --git a/common/src/main/kotlin/TalerErrorCode.kt b/common/src/main/kotlin/TalerErrorCode.kt
@@ -96,20 +96,20 @@ enum class TalerErrorCode(val code: Int, val status: Int, val description: Strin
/** The body is too large to be permissible for the endpoint. If you believe this was a legitimate request, contact the server administrators and/or the software developers to increase the limit. */
GENERIC_UPLOAD_EXCEEDS_LIMIT(32, 413, "The body is too large to be permissible for the endpoint. If you believe this was a legitimate request, contact the server administrators and/or the software developers to increase the limit."),
- /** The service refused the request due to lack of proper authorization. */
- GENERIC_UNAUTHORIZED(40, 401, "The service refused the request due to lack of proper authorization."),
+ /** The service refused the request due to lack of proper authorization. Accessing this endpoint requires an access token from the account owner. */
+ GENERIC_UNAUTHORIZED(40, 401, "The service refused the request due to lack of proper authorization. Accessing this endpoint requires an access token from the account owner."),
- /** The service refused the request as the given authorization token is unknown. */
- GENERIC_TOKEN_UNKNOWN(41, 401, "The service refused the request as the given authorization token is unknown."),
+ /** The service refused the request as the given authorization token is unknown. You should request a valid access token from the account owner. */
+ GENERIC_TOKEN_UNKNOWN(41, 401, "The service refused the request as the given authorization token is unknown. You should request a valid access token from the account owner."),
- /** The service refused the request as the given authorization token expired. */
- GENERIC_TOKEN_EXPIRED(42, 401, "The service refused the request as the given authorization token expired."),
+ /** The service refused the request as the given authorization token expired. You should request a fresh authorization token from the account owner. */
+ GENERIC_TOKEN_EXPIRED(42, 401, "The service refused the request as the given authorization token expired. You should request a fresh authorization token from the account owner."),
- /** The service refused the request as the given authorization token is malformed. */
- GENERIC_TOKEN_MALFORMED(43, 401, "The service refused the request as the given authorization token is malformed."),
+ /** The service refused the request as the given authorization token is invalid or malformed. You should check that you have the right credentials. */
+ GENERIC_TOKEN_MALFORMED(43, 401, "The service refused the request as the given authorization token is invalid or malformed. You should check that you have the right credentials."),
- /** The service refused the request due to lack of proper rights on the resource. */
- GENERIC_FORBIDDEN(44, 403, "The service refused the request due to lack of proper rights on the resource."),
+ /** The service refused the request due to lack of proper rights on the resource. You may need different credentials to be allowed to perform this operation. */
+ GENERIC_FORBIDDEN(44, 403, "The service refused the request due to lack of proper rights on the resource. You may need different credentials to be allowed to perform this operation."),
/** The service failed initialize its connection to the database. The system administrator should check that the service has permissions to access the database and that the database is running. */
GENERIC_DB_SETUP_FAILED(50, 500, "The service failed initialize its connection to the database. The system administrator should check that the service has permissions to access the database and that the database is running."),
@@ -318,8 +318,8 @@ enum class TalerErrorCode(val code: Int, val status: Int, val description: Strin
/** The given reserve does not have sufficient funds to admit the requested withdraw operation at this time. The response includes the current \"balance\" of the reserve as well as the transaction \"history\" that lead to this balance. */
EXCHANGE_WITHDRAW_INSUFFICIENT_FUNDS(1150, 409, "The given reserve does not have sufficient funds to admit the requested withdraw operation at this time. The response includes the current \"balance\" of the reserve as well as the transaction \"history\" that lead to this balance."),
- /** The amount to withdraw or the fees exceeds the numeric range for Taler amounts. This is not a client failure, as coin values and fees come from the exchange's configuration. */
- EXCHANGE_WITHDRAW_AMOUNT_OVERFLOW(1151, 500, "The amount to withdraw or the fees exceeds the numeric range for Taler amounts. This is not a client failure, as coin values and fees come from the exchange's configuration."),
+ /** The given reserve does not have sufficient funds to admit the requested age-withdraw operation at this time. The response includes the current \"balance\" of the reserve as well as the transaction \"history\" that lead to this balance. */
+ EXCHANGE_AGE_WITHDRAW_INSUFFICIENT_FUNDS(1151, 409, "The given reserve does not have sufficient funds to admit the requested age-withdraw operation at this time. The response includes the current \"balance\" of the reserve as well as the transaction \"history\" that lead to this balance."),
/** The amount to withdraw together with the fee exceeds the numeric range for Taler amounts. This is not a client failure, as the coin value and fees come from the exchange's configuration. */
EXCHANGE_WITHDRAW_AMOUNT_FEE_OVERFLOW(1152, 500, "The amount to withdraw together with the fee exceeds the numeric range for Taler amounts. This is not a client failure, as the coin value and fees come from the exchange's configuration."),
@@ -342,11 +342,17 @@ enum class TalerErrorCode(val code: Int, val status: Int, val description: Strin
/** The client failed to unblind the blind signature. */
EXCHANGE_WITHDRAW_UNBLIND_FAILURE(1159, 0, "The client failed to unblind the blind signature."),
- /** The client re-used a withdraw nonce, which is not allowed. */
- EXCHANGE_WITHDRAW_NONCE_REUSE(1160, 409, "The client re-used a withdraw nonce, which is not allowed."),
+ /** The client reused a withdraw nonce, which is not allowed. */
+ EXCHANGE_WITHDRAW_NONCE_REUSE(1160, 409, "The client reused a withdraw nonce, which is not allowed."),
- /** The client provided an unknown commitment for a withdraw request. */
- EXCHANGE_WITHDRAW_COMMITMENT_UNKNOWN(1161, 400, "The client provided an unknown commitment for a withdraw request."),
+ /** The client provided an unknown commitment for an age-withdraw request. */
+ EXCHANGE_WITHDRAW_COMMITMENT_UNKNOWN(1161, 400, "The client provided an unknown commitment for an age-withdraw request."),
+
+ /** The total sum of amounts from the denominations did overflow. */
+ EXCHANGE_WITHDRAW_AMOUNT_OVERFLOW(1162, 500, "The total sum of amounts from the denominations did overflow."),
+
+ /** The total sum of value and fees from the denominations differs from the committed amount with fees. */
+ EXCHANGE_AGE_WITHDRAW_AMOUNT_INCORRECT(1163, 400, "The total sum of value and fees from the denominations differs from the committed amount with fees."),
/** The original commitment differs from the calculated hash */
EXCHANGE_WITHDRAW_REVEAL_INVALID_HASH(1164, 400, "The original commitment differs from the calculated hash"),
@@ -354,8 +360,8 @@ enum class TalerErrorCode(val code: Int, val status: Int, val description: Strin
/** The maximum age in the commitment is too large for the reserve */
EXCHANGE_WITHDRAW_MAXIMUM_AGE_TOO_LARGE(1165, 409, "The maximum age in the commitment is too large for the reserve"),
- /** The withdraw included a planchet that was already withdrawn. This is not allowed. */
- EXCHANGE_WITHDRAW_IDEMPOTENT_PLANCHET(1175, 409, "The withdraw included a planchet that was already withdrawn. This is not allowed."),
+ /** The batch withdraw included a planchet that was already withdrawn. This is not allowed. */
+ EXCHANGE_WITHDRAW_IDEMPOTENT_PLANCHET(1175, 409, "The batch withdraw included a planchet that was already withdrawn. This is not allowed."),
/** The signature made by the coin over the deposit permission is not valid. */
EXCHANGE_DEPOSIT_COIN_SIGNATURE_INVALID(1205, 403, "The signature made by the coin over the deposit permission is not valid."),
@@ -957,6 +963,9 @@ enum class TalerErrorCode(val code: Int, val status: Int, val description: Strin
/** The token family key is not known to the backend. Check the local system time on the client, maybe an expired (or not yet valid) token was used. */
MERCHANT_GENERIC_TOKEN_KEY_UNKNOWN(2027, 404, "The token family key is not known to the backend. Check the local system time on the client, maybe an expired (or not yet valid) token was used."),
+ /** The merchant backend is not configured to support the DONAU protocol. */
+ MERCHANT_GENERIC_DONAU_NOT_CONFIGURED(2028, 501, "The merchant backend is not configured to support the DONAU protocol."),
+
/** The exchange failed to provide a valid answer to the tracking request, thus those details are not in the response. */
MERCHANT_GET_ORDERS_EXCHANGE_TRACKING_FAILURE(2100, 200, "The exchange failed to provide a valid answer to the tracking request, thus those details are not in the response."),
@@ -972,6 +981,9 @@ enum class TalerErrorCode(val code: Int, val status: Int, val description: Strin
/** The contract terms hash used to authenticate the client is invalid for this order. */
MERCHANT_GET_ORDERS_ID_INVALID_CONTRACT_HASH(2106, 403, "The contract terms hash used to authenticate the client is invalid for this order."),
+ /** The contract terms version is not invalid. */
+ MERCHANT_GET_ORDERS_ID_INVALID_CONTRACT_VERSION(2107, 403, "The contract terms version is not invalid."),
+
/** The exchange responded saying that funds were insufficient (for example, due to double-spending). */
MERCHANT_POST_ORDERS_ID_PAY_INSUFFICIENT_FUNDS(2150, 409, "The exchange responded saying that funds were insufficient (for example, due to double-spending)."),
@@ -1197,6 +1209,9 @@ enum class TalerErrorCode(val code: Int, val status: Int, val description: Strin
/** The total order amount exceeds hard legal transaction limits from the available exchanges, thus a customer could never legally make this payment. You may try to increase your limits by passing legitimization checks with exchange operators. You could also inquire with your legislator why the limits are prohibitively low for your business. */
MERCHANT_PRIVATE_POST_ORDERS_AMOUNT_EXCEEDS_LEGAL_LIMITS(2513, 451, "The total order amount exceeds hard legal transaction limits from the available exchanges, thus a customer could never legally make this payment. You may try to increase your limits by passing legitimization checks with exchange operators. You could also inquire with your legislator why the limits are prohibitively low for your business."),
+ /** A currency specified to be paid in the contract is not supported by any exchange that this instance can currently use. Possible solutions include (1) specifying a different currency, (2) adding additional suitable exchange operators to the merchant backend configuration, or (3) satisfying compliance rules of an configured exchange to begin using the service of that provider. */
+ MERCHANT_PRIVATE_POST_ORDERS_NO_EXCHANGE_FOR_CURRENCY(2514, 409, "A currency specified to be paid in the contract is not supported by any exchange that this instance can currently use. Possible solutions include (1) specifying a different currency, (2) adding additional suitable exchange operators to the merchant backend configuration, or (3) satisfying compliance rules of an configured exchange to begin using the service of that provider."),
+
/** The order provided to the backend could not be deleted, our offer is still valid and awaiting payment. Deletion may work later after the offer has expired if it remains unpaid. */
MERCHANT_PRIVATE_DELETE_ORDERS_AWAITING_PAYMENT(2520, 409, "The order provided to the backend could not be deleted, our offer is still valid and awaiting payment. Deletion may work later after the offer has expired if it remains unpaid."),
@@ -1512,6 +1527,9 @@ enum class TalerErrorCode(val code: Int, val status: Int, val description: Strin
/** The client attempted to update a transaction' details that was already aborted. */
BANK_UPDATE_ABORT_CONFLICT(5153, 409, "The client attempted to update a transaction' details that was already aborted."),
+ /** The wtid for a request to transfer funds has already been used, but with a different request unpaid. */
+ BANK_TRANSFER_WTID_REUSED(5154, 409, "The wtid for a request to transfer funds has already been used, but with a different request unpaid."),
+
/** The sync service failed find the account in its database. */
SYNC_ACCOUNT_UNKNOWN(6100, 404, "The sync service failed find the account in its database."),
@@ -1920,8 +1938,8 @@ enum class TalerErrorCode(val code: Int, val status: Int, val description: Strin
/** The signature of the donation receipt is not valid. */
DONAU_DONATION_RECEIPT_SIGNATURE_INVALID(8616, 403, "The signature of the donation receipt is not valid."),
- /** The client re-used a unique donor identifier nonce, which is not allowed. */
- DONAU_DONOR_IDENTIFIER_NONCE_REUSE(8617, 409, "The client re-used a unique donor identifier nonce, which is not allowed."),
+ /** The client reused a unique donor identifier nonce, which is not allowed. */
+ DONAU_DONOR_IDENTIFIER_NONCE_REUSE(8617, 409, "The client reused a unique donor identifier nonce, which is not allowed."),
/** A generic error happened in the LibEuFin nexus. See the enclose details JSON for more information. */
LIBEUFIN_NEXUS_GENERIC_ERROR(9000, 0, "A generic error happened in the LibEuFin nexus. See the enclose details JSON for more information."),
diff --git a/database-versioning/libeufin-bank-procedures.sql b/database-versioning/libeufin-bank-procedures.sql
@@ -649,6 +649,7 @@ CREATE FUNCTION taler_transfer(
OUT out_debtor_not_exchange BOOLEAN,
OUT out_both_exchanges BOOLEAN,
OUT out_request_uid_reuse BOOLEAN,
+ OUT out_wtid_reuse BOOLEAN,
OUT out_exchange_balance_insufficient BOOLEAN,
-- Success return
OUT out_tx_row_id INT8,
@@ -675,6 +676,10 @@ SELECT (amount != in_amount
IF found THEN
RETURN;
END IF;
+out_wtid_reuse = EXISTS(SELECT FROM transfer_operations WHERE wtid = in_wtid);
+IF out_wtid_reuse THEN
+ RETURN;
+END IF;
out_timestamp=in_timestamp;
-- Find exchange bank account id
SELECT
diff --git a/database-versioning/libeufin-nexus-procedures.sql b/database-versioning/libeufin-nexus-procedures.sql
@@ -424,6 +424,7 @@ CREATE FUNCTION taler_transfer(
IN in_timestamp INT8,
-- Error status
OUT out_request_uid_reuse BOOLEAN,
+ OUT out_wtid_reuse BOOLEAN,
-- Success return
OUT out_tx_row_id INT8,
OUT out_timestamp INT8
@@ -444,6 +445,11 @@ SELECT (amount != in_amount
IF FOUND THEN
RETURN;
END IF;
+out_wtid_reuse = EXISTS(SELECT FROM transfer_operations WHERE wtid = in_wtid);
+IF out_wtid_reuse THEN
+ RETURN;
+END IF;
+out_timestamp=in_timestamp;
-- Initiate bank transfer
INSERT INTO initiated_outgoing_transactions (
amount
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/api/WireGatewayApi.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/api/WireGatewayApi.kt
@@ -60,6 +60,10 @@ fun Routing.wireGatewayApi(db: Database, cfg: NexusConfig) = conditional(cfg.wir
"request_uid used already",
TalerErrorCode.BANK_TRANSFER_REQUEST_UID_REUSED
)
+ TransferResult.WtidReuse -> throw conflict(
+ "wtid used already",
+ TalerErrorCode.BANK_TRANSFER_WTID_REUSED
+ )
is TransferResult.Success -> call.respond(
TransferResponse(
timestamp = res.timestamp,
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/db/ExchangeDAO.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/db/ExchangeDAO.kt
@@ -98,6 +98,7 @@ class ExchangeDAO(private val db: Database) {
/** Transaction [id] and wire transfer [timestamp] */
data class Success(val id: Long, val timestamp: TalerProtocolTimestamp): TransferResult
data object RequestUidReuse: TransferResult
+ data object WtidReuse: TransferResult
}
/** Perform a Taler transfer */
@@ -109,6 +110,7 @@ class ExchangeDAO(private val db: Database) {
"""
SELECT
out_request_uid_reuse
+ ,out_wtid_reuse
,out_tx_row_id
,out_timestamp
FROM taler_transfer (
@@ -132,6 +134,7 @@ class ExchangeDAO(private val db: Database) {
one {
when {
it.getBoolean("out_request_uid_reuse") -> TransferResult.RequestUidReuse
+ it.getBoolean("out_wtid_reuse") -> TransferResult.WtidReuse
else -> TransferResult.Success(
id = it.getLong("out_tx_row_id"),
timestamp = it.getTalerTimestamp("out_timestamp")
diff --git a/nexus/src/test/kotlin/WireGatewayApiTest.kt b/nexus/src/test/kotlin/WireGatewayApiTest.kt
@@ -65,6 +65,13 @@ class WireGatewayApiTest {
}
}.assertConflict(TalerErrorCode.BANK_TRANSFER_REQUEST_UID_REUSED)
+ // Trigger conflict due to reused wtid
+ client.postA("/taler-wire-gateway/transfer") {
+ json(valid_req) {
+ "request_uid" to HashCode.rand()
+ }
+ }.assertConflict(TalerErrorCode.BANK_TRANSFER_WTID_REUSED)
+
// Currency mismatch
client.postA("/taler-wire-gateway/transfer") {
json(valid_req) {