From 0fc02f646811b31c1cd2c25f7c4689f0c8fd36cb Mon Sep 17 00:00:00 2001 From: a1denvalu3 Date: Wed, 10 Jun 2026 14:14:54 +0200 Subject: [PATCH 1/6] feat(mint): align NUT-04 with amount_paid/issued + updated_at --- cashu/core/base.py | 44 +++++++++++++++++++++++++- cashu/core/models/mint_quote.py | 10 ++++-- cashu/mint/router.py | 30 ++---------------- cashu/wallet/wallet.py | 55 ++++++++++++++++++++++++--------- tests/mint/test_mint_api.py | 8 +++++ 5 files changed, 103 insertions(+), 44 deletions(-) diff --git a/cashu/core/base.py b/cashu/core/base.py index 51aaf2a98..05def465b 100644 --- a/cashu/core/base.py +++ b/cashu/core/base.py @@ -470,6 +470,26 @@ def from_row(cls, row: Row): @classmethod def from_resp_wallet(cls, mint_quote_resp, mint: str, amount: int, unit: str): + # Prefer amount_paid/amount_issued if present + if getattr(mint_quote_resp, "amount_paid", None) is not None and getattr(mint_quote_resp, "amount_issued", None) is not None: + amount_paid = mint_quote_resp.amount_paid + amount_issued = mint_quote_resp.amount_issued + if amount_paid == 0 and amount_issued == 0: + if getattr(mint_quote_resp, "state", None) == "PENDING": + state = MintQuoteState.pending + else: + state = MintQuoteState.unpaid + elif amount_paid > amount_issued: + state = MintQuoteState.paid + elif amount_paid == amount_issued and amount_issued > 0: + state = MintQuoteState.issued + else: + state = MintQuoteState.unpaid + elif getattr(mint_quote_resp, "state", None): + state = MintQuoteState(mint_quote_resp.state) + else: + state = MintQuoteState.unpaid + return cls( quote=mint_quote_resp.quote, method="bolt11", @@ -479,13 +499,35 @@ def from_resp_wallet(cls, mint_quote_resp, mint: str, amount: int, unit: str): or unit, # BACKWARDS COMPATIBILITY mint response < 0.17.0 amount=mint_quote_resp.amount or amount, # BACKWARDS COMPATIBILITY mint response < 0.17.0 - state=MintQuoteState(mint_quote_resp.state), + state=state, mint=mint, expiry=mint_quote_resp.expiry, created_time=int(time.time()), pubkey=mint_quote_resp.pubkey, ) + @property + def amount_paid(self) -> int: + if self.state in [MintQuoteState.paid, MintQuoteState.issued]: + return self.amount + return 0 + + @property + def amount_issued(self) -> int: + if self.state == MintQuoteState.issued: + return self.amount + return 0 + + @property + def updated_at(self) -> int: + if self.issued_time is not None: + return self.issued_time + if self.paid_time is not None: + return self.paid_time + if self.created_time is not None: + return self.created_time + return 0 + @property def identifier(self) -> str: """Implementation of the abstract method from LedgerEventManager""" diff --git a/cashu/core/models/mint_quote.py b/cashu/core/models/mint_quote.py index 36cd4f77a..1ae2d0318 100644 --- a/cashu/core/models/mint_quote.py +++ b/cashu/core/models/mint_quote.py @@ -38,8 +38,11 @@ class PostMintQuoteResponse(BaseModel): unit: Optional[ str ] # output unit (optional for BACKWARDS COMPAT mint response < 0.17.0) - state: Optional[str] # state of the quote (optional for backwards compat) - expiry: Optional[int] # expiry of the quote + amount_paid: Optional[int] = None + amount_issued: Optional[int] = None + updated_at: Optional[int] = None + state: Optional[str] = None # state of the quote (optional for backwards compat) + expiry: Optional[int] = None # expiry of the quote pubkey: Optional[str] = None # NUT-20 quote lock pubkey @classmethod @@ -47,4 +50,7 @@ def from_mint_quote(cls, mint_quote: MintQuote) -> "PostMintQuoteResponse": to_dict = mint_quote.model_dump() # turn state into string to_dict["state"] = mint_quote.state.value + to_dict["amount_paid"] = mint_quote.amount_paid + to_dict["amount_issued"] = mint_quote.amount_issued + to_dict["updated_at"] = mint_quote.updated_at return cls.model_validate(to_dict) diff --git a/cashu/mint/router.py b/cashu/mint/router.py index 8e5b68caf..68f6c99b6 100644 --- a/cashu/mint/router.py +++ b/cashu/mint/router.py @@ -173,15 +173,7 @@ async def mint_quote( """ logger.trace(f"> POST /v1/mint/quote/bolt11: payload={payload}") quote = await ledger.mint_quote(payload) - resp = PostMintQuoteResponse( - quote=quote.quote, - request=quote.request, - amount=quote.amount, - unit=quote.unit, - state=quote.state.value, - expiry=quote.expiry, - pubkey=quote.pubkey, - ) + resp = PostMintQuoteResponse.from_mint_quote(quote) logger.trace(f"< POST /v1/mint/quote/bolt11: {resp}") return resp @@ -199,15 +191,7 @@ async def get_mint_quote(request: Request, quote: str) -> PostMintQuoteResponse: """ logger.trace(f"> GET /v1/mint/quote/bolt11/{quote}") mint_quote = await ledger.get_mint_quote(quote) - resp = PostMintQuoteResponse( - quote=mint_quote.quote, - request=mint_quote.request, - amount=mint_quote.amount, - unit=mint_quote.unit, - state=mint_quote.state.value, - expiry=mint_quote.expiry, - pubkey=mint_quote.pubkey, - ) + resp = PostMintQuoteResponse.from_mint_quote(mint_quote) logger.trace(f"< GET /v1/mint/quote/bolt11/{quote}") return resp @@ -226,15 +210,7 @@ async def mint_quote_check( logger.trace(f"> POST /v1/mint/quote/bolt11/check: payload={payload}") quotes = await ledger.mint_quote_check(payload) resp = [ - PostMintQuoteResponse( - quote=quote.quote, - request=quote.request, - amount=quote.amount, - unit=quote.unit, - state=quote.state.value, - expiry=quote.expiry, - pubkey=quote.pubkey, - ) + PostMintQuoteResponse.from_mint_quote(quote) for quote in quotes ] logger.trace(f"< POST /v1/mint/quote/bolt11/check: {resp}") diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index 5be1e7b4d..89b92ca56 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -569,25 +569,52 @@ async def get_mint_quote( """ mint_quote_response = await super().get_mint_quote(quote_id) mint_quote_local = await get_bolt11_mint_quote(db=self.db, quote=quote_id) - mint_quote = MintQuote.from_resp_wallet( - mint_quote_response, - mint=self.url, - amount=( - mint_quote_response.amount or mint_quote_local.amount - if mint_quote_local - else 0 # BACKWARD COMPATIBILITY mint response < 0.17.0 - ), - unit=( - mint_quote_response.unit or mint_quote_local.unit - if mint_quote_local - else self.unit.name # BACKWARD COMPATIBILITY mint response < 0.17.0 - ), - ) + + # Check if the response is stale according to the spec + is_stale = False + if mint_quote_local: + response_updated_at = getattr(mint_quote_response, "updated_at", None) + if response_updated_at is not None and response_updated_at < mint_quote_local.updated_at: + is_stale = True + + response_amount_paid = getattr(mint_quote_response, "amount_paid", None) + if response_amount_paid is not None and response_amount_paid < mint_quote_local.amount_paid: + is_stale = True + + response_amount_issued = getattr(mint_quote_response, "amount_issued", None) + if response_amount_issued is not None and response_amount_issued < mint_quote_local.amount_issued: + is_stale = True + + if is_stale and mint_quote_local: + mint_quote = mint_quote_local + else: + mint_quote = MintQuote.from_resp_wallet( + mint_quote_response, + mint=self.url, + amount=( + mint_quote_response.amount or mint_quote_local.amount + if mint_quote_local + else 0 # BACKWARD COMPATIBILITY mint response < 0.17.0 + ), + unit=( + mint_quote_response.unit or mint_quote_local.unit + if mint_quote_local + else self.unit.name # BACKWARD COMPATIBILITY mint response < 0.17.0 + ), + ) + if mint_quote_local and mint_quote_local.privkey: mint_quote.privkey = mint_quote_local.privkey if not mint_quote_local: await store_bolt11_mint_quote(db=self.db, quote=mint_quote) + elif mint_quote_local.state != mint_quote.state: + await update_bolt11_mint_quote( + db=self.db, + quote=mint_quote.quote, + state=mint_quote.state, + paid_time=mint_quote.paid_time or int(time.time()), + ) return mint_quote diff --git a/tests/mint/test_mint_api.py b/tests/mint/test_mint_api.py index 5d111809a..3676210a3 100644 --- a/tests/mint/test_mint_api.py +++ b/tests/mint/test_mint_api.py @@ -218,6 +218,10 @@ async def test_mint_quote(ledger: Ledger): assert resp_quote.amount == 100 assert resp_quote.unit == "sat" assert resp_quote.request == result["request"] + assert resp_quote.amount_paid == 0 + assert resp_quote.amount_issued == 0 + assert resp_quote.updated_at is not None + assert resp_quote.updated_at > 0 invoice = bolt11.decode(result["request"]) assert invoice.amount_msat == 100 * 1000 @@ -245,6 +249,10 @@ async def test_mint_quote(ledger: Ledger): assert resp_quote.amount == 100 assert resp_quote.unit == "sat" assert resp_quote.request == result["request"] + assert resp_quote.amount_paid == 100 + assert resp_quote.amount_issued == 0 + assert resp_quote.updated_at is not None + assert resp_quote.updated_at >= result["updated_at"] assert resp_quote.pubkey == "02" + "00" * 32 From 0de42e1f2a22893ea404b6114ca0365144ff03e7 Mon Sep 17 00:00:00 2001 From: a1denvalu3 Date: Wed, 10 Jun 2026 14:32:04 +0200 Subject: [PATCH 2/6] fix(mint): handle mock and non-pydantic objects in from_mint_quote --- cashu/core/models/mint_quote.py | 47 ++++++++++++++++++++++++++++----- 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/cashu/core/models/mint_quote.py b/cashu/core/models/mint_quote.py index 1ae2d0318..04a6ec197 100644 --- a/cashu/core/models/mint_quote.py +++ b/cashu/core/models/mint_quote.py @@ -47,10 +47,45 @@ class PostMintQuoteResponse(BaseModel): @classmethod def from_mint_quote(cls, mint_quote: MintQuote) -> "PostMintQuoteResponse": - to_dict = mint_quote.model_dump() - # turn state into string - to_dict["state"] = mint_quote.state.value - to_dict["amount_paid"] = mint_quote.amount_paid - to_dict["amount_issued"] = mint_quote.amount_issued - to_dict["updated_at"] = mint_quote.updated_at + if hasattr(mint_quote, "model_dump"): + to_dict = mint_quote.model_dump() + else: + to_dict = { + "quote": mint_quote.quote, + "request": mint_quote.request, + "amount": mint_quote.amount, + "unit": mint_quote.unit, + "state": getattr(mint_quote, "state", None), + "expiry": getattr(mint_quote, "expiry", None), + "pubkey": getattr(mint_quote, "pubkey", None), + } + + if hasattr(to_dict.get("state"), "value"): + to_dict["state"] = to_dict["state"].value + elif hasattr(mint_quote, "state") and hasattr(mint_quote.state, "value"): + to_dict["state"] = mint_quote.state.value + + amount_paid = getattr(mint_quote, "amount_paid", None) + if amount_paid is None: + state_val = to_dict.get("state") + if state_val in ["PAID", "ISSUED"]: + amount_paid = mint_quote.amount + else: + amount_paid = 0 + to_dict["amount_paid"] = amount_paid + + amount_issued = getattr(mint_quote, "amount_issued", None) + if amount_issued is None: + state_val = to_dict.get("state") + if state_val == "ISSUED": + amount_issued = mint_quote.amount + else: + amount_issued = 0 + to_dict["amount_issued"] = amount_issued + + updated_at = getattr(mint_quote, "updated_at", None) + if updated_at is None: + updated_at = getattr(mint_quote, "created_time", 0) or getattr(mint_quote, "expiry", 0) or 0 + to_dict["updated_at"] = updated_at + return cls.model_validate(to_dict) From daa08b9e12d4e9c0da47004ce6a246902df457c0 Mon Sep 17 00:00:00 2001 From: a1denvalu3 Date: Wed, 10 Jun 2026 14:36:39 +0200 Subject: [PATCH 3/6] refactor(mint): remove fallback slop, use direct construction in router, align test mock --- cashu/core/models/mint_quote.py | 47 ++++-------------------------- cashu/mint/router.py | 39 +++++++++++++++++++++++-- tests/mint/test_mint_app_router.py | 6 ++++ 3 files changed, 48 insertions(+), 44 deletions(-) diff --git a/cashu/core/models/mint_quote.py b/cashu/core/models/mint_quote.py index 04a6ec197..1ae2d0318 100644 --- a/cashu/core/models/mint_quote.py +++ b/cashu/core/models/mint_quote.py @@ -47,45 +47,10 @@ class PostMintQuoteResponse(BaseModel): @classmethod def from_mint_quote(cls, mint_quote: MintQuote) -> "PostMintQuoteResponse": - if hasattr(mint_quote, "model_dump"): - to_dict = mint_quote.model_dump() - else: - to_dict = { - "quote": mint_quote.quote, - "request": mint_quote.request, - "amount": mint_quote.amount, - "unit": mint_quote.unit, - "state": getattr(mint_quote, "state", None), - "expiry": getattr(mint_quote, "expiry", None), - "pubkey": getattr(mint_quote, "pubkey", None), - } - - if hasattr(to_dict.get("state"), "value"): - to_dict["state"] = to_dict["state"].value - elif hasattr(mint_quote, "state") and hasattr(mint_quote.state, "value"): - to_dict["state"] = mint_quote.state.value - - amount_paid = getattr(mint_quote, "amount_paid", None) - if amount_paid is None: - state_val = to_dict.get("state") - if state_val in ["PAID", "ISSUED"]: - amount_paid = mint_quote.amount - else: - amount_paid = 0 - to_dict["amount_paid"] = amount_paid - - amount_issued = getattr(mint_quote, "amount_issued", None) - if amount_issued is None: - state_val = to_dict.get("state") - if state_val == "ISSUED": - amount_issued = mint_quote.amount - else: - amount_issued = 0 - to_dict["amount_issued"] = amount_issued - - updated_at = getattr(mint_quote, "updated_at", None) - if updated_at is None: - updated_at = getattr(mint_quote, "created_time", 0) or getattr(mint_quote, "expiry", 0) or 0 - to_dict["updated_at"] = updated_at - + to_dict = mint_quote.model_dump() + # turn state into string + to_dict["state"] = mint_quote.state.value + to_dict["amount_paid"] = mint_quote.amount_paid + to_dict["amount_issued"] = mint_quote.amount_issued + to_dict["updated_at"] = mint_quote.updated_at return cls.model_validate(to_dict) diff --git a/cashu/mint/router.py b/cashu/mint/router.py index 68f6c99b6..f2ffcc041 100644 --- a/cashu/mint/router.py +++ b/cashu/mint/router.py @@ -173,7 +173,18 @@ async def mint_quote( """ logger.trace(f"> POST /v1/mint/quote/bolt11: payload={payload}") quote = await ledger.mint_quote(payload) - resp = PostMintQuoteResponse.from_mint_quote(quote) + resp = PostMintQuoteResponse( + quote=quote.quote, + request=quote.request, + amount=quote.amount, + unit=quote.unit, + state=str(quote.state.value), + expiry=quote.expiry, + pubkey=quote.pubkey, + amount_paid=quote.amount_paid, + amount_issued=quote.amount_issued, + updated_at=quote.updated_at, + ) logger.trace(f"< POST /v1/mint/quote/bolt11: {resp}") return resp @@ -191,7 +202,18 @@ async def get_mint_quote(request: Request, quote: str) -> PostMintQuoteResponse: """ logger.trace(f"> GET /v1/mint/quote/bolt11/{quote}") mint_quote = await ledger.get_mint_quote(quote) - resp = PostMintQuoteResponse.from_mint_quote(mint_quote) + resp = PostMintQuoteResponse( + quote=mint_quote.quote, + request=mint_quote.request, + amount=mint_quote.amount, + unit=mint_quote.unit, + state=str(mint_quote.state.value), + expiry=mint_quote.expiry, + pubkey=mint_quote.pubkey, + amount_paid=mint_quote.amount_paid, + amount_issued=mint_quote.amount_issued, + updated_at=mint_quote.updated_at, + ) logger.trace(f"< GET /v1/mint/quote/bolt11/{quote}") return resp @@ -210,7 +232,18 @@ async def mint_quote_check( logger.trace(f"> POST /v1/mint/quote/bolt11/check: payload={payload}") quotes = await ledger.mint_quote_check(payload) resp = [ - PostMintQuoteResponse.from_mint_quote(quote) + PostMintQuoteResponse( + quote=quote.quote, + request=quote.request, + amount=quote.amount, + unit=quote.unit, + state=str(quote.state.value), + expiry=quote.expiry, + pubkey=quote.pubkey, + amount_paid=quote.amount_paid, + amount_issued=quote.amount_issued, + updated_at=quote.updated_at, + ) for quote in quotes ] logger.trace(f"< POST /v1/mint/quote/bolt11/check: {resp}") diff --git a/tests/mint/test_mint_app_router.py b/tests/mint/test_mint_app_router.py index 80c6074d0..d13a9989a 100644 --- a/tests/mint/test_mint_app_router.py +++ b/tests/mint/test_mint_app_router.py @@ -70,6 +70,9 @@ async def mint_quote(payload): state=SimpleNamespace(value="UNPAID"), expiry=123, pubkey=payload.pubkey, + amount_paid=0, + amount_issued=0, + updated_at=123, ) async def get_mint_quote(quote): @@ -82,6 +85,9 @@ async def get_mint_quote(quote): state=SimpleNamespace(value="UNPAID"), expiry=123, pubkey=None, + amount_paid=0, + amount_issued=0, + updated_at=123, ) async def melt_quote(payload): From 67e3282d9ad459d76f4542da52b08ee08eec9229 Mon Sep 17 00:00:00 2001 From: a1denvalu3 Date: Tue, 23 Jun 2026 22:03:00 +0200 Subject: [PATCH 4/6] refactor(wallet): encapsulate stale quote check logic in MintQuote, remove getattr --- cashu/core/base.py | 61 +++++++++++++++++++++++++++++++++++------- cashu/wallet/wallet.py | 39 +++++---------------------- 2 files changed, 59 insertions(+), 41 deletions(-) diff --git a/cashu/core/base.py b/cashu/core/base.py index 05def465b..d4fc6c289 100644 --- a/cashu/core/base.py +++ b/cashu/core/base.py @@ -471,11 +471,12 @@ def from_row(cls, row: Row): @classmethod def from_resp_wallet(cls, mint_quote_resp, mint: str, amount: int, unit: str): # Prefer amount_paid/amount_issued if present - if getattr(mint_quote_resp, "amount_paid", None) is not None and getattr(mint_quote_resp, "amount_issued", None) is not None: - amount_paid = mint_quote_resp.amount_paid - amount_issued = mint_quote_resp.amount_issued + amount_paid = mint_quote_resp.amount_paid + amount_issued = mint_quote_resp.amount_issued + + if amount_paid is not None and amount_issued is not None: if amount_paid == 0 and amount_issued == 0: - if getattr(mint_quote_resp, "state", None) == "PENDING": + if mint_quote_resp.state == "PENDING": state = MintQuoteState.pending else: state = MintQuoteState.unpaid @@ -485,7 +486,7 @@ def from_resp_wallet(cls, mint_quote_resp, mint: str, amount: int, unit: str): state = MintQuoteState.issued else: state = MintQuoteState.unpaid - elif getattr(mint_quote_resp, "state", None): + elif mint_quote_resp.state: state = MintQuoteState(mint_quote_resp.state) else: state = MintQuoteState.unpaid @@ -495,10 +496,8 @@ def from_resp_wallet(cls, mint_quote_resp, mint: str, amount: int, unit: str): method="bolt11", request=mint_quote_resp.request, checking_id="", - unit=mint_quote_resp.unit - or unit, # BACKWARDS COMPATIBILITY mint response < 0.17.0 - amount=mint_quote_resp.amount - or amount, # BACKWARDS COMPATIBILITY mint response < 0.17.0 + unit=mint_quote_resp.unit or unit, # BACKWARDS COMPATIBILITY mint response < 0.17.0 + amount=mint_quote_resp.amount or amount, # BACKWARDS COMPATIBILITY mint response < 0.17.0 state=state, mint=mint, expiry=mint_quote_resp.expiry, @@ -506,6 +505,50 @@ def from_resp_wallet(cls, mint_quote_resp, mint: str, amount: int, unit: str): pubkey=mint_quote_resp.pubkey, ) + @classmethod + def check_stale_and_from_resp_wallet( + cls, + mint_quote_resp, + mint: str, + mint_quote_local: Optional["MintQuote"] = None, + default_amount: int = 0, + default_unit: str = "sat", + ) -> "MintQuote": + # Check if the response is stale according to the spec + is_stale = False + if mint_quote_local: + resp_updated_at = mint_quote_resp.updated_at + resp_amount_paid = mint_quote_resp.amount_paid + resp_amount_issued = mint_quote_resp.amount_issued + + if ( + (resp_updated_at is not None and resp_updated_at < mint_quote_local.updated_at) + or (resp_amount_paid is not None and resp_amount_paid < mint_quote_local.amount_paid) + or (resp_amount_issued is not None and resp_amount_issued < mint_quote_local.amount_issued) + ): + is_stale = True + + if is_stale and mint_quote_local: + return mint_quote_local + + amount = ( + mint_quote_resp.amount or mint_quote_local.amount + if mint_quote_local + else default_amount + ) + unit = ( + mint_quote_resp.unit or mint_quote_local.unit + if mint_quote_local + else default_unit + ) + + return cls.from_resp_wallet( + mint_quote_resp, + mint=mint, + amount=amount, + unit=unit, + ) + @property def amount_paid(self) -> int: if self.state in [MintQuoteState.paid, MintQuoteState.issued]: diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index 89b92ca56..5a07c1fff 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -570,38 +570,13 @@ async def get_mint_quote( mint_quote_response = await super().get_mint_quote(quote_id) mint_quote_local = await get_bolt11_mint_quote(db=self.db, quote=quote_id) - # Check if the response is stale according to the spec - is_stale = False - if mint_quote_local: - response_updated_at = getattr(mint_quote_response, "updated_at", None) - if response_updated_at is not None and response_updated_at < mint_quote_local.updated_at: - is_stale = True - - response_amount_paid = getattr(mint_quote_response, "amount_paid", None) - if response_amount_paid is not None and response_amount_paid < mint_quote_local.amount_paid: - is_stale = True - - response_amount_issued = getattr(mint_quote_response, "amount_issued", None) - if response_amount_issued is not None and response_amount_issued < mint_quote_local.amount_issued: - is_stale = True - - if is_stale and mint_quote_local: - mint_quote = mint_quote_local - else: - mint_quote = MintQuote.from_resp_wallet( - mint_quote_response, - mint=self.url, - amount=( - mint_quote_response.amount or mint_quote_local.amount - if mint_quote_local - else 0 # BACKWARD COMPATIBILITY mint response < 0.17.0 - ), - unit=( - mint_quote_response.unit or mint_quote_local.unit - if mint_quote_local - else self.unit.name # BACKWARD COMPATIBILITY mint response < 0.17.0 - ), - ) + mint_quote = MintQuote.check_stale_and_from_resp_wallet( + mint_quote_resp=mint_quote_response, + mint=self.url, + mint_quote_local=mint_quote_local, + default_amount=0, + default_unit=self.unit.name, + ) if mint_quote_local and mint_quote_local.privkey: mint_quote.privkey = mint_quote_local.privkey From 08fdb5cf4fc357b9204e5f4b99ca1d8b8ecd133d Mon Sep 17 00:00:00 2001 From: a1denvalu3 Date: Fri, 26 Jun 2026 13:02:03 +0200 Subject: [PATCH 5/6] fix(wallet): resolve staleness checking bug by preserving mint timestamps --- cashu/core/base.py | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/cashu/core/base.py b/cashu/core/base.py index d4fc6c289..008965fdb 100644 --- a/cashu/core/base.py +++ b/cashu/core/base.py @@ -469,7 +469,15 @@ def from_row(cls, row: Row): ) @classmethod - def from_resp_wallet(cls, mint_quote_resp, mint: str, amount: int, unit: str): + def from_resp_wallet( + cls, + mint_quote_resp, + mint: str, + amount: int, + unit: str, + paid_time: Optional[int] = None, + issued_time: Optional[int] = None, + ): # Prefer amount_paid/amount_issued if present amount_paid = mint_quote_resp.amount_paid amount_issued = mint_quote_resp.amount_issued @@ -491,6 +499,14 @@ def from_resp_wallet(cls, mint_quote_resp, mint: str, amount: int, unit: str): else: state = MintQuoteState.unpaid + if paid_time is None and mint_quote_resp.updated_at is not None: + if state in [MintQuoteState.paid, MintQuoteState.issued]: + paid_time = mint_quote_resp.updated_at + + if issued_time is None and mint_quote_resp.updated_at is not None: + if state == MintQuoteState.issued: + issued_time = mint_quote_resp.updated_at + return cls( quote=mint_quote_resp.quote, method="bolt11", @@ -502,6 +518,8 @@ def from_resp_wallet(cls, mint_quote_resp, mint: str, amount: int, unit: str): mint=mint, expiry=mint_quote_resp.expiry, created_time=int(time.time()), + paid_time=paid_time, + issued_time=issued_time, pubkey=mint_quote_resp.pubkey, ) @@ -532,21 +550,26 @@ def check_stale_and_from_resp_wallet( return mint_quote_local amount = ( - mint_quote_resp.amount or mint_quote_local.amount + (mint_quote_resp.amount or mint_quote_local.amount) if mint_quote_local else default_amount ) unit = ( - mint_quote_resp.unit or mint_quote_local.unit + (mint_quote_resp.unit or mint_quote_local.unit) if mint_quote_local else default_unit ) + paid_time = mint_quote_local.paid_time if mint_quote_local else None + issued_time = mint_quote_local.issued_time if mint_quote_local else None + return cls.from_resp_wallet( mint_quote_resp, mint=mint, amount=amount, unit=unit, + paid_time=paid_time, + issued_time=issued_time, ) @property From e1ec396b42b0c6bd5462919813e5090d4244aae8 Mon Sep 17 00:00:00 2001 From: a1denvalu3 Date: Fri, 26 Jun 2026 16:16:39 +0200 Subject: [PATCH 6/6] fix(wallet): avoid comparing client-generated clocks in quote staleness check --- cashu/core/base.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cashu/core/base.py b/cashu/core/base.py index 008965fdb..bea6ef720 100644 --- a/cashu/core/base.py +++ b/cashu/core/base.py @@ -538,9 +538,12 @@ def check_stale_and_from_resp_wallet( resp_updated_at = mint_quote_resp.updated_at resp_amount_paid = mint_quote_resp.amount_paid resp_amount_issued = mint_quote_resp.amount_issued + local_updated_at = ( + mint_quote_local.issued_time or mint_quote_local.paid_time + ) if ( - (resp_updated_at is not None and resp_updated_at < mint_quote_local.updated_at) + (resp_updated_at is not None and local_updated_at is not None and resp_updated_at < local_updated_at) or (resp_amount_paid is not None and resp_amount_paid < mint_quote_local.amount_paid) or (resp_amount_issued is not None and resp_amount_issued < mint_quote_local.amount_issued) ):