From b288a6d50e1b8b2a3703a9098ed4aeeabd07d232 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 23 Mar 2024 01:16:28 +0100 Subject: [PATCH] Refactor: melt quote unit validation (#489) * refactor: mint quote validation * convert amount in Lightning backend for later validation * fix blink amount return tests * retry tests * fix conftest fakewallet * fix start --- .env.example | 2 ++ .github/workflows/regtest.yml | 2 +- cashu/lightning/blink.py | 6 +++++- cashu/lightning/corelightningrest.py | 4 +++- cashu/lightning/fake.py | 4 +++- cashu/lightning/lnbits.py | 4 +++- cashu/lightning/lndrest.py | 4 +++- cashu/lightning/strike.py | 24 ------------------------ cashu/mint/ledger.py | 27 ++++++++++++++++----------- tests/conftest.py | 4 ++-- tests/test_mint_lightning_blink.py | 22 +++++++++++----------- tests/test_mint_operations.py | 2 +- 12 files changed, 50 insertions(+), 55 deletions(-) diff --git a/.env.example b/.env.example index 7664190..f0fcb4f 100644 --- a/.env.example +++ b/.env.example @@ -58,6 +58,8 @@ MINT_DATABASE=data/mint # Funding source backends # Supported: FakeWallet, LndRestWallet, CoreLightningRestWallet, BlinkWallet, LNbitsWallet, StrikeUSDWallet MINT_BACKEND_BOLT11_SAT=FakeWallet +# Only works if a usd derivation path is set +# MINT_BACKEND_BOLT11_SAT=FakeWallet # for use with LNbitsWallet MINT_LNBITS_ENDPOINT=https://legend.lnbits.com diff --git a/.github/workflows/regtest.yml b/.github/workflows/regtest.yml index 46e2d8e..ed052a4 100644 --- a/.github/workflows/regtest.yml +++ b/.github/workflows/regtest.yml @@ -63,7 +63,7 @@ jobs: MINT_PORT: 3337 MINT_TEST_DATABASE: ${{ inputs.mint-database }} TOR: false - MINT_LIGHTNING_BACKEND: ${{ inputs.backend-wallet-class }} + MINT_BACKEND_BOLT11_SAT: ${{ inputs.backend-wallet-class }} MINT_LNBITS_ENDPOINT: http://localhost:5001 MINT_LNBITS_KEY: d08a3313322a4514af75d488bcc27eee MINT_LND_REST_ENDPOINT: https://localhost:8081/ diff --git a/cashu/lightning/blink.py b/cashu/lightning/blink.py index 0e12fb1..5c9f0dc 100644 --- a/cashu/lightning/blink.py +++ b/cashu/lightning/blink.py @@ -444,7 +444,11 @@ class BlinkWallet(LightningBackend): fees = Amount(unit=Unit.msat, amount=fees_msat) amount = Amount(unit=Unit.msat, amount=amount_msat) - return PaymentQuoteResponse(checking_id=bolt11, fee=fees, amount=amount) + return PaymentQuoteResponse( + checking_id=bolt11, + fee=fees.to(self.unit, round="up"), + amount=amount.to(self.unit, round="up"), + ) async def main(): diff --git a/cashu/lightning/corelightningrest.py b/cashu/lightning/corelightningrest.py index dff61a4..da36cdc 100644 --- a/cashu/lightning/corelightningrest.py +++ b/cashu/lightning/corelightningrest.py @@ -320,5 +320,7 @@ class CoreLightningRestWallet(LightningBackend): fees = Amount(unit=Unit.msat, amount=fees_msat) amount = Amount(unit=Unit.msat, amount=amount_msat) return PaymentQuoteResponse( - checking_id=invoice_obj.payment_hash, fee=fees, amount=amount + checking_id=invoice_obj.payment_hash, + fee=fees.to(self.unit, round="up"), + amount=amount.to(self.unit, round="up"), ) diff --git a/cashu/lightning/fake.py b/cashu/lightning/fake.py index 111eae1..97834ee 100644 --- a/cashu/lightning/fake.py +++ b/cashu/lightning/fake.py @@ -169,5 +169,7 @@ class FakeWallet(LightningBackend): raise NotImplementedError() return PaymentQuoteResponse( - checking_id=invoice_obj.payment_hash, fee=fees, amount=amount + checking_id=invoice_obj.payment_hash, + fee=fees.to(self.unit, round="up"), + amount=amount.to(self.unit, round="up"), ) diff --git a/cashu/lightning/lnbits.py b/cashu/lightning/lnbits.py index 916cb71..96dff6b 100644 --- a/cashu/lightning/lnbits.py +++ b/cashu/lightning/lnbits.py @@ -165,5 +165,7 @@ class LNbitsWallet(LightningBackend): fees = Amount(unit=Unit.msat, amount=fees_msat) amount = Amount(unit=Unit.msat, amount=amount_msat) return PaymentQuoteResponse( - checking_id=invoice_obj.payment_hash, fee=fees, amount=amount + checking_id=invoice_obj.payment_hash, + fee=fees.to(self.unit, round="up"), + amount=amount.to(self.unit, round="up"), ) diff --git a/cashu/lightning/lndrest.py b/cashu/lightning/lndrest.py index dd47ea0..2919732 100644 --- a/cashu/lightning/lndrest.py +++ b/cashu/lightning/lndrest.py @@ -272,5 +272,7 @@ class LndRestWallet(LightningBackend): fees = Amount(unit=Unit.msat, amount=fees_msat) amount = Amount(unit=Unit.msat, amount=amount_msat) return PaymentQuoteResponse( - checking_id=invoice_obj.payment_hash, fee=fees, amount=amount + checking_id=invoice_obj.payment_hash, + fee=fees.to(self.unit, round="up"), + amount=amount.to(self.unit, round="up"), ) diff --git a/cashu/lightning/strike.py b/cashu/lightning/strike.py index 5abd5fb..1824c79 100644 --- a/cashu/lightning/strike.py +++ b/cashu/lightning/strike.py @@ -195,27 +195,3 @@ class StrikeUSDWallet(LightningBackend): fee_msat=data["details"]["fee"], preimage=data["preimage"], ) - - # async def paid_invoices_stream(self) -> AsyncGenerator[str, None]: - # url = f"{self.endpoint}/api/v1/payments/sse" - - # while True: - # try: - # async with requests.stream("GET", url) as r: - # async for line in r.aiter_lines(): - # if line.startswith("data:"): - # try: - # data = json.loads(line[5:]) - # except json.decoder.JSONDecodeError: - # continue - - # if type(data) is not dict: - # continue - - # yield data["payment_hash"] # payment_hash - - # except: - # pass - - # print("lost connection to lnbits /payments/sse, retrying in 5 seconds") - # await asyncio.sleep(5) diff --git a/cashu/mint/ledger.py b/cashu/mint/ledger.py index d994448..255a61b 100644 --- a/cashu/mint/ledger.py +++ b/cashu/mint/ledger.py @@ -480,22 +480,14 @@ class Ledger(LedgerVerification, LedgerSpendingConditions): # NOTE: we normalize the request to lowercase to avoid case sensitivity # This works with Lightning but might not work with other methods request = melt_quote.request.lower() - invoice_obj = bolt11.decode(melt_quote.request) - assert invoice_obj.amount_msat, "invoice has no amount." # check if there is a mint quote with the same payment request - # so that we can handle the transaction internally without lightning - # and respond with zero fees + # so that we would be able to handle the transaction internally + # and therefore respond with internal transaction fees (0 for now) mint_quote = await self.crud.get_mint_quote_by_request( request=request, db=self.db ) if mint_quote: - # internal transaction, validate and return amount from - # associated mint quote and demand zero fees - # assert ( - # Amount(unit, mint_quote.amount).to(Unit.msat).amount - # == invoice_obj.amount_msat - # ), "amounts do not match" assert request == mint_quote.request, "bolt11 requests do not match" assert mint_quote.unit == melt_quote.unit, "units do not match" assert mint_quote.method == method.name, "methods do not match" @@ -512,10 +504,23 @@ class Ledger(LedgerVerification, LedgerSpendingConditions): f" {mint_quote.quote} ({mint_quote.amount} {mint_quote.unit})" ) else: - # not internal, get quote by backend + # not internal, get payment quote by backend payment_quote = await self.backends[method][unit].get_payment_quote(request) assert payment_quote.checking_id, "quote has no checking id" + # make sure the backend returned the amount with a correct unit + assert ( + payment_quote.amount.unit == unit + ), "payment quote amount units do not match" + # fee from the backend must be in the same unit as the amount + assert ( + payment_quote.fee.unit == unit + ), "payment quote fee units do not match" + # 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) + assert invoice_obj.amount_msat, "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: expiry = invoice_obj.date + invoice_obj.expiry diff --git a/tests/conftest.py b/tests/conftest.py index f6887cb..af682cc 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -31,7 +31,7 @@ settings.mint_listen_port = SERVER_PORT settings.mint_url = SERVER_ENDPOINT settings.tor = False settings.wallet_unit = "sat" -settings.mint_lightning_backend = settings.mint_lightning_backend or "FakeWallet" +settings.mint_backend_bolt11_sat = settings.mint_backend_bolt11_sat or "FakeWallet" settings.fakewallet_brr = True settings.fakewallet_delay_payment = False settings.fakewallet_stochastic_invoice = False @@ -111,7 +111,7 @@ async def ledger(): await conn.execute("CREATE SCHEMA public;") wallets_module = importlib.import_module("cashu.lightning") - lightning_backend = getattr(wallets_module, settings.mint_lightning_backend)() + lightning_backend = getattr(wallets_module, settings.mint_backend_bolt11_sat)() backends = { Method.bolt11: {Unit.sat: lightning_backend}, } diff --git a/tests/test_mint_lightning_blink.py b/tests/test_mint_lightning_blink.py index 224a635..85312e1 100644 --- a/tests/test_mint_lightning_blink.py +++ b/tests/test_mint_lightning_blink.py @@ -7,7 +7,7 @@ from cashu.core.settings import settings from cashu.lightning.blink import MINIMUM_FEE_MSAT, BlinkWallet settings.mint_blink_key = "123" -blink = BlinkWallet() +blink = BlinkWallet(unit=Unit.sat) payment_request = ( "lnbc10u1pjap7phpp50s9lzr3477j0tvacpfy2ucrs4q0q6cvn232ex7nt2zqxxxj8gxrsdpv2phhwetjv4jzqcneypqyc6t8dp6xu6twva2xjuzzda6qcqzzsxqrrsss" "p575z0n39w2j7zgnpqtdlrgz9rycner4eptjm3lz363dzylnrm3h4s9qyyssqfz8jglcshnlcf0zkw4qu8fyr564lg59x5al724kms3h6gpuhx9xrfv27tgx3l3u3cyf6" @@ -194,32 +194,32 @@ async def test_blink_get_payment_quote(): respx.post(blink.endpoint).mock(return_value=Response(200, json=mock_response)) quote = await blink.get_payment_quote(payment_request) assert quote.checking_id == payment_request - assert quote.amount == Amount(Unit.msat, 1000000) # msat - assert quote.fee == Amount(Unit.msat, 5000) # msat + assert quote.amount == Amount(Unit.sat, 1000) # sat + assert quote.fee == Amount(Unit.sat, 5) # sat # response says 10 sat fees but invoice (1000 sat) * 0.5% is 5 sat so we expect 10 sat mock_response = {"data": {"lnInvoiceFeeProbe": {"amount": 10}}} respx.post(blink.endpoint).mock(return_value=Response(200, json=mock_response)) quote = await blink.get_payment_quote(payment_request) assert quote.checking_id == payment_request - assert quote.amount == Amount(Unit.msat, 1000000) # msat - assert quote.fee == Amount(Unit.msat, 10000) # msat + assert quote.amount == Amount(Unit.sat, 1000) # sat + assert quote.fee == Amount(Unit.sat, 10) # sat # response says 10 sat fees but invoice (4973 sat) * 0.5% is 24.865 sat so we expect 25 sat mock_response = {"data": {"lnInvoiceFeeProbe": {"amount": 10}}} respx.post(blink.endpoint).mock(return_value=Response(200, json=mock_response)) quote = await blink.get_payment_quote(payment_request_4973) assert quote.checking_id == payment_request_4973 - assert quote.amount == Amount(Unit.msat, 4973000) # msat - assert quote.fee == Amount(Unit.msat, 25000) # msat + assert quote.amount == Amount(Unit.sat, 4973) # sat + assert quote.fee == Amount(Unit.sat, 25) # sat # response says 0 sat fees but invoice (1 sat) * 0.5% is 0.005 sat so we expect MINIMUM_FEE_MSAT/1000 sat mock_response = {"data": {"lnInvoiceFeeProbe": {"amount": 0}}} respx.post(blink.endpoint).mock(return_value=Response(200, json=mock_response)) quote = await blink.get_payment_quote(payment_request_1) assert quote.checking_id == payment_request_1 - assert quote.amount == Amount(Unit.msat, 1000) # msat - assert quote.fee == Amount(Unit.msat, MINIMUM_FEE_MSAT) # msat + assert quote.amount == Amount(Unit.sat, 1) # sat + assert quote.fee == Amount(Unit.sat, MINIMUM_FEE_MSAT // 1000) # msat @respx.mock @@ -230,5 +230,5 @@ async def test_blink_get_payment_quote_backend_error(): respx.post(blink.endpoint).mock(return_value=Response(200, json=mock_response)) quote = await blink.get_payment_quote(payment_request) assert quote.checking_id == payment_request - assert quote.amount == Amount(Unit.msat, 1000000) # msat - assert quote.fee == Amount(Unit.msat, 5000) # msat + assert quote.amount == Amount(Unit.sat, 1000) # sat + assert quote.fee == Amount(Unit.sat, 5) # sat diff --git a/tests/test_mint_operations.py b/tests/test_mint_operations.py index d5bca55..faa58df 100644 --- a/tests/test_mint_operations.py +++ b/tests/test_mint_operations.py @@ -118,7 +118,7 @@ async def test_mint_external(wallet1: Wallet, ledger: Ledger): quote = await ledger.mint_quote(PostMintQuoteRequest(amount=128, unit="sat")) mint_quote = await ledger.get_mint_quote(quote.quote) - assert not mint_quote.paid, "mint quote not should be paid" + assert not mint_quote.paid, "mint quote already paid" await assert_err( wallet1.mint(128, id=quote.quote),