diff --git a/cashu/core/models.py b/cashu/core/models.py index 81711cd..7777a8f 100644 --- a/cashu/core/models.py +++ b/cashu/core/models.py @@ -131,12 +131,34 @@ class PostMintResponse_deprecated(BaseModel): # ------- API: MELT QUOTE ------- +class PostMeltRequestOptionMpp(BaseModel): + amount: int = Field(gt=0) # input amount + + +class PostMeltRequestOptions(BaseModel): + mpp: Optional[PostMeltRequestOptionMpp] + + class PostMeltQuoteRequest(BaseModel): unit: str = Field(..., max_length=settings.mint_max_request_length) # input unit request: str = Field( ..., max_length=settings.mint_max_request_length ) # output payment request - amount: Optional[int] = Field(default=None, gt=0) # input amount + options: Optional[PostMeltRequestOptions] = None + + @property + def is_mpp(self) -> bool: + if self.options and self.options.mpp: + return True + else: + return False + + @property + def mpp_amount(self) -> int: + if self.is_mpp and self.options and self.options.mpp: + return self.options.mpp.amount + else: + raise Exception("quote request is not mpp.") class PostMeltQuoteResponse(BaseModel): diff --git a/cashu/lightning/lndrest.py b/cashu/lightning/lndrest.py index 187912f..477e84f 100644 --- a/cashu/lightning/lndrest.py +++ b/cashu/lightning/lndrest.py @@ -376,8 +376,8 @@ class LndRestWallet(LightningBackend): ) -> PaymentQuoteResponse: # get amount from melt_quote or from bolt11 amount = ( - Amount(Unit[melt_quote.unit], melt_quote.amount) - if melt_quote.amount + Amount(Unit[melt_quote.unit], melt_quote.mpp_amount) + if melt_quote.is_mpp else None ) diff --git a/cashu/mint/ledger.py b/cashu/mint/ledger.py index cb1c1bb..01cb936 100644 --- a/cashu/mint/ledger.py +++ b/cashu/mint/ledger.py @@ -519,6 +519,65 @@ class Ledger(LedgerVerification, LedgerSpendingConditions): del self.locks[quote_id] return promises + def create_internal_melt_quote( + self, mint_quote: MintQuote, melt_quote: PostMeltQuoteRequest + ) -> PaymentQuoteResponse: + unit, method = self._verify_and_get_unit_method( + melt_quote.unit, Method.bolt11.name + ) + # NOTE: we normalize the request to lowercase to avoid case sensitivity + # This works with Lightning but might not work with other methods + request = melt_quote.request.lower() + + if not request == mint_quote.request: + raise TransactionError("bolt11 requests do not match") + if not mint_quote.unit == melt_quote.unit: + raise TransactionError("units do not match") + if not mint_quote.method == method.name: + raise TransactionError("methods do not match") + if mint_quote.paid: + raise TransactionError("mint quote already paid") + if mint_quote.issued: + raise TransactionError("mint quote already issued") + if not mint_quote.checking_id: + raise TransactionError("mint quote has no checking id") + if melt_quote.is_mpp: + raise TransactionError("internal payments do not support mpp") + + internal_fee = Amount(unit, 0) # no internal fees + amount = Amount(unit, mint_quote.amount) + + payment_quote = PaymentQuoteResponse( + checking_id=mint_quote.checking_id, + amount=amount, + fee=internal_fee, + ) + logger.info( + f"Issuing internal melt quote: {request} ->" + f" {mint_quote.quote} ({amount.str()} + {internal_fee.str()} fees)" + ) + + return payment_quote + + def validate_payment_quote( + self, melt_quote: PostMeltQuoteRequest, payment_quote: PaymentQuoteResponse + ): + # payment quote validation + unit, method = self._verify_and_get_unit_method( + melt_quote.unit, Method.bolt11.name + ) + if not payment_quote.checking_id: + raise Exception("quote has no checking id") + # verify that payment quote amount is as expected + if melt_quote.is_mpp and melt_quote.mpp_amount != payment_quote.amount.amount: + raise TransactionError("quote amount not as requested") + # make sure the backend returned the amount with a correct unit + if not payment_quote.amount.unit == unit: + raise TransactionError("payment quote amount units do not match") + # fee from the backend must be in the same unit as the amount + if not payment_quote.fee.unit == unit: + raise TransactionError("payment quote fee units do not match") + async def melt_quote( self, melt_quote: PostMeltQuoteRequest ) -> PostMeltQuoteResponse: @@ -550,43 +609,19 @@ class Ledger(LedgerVerification, LedgerSpendingConditions): request=request, db=self.db ) if mint_quote: - if not request == mint_quote.request: - raise TransactionError("bolt11 requests do not match") - if not mint_quote.unit == melt_quote.unit: - raise TransactionError("units do not match") - if not mint_quote.method == method.name: - raise TransactionError("methods do not match") - if mint_quote.paid: - raise TransactionError("mint quote already paid") - if mint_quote.issued: - raise TransactionError("mint quote already issued") - if not mint_quote.checking_id: - raise TransactionError("mint quote has no checking id") + payment_quote = self.create_internal_melt_quote(mint_quote, melt_quote) - internal_fee = Amount(unit, 0) # no internal fees - amount = Amount(unit, mint_quote.amount) - - payment_quote = PaymentQuoteResponse( - checking_id=mint_quote.checking_id, - amount=amount, - fee=internal_fee, - ) - logger.info( - f"Issuing internal melt quote: {request} ->" - f" {mint_quote.quote} ({amount.str()} + {internal_fee.str()} fees)" - ) else: - # not internal, get payment quote by backend + # not internal + # verify that the backend supports mpp if the quote request has an amount + if melt_quote.is_mpp and not self.backends[method][unit].supports_mpp: + raise TransactionError("backend does not support mpp") + # get payment quote by backend payment_quote = await self.backends[method][unit].get_payment_quote( melt_quote=melt_quote ) - assert payment_quote.checking_id, "quote has no checking id" - # make sure the backend returned the amount with a correct unit - if not payment_quote.amount.unit == unit: - raise TransactionError("payment quote amount units do not match") - # fee from the backend must be in the same unit as the amount - if not payment_quote.fee.unit == unit: - raise TransactionError("payment quote fee units do not match") + + self.validate_payment_quote(melt_quote, payment_quote) # verify that the amount of the proofs is not larger than the maximum allowed if ( diff --git a/cashu/wallet/v1_api.py b/cashu/wallet/v1_api.py index 59e9ba1..fb32c48 100644 --- a/cashu/wallet/v1_api.py +++ b/cashu/wallet/v1_api.py @@ -30,6 +30,8 @@ from ..core.models import ( PostMeltQuoteRequest, PostMeltQuoteResponse, PostMeltRequest, + PostMeltRequestOptionMpp, + PostMeltRequestOptions, PostMeltResponse, PostMeltResponse_deprecated, PostMintQuoteRequest, @@ -361,9 +363,17 @@ class LedgerAPI(LedgerAPIDeprecated, object): """Checks whether the Lightning payment is internal.""" invoice_obj = bolt11.decode(payment_request) assert invoice_obj.amount_msat, "invoice must have amount" + # add mpp amount for partial melts + melt_options = None + if amount: + melt_options = PostMeltRequestOptions( + mpp=PostMeltRequestOptionMpp(amount=amount) + ) + payload = PostMeltQuoteRequest( - unit=unit.name, request=payment_request, amount=amount + unit=unit.name, request=payment_request, options=melt_options ) + resp = await self.httpx.post( join(self.url, "/v1/melt/quote/bolt11"), json=payload.dict(),