Fix false mpp payment handling of unsupported backends (#547)

* fix mpp spec and backend support check

* refactor validation in ledger

* remove weird error

* fix mpp melt model
This commit is contained in:
callebtc
2024-06-16 17:16:10 +02:00
committed by GitHub
parent 2fae0ba2e2
commit 75987beaf1
4 changed files with 103 additions and 36 deletions

View File

@@ -131,12 +131,34 @@ class PostMintResponse_deprecated(BaseModel):
# ------- API: MELT QUOTE ------- # ------- API: MELT QUOTE -------
class PostMeltRequestOptionMpp(BaseModel):
amount: int = Field(gt=0) # input amount
class PostMeltRequestOptions(BaseModel):
mpp: Optional[PostMeltRequestOptionMpp]
class PostMeltQuoteRequest(BaseModel): class PostMeltQuoteRequest(BaseModel):
unit: str = Field(..., max_length=settings.mint_max_request_length) # input unit unit: str = Field(..., max_length=settings.mint_max_request_length) # input unit
request: str = Field( request: str = Field(
..., max_length=settings.mint_max_request_length ..., max_length=settings.mint_max_request_length
) # output payment request ) # 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): class PostMeltQuoteResponse(BaseModel):

View File

@@ -376,8 +376,8 @@ class LndRestWallet(LightningBackend):
) -> PaymentQuoteResponse: ) -> PaymentQuoteResponse:
# get amount from melt_quote or from bolt11 # get amount from melt_quote or from bolt11
amount = ( amount = (
Amount(Unit[melt_quote.unit], melt_quote.amount) Amount(Unit[melt_quote.unit], melt_quote.mpp_amount)
if melt_quote.amount if melt_quote.is_mpp
else None else None
) )

View File

@@ -519,6 +519,65 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
del self.locks[quote_id] del self.locks[quote_id]
return promises 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( async def melt_quote(
self, melt_quote: PostMeltQuoteRequest self, melt_quote: PostMeltQuoteRequest
) -> PostMeltQuoteResponse: ) -> PostMeltQuoteResponse:
@@ -550,43 +609,19 @@ class Ledger(LedgerVerification, LedgerSpendingConditions):
request=request, db=self.db request=request, db=self.db
) )
if mint_quote: if mint_quote:
if not request == mint_quote.request: payment_quote = self.create_internal_melt_quote(mint_quote, melt_quote)
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")
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: 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( payment_quote = await self.backends[method][unit].get_payment_quote(
melt_quote=melt_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 self.validate_payment_quote(melt_quote, payment_quote)
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")
# verify that the amount of the proofs is not larger than the maximum allowed # verify that the amount of the proofs is not larger than the maximum allowed
if ( if (

View File

@@ -30,6 +30,8 @@ from ..core.models import (
PostMeltQuoteRequest, PostMeltQuoteRequest,
PostMeltQuoteResponse, PostMeltQuoteResponse,
PostMeltRequest, PostMeltRequest,
PostMeltRequestOptionMpp,
PostMeltRequestOptions,
PostMeltResponse, PostMeltResponse,
PostMeltResponse_deprecated, PostMeltResponse_deprecated,
PostMintQuoteRequest, PostMintQuoteRequest,
@@ -361,9 +363,17 @@ class LedgerAPI(LedgerAPIDeprecated, object):
"""Checks whether the Lightning payment is internal.""" """Checks whether the Lightning payment is internal."""
invoice_obj = bolt11.decode(payment_request) invoice_obj = bolt11.decode(payment_request)
assert invoice_obj.amount_msat, "invoice must have amount" 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( payload = PostMeltQuoteRequest(
unit=unit.name, request=payment_request, amount=amount unit=unit.name, request=payment_request, options=melt_options
) )
resp = await self.httpx.post( resp = await self.httpx.post(
join(self.url, "/v1/melt/quote/bolt11"), join(self.url, "/v1/melt/quote/bolt11"),
json=payload.dict(), json=payload.dict(),