From 7f48ef2ad6f8e847ca63975a3f36fade36750c15 Mon Sep 17 00:00:00 2001 From: kvngmikey Date: Thu, 28 May 2026 15:26:31 +0100 Subject: [PATCH 1/2] chore: remove paid field in melt quote --- cashu/core/base.py | 7 ----- cashu/core/models/melt_quote.py | 5 ---- cashu/mint/auth/crud.py | 12 ++++----- cashu/mint/crud.py | 5 ++-- cashu/mint/ledger.py | 5 ++-- cashu/mint/migrations.py | 45 +++++++++++++++++++++++++++++++++ cashu/mint/router.py | 1 - cashu/wallet/v1_api.py | 1 - tests/fuzz/test_fuzz_core.py | 18 ++++++------- tests/mint/test_mint_api.py | 20 --------------- 10 files changed, 63 insertions(+), 56 deletions(-) diff --git a/cashu/core/base.py b/cashu/core/base.py index d831e05e3..db9268af0 100644 --- a/cashu/core/base.py +++ b/cashu/core/base.py @@ -343,13 +343,6 @@ def from_row(cls, row: Row, change: Optional[List[BlindedSignature]] = None): @classmethod def from_resp_wallet(cls, melt_quote_resp, mint: str, unit: str, request: str): - # BEGIN: BACKWARDS COMPATIBILITY < 0.16.0: "paid" field to "state" - if melt_quote_resp.state is None: - if melt_quote_resp.paid is True: - melt_quote_resp.state = MeltQuoteState.paid - elif melt_quote_resp.paid is False: - melt_quote_resp.state = MeltQuoteState.unpaid - # END: BACKWARDS COMPATIBILITY < 0.16.0 return cls( quote=melt_quote_resp.quote, method="bolt11", diff --git a/cashu/core/models/melt_quote.py b/cashu/core/models/melt_quote.py index 480ac152f..cb6334b32 100644 --- a/cashu/core/models/melt_quote.py +++ b/cashu/core/models/melt_quote.py @@ -47,9 +47,6 @@ class PostMeltQuoteResponse(BaseModel): str ] # output payment request (optional for BACKWARDS COMPAT mint response < 0.17.0) fee_reserve: int # input fee reserve - paid: Optional[bool] = ( - None # whether the request has been paid # DEPRECATED as per NUT PR #136 - ) state: Optional[str] # state of the quote expiry: Optional[int] # expiry of the quote payment_preimage: Optional[str] = None # payment preimage @@ -60,6 +57,4 @@ def from_melt_quote(cls, melt_quote: MeltQuote) -> "PostMeltQuoteResponse": to_dict = melt_quote.model_dump() # turn state into string to_dict["state"] = melt_quote.state.value - # add deprecated "paid" field - to_dict["paid"] = melt_quote.paid return cls.model_validate(to_dict) diff --git a/cashu/mint/auth/crud.py b/cashu/mint/auth/crud.py index 0fa5e1b92..f4425300f 100644 --- a/cashu/mint/auth/crud.py +++ b/cashu/mint/auth/crud.py @@ -500,8 +500,8 @@ async def store_melt_quote( await (conn or db).execute( f""" INSERT INTO {db.table_with_schema('melt_quotes')} - (quote, method, request, checking_id, unit, amount, fee_reserve, paid, state, created_time, paid_time, fee_paid, proof, change, expiry) - VALUES (:quote, :method, :request, :checking_id, :unit, :amount, :fee_reserve, :paid, :state, :created_time, :paid_time, :fee_paid, :proof, :change, :expiry) + (quote, method, request, checking_id, unit, amount, fee_reserve, state, created_time, paid_time, fee_paid, proof, change, expiry) + VALUES (:quote, :method, :request, :checking_id, :unit, :amount, :fee_reserve, :state, :created_time, :paid_time, :fee_paid, :proof, :change, :expiry) """, { "quote": quote.quote, @@ -511,8 +511,7 @@ async def store_melt_quote( "unit": quote.unit, "amount": quote.amount, "fee_reserve": quote.fee_reserve or 0, - "paid": quote.paid, - "state": quote.state.name, + "state": quote.state.value, "created_time": db.to_timestamp( db.timestamp_from_seconds(quote.created_time) or "" ), @@ -571,11 +570,10 @@ async def update_melt_quote( ) -> None: await (conn or db).execute( f""" - UPDATE {db.table_with_schema('melt_quotes')} SET paid = :paid, state = :state, fee_paid = :fee_paid, paid_time = :paid_time, proof = :proof, change = :change WHERE quote = :quote + UPDATE {db.table_with_schema('melt_quotes')} SET state = :state, fee_paid = :fee_paid, paid_time = :paid_time, proof = :proof, change = :change WHERE quote = :quote """, { - "paid": quote.paid, - "state": quote.state.name, + "state": quote.state.value, "fee_paid": quote.fee_paid, "paid_time": db.to_timestamp( db.timestamp_from_seconds(quote.paid_time) or "" diff --git a/cashu/mint/crud.py b/cashu/mint/crud.py index 2421789ea..823cb5409 100644 --- a/cashu/mint/crud.py +++ b/cashu/mint/crud.py @@ -758,8 +758,8 @@ async def store_melt_quote( await (conn or db).execute( f""" INSERT INTO {db.table_with_schema("melt_quotes")} - (quote, method, request, checking_id, unit, amount, fee_reserve, state, paid, created_time, paid_time, fee_paid, proof, expiry) - VALUES (:quote, :method, :request, :checking_id, :unit, :amount, :fee_reserve, :state, :paid, :created_time, :paid_time, :fee_paid, :proof, :expiry) + (quote, method, request, checking_id, unit, amount, fee_reserve, state, created_time, paid_time, fee_paid, proof, expiry) + VALUES (:quote, :method, :request, :checking_id, :unit, :amount, :fee_reserve, :state, :created_time, :paid_time, :fee_paid, :proof, :expiry) """, { "quote": quote.quote, @@ -770,7 +770,6 @@ async def store_melt_quote( "amount": quote.amount, "fee_reserve": quote.fee_reserve or 0, "state": quote.state.value, - "paid": quote.paid, # this is deprecated! we need to store it because we have a NOT NULL constraint | we could also remove the column but sqlite doesn't support that (we would have to make a new table) "created_time": db.to_timestamp( db.timestamp_from_seconds(quote.created_time) or "" ), diff --git a/cashu/mint/ledger.py b/cashu/mint/ledger.py index ffa651e24..ea5d3b1d6 100644 --- a/cashu/mint/ledger.py +++ b/cashu/mint/ledger.py @@ -807,7 +807,6 @@ async def melt_quote( unit=quote.unit, request=quote.request, fee_reserve=quote.fee_reserve, - paid=quote.paid, # deprecated state=quote.state.value, expiry=quote.expiry, ) @@ -946,7 +945,7 @@ async def melt_mint_settle_internally( return melt_quote # we settle the transaction internally - if melt_quote.paid: + if melt_quote.state == MeltQuoteState.paid: raise TransactionError("melt quote already paid") # verify amounts from bolt11 invoice @@ -1102,7 +1101,7 @@ async def melt( # if the melt corresponds to an internal mint, mark both as paid melt_quote = await self.melt_mint_settle_internally(melt_quote, proofs) # quote not paid yet (not internal), pay it with the backend - if not melt_quote.paid: + if melt_quote.state != MeltQuoteState.paid: logger.debug(f"Lightning: pay invoice {melt_quote.request}") try: payment = await self.backends[method][unit].pay_invoice( diff --git a/cashu/mint/migrations.py b/cashu/mint/migrations.py index 571efd574..cc313dcff 100644 --- a/cashu/mint/migrations.py +++ b/cashu/mint/migrations.py @@ -1261,3 +1261,48 @@ async def m035_add_last_checked_to_mint_quotes(db: Database): ADD COLUMN last_checked TIMESTAMP NULL """ ) + + +async def m036_remove_paid_from_melt_quote(db: Database): + """Remove the deprecated 'paid' field from melt_quotes. + The 'state' column now fully represents payment status.""" + async with db.connect() as conn: + if conn.type == "SQLITE": + await conn.execute("PRAGMA foreign_keys=OFF;") + await conn.execute( + f""" + CREATE TABLE {db.table_with_schema('melt_quotes_new')} ( + quote TEXT NOT NULL, + method TEXT NOT NULL, + request TEXT NOT NULL, + checking_id TEXT NOT NULL, + unit TEXT NOT NULL, + amount {db.big_int} NOT NULL, + fee_reserve {db.big_int}, + created_time TIMESTAMP, + paid_time TIMESTAMP, + fee_paid {db.big_int}, + proof TEXT, + state TEXT, + expiry TIMESTAMP, + UNIQUE (quote) + ); + """ + ) + await conn.execute( + f""" + INSERT INTO {db.table_with_schema('melt_quotes_new')} + (quote, method, request, checking_id, unit, amount, fee_reserve, created_time, paid_time, fee_paid, proof, state, expiry) + SELECT quote, method, request, checking_id, unit, amount, fee_reserve, created_time, paid_time, fee_paid, proof, state, expiry + FROM {db.table_with_schema('melt_quotes')}; + """ + ) + await conn.execute(f"DROP TABLE {db.table_with_schema('melt_quotes')};") + await conn.execute( + f"ALTER TABLE {db.table_with_schema('melt_quotes_new')} RENAME TO {db.table_with_schema('melt_quotes')};" + ) + await conn.execute("PRAGMA foreign_keys=ON;") + elif conn.type == "POSTGRES": + await conn.execute( + f"ALTER TABLE {db.table_with_schema('melt_quotes')} DROP COLUMN IF EXISTS paid;" + ) diff --git a/cashu/mint/router.py b/cashu/mint/router.py index d61da7313..ef9e9395a 100644 --- a/cashu/mint/router.py +++ b/cashu/mint/router.py @@ -353,7 +353,6 @@ async def get_melt_quote(request: Request, quote: str) -> PostMeltQuoteResponse: unit=melt_quote.unit, request=melt_quote.request, fee_reserve=melt_quote.fee_reserve, - paid=melt_quote.paid, state=melt_quote.state.value, expiry=melt_quote.expiry, payment_preimage=melt_quote.payment_preimage, diff --git a/cashu/wallet/v1_api.py b/cashu/wallet/v1_api.py index 27ed02672..060177cd7 100644 --- a/cashu/wallet/v1_api.py +++ b/cashu/wallet/v1_api.py @@ -533,7 +533,6 @@ def _meltrequest_include_fields( unit="sat", request="lnbc0", fee_reserve=0, - paid=ret.paid or False, state=( MeltQuoteState.paid.value if ret.paid diff --git a/tests/fuzz/test_fuzz_core.py b/tests/fuzz/test_fuzz_core.py index 5cd949f2f..e91a57fc8 100644 --- a/tests/fuzz/test_fuzz_core.py +++ b/tests/fuzz/test_fuzz_core.py @@ -171,17 +171,17 @@ def test_fuzz_melt_quote(quote, method, request, checking_id, unit, amount, fee_ # Test property accessors if state == MeltQuoteState.paid: - assert mq.paid - assert not mq.unpaid - assert not mq.pending + assert mq.state == MeltQuoteState.paid + assert mq.state != MeltQuoteState.unpaid + assert mq.state != MeltQuoteState.pending elif state == MeltQuoteState.unpaid: - assert not mq.paid - assert mq.unpaid - assert not mq.pending + assert mq.state != MeltQuoteState.paid + assert mq.state == MeltQuoteState.unpaid + assert mq.state != MeltQuoteState.pending elif state == MeltQuoteState.pending: - assert not mq.paid - assert not mq.unpaid - assert mq.pending + assert mq.state != MeltQuoteState.paid + assert mq.state != MeltQuoteState.unpaid + assert mq.state == MeltQuoteState.pending @given( quote=st.text(min_size=1, max_size=50), diff --git a/tests/mint/test_mint_api.py b/tests/mint/test_mint_api.py index 5d111809a..267384077 100644 --- a/tests/mint/test_mint_api.py +++ b/tests/mint/test_mint_api.py @@ -362,26 +362,6 @@ async def test_melt_quote_internal(ledger: Ledger, wallet: Wallet): assert result["expiry"] == expiry - # # get melt quote again from api - # response = httpx.get( - # f"{BASE_URL}/v1/melt/quote/bolt11/{result['quote']}", - # ) - # assert response.status_code == 200, f"{response.url} {response.status_code}" - # result2 = response.json() - # assert result2["quote"] == result["quote"] - - # # deserialize the response - # resp_quote = PostMeltQuoteResponse(**result2) - # assert resp_quote.quote == result["quote"] - # assert resp_quote.payment_preimage is not None - # assert len(resp_quote.payment_preimage) == 64 - # assert resp_quote.change is not None - # assert resp_quote.state == MeltQuoteState.paid.value - - # # check if DEPRECATED paid flag is also returned - # assert result2["paid"] is True - # assert resp_quote.paid is True - @pytest.mark.asyncio @pytest.mark.skipif( From 1ab1bf4996c98fdddf2f330346677d18fa018d8f Mon Sep 17 00:00:00 2001 From: kvngmikey Date: Thu, 28 May 2026 16:36:48 +0100 Subject: [PATCH 2/2] chore: update ledger check --- cashu/mint/ledger.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cashu/mint/ledger.py b/cashu/mint/ledger.py index ea5d3b1d6..7721e69b5 100644 --- a/cashu/mint/ledger.py +++ b/cashu/mint/ledger.py @@ -1101,7 +1101,7 @@ async def melt( # if the melt corresponds to an internal mint, mark both as paid melt_quote = await self.melt_mint_settle_internally(melt_quote, proofs) # quote not paid yet (not internal), pay it with the backend - if melt_quote.state != MeltQuoteState.paid: + if melt_quote.state == MeltQuoteState.pending: logger.debug(f"Lightning: pay invoice {melt_quote.request}") try: payment = await self.backends[method][unit].pay_invoice(