merchant

Merchant backend to process payments, run by merchants
Log | Files | Refs | Submodules | README | LICENSE

commit d0aa19952bfc26a8cd9923a1a2799160191faa70
parent f405f6f043d5e32b3365b4b40df7a73e503c6e3d
Author: Christian Grothoff <grothoff@gnunet.org>
Date:   Fri, 10 Jul 2026 11:31:00 +0200

fix /pay idempotency issue with uninitialized wire fee

Diffstat:
Msrc/backend/taler-merchant-httpd_post-orders-ORDER_ID-abort.c | 5++++-
Msrc/backend/taler-merchant-httpd_post-orders-ORDER_ID-pay.c | 51++++++++++++++++++++++++++++++++++++++++++++-------
Msrc/backenddb/lookup_deposits.c | 7++++++-
Msrc/backenddb/test_merchantdb.c | 5++++-
Msrc/include/merchant-database/lookup_deposits.h | 4+++-
Msrc/testing/testing_api_cmd_post_transfers.c | 3++-
6 files changed, 63 insertions(+), 12 deletions(-)

diff --git a/src/backend/taler-merchant-httpd_post-orders-ORDER_ID-abort.c b/src/backend/taler-merchant-httpd_post-orders-ORDER_ID-abort.c @@ -659,6 +659,7 @@ find_next_exchange (struct AbortContext *ac) * @param amount_with_fee amount the exchange will deposit for this coin * @param deposit_fee fee the exchange will charge for this coin * @param refund_fee fee the exchange will charge for refunding this coin + * @param wire_fee fee to be paid for the wire transfer */ static void refund_coins (void *cls, @@ -666,13 +667,15 @@ refund_coins (void *cls, const struct TALER_CoinSpendPublicKeyP *coin_pub, const struct TALER_Amount *amount_with_fee, const struct TALER_Amount *deposit_fee, - const struct TALER_Amount *refund_fee) + const struct TALER_Amount *refund_fee, + const struct TALER_Amount *wire_fee) { struct AbortContext *ac = cls; struct GNUNET_TIME_Timestamp now; (void) deposit_fee; (void) refund_fee; + (void) wire_fee; now = GNUNET_TIME_timestamp_get (); for (size_t i = 0; i<ac->coins_cnt; i++) { diff --git a/src/backend/taler-merchant-httpd_post-orders-ORDER_ID-pay.c b/src/backend/taler-merchant-httpd_post-orders-ORDER_ID-pay.c @@ -230,6 +230,11 @@ struct DepositConfirmation struct TALER_Amount refund_fee; /** + * Fee charged by the exchange for the wire transfer. + */ + struct TALER_Amount wire_fee; + + /** * If a minimum age was required (i. e. pc->minimum_age is large enough), * this is the signature of the minimum age (as a single uint8_t), using the * private key to the corresponding age group. Might be all zeroes for no @@ -1091,6 +1096,7 @@ batch_deposit_transaction ( continue; if (! dc->in_batch) continue; + dc->wire_fee = eg->wire_fee; /* FIXME-#9457: We might want to check if the order was fully paid concurrently by some other wallet here, and if so, issue an auto-refund. Right now, it is possible to over-pay if two wallets literally make a concurrent @@ -2526,6 +2532,7 @@ phase_compute_money_pots (struct PayContext *pc) * @param amount_with_fee amount the exchange will deposit for this coin * @param deposit_fee fee the exchange will charge for this coin * @param refund_fee fee the exchange will charge for refunding this coin + * @param wire_fee fee the exchange will charge for wiring this coin */ static void check_coin_paid (void *cls, @@ -2533,7 +2540,8 @@ check_coin_paid (void *cls, const struct TALER_CoinSpendPublicKeyP *coin_pub, const struct TALER_Amount *amount_with_fee, const struct TALER_Amount *deposit_fee, - const struct TALER_Amount *refund_fee) + const struct TALER_Amount *refund_fee, + const struct TALER_Amount *wire_fee) { struct PayContext *pc = cls; @@ -2579,6 +2587,7 @@ check_coin_paid (void *cls, deposit_fee)); dc->deposit_fee = *deposit_fee; dc->refund_fee = *refund_fee; + dc->wire_fee = *wire_fee; dc->cdd.amount = *amount_with_fee; dc->found_in_db = true; pc->pay_transaction.pending--; @@ -2659,9 +2668,36 @@ check_payment_sufficient (struct PayContext *pc) &total_wire_fee)); for (unsigned int i = 0; i < pc->parse_pay.num_exchanges; i++) { + const struct ExchangeGroup *egsi = pc->parse_pay.egs[i]; + const struct TALER_Amount *wire_fee = NULL; + + /* Note: we cannot just use egsi->wire_fee here, as that field + MAY not be initialized if the deposit for that exchange was + done earlier this is an idempotent request, for example + to deposit coins of another exchange or just because the + previous answer was lost; thus, we must get the fee from + the "dc" as that is guaranteed to be set! */ + for (unsigned int j = 0; j < pc->parse_pay.num_exchanges; j++) + { + const struct DepositConfirmation *dc = &pc->parse_pay.dc[j]; + + if (0 == strcmp (dc->exchange_url, + egsi->exchange_url)) + { + wire_fee = &dc->wire_fee; + break; + } + } + if (NULL == wire_fee) + { + /* Exchange group without a single deposit? Strange! */ + GNUNET_break (0); + continue; + } + if (GNUNET_OK != TALER_amount_cmp_currency (&total_wire_fee, - &pc->parse_pay.egs[i]->wire_fee)) + wire_fee)) { GNUNET_break_op (0); pay_end (pc, @@ -2674,7 +2710,7 @@ check_payment_sufficient (struct PayContext *pc) if (0 > TALER_amount_add (&total_wire_fee, &total_wire_fee, - &pc->parse_pay.egs[i]->wire_fee)) + wire_fee)) { GNUNET_break (0); pay_end (pc, @@ -3017,10 +3053,11 @@ phase_execute_pay_transaction (struct PayContext *pc) /* Always report on hard error as well to enable diagnostics */ GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); pay_end (pc, - TALER_MHD_reply_with_error (pc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "lookup deposits")); + TALER_MHD_reply_with_error ( + pc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "lookup deposits")); return; } if (pc->pay_transaction.deposit_currency_mismatch) diff --git a/src/backenddb/lookup_deposits.c b/src/backenddb/lookup_deposits.c @@ -71,6 +71,7 @@ lookup_deposits_cb (void *cls, struct TALER_Amount amount_with_fee; struct TALER_Amount deposit_fee; struct TALER_Amount refund_fee; + struct TALER_Amount wire_fee; char *exchange_url; struct GNUNET_PQ_ResultSpec rs[] = { GNUNET_PQ_result_spec_string ("exchange_url", @@ -83,6 +84,8 @@ lookup_deposits_cb (void *cls, &deposit_fee), TALER_PQ_result_spec_amount_with_currency ("refund_fee", &refund_fee), + TALER_PQ_result_spec_amount_with_currency ("wire_fee", + &wire_fee), GNUNET_PQ_result_spec_end }; @@ -100,7 +103,8 @@ lookup_deposits_cb (void *cls, &coin_pub, &amount_with_fee, &deposit_fee, - &refund_fee); + &refund_fee, + &wire_fee); GNUNET_PQ_cleanup_result (rs); } ldc->qs = num_results; @@ -136,6 +140,7 @@ TALER_MERCHANTDB_lookup_deposits ( TMH_PQ_prepare_anon (pg, "SELECT" " dcom.exchange_url" + ",dcom.wire_fee" ",dep.coin_pub" ",dep.amount_with_fee" ",dep.deposit_fee" diff --git a/src/backenddb/test_merchantdb.c b/src/backenddb/test_merchantdb.c @@ -2993,6 +2993,7 @@ struct TestLookupDeposits_Closure * @param amount_with_fee amount of the deposit with fees. * @param deposit_fee fee charged for the deposit. * @param refund_fee fee charged in case of a refund. + * @param wire_fee wire transfer fee to be paid */ static void lookup_deposits_cb (void *cls, @@ -3000,9 +3001,11 @@ lookup_deposits_cb (void *cls, const struct TALER_CoinSpendPublicKeyP *coin_pub, const struct TALER_Amount *amount_with_fee, const struct TALER_Amount *deposit_fee, - const struct TALER_Amount *refund_fee) + const struct TALER_Amount *refund_fee, + const struct TALER_Amount *wire_fee) { struct TestLookupDeposits_Closure *cmp = cls; + if (NULL == cmp) return; cmp->results_length += 1; diff --git a/src/include/merchant-database/lookup_deposits.h b/src/include/merchant-database/lookup_deposits.h @@ -37,6 +37,7 @@ struct TALER_MERCHANTDB_PostgresContext; * @param amount_with_fee amount the exchange will deposit for this coin * @param deposit_fee fee the exchange will charge for this coin * @param refund_fee fee the exchange will charge for refunding this coin + * @param wire_fee fee the exchange will charge for the wire transfer */ typedef void (*TALER_MERCHANTDB_DepositsCallback)( @@ -45,7 +46,8 @@ typedef void const struct TALER_CoinSpendPublicKeyP *coin_pub, const struct TALER_Amount *amount_with_fee, const struct TALER_Amount *deposit_fee, - const struct TALER_Amount *refund_fee); + const struct TALER_Amount *refund_fee, + const struct TALER_Amount *wire_fee); /** * Lookup information about coins that were successfully deposited for a diff --git a/src/testing/testing_api_cmd_post_transfers.c b/src/testing/testing_api_cmd_post_transfers.c @@ -485,7 +485,8 @@ TALER_TESTING_cmd_merchant_post_transfer_set_serial ( { struct PostTransfersState *pts = cmd->cls; - GNUNET_assert (cmd->run == &post_transfers_run); + GNUNET_assert ( (cmd->run == &post_transfers_run) || + (cmd->run == &post_transfers_run2) ); pts->serial = serial; }