fix: unique ('PENDING', checking_ID) for melt quotes (#800)

* fix: only one melt_quote with shared checking_id is allowed to be in a pending state.

fix mypy

add comprehensive tests

remove SQL unique index

remove test db constraint

fix lock statement

remove `test_regtest_pay_mpp_incomplete_payment`

format

* remove `test_set_melt_quote_pending_with_outputs`

* client self-rug mitigation

* fix

* format

* DB level check: error if payment reference paid or pending

* fix test

* comments

* restore

* restore

---------

Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com>
This commit is contained in:
lollerfirst
2025-10-28 11:47:23 +01:00
committed by GitHub
parent 184f8ba6ca
commit 4a4b7f79f7
6 changed files with 431 additions and 8 deletions

View File

@@ -753,3 +753,55 @@ async def test_promises_fk_constraints_enforced(ledger: Ledger):
)
# Done. This test only checks FK enforcement paths.
# Tests for unique pending melt quote checking_id - concurrency and migration
@pytest.mark.asyncio
async def test_concurrent_set_melt_quote_pending_same_checking_id(ledger: Ledger):
"""Test that concurrent attempts to set quotes with same checking_id as pending are handled correctly."""
from cashu.core.base import MeltQuote, MeltQuoteState
checking_id = "test_checking_id_concurrent"
# Create two quotes with the same checking_id
quote1 = MeltQuote(
quote="quote_id_conc_1",
method="bolt11",
request="lnbc123",
checking_id=checking_id,
unit="sat",
amount=100,
fee_reserve=1,
state=MeltQuoteState.unpaid,
)
quote2 = MeltQuote(
quote="quote_id_conc_2",
method="bolt11",
request="lnbc456",
checking_id=checking_id,
unit="sat",
amount=200,
fee_reserve=2,
state=MeltQuoteState.unpaid,
)
await ledger.crud.store_melt_quote(quote=quote1, db=ledger.db)
await ledger.crud.store_melt_quote(quote=quote2, db=ledger.db)
# Try to set both as pending concurrently
results = await asyncio.gather(
ledger.db_write._set_melt_quote_pending(quote=quote1),
ledger.db_write._set_melt_quote_pending(quote=quote2),
return_exceptions=True
)
# One should succeed, one should fail
success_count = sum(1 for r in results if isinstance(r, MeltQuote))
error_count = sum(1 for r in results if isinstance(r, Exception))
assert success_count == 1, "Exactly one quote should be set as pending"
assert error_count == 1, "Exactly one should fail"
# The error should be about the quote already being pending
error = next(r for r in results if isinstance(r, Exception))
assert "Melt quote already paid or pending." in str(error)