diff --git a/cashu/core/base.py b/cashu/core/base.py index 3d9312a2..d5034f64 100644 --- a/cashu/core/base.py +++ b/cashu/core/base.py @@ -290,8 +290,9 @@ class MeltQuote(LedgerEvent): created_time: Union[int, None] = None paid_time: Union[int, None] = None fee_paid: int = 0 - proof: str = "" + payment_preimage: str = "" expiry: Optional[int] = None + change: Optional[List[BlindedSignature]] = None @classmethod def from_row(cls, row: Row): @@ -304,6 +305,11 @@ def from_row(cls, row: Row): ) paid_time = int(row["paid_time"].timestamp()) if row["paid_time"] else None + # parse change from row as json + change = None + if row["change"]: + change = json.loads(row["change"]) + return cls( quote=row["quote"], method=row["method"], @@ -317,7 +323,9 @@ def from_row(cls, row: Row): created_time=created_time, paid_time=paid_time, fee_paid=row["fee_paid"], - proof=row["proof"], + change=change, + expiry=row["expiry"], + payment_preimage=row["proof"], ) @property diff --git a/cashu/core/models.py b/cashu/core/models.py index 9d1b823a..f4cea2b1 100644 --- a/cashu/core/models.py +++ b/cashu/core/models.py @@ -183,6 +183,8 @@ class PostMeltQuoteResponse(BaseModel): paid: bool # whether the request has been paid # DEPRECATED as per NUT PR #136 state: str # state of the quote expiry: Optional[int] # expiry of the quote + payment_preimage: Optional[str] = None # payment preimage + change: Union[List[BlindedSignature], None] = None @classmethod def from_melt_quote(self, melt_quote: MeltQuote) -> "PostMeltQuoteResponse": @@ -203,9 +205,9 @@ class PostMeltRequest(BaseModel): ) -class PostMeltResponse(BaseModel): +class PostMeltResponse_deprecated(BaseModel): paid: Union[bool, None] - payment_preimage: Union[str, None] + preimage: Union[str, None] change: Union[List[BlindedSignature], None] = None @@ -217,12 +219,6 @@ class PostMeltRequest_deprecated(BaseModel): ) -class PostMeltResponse_deprecated(BaseModel): - paid: Union[bool, None] - preimage: Union[str, None] - change: Union[List[BlindedSignature], None] = None - - # ------- API: SPLIT ------- diff --git a/cashu/mint/crud.py b/cashu/mint/crud.py index f6fe5543..1d8e8dc7 100644 --- a/cashu/mint/crud.py +++ b/cashu/mint/crud.py @@ -1,3 +1,4 @@ +import json from abc import ABC, abstractmethod from typing import Any, List, Optional @@ -548,8 +549,8 @@ async def store_melt_quote( await (conn or db).execute( f""" INSERT INTO {table_with_schema(db, 'melt_quotes')} - (quote, method, request, checking_id, unit, amount, fee_reserve, paid, state, created_time, paid_time, fee_paid, proof) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + (quote, method, request, checking_id, unit, amount, fee_reserve, paid, state, created_time, paid_time, fee_paid, proof, change, expiry) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( quote.quote, @@ -564,7 +565,9 @@ async def store_melt_quote( timestamp_from_seconds(db, quote.created_time), timestamp_from_seconds(db, quote.paid_time), quote.fee_paid, - quote.proof, + quote.payment_preimage, + json.dumps(quote.change) if quote.change else None, + quote.expiry, ), ) @@ -612,13 +615,14 @@ async def update_melt_quote( ) -> None: await (conn or db).execute( f"UPDATE {table_with_schema(db, 'melt_quotes')} SET paid = ?, state = ?," - " fee_paid = ?, paid_time = ?, proof = ? WHERE quote = ?", + " fee_paid = ?, paid_time = ?, proof = ?, change = ? WHERE quote = ?", ( quote.paid, quote.state.name, quote.fee_paid, timestamp_from_seconds(db, quote.paid_time), - quote.proof, + quote.payment_preimage, + json.dumps([s.dict() for s in quote.change]) if quote.change else None, quote.quote, ), ) diff --git a/cashu/mint/ledger.py b/cashu/mint/ledger.py index 21c95506..57aff803 100644 --- a/cashu/mint/ledger.py +++ b/cashu/mint/ledger.py @@ -158,7 +158,7 @@ async def _check_pending_proofs_and_melt_quotes(self): quote.state = MeltQuoteState.paid if payment.fee: quote.fee_paid = payment.fee.to(Unit[quote.unit]).amount - quote.proof = payment.preimage or "" + quote.payment_preimage = payment.preimage or "" await self.crud.update_melt_quote(quote=quote, db=self.db) # invalidate proofs await self._invalidate_proofs( @@ -740,7 +740,7 @@ async def get_melt_quote(self, quote_id: str) -> MeltQuote: if status.fee: melt_quote.fee_paid = status.fee.to(unit).amount if status.preimage: - melt_quote.proof = 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) @@ -831,7 +831,7 @@ async def melt( proofs: List[Proof], quote: str, outputs: Optional[List[BlindedMessage]] = None, - ) -> Tuple[str, List[BlindedSignature]]: + ) -> PostMeltQuoteResponse: """Invalidates proofs and pays a Lightning invoice. Args: @@ -915,13 +915,11 @@ async def melt( to_unit=unit, round="up" ).amount if payment.preimage: - melt_quote.proof = payment.preimage + melt_quote.payment_preimage = payment.preimage # set quote as paid melt_quote.paid = True melt_quote.state = MeltQuoteState.paid 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) # melt successful, invalidate proofs await self._invalidate_proofs(proofs=proofs, quote_id=melt_quote.quote) @@ -936,6 +934,11 @@ async def melt( keyset=self.keysets[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) + except Exception as e: logger.trace(f"Melt exception: {e}") raise e @@ -943,7 +946,7 @@ async def melt( # delete proofs from pending list await self.db_write._unset_proofs_pending(proofs) - return melt_quote.proof or "", return_promises + return PostMeltQuoteResponse.from_melt_quote(melt_quote) async def split( self, diff --git a/cashu/mint/migrations.py b/cashu/mint/migrations.py index cbf26454..3c03250f 100644 --- a/cashu/mint/migrations.py +++ b/cashu/mint/migrations.py @@ -815,3 +815,13 @@ async def m020_add_state_to_mint_and_melt_quotes(db: Database): await conn.execute( f"UPDATE {table_with_schema(db, 'melt_quotes')} SET state = '{state}' WHERE quote = '{row['quote']}'" ) + + +async def m021_add_change_and_expiry_to_melt_quotes(db: Database): + async with db.connect() as conn: + await conn.execute( + f"ALTER TABLE {table_with_schema(db, 'melt_quotes')} ADD COLUMN change TEXT" + ) + await conn.execute( + f"ALTER TABLE {table_with_schema(db, 'melt_quotes')} ADD COLUMN expiry TIMESTAMP" + ) diff --git a/cashu/mint/router.py b/cashu/mint/router.py index 8236279a..9a3c26d7 100644 --- a/cashu/mint/router.py +++ b/cashu/mint/router.py @@ -15,7 +15,6 @@ PostMeltQuoteRequest, PostMeltQuoteResponse, PostMeltRequest, - PostMeltResponse, PostMintQuoteRequest, PostMintQuoteResponse, PostMintRequest, @@ -290,24 +289,21 @@ async def get_melt_quote(request: Request, quote: str) -> PostMeltQuoteResponse: "Melt tokens for a Bitcoin payment that the mint will make for the user in" " exchange" ), - response_model=PostMeltResponse, + response_model=PostMeltQuoteResponse, response_description=( "The state of the payment, a preimage as proof of payment, and a list of" " promises for change." ), ) @limiter.limit(f"{settings.mint_transaction_rate_limit_per_minute}/minute") -async def melt(request: Request, payload: PostMeltRequest) -> PostMeltResponse: +async def melt(request: Request, payload: PostMeltRequest) -> PostMeltQuoteResponse: """ Requests tokens to be destroyed and sent out via Lightning. """ logger.trace(f"> POST /v1/melt/bolt11: {payload}") - preimage, change_promises = await ledger.melt( + resp = await ledger.melt( proofs=payload.inputs, quote=payload.quote, outputs=payload.outputs ) - resp = PostMeltResponse( - paid=True, payment_preimage=preimage, change=change_promises - ) logger.trace(f"< POST /v1/melt/bolt11: {resp}") return resp diff --git a/cashu/mint/router_deprecated.py b/cashu/mint/router_deprecated.py index 0fccc60b..74d87c7e 100644 --- a/cashu/mint/router_deprecated.py +++ b/cashu/mint/router_deprecated.py @@ -230,11 +230,11 @@ async def melt_deprecated( quote = await ledger.melt_quote( PostMeltQuoteRequest(request=payload.pr, unit="sat") ) - preimage, change_promises = await ledger.melt( + melt_resp = await ledger.melt( proofs=payload.proofs, quote=quote.quote, outputs=outputs ) resp = PostMeltResponse_deprecated( - paid=True, preimage=preimage, change=change_promises + paid=True, preimage=melt_resp.payment_preimage, change=melt_resp.change ) logger.trace(f"< POST /melt: {resp}") return resp diff --git a/cashu/wallet/v1_api.py b/cashu/wallet/v1_api.py index 7c1194c5..5acb7a90 100644 --- a/cashu/wallet/v1_api.py +++ b/cashu/wallet/v1_api.py @@ -33,7 +33,6 @@ PostMeltRequest, PostMeltRequestOptionMpp, PostMeltRequestOptions, - PostMeltResponse, PostMeltResponse_deprecated, PostMintQuoteRequest, PostMintQuoteResponse, @@ -406,7 +405,7 @@ async def melt( quote: str, proofs: List[Proof], outputs: Optional[List[BlindedMessage]], - ) -> PostMeltResponse: + ) -> PostMeltQuoteResponse: """ Accepts proofs and a lightning invoice to pay in exchange. """ @@ -438,13 +437,24 @@ def _meltrequest_include_fields( ret: PostMeltResponse_deprecated = await self.melt_deprecated( proofs=proofs, outputs=outputs, invoice=invoice.bolt11 ) - return PostMeltResponse( - paid=ret.paid, payment_preimage=ret.preimage, change=ret.change + return PostMeltQuoteResponse( + quote=quote, + amount=0, + fee_reserve=0, + paid=ret.paid or False, + state=( + MeltQuoteState.paid.value + if ret.paid + else MeltQuoteState.unpaid.value + ), + payment_preimage=ret.preimage, + change=ret.change, + expiry=None, ) # END backwards compatibility < 0.15.0 self.raise_on_error_request(resp) return_dict = resp.json() - return PostMeltResponse.parse_obj(return_dict) + return PostMeltQuoteResponse.parse_obj(return_dict) @async_set_httpx_client @async_ensure_mint_loaded diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index 96c440e3..ac236e4e 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -28,7 +28,6 @@ from ..core.models import ( PostCheckStateResponse, PostMeltQuoteResponse, - PostMeltResponse, ) from ..core.p2pk import Secret from ..core.settings import settings @@ -639,7 +638,7 @@ async def melt_quote( async def melt( self, proofs: List[Proof], invoice: str, fee_reserve_sat: int, quote_id: str - ) -> PostMeltResponse: + ) -> PostMeltQuoteResponse: """Pays a lightning invoice and returns the status of the payment. Args: diff --git a/cashu/wallet/wallet_deprecated.py b/cashu/wallet/wallet_deprecated.py index b418a548..e5b953a4 100644 --- a/cashu/wallet/wallet_deprecated.py +++ b/cashu/wallet/wallet_deprecated.py @@ -306,7 +306,7 @@ def _mintrequest_include_fields(outputs: List[BlindedMessage]): @async_ensure_mint_loaded_deprecated async def melt_deprecated( self, proofs: List[Proof], invoice: str, outputs: Optional[List[BlindedMessage]] - ): + ) -> PostMeltResponse_deprecated: """ Accepts proofs and a lightning invoice to pay in exchange. """