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
3 changes: 2 additions & 1 deletion cashu/mint/auth/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,8 @@ async def verify_blind_auth(self, blind_auth_token):

try:
yield
await self._invalidate_proofs(proofs=[proof])
# We do not calculate fees for auth keysets
await self.db_write.invalidate_proofs(proofs=[proof], keysets=self.keysets)
except Exception as e:
logger.error(f"Blind auth error: {e}")
raise BlindAuthFailedError()
Expand Down
168 changes: 165 additions & 3 deletions cashu/mint/db/write.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import time
from typing import Dict, List, Optional, Union

from loguru import logger
Expand Down Expand Up @@ -45,6 +46,7 @@ async def _verify_spent_proofs_and_set_pending(
proofs: List[Proof],
keysets: Dict[str, MintKeyset],
quote_id: Optional[str] = None,
conn: Optional[Connection] = None,
) -> None:
"""
Method to check if proofs are already spent. If they are not spent, we check if they are pending.
Expand All @@ -53,6 +55,7 @@ async def _verify_spent_proofs_and_set_pending(
proofs (List[Proof]): Proofs to add to pending table.
keysets (Dict[str, MintKeyset]): Keysets of the mint (needed to update keyset balances)
quote_id (Optional[str]): Melt quote ID. If it is not set, we assume the pending tokens to be from a swap.
conn (Optional[Connection]): Connection to use. If not set, a new connection will be created.
Raises:
TransactionError: If any one of the proofs is already spent or pending.
"""
Expand All @@ -62,6 +65,7 @@ async def _verify_spent_proofs_and_set_pending(
async with self.db.get_connection(
lock_table="proofs_pending",
lock_timeout=1,
conn=conn,
) as conn:
logger.trace("checking whether proofs are already spent")
await self.db_read._verify_proofs_spendable(proofs, conn)
Expand Down Expand Up @@ -101,7 +105,7 @@ async def _unset_proofs_pending(
spent (bool): Whether the proofs have been spent or not. Defaults to True.
This should be False if the proofs were NOT invalidated before calling this function.
It is used to emit the unspent state for the proofs (otherwise the spent state is emitted
by the _invalidate_proofs function when the proofs are spent).
by the invalidate_proofs function when the proofs are spent).
conn (Optional[Connection]): Connection to use. If not set, a new connection will be created.
"""
async with self.db.get_connection(conn) as conn:
Expand Down Expand Up @@ -204,11 +208,14 @@ async def _unset_mint_quote_pending(
await self.events.submit(quote)
return quote

async def _set_melt_quote_pending(self, quote: MeltQuote) -> MeltQuote:
async def _set_melt_quote_pending(
self, quote: MeltQuote, conn: Optional[Connection] = None
) -> MeltQuote:
"""Sets the melt quote as pending.

Args:
quote (MeltQuote): Melt quote to set as pending.
conn (Optional[Connection]): Connection to use. If not set, a new connection will be created.
"""
quote_copy = quote.model_copy()
if not quote.checking_id:
Expand All @@ -217,6 +224,7 @@ async def _set_melt_quote_pending(self, quote: MeltQuote) -> MeltQuote:
lock_table="melt_quotes",
lock_select_statement="checking_id = :checking_id",
lock_parameters={"checking_id": quote.checking_id},
conn=conn,
) as conn:
# get all melt quotes with same checking_id from db and check if there is one already pending or paid
quotes_db = await self.crud.get_melt_quotes_by_checking_id(
Expand All @@ -239,13 +247,17 @@ async def _set_melt_quote_pending(self, quote: MeltQuote) -> MeltQuote:
return quote_copy

async def _unset_melt_quote_pending(
self, quote: MeltQuote, state: MeltQuoteState
self,
quote: MeltQuote,
state: MeltQuoteState,
conn: Optional[Connection] = None,
) -> MeltQuote:
"""Unsets the melt quote as pending.

Args:
quote (MeltQuote): Melt quote to unset as pending.
state (MeltQuoteState): New state of the melt quote.
conn (Optional[Connection]): Connection to use. If not set, a new connection will be created.
Raises:
TransactionError: If the melt quote is not found or not pending.
"""
Expand All @@ -254,6 +266,7 @@ async def _unset_melt_quote_pending(
lock_table="melt_quotes",
lock_select_statement="quote = :quote",
lock_parameters={"quote": quote.quote},
conn=conn
) as conn:
# get melt quote from db and check if it is pending
quote_db = await self.crud.get_melt_quote(
Expand Down Expand Up @@ -342,3 +355,152 @@ async def _store_melt_quote(self, quote: MeltQuote):

# store the melt quote
await self.crud.store_melt_quote(quote=quote, db=self.db, conn=conn)

async def verify_and_set_melt_quote_pending(
self,
quote: MeltQuote,
proofs: List[Proof],
keysets: Dict[str, MintKeyset],
) -> MeltQuote:
"""Sets the melt quote and proofs as pending in a single transaction.

Args:
quote (MeltQuote): Melt quote to set as pending.
proofs (List[Proof]): Proofs to set as pending.
keysets (Dict[str, MintKeyset]): Keysets for updating balances.

Returns:
MeltQuote: Updated melt quote object.
"""
async with self.db.get_connection(
lock_table="proofs_pending",
lock_timeout=1,
) as conn:
await self._verify_spent_proofs_and_set_pending(
proofs, keysets, quote_id=quote.quote, conn=conn
)
quote = await self._set_melt_quote_pending(quote, conn=conn)

return quote

async def unset_melt_quote_pending_and_proofs(
self,
quote: MeltQuote,
proofs: List[Proof],
keysets: Dict[str, MintKeyset],
state: MeltQuoteState,
) -> MeltQuote:
"""Unsets the melt quote and proofs as pending in a single transaction.

Args:
quote (MeltQuote): Melt quote to update.
proofs (List[Proof]): Proofs to unset as pending.
keysets (Dict[str, MintKeyset]): Keysets for updating balances.
state (MeltQuoteState): New state for the melt quote (e.g. UNPAID).
"""
async with self.db.get_connection(
lock_table="proofs_pending",
lock_timeout=1,
) as conn:
await self._unset_proofs_pending(proofs, keysets, spent=False, conn=conn)
quote = await self._unset_melt_quote_pending(quote, state, conn=conn)
# Clean up blinded messages associated with this melt
await self.crud.delete_blinded_messages_melt_id(
melt_id=quote.quote, db=self.db, conn=conn
)

return quote

async def invalidate_proofs(
self,
proofs: List[Proof],
keysets: Dict[str, MintKeyset],
quote_id: Optional[str] = None,
keyset_fees: Optional[Dict[str, int]] = None,
conn: Optional[Connection] = None,
) -> None:
"""Invalidates proofs (spends them) and updates keyset balances and fees.

Args:
proofs (List[Proof]): Proofs to invalidate.
keysets (Dict[str, MintKeyset]): Keysets to update.
quote_id (Optional[str]): Melt quote ID if applicable.
keyset_fees (Optional[Dict[str, int]]): Fees paid per keyset.
conn (Optional[Connection]): Database connection.
"""
async with self.db.get_connection(conn) as conn:
# Invalidate proofs (spend them)
# This bumps balance down.
for p in proofs:
logger.trace(f"Invalidating proof {p.Y}")
await self.crud.invalidate_proof(
proof=p, db=self.db, quote_id=quote_id, conn=conn
)
await self.crud.bump_keyset_balance(
db=self.db,
keyset=keysets[p.id],
amount=-p.amount,
conn=conn,
)
await self.events.submit(
ProofState(
Y=p.Y, state=ProofSpentState.spent, witness=p.witness or None
)
)

# Update fees
if keyset_fees:
for keyset_id, fee in keyset_fees.items():
if fee > 0:
await self.crud.bump_keyset_fees_paid(
keyset=keysets[keyset_id],
amount=fee,
db=self.db,
conn=conn,
)

async def set_melt_quote_paid_and_invalidate_proofs(
self,
quote: MeltQuote,
proofs: List[Proof],
keysets: Dict[str, MintKeyset],
keyset_fees: Dict[str, int],
) -> MeltQuote:
"""Sets the melt quote as PAID and invalidates proofs in a single transaction.

Args:
quote (MeltQuote): Melt quote to set as PAID.
proofs (List[Proof]): Proofs to invalidate (spend).
keysets (Dict[str, MintKeyset]): Keysets for updating balances/fees.
keyset_fees (Dict[str, int]): Fees paid per keyset.
"""
quote_copy = quote.model_copy()

async with self.db.get_connection(
lock_table="proofs_pending",
Comment thread
a1denvalu3 marked this conversation as resolved.
lock_timeout=1,
) as conn:
# 1. Unset proofs PENDING
# This bumps balance back up.
await self._unset_proofs_pending(proofs, keysets, spent=True, conn=conn)

# 2. Invalidate proofs (spend them) and update fees
await self.invalidate_proofs(
proofs=proofs,
keysets=keysets,
quote_id=quote.quote,
keyset_fees=keyset_fees,
conn=conn,
)

# 3. Update melt quote to PAID
if quote_copy.state != MeltQuoteState.paid:
quote_copy.state = MeltQuoteState.paid
quote_copy.paid_time = int(time.time())
await self.crud.update_melt_quote(quote=quote_copy, db=self.db, conn=conn)

# Events
await self.events.submit(quote_copy)

return quote_copy

Loading
Loading