Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 117 additions & 6 deletions cashu/core/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -469,23 +469,134 @@ 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

if amount_paid is not None and amount_issued is not None:
if amount_paid == 0 and amount_issued == 0:
if mint_quote_resp.state == "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 mint_quote_resp.state:
state = MintQuoteState(mint_quote_resp.state)
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",
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
state=MintQuoteState(mint_quote_resp.state),
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,
created_time=int(time.time()),
paid_time=paid_time,
issued_time=issued_time,
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
local_updated_at = (
mint_quote_local.issued_time or mint_quote_local.paid_time
)

if (
(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)
):
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
)

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
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"""
Expand Down
10 changes: 8 additions & 2 deletions cashu/core/models/mint_quote.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,19 @@ 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
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)
15 changes: 12 additions & 3 deletions cashu/mint/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,9 +178,12 @@ async def mint_quote(
request=quote.request,
amount=quote.amount,
unit=quote.unit,
state=quote.state.value,
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
Expand All @@ -204,9 +207,12 @@ async def get_mint_quote(request: Request, quote: str) -> PostMintQuoteResponse:
request=mint_quote.request,
amount=mint_quote.amount,
unit=mint_quote.unit,
state=mint_quote.state.value,
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
Expand All @@ -231,9 +237,12 @@ async def mint_quote_check(
request=quote.request,
amount=quote.amount,
unit=quote.unit,
state=quote.state.value,
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
]
Expand Down
26 changes: 14 additions & 12 deletions cashu/wallet/wallet.py
Original file line number Diff line number Diff line change
Expand Up @@ -569,25 +569,27 @@ 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_quote = MintQuote.check_stale_and_from_resp_wallet(
mint_quote_resp=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_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

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

Expand Down
8 changes: 8 additions & 0 deletions tests/mint/test_mint_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
6 changes: 6 additions & 0 deletions tests/mint/test_mint_app_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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):
Expand Down
Loading