From 3197720d917b5857404c1b61e1037e5fdf368063 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Tue, 25 Feb 2025 00:53:56 +0100 Subject: [PATCH 01/69] models + blink --- cashu/core/models.py | 10 ++++++++++ cashu/lightning/blink.py | 11 +++++++++-- cashu/mint/ledger.py | 4 ---- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/cashu/core/models.py b/cashu/core/models.py index 0438165c5..2c624916c 100644 --- a/cashu/core/models.py +++ b/cashu/core/models.py @@ -192,9 +192,12 @@ class PostMintResponse_deprecated(BaseModel): class PostMeltRequestOptionMpp(BaseModel): amount: int = Field(gt=0) # input amount +class PostMeltRequestOptionAmountless(BaseModel): + amount_msat: int = Field(gt=0) # amount to pay to the amountless request class PostMeltRequestOptions(BaseModel): mpp: Optional[PostMeltRequestOptionMpp] + amountless: Optional[PostMeltRequestAmount] class PostMeltQuoteRequest(BaseModel): @@ -211,6 +214,13 @@ def is_mpp(self) -> bool: else: return False + @property + def is_amountless(self) -> bool: + if self.options and self.options.amountless: + return True + else: + return False + @property def mpp_amount(self) -> int: if self.is_mpp and self.options and self.options.mpp: diff --git a/cashu/lightning/blink.py b/cashu/lightning/blink.py index 9d18d671b..5397b2610 100644 --- a/cashu/lightning/blink.py +++ b/cashu/lightning/blink.py @@ -21,6 +21,7 @@ PaymentStatus, StatusResponse, ) +from .errors import TransactionError # according to https://github.com/GaloyMoney/galoy/blob/7e79cc27304de9b9c2e7d7f4fdd3bac09df23aac/core/api/src/domain/bitcoin/index.ts#L59 BLINK_MAX_FEE_PERCENT = 0.5 @@ -453,9 +454,15 @@ async def get_payment_quote( raise e invoice_obj = decode(bolt11) - assert invoice_obj.amount_msat, "invoice has no amount." - amount_msat = int(invoice_obj.amount_msat) + # Detect and handle amountless request + amount_msat = 0 + if melt_quote.is_amountless: + amount_msat = melt_quote.options.amountless.amount_msat + elif invoice_obj.amount_msat: + amount_msat = invoice_obj.amount_msat + else: + raise TransactionError("request has no amount and is not specified as amountless") # we take the highest: fee_msat_response, or BLINK_MAX_FEE_PERCENT, or MINIMUM_FEE_MSAT msat # Note: fees with BLINK_MAX_FEE_PERCENT are rounded to the nearest 1000 msat diff --git a/cashu/mint/ledger.py b/cashu/mint/ledger.py index 9c0824235..80325337a 100644 --- a/cashu/mint/ledger.py +++ b/cashu/mint/ledger.py @@ -695,10 +695,6 @@ async def melt_quote( # and therefore respond with internal transaction fees (0 for now) mint_quote = await self.crud.get_mint_quote(request=request, db=self.db) if mint_quote and mint_quote.unit == melt_quote.unit: - # check if the melt quote is partial and error if it is. - # it's just not possible to handle this case - if melt_quote.is_mpp: - raise TransactionError("internal mpp not allowed.") payment_quote = self.create_internal_melt_quote(mint_quote, melt_quote) else: # not internal From 5bb22cf2649140ade6d0b846ebf92b94499ba503 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Tue, 25 Feb 2025 00:57:21 +0100 Subject: [PATCH 02/69] clnrest --- cashu/lightning/clnrest.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/cashu/lightning/clnrest.py b/cashu/lightning/clnrest.py index 23832f83f..f5909b6b5 100644 --- a/cashu/lightning/clnrest.py +++ b/cashu/lightning/clnrest.py @@ -329,16 +329,25 @@ async def paid_invoices_stream(self) -> AsyncGenerator[str, None]: async def get_payment_quote( self, melt_quote: PostMeltQuoteRequest ) -> PaymentQuoteResponse: + 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 + + # Detect and handle amountless request + amount_msat = 0 + if melt_quote.is_amountless: + amount_msat = melt_quote.options.amountless.amount_msat + elif invoice_obj.amount_msat: + amount_msat = invoice_obj.amount_msat + else: + raise TransactionError("request has no amount and is not specified as amountless") + if melt_quote.is_mpp: amount_msat = ( Amount(Unit[melt_quote.unit], melt_quote.mpp_amount) .to(Unit.msat) .amount ) + fees_msat = fee_reserve(amount_msat) fees = Amount(unit=Unit.msat, amount=fees_msat) amount = Amount(unit=Unit.msat, amount=amount_msat) From 7249b61362e003353a9bfa7f372e05a1d48f1a60 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Tue, 25 Feb 2025 00:59:18 +0100 Subject: [PATCH 03/69] fakewallet --- cashu/core/models.py | 2 +- cashu/lightning/fake.py | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/cashu/core/models.py b/cashu/core/models.py index 2c624916c..8d86f0cf7 100644 --- a/cashu/core/models.py +++ b/cashu/core/models.py @@ -197,7 +197,7 @@ class PostMeltRequestOptionAmountless(BaseModel): class PostMeltRequestOptions(BaseModel): mpp: Optional[PostMeltRequestOptionMpp] - amountless: Optional[PostMeltRequestAmount] + amountless: Optional[PostMeltRequestOptionAmountless] class PostMeltQuoteRequest(BaseModel): diff --git a/cashu/lightning/fake.py b/cashu/lightning/fake.py index 3d51c3699..87d7beef7 100644 --- a/cashu/lightning/fake.py +++ b/cashu/lightning/fake.py @@ -213,7 +213,15 @@ async def get_payment_quote( self, melt_quote: PostMeltQuoteRequest ) -> PaymentQuoteResponse: invoice_obj = decode(melt_quote.request) - assert invoice_obj.amount_msat, "invoice has no amount." + + # Detect and handle amountless request + amount_msat = 0 + if melt_quote.is_amountless: + amount_msat = melt_quote.options.amountless.amount_msat + elif invoice_obj.amount_msat: + amount_msat = invoice_obj.amount_msat + else: + raise TransactionError("request has no amount and is not specified as amountless") if self.unit == Unit.sat: amount_msat = int(invoice_obj.amount_msat) From 38addb49c25c718dd8c5db40a261c4204d6e28e7 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Tue, 25 Feb 2025 01:01:31 +0100 Subject: [PATCH 04/69] lnbits --- cashu/lightning/fake.py | 6 +++--- cashu/lightning/lnbits.py | 11 +++++++++-- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/cashu/lightning/fake.py b/cashu/lightning/fake.py index 87d7beef7..0ed932176 100644 --- a/cashu/lightning/fake.py +++ b/cashu/lightning/fake.py @@ -213,7 +213,7 @@ async def get_payment_quote( self, melt_quote: PostMeltQuoteRequest ) -> PaymentQuoteResponse: invoice_obj = decode(melt_quote.request) - + # Detect and handle amountless request amount_msat = 0 if melt_quote.is_amountless: @@ -224,12 +224,12 @@ async def get_payment_quote( raise TransactionError("request has no amount and is not specified as amountless") if self.unit == Unit.sat: - amount_msat = int(invoice_obj.amount_msat) + amount_msat = int(amount_msat) fees_msat = fee_reserve(amount_msat) fees = Amount(unit=Unit.msat, amount=fees_msat) amount = Amount(unit=Unit.msat, amount=amount_msat) elif self.unit == Unit.usd or self.unit == Unit.eur: - amount_usd = math.ceil(invoice_obj.amount_msat / 1e9 * self.fake_btc_price) + amount_usd = math.ceil(amount_msat / 1e9 * self.fake_btc_price) amount = Amount(unit=self.unit, amount=amount_usd) fees = Amount(unit=self.unit, amount=2) else: diff --git a/cashu/lightning/lnbits.py b/cashu/lightning/lnbits.py index 81489b07a..c5f7de71c 100644 --- a/cashu/lightning/lnbits.py +++ b/cashu/lightning/lnbits.py @@ -205,8 +205,15 @@ async def get_payment_quote( self, melt_quote: PostMeltQuoteRequest ) -> PaymentQuoteResponse: invoice_obj = decode(melt_quote.request) - assert invoice_obj.amount_msat, "invoice has no amount." - amount_msat = int(invoice_obj.amount_msat) + # Detect and handle amountless request + amount_msat = 0 + if melt_quote.is_amountless: + amount_msat = melt_quote.options.amountless.amount_msat + elif invoice_obj.amount_msat: + amount_msat = invoice_obj.amount_msat + else: + raise TransactionError("request has no amount and is not specified as amountless") + fees_msat = fee_reserve(amount_msat) fees = Amount(unit=Unit.msat, amount=fees_msat) amount = Amount(unit=Unit.msat, amount=amount_msat) From 4a91cf9473350b2a071b61c7b5a23ff79f43abde Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Tue, 25 Feb 2025 01:06:02 +0100 Subject: [PATCH 05/69] lndrest + fixes --- cashu/lightning/blink.py | 2 +- cashu/lightning/clnrest.py | 2 +- cashu/lightning/fake.py | 3 +-- cashu/lightning/lnbits.py | 2 +- cashu/lightning/lndrest.py | 16 +++++++++++----- 5 files changed, 15 insertions(+), 10 deletions(-) diff --git a/cashu/lightning/blink.py b/cashu/lightning/blink.py index 5397b2610..5f15fbcf3 100644 --- a/cashu/lightning/blink.py +++ b/cashu/lightning/blink.py @@ -460,7 +460,7 @@ async def get_payment_quote( if melt_quote.is_amountless: amount_msat = melt_quote.options.amountless.amount_msat elif invoice_obj.amount_msat: - amount_msat = invoice_obj.amount_msat + amount_msat = int(invoice_obj.amount_msat) else: raise TransactionError("request has no amount and is not specified as amountless") diff --git a/cashu/lightning/clnrest.py b/cashu/lightning/clnrest.py index f5909b6b5..285cc6c26 100644 --- a/cashu/lightning/clnrest.py +++ b/cashu/lightning/clnrest.py @@ -337,7 +337,7 @@ async def get_payment_quote( if melt_quote.is_amountless: amount_msat = melt_quote.options.amountless.amount_msat elif invoice_obj.amount_msat: - amount_msat = invoice_obj.amount_msat + amount_msat = int(invoice_obj.amount_msat) else: raise TransactionError("request has no amount and is not specified as amountless") diff --git a/cashu/lightning/fake.py b/cashu/lightning/fake.py index 0ed932176..d22ae60ab 100644 --- a/cashu/lightning/fake.py +++ b/cashu/lightning/fake.py @@ -219,12 +219,11 @@ async def get_payment_quote( if melt_quote.is_amountless: amount_msat = melt_quote.options.amountless.amount_msat elif invoice_obj.amount_msat: - amount_msat = invoice_obj.amount_msat + amount_msat = int(invoice_obj.amount_msat) else: raise TransactionError("request has no amount and is not specified as amountless") if self.unit == Unit.sat: - amount_msat = int(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/lnbits.py b/cashu/lightning/lnbits.py index c5f7de71c..b8fbd0c84 100644 --- a/cashu/lightning/lnbits.py +++ b/cashu/lightning/lnbits.py @@ -210,7 +210,7 @@ async def get_payment_quote( if melt_quote.is_amountless: amount_msat = melt_quote.options.amountless.amount_msat elif invoice_obj.amount_msat: - amount_msat = invoice_obj.amount_msat + amount_msat = int(invoice_obj.amount_msat) else: raise TransactionError("request has no amount and is not specified as amountless") diff --git a/cashu/lightning/lndrest.py b/cashu/lightning/lndrest.py index e7ea3d180..c28807e9a 100644 --- a/cashu/lightning/lndrest.py +++ b/cashu/lightning/lndrest.py @@ -395,19 +395,25 @@ async def get_payment_quote( self, melt_quote: PostMeltQuoteRequest ) -> PaymentQuoteResponse: # get amount from melt_quote or from bolt11 - amount = ( + mpp_amount = ( Amount(Unit[melt_quote.unit], 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: + # Detect and handle amountless request + amount_msat = 0 + if melt_quote.is_amountless: + amount_msat = melt_quote.options.amountless.amount_msat + elif invoice_obj.amount_msat: amount_msat = int(invoice_obj.amount_msat) + else: + raise TransactionError("request has no amount and is not specified as amountless") + + if mpp_amount: + amount_msat = mpp_amount.to(Unit.msat).amount fees_msat = fee_reserve(amount_msat) fees = Amount(unit=Unit.msat, amount=fees_msat) From 470580b0ad9f4cce51eae2dbe07188a2c4ff2f7c Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Tue, 25 Feb 2025 01:18:19 +0100 Subject: [PATCH 06/69] lnd_grpc + some more fixes --- cashu/core/models.py | 4 ++-- cashu/lightning/blink.py | 5 ++--- cashu/lightning/clnrest.py | 4 ++-- cashu/lightning/fake.py | 4 ++-- cashu/lightning/lnbits.py | 4 ++-- cashu/lightning/lnd_grpc/lnd_grpc.py | 18 ++++++++++++------ cashu/lightning/lndrest.py | 4 ++-- 7 files changed, 24 insertions(+), 19 deletions(-) diff --git a/cashu/core/models.py b/cashu/core/models.py index 8d86f0cf7..9c46aad93 100644 --- a/cashu/core/models.py +++ b/cashu/core/models.py @@ -196,8 +196,8 @@ class PostMeltRequestOptionAmountless(BaseModel): amount_msat: int = Field(gt=0) # amount to pay to the amountless request class PostMeltRequestOptions(BaseModel): - mpp: Optional[PostMeltRequestOptionMpp] - amountless: Optional[PostMeltRequestOptionAmountless] + mpp: Optional[PostMeltRequestOptionMpp] = None + amountless: Optional[PostMeltRequestOptionAmountless] = None class PostMeltQuoteRequest(BaseModel): diff --git a/cashu/lightning/blink.py b/cashu/lightning/blink.py index 5f15fbcf3..39e19e61d 100644 --- a/cashu/lightning/blink.py +++ b/cashu/lightning/blink.py @@ -21,7 +21,6 @@ PaymentStatus, StatusResponse, ) -from .errors import TransactionError # according to https://github.com/GaloyMoney/galoy/blob/7e79cc27304de9b9c2e7d7f4fdd3bac09df23aac/core/api/src/domain/bitcoin/index.ts#L59 BLINK_MAX_FEE_PERCENT = 0.5 @@ -458,11 +457,11 @@ async def get_payment_quote( # Detect and handle amountless request amount_msat = 0 if melt_quote.is_amountless: - amount_msat = melt_quote.options.amountless.amount_msat + amount_msat = melt_quote.options.amountless.amount_msat # type: ignore elif invoice_obj.amount_msat: amount_msat = int(invoice_obj.amount_msat) else: - raise TransactionError("request has no amount and is not specified as amountless") + raise Exception("request has no amount and is not specified as amountless") # we take the highest: fee_msat_response, or BLINK_MAX_FEE_PERCENT, or MINIMUM_FEE_MSAT msat # Note: fees with BLINK_MAX_FEE_PERCENT are rounded to the nearest 1000 msat diff --git a/cashu/lightning/clnrest.py b/cashu/lightning/clnrest.py index 285cc6c26..ecddd28aa 100644 --- a/cashu/lightning/clnrest.py +++ b/cashu/lightning/clnrest.py @@ -335,11 +335,11 @@ async def get_payment_quote( # Detect and handle amountless request amount_msat = 0 if melt_quote.is_amountless: - amount_msat = melt_quote.options.amountless.amount_msat + amount_msat = melt_quote.options.amountless.amount_msat # type: ignore elif invoice_obj.amount_msat: amount_msat = int(invoice_obj.amount_msat) else: - raise TransactionError("request has no amount and is not specified as amountless") + raise Exception("request has no amount and is not specified as amountless") if melt_quote.is_mpp: amount_msat = ( diff --git a/cashu/lightning/fake.py b/cashu/lightning/fake.py index d22ae60ab..40c3dae4f 100644 --- a/cashu/lightning/fake.py +++ b/cashu/lightning/fake.py @@ -217,11 +217,11 @@ async def get_payment_quote( # Detect and handle amountless request amount_msat = 0 if melt_quote.is_amountless: - amount_msat = melt_quote.options.amountless.amount_msat + amount_msat = melt_quote.options.amountless.amount_msat # type: ignore elif invoice_obj.amount_msat: amount_msat = int(invoice_obj.amount_msat) else: - raise TransactionError("request has no amount and is not specified as amountless") + raise Exception("request has no amount and is not specified as amountless") if self.unit == Unit.sat: fees_msat = fee_reserve(amount_msat) diff --git a/cashu/lightning/lnbits.py b/cashu/lightning/lnbits.py index b8fbd0c84..116204d0e 100644 --- a/cashu/lightning/lnbits.py +++ b/cashu/lightning/lnbits.py @@ -208,11 +208,11 @@ async def get_payment_quote( # Detect and handle amountless request amount_msat = 0 if melt_quote.is_amountless: - amount_msat = melt_quote.options.amountless.amount_msat + amount_msat = melt_quote.options.amountless.amount_msat # type: ignore elif invoice_obj.amount_msat: amount_msat = int(invoice_obj.amount_msat) else: - raise TransactionError("request has no amount and is not specified as amountless") + raise Exception("request has no amount and is not specified as amountless") fees_msat = fee_reserve(amount_msat) fees = Amount(unit=Unit.msat, amount=fees_msat) diff --git a/cashu/lightning/lnd_grpc/lnd_grpc.py b/cashu/lightning/lnd_grpc/lnd_grpc.py index d6b8cb0fa..612433973 100644 --- a/cashu/lightning/lnd_grpc/lnd_grpc.py +++ b/cashu/lightning/lnd_grpc/lnd_grpc.py @@ -371,19 +371,25 @@ async def get_payment_quote( self, melt_quote: PostMeltQuoteRequest ) -> PaymentQuoteResponse: # get amount from melt_quote or from bolt11 - amount = ( + mpp_amount = ( Amount(Unit[melt_quote.unit], 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: + + # Detect and handle amountless request + amount_msat = 0 + if melt_quote.is_amountless: + amount_msat = melt_quote.options.amountless.amount_msat # type: ignore + elif invoice_obj.amount_msat: amount_msat = int(invoice_obj.amount_msat) + else: + raise Exception("request has no amount and is not specified as amountless") + + if mpp_amount: + amount_msat = mpp_amount.to(Unit.msat).amount fees_msat = fee_reserve(amount_msat) fees = Amount(unit=Unit.msat, amount=fees_msat) diff --git a/cashu/lightning/lndrest.py b/cashu/lightning/lndrest.py index c28807e9a..405ac3163 100644 --- a/cashu/lightning/lndrest.py +++ b/cashu/lightning/lndrest.py @@ -406,11 +406,11 @@ async def get_payment_quote( # Detect and handle amountless request amount_msat = 0 if melt_quote.is_amountless: - amount_msat = melt_quote.options.amountless.amount_msat + amount_msat = melt_quote.options.amountless.amount_msat # type: ignore elif invoice_obj.amount_msat: amount_msat = int(invoice_obj.amount_msat) else: - raise TransactionError("request has no amount and is not specified as amountless") + raise Exception("request has no amount and is not specified as amountless") if mpp_amount: amount_msat = mpp_amount.to(Unit.msat).amount From 7a5a1279766ce31885944526e82c84673107e89e Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Tue, 25 Feb 2025 03:12:04 +0100 Subject: [PATCH 07/69] fix test --- tests/test_wallet_regtest_mpp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_wallet_regtest_mpp.py b/tests/test_wallet_regtest_mpp.py index 0f788e6d6..cbf65f141 100644 --- a/tests/test_wallet_regtest_mpp.py +++ b/tests/test_wallet_regtest_mpp.py @@ -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), "internal payments do not support mpp" ) From 8c7c1ebb3292f0957b95eada62b2fe236f5a086b Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Fri, 28 Feb 2025 11:05:19 +0100 Subject: [PATCH 08/69] errors --- cashu/core/errors.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/cashu/core/errors.py b/cashu/core/errors.py index d1d0c6dbb..c3a59df3d 100644 --- a/cashu/core/errors.py +++ b/cashu/core/errors.py @@ -86,6 +86,19 @@ class TransactionAmountExceedsLimitError(TransactionError): def __init__(self, detail): super().__init__(detail, code=self.code) +class TransactionAmountExceedsLimitError(CashuError): + detail = "Amountless invoice is not supported" + code = 11007 + + def __init__(self, detail): + super().__init__(detail, code=self.code) + +class TransactionAmountExceedsLimitError(CashuError): + detail = "Amount in request does not equal invoice" + code = 11008 + + def __init__(self, detail): + super().__init__(detail, code=self.code) class KeysetError(CashuError): detail = "keyset error" From 823b5273c8e631810288910b5446831cd9c93864 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Fri, 28 Feb 2025 11:09:16 +0100 Subject: [PATCH 09/69] errors+ --- cashu/core/errors.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cashu/core/errors.py b/cashu/core/errors.py index c3a59df3d..691db2d19 100644 --- a/cashu/core/errors.py +++ b/cashu/core/errors.py @@ -80,20 +80,20 @@ def __init__(self, detail): super().__init__(detail, code=self.code) -class TransactionAmountExceedsLimitError(TransactionError): +class TransactionAmountExceedsLimitError(CashuError): code = 11006 def __init__(self, detail): super().__init__(detail, code=self.code) -class TransactionAmountExceedsLimitError(CashuError): +class AmountlessInvoiceNotSupportedError(CashuError): detail = "Amountless invoice is not supported" code = 11007 def __init__(self, detail): super().__init__(detail, code=self.code) -class TransactionAmountExceedsLimitError(CashuError): +class IncorrectRequestAmountError(TransactionError): detail = "Amount in request does not equal invoice" code = 11008 From 6a7d800b0340b172fceaf6f420db58fd71ac8b5e Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Fri, 28 Feb 2025 12:16:35 +0100 Subject: [PATCH 10/69] settings and features --- cashu/core/errors.py | 2 +- cashu/core/models.py | 1 + cashu/lightning/base.py | 1 + cashu/mint/features.py | 1 + cashu/mint/ledger.py | 4 ++++ 5 files changed, 8 insertions(+), 1 deletion(-) diff --git a/cashu/core/errors.py b/cashu/core/errors.py index 691db2d19..993a5d81b 100644 --- a/cashu/core/errors.py +++ b/cashu/core/errors.py @@ -86,7 +86,7 @@ class TransactionAmountExceedsLimitError(CashuError): def __init__(self, detail): super().__init__(detail, code=self.code) -class AmountlessInvoiceNotSupportedError(CashuError): +class AmountlessInvoiceNotSupportedError(TransactionError): detail = "Amountless invoice is not supported" code = 11007 diff --git a/cashu/core/models.py b/cashu/core/models.py index 9c46aad93..11023e319 100644 --- a/cashu/core/models.py +++ b/cashu/core/models.py @@ -31,6 +31,7 @@ class MeltMethodSetting(BaseModel): unit: str min_amount: Optional[int] = None max_amount: Optional[int] = None + amountless: Optional[bool] = None class MintInfoContact(BaseModel): diff --git a/cashu/lightning/base.py b/cashu/lightning/base.py index c4169946a..1f47ff709 100644 --- a/cashu/lightning/base.py +++ b/cashu/lightning/base.py @@ -110,6 +110,7 @@ def __str__(self) -> str: class LightningBackend(ABC): supports_mpp: bool = False + supports_amountless: bool = False supports_incoming_payment_stream: bool = False supported_units: set[Unit] supports_description: bool = False diff --git a/cashu/mint/features.py b/cashu/mint/features.py index 63bc5dff1..17614c9dc 100644 --- a/cashu/mint/features.py +++ b/cashu/mint/features.py @@ -92,6 +92,7 @@ def create_mint_features(self) -> Dict[int, Union[List[Any], Dict[str, Any]]]: if settings.mint_max_peg_out: melt_setting.max_amount = settings.mint_max_peg_out melt_setting.min_amount = 0 + melt_settings.amountless = unit_dict[unit].supports_amountless melt_method_settings.append(melt_setting) mint_features: Dict[int, Union[List[Any], Dict[str, Any]]] = { diff --git a/cashu/mint/ledger.py b/cashu/mint/ledger.py index 80325337a..c492b1d27 100644 --- a/cashu/mint/ledger.py +++ b/cashu/mint/ledger.py @@ -41,6 +41,7 @@ QuoteSignatureInvalidError, TransactionAmountExceedsLimitError, TransactionError, + AmountlessInvoiceNotSupportedError, ) from ..core.helpers import sum_proofs from ..core.models import ( @@ -701,6 +702,9 @@ async def melt_quote( # 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.") + # verify that the backend supports amountless if the payment string does not have an amount + if melt_quote.is_amountless and not self.backends[method][unit].supports_amountless: + raise AmountlessInvoiceNotSupportedError() # get payment quote by backend payment_quote = await self.backends[method][unit].get_payment_quote( melt_quote=melt_quote From edf55f52e8663a2d16162bb7d23c1e0e2085eff3 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Fri, 28 Feb 2025 12:16:58 +0100 Subject: [PATCH 11/69] revert blink --- cashu/lightning/blink.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/cashu/lightning/blink.py b/cashu/lightning/blink.py index 39e19e61d..ffce4bef9 100644 --- a/cashu/lightning/blink.py +++ b/cashu/lightning/blink.py @@ -454,14 +454,9 @@ async def get_payment_quote( invoice_obj = decode(bolt11) - # Detect and handle amountless request - amount_msat = 0 - if melt_quote.is_amountless: - amount_msat = melt_quote.options.amountless.amount_msat # type: ignore - elif invoice_obj.amount_msat: - amount_msat = int(invoice_obj.amount_msat) - else: - raise Exception("request has no amount and is not specified as amountless") + amount_msat = int(invoice_obj.amount_msat) + if not amount_msat: + raise Exception("Invoice with no amount") # we take the highest: fee_msat_response, or BLINK_MAX_FEE_PERCENT, or MINIMUM_FEE_MSAT msat # Note: fees with BLINK_MAX_FEE_PERCENT are rounded to the nearest 1000 msat From 7e85e3b9419e705e162921c239dfeeceefd454ae Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Fri, 28 Feb 2025 12:19:44 +0100 Subject: [PATCH 12/69] revert lnbits --- cashu/lightning/lnbits.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/cashu/lightning/lnbits.py b/cashu/lightning/lnbits.py index 116204d0e..a4973de41 100644 --- a/cashu/lightning/lnbits.py +++ b/cashu/lightning/lnbits.py @@ -205,14 +205,10 @@ async def get_payment_quote( self, melt_quote: PostMeltQuoteRequest ) -> PaymentQuoteResponse: invoice_obj = decode(melt_quote.request) - # Detect and handle amountless request - amount_msat = 0 - if melt_quote.is_amountless: - amount_msat = melt_quote.options.amountless.amount_msat # type: ignore - elif invoice_obj.amount_msat: - amount_msat = int(invoice_obj.amount_msat) - else: - raise Exception("request has no amount and is not specified as amountless") + + amount_msat = int(invoice_obj.amount_msat) + if not amount_msat: + raise Exception("request has no amount") fees_msat = fee_reserve(amount_msat) fees = Amount(unit=Unit.msat, amount=fees_msat) From 2c6da1366c45cb7253b5d14c6bdb91e089af1f8e Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Fri, 28 Feb 2025 12:26:58 +0100 Subject: [PATCH 13/69] fix --- cashu/lightning/blink.py | 5 +++-- cashu/lightning/lnbits.py | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/cashu/lightning/blink.py b/cashu/lightning/blink.py index ffce4bef9..cb543204c 100644 --- a/cashu/lightning/blink.py +++ b/cashu/lightning/blink.py @@ -454,10 +454,11 @@ async def get_payment_quote( invoice_obj = decode(bolt11) - amount_msat = int(invoice_obj.amount_msat) - if not amount_msat: + if not invoice_obj.amount_msat: raise Exception("Invoice with no amount") + amount_msat = int(invoice_obj.amount_msat) + # we take the highest: fee_msat_response, or BLINK_MAX_FEE_PERCENT, or MINIMUM_FEE_MSAT msat # Note: fees with BLINK_MAX_FEE_PERCENT are rounded to the nearest 1000 msat fees_amount_msat: int = ( diff --git a/cashu/lightning/lnbits.py b/cashu/lightning/lnbits.py index a4973de41..22fc8093a 100644 --- a/cashu/lightning/lnbits.py +++ b/cashu/lightning/lnbits.py @@ -206,10 +206,11 @@ async def get_payment_quote( ) -> PaymentQuoteResponse: invoice_obj = decode(melt_quote.request) - amount_msat = int(invoice_obj.amount_msat) - if not amount_msat: + if not invoice_obj.amount_msat: raise Exception("request has no amount") + amount_msat = int(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) From 5b95277aca3cfc07604b0691352c08b2a3a7c9fe Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Fri, 28 Feb 2025 12:47:48 +0100 Subject: [PATCH 14/69] support amountless flags in cln, fakewallet an lnd --- cashu/lightning/clnrest.py | 1 + cashu/lightning/fake.py | 1 + cashu/lightning/lnd_grpc/lnd_grpc.py | 1 + cashu/lightning/lndrest.py | 1 + 4 files changed, 4 insertions(+) diff --git a/cashu/lightning/clnrest.py b/cashu/lightning/clnrest.py index ecddd28aa..d59e14da8 100644 --- a/cashu/lightning/clnrest.py +++ b/cashu/lightning/clnrest.py @@ -45,6 +45,7 @@ class CLNRestWallet(LightningBackend): supported_units = {Unit.sat, Unit.msat} unit = Unit.sat supports_mpp = settings.mint_clnrest_enable_mpp + supports_amountless: bool = True supports_incoming_payment_stream: bool = True supports_description: bool = True diff --git a/cashu/lightning/fake.py b/cashu/lightning/fake.py index 40c3dae4f..44adb9c7a 100644 --- a/cashu/lightning/fake.py +++ b/cashu/lightning/fake.py @@ -53,6 +53,7 @@ class FakeWallet(LightningBackend): supports_incoming_payment_stream: bool = True supports_description: bool = True + supports_amountless: bool = True def __init__(self, unit: Unit = Unit.sat, **kwargs): self.assert_unit_supported(unit) diff --git a/cashu/lightning/lnd_grpc/lnd_grpc.py b/cashu/lightning/lnd_grpc/lnd_grpc.py index 612433973..d3d88bb8a 100644 --- a/cashu/lightning/lnd_grpc/lnd_grpc.py +++ b/cashu/lightning/lnd_grpc/lnd_grpc.py @@ -49,6 +49,7 @@ class LndRPCWallet(LightningBackend): supports_mpp = settings.mint_lnd_enable_mpp + supports_amountless: bool = True supports_incoming_payment_stream = True supported_units = {Unit.sat, Unit.msat} supports_description: bool = True diff --git a/cashu/lightning/lndrest.py b/cashu/lightning/lndrest.py index 405ac3163..045c9c8fd 100644 --- a/cashu/lightning/lndrest.py +++ b/cashu/lightning/lndrest.py @@ -46,6 +46,7 @@ class LndRestWallet(LightningBackend): """https://api.lightning.community/rest/index.html#lnd-rest-api-reference""" supports_mpp = settings.mint_lnd_enable_mpp + supports_amountless: bool = True supports_incoming_payment_stream = True supported_units = {Unit.sat, Unit.msat} supports_description: bool = True From c23b9a231abdee126ec41d2a160799d25069f462 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Fri, 28 Feb 2025 12:49:03 +0100 Subject: [PATCH 15/69] format --- cashu/mint/ledger.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cashu/mint/ledger.py b/cashu/mint/ledger.py index c492b1d27..3864e5ae3 100644 --- a/cashu/mint/ledger.py +++ b/cashu/mint/ledger.py @@ -32,6 +32,7 @@ from ..core.crypto.secp import PrivateKey, PublicKey from ..core.db import Connection, Database from ..core.errors import ( + AmountlessInvoiceNotSupportedError, CashuError, KeysetError, KeysetNotFoundError, @@ -41,7 +42,6 @@ QuoteSignatureInvalidError, TransactionAmountExceedsLimitError, TransactionError, - AmountlessInvoiceNotSupportedError, ) from ..core.helpers import sum_proofs from ..core.models import ( From fc51f302dc20f7a70c8dcccbcf9cdf0da38c7014 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Fri, 28 Feb 2025 12:51:23 +0100 Subject: [PATCH 16/69] fix errors --- cashu/core/errors.py | 8 ++++---- cashu/mint/features.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/cashu/core/errors.py b/cashu/core/errors.py index 993a5d81b..2f3238741 100644 --- a/cashu/core/errors.py +++ b/cashu/core/errors.py @@ -90,15 +90,15 @@ class AmountlessInvoiceNotSupportedError(TransactionError): detail = "Amountless invoice is not supported" code = 11007 - def __init__(self, detail): - super().__init__(detail, code=self.code) + def __init__(self): + super().__init__(detail=self.detail, code=self.code) class IncorrectRequestAmountError(TransactionError): detail = "Amount in request does not equal invoice" code = 11008 - def __init__(self, detail): - super().__init__(detail, code=self.code) + def __init__(self): + super().__init__(detail=self.detail, code=self.code) class KeysetError(CashuError): detail = "keyset error" diff --git a/cashu/mint/features.py b/cashu/mint/features.py index 17614c9dc..5d32cd908 100644 --- a/cashu/mint/features.py +++ b/cashu/mint/features.py @@ -92,7 +92,7 @@ def create_mint_features(self) -> Dict[int, Union[List[Any], Dict[str, Any]]]: if settings.mint_max_peg_out: melt_setting.max_amount = settings.mint_max_peg_out melt_setting.min_amount = 0 - melt_settings.amountless = unit_dict[unit].supports_amountless + melt_setting.amountless = unit_dict[unit].supports_amountless melt_method_settings.append(melt_setting) mint_features: Dict[int, Union[List[Any], Dict[str, Any]]] = { From 80ff714e842fa908af6645ac8a2d2fe482861e0b Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Fri, 28 Feb 2025 13:13:25 +0100 Subject: [PATCH 17/69] check invoice `amount_msat` against amountless `amount_msat` --- cashu/lightning/clnrest.py | 8 ++++++++ cashu/lightning/fake.py | 5 +++++ cashu/lightning/lnd_grpc/lnd_grpc.py | 6 ++++++ cashu/lightning/lndrest.py | 6 ++++++ 4 files changed, 25 insertions(+) diff --git a/cashu/lightning/clnrest.py b/cashu/lightning/clnrest.py index d59e14da8..43ea68442 100644 --- a/cashu/lightning/clnrest.py +++ b/cashu/lightning/clnrest.py @@ -25,6 +25,9 @@ StatusResponse, Unsupported, ) +from .errors import ( + IncorrectRequestAmountError +) # https://docs.corelightning.org/reference/lightning-pay PAYMENT_RESULT_MAP = { @@ -336,6 +339,11 @@ async def get_payment_quote( # Detect and handle amountless request amount_msat = 0 if melt_quote.is_amountless: + # Check that the user isn't doing something cheeky + if (invoice_obj.amount_msat + and melt_quote.options.amountless.amount_msat != invoice.amount_msat + ): + raise IncorrectRequestAmountError() amount_msat = melt_quote.options.amountless.amount_msat # type: ignore elif invoice_obj.amount_msat: amount_msat = int(invoice_obj.amount_msat) diff --git a/cashu/lightning/fake.py b/cashu/lightning/fake.py index 44adb9c7a..c8d4bbdc2 100644 --- a/cashu/lightning/fake.py +++ b/cashu/lightning/fake.py @@ -218,6 +218,11 @@ async def get_payment_quote( # Detect and handle amountless request amount_msat = 0 if melt_quote.is_amountless: + # Check that the user isn't doing something cheeky + if (invoice_obj.amount_msat + and melt_quote.options.amountless.amount_msat != invoice.amount_msat + ): + raise IncorrectRequestAmountError() amount_msat = melt_quote.options.amountless.amount_msat # type: ignore elif invoice_obj.amount_msat: amount_msat = int(invoice_obj.amount_msat) diff --git a/cashu/lightning/lnd_grpc/lnd_grpc.py b/cashu/lightning/lnd_grpc/lnd_grpc.py index d3d88bb8a..4fb015f7f 100644 --- a/cashu/lightning/lnd_grpc/lnd_grpc.py +++ b/cashu/lightning/lnd_grpc/lnd_grpc.py @@ -383,6 +383,12 @@ async def get_payment_quote( # Detect and handle amountless request amount_msat = 0 if melt_quote.is_amountless: + # Check that the user isn't doing something cheeky + if (invoice_obj.amount_msat + and melt_quote.options.amountless.amount_msat != invoice.amount_msat + ): + raise IncorrectRequestAmountError() + amount_msat = melt_quote.options.amountless.amount_msat # type: ignore elif invoice_obj.amount_msat: amount_msat = int(invoice_obj.amount_msat) diff --git a/cashu/lightning/lndrest.py b/cashu/lightning/lndrest.py index 045c9c8fd..1007849f7 100644 --- a/cashu/lightning/lndrest.py +++ b/cashu/lightning/lndrest.py @@ -407,6 +407,12 @@ async def get_payment_quote( # Detect and handle amountless request amount_msat = 0 if melt_quote.is_amountless: + # Check that the user isn't doing something cheeky + if ( + invoice_obj.amount_msat + and melt_quote.options.amountless.amount_msat != invoice.amount_msat + ): + raise IncorrectRequestAmountError() amount_msat = melt_quote.options.amountless.amount_msat # type: ignore elif invoice_obj.amount_msat: amount_msat = int(invoice_obj.amount_msat) From 31b5f571fd64dde06da8e9ba38b48af59f954380 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Fri, 28 Feb 2025 13:19:48 +0100 Subject: [PATCH 18/69] fix --- cashu/lightning/clnrest.py | 8 +++----- cashu/lightning/fake.py | 7 +++++-- cashu/lightning/lnd_grpc/lnd_grpc.py | 8 +++++--- cashu/lightning/lndrest.py | 10 ++++++---- 4 files changed, 19 insertions(+), 14 deletions(-) diff --git a/cashu/lightning/clnrest.py b/cashu/lightning/clnrest.py index 43ea68442..24507dae4 100644 --- a/cashu/lightning/clnrest.py +++ b/cashu/lightning/clnrest.py @@ -25,9 +25,7 @@ StatusResponse, Unsupported, ) -from .errors import ( - IncorrectRequestAmountError -) +from .errors import IncorrectRequestAmountError # https://docs.corelightning.org/reference/lightning-pay PAYMENT_RESULT_MAP = { @@ -338,10 +336,10 @@ async def get_payment_quote( # Detect and handle amountless request amount_msat = 0 - if melt_quote.is_amountless: + if melt_quote.options and melt_quote.options.amountless: # Check that the user isn't doing something cheeky if (invoice_obj.amount_msat - and melt_quote.options.amountless.amount_msat != invoice.amount_msat + and melt_quote.options.amountless.amount_msat != invoice_obj.amount_msat ): raise IncorrectRequestAmountError() amount_msat = melt_quote.options.amountless.amount_msat # type: ignore diff --git a/cashu/lightning/fake.py b/cashu/lightning/fake.py index c8d4bbdc2..2ba6f61aa 100644 --- a/cashu/lightning/fake.py +++ b/cashu/lightning/fake.py @@ -30,6 +30,9 @@ PaymentStatus, StatusResponse, ) +from .errors import ( + IncorrectRequestAmountError, +) class FakeWallet(LightningBackend): @@ -217,10 +220,10 @@ async def get_payment_quote( # Detect and handle amountless request amount_msat = 0 - if melt_quote.is_amountless: + if melt_quote.options and melt_quote.options.amountless: # Check that the user isn't doing something cheeky if (invoice_obj.amount_msat - and melt_quote.options.amountless.amount_msat != invoice.amount_msat + and melt_quote.options.amountless.amount_msat != invoice_obj.amount_msat ): raise IncorrectRequestAmountError() amount_msat = melt_quote.options.amountless.amount_msat # type: ignore diff --git a/cashu/lightning/lnd_grpc/lnd_grpc.py b/cashu/lightning/lnd_grpc/lnd_grpc.py index 4fb015f7f..4577a7443 100644 --- a/cashu/lightning/lnd_grpc/lnd_grpc.py +++ b/cashu/lightning/lnd_grpc/lnd_grpc.py @@ -29,6 +29,9 @@ PostMeltQuoteRequest, StatusResponse, ) +from cashu.lightning.errors import ( + IncorrectRequestAmountError, +) # maps statuses to None, False, True: # https://api.lightning.community/?python=#paymentpaymentstatus @@ -382,13 +385,12 @@ async def get_payment_quote( # Detect and handle amountless request amount_msat = 0 - if melt_quote.is_amountless: + if melt_quote.options and melt_quote.options.amountless: # Check that the user isn't doing something cheeky if (invoice_obj.amount_msat - and melt_quote.options.amountless.amount_msat != invoice.amount_msat + and melt_quote.options.amountless.amount_msat != invoice_obj.amount_msat ): raise IncorrectRequestAmountError() - amount_msat = melt_quote.options.amountless.amount_msat # type: ignore elif invoice_obj.amount_msat: amount_msat = int(invoice_obj.amount_msat) diff --git a/cashu/lightning/lndrest.py b/cashu/lightning/lndrest.py index 1007849f7..a421302c8 100644 --- a/cashu/lightning/lndrest.py +++ b/cashu/lightning/lndrest.py @@ -25,6 +25,9 @@ PaymentStatus, StatusResponse, ) +from .errors import ( + IncorrectRequestAmountError, +) from .macaroon import load_macaroon PAYMENT_RESULT_MAP = { @@ -406,11 +409,10 @@ async def get_payment_quote( # Detect and handle amountless request amount_msat = 0 - if melt_quote.is_amountless: + if melt_quote.options and melt_quote.options.amountless: # Check that the user isn't doing something cheeky - if ( - invoice_obj.amount_msat - and melt_quote.options.amountless.amount_msat != invoice.amount_msat + if (invoice_obj.amount_msat + and melt_quote.options.amountless.amount_msat != invoice_obj.amount_msat ): raise IncorrectRequestAmountError() amount_msat = melt_quote.options.amountless.amount_msat # type: ignore From da42cf3fe20ef8aaf024832ffcee58263680eefb Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Fri, 28 Feb 2025 13:23:32 +0100 Subject: [PATCH 19/69] fix errors imports --- cashu/lightning/clnrest.py | 2 +- cashu/lightning/fake.py | 2 +- cashu/lightning/lnd_grpc/lnd_grpc.py | 4 +--- cashu/lightning/lndrest.py | 2 +- 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/cashu/lightning/clnrest.py b/cashu/lightning/clnrest.py index 24507dae4..2d37e865d 100644 --- a/cashu/lightning/clnrest.py +++ b/cashu/lightning/clnrest.py @@ -25,7 +25,7 @@ StatusResponse, Unsupported, ) -from .errors import IncorrectRequestAmountError +from ..core.errors import IncorrectRequestAmountError # https://docs.corelightning.org/reference/lightning-pay PAYMENT_RESULT_MAP = { diff --git a/cashu/lightning/fake.py b/cashu/lightning/fake.py index 2ba6f61aa..08046a3b6 100644 --- a/cashu/lightning/fake.py +++ b/cashu/lightning/fake.py @@ -30,7 +30,7 @@ PaymentStatus, StatusResponse, ) -from .errors import ( +from ..core.errors import ( IncorrectRequestAmountError, ) diff --git a/cashu/lightning/lnd_grpc/lnd_grpc.py b/cashu/lightning/lnd_grpc/lnd_grpc.py index 4577a7443..415008a22 100644 --- a/cashu/lightning/lnd_grpc/lnd_grpc.py +++ b/cashu/lightning/lnd_grpc/lnd_grpc.py @@ -29,9 +29,7 @@ PostMeltQuoteRequest, StatusResponse, ) -from cashu.lightning.errors import ( - IncorrectRequestAmountError, -) +from cashu.core.errors import IncorrectRequestAmountError # maps statuses to None, False, True: # https://api.lightning.community/?python=#paymentpaymentstatus diff --git a/cashu/lightning/lndrest.py b/cashu/lightning/lndrest.py index a421302c8..d07d94772 100644 --- a/cashu/lightning/lndrest.py +++ b/cashu/lightning/lndrest.py @@ -25,7 +25,7 @@ PaymentStatus, StatusResponse, ) -from .errors import ( +from ..core.errors import ( IncorrectRequestAmountError, ) from .macaroon import load_macaroon From c4cf9aad33e53a26a489f57bd04e3b4eb722f52d Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Fri, 28 Feb 2025 13:23:55 +0100 Subject: [PATCH 20/69] format --- cashu/lightning/clnrest.py | 2 +- cashu/lightning/fake.py | 6 +++--- cashu/lightning/lnd_grpc/lnd_grpc.py | 2 +- cashu/lightning/lndrest.py | 6 +++--- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/cashu/lightning/clnrest.py b/cashu/lightning/clnrest.py index 2d37e865d..7d3ff2344 100644 --- a/cashu/lightning/clnrest.py +++ b/cashu/lightning/clnrest.py @@ -12,6 +12,7 @@ from loguru import logger from ..core.base import Amount, MeltQuote, Unit +from ..core.errors import IncorrectRequestAmountError from ..core.helpers import fee_reserve from ..core.models import PostMeltQuoteRequest from ..core.settings import settings @@ -25,7 +26,6 @@ StatusResponse, Unsupported, ) -from ..core.errors import IncorrectRequestAmountError # https://docs.corelightning.org/reference/lightning-pay PAYMENT_RESULT_MAP = { diff --git a/cashu/lightning/fake.py b/cashu/lightning/fake.py index 08046a3b6..2c4ebc58c 100644 --- a/cashu/lightning/fake.py +++ b/cashu/lightning/fake.py @@ -18,6 +18,9 @@ ) from ..core.base import Amount, MeltQuote, Unit +from ..core.errors import ( + IncorrectRequestAmountError, +) from ..core.helpers import fee_reserve from ..core.models import PostMeltQuoteRequest from ..core.settings import settings @@ -30,9 +33,6 @@ PaymentStatus, StatusResponse, ) -from ..core.errors import ( - IncorrectRequestAmountError, -) class FakeWallet(LightningBackend): diff --git a/cashu/lightning/lnd_grpc/lnd_grpc.py b/cashu/lightning/lnd_grpc/lnd_grpc.py index 415008a22..f5fe8c339 100644 --- a/cashu/lightning/lnd_grpc/lnd_grpc.py +++ b/cashu/lightning/lnd_grpc/lnd_grpc.py @@ -17,6 +17,7 @@ import cashu.lightning.lnd_grpc.protos.router_pb2 as routerrpc import cashu.lightning.lnd_grpc.protos.router_pb2_grpc as routerstub from cashu.core.base import Amount, MeltQuote, Unit +from cashu.core.errors import IncorrectRequestAmountError from cashu.core.helpers import fee_reserve from cashu.core.settings import settings from cashu.lightning.base import ( @@ -29,7 +30,6 @@ PostMeltQuoteRequest, StatusResponse, ) -from cashu.core.errors import IncorrectRequestAmountError # maps statuses to None, False, True: # https://api.lightning.community/?python=#paymentpaymentstatus diff --git a/cashu/lightning/lndrest.py b/cashu/lightning/lndrest.py index d07d94772..e90b356f0 100644 --- a/cashu/lightning/lndrest.py +++ b/cashu/lightning/lndrest.py @@ -13,6 +13,9 @@ from loguru import logger from ..core.base import Amount, MeltQuote, Unit +from ..core.errors import ( + IncorrectRequestAmountError, +) from ..core.helpers import fee_reserve from ..core.models import PostMeltQuoteRequest from ..core.settings import settings @@ -25,9 +28,6 @@ PaymentStatus, StatusResponse, ) -from ..core.errors import ( - IncorrectRequestAmountError, -) from .macaroon import load_macaroon PAYMENT_RESULT_MAP = { From 1f14340dea40d04720f9b0053d776d9c7d36bd28 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Wed, 5 Mar 2025 13:21:01 +0100 Subject: [PATCH 21/69] add tests + fix --- cashu/mint/ledger.py | 4 --- tests/helpers.py | 2 ++ tests/test_mint_bolt11_amountless.py | 50 ++++++++++++++++++++++++++++ 3 files changed, 52 insertions(+), 4 deletions(-) create mode 100644 tests/test_mint_bolt11_amountless.py diff --git a/cashu/mint/ledger.py b/cashu/mint/ledger.py index 3864e5ae3..8113cacd2 100644 --- a/cashu/mint/ledger.py +++ b/cashu/mint/ledger.py @@ -724,8 +724,6 @@ async def melt_quote( # We assume that the request is a bolt11 invoice, this works since we # support only the bol11 method for now. invoice_obj = bolt11.decode(melt_quote.request) - if not invoice_obj.amount_msat: - raise TransactionError("invoice has no amount.") # we set the expiry of this quote to the expiry of the bolt11 invoice expiry = None if invoice_obj.expiry is not None: @@ -875,8 +873,6 @@ async def melt_mint_settle_internally( bolt11_request = melt_quote.request invoice_obj = bolt11.decode(bolt11_request) - if not invoice_obj.amount_msat: - raise TransactionError("invoice has no amount.") if not mint_quote.amount == melt_quote.amount: raise TransactionError("amounts do not match") if not bolt11_request == mint_quote.request: diff --git a/tests/helpers.py b/tests/helpers.py index d769b1907..2fa1b3015 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -60,6 +60,8 @@ async def get_random_invoice_data(): wallet_class = getattr(wallets_module, settings.mint_backend_bolt11_sat) WALLET = wallet_class() is_fake: bool = WALLET.__class__.__name__ == "FakeWallet" +is_cln: bool = WALLET.__class__.__name__ == "CLNRestWallet" +is_lnd: bool = WALLET.__class__.__name__ == "LndRPCWallet" or WALLET.__class__.__name__ == "LndRestWallet" is_regtest: bool = not is_fake is_deprecated_api_only = settings.debug_mint_only_deprecated is_github_actions = os.getenv("GITHUB_ACTIONS") == "true" diff --git a/tests/test_mint_bolt11_amountless.py b/tests/test_mint_bolt11_amountless.py new file mode 100644 index 000000000..d2587d045 --- /dev/null +++ b/tests/test_mint_bolt11_amountless.py @@ -0,0 +1,50 @@ +import httpx +import pytest +import pytest_asyncio +from .helpers import is_fake, is_cln, is_lnd, assert_err + +BASE_URL = "http://localhost:3337" + +invoice_no_amount = "lnbcrt1pnusdsqpp5fcxhgur2eewvsfy52q8xwanrjdglnf7htacp0ldeeakz6j62rj8sdqqcqzzsxqyz5vqsp5qk6l5dwhldy3gqjnr4806mtg22e25ekud4vdlf3p0hk89ud93lxs9qxpqysgq72fmgd460q04mvr5jetw7wys0vnt6ydl58gcg4jdy5jwx5d7epx8tr04et7a5yskwg4le54wrn6u6k0jjfehkc8n5spxkwxum239zxcqpuzakn" + +@pytest.mark.asyncio +@pytest.mark.skipif( + not (is_cln or is_lnd or is_fake), + reason="only run this test on fake, lnd or cln" +) +async def test_amountless_bolt11_invoice(mint): + response = httpx.post( + f"{BASE_URL}/v1/melt/quote/bolt11", + json={ + "request": invoice_no_amount, + "unit": "sat", + "options": { + "amountless": { + "amount_msat": 100000 + } + } + } + ) + + assert response.status_code == 200 + +@pytest.mark.asyncio +@pytest.mark.skipif( + is_cln or is_lnd or is_fake, + reason="only run for backends where amountless is not supported" +) +async def test_unsupported_amountless_bolt11_invoice(mint): + response = httpx.post( + f"{BASE_URL}/v1/melt/quote/bolt11", + json={ + "request": invoice_no_amount, + "unit": "sat", + "options": { + "amountless": { + "amount_msat": 100000 + } + } + } + ) + + assert response.status_code == 400 \ No newline at end of file From accfb1e28a4537ee03ad9a456f01d23a4ce645ce Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Wed, 5 Mar 2025 13:21:16 +0100 Subject: [PATCH 22/69] format --- tests/test_mint_bolt11_amountless.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_mint_bolt11_amountless.py b/tests/test_mint_bolt11_amountless.py index d2587d045..42a2c2432 100644 --- a/tests/test_mint_bolt11_amountless.py +++ b/tests/test_mint_bolt11_amountless.py @@ -1,7 +1,7 @@ import httpx import pytest -import pytest_asyncio -from .helpers import is_fake, is_cln, is_lnd, assert_err + +from .helpers import is_cln, is_fake, is_lnd BASE_URL = "http://localhost:3337" From e47c5c69faf15598c736f555d858168983ac94c8 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Wed, 5 Mar 2025 13:44:18 +0100 Subject: [PATCH 23/69] fix format err --- cashu/mint/ledger.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cashu/mint/ledger.py b/cashu/mint/ledger.py index 8113cacd2..f32b60c37 100644 --- a/cashu/mint/ledger.py +++ b/cashu/mint/ledger.py @@ -871,7 +871,6 @@ async def melt_mint_settle_internally( # verify amounts from bolt11 invoice bolt11_request = melt_quote.request - invoice_obj = bolt11.decode(bolt11_request) if not mint_quote.amount == melt_quote.amount: raise TransactionError("amounts do not match") From a12ab8fd5a25a3df05cb9e45402df241c27c2f00 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Wed, 5 Mar 2025 15:03:28 +0100 Subject: [PATCH 24/69] test info endpoint --- tests/test_mint_bolt11_amountless.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/test_mint_bolt11_amountless.py b/tests/test_mint_bolt11_amountless.py index 42a2c2432..1e8b29f93 100644 --- a/tests/test_mint_bolt11_amountless.py +++ b/tests/test_mint_bolt11_amountless.py @@ -47,4 +47,14 @@ async def test_unsupported_amountless_bolt11_invoice(mint): } ) - assert response.status_code == 400 \ No newline at end of file + assert response.status_code == 400 + +@pytest.mark.asyncio +@pytest.mark.skipif( + not(is_cln or is_lnd or is_fake), + reason="only run this test on fake, lnd or cln" +) +async def test_amountless_in_info_endpoint(mint): + response = httpx.get(f"{BASE_URL}/v1/info") + info = response.json() + assert info['nuts']['5']['methods'][0]['amountless'] == True \ No newline at end of file From c2aec9dbc6792adab7f2789313c3491f7bf2f43e Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Wed, 5 Mar 2025 15:23:03 +0100 Subject: [PATCH 25/69] test assert response.status_code == 200 --- tests/test_mint_bolt11_amountless.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_mint_bolt11_amountless.py b/tests/test_mint_bolt11_amountless.py index 1e8b29f93..2ab4da1b5 100644 --- a/tests/test_mint_bolt11_amountless.py +++ b/tests/test_mint_bolt11_amountless.py @@ -57,4 +57,5 @@ async def test_unsupported_amountless_bolt11_invoice(mint): async def test_amountless_in_info_endpoint(mint): response = httpx.get(f"{BASE_URL}/v1/info") info = response.json() + assert response.status_code == 200 assert info['nuts']['5']['methods'][0]['amountless'] == True \ No newline at end of file From 0cb8e8993f98a4069d582126e24a39933e6dfb33 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Thu, 6 Mar 2025 11:41:58 +0100 Subject: [PATCH 26/69] new `PaymentQuoteKind` enum --- cashu/core/base.py | 5 +++++ cashu/lightning/base.py | 2 ++ cashu/lightning/clnrest.py | 17 ++++++++++------- cashu/lightning/fake.py | 16 ++++++++++++---- cashu/lightning/lnd_grpc/lnd_grpc.py | 17 ++++++++++------- cashu/lightning/lndrest.py | 16 ++++++++++------ cashu/mint/ledger.py | 6 ++++++ 7 files changed, 55 insertions(+), 24 deletions(-) diff --git a/cashu/core/base.py b/cashu/core/base.py index 02c789b89..3420e9c60 100644 --- a/cashu/core/base.py +++ b/cashu/core/base.py @@ -275,6 +275,10 @@ class MeltQuoteState(Enum): def __str__(self): return self.name +class PaymentQuoteKind(Enum): + REGULAR = 0 + AMOUNTLESS = 1 + PARTIAL = 2 class MeltQuote(LedgerEvent): quote: str @@ -293,6 +297,7 @@ class MeltQuote(LedgerEvent): outputs: Optional[List[BlindedMessage]] = None change: Optional[List[BlindedSignature]] = None mint: Optional[str] = None + quote_kind: PaymentQuoteKind = PaymentQuoteKind.REGULAR @classmethod def from_row(cls, row: Row): diff --git a/cashu/lightning/base.py b/cashu/lightning/base.py index 1f47ff709..a0450f11e 100644 --- a/cashu/lightning/base.py +++ b/cashu/lightning/base.py @@ -7,6 +7,7 @@ from ..core.base import ( Amount, MeltQuote, + PaymentQuoteKind, Unit, ) from ..core.models import PostMeltQuoteRequest @@ -26,6 +27,7 @@ class PaymentQuoteResponse(BaseModel): checking_id: str amount: Amount fee: Amount + kind: PaymentQuoteKind = PaymentQuoteKind.REGULAR class InvoiceResponse(BaseModel): diff --git a/cashu/lightning/clnrest.py b/cashu/lightning/clnrest.py index a85e50d57..492bb9d78 100644 --- a/cashu/lightning/clnrest.py +++ b/cashu/lightning/clnrest.py @@ -11,7 +11,7 @@ ) from loguru import logger -from ..core.base import Amount, MeltQuote, Unit +from ..core.base import Amount, MeltQuote, PaymentQuoteKind, Unit from ..core.errors import IncorrectRequestAmountError from ..core.helpers import fee_reserve from ..core.models import PostMeltQuoteRequest @@ -334,23 +334,25 @@ async def get_payment_quote( invoice_obj = decode(melt_quote.request) - # Detect and handle amountless request + kind = PaymentQuoteKind.REGULAR + # Detect and handle amountless/partial/normal request amount_msat = 0 - if melt_quote.options and melt_quote.options.amountless: + if melt_quote.is_amountless: # Check that the user isn't doing something cheeky if (invoice_obj.amount_msat - and melt_quote.options.amountless.amount_msat != invoice_obj.amount_msat + and melt_quote.options.amountless.amount_msat != invoice_obj.amount_msat # type: ignore ): raise IncorrectRequestAmountError() amount_msat = melt_quote.options.amountless.amount_msat # type: ignore + kind = PaymentQuoteKind.AMOUNTLESS + elif melt_quote.is_mpp: + amount_msat = melt_quote.options.mpp.amount # type: ignore + kind = PaymentQuoteKind.PARTIAL elif invoice_obj.amount_msat: amount_msat = int(invoice_obj.amount_msat) else: raise Exception("request has no amount and is not specified as amountless") - if melt_quote.is_mpp: - amount_msat = melt_quote.mpp_amount - fees_msat = fee_reserve(amount_msat) fees = Amount(unit=Unit.msat, amount=fees_msat) amount = Amount(unit=Unit.msat, amount=amount_msat) @@ -358,4 +360,5 @@ async def get_payment_quote( checking_id=invoice_obj.payment_hash, fee=fees.to(self.unit, round="up"), amount=amount.to(self.unit, round="up"), + kind=kind ) diff --git a/cashu/lightning/fake.py b/cashu/lightning/fake.py index 2c4ebc58c..825a7fdb7 100644 --- a/cashu/lightning/fake.py +++ b/cashu/lightning/fake.py @@ -17,7 +17,7 @@ encode, ) -from ..core.base import Amount, MeltQuote, Unit +from ..core.base import Amount, MeltQuote, PaymentQuoteKind, Unit from ..core.errors import ( IncorrectRequestAmountError, ) @@ -218,15 +218,22 @@ async def get_payment_quote( ) -> PaymentQuoteResponse: invoice_obj = decode(melt_quote.request) - # Detect and handle amountless request + # Payment quote is determined and saved in the response + kind = PaymentQuoteKind.REGULAR + + # Detect and handle amountless/partial/normal request amount_msat = 0 - if melt_quote.options and melt_quote.options.amountless: + if melt_quote.is_amountless: # Check that the user isn't doing something cheeky if (invoice_obj.amount_msat - and melt_quote.options.amountless.amount_msat != invoice_obj.amount_msat + and melt_quote.options.amountless.amount_msat != invoice_obj.amount_msat # type: ignore ): raise IncorrectRequestAmountError() amount_msat = melt_quote.options.amountless.amount_msat # type: ignore + kind = PaymentQuoteKind.AMOUNTLESS + elif melt_quote.is_mpp: + amount_msat = melt_quote.options.mpp.amount # type: ignore + kind = PaymentQuoteKind.PARTIAL elif invoice_obj.amount_msat: amount_msat = int(invoice_obj.amount_msat) else: @@ -247,6 +254,7 @@ async def get_payment_quote( checking_id=invoice_obj.payment_hash, fee=fees.to(self.unit, round="up"), amount=amount.to(self.unit, round="up"), + kind=kind, ) async def paid_invoices_stream(self) -> AsyncGenerator[str, None]: diff --git a/cashu/lightning/lnd_grpc/lnd_grpc.py b/cashu/lightning/lnd_grpc/lnd_grpc.py index 710b3b611..bd2186b69 100644 --- a/cashu/lightning/lnd_grpc/lnd_grpc.py +++ b/cashu/lightning/lnd_grpc/lnd_grpc.py @@ -16,7 +16,7 @@ import cashu.lightning.lnd_grpc.protos.lightning_pb2_grpc as lightningstub import cashu.lightning.lnd_grpc.protos.router_pb2 as routerrpc import cashu.lightning.lnd_grpc.protos.router_pb2_grpc as routerstub -from cashu.core.base import Amount, MeltQuote, Unit +from cashu.core.base import Amount, MeltQuote, PaymentQuoteKind, Unit from cashu.core.errors import IncorrectRequestAmountError from cashu.core.helpers import fee_reserve from cashu.core.settings import settings @@ -379,23 +379,25 @@ async def get_payment_quote( ) -> PaymentQuoteResponse: invoice_obj = bolt11.decode(melt_quote.request) - # Detect and handle amountless request + kind = PaymentQuoteKind.REGULAR + # Detect and handle amountless/partial/normal request amount_msat = 0 - if melt_quote.options and melt_quote.options.amountless: + if melt_quote.is_amountless: # Check that the user isn't doing something cheeky if (invoice_obj.amount_msat - and melt_quote.options.amountless.amount_msat != invoice_obj.amount_msat + and melt_quote.options.amountless.amount_msat != invoice_obj.amount_msat # type: ignore ): raise IncorrectRequestAmountError() amount_msat = melt_quote.options.amountless.amount_msat # type: ignore + kind = PaymentQuoteKind.AMOUNTLESS + elif melt_quote.is_mpp: + amount_msat = melt_quote.options.mpp.amount # type: ignore + kind = PaymentQuoteKind.PARTIAL elif invoice_obj.amount_msat: amount_msat = int(invoice_obj.amount_msat) else: raise Exception("request has no amount and is not specified as amountless") - if melt_quote.is_mpp: - amount_msat = int(invoice_obj.amount_msat) - fees_msat = fee_reserve(amount_msat) fees = Amount(unit=Unit.msat, amount=fees_msat) @@ -405,4 +407,5 @@ async def get_payment_quote( checking_id=invoice_obj.payment_hash, fee=fees.to(self.unit, round="up"), amount=amount.to(self.unit, round="up"), + kind=kind, ) diff --git a/cashu/lightning/lndrest.py b/cashu/lightning/lndrest.py index 841c9eacb..cda4d1444 100644 --- a/cashu/lightning/lndrest.py +++ b/cashu/lightning/lndrest.py @@ -12,7 +12,7 @@ ) from loguru import logger -from ..core.base import Amount, MeltQuote, Unit +from ..core.base import Amount, MeltQuote, PaymentQuoteKind, Unit from ..core.errors import ( IncorrectRequestAmountError, ) @@ -409,22 +409,25 @@ async def get_payment_quote( ) -> PaymentQuoteResponse: invoice_obj = decode(melt_quote.request) - # Detect and handle amountless request + kind = PaymentQuoteKind.REGULAR + # Detect and handle amountless/partial/normal request amount_msat = 0 - if melt_quote.options and melt_quote.options.amountless: + if melt_quote.is_amountless: # Check that the user isn't doing something cheeky if (invoice_obj.amount_msat - and melt_quote.options.amountless.amount_msat != invoice_obj.amount_msat + and melt_quote.options.amountless.amount_msat != invoice_obj.amount_msat # type: ignore ): raise IncorrectRequestAmountError() amount_msat = melt_quote.options.amountless.amount_msat # type: ignore + kind = PaymentQuoteKind.AMOUNTLESS + elif melt_quote.is_mpp: + amount_msat = melt_quote.options.mpp.amount # type: ignore + kind = PaymentQuoteKind.PARTIAL elif invoice_obj.amount_msat: amount_msat = int(invoice_obj.amount_msat) else: raise Exception("request has no amount and is not specified as amountless") - amount_msat = melt_quote.mpp_amount if melt_quote.is_mpp else amount_msat - fees_msat = fee_reserve(amount_msat) fees = Amount(unit=Unit.msat, amount=fees_msat) @@ -434,4 +437,5 @@ async def get_payment_quote( checking_id=invoice_obj.payment_hash, fee=fees.to(self.unit, round="up"), amount=amount.to(self.unit, round="up"), + kind=kind, ) diff --git a/cashu/mint/ledger.py b/cashu/mint/ledger.py index 46042c0a2..0a713b4c2 100644 --- a/cashu/mint/ledger.py +++ b/cashu/mint/ledger.py @@ -640,8 +640,12 @@ def create_internal_melt_quote( if not mint_quote.checking_id: raise TransactionError("mint quote has no checking id") + + # Kind of payment quote: REGULAR, PARTIAL, AMOUNTLESS if melt_quote.is_mpp: raise TransactionError("internal payments do not support mpp") + elif melt_quote.is_amountless: + raise TransactionError("internal payments cannot have amountless request strings") internal_fee = Amount(unit, 0) # no internal fees amount = Amount(unit, mint_quote.amount) @@ -734,6 +738,7 @@ async def melt_quote( # We assume that the request is a bolt11 invoice, this works since we # support only the bol11 method for now. invoice_obj = bolt11.decode(melt_quote.request) + # we set the expiry of this quote to the expiry of the bolt11 invoice expiry = None if invoice_obj.expiry is not None: @@ -750,6 +755,7 @@ async def melt_quote( fee_reserve=payment_quote.fee.to(unit).amount, created_time=int(time.time()), expiry=expiry, + quote_kind=payment_quote.kind, ) await self.crud.store_melt_quote(quote=quote, db=self.db) await self.events.submit(quote) From 58f888ee16e9ed669523f2dc3c96e9bcc3fc8b3f Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Thu, 6 Mar 2025 15:07:10 +0100 Subject: [PATCH 27/69] `pay_invoice` handle different `PaymentQuoteKind` --- cashu/core/base.py | 4 +++ cashu/lightning/clnrest.py | 51 ++++++++++++++++++---------- cashu/lightning/lnd_grpc/lnd_grpc.py | 50 +++++++++++++++++++-------- cashu/lightning/lndrest.py | 39 +++++++++++++++------ 4 files changed, 102 insertions(+), 42 deletions(-) diff --git a/cashu/core/base.py b/cashu/core/base.py index 3420e9c60..f58cd4f7f 100644 --- a/cashu/core/base.py +++ b/cashu/core/base.py @@ -276,8 +276,12 @@ def __str__(self): return self.name class PaymentQuoteKind(Enum): + # Regular payments REGULAR = 0 + # Payments for which the request string did not specify an amount AMOUNTLESS = 1 + # Payments for which this Mint is expect to pay only a part of the total amount + # of the request string PARTIAL = 2 class MeltQuote(LedgerEvent): diff --git a/cashu/lightning/clnrest.py b/cashu/lightning/clnrest.py index 492bb9d78..4e7a315f9 100644 --- a/cashu/lightning/clnrest.py +++ b/cashu/lightning/clnrest.py @@ -181,13 +181,6 @@ async def pay_invoice( error_message=str(exc), ) - if not invoice.amount_msat or invoice.amount_msat <= 0: - error_message = "0 amount invoices are not allowed" - return PaymentResponse( - result=PaymentResult.FAILED, - error_message=error_message, - ) - quote_amount_msat = Amount(Unit[quote.unit], quote.amount).to(Unit.msat).amount fee_limit_percent = fee_limit_msat / quote_amount_msat * 100 post_data = { @@ -197,18 +190,40 @@ async def pay_invoice( # with fee < 5000 millisatoshi (which is default value of exemptfee) } - # Handle Multi-Mint payout where we must only pay part of the invoice amount logger.trace(f"{quote_amount_msat = }, {invoice.amount_msat = }") - if quote_amount_msat != invoice.amount_msat: - logger.trace("Detected Multi-Nut payment") - if self.supports_mpp: - post_data["partial_msat"] = quote_amount_msat - else: - error_message = "mint does not support MPP" - logger.error(error_message) - return PaymentResponse( - result=PaymentResult.FAILED, error_message=error_message - ) + + # Handle AMOUNTLESS, PARTIAL and REGULAR payment quotes + match quote.quote_kind: + case PaymentQuoteKind.REGULAR: + logger.debug("Paying REGULAR quote") + if not invoice.amount_msat: + error_message = "amountless invoices are not allowed" + return PaymentResponse( + result=PaymentResult.FAILED, + error_message=error_message, + ) + case PaymentQuoteKind.AMOUNTLESS: + logger.debug("Paying AMOUNTLESS quote") + if self.supports_amountless: + post_data["amount_msat"] = quote_amount_msat + else: + error_message = "mint does not support amountless invoices" + logger.error(error_message) + return PaymentResponse( + result=PaymentResult.FAILED, error_message=error_message + ) + case PaymentQuoteKind.PARTIAL: + logger.debug("Paying PARTIAL/MPP quote") + # Handle Multi-Mint payout where we must only pay part of the invoice amount + if self.supports_mpp: + post_data["partial_msat"] = quote_amount_msat + else: + error_message = "mint does not support MPP" + logger.error(error_message) + return PaymentResponse( + result=PaymentResult.FAILED, error_message=error_message + ) + r = await self.client.post("/v1/pay", data=post_data, timeout=None) if r.is_error or "message" in r.json(): diff --git a/cashu/lightning/lnd_grpc/lnd_grpc.py b/cashu/lightning/lnd_grpc/lnd_grpc.py index bd2186b69..be1cf8703 100644 --- a/cashu/lightning/lnd_grpc/lnd_grpc.py +++ b/cashu/lightning/lnd_grpc/lnd_grpc.py @@ -157,30 +157,52 @@ async def create_invoice( async def pay_invoice( self, quote: MeltQuote, fee_limit_msat: int ) -> PaymentResponse: - # if the amount of the melt quote is different from the request - # call pay_partial_invoice instead + + # set the fee limit for the payment + send_request = None invoice = bolt11.decode(quote.request) - if invoice.amount_msat: - amount_msat = int(invoice.amount_msat) - if amount_msat != quote.amount * 1000 and self.supports_mpp: - return await self.pay_partial_invoice( - quote, Amount(Unit.sat, quote.amount), fee_limit_msat + feelimit = lnrpc.FeeLimit(fixed_msat=fee_limit_msat) + + match quote.quote_kind: + case PaymentQuoteKind.PARTIAL: + if self.supports_mpp: + return await self.pay_partial_invoice( + quote, Amount(Unit.sat, quote.amount), fee_limit_msat + ) + else: + error_message = "MPP Payments are not enabled" + logger.error(error_message) + return PaymentResponse( + result=PaymentResult.FAILED, error_message=error_message + ) + case PaymentQuoteKind.AMOUNTLESS: + if self.supports_amountless: + # amount of the quote converted to msat + send_request = lnrpc.SendRequest( + payment_request=quote.request, + amount_msat=Amount(quote.unit, quote.amount).to(Unit.msat, round="up").amount, + fee_limit=feelimit + ) + else: + error_message = "Amountless payments are not enabled" + logger.error(error_message) + return PaymentResponse( + result=PaymentResult.FAILED, error_message=error_message + ) + case PaymentQuoteKind.REGULAR: + send_request = lnrpc.SendRequest( + payment_request=quote.request, + fee_limit=feelimit ) # set the fee limit for the payment - feelimit = lnrpc.FeeLimit(fixed_msat=fee_limit_msat) r = None try: async with grpc.aio.secure_channel( self.endpoint, self.combined_creds ) as channel: lnstub = lightningstub.LightningStub(channel) - r = await lnstub.SendPaymentSync( - lnrpc.SendRequest( - payment_request=quote.request, - fee_limit=feelimit, - ) - ) + r = await lnstub.SendPaymentSync(send_request) except AioRpcError as e: error_message = f"SendPaymentSync failed: {e}" return PaymentResponse( diff --git a/cashu/lightning/lndrest.py b/cashu/lightning/lndrest.py index cda4d1444..f7d330a77 100644 --- a/cashu/lightning/lndrest.py +++ b/cashu/lightning/lndrest.py @@ -183,23 +183,42 @@ async def create_invoice( async def pay_invoice( self, quote: MeltQuote, fee_limit_msat: int ) -> PaymentResponse: - # if the amount of the melt quote is different from the request - # call pay_partial_invoice instead - invoice = bolt11.decode(quote.request) - if invoice.amount_msat: - amount_msat = int(invoice.amount_msat) - if amount_msat != quote.amount * 1000 and self.supports_mpp: - return await self.pay_partial_invoice( - quote, Amount(Unit.sat, quote.amount), fee_limit_msat - ) # set the fee limit for the payment lnrpcFeeLimit = dict() lnrpcFeeLimit["fixed_msat"] = f"{fee_limit_msat}" + post_data = {"payment_request": quote.request, "fee_limit": lnrpcFeeLimit} + invoice = bolt11.decode(quote.request) + + match quote.quote_kind: + case PaymentQuoteKind.PARTIAL: + if self.supports_mpp: + return await self.pay_partial_invoice( + quote, Amount(Unit.sat, quote.amount), fee_limit_msat + ) + else: + error_message = "MPP Payments are not enabled" + logger.error(error_message) + return PaymentResponse( + result=PaymentResult.FAILED, error_message=error_message + ) + case PaymentQuoteKind.AMOUNTLESS: + if self.supports_amountless: + # amount of the quote converted to msat + post_data["amt_msat"] = Amount(quote.unit, quote.amount).to(Unit.msat, round="up").amount + else: + error_message = "Amountless payments are not enabled" + logger.error(error_message) + return PaymentResponse( + result=PaymentResult.FAILED, error_message=error_message + ) + case _: + pass + r = await self.client.post( url="/v1/channels/transactions", - json={"payment_request": quote.request, "fee_limit": lnrpcFeeLimit}, + json=post_data, timeout=None, ) From c97d70b1a93a7fa2158a024540c85d1563f3752f Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Thu, 6 Mar 2025 15:14:09 +0100 Subject: [PATCH 28/69] fix --- cashu/lightning/lnd_grpc/lnd_grpc.py | 2 +- cashu/lightning/lndrest.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cashu/lightning/lnd_grpc/lnd_grpc.py b/cashu/lightning/lnd_grpc/lnd_grpc.py index be1cf8703..7dbb21be1 100644 --- a/cashu/lightning/lnd_grpc/lnd_grpc.py +++ b/cashu/lightning/lnd_grpc/lnd_grpc.py @@ -180,7 +180,7 @@ async def pay_invoice( # amount of the quote converted to msat send_request = lnrpc.SendRequest( payment_request=quote.request, - amount_msat=Amount(quote.unit, quote.amount).to(Unit.msat, round="up").amount, + amt_msat=Amount(Unit[quote.unit], quote.amount).to(Unit.msat, round="up").amount, fee_limit=feelimit ) else: diff --git a/cashu/lightning/lndrest.py b/cashu/lightning/lndrest.py index f7d330a77..15cc36dee 100644 --- a/cashu/lightning/lndrest.py +++ b/cashu/lightning/lndrest.py @@ -206,7 +206,7 @@ async def pay_invoice( case PaymentQuoteKind.AMOUNTLESS: if self.supports_amountless: # amount of the quote converted to msat - post_data["amt_msat"] = Amount(quote.unit, quote.amount).to(Unit.msat, round="up").amount + post_data["amt_msat"] = Amount(Unit[quote.unit], quote.amount).to(Unit.msat, round="up").amount else: error_message = "Amountless payments are not enabled" logger.error(error_message) From db6bc123181e17d0aa9f6921521c44fd67661fad Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Thu, 6 Mar 2025 15:51:42 +0100 Subject: [PATCH 29/69] add `PaymentQuoteKind` to db with a new migration --- cashu/core/base.py | 10 +++++++--- cashu/mint/crud.py | 5 +++-- cashu/mint/migrations.py | 11 ++++++++++- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/cashu/core/base.py b/cashu/core/base.py index f58cd4f7f..10597a59a 100644 --- a/cashu/core/base.py +++ b/cashu/core/base.py @@ -277,12 +277,15 @@ def __str__(self): class PaymentQuoteKind(Enum): # Regular payments - REGULAR = 0 + REGULAR = "REGULAR" # Payments for which the request string did not specify an amount - AMOUNTLESS = 1 + AMOUNTLESS = "AMOUNTLESS" # Payments for which this Mint is expect to pay only a part of the total amount # of the request string - PARTIAL = 2 + PARTIAL = "PARTIAL" + + def __str__(self) -> str: + return self.name class MeltQuote(LedgerEvent): quote: str @@ -343,6 +346,7 @@ def from_row(cls, row: Row): change=change, expiry=expiry, payment_preimage=payment_preimage, + quote_kind=PaymentQuoteKind(row["kind"]), ) @classmethod diff --git a/cashu/mint/crud.py b/cashu/mint/crud.py index 6ff2a6302..1ef52e96d 100644 --- a/cashu/mint/crud.py +++ b/cashu/mint/crud.py @@ -523,8 +523,8 @@ async def store_melt_quote( await (conn or db).execute( f""" INSERT INTO {db.table_with_schema('melt_quotes')} - (quote, method, request, checking_id, unit, amount, fee_reserve, state, paid, created_time, paid_time, fee_paid, proof, outputs, change, expiry) - VALUES (:quote, :method, :request, :checking_id, :unit, :amount, :fee_reserve, :state, :paid, :created_time, :paid_time, :fee_paid, :proof, :outputs, :change, :expiry) + (quote, method, request, checking_id, unit, amount, fee_reserve, state, paid, created_time, paid_time, fee_paid, proof, outputs, change, expiry, kind) + VALUES (:quote, :method, :request, :checking_id, :unit, :amount, :fee_reserve, :state, :paid, :created_time, :paid_time, :fee_paid, :proof, :outputs, :change, :expiry, :kind) """, { "quote": quote.quote, @@ -549,6 +549,7 @@ async def store_melt_quote( "expiry": db.to_timestamp( db.timestamp_from_seconds(quote.expiry) or "" ), + "kind": str(quote.quote_kind) }, ) diff --git a/cashu/mint/migrations.py b/cashu/mint/migrations.py index 79f21dccd..15ca944d4 100644 --- a/cashu/mint/migrations.py +++ b/cashu/mint/migrations.py @@ -1,7 +1,7 @@ import copy from typing import Dict, List -from ..core.base import MeltQuoteState, MintKeyset, MintQuoteState, Proof +from ..core.base import MeltQuoteState, MintKeyset, MintQuoteState, Proof, PaymentQuoteKind from ..core.crypto.keys import derive_keyset_id, derive_keyset_id_deprecated from ..core.db import Connection, Database from ..core.settings import settings @@ -908,3 +908,12 @@ async def m026_keyset_specific_balance_views(db: Database): ); """ ) + +async def m027_add_payment_quote_kind(db: Database): + async with db.connect() as conn: + await conn.execute( + f""" + ALTER TABLE {db.table_with_schema('melt_quotes')} + ADD COLUMN kind TEXT NOT NULL DEFAULT '{str(PaymentQuoteKind.REGULAR)}' + """ + ) \ No newline at end of file From c0e550eb34b30ebb2683a6306a18aedb3428f5cf Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Fri, 7 Mar 2025 10:42:24 +0100 Subject: [PATCH 30/69] wallet --- cashu/core/mint_info.py | 17 ++++++++++++++++- cashu/wallet/v1_api.py | 18 ++++++++++++------ cashu/wallet/wallet.py | 9 +++++++-- 3 files changed, 35 insertions(+), 9 deletions(-) diff --git a/cashu/core/mint_info.py b/cashu/core/mint_info.py index 93e118f2b..958d48dfa 100644 --- a/cashu/core/mint_info.py +++ b/cashu/core/mint_info.py @@ -6,7 +6,7 @@ from .base import Method, Unit from .models import MintInfoContact, MintInfoProtectedEndpoint, Nut15MppSupport -from .nuts.nuts import BLIND_AUTH_NUT, CLEAR_AUTH_NUT, MPP_NUT, WEBSOCKETS_NUT +from .nuts.nuts import BLIND_AUTH_NUT, CLEAR_AUTH_NUT, MPP_NUT, WEBSOCKETS_NUT, MELT_NUT class MintInfo(BaseModel): @@ -47,6 +47,21 @@ def supports_mpp(self, method: str, unit: Unit) -> bool: return False + def supports_amountless(self, method: str, unit: Unit) -> bool: + if not self.nuts: + return False + nut_5 = self.nuts.get(MELT_NUT) + + for entry in nut_5["methods"]: + if ( + entry["method"] == method + and entry["unit"] == str(unit) + and entry.get("amountless", None) + ): + return True + + return False + def supports_websocket_mint_quote(self, method: Method, unit: Unit) -> bool: if not self.nuts or not self.supports_nut(WEBSOCKETS_NUT): return False diff --git a/cashu/wallet/v1_api.py b/cashu/wallet/v1_api.py index a62c8a475..b19f4b87e 100644 --- a/cashu/wallet/v1_api.py +++ b/cashu/wallet/v1_api.py @@ -38,6 +38,7 @@ PostMeltQuoteResponse, PostMeltRequest, PostMeltRequestOptionMpp, + PostMeltRequestOptionAmountless, PostMeltRequestOptions, PostMeltResponse_deprecated, PostMintQuoteRequest, @@ -438,14 +439,19 @@ async def melt_quote( ) -> 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 + + # Add melt options: detect whether this should be an amountless option + # or a MPP option melt_options = None if amount_msat: - melt_options = PostMeltRequestOptions( - mpp=PostMeltRequestOptionMpp(amount=amount_msat) - ) + if invoice_obj.amount_msat: + melt_options = PostMeltRequestOptions( + mpp=PostMeltRequestOptionMpp(amount=amount_msat) + ) + else: + melt_options = PostMeltRequestOptions( + amountless=PostMeltRequestOptionAmountless(amount_msat=amount_msat) + ) payload = PostMeltQuoteRequest( unit=unit.name, request=payment_request, options=melt_options diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index dac05dc2a..bed6964b4 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -2,6 +2,7 @@ import json import threading import time +import bolt11 from typing import Callable, Dict, List, Optional, Tuple, Union from bip32 import BIP32 @@ -706,8 +707,12 @@ async def melt_quote( """ Fetches a melt quote from the mint and either uses the amount in the invoice or the amount provided. """ - if amount_msat and not self.mint_info.supports_mpp("bolt11", self.unit): - raise Exception("Mint does not support MPP, cannot specify amount.") + if amount_msat: + invoice_obj = bolt11.decode(invoice) + if not invoice_obj.amount_msat and not self.mint_info.supports_amountless("bolt11", self.unit): + raise Exception("Mint does not support amountless invoices, cannot pay this invoice.") + if invoice_obj.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_msat) logger.debug( f"Mint wants {self.unit.str(melt_quote_resp.fee_reserve)} as fee reserve." From 65d4200f56a78686adf64fda1a8518275c242274 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Fri, 7 Mar 2025 10:47:57 +0100 Subject: [PATCH 31/69] fix --- cashu/core/mint_info.py | 3 ++- cashu/wallet/v1_api.py | 6 +++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/cashu/core/mint_info.py b/cashu/core/mint_info.py index 958d48dfa..27cfd46ff 100644 --- a/cashu/core/mint_info.py +++ b/cashu/core/mint_info.py @@ -51,7 +51,8 @@ def supports_amountless(self, method: str, unit: Unit) -> bool: if not self.nuts: return False nut_5 = self.nuts.get(MELT_NUT) - + if not nut_5 or not nut_5.get("methods"): + return False for entry in nut_5["methods"]: if ( entry["method"] == method diff --git a/cashu/wallet/v1_api.py b/cashu/wallet/v1_api.py index b19f4b87e..a5b9eaf51 100644 --- a/cashu/wallet/v1_api.py +++ b/cashu/wallet/v1_api.py @@ -440,6 +440,10 @@ async def melt_quote( """Checks whether the Lightning payment is internal.""" invoice_obj = bolt11.decode(payment_request) + assert amount_msat is not None or invoice_obj.amount_msat is not None, ( + "No amount found. Either the invoice has an amount or the amount must be specified." + ) + # Add melt options: detect whether this should be an amountless option # or a MPP option melt_options = None @@ -470,7 +474,7 @@ async def melt_quote( ) quote_id = f"deprecated_{uuid.uuid4()}" amount_sat = ( - amount_msat // 1000 if amount_msat else invoice_obj.amount_msat // 1000 + amount_msat // 1000 if amount_msat else (invoice_obj.amount_msat or 0) // 1000 ) return PostMeltQuoteResponse( quote=quote_id, From f1108df64e690521f10e66b2ede6b94fad12a497 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Fri, 7 Mar 2025 10:48:40 +0100 Subject: [PATCH 32/69] concise assert --- cashu/wallet/v1_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cashu/wallet/v1_api.py b/cashu/wallet/v1_api.py index a5b9eaf51..5164a03d8 100644 --- a/cashu/wallet/v1_api.py +++ b/cashu/wallet/v1_api.py @@ -440,7 +440,7 @@ async def melt_quote( """Checks whether the Lightning payment is internal.""" invoice_obj = bolt11.decode(payment_request) - assert amount_msat is not None or invoice_obj.amount_msat is not None, ( + assert amount_msat or invoice_obj.amount_msat ( "No amount found. Either the invoice has an amount or the amount must be specified." ) From 8600ff81c6fde0c7eed4ae04a6c8d540636a01b8 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Fri, 7 Mar 2025 12:43:31 +0100 Subject: [PATCH 33/69] tests + wallet fixes --- cashu/core/mint_info.py | 2 +- cashu/lightning/lnd_grpc/lnd_grpc.py | 2 +- cashu/lightning/lndrest.py | 8 ++-- cashu/mint/migrations.py | 8 +++- cashu/wallet/v1_api.py | 4 +- cashu/wallet/wallet.py | 5 ++- tests/test_mint_bolt11_amountless.py | 61 ---------------------------- tests/test_wallet_amountless.py | 59 +++++++++++++++++++++++++++ 8 files changed, 77 insertions(+), 72 deletions(-) delete mode 100644 tests/test_mint_bolt11_amountless.py create mode 100644 tests/test_wallet_amountless.py diff --git a/cashu/core/mint_info.py b/cashu/core/mint_info.py index 27cfd46ff..a9a56ae29 100644 --- a/cashu/core/mint_info.py +++ b/cashu/core/mint_info.py @@ -6,7 +6,7 @@ from .base import Method, Unit from .models import MintInfoContact, MintInfoProtectedEndpoint, Nut15MppSupport -from .nuts.nuts import BLIND_AUTH_NUT, CLEAR_AUTH_NUT, MPP_NUT, WEBSOCKETS_NUT, MELT_NUT +from .nuts.nuts import BLIND_AUTH_NUT, CLEAR_AUTH_NUT, MELT_NUT, MPP_NUT, WEBSOCKETS_NUT class MintInfo(BaseModel): diff --git a/cashu/lightning/lnd_grpc/lnd_grpc.py b/cashu/lightning/lnd_grpc/lnd_grpc.py index 7dbb21be1..b443c4463 100644 --- a/cashu/lightning/lnd_grpc/lnd_grpc.py +++ b/cashu/lightning/lnd_grpc/lnd_grpc.py @@ -160,7 +160,7 @@ async def pay_invoice( # set the fee limit for the payment send_request = None - invoice = bolt11.decode(quote.request) + #invoice = bolt11.decode(quote.request) feelimit = lnrpc.FeeLimit(fixed_msat=fee_limit_msat) match quote.quote_kind: diff --git a/cashu/lightning/lndrest.py b/cashu/lightning/lndrest.py index 15cc36dee..5e4c738f7 100644 --- a/cashu/lightning/lndrest.py +++ b/cashu/lightning/lndrest.py @@ -185,11 +185,11 @@ async def pay_invoice( ) -> PaymentResponse: # set the fee limit for the payment - lnrpcFeeLimit = dict() - lnrpcFeeLimit["fixed_msat"] = f"{fee_limit_msat}" + fee_limit_dict = dict() + fee_limit_dict["fixed_msat"] = f"{fee_limit_msat}" - post_data = {"payment_request": quote.request, "fee_limit": lnrpcFeeLimit} - invoice = bolt11.decode(quote.request) + post_data = {"payment_request": quote.request, "fee_limit": fee_limit_dict} + #invoice = bolt11.decode(quote.request) match quote.quote_kind: case PaymentQuoteKind.PARTIAL: diff --git a/cashu/mint/migrations.py b/cashu/mint/migrations.py index 15ca944d4..98b1928c1 100644 --- a/cashu/mint/migrations.py +++ b/cashu/mint/migrations.py @@ -1,7 +1,13 @@ import copy from typing import Dict, List -from ..core.base import MeltQuoteState, MintKeyset, MintQuoteState, Proof, PaymentQuoteKind +from ..core.base import ( + MeltQuoteState, + MintKeyset, + MintQuoteState, + PaymentQuoteKind, + Proof, +) from ..core.crypto.keys import derive_keyset_id, derive_keyset_id_deprecated from ..core.db import Connection, Database from ..core.settings import settings diff --git a/cashu/wallet/v1_api.py b/cashu/wallet/v1_api.py index 5164a03d8..c508733e8 100644 --- a/cashu/wallet/v1_api.py +++ b/cashu/wallet/v1_api.py @@ -37,8 +37,8 @@ PostMeltQuoteRequest, PostMeltQuoteResponse, PostMeltRequest, - PostMeltRequestOptionMpp, PostMeltRequestOptionAmountless, + PostMeltRequestOptionMpp, PostMeltRequestOptions, PostMeltResponse_deprecated, PostMintQuoteRequest, @@ -440,7 +440,7 @@ async def melt_quote( """Checks whether the Lightning payment is internal.""" invoice_obj = bolt11.decode(payment_request) - assert amount_msat or invoice_obj.amount_msat ( + assert amount_msat or invoice_obj.amount_msat, ( "No amount found. Either the invoice has an amount or the amount must be specified." ) diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index bed6964b4..c97a33ee2 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -2,9 +2,9 @@ import json import threading import time -import bolt11 from typing import Callable, Dict, List, Optional, Tuple, Union +import bolt11 from bip32 import BIP32 from loguru import logger @@ -736,7 +736,8 @@ async def melt( proofs (List[Proof]): List of proofs to be spent. invoice (str): Lightning invoice to be paid. fee_reserve_sat (int): Amount of fees to be reserved for the payment. - + quote_id (str): ID of the melt quote to pay. + """ # Make sure we're operating on an independent copy of proofs proofs = copy.copy(proofs) diff --git a/tests/test_mint_bolt11_amountless.py b/tests/test_mint_bolt11_amountless.py deleted file mode 100644 index 2ab4da1b5..000000000 --- a/tests/test_mint_bolt11_amountless.py +++ /dev/null @@ -1,61 +0,0 @@ -import httpx -import pytest - -from .helpers import is_cln, is_fake, is_lnd - -BASE_URL = "http://localhost:3337" - -invoice_no_amount = "lnbcrt1pnusdsqpp5fcxhgur2eewvsfy52q8xwanrjdglnf7htacp0ldeeakz6j62rj8sdqqcqzzsxqyz5vqsp5qk6l5dwhldy3gqjnr4806mtg22e25ekud4vdlf3p0hk89ud93lxs9qxpqysgq72fmgd460q04mvr5jetw7wys0vnt6ydl58gcg4jdy5jwx5d7epx8tr04et7a5yskwg4le54wrn6u6k0jjfehkc8n5spxkwxum239zxcqpuzakn" - -@pytest.mark.asyncio -@pytest.mark.skipif( - not (is_cln or is_lnd or is_fake), - reason="only run this test on fake, lnd or cln" -) -async def test_amountless_bolt11_invoice(mint): - response = httpx.post( - f"{BASE_URL}/v1/melt/quote/bolt11", - json={ - "request": invoice_no_amount, - "unit": "sat", - "options": { - "amountless": { - "amount_msat": 100000 - } - } - } - ) - - assert response.status_code == 200 - -@pytest.mark.asyncio -@pytest.mark.skipif( - is_cln or is_lnd or is_fake, - reason="only run for backends where amountless is not supported" -) -async def test_unsupported_amountless_bolt11_invoice(mint): - response = httpx.post( - f"{BASE_URL}/v1/melt/quote/bolt11", - json={ - "request": invoice_no_amount, - "unit": "sat", - "options": { - "amountless": { - "amount_msat": 100000 - } - } - } - ) - - assert response.status_code == 400 - -@pytest.mark.asyncio -@pytest.mark.skipif( - not(is_cln or is_lnd or is_fake), - reason="only run this test on fake, lnd or cln" -) -async def test_amountless_in_info_endpoint(mint): - response = httpx.get(f"{BASE_URL}/v1/info") - info = response.json() - assert response.status_code == 200 - assert info['nuts']['5']['methods'][0]['amountless'] == True \ No newline at end of file diff --git a/tests/test_wallet_amountless.py b/tests/test_wallet_amountless.py new file mode 100644 index 000000000..e9dd80618 --- /dev/null +++ b/tests/test_wallet_amountless.py @@ -0,0 +1,59 @@ +import pytest +import pytest_asyncio + +from cashu.wallet.wallet import Wallet +from tests.conftest import SERVER_ENDPOINT +from tests.helpers import ( + get_real_invoice, + is_cln, + is_fake, + is_lnd, + pay_if_regtest, +) + +invoice_no_amount = "lnbcrt1pnusdsqpp5fcxhgur2eewvsfy52q8xwanrjdglnf7htacp0ldeeakz6j62rj8sdqqcqzzsxqyz5vqsp5qk6l5dwhldy3gqjnr4806mtg22e25ekud4vdlf3p0hk89ud93lxs9qxpqysgq72fmgd460q04mvr5jetw7wys0vnt6ydl58gcg4jdy5jwx5d7epx8tr04et7a5yskwg4le54wrn6u6k0jjfehkc8n5spxkwxum239zxcqpuzakn" + +@pytest_asyncio.fixture(scope="function") +async def wallet(): + wallet = await Wallet.with_db( + url=SERVER_ENDPOINT, + db="test_data/wallet", + name="wallet", + ) + await wallet.load_mint() + yield wallet + +@pytest.mark.asyncio +@pytest.mark.skipif( + not (is_cln or is_lnd or is_fake), + reason="only run this test on fake, lnd or cln" +) +async def test_amountless_bolt11_invoice(wallet: Wallet): + # make sure wallet knows the backend supports mpp + assert wallet.mint_info.supports_amountless("bolt11", wallet.unit) + + # top up wallet + topup_mint_quote = await wallet.request_mint(128) + + await pay_if_regtest(topup_mint_quote.request) + proofs = await wallet.mint(128, quote_id=topup_mint_quote.quote) + assert wallet.balance == 128 + + amountless_invoice = invoice_no_amount if is_fake else get_real_invoice(0)['payment_request'] + + melt_quote = await wallet.melt_quote(amountless_invoice, 100*1000) + assert melt_quote.amount == 100 + + result = await wallet.melt(proofs, amountless_invoice, melt_quote.fee_reserve, melt_quote.quote) + assert result.state == "PAID" + + +@pytest.mark.asyncio +@pytest.mark.skipif( + is_cln or is_lnd or is_fake, + reason="only run for backends where amountless is not supported" +) +async def test_unsupported_amountless_bolt11_invoice(wallet: Wallet): + amountless_invoice = invoice_no_amount if is_fake else get_real_invoice(0)['payment_request'] + assert_err(wallet.melt_quote(amountless_invoice, 100*1000), "Mint does not support amountless invoices, cannot pay this invoice.") + From c9b3f33856937d5af98d829737f756d9ce96d031 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Fri, 7 Mar 2025 12:44:41 +0100 Subject: [PATCH 34/69] fix test --- tests/test_wallet_amountless.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_wallet_amountless.py b/tests/test_wallet_amountless.py index e9dd80618..24622343d 100644 --- a/tests/test_wallet_amountless.py +++ b/tests/test_wallet_amountless.py @@ -4,6 +4,7 @@ from cashu.wallet.wallet import Wallet from tests.conftest import SERVER_ENDPOINT from tests.helpers import ( + assert_err, get_real_invoice, is_cln, is_fake, @@ -44,6 +45,8 @@ async def test_amountless_bolt11_invoice(wallet: Wallet): melt_quote = await wallet.melt_quote(amountless_invoice, 100*1000) assert melt_quote.amount == 100 + await pay_if_regtest(amountless_invoice) + result = await wallet.melt(proofs, amountless_invoice, melt_quote.fee_reserve, melt_quote.quote) assert result.state == "PAID" From e500e922c7ea1696a8c0340e636bb663732eed6d Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Fri, 7 Mar 2025 12:55:12 +0100 Subject: [PATCH 35/69] fix regtest mpp --- tests/test_wallet_regtest_mpp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_wallet_regtest_mpp.py b/tests/test_wallet_regtest_mpp.py index b07ce52f6..8236c3735 100644 --- a/tests/test_wallet_regtest_mpp.py +++ b/tests/test_wallet_regtest_mpp.py @@ -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*1000), "internal mpp not allowed" + wallet.melt_quote(mint_quote.request, 100*1000), "internal payments do not support mpp" ) From d7ebd5716700f606acb4e52102b86aed7e5248dc Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Fri, 7 Mar 2025 14:50:59 +0100 Subject: [PATCH 36/69] `quote_kind` is regular if no `kind` row is found in the DB --- cashu/core/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cashu/core/base.py b/cashu/core/base.py index 10597a59a..f294accd7 100644 --- a/cashu/core/base.py +++ b/cashu/core/base.py @@ -346,7 +346,7 @@ def from_row(cls, row: Row): change=change, expiry=expiry, payment_preimage=payment_preimage, - quote_kind=PaymentQuoteKind(row["kind"]), + quote_kind=PaymentQuoteKind(row.get("kind", "REGULAR")), ) @classmethod From 6e4ac03a31af80f50b80da69d58dd1378c50c823 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Fri, 7 Mar 2025 15:53:48 +0100 Subject: [PATCH 37/69] mypy --- cashu/core/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cashu/core/base.py b/cashu/core/base.py index f294accd7..99a1066f7 100644 --- a/cashu/core/base.py +++ b/cashu/core/base.py @@ -346,7 +346,7 @@ def from_row(cls, row: Row): change=change, expiry=expiry, payment_preimage=payment_preimage, - quote_kind=PaymentQuoteKind(row.get("kind", "REGULAR")), + quote_kind=PaymentQuoteKind(row.get("kind", "REGULAR")), # type: ignore ) @classmethod From 478e4059ce3983bf75c10a7786361517f5747e58 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Mon, 10 Mar 2025 12:49:47 +0100 Subject: [PATCH 38/69] skip test if deprecated API --- tests/test_wallet_amountless.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/test_wallet_amountless.py b/tests/test_wallet_amountless.py index 24622343d..162872471 100644 --- a/tests/test_wallet_amountless.py +++ b/tests/test_wallet_amountless.py @@ -29,6 +29,10 @@ async def wallet(): not (is_cln or is_lnd or is_fake), reason="only run this test on fake, lnd or cln" ) +@pytest.mark.skipif( + settings.debug_mint_only_deprecated, + reason="settings.debug_mint_only_deprecated is set", +) async def test_amountless_bolt11_invoice(wallet: Wallet): # make sure wallet knows the backend supports mpp assert wallet.mint_info.supports_amountless("bolt11", wallet.unit) @@ -56,6 +60,10 @@ async def test_amountless_bolt11_invoice(wallet: Wallet): is_cln or is_lnd or is_fake, reason="only run for backends where amountless is not supported" ) +@pytest.mark.skipif( + settings.debug_mint_only_deprecated, + reason="settings.debug_mint_only_deprecated is set", +) async def test_unsupported_amountless_bolt11_invoice(wallet: Wallet): amountless_invoice = invoice_no_amount if is_fake else get_real_invoice(0)['payment_request'] assert_err(wallet.melt_quote(amountless_invoice, 100*1000), "Mint does not support amountless invoices, cannot pay this invoice.") From 5af36242c098b2e7addc72ac25bd730511c06ecb Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Mon, 10 Mar 2025 12:50:31 +0100 Subject: [PATCH 39/69] fix --- tests/test_wallet_amountless.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_wallet_amountless.py b/tests/test_wallet_amountless.py index 162872471..eff806390 100644 --- a/tests/test_wallet_amountless.py +++ b/tests/test_wallet_amountless.py @@ -2,7 +2,7 @@ import pytest_asyncio from cashu.wallet.wallet import Wallet -from tests.conftest import SERVER_ENDPOINT +from tests.conftest import SERVER_ENDPOINT, settings from tests.helpers import ( assert_err, get_real_invoice, From bdcafbf254c626d2e86a3bd9ece5c472cc7b0411 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Mon, 10 Mar 2025 13:39:59 +0100 Subject: [PATCH 40/69] test cheeky behaviour --- tests/test_wallet_amountless.py | 41 ++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/tests/test_wallet_amountless.py b/tests/test_wallet_amountless.py index eff806390..600fd15b6 100644 --- a/tests/test_wallet_amountless.py +++ b/tests/test_wallet_amountless.py @@ -1,6 +1,6 @@ import pytest import pytest_asyncio - +import httpx from cashu.wallet.wallet import Wallet from tests.conftest import SERVER_ENDPOINT, settings from tests.helpers import ( @@ -13,6 +13,7 @@ ) invoice_no_amount = "lnbcrt1pnusdsqpp5fcxhgur2eewvsfy52q8xwanrjdglnf7htacp0ldeeakz6j62rj8sdqqcqzzsxqyz5vqsp5qk6l5dwhldy3gqjnr4806mtg22e25ekud4vdlf3p0hk89ud93lxs9qxpqysgq72fmgd460q04mvr5jetw7wys0vnt6ydl58gcg4jdy5jwx5d7epx8tr04et7a5yskwg4le54wrn6u6k0jjfehkc8n5spxkwxum239zxcqpuzakn" +normal_invoice = "lnbcrt10u1pnuakkapp5sgc2whvdcsl53cpmyvpvslrlgc3h9al42xpayw86ykl8nhp2j69sdqqcqzzsxqyz5vqsp52w4vs63hx264tqu3pq2dtkwg6c8eummmjsel8r46adp3ascthgvs9qxpqysgqdjjexqh6acf77gpvkf3usjs0t30w0ru8e2v6pv42j7tcdy5tjxtrkqak8wp6mnrslnrkxqfv4pxjapylnn37m367zsqx4uvzsa79dkqpzdg2ex" @pytest_asyncio.fixture(scope="function") async def wallet(): @@ -54,6 +55,44 @@ async def test_amountless_bolt11_invoice(wallet: Wallet): result = await wallet.melt(proofs, amountless_invoice, melt_quote.fee_reserve, melt_quote.quote) assert result.state == "PAID" +@pytest.mark.asyncio +@pytest.mark.skipif( + not (is_cln or is_lnd or is_fake), + reason="only run this test on fake, lnd or cln" +) +@pytest.mark.skipif( + settings.debug_mint_only_deprecated, + reason="settings.debug_mint_only_deprecated is set", +) +async def test_cheating_attempt_amountless_bolt11_invoice(wallet: Wallet): + # make sure wallet knows the backend supports mpp + assert wallet.mint_info.supports_amountless("bolt11", wallet.unit) + + # top up wallet + topup_mint_quote = await wallet.request_mint(128) + + await pay_if_regtest(topup_mint_quote.request) + proofs = await wallet.mint(128, quote_id=topup_mint_quote.quote) + assert wallet.balance == 128 + + # We get an invoice for 1000 sats + invoice = normal_invoice if is_fake else get_real_invoice(1000)['payment_request'] + + # We try and get a quote for 1 sat. + # This should not succeed. + assert_err( + httpx.post( + SERVER_ENDPOINT+"/v1/melt/quote/bolt11", + json={ + "unit": "sat", + "request": invoice, + "options": { + "amountless": "1000", + }, + }, + ), + "Amount in request does not equal invoice" + ) @pytest.mark.asyncio @pytest.mark.skipif( From 86239ba3622ee36955b704c9bd24dbc6310d23b7 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Mon, 10 Mar 2025 13:40:32 +0100 Subject: [PATCH 41/69] format --- .gitignore | 3 +++ tests/test_wallet_amountless.py | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 21e0d7740..6bdf86833 100644 --- a/.gitignore +++ b/.gitignore @@ -137,3 +137,6 @@ tor.pid # MacOS .DS_Store + +# angry-duck vscode extension +.angry-duck/ \ No newline at end of file diff --git a/tests/test_wallet_amountless.py b/tests/test_wallet_amountless.py index 600fd15b6..cb23180da 100644 --- a/tests/test_wallet_amountless.py +++ b/tests/test_wallet_amountless.py @@ -1,6 +1,7 @@ +import httpx import pytest import pytest_asyncio -import httpx + from cashu.wallet.wallet import Wallet from tests.conftest import SERVER_ENDPOINT, settings from tests.helpers import ( From 61a5afd511c5c8b181ce4719e52b292612ab33ed Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Mon, 10 Mar 2025 13:42:22 +0100 Subject: [PATCH 42/69] fix test --- tests/test_wallet_amountless.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_wallet_amountless.py b/tests/test_wallet_amountless.py index cb23180da..888463b0a 100644 --- a/tests/test_wallet_amountless.py +++ b/tests/test_wallet_amountless.py @@ -82,7 +82,7 @@ async def test_cheating_attempt_amountless_bolt11_invoice(wallet: Wallet): # We try and get a quote for 1 sat. # This should not succeed. assert_err( - httpx.post( + lambda x : httpx.post( SERVER_ENDPOINT+"/v1/melt/quote/bolt11", json={ "unit": "sat", From 3e24b9f19c683fcffb63c5f48efc11e6c0715deb Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Mon, 10 Mar 2025 13:44:16 +0100 Subject: [PATCH 43/69] format --- tests/test_wallet_amountless.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/tests/test_wallet_amountless.py b/tests/test_wallet_amountless.py index 888463b0a..44ae33f82 100644 --- a/tests/test_wallet_amountless.py +++ b/tests/test_wallet_amountless.py @@ -69,13 +69,6 @@ async def test_cheating_attempt_amountless_bolt11_invoice(wallet: Wallet): # make sure wallet knows the backend supports mpp assert wallet.mint_info.supports_amountless("bolt11", wallet.unit) - # top up wallet - topup_mint_quote = await wallet.request_mint(128) - - await pay_if_regtest(topup_mint_quote.request) - proofs = await wallet.mint(128, quote_id=topup_mint_quote.quote) - assert wallet.balance == 128 - # We get an invoice for 1000 sats invoice = normal_invoice if is_fake else get_real_invoice(1000)['payment_request'] From c8ad2ab875acc164dd4773d09f8c327220fab1df Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Wed, 12 Mar 2025 18:23:36 +0100 Subject: [PATCH 44/69] fix .gitignore --- .gitignore | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 6bdf86833..14d878e6d 100644 --- a/.gitignore +++ b/.gitignore @@ -136,7 +136,4 @@ tor.pid /test_data # MacOS -.DS_Store - -# angry-duck vscode extension -.angry-duck/ \ No newline at end of file +.DS_Store \ No newline at end of file From a0995aa41372f60ac2723e37b397c436bc663ee4 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Fri, 2 May 2025 12:03:32 +0200 Subject: [PATCH 45/69] fix `get_payment_quote` behaviour in assigning the kind --- cashu/lightning/clnrest.py | 13 +++++++------ cashu/lightning/fake.py | 17 +++++++++++------ cashu/lightning/lnd_grpc/lnd_grpc.py | 13 +++++++------ cashu/lightning/lndrest.py | 13 +++++++------ cashu/wallet/cli/cli.py | 5 +---- tests/test_wallet_cli.py | 12 ++++++++++++ 6 files changed, 45 insertions(+), 28 deletions(-) diff --git a/cashu/lightning/clnrest.py b/cashu/lightning/clnrest.py index 4e7a315f9..5e33b6b9a 100644 --- a/cashu/lightning/clnrest.py +++ b/cashu/lightning/clnrest.py @@ -354,19 +354,20 @@ async def get_payment_quote( amount_msat = 0 if melt_quote.is_amountless: # Check that the user isn't doing something cheeky - if (invoice_obj.amount_msat - and melt_quote.options.amountless.amount_msat != invoice_obj.amount_msat # type: ignore - ): + if invoice_obj.amount_msat: raise IncorrectRequestAmountError() amount_msat = melt_quote.options.amountless.amount_msat # type: ignore kind = PaymentQuoteKind.AMOUNTLESS elif melt_quote.is_mpp: + # Check that the user isn't doing something cheeky + if not invoice_obj.amount_msat: + raise IncorrectRequestAmountError() amount_msat = melt_quote.options.mpp.amount # type: ignore kind = PaymentQuoteKind.PARTIAL - elif invoice_obj.amount_msat: - amount_msat = int(invoice_obj.amount_msat) else: - raise Exception("request has no amount and is not specified as amountless") + if not invoice_obj.amount_msat: + raise Exception("request has no amount and is not specified as amountless") + amount_msat = int(invoice_obj.amount_msat) fees_msat = fee_reserve(amount_msat) fees = Amount(unit=Unit.msat, amount=fees_msat) diff --git a/cashu/lightning/fake.py b/cashu/lightning/fake.py index 825a7fdb7..1ec7e1e62 100644 --- a/cashu/lightning/fake.py +++ b/cashu/lightning/fake.py @@ -5,6 +5,8 @@ from os import urandom from typing import AsyncGenerator, Dict, List, Optional +from loguru import logger + from bolt11 import ( Bolt11, Feature, @@ -221,23 +223,26 @@ async def get_payment_quote( # Payment quote is determined and saved in the response kind = PaymentQuoteKind.REGULAR + logger.debug(f"Received melt quote request with {melt_quote.is_amountless = } and {melt_quote.is_mpp = }") + # Detect and handle amountless/partial/normal request amount_msat = 0 if melt_quote.is_amountless: # Check that the user isn't doing something cheeky - if (invoice_obj.amount_msat - and melt_quote.options.amountless.amount_msat != invoice_obj.amount_msat # type: ignore - ): + if invoice_obj.amount_msat: raise IncorrectRequestAmountError() amount_msat = melt_quote.options.amountless.amount_msat # type: ignore kind = PaymentQuoteKind.AMOUNTLESS elif melt_quote.is_mpp: + # Check that the user isn't doing something cheeky + if not invoice_obj.amount_msat: + raise IncorrectRequestAmountError() amount_msat = melt_quote.options.mpp.amount # type: ignore kind = PaymentQuoteKind.PARTIAL - elif invoice_obj.amount_msat: - amount_msat = int(invoice_obj.amount_msat) else: - raise Exception("request has no amount and is not specified as amountless") + if not invoice_obj.amount_msat: + raise Exception("request has no amount and is not specified as amountless") + amount_msat = int(invoice_obj.amount_msat) if self.unit == Unit.sat: fees_msat = fee_reserve(amount_msat) diff --git a/cashu/lightning/lnd_grpc/lnd_grpc.py b/cashu/lightning/lnd_grpc/lnd_grpc.py index b443c4463..c81e1da32 100644 --- a/cashu/lightning/lnd_grpc/lnd_grpc.py +++ b/cashu/lightning/lnd_grpc/lnd_grpc.py @@ -406,19 +406,20 @@ async def get_payment_quote( amount_msat = 0 if melt_quote.is_amountless: # Check that the user isn't doing something cheeky - if (invoice_obj.amount_msat - and melt_quote.options.amountless.amount_msat != invoice_obj.amount_msat # type: ignore - ): + if invoice_obj.amount_msat: raise IncorrectRequestAmountError() amount_msat = melt_quote.options.amountless.amount_msat # type: ignore kind = PaymentQuoteKind.AMOUNTLESS elif melt_quote.is_mpp: + # Check that the user isn't doing something cheeky + if not invoice_obj.amount_msat: + raise IncorrectRequestAmountError() amount_msat = melt_quote.options.mpp.amount # type: ignore kind = PaymentQuoteKind.PARTIAL - elif invoice_obj.amount_msat: - amount_msat = int(invoice_obj.amount_msat) else: - raise Exception("request has no amount and is not specified as amountless") + if not invoice_obj.amount_msat: + raise Exception("request has no amount and is not specified as amountless") + amount_msat = int(invoice_obj.amount_msat) fees_msat = fee_reserve(amount_msat) fees = Amount(unit=Unit.msat, amount=fees_msat) diff --git a/cashu/lightning/lndrest.py b/cashu/lightning/lndrest.py index 5e4c738f7..18a5e575a 100644 --- a/cashu/lightning/lndrest.py +++ b/cashu/lightning/lndrest.py @@ -433,19 +433,20 @@ async def get_payment_quote( amount_msat = 0 if melt_quote.is_amountless: # Check that the user isn't doing something cheeky - if (invoice_obj.amount_msat - and melt_quote.options.amountless.amount_msat != invoice_obj.amount_msat # type: ignore - ): + if invoice_obj.amount_msat: raise IncorrectRequestAmountError() amount_msat = melt_quote.options.amountless.amount_msat # type: ignore kind = PaymentQuoteKind.AMOUNTLESS elif melt_quote.is_mpp: + # Check that the user isn't doing something cheeky + if not invoice_obj.amount_msat: + raise IncorrectRequestAmountError() amount_msat = melt_quote.options.mpp.amount # type: ignore kind = PaymentQuoteKind.PARTIAL - elif invoice_obj.amount_msat: - amount_msat = int(invoice_obj.amount_msat) else: - raise Exception("request has no amount and is not specified as amountless") + if not invoice_obj.amount_msat: + raise Exception("request has no amount and is not specified as amountless") + amount_msat = int(invoice_obj.amount_msat) fees_msat = fee_reserve(amount_msat) fees = Amount(unit=Unit.msat, amount=fees_msat) diff --git a/cashu/wallet/cli/cli.py b/cashu/wallet/cli/cli.py index a75ed6ca6..1bfc17594 100644 --- a/cashu/wallet/cli/cli.py +++ b/cashu/wallet/cli/cli.py @@ -263,10 +263,7 @@ async def pay( await wallet.load_mint() await print_balance(ctx) payment_hash = bolt11.decode(invoice).payment_hash - if amount: - # we assume `amount` to be in sats - amount_mpp_msat = amount * 1000 - quote = await wallet.melt_quote(invoice, amount_mpp_msat) + quote = await wallet.melt_quote(invoice, amount * 1000 if amount else None) logger.debug(f"Quote: {quote}") total_amount = quote.amount + quote.fee_reserve # estimate ecash fee for the coinselected proofs diff --git a/tests/test_wallet_cli.py b/tests/test_wallet_cli.py index 94cce0110..1cd1f43d9 100644 --- a/tests/test_wallet_cli.py +++ b/tests/test_wallet_cli.py @@ -11,6 +11,7 @@ from cashu.wallet.wallet import Wallet from tests.helpers import is_deprecated_api_only, is_fake, is_regtest, pay_if_regtest +invoice_no_amount = "lnbcrt1pnusdsqpp5fcxhgur2eewvsfy52q8xwanrjdglnf7htacp0ldeeakz6j62rj8sdqqcqzzsxqyz5vqsp5qk6l5dwhldy3gqjnr4806mtg22e25ekud4vdlf3p0hk89ud93lxs9qxpqysgq72fmgd460q04mvr5jetw7wys0vnt6ydl58gcg4jdy5jwx5d7epx8tr04et7a5yskwg4le54wrn6u6k0jjfehkc8n5spxkwxum239zxcqpuzakn" @pytest.fixture(autouse=True, scope="session") def cli_prefix(): @@ -565,3 +566,14 @@ def test_send_with_lock(mint, cli_prefix): assert "cashuB" in token_str, "output does not have a token" token = TokenV4.deserialize(token_str).to_tokenv3() assert pubkey in token.token[0].proofs[0].secret + +@pytest.mark.skipif( + not is_fake, + reason="Cannot really pay fake invoices" +) +def test_pay_amountless_invoice(mint, cli_prefix): + runner = CliRunner() + result = runner.invoke( + cli, + [*cli_prefix, "pay", invoice_no_amount, "1000"] + ) \ No newline at end of file From ff58f3f2ab1511e1ee95c5d95919002d831010cd Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Fri, 2 May 2025 15:37:18 +0200 Subject: [PATCH 46/69] remove debug --- cashu/lightning/fake.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/cashu/lightning/fake.py b/cashu/lightning/fake.py index 1ec7e1e62..9d5bf9da3 100644 --- a/cashu/lightning/fake.py +++ b/cashu/lightning/fake.py @@ -5,8 +5,6 @@ from os import urandom from typing import AsyncGenerator, Dict, List, Optional -from loguru import logger - from bolt11 import ( Bolt11, Feature, @@ -223,8 +221,6 @@ async def get_payment_quote( # Payment quote is determined and saved in the response kind = PaymentQuoteKind.REGULAR - logger.debug(f"Received melt quote request with {melt_quote.is_amountless = } and {melt_quote.is_mpp = }") - # Detect and handle amountless/partial/normal request amount_msat = 0 if melt_quote.is_amountless: From c4c49345f5b3ec32422f66cde6d895633ab7c67c Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Fri, 2 May 2025 18:01:25 +0200 Subject: [PATCH 47/69] modify tests wallet --- tests/test_wallet_amountless.py | 21 ++++----------------- tests/test_wallet_cli.py | 9 ++++++--- 2 files changed, 10 insertions(+), 20 deletions(-) diff --git a/tests/test_wallet_amountless.py b/tests/test_wallet_amountless.py index 44ae33f82..603410e68 100644 --- a/tests/test_wallet_amountless.py +++ b/tests/test_wallet_amountless.py @@ -65,28 +65,15 @@ async def test_amountless_bolt11_invoice(wallet: Wallet): settings.debug_mint_only_deprecated, reason="settings.debug_mint_only_deprecated is set", ) -async def test_cheating_attempt_amountless_bolt11_invoice(wallet: Wallet): +async def test_pay_amountless_invoice_without_specified_amount(wallet: Wallet): # make sure wallet knows the backend supports mpp assert wallet.mint_info.supports_amountless("bolt11", wallet.unit) - # We get an invoice for 1000 sats - invoice = normal_invoice if is_fake else get_real_invoice(1000)['payment_request'] + # We get an amountless invoice + invoice = invoice_no_amount if is_fake else get_real_invoice(0)['payment_request'] - # We try and get a quote for 1 sat. # This should not succeed. - assert_err( - lambda x : httpx.post( - SERVER_ENDPOINT+"/v1/melt/quote/bolt11", - json={ - "unit": "sat", - "request": invoice, - "options": { - "amountless": "1000", - }, - }, - ), - "Amount in request does not equal invoice" - ) + assert_err(wallet.melt_quote(invoice), "No amount found. Either the invoice has an amount or the amount must be specified.") @pytest.mark.asyncio @pytest.mark.skipif( diff --git a/tests/test_wallet_cli.py b/tests/test_wallet_cli.py index 1cd1f43d9..569df63e0 100644 --- a/tests/test_wallet_cli.py +++ b/tests/test_wallet_cli.py @@ -9,7 +9,7 @@ from cashu.core.settings import settings from cashu.wallet.cli.cli import cli from cashu.wallet.wallet import Wallet -from tests.helpers import is_deprecated_api_only, is_fake, is_regtest, pay_if_regtest +from tests.helpers import is_deprecated_api_only, is_fake, is_regtest, pay_if_regtest, get_real_invoice invoice_no_amount = "lnbcrt1pnusdsqpp5fcxhgur2eewvsfy52q8xwanrjdglnf7htacp0ldeeakz6j62rj8sdqqcqzzsxqyz5vqsp5qk6l5dwhldy3gqjnr4806mtg22e25ekud4vdlf3p0hk89ud93lxs9qxpqysgq72fmgd460q04mvr5jetw7wys0vnt6ydl58gcg4jdy5jwx5d7epx8tr04et7a5yskwg4le54wrn6u6k0jjfehkc8n5spxkwxum239zxcqpuzakn" @@ -575,5 +575,8 @@ def test_pay_amountless_invoice(mint, cli_prefix): runner = CliRunner() result = runner.invoke( cli, - [*cli_prefix, "pay", invoice_no_amount, "1000"] - ) \ No newline at end of file + [*cli_prefix, "pay", invoice_no_amount if is_fake else get_real_invoice(0)["payment_request"], "1000"] + ) + assert result.exception is None + print("test_pay_amountless_invoice ", result.output) + assert "Invoice paid" in result.output \ No newline at end of file From 5ed1d329df607be6f7afc9d39cbf2ae8c5557037 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Fri, 2 May 2025 18:02:03 +0200 Subject: [PATCH 48/69] format checks --- tests/test_wallet_amountless.py | 1 - tests/test_wallet_cli.py | 8 +++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/test_wallet_amountless.py b/tests/test_wallet_amountless.py index 603410e68..fcdcd83be 100644 --- a/tests/test_wallet_amountless.py +++ b/tests/test_wallet_amountless.py @@ -1,4 +1,3 @@ -import httpx import pytest import pytest_asyncio diff --git a/tests/test_wallet_cli.py b/tests/test_wallet_cli.py index 569df63e0..1eda4c461 100644 --- a/tests/test_wallet_cli.py +++ b/tests/test_wallet_cli.py @@ -9,7 +9,13 @@ from cashu.core.settings import settings from cashu.wallet.cli.cli import cli from cashu.wallet.wallet import Wallet -from tests.helpers import is_deprecated_api_only, is_fake, is_regtest, pay_if_regtest, get_real_invoice +from tests.helpers import ( + get_real_invoice, + is_deprecated_api_only, + is_fake, + is_regtest, + pay_if_regtest, +) invoice_no_amount = "lnbcrt1pnusdsqpp5fcxhgur2eewvsfy52q8xwanrjdglnf7htacp0ldeeakz6j62rj8sdqqcqzzsxqyz5vqsp5qk6l5dwhldy3gqjnr4806mtg22e25ekud4vdlf3p0hk89ud93lxs9qxpqysgq72fmgd460q04mvr5jetw7wys0vnt6ydl58gcg4jdy5jwx5d7epx8tr04et7a5yskwg4le54wrn6u6k0jjfehkc8n5spxkwxum239zxcqpuzakn" From b5d599334c0ef7f5bf0400165c5bcf5c4bf999a0 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Sat, 3 May 2025 11:18:54 +0200 Subject: [PATCH 49/69] update tests --- tests/conftest.py | 12 ++++++++ tests/test_mint_amountless.py | 51 +++++++++++++++++++++++++++++++++ tests/test_wallet_amountless.py | 2 -- 3 files changed, 63 insertions(+), 2 deletions(-) create mode 100644 tests/test_mint_amountless.py diff --git a/tests/conftest.py b/tests/conftest.py index a8f1e1691..4ce20266f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -18,6 +18,7 @@ from cashu.mint import migrations as migrations_mint from cashu.mint.crud import LedgerCrudSqlite from cashu.mint.ledger import Ledger +from cashu.wallet.wallet import Wallet SERVER_PORT = 3337 SERVER_ENDPOINT = f"http://localhost:{SERVER_PORT}" @@ -146,3 +147,14 @@ def mint(): yield server server.stop() + +@pytest_asyncio.fixture(scope="function") +async def wallet(): + wallet = await Wallet.with_db( + url=SERVER_ENDPOINT, + db="test_data/wallet", + name="wallet", + ) + await wallet.load_mint() + yield wallet + await wallet.db.engine.dispose() diff --git a/tests/test_mint_amountless.py b/tests/test_mint_amountless.py new file mode 100644 index 000000000..2b5961111 --- /dev/null +++ b/tests/test_mint_amountless.py @@ -0,0 +1,51 @@ +import pytest +import pytest_asyncio + +from cashu.core.base import Unit +from cashu.core.models import ( + PostMeltQuoteRequest, + PostMeltRequestOptions, + PostMeltRequestOptionAmountless, +) +from .conftest import wallet, ledger +from .helpers import is_fake, get_real_invoice, assert_err + +invoice_no_amount = "lnbcrt1pnusdsqpp5fcxhgur2eewvsfy52q8xwanrjdglnf7htacp0ldeeakz6j62rj8sdqqcqzzsxqyz5vqsp5qk6l5dwhldy3gqjnr4806mtg22e25ekud4vdlf3p0hk89ud93lxs9qxpqysgq72fmgd460q04mvr5jetw7wys0vnt6ydl58gcg4jdy5jwx5d7epx8tr04et7a5yskwg4le54wrn6u6k0jjfehkc8n5spxkwxum239zxcqpuzakn" +normal_invoice = "lnbcrt10u1pnuakkapp5sgc2whvdcsl53cpmyvpvslrlgc3h9al42xpayw86ykl8nhp2j69sdqqcqzzsxqyz5vqsp52w4vs63hx264tqu3pq2dtkwg6c8eummmjsel8r46adp3ascthgvs9qxpqysgqdjjexqh6acf77gpvkf3usjs0t30w0ru8e2v6pv42j7tcdy5tjxtrkqak8wp6mnrslnrkxqfv4pxjapylnn37m367zsqx4uvzsa79dkqpzdg2ex" + +@pytest.mark.asyncio +async def test_get_quote_for_amountless_invoice(wallet, ledger): + # Get an amountless invoice + invoice = invoice_no_amount if is_fake else get_real_invoice(0) + + request = PostMeltQuoteRequest( + unit='sat', + request=invoice, + options=PostMeltRequestOptions( + amountless=PostMeltRequestOptionAmountless( + amount_msat=1000, + ) + ), + ) + + response = await ledger.melt_quote(request) + assert response.unit == 'sat' + assert response.amount == 1 + +@pytest.mark.asyncio +async def test_get_amountless_quote_for_non_amountless_invoice(wallet, ledger): + # Get normal invoice + invoice = normal_invoice if is_fake else get_real_invoice(1000) + + request = PostMeltQuoteRequest( + unit='sat', + request=invoice, + options=PostMeltRequestOptions( + amountless=PostMeltRequestOptionAmountless( + amount_msat=1000, + ) + ), + ) + + assert_err(ledger.melt_quote(request), "Amount in request does not equal invoice") + \ No newline at end of file diff --git a/tests/test_wallet_amountless.py b/tests/test_wallet_amountless.py index fcdcd83be..e65fc1e3e 100644 --- a/tests/test_wallet_amountless.py +++ b/tests/test_wallet_amountless.py @@ -50,8 +50,6 @@ async def test_amountless_bolt11_invoice(wallet: Wallet): melt_quote = await wallet.melt_quote(amountless_invoice, 100*1000) assert melt_quote.amount == 100 - await pay_if_regtest(amountless_invoice) - result = await wallet.melt(proofs, amountless_invoice, melt_quote.fee_reserve, melt_quote.quote) assert result.state == "PAID" From 594aaec71381e90c9e84404adcb1d8f74035b484 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Sat, 3 May 2025 11:28:53 +0200 Subject: [PATCH 50/69] fix tests --- tests/test_mint_amountless.py | 4 ++-- tests/test_wallet_cli.py | 4 ---- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/tests/test_mint_amountless.py b/tests/test_mint_amountless.py index 2b5961111..ed964c1da 100644 --- a/tests/test_mint_amountless.py +++ b/tests/test_mint_amountless.py @@ -16,7 +16,7 @@ @pytest.mark.asyncio async def test_get_quote_for_amountless_invoice(wallet, ledger): # Get an amountless invoice - invoice = invoice_no_amount if is_fake else get_real_invoice(0) + invoice = invoice_no_amount if is_fake else get_real_invoice(0)['payment_request'] request = PostMeltQuoteRequest( unit='sat', @@ -35,7 +35,7 @@ async def test_get_quote_for_amountless_invoice(wallet, ledger): @pytest.mark.asyncio async def test_get_amountless_quote_for_non_amountless_invoice(wallet, ledger): # Get normal invoice - invoice = normal_invoice if is_fake else get_real_invoice(1000) + invoice = normal_invoice if is_fake else get_real_invoice(1000)['payment_request'] request = PostMeltQuoteRequest( unit='sat', diff --git a/tests/test_wallet_cli.py b/tests/test_wallet_cli.py index 1eda4c461..2891c5263 100644 --- a/tests/test_wallet_cli.py +++ b/tests/test_wallet_cli.py @@ -573,10 +573,6 @@ def test_send_with_lock(mint, cli_prefix): token = TokenV4.deserialize(token_str).to_tokenv3() assert pubkey in token.token[0].proofs[0].secret -@pytest.mark.skipif( - not is_fake, - reason="Cannot really pay fake invoices" -) def test_pay_amountless_invoice(mint, cli_prefix): runner = CliRunner() result = runner.invoke( From f8157ca6de1a256546425d5aa147c2680b0544bb Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Sat, 3 May 2025 11:30:23 +0200 Subject: [PATCH 51/69] modify skip reason for `test_pay_amountless_invoice_without_specified_amount` --- tests/test_wallet_amountless.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_wallet_amountless.py b/tests/test_wallet_amountless.py index e65fc1e3e..d83964259 100644 --- a/tests/test_wallet_amountless.py +++ b/tests/test_wallet_amountless.py @@ -56,7 +56,7 @@ async def test_amountless_bolt11_invoice(wallet: Wallet): @pytest.mark.asyncio @pytest.mark.skipif( not (is_cln or is_lnd or is_fake), - reason="only run this test on fake, lnd or cln" + reason="only run for backends where amountless is supported" ) @pytest.mark.skipif( settings.debug_mint_only_deprecated, From 2b31f1ea5a8a6cd8d6bc896e8b1792338a590dc0 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Sat, 3 May 2025 11:33:14 +0200 Subject: [PATCH 52/69] format checks --- tests/test_mint_amountless.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/tests/test_mint_amountless.py b/tests/test_mint_amountless.py index ed964c1da..affab1096 100644 --- a/tests/test_mint_amountless.py +++ b/tests/test_mint_amountless.py @@ -1,14 +1,12 @@ import pytest -import pytest_asyncio -from cashu.core.base import Unit from cashu.core.models import ( PostMeltQuoteRequest, - PostMeltRequestOptions, PostMeltRequestOptionAmountless, + PostMeltRequestOptions, ) -from .conftest import wallet, ledger -from .helpers import is_fake, get_real_invoice, assert_err + +from .helpers import assert_err, get_real_invoice, is_fake invoice_no_amount = "lnbcrt1pnusdsqpp5fcxhgur2eewvsfy52q8xwanrjdglnf7htacp0ldeeakz6j62rj8sdqqcqzzsxqyz5vqsp5qk6l5dwhldy3gqjnr4806mtg22e25ekud4vdlf3p0hk89ud93lxs9qxpqysgq72fmgd460q04mvr5jetw7wys0vnt6ydl58gcg4jdy5jwx5d7epx8tr04et7a5yskwg4le54wrn6u6k0jjfehkc8n5spxkwxum239zxcqpuzakn" normal_invoice = "lnbcrt10u1pnuakkapp5sgc2whvdcsl53cpmyvpvslrlgc3h9al42xpayw86ykl8nhp2j69sdqqcqzzsxqyz5vqsp52w4vs63hx264tqu3pq2dtkwg6c8eummmjsel8r46adp3ascthgvs9qxpqysgqdjjexqh6acf77gpvkf3usjs0t30w0ru8e2v6pv42j7tcdy5tjxtrkqak8wp6mnrslnrkxqfv4pxjapylnn37m367zsqx4uvzsa79dkqpzdg2ex" From 99183bacee2f3123fcf91ba3a28c3ac3477f86ac Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Sat, 3 May 2025 11:58:36 +0200 Subject: [PATCH 53/69] skip if deprecated API --- tests/test_wallet_cli.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/test_wallet_cli.py b/tests/test_wallet_cli.py index 2891c5263..b6e8a5731 100644 --- a/tests/test_wallet_cli.py +++ b/tests/test_wallet_cli.py @@ -573,11 +573,15 @@ def test_send_with_lock(mint, cli_prefix): token = TokenV4.deserialize(token_str).to_tokenv3() assert pubkey in token.token[0].proofs[0].secret +pytest.mark.skipif( + settings.debug_mint_only_deprecated, + reason="settings.debug_mint_only_deprecated is set", +) def test_pay_amountless_invoice(mint, cli_prefix): runner = CliRunner() result = runner.invoke( cli, - [*cli_prefix, "pay", invoice_no_amount if is_fake else get_real_invoice(0)["payment_request"], "1000"] + [*cli_prefix, "pay", invoice_no_amount if is_fake else get_real_invoice(0)["payment_request"], "10"] ) assert result.exception is None print("test_pay_amountless_invoice ", result.output) From 06e7e2110ca906daeff39e3141c3bdefa243e047 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Sat, 3 May 2025 12:31:00 +0200 Subject: [PATCH 54/69] hopefully fix the tests? --- tests/test_mint_amountless.py | 8 ++++++++ tests/test_wallet_cli.py | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/tests/test_mint_amountless.py b/tests/test_mint_amountless.py index affab1096..7109ab0bf 100644 --- a/tests/test_mint_amountless.py +++ b/tests/test_mint_amountless.py @@ -12,6 +12,10 @@ normal_invoice = "lnbcrt10u1pnuakkapp5sgc2whvdcsl53cpmyvpvslrlgc3h9al42xpayw86ykl8nhp2j69sdqqcqzzsxqyz5vqsp52w4vs63hx264tqu3pq2dtkwg6c8eummmjsel8r46adp3ascthgvs9qxpqysgqdjjexqh6acf77gpvkf3usjs0t30w0ru8e2v6pv42j7tcdy5tjxtrkqak8wp6mnrslnrkxqfv4pxjapylnn37m367zsqx4uvzsa79dkqpzdg2ex" @pytest.mark.asyncio +@pytest.mark.skipif( + not (is_cln or is_lnd or is_fake), + reason="Only run when amountless is supported", +) async def test_get_quote_for_amountless_invoice(wallet, ledger): # Get an amountless invoice invoice = invoice_no_amount if is_fake else get_real_invoice(0)['payment_request'] @@ -31,6 +35,10 @@ async def test_get_quote_for_amountless_invoice(wallet, ledger): assert response.amount == 1 @pytest.mark.asyncio +@pytest.mark.skipif( + not (is_cln or is_lnd or is_fake), + reason="Only run when amountless is supported", +) async def test_get_amountless_quote_for_non_amountless_invoice(wallet, ledger): # Get normal invoice invoice = normal_invoice if is_fake else get_real_invoice(1000)['payment_request'] diff --git a/tests/test_wallet_cli.py b/tests/test_wallet_cli.py index b6e8a5731..467fda45c 100644 --- a/tests/test_wallet_cli.py +++ b/tests/test_wallet_cli.py @@ -577,6 +577,10 @@ def test_send_with_lock(mint, cli_prefix): settings.debug_mint_only_deprecated, reason="settings.debug_mint_only_deprecated is set", ) +pytest.mark.skipif( + not (is_fake or is_cln or is_lnd), + reason="Only run where amountless is supported", +) def test_pay_amountless_invoice(mint, cli_prefix): runner = CliRunner() result = runner.invoke( From 4c3ac71b90ce0afd3894fbfaa9b1d59641a69d86 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Sat, 3 May 2025 15:32:47 +0200 Subject: [PATCH 55/69] import `is_cln` --- tests/test_wallet_cli.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_wallet_cli.py b/tests/test_wallet_cli.py index 467fda45c..81d298580 100644 --- a/tests/test_wallet_cli.py +++ b/tests/test_wallet_cli.py @@ -14,6 +14,7 @@ is_deprecated_api_only, is_fake, is_regtest, + is_cln, pay_if_regtest, ) From 0985f3493c985cd71b8a9065e42d27a7c285c6ec Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Sat, 3 May 2025 15:36:29 +0200 Subject: [PATCH 56/69] import is_cln --- tests/test_mint_amountless.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_mint_amountless.py b/tests/test_mint_amountless.py index 7109ab0bf..c5ee910cd 100644 --- a/tests/test_mint_amountless.py +++ b/tests/test_mint_amountless.py @@ -6,7 +6,7 @@ PostMeltRequestOptions, ) -from .helpers import assert_err, get_real_invoice, is_fake +from .helpers import assert_err, get_real_invoice, is_fake, is_cln invoice_no_amount = "lnbcrt1pnusdsqpp5fcxhgur2eewvsfy52q8xwanrjdglnf7htacp0ldeeakz6j62rj8sdqqcqzzsxqyz5vqsp5qk6l5dwhldy3gqjnr4806mtg22e25ekud4vdlf3p0hk89ud93lxs9qxpqysgq72fmgd460q04mvr5jetw7wys0vnt6ydl58gcg4jdy5jwx5d7epx8tr04et7a5yskwg4le54wrn6u6k0jjfehkc8n5spxkwxum239zxcqpuzakn" normal_invoice = "lnbcrt10u1pnuakkapp5sgc2whvdcsl53cpmyvpvslrlgc3h9al42xpayw86ykl8nhp2j69sdqqcqzzsxqyz5vqsp52w4vs63hx264tqu3pq2dtkwg6c8eummmjsel8r46adp3ascthgvs9qxpqysgqdjjexqh6acf77gpvkf3usjs0t30w0ru8e2v6pv42j7tcdy5tjxtrkqak8wp6mnrslnrkxqfv4pxjapylnn37m367zsqx4uvzsa79dkqpzdg2ex" From 11a7b840c6825c5444c529204d6f9b6b1bd76948 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Sat, 3 May 2025 15:39:27 +0200 Subject: [PATCH 57/69] import `is_lnd` --- tests/test_mint_amountless.py | 2 +- tests/test_wallet_cli.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_mint_amountless.py b/tests/test_mint_amountless.py index c5ee910cd..8930794ec 100644 --- a/tests/test_mint_amountless.py +++ b/tests/test_mint_amountless.py @@ -6,7 +6,7 @@ PostMeltRequestOptions, ) -from .helpers import assert_err, get_real_invoice, is_fake, is_cln +from .helpers import assert_err, get_real_invoice, is_cln, is_fake, is_lnd invoice_no_amount = "lnbcrt1pnusdsqpp5fcxhgur2eewvsfy52q8xwanrjdglnf7htacp0ldeeakz6j62rj8sdqqcqzzsxqyz5vqsp5qk6l5dwhldy3gqjnr4806mtg22e25ekud4vdlf3p0hk89ud93lxs9qxpqysgq72fmgd460q04mvr5jetw7wys0vnt6ydl58gcg4jdy5jwx5d7epx8tr04et7a5yskwg4le54wrn6u6k0jjfehkc8n5spxkwxum239zxcqpuzakn" normal_invoice = "lnbcrt10u1pnuakkapp5sgc2whvdcsl53cpmyvpvslrlgc3h9al42xpayw86ykl8nhp2j69sdqqcqzzsxqyz5vqsp52w4vs63hx264tqu3pq2dtkwg6c8eummmjsel8r46adp3ascthgvs9qxpqysgqdjjexqh6acf77gpvkf3usjs0t30w0ru8e2v6pv42j7tcdy5tjxtrkqak8wp6mnrslnrkxqfv4pxjapylnn37m367zsqx4uvzsa79dkqpzdg2ex" diff --git a/tests/test_wallet_cli.py b/tests/test_wallet_cli.py index 81d298580..d320ee794 100644 --- a/tests/test_wallet_cli.py +++ b/tests/test_wallet_cli.py @@ -11,10 +11,11 @@ from cashu.wallet.wallet import Wallet from tests.helpers import ( get_real_invoice, + is_cln, is_deprecated_api_only, is_fake, + is_lnd, is_regtest, - is_cln, pay_if_regtest, ) From 818a458fe95790e780f8f739fc71e74854ef69f4 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Sat, 3 May 2025 16:05:37 +0200 Subject: [PATCH 58/69] try --- cashu/wallet/wallet.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index 560eef594..f5403346d 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -745,6 +745,7 @@ async def melt_quote( """ if amount_msat: invoice_obj = bolt11.decode(invoice) + logger.debug(self.mint_info.supports_amountless("bolt11", self.unit)) if not invoice_obj.amount_msat and not self.mint_info.supports_amountless("bolt11", self.unit): raise Exception("Mint does not support amountless invoices, cannot pay this invoice.") if invoice_obj.amount_msat and not self.mint_info.supports_mpp("bolt11", self.unit): From a098828e36170d7199dd53e55593b3bec855bbc1 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Tue, 3 Jun 2025 13:00:47 +0200 Subject: [PATCH 59/69] format and mypy fixes --- cashu/lightning/fake.py | 1 + tests/wallet/test_wallet_cli.py | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/cashu/lightning/fake.py b/cashu/lightning/fake.py index f0c985135..6e4ad76d6 100644 --- a/cashu/lightning/fake.py +++ b/cashu/lightning/fake.py @@ -278,6 +278,7 @@ async def get_payment_quote( amount_msat = int(invoice_obj.amount_msat) if self.unit == Unit.sat or self.unit == Unit.msat: + assert invoice_obj.amount_msat amount_msat = int(invoice_obj.amount_msat) fees_msat = fee_reserve(amount_msat) fees = Amount(unit=Unit.msat, amount=fees_msat) diff --git a/tests/wallet/test_wallet_cli.py b/tests/wallet/test_wallet_cli.py index c1254243d..20755e158 100644 --- a/tests/wallet/test_wallet_cli.py +++ b/tests/wallet/test_wallet_cli.py @@ -15,8 +15,6 @@ is_deprecated_api_only, is_fake, is_lnd, - is_deprecated_api_only, - is_fake, is_regtest, pay_if_regtest, ) From 5e4aac82b9365e7edff2052e927a2d809579bcde Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Tue, 3 Jun 2025 14:51:48 +0200 Subject: [PATCH 60/69] fixes --- cashu/lightning/fake.py | 3 +-- tests/test_mint_amountless.py | 1 + tests/test_wallet_amountless.py | 2 +- tests/wallet/test_wallet_cli.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cashu/lightning/fake.py b/cashu/lightning/fake.py index 6e4ad76d6..744fccddb 100644 --- a/cashu/lightning/fake.py +++ b/cashu/lightning/fake.py @@ -199,6 +199,7 @@ async def pay_invoice(self, quote: MeltQuote, fee_limit: int) -> PaymentResponse await asyncio.sleep(settings.fakewallet_delay_outgoing_payment) if settings.fakewallet_pay_invoice_state: + print(settings.fakewallet_pay_invoice_state) if settings.fakewallet_pay_invoice_state == "SETTLED": self.update_balance(invoice, incoming=False) return PaymentResponse( @@ -278,8 +279,6 @@ async def get_payment_quote( amount_msat = int(invoice_obj.amount_msat) if self.unit == Unit.sat or self.unit == Unit.msat: - assert invoice_obj.amount_msat - amount_msat = int(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/tests/test_mint_amountless.py b/tests/test_mint_amountless.py index 8930794ec..20c8e2baa 100644 --- a/tests/test_mint_amountless.py +++ b/tests/test_mint_amountless.py @@ -31,6 +31,7 @@ async def test_get_quote_for_amountless_invoice(wallet, ledger): ) response = await ledger.melt_quote(request) + print(f"amountless quote: {response = }") assert response.unit == 'sat' assert response.amount == 1 diff --git a/tests/test_wallet_amountless.py b/tests/test_wallet_amountless.py index d83964259..60c713574 100644 --- a/tests/test_wallet_amountless.py +++ b/tests/test_wallet_amountless.py @@ -51,7 +51,7 @@ async def test_amountless_bolt11_invoice(wallet: Wallet): assert melt_quote.amount == 100 result = await wallet.melt(proofs, amountless_invoice, melt_quote.fee_reserve, melt_quote.quote) - assert result.state == "PAID" + assert result.state == "PAID" or result.state == "PENDING" @pytest.mark.asyncio @pytest.mark.skipif( diff --git a/tests/wallet/test_wallet_cli.py b/tests/wallet/test_wallet_cli.py index 20755e158..4b69c6d8a 100644 --- a/tests/wallet/test_wallet_cli.py +++ b/tests/wallet/test_wallet_cli.py @@ -605,4 +605,4 @@ def test_pay_amountless_invoice(mint, cli_prefix): ) assert result.exception is None print("test_pay_amountless_invoice ", result.output) - assert "Invoice paid" in result.output \ No newline at end of file + assert "Invoice paid" in result.output or "Invoice pending" in result.output \ No newline at end of file From 7aaaef159810d76671adaae19608252c92ee04d1 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Wed, 4 Jun 2025 09:19:52 +0200 Subject: [PATCH 61/69] await assert_err --- tests/{ => mint}/test_mint_amountless.py | 2 +- tests/{ => wallet}/test_wallet_amountless.py | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename tests/{ => mint}/test_mint_amountless.py (95%) rename tests/{ => wallet}/test_wallet_amountless.py (100%) diff --git a/tests/test_mint_amountless.py b/tests/mint/test_mint_amountless.py similarity index 95% rename from tests/test_mint_amountless.py rename to tests/mint/test_mint_amountless.py index 20c8e2baa..ccdc094e7 100644 --- a/tests/test_mint_amountless.py +++ b/tests/mint/test_mint_amountless.py @@ -54,5 +54,5 @@ async def test_get_amountless_quote_for_non_amountless_invoice(wallet, ledger): ), ) - assert_err(ledger.melt_quote(request), "Amount in request does not equal invoice") + await assert_err(ledger.melt_quote(request), "Amount in request does not equal invoice") \ No newline at end of file diff --git a/tests/test_wallet_amountless.py b/tests/wallet/test_wallet_amountless.py similarity index 100% rename from tests/test_wallet_amountless.py rename to tests/wallet/test_wallet_amountless.py From 0bb5e09ceec22c7e7c0f971d50ee96abc14c004e Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Wed, 4 Jun 2025 09:37:00 +0200 Subject: [PATCH 62/69] fix import --- tests/mint/test_mint_amountless.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/mint/test_mint_amountless.py b/tests/mint/test_mint_amountless.py index ccdc094e7..a4e62ca97 100644 --- a/tests/mint/test_mint_amountless.py +++ b/tests/mint/test_mint_amountless.py @@ -6,7 +6,7 @@ PostMeltRequestOptions, ) -from .helpers import assert_err, get_real_invoice, is_cln, is_fake, is_lnd +from tests.helpers import assert_err, get_real_invoice, is_cln, is_fake, is_lnd invoice_no_amount = "lnbcrt1pnusdsqpp5fcxhgur2eewvsfy52q8xwanrjdglnf7htacp0ldeeakz6j62rj8sdqqcqzzsxqyz5vqsp5qk6l5dwhldy3gqjnr4806mtg22e25ekud4vdlf3p0hk89ud93lxs9qxpqysgq72fmgd460q04mvr5jetw7wys0vnt6ydl58gcg4jdy5jwx5d7epx8tr04et7a5yskwg4le54wrn6u6k0jjfehkc8n5spxkwxum239zxcqpuzakn" normal_invoice = "lnbcrt10u1pnuakkapp5sgc2whvdcsl53cpmyvpvslrlgc3h9al42xpayw86ykl8nhp2j69sdqqcqzzsxqyz5vqsp52w4vs63hx264tqu3pq2dtkwg6c8eummmjsel8r46adp3ascthgvs9qxpqysgqdjjexqh6acf77gpvkf3usjs0t30w0ru8e2v6pv42j7tcdy5tjxtrkqak8wp6mnrslnrkxqfv4pxjapylnn37m367zsqx4uvzsa79dkqpzdg2ex" From 46cd7cf00dbc2e41b7389e7d0c1b55d1a6491913 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Wed, 4 Jun 2025 09:39:46 +0200 Subject: [PATCH 63/69] format --- tests/mint/test_mint_amountless.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/mint/test_mint_amountless.py b/tests/mint/test_mint_amountless.py index a4e62ca97..59e77c715 100644 --- a/tests/mint/test_mint_amountless.py +++ b/tests/mint/test_mint_amountless.py @@ -5,7 +5,6 @@ PostMeltRequestOptionAmountless, PostMeltRequestOptions, ) - from tests.helpers import assert_err, get_real_invoice, is_cln, is_fake, is_lnd invoice_no_amount = "lnbcrt1pnusdsqpp5fcxhgur2eewvsfy52q8xwanrjdglnf7htacp0ldeeakz6j62rj8sdqqcqzzsxqyz5vqsp5qk6l5dwhldy3gqjnr4806mtg22e25ekud4vdlf3p0hk89ud93lxs9qxpqysgq72fmgd460q04mvr5jetw7wys0vnt6ydl58gcg4jdy5jwx5d7epx8tr04et7a5yskwg4le54wrn6u6k0jjfehkc8n5spxkwxum239zxcqpuzakn" From 69e84375bc41eafc61f2e8d8993c05637e4165e2 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Wed, 4 Jun 2025 10:41:47 +0200 Subject: [PATCH 64/69] try joint skip_if decorator --- tests/wallet/test_wallet_amountless.py | 6 +----- tests/wallet/test_wallet_cli.py | 6 +----- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/tests/wallet/test_wallet_amountless.py b/tests/wallet/test_wallet_amountless.py index 60c713574..fe46615bb 100644 --- a/tests/wallet/test_wallet_amountless.py +++ b/tests/wallet/test_wallet_amountless.py @@ -55,13 +55,9 @@ async def test_amountless_bolt11_invoice(wallet: Wallet): @pytest.mark.asyncio @pytest.mark.skipif( - not (is_cln or is_lnd or is_fake), + not (is_cln or is_lnd or is_fake) or settings.debug_mint_only_deprecated, reason="only run for backends where amountless is supported" ) -@pytest.mark.skipif( - settings.debug_mint_only_deprecated, - reason="settings.debug_mint_only_deprecated is set", -) async def test_pay_amountless_invoice_without_specified_amount(wallet: Wallet): # make sure wallet knows the backend supports mpp assert wallet.mint_info.supports_amountless("bolt11", wallet.unit) diff --git a/tests/wallet/test_wallet_cli.py b/tests/wallet/test_wallet_cli.py index 4b69c6d8a..65ffa814b 100644 --- a/tests/wallet/test_wallet_cli.py +++ b/tests/wallet/test_wallet_cli.py @@ -590,11 +590,7 @@ def test_send_with_lock(mint, cli_prefix): assert pubkey in token.token[0].proofs[0].secret pytest.mark.skipif( - settings.debug_mint_only_deprecated, - reason="settings.debug_mint_only_deprecated is set", -) -pytest.mark.skipif( - not (is_fake or is_cln or is_lnd), + not (is_fake or is_cln or is_lnd) or settings.debug_mint_only_deprecated, reason="Only run where amountless is supported", ) def test_pay_amountless_invoice(mint, cli_prefix): From 0dac8f40d7e349f66ce7b1c4ae593ecaaebdb4fd Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Wed, 4 Jun 2025 11:16:53 +0200 Subject: [PATCH 65/69] modify test_pay_amountless_invoice to expect "amountless rejection" exception as well. --- tests/wallet/test_wallet_cli.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/tests/wallet/test_wallet_cli.py b/tests/wallet/test_wallet_cli.py index 65ffa814b..118b6c175 100644 --- a/tests/wallet/test_wallet_cli.py +++ b/tests/wallet/test_wallet_cli.py @@ -589,16 +589,12 @@ def test_send_with_lock(mint, cli_prefix): token = TokenV4.deserialize(token_str).to_tokenv3() assert pubkey in token.token[0].proofs[0].secret -pytest.mark.skipif( - not (is_fake or is_cln or is_lnd) or settings.debug_mint_only_deprecated, - reason="Only run where amountless is supported", -) def test_pay_amountless_invoice(mint, cli_prefix): runner = CliRunner() result = runner.invoke( cli, [*cli_prefix, "pay", invoice_no_amount if is_fake else get_real_invoice(0)["payment_request"], "10"] ) - assert result.exception is None + assert result.exception is None or result.exception == "Mint does not support amountless invoices, cannot pay this invoice." print("test_pay_amountless_invoice ", result.output) assert "Invoice paid" in result.output or "Invoice pending" in result.output \ No newline at end of file From af9cb2b9315c2a6069841fbcb4555313a4202414 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Wed, 4 Jun 2025 11:17:49 +0200 Subject: [PATCH 66/69] format --- tests/wallet/test_wallet_cli.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/wallet/test_wallet_cli.py b/tests/wallet/test_wallet_cli.py index 118b6c175..8ca9ed5bd 100644 --- a/tests/wallet/test_wallet_cli.py +++ b/tests/wallet/test_wallet_cli.py @@ -11,10 +11,8 @@ from cashu.wallet.wallet import Wallet from tests.helpers import ( get_real_invoice, - is_cln, is_deprecated_api_only, is_fake, - is_lnd, is_regtest, pay_if_regtest, ) From 92c3a503734a35e7732790010075a0102a01cc3b Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Wed, 4 Jun 2025 11:26:32 +0200 Subject: [PATCH 67/69] exception --- tests/wallet/test_wallet_cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/wallet/test_wallet_cli.py b/tests/wallet/test_wallet_cli.py index 8ca9ed5bd..1baa088f4 100644 --- a/tests/wallet/test_wallet_cli.py +++ b/tests/wallet/test_wallet_cli.py @@ -593,6 +593,6 @@ def test_pay_amountless_invoice(mint, cli_prefix): cli, [*cli_prefix, "pay", invoice_no_amount if is_fake else get_real_invoice(0)["payment_request"], "10"] ) - assert result.exception is None or result.exception == "Mint does not support amountless invoices, cannot pay this invoice." + assert result.exception is None or result.exception == Exception("Mint does not support amountless invoices, cannot pay this invoice.") print("test_pay_amountless_invoice ", result.output) assert "Invoice paid" in result.output or "Invoice pending" in result.output \ No newline at end of file From 9df7bd06f24d81dde670bf077945305bb0cd4814 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Wed, 4 Jun 2025 12:07:01 +0200 Subject: [PATCH 68/69] try string conversion --- tests/wallet/test_wallet_cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/wallet/test_wallet_cli.py b/tests/wallet/test_wallet_cli.py index 1baa088f4..8fb66f134 100644 --- a/tests/wallet/test_wallet_cli.py +++ b/tests/wallet/test_wallet_cli.py @@ -593,6 +593,6 @@ def test_pay_amountless_invoice(mint, cli_prefix): cli, [*cli_prefix, "pay", invoice_no_amount if is_fake else get_real_invoice(0)["payment_request"], "10"] ) - assert result.exception is None or result.exception == Exception("Mint does not support amountless invoices, cannot pay this invoice.") + assert result.exception is None or str(result.exception) == "Mint does not support amountless invoices, cannot pay this invoice." print("test_pay_amountless_invoice ", result.output) assert "Invoice paid" in result.output or "Invoice pending" in result.output \ No newline at end of file From 4790876b0c5ba63f852f6d72d4a520b8b3d8ebb1 Mon Sep 17 00:00:00 2001 From: lollerfirst Date: Wed, 4 Jun 2025 12:14:37 +0200 Subject: [PATCH 69/69] fix --- tests/wallet/test_wallet_cli.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/wallet/test_wallet_cli.py b/tests/wallet/test_wallet_cli.py index 8fb66f134..ed6e9160d 100644 --- a/tests/wallet/test_wallet_cli.py +++ b/tests/wallet/test_wallet_cli.py @@ -593,6 +593,7 @@ def test_pay_amountless_invoice(mint, cli_prefix): cli, [*cli_prefix, "pay", invoice_no_amount if is_fake else get_real_invoice(0)["payment_request"], "10"] ) - assert result.exception is None or str(result.exception) == "Mint does not support amountless invoices, cannot pay this invoice." - print("test_pay_amountless_invoice ", result.output) - assert "Invoice paid" in result.output or "Invoice pending" in result.output \ No newline at end of file + if result.exception: + assert str(result.exception) == "Mint does not support amountless invoices, cannot pay this invoice." + else: + assert "Invoice paid" in result.output or "Invoice pending" in result.output \ No newline at end of file