Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FIX] NUT-15 mpp amount in millisats #703

Merged
merged 13 commits into from
Mar 5, 2025
10 changes: 3 additions & 7 deletions cashu/lightning/clnrest.py
Original file line number Diff line number Diff line change
Expand Up @@ -332,13 +332,9 @@ async def get_payment_quote(
invoice_obj = decode(melt_quote.request)
assert invoice_obj.amount_msat, "invoice has no amount."
assert invoice_obj.amount_msat > 0, "invoice has 0 amount."
amount_msat = invoice_obj.amount_msat
if melt_quote.is_mpp:
amount_msat = (
Amount(Unit[melt_quote.unit], melt_quote.mpp_amount)
.to(Unit.msat)
.amount
)
amount_msat = melt_quote.mpp_amount if melt_quote.is_mpp else (
invoice_obj.amount_msat
)
fees_msat = fee_reserve(amount_msat)
fees = Amount(unit=Unit.msat, amount=fees_msat)
amount = Amount(unit=Unit.msat, amount=amount_msat)
Expand Down
8 changes: 3 additions & 5 deletions cashu/lightning/lnd_grpc/lnd_grpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -371,18 +371,16 @@ async def get_payment_quote(
self, melt_quote: PostMeltQuoteRequest
) -> PaymentQuoteResponse:
# get amount from melt_quote or from bolt11
amount = (
Amount(Unit[melt_quote.unit], melt_quote.mpp_amount)
amount_msat = (
melt_quote.mpp_amount
if melt_quote.is_mpp
else None
)

invoice_obj = bolt11.decode(melt_quote.request)
assert invoice_obj.amount_msat, "invoice has no amount."

if amount:
amount_msat = amount.to(Unit.msat).amount
else:
if amount_msat is None:
amount_msat = int(invoice_obj.amount_msat)

fees_msat = fee_reserve(amount_msat)
Expand Down
11 changes: 2 additions & 9 deletions cashu/lightning/lndrest.py
Original file line number Diff line number Diff line change
Expand Up @@ -394,19 +394,12 @@ async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
async def get_payment_quote(
self, melt_quote: PostMeltQuoteRequest
) -> PaymentQuoteResponse:
# get amount from melt_quote or from bolt11
amount = (
Amount(Unit[melt_quote.unit], melt_quote.mpp_amount)
if melt_quote.is_mpp
else None
)
amount_msat = melt_quote.mpp_amount if melt_quote.is_mpp else None

invoice_obj = decode(melt_quote.request)
assert invoice_obj.amount_msat, "invoice has no amount."

if amount:
amount_msat = amount.to(Unit.msat).amount
else:
if amount_msat is None:
amount_msat = int(invoice_obj.amount_msat)

fees_msat = fee_reserve(amount_msat)
Expand Down
2 changes: 1 addition & 1 deletion cashu/mint/ledger.py
Original file line number Diff line number Diff line change
Expand Up @@ -657,7 +657,7 @@ def validate_payment_quote(
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:
if melt_quote.is_mpp and melt_quote.mpp_amount != payment_quote.amount.to(Unit.msat).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:
Expand Down
5 changes: 4 additions & 1 deletion cashu/wallet/cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,10 @@ async def pay(
await wallet.load_mint()
await print_balance(ctx)
payment_hash = bolt11.decode(invoice).payment_hash
quote = await wallet.melt_quote(invoice, amount)
if amount:
# we assume `amount` to be in sats
amount_mpp_msat = amount * 1000
quote = await wallet.melt_quote(invoice, amount_mpp_msat)
logger.debug(f"Quote: {quote}")
total_amount = quote.amount + quote.fee_reserve
# estimate ecash fee for the coinselected proofs
Expand Down
12 changes: 8 additions & 4 deletions cashu/wallet/v1_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -434,16 +434,17 @@ def _mintrequest_include_fields(outputs: List[BlindedMessage]):
@async_set_httpx_client
@async_ensure_mint_loaded
async def melt_quote(
self, payment_request: str, unit: Unit, amount: Optional[int] = None
self, payment_request: str, unit: Unit, amount_msat: Optional[int] = None
) -> PostMeltQuoteResponse:
"""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:
if amount_msat:
melt_options = PostMeltRequestOptions(
mpp=PostMeltRequestOptionMpp(amount=amount)
mpp=PostMeltRequestOptionMpp(amount=amount_msat)
)

payload = PostMeltQuoteRequest(
Expand All @@ -462,9 +463,12 @@ async def melt_quote(
payment_request
)
quote_id = f"deprecated_{uuid.uuid4()}"
amount_sat = (
amount_msat // 1000 if amount_msat else invoice_obj.amount_msat // 1000
)
return PostMeltQuoteResponse(
quote=quote_id,
amount=amount or invoice_obj.amount_msat // 1000,
amount=amount_sat,
fee_reserve=ret.fee or 0,
paid=False,
state=MeltQuoteState.unpaid.value,
Expand Down
6 changes: 3 additions & 3 deletions cashu/wallet/wallet.py
Original file line number Diff line number Diff line change
Expand Up @@ -701,14 +701,14 @@ async def split(
return keep_proofs, send_proofs

async def melt_quote(
self, invoice: str, amount: Optional[int] = None
self, invoice: str, amount_msat: Optional[int] = None
) -> PostMeltQuoteResponse:
"""
Fetches a melt quote from the mint and either uses the amount in the invoice or the amount provided.
"""
if amount and not self.mint_info.supports_mpp("bolt11", self.unit):
if amount_msat and not self.mint_info.supports_mpp("bolt11", self.unit):
raise Exception("Mint does not support MPP, cannot specify amount.")
melt_quote_resp = await super().melt_quote(invoice, self.unit, amount)
melt_quote_resp = await super().melt_quote(invoice, self.unit, amount_msat)
logger.debug(
f"Mint wants {self.unit.str(melt_quote_resp.fee_reserve)} as fee reserve."
)
Expand Down
6 changes: 3 additions & 3 deletions tests/test_wallet_regtest_mpp.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ async def test_regtest_pay_mpp(wallet: Wallet, ledger: Ledger):

async def _mint_pay_mpp(invoice: str, amount: int, proofs: List[Proof]):
# wallet pays 32 sat of the invoice
quote = await wallet.melt_quote(invoice, amount=amount)
quote = await wallet.melt_quote(invoice, amount_msat=amount*1000)
assert quote.amount == amount
await wallet.melt(
proofs,
Expand Down Expand Up @@ -118,7 +118,7 @@ async def test_regtest_pay_mpp_incomplete_payment(wallet: Wallet, ledger: Ledger
async def pay_mpp(amount: int, proofs: List[Proof], delay: float = 0.0):
await asyncio.sleep(delay)
# wallet pays 32 sat of the invoice
quote = await wallet.melt_quote(invoice_payment_request, amount=amount)
quote = await wallet.melt_quote(invoice_payment_request, amount_msat=amount*1000)
assert quote.amount == amount
await wallet.melt(
proofs,
Expand Down Expand Up @@ -154,5 +154,5 @@ async def test_regtest_internal_mpp_melt_quotes(wallet: Wallet, ledger: Ledger):

# try and create a multi-part melt quote
await assert_err(
wallet.melt_quote(mint_quote.request, 100), "internal mpp not allowed"
wallet.melt_quote(mint_quote.request, 100*1000), "internal mpp not allowed"
)
Loading