From 498fa3d07c6e75136bfeeb88b88f301c246a49a1 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Thu, 19 Oct 2023 13:11:20 +0200 Subject: [PATCH] fix tests --- cashu/lightning/base.py | 12 ++--- cashu/lightning/fake.py | 12 ++--- cashu/mint/ledger.py | 44 +++++++++--------- cashu/mint/lightning.py | 70 ++++++++++++++--------------- cashu/mint/startup.py | 8 ++-- cashu/wallet/api/router.py | 20 ++++----- cashu/wallet/lightning/lightning.py | 10 +++-- tests/test_wallet.py | 4 ++ tests/test_wallet_api.py | 8 ++-- 9 files changed, 100 insertions(+), 88 deletions(-) diff --git a/cashu/lightning/base.py b/cashu/lightning/base.py index dd281bce..17e681c8 100644 --- a/cashu/lightning/base.py +++ b/cashu/lightning/base.py @@ -1,20 +1,22 @@ from abc import ABC, abstractmethod -from typing import Coroutine, NamedTuple, Optional +from typing import Coroutine, Optional +from pydantic import BaseModel -class StatusResponse(NamedTuple): + +class StatusResponse(BaseModel): error_message: Optional[str] balance_msat: int -class InvoiceResponse(NamedTuple): +class InvoiceResponse(BaseModel): ok: bool # True: invoice created, False: failed checking_id: Optional[str] = None payment_request: Optional[str] = None error_message: Optional[str] = None -class PaymentResponse(NamedTuple): +class PaymentResponse(BaseModel): ok: Optional[bool] = None # True: paid, False: failed, None: pending or unknown checking_id: Optional[str] = None fee_msat: Optional[int] = None @@ -22,7 +24,7 @@ class PaymentResponse(NamedTuple): error_message: Optional[str] = None -class PaymentStatus(NamedTuple): +class PaymentStatus(BaseModel): paid: Optional[bool] = None fee_msat: Optional[int] = None preimage: Optional[str] = None diff --git a/cashu/lightning/fake.py b/cashu/lightning/fake.py index 044bd80d..ae956195 100644 --- a/cashu/lightning/fake.py +++ b/cashu/lightning/fake.py @@ -42,7 +42,7 @@ class FakeWallet(Wallet): ).hex() async def status(self) -> StatusResponse: - return StatusResponse(None, 1337) + return StatusResponse(error_message=None, balance_msat=1337) async def create_invoice( self, @@ -92,7 +92,9 @@ async def create_invoice( payment_request = encode(bolt11, self.privkey) - return InvoiceResponse(True, checking_id, payment_request) + return InvoiceResponse( + ok=True, checking_id=checking_id, payment_request=payment_request + ) async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse: invoice = decode(bolt11) @@ -114,12 +116,12 @@ async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse async def get_invoice_status(self, checking_id: str) -> PaymentStatus: if STOCHASTIC_INVOICE: paid = random.random() > 0.7 - return PaymentStatus(paid) + return PaymentStatus(paid=paid) paid = checking_id in self.paid_invoices or BRR - return PaymentStatus(paid or None) + return PaymentStatus(paid=paid or None) async def get_payment_status(self, _: str) -> PaymentStatus: - return PaymentStatus(None) + return PaymentStatus(paid=None) async def paid_invoices_stream(self) -> AsyncGenerator[str, None]: while True: diff --git a/cashu/mint/ledger.py b/cashu/mint/ledger.py index a4fd9b4e..ed1a0329 100644 --- a/cashu/mint/ledger.py +++ b/cashu/mint/ledger.py @@ -28,7 +28,7 @@ from ..core.helpers import fee_reserve, sum_proofs from ..core.settings import settings from ..core.split import amount_split -from ..lightning.base import Wallet +from ..lightning.base import PaymentResponse, Wallet from ..mint.crud import LedgerCrud from .conditions import LedgerSpendingConditions from .lightning import LedgerLightning @@ -247,23 +247,26 @@ async def request_mint(self, amount: int) -> Tuple[str, str]: raise NotAllowedError("Mint does not allow minting new tokens.") logger.trace(f"requesting invoice for {amount} satoshis") - payment_request, payment_hash = await self._request_lightning_invoice(amount) - logger.trace(f"got invoice {payment_request} with hash {payment_hash}") - assert payment_request and payment_hash, LightningError( - "could not fetch invoice from Lightning backend" + invoice_response = await self._request_lightning_invoice(amount) + logger.trace( + f"got invoice {invoice_response.payment_request} with check id" + f" {invoice_response.checking_id}" ) + assert ( + invoice_response.payment_request and invoice_response.checking_id + ), LightningError("could not fetch invoice from Lightning backend") invoice = Invoice( amount=amount, id=random_hash(), - bolt11=payment_request, - payment_hash=payment_hash, # what we got from the backend + bolt11=invoice_response.payment_request, + payment_hash=invoice_response.checking_id, # what we got from the backend issued=False, ) logger.trace(f"crud: storing invoice {invoice.id} in db") await self.crud.store_lightning_invoice(invoice=invoice, db=self.db) logger.trace(f"crud: stored invoice {invoice.id} in db") - return payment_request, invoice.id + return invoice_response.payment_request, invoice.id async def mint( self, @@ -357,20 +360,19 @@ async def melt( if settings.lightning: logger.trace(f"paying lightning invoice {invoice}") - status, preimage, paid_fee_msat = await self._pay_lightning_invoice( + payment = await self._pay_lightning_invoice( invoice, reserve_fees_sat * 1000 ) - preimage = preimage or "" logger.trace("paid lightning invoice") else: - status, preimage, paid_fee_msat = True, "preimage", 0 + payment = PaymentResponse(ok=True, preimage="preimage", fee_msat=0) logger.debug( - f"Melt status: {status}: preimage: {preimage}, fee_msat:" - f" {paid_fee_msat}" + f"Melt status: {payment.ok}: preimage: {payment.preimage}, fee_msat:" + f" {payment.fee_msat}" ) - if not status: + if not payment.ok: raise LightningError("Lightning payment unsuccessful.") # melt successful, invalidate proofs @@ -378,11 +380,11 @@ async def melt( # prepare change to compensate wallet for overpaid fees return_promises: List[BlindedSignature] = [] - if outputs and paid_fee_msat is not None: + if outputs and payment.fee_msat is not None: return_promises = await self._generate_change_promises( total_provided=total_provided, invoice_amount=invoice_amount, - ln_fee_msat=paid_fee_msat, + ln_fee_msat=payment.fee_msat, outputs=outputs, ) @@ -393,7 +395,7 @@ async def melt( # delete proofs from pending list await self._unset_proofs_pending(proofs) - return status, preimage, return_promises + return payment.ok, payment.preimage or "", return_promises async def get_melt_fees(self, pr: str) -> int: """Returns the fee reserve (in sat) that a wallet must add to its proofs @@ -416,9 +418,11 @@ async def get_melt_fees(self, pr: str) -> int: "get_melt_fees: checking lightning invoice:" f" {decoded_invoice.payment_hash}" ) - paid = await self.lightning.get_invoice_status(decoded_invoice.payment_hash) - logger.trace(f"get_melt_fees: paid: {paid}") - internal = paid.paid is False + payment = await self.lightning.get_invoice_status( + decoded_invoice.payment_hash + ) + logger.trace(f"get_melt_fees: paid: {payment.paid}") + internal = payment.paid is False else: amount_msat = 0 internal = True diff --git a/cashu/mint/lightning.py b/cashu/mint/lightning.py index ea664ce3..076c714c 100644 --- a/cashu/mint/lightning.py +++ b/cashu/mint/lightning.py @@ -1,4 +1,4 @@ -from typing import Literal, Optional, Tuple, Union +from typing import Optional, Union from loguru import logger @@ -10,7 +10,7 @@ InvoiceNotPaidError, LightningError, ) -from ..lightning.base import Wallet +from ..lightning.base import InvoiceResponse, PaymentResponse, PaymentStatus, Wallet from ..mint.crud import LedgerCrud from .protocols import SupportLightning, SupportsDb @@ -22,7 +22,7 @@ class LedgerLightning(SupportLightning, SupportsDb): crud: LedgerCrud db: Database - async def _request_lightning_invoice(self, amount: int) -> Tuple[str, str]: + async def _request_lightning_invoice(self, amount: int) -> InvoiceResponse: """Generate a Lightning invoice using the funding source backend. Args: @@ -38,30 +38,30 @@ async def _request_lightning_invoice(self, amount: int) -> Tuple[str, str]: "_request_lightning_invoice: Requesting Lightning invoice for" f" {amount} satoshis." ) - error, balance = await self.lightning.status() - logger.trace(f"_request_lightning_invoice: Lightning wallet balance: {balance}") - if error: - raise LightningError(f"Lightning wallet not responding: {error}") - ( - ok, - checking_id, - payment_request, - error_message, - ) = await self.lightning.create_invoice(amount, "Cashu deposit") + status = await self.lightning.status() logger.trace( - f"_request_lightning_invoice: Lightning invoice: {payment_request}" + "_request_lightning_invoice: Lightning wallet balance:" + f" {status.balance_msat}" + ) + if status.error_message: + raise LightningError( + f"Lightning wallet not responding: {status.error_message}" + ) + payment = await self.lightning.create_invoice(amount, "Cashu deposit") + logger.trace( + f"_request_lightning_invoice: Lightning invoice: {payment.payment_request}" ) - if not ok: - raise LightningError(f"Lightning wallet error: {error_message}") - assert payment_request and checking_id, LightningError( + if not payment.ok: + raise LightningError(f"Lightning wallet error: {payment.error_message}") + assert payment.payment_request and payment.checking_id, LightningError( "could not fetch invoice from Lightning backend" ) - return payment_request, checking_id + return payment async def _check_lightning_invoice( self, *, amount: int, id: str, conn: Optional[Connection] = None - ) -> Literal[True]: + ) -> PaymentStatus: """Checks with the Lightning backend whether an invoice with `id` was paid. Args: @@ -98,7 +98,7 @@ async def _check_lightning_invoice( try: status = await self.lightning.get_invoice_status(invoice.payment_hash) if status.paid: - return status.paid + return status else: raise InvoiceNotPaidError() except Exception as e: @@ -108,7 +108,9 @@ async def _check_lightning_invoice( ) raise e - async def _pay_lightning_invoice(self, invoice: str, fee_limit_msat: int): + async def _pay_lightning_invoice( + self, invoice: str, fee_limit_msat: int + ) -> PaymentResponse: """Pays a Lightning invoice via the funding source backend. Args: @@ -121,17 +123,15 @@ async def _pay_lightning_invoice(self, invoice: str, fee_limit_msat: int): Returns: Tuple[bool, string, int]: Returns payment status, preimage of invoice, paid fees (in Millisatoshi) """ - error, balance = await self.lightning.status() - if error: - raise LightningError(f"Lightning wallet not responding: {error}") - ( - ok, - checking_id, - fee_msat, - preimage, - error_message, - ) = await self.lightning.pay_invoice(invoice, fee_limit_msat=fee_limit_msat) - logger.trace(f"_pay_lightning_invoice: Lightning payment status: {ok}") - # make sure that fee is positive - fee_msat = abs(fee_msat) if fee_msat else fee_msat - return ok, preimage, fee_msat + status = await self.lightning.status() + if status.error_message: + raise LightningError( + f"Lightning wallet not responding: {status.error_message}" + ) + payment = await self.lightning.pay_invoice( + invoice, fee_limit_msat=fee_limit_msat + ) + logger.trace(f"_pay_lightning_invoice: Lightning payment status: {payment.ok}") + # make sure that fee is positive and not None + payment.fee_msat = abs(payment.fee_msat) if payment.fee_msat else 0 + return payment diff --git a/cashu/mint/startup.py b/cashu/mint/startup.py index d7a4ac13..162c853f 100644 --- a/cashu/mint/startup.py +++ b/cashu/mint/startup.py @@ -52,14 +52,14 @@ async def start_mint_init(): if settings.lightning: logger.info(f"Using backend: {settings.mint_lightning_backend}") - error_message, balance = await ledger.lightning.status() - if error_message: + status = await ledger.lightning.status() + if status.error_message: logger.warning( f"The backend for {ledger.lightning.__class__.__name__} isn't" - f" working properly: '{error_message}'", + f" working properly: '{status.error_message}'", RuntimeWarning, ) - logger.info(f"Lightning balance: {balance} msat") + logger.info(f"Lightning balance: {status.balance_msat} msat") logger.info(f"Data dir: {settings.cashu_dir}") logger.info("Mint started.") diff --git a/cashu/wallet/api/router.py b/cashu/wallet/api/router.py index 108fba89..e10d73f6 100644 --- a/cashu/wallet/api/router.py +++ b/cashu/wallet/api/router.py @@ -49,13 +49,15 @@ router: APIRouter = APIRouter() -async def mint_wallet(mint_url: Optional[str] = None): +async def mint_wallet( + mint_url: Optional[str] = None, raise_connection_error: bool = True +): wallet = await LightningWallet.with_db( mint_url or settings.mint_url, db=os.path.join(settings.cashu_dir, settings.wallet_name), name=settings.wallet_name, ) - await wallet.async_init() + await wallet.async_init(raise_connection_error=raise_connection_error) return wallet @@ -69,15 +71,9 @@ async def mint_wallet(mint_url: Optional[str] = None): @router.on_event("startup") async def start_wallet(): global wallet - wallet = await LightningWallet.with_db( - settings.mint_url, - db=os.path.join(settings.cashu_dir, settings.wallet_name), - name=settings.wallet_name, - ) - + wallet = await mint_wallet(settings.mint_url, raise_connection_error=False) if settings.tor and not TorProxy().check_platform(): raise Exception("tor not working.") - await wallet.async_init() @router.post( @@ -166,8 +162,10 @@ async def lightning_balance() -> StatusResponse: try: await wallet.load_proofs(reload=True) except Exception as exc: - return StatusResponse(str(exc), balance_msat=0) - return StatusResponse(None, balance_msat=wallet.available_balance * 1000) + return StatusResponse(error_message=str(exc), balance_msat=0) + return StatusResponse( + error_message=None, balance_msat=wallet.available_balance * 1000 + ) @router.post( diff --git a/cashu/wallet/lightning/lightning.py b/cashu/wallet/lightning/lightning.py index c22bf3cd..6714fb84 100644 --- a/cashu/wallet/lightning/lightning.py +++ b/cashu/wallet/lightning/lightning.py @@ -17,13 +17,15 @@ class LightningWallet(Wallet): Lightning wallet interface for Cashu """ - # wallet: Wallet - - async def async_init(self): + async def async_init(self, raise_connection_error: bool = True): """Async init for lightning wallet""" settings.tor = False await self.load_proofs() - await self.load_mint() + try: + await self.load_mint() + except Exception as e: + if raise_connection_error: + raise e def __init__(self, *args, **kwargs): if not args and not kwargs: diff --git a/tests/test_wallet.py b/tests/test_wallet.py index 11b19545..a869a85a 100644 --- a/tests/test_wallet.py +++ b/tests/test_wallet.py @@ -234,9 +234,12 @@ async def test_melt(wallet1: Wallet): invoice = await wallet1.request_mint(64) await wallet1.mint(64, id=invoice.id) assert wallet1.balance == 128 + total_amount, fee_reserve_sat = await wallet1.get_pay_amount_with_fees( invoice.bolt11 ) + assert total_amount == 66 + assert fee_reserve_sat == 2 _, send_proofs = await wallet1.split_to_send(wallet1.proofs, total_amount) @@ -259,6 +262,7 @@ async def test_melt(wallet1: Wallet): # the payment was without fees so we need to remove it from the total amount assert wallet1.balance == 128 - (total_amount - fee_reserve_sat) + assert wallet1.balance == 64 @pytest.mark.asyncio diff --git a/tests/test_wallet_api.py b/tests/test_wallet_api.py index 3d46cbf8..d07fa388 100644 --- a/tests/test_wallet_api.py +++ b/tests/test_wallet_api.py @@ -27,14 +27,14 @@ async def test_invoice(wallet: Wallet): with TestClient(app) as client: response = client.post("/lightning/create_invoice?amount=100") assert response.status_code == 200 - invoice_response = InvoiceResponse(*response.json()) + invoice_response = InvoiceResponse.parse_obj(response.json()) state = PaymentStatus(paid=False) while not state.paid: print("checking invoice state") response2 = client.get( f"/lightning/invoice_state?payment_hash={invoice_response.checking_id}" ) - state = PaymentStatus(*response2.json()) + state = PaymentStatus.parse_obj(response2.json()) await asyncio.sleep(0.1) print("state:", state) print("paid") @@ -155,14 +155,14 @@ async def test_flow(wallet: Wallet): response = client.get("/balance") initial_balance = response.json()["balance"] response = client.post("/lightning/create_invoice?amount=100") - invoice_response = InvoiceResponse(*response.json()) + invoice_response = InvoiceResponse.parse_obj(response.json()) state = PaymentStatus(paid=False) while not state.paid: print("checking invoice state") response2 = client.get( f"/lightning/invoice_state?payment_hash={invoice_response.checking_id}" ) - state = PaymentStatus(*response2.json()) + state = PaymentStatus.parse_obj(response2.json()) await asyncio.sleep(0.1) print("state:", state)