Skip to content

Commit

Permalink
fix: provide overpaid fees on startup
Browse files Browse the repository at this point in the history
  • Loading branch information
callebtc committed Jan 4, 2025
1 parent 53886fb commit 9975bd6
Show file tree
Hide file tree
Showing 6 changed files with 65 additions and 17 deletions.
6 changes: 6 additions & 0 deletions cashu/core/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,7 @@ class MeltQuote(LedgerEvent):
fee_paid: int = 0
payment_preimage: Optional[str] = None
expiry: Optional[int] = None
outputs: Optional[List[BlindedMessage]] = None
change: Optional[List[BlindedSignature]] = None
mint: Optional[str] = None

Expand All @@ -310,6 +311,10 @@ def from_row(cls, row: Row):
if row["change"]:
change = json.loads(row["change"])

outputs = None
if row["outputs"]:
outputs = json.loads(row["outputs"])

return cls(
quote=row["quote"],
method=row["method"],
Expand All @@ -322,6 +327,7 @@ def from_row(cls, row: Row):
created_time=created_time,
paid_time=paid_time,
fee_paid=row["fee_paid"],
outputs=outputs,
change=change,
expiry=expiry,
payment_preimage=payment_preimage,
Expand Down
14 changes: 10 additions & 4 deletions cashu/mint/crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -440,7 +440,7 @@ async def store_mint_quote(
"paid_time": db.to_timestamp(
db.timestamp_from_seconds(quote.paid_time) or ""
),
"pubkey": quote.pubkey or ""
"pubkey": quote.pubkey or "",
},
)

Expand Down Expand Up @@ -522,8 +522,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, change, expiry)
VALUES (:quote, :method, :request, :checking_id, :unit, :amount, :fee_reserve, :state, :paid, :created_time, :paid_time, :fee_paid, :proof, :change, :expiry)
(quote, method, request, checking_id, unit, amount, fee_reserve, state, paid, created_time, paid_time, fee_paid, proof, outputs, change, expiry)
VALUES (:quote, :method, :request, :checking_id, :unit, :amount, :fee_reserve, :state, :paid, :created_time, :paid_time, :fee_paid, :proof, :outputs, :change, :expiry)
""",
{
"quote": quote.quote,
Expand All @@ -543,6 +543,7 @@ async def store_melt_quote(
),
"fee_paid": quote.fee_paid,
"proof": quote.payment_preimage,
"outputs": json.dumps(quote.outputs) if quote.outputs else None,
"change": json.dumps(quote.change) if quote.change else None,
"expiry": db.to_timestamp(
db.timestamp_from_seconds(quote.expiry) or ""
Expand Down Expand Up @@ -607,7 +608,7 @@ async def update_melt_quote(
) -> None:
await (conn or db).execute(
f"""
UPDATE {db.table_with_schema('melt_quotes')} SET state = :state, fee_paid = :fee_paid, paid_time = :paid_time, proof = :proof, change = :change, checking_id = :checking_id WHERE quote = :quote
UPDATE {db.table_with_schema('melt_quotes')} SET state = :state, fee_paid = :fee_paid, paid_time = :paid_time, proof = :proof, outputs = :outputs, change = :change, checking_id = :checking_id WHERE quote = :quote
""",
{
"state": quote.state.value,
Expand All @@ -616,6 +617,11 @@ async def update_melt_quote(
db.timestamp_from_seconds(quote.paid_time) or ""
),
"proof": quote.payment_preimage,
"outputs": (
json.dumps([s.dict() for s in quote.outputs])
if quote.outputs
else None
),
"change": (
json.dumps([s.dict() for s in quote.change])
if quote.change
Expand Down
11 changes: 10 additions & 1 deletion cashu/mint/db/write.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from loguru import logger

from ...core.base import (
BlindedMessage,
MeltQuote,
MeltQuoteState,
MintQuote,
Expand Down Expand Up @@ -172,7 +173,9 @@ 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, outputs: Optional[List[BlindedMessage]] = None
) -> MeltQuote:
"""Sets the melt quote as pending.
Args:
Expand All @@ -193,6 +196,9 @@ async def _set_melt_quote_pending(self, quote: MeltQuote) -> MeltQuote:
raise TransactionError("Melt quote already pending.")
# set the quote as pending
quote_copy.state = MeltQuoteState.pending

if outputs:
quote_copy.outputs = outputs
await self.crud.update_melt_quote(quote=quote_copy, db=self.db, conn=conn)

await self.events.submit(quote_copy)
Expand All @@ -219,6 +225,9 @@ async def _unset_melt_quote_pending(
raise TransactionError("Melt quote not pending.")
# set the quote as pending
quote_copy.state = state

# unset outputs
quote_copy.outputs = None
await self.crud.update_melt_quote(quote=quote_copy, db=self.db, conn=conn)

await self.events.submit(quote_copy)
Expand Down
34 changes: 25 additions & 9 deletions cashu/mint/ledger.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,17 +144,17 @@ async def shutdown_ledger(self):
task.cancel()

async def _check_pending_proofs_and_melt_quotes(self):
"""Startup routine that checks all pending proofs for their melt state and either invalidates
them for a successful melt or deletes them if the melt failed.
"""Startup routine that checks all pending melt quotes and either invalidates
their pending proofs for a successful melt or deletes them if the melt failed.
"""
# get all pending melt quotes
melt_quotes = await self.crud.get_all_melt_quotes_from_pending_proofs(
pending_melt_quotes = await self.crud.get_all_melt_quotes_from_pending_proofs(
db=self.db
)
if not melt_quotes:
if not pending_melt_quotes:
return
logger.info("Checking pending melt quotes")
for quote in melt_quotes:
logger.info(f"Checking {len(pending_melt_quotes)} pending melt quotes")
for quote in pending_melt_quotes:
quote = await self.get_melt_quote(quote_id=quote.quote)
logger.info(f"Melt quote {quote.quote} state: {quote.state}")

Expand Down Expand Up @@ -772,13 +772,27 @@ async def get_melt_quote(self, quote_id: str, rollback_unknown=False) -> MeltQuo
if status.preimage:
melt_quote.payment_preimage = status.preimage
melt_quote.paid_time = int(time.time())
await self.crud.update_melt_quote(quote=melt_quote, db=self.db)
await self.events.submit(melt_quote)
pending_proofs = await self.crud.get_pending_proofs_for_quote(
quote_id=quote_id, db=self.db
)
await self._invalidate_proofs(proofs=pending_proofs, quote_id=quote_id)
await self.db_write._unset_proofs_pending(pending_proofs)
# change to compensate wallet for overpaid fees
if melt_quote.outputs:
total_provided = sum_proofs(pending_proofs)
input_fees = self.get_fees_for_proofs(pending_proofs)
fee_reserve_provided = (
total_provided - melt_quote.amount - input_fees
)
return_promises = await self._generate_change_promises(
fee_provided=fee_reserve_provided,
fee_paid=melt_quote.fee_paid,
outputs=melt_quote.outputs,
keyset=self.keysets[melt_quote.outputs[0].id],
)
melt_quote.change = return_promises
await self.crud.update_melt_quote(quote=melt_quote, db=self.db)
await self.events.submit(melt_quote)
if status.failed or (rollback_unknown and status.unknown):
logger.debug(f"Setting quote {quote_id} as unpaid")
melt_quote.state = MeltQuoteState.unpaid
Expand Down Expand Up @@ -909,6 +923,8 @@ async def melt(
raise TransactionError(
f"output unit {outputs_unit.name} does not match quote unit {melt_quote.unit}"
)
# we don't need to set it here, _set_melt_quote_pending will set it in the db
melt_quote.outputs = outputs

# verify that the amount of the input proofs is equal to the amount of the quote
total_provided = sum_proofs(proofs)
Expand Down Expand Up @@ -939,7 +955,7 @@ async def melt(
proofs, quote_id=melt_quote.quote
)
previous_state = melt_quote.state
melt_quote = await self.db_write._set_melt_quote_pending(melt_quote)
melt_quote = await self.db_write._set_melt_quote_pending(melt_quote, outputs)

# if the melt corresponds to an internal mint, mark both as paid
melt_quote = await self.melt_mint_settle_internally(melt_quote, proofs)
Expand Down
13 changes: 12 additions & 1 deletion cashu/mint/migrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -839,11 +839,22 @@ async def m022_quote_set_states_to_values(db: Database):
f"UPDATE {db.table_with_schema('mint_quotes')} SET state = '{mint_quote_states.value}' WHERE state = '{mint_quote_states.name}'"
)


async def m023_add_key_to_mint_quote_table(db: Database):
async with db.connect() as conn:
await conn.execute(
f"""
ALTER TABLE {db.table_with_schema('mint_quotes')}
ADD COLUMN pubkey TEXT DEFAULT NULL
"""
)
)


async def m024_add_melt_quote_outputs(db: Database):
async with db.connect() as conn:
await conn.execute(
f"""
ALTER TABLE {db.table_with_schema('melt_quotes')}
ADD COLUMN outputs TEXT DEFAULT NULL
"""
)
4 changes: 2 additions & 2 deletions cashu/wallet/transactions.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,9 @@ def coinselect(
remainder = amount_to_send
selected_proofs = [smaller_proofs[0]]
fee_ppk = self.get_fees_for_proofs_ppk(selected_proofs) if include_fees else 0
logger.debug(f"adding proof: {smaller_proofs[0].amount} – fee: {fee_ppk} ppk")
logger.trace(f"adding proof: {smaller_proofs[0].amount} – fee: {fee_ppk} ppk")
remainder -= smaller_proofs[0].amount - fee_ppk / 1000
logger.debug(f"remainder: {remainder}")
logger.trace(f"remainder: {remainder}")
if remainder > 0:
logger.trace(
f"> selecting more proofs from {amount_summary(smaller_proofs[1:], self.unit)} sum: {sum_proofs(smaller_proofs[1:])} to reach {remainder}"
Expand Down

0 comments on commit 9975bd6

Please sign in to comment.