From f72a3f260fa91d3729f81993ede537c3619b2a10 Mon Sep 17 00:00:00 2001 From: lollerfirst <43107113+lollerfirst@users.noreply.github.com> Date: Wed, 5 Mar 2025 23:47:03 +0100 Subject: [PATCH] [FIX] NUT-15 mpp amount in millisats (#703) * fix lndrest * fix clnrest * fix clnrest and lndrest * lnd grpc fix * wallet * convert amount to millisats in CLI pay invoice * fix tests * format * fix kw arg in regtest test * fix payment quote validation check * clean comment * avoid overwriting variable * deprecated response with amount --------- Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- cashu/lightning/clnrest.py | 10 +++------- cashu/lightning/lnd_grpc/lnd_grpc.py | 8 +++----- cashu/lightning/lndrest.py | 11 ++--------- cashu/mint/ledger.py | 2 +- cashu/wallet/cli/cli.py | 5 ++++- cashu/wallet/v1_api.py | 12 ++++++++---- cashu/wallet/wallet.py | 6 +++--- tests/test_wallet_regtest_mpp.py | 6 +++--- 8 files changed, 27 insertions(+), 33 deletions(-) diff --git a/cashu/lightning/clnrest.py b/cashu/lightning/clnrest.py index 23832f8..36b7414 100644 --- a/cashu/lightning/clnrest.py +++ b/cashu/lightning/clnrest.py @@ -332,13 +332,9 @@ class CLNRestWallet(LightningBackend): 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) diff --git a/cashu/lightning/lnd_grpc/lnd_grpc.py b/cashu/lightning/lnd_grpc/lnd_grpc.py index d6b8cb0..b961c56 100644 --- a/cashu/lightning/lnd_grpc/lnd_grpc.py +++ b/cashu/lightning/lnd_grpc/lnd_grpc.py @@ -371,8 +371,8 @@ class LndRPCWallet(LightningBackend): 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 ) @@ -380,9 +380,7 @@ class LndRPCWallet(LightningBackend): 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) diff --git a/cashu/lightning/lndrest.py b/cashu/lightning/lndrest.py index e7ea3d1..1baa556 100644 --- a/cashu/lightning/lndrest.py +++ b/cashu/lightning/lndrest.py @@ -394,19 +394,12 @@ class LndRestWallet(LightningBackend): 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) diff --git a/cashu/mint/ledger.py b/cashu/mint/ledger.py index 27e55e1..1270807 100644 --- a/cashu/mint/ledger.py +++ b/cashu/mint/ledger.py @@ -666,7 +666,7 @@ class Ledger(LedgerVerification, LedgerSpendingConditions, LedgerTasks, LedgerFe 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: diff --git a/cashu/wallet/cli/cli.py b/cashu/wallet/cli/cli.py index b759dcc..90ea949 100644 --- a/cashu/wallet/cli/cli.py +++ b/cashu/wallet/cli/cli.py @@ -263,7 +263,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 diff --git a/cashu/wallet/v1_api.py b/cashu/wallet/v1_api.py index 8297f98..a62c8a4 100644 --- a/cashu/wallet/v1_api.py +++ b/cashu/wallet/v1_api.py @@ -434,16 +434,17 @@ class LedgerAPI(LedgerAPIDeprecated, SupportsAuth): @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( @@ -462,9 +463,12 @@ class LedgerAPI(LedgerAPIDeprecated, SupportsAuth): 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, diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index b253efa..dac05dc 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -701,14 +701,14 @@ class Wallet( 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." ) diff --git a/tests/test_wallet_regtest_mpp.py b/tests/test_wallet_regtest_mpp.py index 0f788e6..b07ce52 100644 --- a/tests/test_wallet_regtest_mpp.py +++ b/tests/test_wallet_regtest_mpp.py @@ -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, @@ -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, @@ -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" )