diff --git a/cashu/core/models.py b/cashu/core/models.py index 4f3c8f3..fa0b57b 100644 --- a/cashu/core/models.py +++ b/cashu/core/models.py @@ -117,6 +117,9 @@ class KeysetsResponse_deprecated(BaseModel): class PostMintQuoteRequest(BaseModel): unit: str = Field(..., max_length=settings.mint_max_request_length) # output unit amount: int = Field(..., gt=0) # output amount + description: Optional[str] = Field( + default=None, max_length=settings.mint_max_request_length + ) # invoice description class PostMintQuoteResponse(BaseModel): @@ -206,7 +209,7 @@ class PostMeltQuoteResponse(BaseModel): state: Optional[str] # state of the quote expiry: Optional[int] # expiry of the quote payment_preimage: Optional[str] = None # payment preimage - change: Union[List[BlindedSignature], None] = None + change: Union[List[BlindedSignature], None] = None # NUT-08 change @classmethod def from_melt_quote(self, melt_quote: MeltQuote) -> "PostMeltQuoteResponse": diff --git a/cashu/lightning/base.py b/cashu/lightning/base.py index 06fbe97..c1500ba 100644 --- a/cashu/lightning/base.py +++ b/cashu/lightning/base.py @@ -70,6 +70,7 @@ class LightningBackend(ABC): supports_mpp: bool = False supports_incoming_payment_stream: bool = False supported_units: set[Unit] + supports_description: bool = False unit: Unit def assert_unit_supported(self, unit: Unit): diff --git a/cashu/lightning/blink.py b/cashu/lightning/blink.py index 2320baa..1acb8a5 100644 --- a/cashu/lightning/blink.py +++ b/cashu/lightning/blink.py @@ -47,6 +47,7 @@ class BlinkWallet(LightningBackend): payment_statuses = {"SUCCESS": True, "PENDING": None, "FAILURE": False} supported_units = set([Unit.sat, Unit.msat]) + supports_description: bool = True unit = Unit.sat def __init__(self, unit: Unit = Unit.sat, **kwargs): diff --git a/cashu/lightning/clnrest.py b/cashu/lightning/clnrest.py index 129c4d7..0dcb68e 100644 --- a/cashu/lightning/clnrest.py +++ b/cashu/lightning/clnrest.py @@ -31,6 +31,7 @@ class CLNRestWallet(LightningBackend): unit = Unit.sat supports_mpp = settings.mint_clnrest_enable_mpp supports_incoming_payment_stream: bool = True + supports_description: bool = True def __init__(self, unit: Unit = Unit.sat, **kwargs): self.assert_unit_supported(unit) diff --git a/cashu/lightning/corelightningrest.py b/cashu/lightning/corelightningrest.py index 94dcf6f..51a1ac2 100644 --- a/cashu/lightning/corelightningrest.py +++ b/cashu/lightning/corelightningrest.py @@ -30,6 +30,7 @@ class CoreLightningRestWallet(LightningBackend): supported_units = set([Unit.sat, Unit.msat]) unit = Unit.sat supports_incoming_payment_stream: bool = True + supports_description: bool = True def __init__(self, unit: Unit = Unit.sat, **kwargs): self.assert_unit_supported(unit) diff --git a/cashu/lightning/fake.py b/cashu/lightning/fake.py index 7517bfe..b9d3cb3 100644 --- a/cashu/lightning/fake.py +++ b/cashu/lightning/fake.py @@ -48,6 +48,7 @@ class FakeWallet(LightningBackend): unit = Unit.sat supports_incoming_payment_stream: bool = True + supports_description: bool = True def __init__(self, unit: Unit = Unit.sat, **kwargs): self.assert_unit_supported(unit) diff --git a/cashu/lightning/lnbits.py b/cashu/lightning/lnbits.py index d231e04..7daff1f 100644 --- a/cashu/lightning/lnbits.py +++ b/cashu/lightning/lnbits.py @@ -28,6 +28,7 @@ class LNbitsWallet(LightningBackend): supported_units = set([Unit.sat]) unit = Unit.sat supports_incoming_payment_stream: bool = True + supports_description: 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 0c38e62..fa667ee 100644 --- a/cashu/lightning/lnd_grpc/lnd_grpc.py +++ b/cashu/lightning/lnd_grpc/lnd_grpc.py @@ -45,39 +45,43 @@ INVOICE_STATUSES = { lnrpc.Invoice.InvoiceState.ACCEPTED: None, } -class LndRPCWallet(LightningBackend): +class LndRPCWallet(LightningBackend): supports_mpp = settings.mint_lnd_enable_mpp supports_incoming_payment_stream = True supported_units = set([Unit.sat, Unit.msat]) + supports_description: bool = True + unit = Unit.sat def __init__(self, unit: Unit = Unit.sat, **kwargs): self.assert_unit_supported(unit) self.unit = unit - self.endpoint = settings.mint_lnd_rpc_endpoint cert_path = settings.mint_lnd_rpc_cert macaroon_path = settings.mint_lnd_rpc_macaroon - if not self.endpoint: + if not settings.mint_lnd_rpc_endpoint: raise Exception("cannot initialize LndRPCWallet: no endpoint") + self.endpoint = settings.mint_lnd_rpc_endpoint + if not macaroon_path: raise Exception("cannot initialize LndRPCWallet: no macaroon") if not cert_path: raise Exception("no certificate for LndRPCWallet provided") - self.macaroon = codecs.encode(open(macaroon_path, 'rb').read(), 'hex') + self.macaroon = codecs.encode(open(macaroon_path, "rb").read(), "hex") def metadata_callback(context, callback): - callback([('macaroon', self.macaroon)], None) + callback([("macaroon", self.macaroon)], None) + auth_creds = grpc.metadata_call_credentials(metadata_callback) # create SSL credentials - os.environ['GRPC_SSL_CIPHER_SUITES'] = 'HIGH+ECDSA' - cert = open(cert_path, 'rb').read() + os.environ["GRPC_SSL_CIPHER_SUITES"] = "HIGH+ECDSA" + cert = open(cert_path, "rb").read() ssl_creds = grpc.ssl_channel_credentials(cert) # combine macaroon and SSL credentials @@ -86,11 +90,12 @@ class LndRPCWallet(LightningBackend): if self.supports_mpp: logger.info("LndRPCWallet enabling MPP feature") - async def status(self) -> StatusResponse: r = None try: - async with grpc.aio.secure_channel(self.endpoint, self.combined_creds) as channel: + async with grpc.aio.secure_channel( + self.endpoint, self.combined_creds + ) as channel: lnstub = lightningstub.LightningStub(channel) r = await lnstub.ChannelBalance(lnrpc.ChannelBalanceRequest()) except AioRpcError as e: @@ -98,9 +103,8 @@ class LndRPCWallet(LightningBackend): error_message=f"Error calling Lnd gRPC: {e}", balance=0 ) # NOTE: `balance` field is deprecated. Change this. - return StatusResponse(error_message=None, balance=r.balance*1000) + return StatusResponse(error_message=None, balance=r.balance * 1000) - async def create_invoice( self, amount: Amount, @@ -124,7 +128,9 @@ class LndRPCWallet(LightningBackend): r = None try: - async with grpc.aio.secure_channel(self.endpoint, self.combined_creds) as channel: + async with grpc.aio.secure_channel( + self.endpoint, self.combined_creds + ) as channel: lnstub = lightningstub.LightningStub(channel) r = await lnstub.AddInvoice(data) except AioRpcError as e: @@ -144,7 +150,7 @@ class LndRPCWallet(LightningBackend): payment_request=payment_request, error_message=None, ) - + async def pay_invoice( self, quote: MeltQuote, fee_limit_msat: int ) -> PaymentResponse: @@ -159,12 +165,12 @@ class LndRPCWallet(LightningBackend): ) # set the fee limit for the payment - feelimit = lnrpc.FeeLimit( - fixed_msat=fee_limit_msat - ) + feelimit = lnrpc.FeeLimit(fixed_msat=fee_limit_msat) r = None try: - async with grpc.aio.secure_channel(self.endpoint, self.combined_creds) as channel: + async with grpc.aio.secure_channel( + self.endpoint, self.combined_creds + ) as channel: lnstub = lightningstub.LightningStub(channel) r = await lnstub.SendPaymentSync( lnrpc.SendRequest( @@ -195,14 +201,12 @@ class LndRPCWallet(LightningBackend): preimage=preimage, error_message=None, ) - + async def pay_partial_invoice( self, quote: MeltQuote, amount: Amount, fee_limit_msat: int ) -> PaymentResponse: # set the fee limit for the payment - feelimit = lnrpc.FeeLimit( - fixed_msat=fee_limit_msat - ) + feelimit = lnrpc.FeeLimit(fixed_msat=fee_limit_msat) invoice = bolt11.decode(quote.request) invoice_amount = invoice.amount_msat @@ -220,7 +224,9 @@ class LndRPCWallet(LightningBackend): # get the route r = None try: - async with grpc.aio.secure_channel(self.endpoint, self.combined_creds) as channel: + async with grpc.aio.secure_channel( + self.endpoint, self.combined_creds + ) as channel: lnstub = lightningstub.LightningStub(channel) router_stub = routerstub.RouterStub(channel) r = await lnstub.QueryRoutes( @@ -230,23 +236,27 @@ class LndRPCWallet(LightningBackend): fee_limit=feelimit, ) ) - ''' + """ # We need to set the mpp_record for a partial payment mpp_record = lnrpc.MPPRecord( payment_addr=bytes.fromhex(payer_addr), total_amt_msat=total_amount_msat, ) - ''' + """ # modify the mpp_record in the last hop route_nr = 0 - r.routes[route_nr].hops[-1].mpp_record.payment_addr = bytes.fromhex(payer_addr) - r.routes[route_nr].hops[-1].mpp_record.total_amt_msat = total_amount_msat + r.routes[route_nr].hops[-1].mpp_record.payment_addr = bytes.fromhex( # type: ignore + payer_addr + ) + r.routes[route_nr].hops[ # type: ignore + -1 + ].mpp_record.total_amt_msat = total_amount_msat # Send to route request r = await router_stub.SendToRouteV2( routerrpc.SendToRouteRequest( payment_hash=bytes.fromhex(invoice.payment_hash), - route=r.routes[route_nr], + route=r.routes[route_nr], # type: ignore ) ) except AioRpcError as e: @@ -275,16 +285,16 @@ class LndRPCWallet(LightningBackend): preimage=preimage, error_message=None, ) - + async def get_invoice_status(self, checking_id: str) -> PaymentStatus: r = None try: - async with grpc.aio.secure_channel(self.endpoint, self.combined_creds) as channel: + async with grpc.aio.secure_channel( + self.endpoint, self.combined_creds + ) as channel: lnstub = lightningstub.LightningStub(channel) r = await lnstub.LookupInvoice( - lnrpc.PaymentHash( - r_hash=bytes.fromhex(checking_id) - ) + lnrpc.PaymentHash(r_hash=bytes.fromhex(checking_id)) ) except AioRpcError as e: error_message = f"LookupInvoice failed: {e}" @@ -292,7 +302,7 @@ class LndRPCWallet(LightningBackend): return PaymentStatus(paid=None) return PaymentStatus(paid=INVOICE_STATUSES[r.state]) - + async def get_payment_status(self, checking_id: str) -> PaymentStatus: """ This routine checks the payment status using routerpc.TrackPaymentV2. @@ -307,7 +317,9 @@ class LndRPCWallet(LightningBackend): request = routerrpc.TrackPaymentRequest(payment_hash=checking_id_bytes) try: - async with grpc.aio.secure_channel(self.endpoint, self.combined_creds) as channel: + async with grpc.aio.secure_channel( + self.endpoint, self.combined_creds + ) as channel: router_stub = routerstub.RouterStub(channel) async for payment in router_stub.TrackPaymentV2(request): if payment is not None and payment.status: @@ -325,21 +337,23 @@ class LndRPCWallet(LightningBackend): logger.error(error_message) return PaymentStatus(paid=None) - + async def paid_invoices_stream(self) -> AsyncGenerator[str, None]: while True: try: - async with grpc.aio.secure_channel(self.endpoint, self.combined_creds) as channel: + async with grpc.aio.secure_channel( + self.endpoint, self.combined_creds + ) as channel: lnstub = lightningstub.LightningStub(channel) - async for invoice in lnstub.SubscribeInvoices(lnrpc.InvoiceSubscription()): + async for invoice in lnstub.SubscribeInvoices( + lnrpc.InvoiceSubscription() + ): if invoice.state != lnrpc.Invoice.InvoiceState.SETTLED: continue payment_hash = invoice.r_hash.hex() yield payment_hash except AioRpcError as exc: - logger.error( - f"SubscribeInvoices failed: {exc}. Retrying in 1 sec..." - ) + logger.error(f"SubscribeInvoices failed: {exc}. Retrying in 1 sec...") await asyncio.sleep(1) async def get_payment_quote( diff --git a/cashu/lightning/lndrest.py b/cashu/lightning/lndrest.py index 3274da9..18dbcd4 100644 --- a/cashu/lightning/lndrest.py +++ b/cashu/lightning/lndrest.py @@ -33,6 +33,7 @@ class LndRestWallet(LightningBackend): supports_mpp = settings.mint_lnd_enable_mpp supports_incoming_payment_stream = True supported_units = set([Unit.sat, Unit.msat]) + supports_description: bool = True unit = Unit.sat def __init__(self, unit: Unit = Unit.sat, **kwargs): diff --git a/cashu/lightning/strike.py b/cashu/lightning/strike.py index cf35cb4..2103c2c 100644 --- a/cashu/lightning/strike.py +++ b/cashu/lightning/strike.py @@ -1,6 +1,6 @@ # type: ignore import secrets -from typing import AsyncGenerator, Dict, Optional +from typing import AsyncGenerator, Optional import httpx @@ -23,6 +23,7 @@ class StrikeWallet(LightningBackend): """https://docs.strike.me/api/""" supported_units = [Unit.sat, Unit.usd, Unit.eur] + supports_description: bool = False currency_map = {Unit.sat: "BTC", Unit.usd: "USD", Unit.eur: "EUR"} def __init__(self, unit: Unit, **kwargs): @@ -95,13 +96,6 @@ class StrikeWallet(LightningBackend): ) -> InvoiceResponse: self.assert_unit_supported(amount.unit) - data: Dict = {"out": False, "amount": amount} - if description_hash: - data["description_hash"] = description_hash.hex() - if unhashed_description: - data["unhashed_description"] = unhashed_description.hex() - - data["memo"] = memo or "" payload = { "correlationId": secrets.token_hex(16), "description": "Invoice for order 123", diff --git a/cashu/mint/ledger.py b/cashu/mint/ledger.py index 434cbb9..324de90 100644 --- a/cashu/mint/ledger.py +++ b/cashu/mint/ledger.py @@ -403,6 +403,12 @@ class Ledger(LedgerVerification, LedgerSpendingConditions, LedgerTasks, LedgerFe quote_request.unit, Method.bolt11.name ) + if ( + quote_request.description + and not self.backends[method][unit].supports_description + ): + raise NotAllowedError("Backend does not support descriptions.") + if settings.mint_max_balance: balance = await self.get_balance() if balance + quote_request.amount > settings.mint_max_balance: @@ -411,7 +417,10 @@ class Ledger(LedgerVerification, LedgerSpendingConditions, LedgerTasks, LedgerFe logger.trace(f"requesting invoice for {unit.str(quote_request.amount)}") invoice_response: InvoiceResponse = await self.backends[method][ unit - ].create_invoice(Amount(unit=unit, amount=quote_request.amount)) + ].create_invoice( + amount=Amount(unit=unit, amount=quote_request.amount), + memo=quote_request.description, + ) logger.trace( f"got invoice {invoice_response.payment_request} with checking id" f" {invoice_response.checking_id}" diff --git a/cashu/wallet/cli/cli.py b/cashu/wallet/cli/cli.py index 59e8009..d41c45c 100644 --- a/cashu/wallet/cli/cli.py +++ b/cashu/wallet/cli/cli.py @@ -241,6 +241,7 @@ async def pay( @cli.command("invoice", help="Create Lighting invoice.") @click.argument("amount", type=float) +@click.option("memo", "-m", default="", help="Memo for the invoice.", type=str) @click.option("--id", default="", help="Id of the paid invoice.", type=str) @click.option( "--split", @@ -259,7 +260,14 @@ async def pay( ) @click.pass_context @coro -async def invoice(ctx: Context, amount: float, id: str, split: int, no_check: bool): +async def invoice( + ctx: Context, + amount: float, + memo: str, + id: str, + split: int, + no_check: bool, +): wallet: Wallet = ctx.obj["WALLET"] await wallet.load_mint() await print_balance(ctx) @@ -324,11 +332,11 @@ async def invoice(ctx: Context, amount: float, id: str, split: int, no_check: bo ) if mint_supports_websockets and not no_check: invoice, subscription = await wallet.request_mint_with_callback( - amount, callback=mint_invoice_callback + amount, callback=mint_invoice_callback, memo=memo ) invoice_nonlocal, subscription_nonlocal = invoice, subscription else: - invoice = await wallet.request_mint(amount) + invoice = await wallet.request_mint(amount, memo=memo) if invoice.bolt11: print("") print(f"Pay invoice to mint {wallet.unit.str(amount)}:") diff --git a/cashu/wallet/lightning/lightning.py b/cashu/wallet/lightning/lightning.py index 56ab06a..3e5489d 100644 --- a/cashu/wallet/lightning/lightning.py +++ b/cashu/wallet/lightning/lightning.py @@ -1,3 +1,5 @@ +from typing import Optional + import bolt11 from ...core.base import Amount, ProofSpentState, Unit @@ -33,15 +35,18 @@ class LightningWallet(Wallet): pass super().__init__(*args, **kwargs) - async def create_invoice(self, amount: int) -> InvoiceResponse: + async def create_invoice( + self, amount: int, memo: Optional[str] = None + ) -> InvoiceResponse: """Create lightning invoice Args: amount (int): amount in satoshis + memo (str, optional): invoice memo. Defaults to None. Returns: str: invoice """ - invoice = await self.request_mint(amount) + invoice = await self.request_mint(amount, memo) return InvoiceResponse( ok=True, payment_request=invoice.bolt11, checking_id=invoice.payment_hash ) diff --git a/cashu/wallet/v1_api.py b/cashu/wallet/v1_api.py index e68c556..f1a8bb1 100644 --- a/cashu/wallet/v1_api.py +++ b/cashu/wallet/v1_api.py @@ -283,11 +283,15 @@ class LedgerAPI(LedgerAPIDeprecated, object): @async_set_httpx_client @async_ensure_mint_loaded - async def mint_quote(self, amount: int, unit: Unit) -> PostMintQuoteResponse: + async def mint_quote( + self, amount: int, unit: Unit, memo: Optional[str] = None + ) -> PostMintQuoteResponse: """Requests a mint quote from the server and returns a payment request. Args: amount (int): Amount of tokens to mint + unit (Unit): Unit of the amount + memo (Optional[str], optional): Memo to attach to Lightning invoice. Defaults to None. Returns: PostMintQuoteResponse: Mint Quote Response @@ -296,7 +300,7 @@ class LedgerAPI(LedgerAPIDeprecated, object): Exception: If the mint request fails """ logger.trace("Requesting mint: POST /v1/mint/bolt11") - payload = PostMintQuoteRequest(unit=unit.name, amount=amount) + payload = PostMintQuoteRequest(unit=unit.name, amount=amount, description=memo) resp = await self.httpx.post( join(self.url, "/v1/mint/quote/bolt11"), json=payload.dict() ) diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index 8a255d5..20f9839 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -379,18 +379,19 @@ class Wallet( logger.trace("Secret check complete.") async def request_mint_with_callback( - self, amount: int, callback: Callable + self, amount: int, callback: Callable, memo: Optional[str] = None ) -> Tuple[Invoice, SubscriptionManager]: """Request a Lightning invoice for minting tokens. Args: amount (int): Amount for Lightning invoice in satoshis callback (Callable): Callback function to be called when the invoice is paid. + memo (Optional[str], optional): Memo for the Lightning invoice. Defaults Returns: Invoice: Lightning invoice """ - mint_qoute = await super().mint_quote(amount, self.unit) + mint_qoute = await super().mint_quote(amount, self.unit, memo) subscriptions = SubscriptionManager(self.url) threading.Thread( target=subscriptions.connect, name="SubscriptionManager", daemon=True @@ -413,17 +414,18 @@ class Wallet( await store_lightning_invoice(db=self.db, invoice=invoice) return invoice, subscriptions - async def request_mint(self, amount: int) -> Invoice: + async def request_mint(self, amount: int, memo: Optional[str] = None) -> Invoice: """Request a Lightning invoice for minting tokens. Args: amount (int): Amount for Lightning invoice in satoshis callback (Optional[Callable], optional): Callback function to be called when the invoice is paid. Defaults to None. + memo (Optional[str], optional): Memo for the Lightning invoice. Defaults to None. Returns: PostMintQuoteResponse: Mint Quote Response """ - mint_quote_response = await super().mint_quote(amount, self.unit) + mint_quote_response = await super().mint_quote(amount, self.unit, memo) decoded_invoice = bolt11.decode(mint_quote_response.request) invoice = Invoice( amount=amount, @@ -480,11 +482,12 @@ class Wallet( return amounts - async def mint_quote(self, amount: int) -> Invoice: + async def mint_quote(self, amount: int, memo: Optional[str] = None) -> Invoice: """Request a Lightning invoice for minting tokens. Args: amount (int): Amount for Lightning invoice in satoshis + memo (Optional[str], optional): Memo for the Lightning invoice. Defaults to None. Returns: Invoice: Lightning invoice for minting tokens diff --git a/mypy.ini b/mypy.ini index 0df56bd..087ac3c 100644 --- a/mypy.ini +++ b/mypy.ini @@ -6,3 +6,6 @@ ignore_missing_imports = True [mypy-cashu.nostr.*] ignore_errors = True + +[mypy-cashu.lightning.lnd_grpc.protos.*] +ignore_errors = True diff --git a/tests/test_mint.py b/tests/test_mint.py index f5976a8..9d09729 100644 --- a/tests/test_mint.py +++ b/tests/test_mint.py @@ -86,6 +86,22 @@ async def test_mint(ledger: Ledger): ) +@pytest.mark.asyncio +async def test_mint_invalid_quote(ledger: Ledger): + await assert_err( + ledger.get_mint_quote(quote_id="invalid_quote_id"), + "quote not found", + ) + + +@pytest.mark.asyncio +async def test_melt_invalid_quote(ledger: Ledger): + await assert_err( + ledger.get_melt_quote(quote_id="invalid_quote_id"), + "quote not found", + ) + + @pytest.mark.asyncio async def test_mint_invalid_blinded_message(ledger: Ledger): quote = await ledger.mint_quote(PostMintQuoteRequest(amount=8, unit="sat")) diff --git a/tests/test_wallet_cli.py b/tests/test_wallet_cli.py index 81a2e4b..995518d 100644 --- a/tests/test_wallet_cli.py +++ b/tests/test_wallet_cli.py @@ -1,6 +1,7 @@ import asyncio from typing import Tuple +import bolt11 import pytest from click.testing import CliRunner @@ -8,7 +9,7 @@ from cashu.core.base import TokenV4 from cashu.core.settings import settings from cashu.wallet.cli.cli import cli from cashu.wallet.wallet import Wallet -from tests.helpers import is_fake, pay_if_regtest +from tests.helpers import is_deprecated_api_only, is_fake, is_regtest, pay_if_regtest @pytest.fixture(autouse=True, scope="session") @@ -107,6 +108,19 @@ def test_balance(cli_prefix): assert result.exit_code == 0 +@pytest.mark.skipif(is_regtest, reason="only works with FakeWallet") +def test_invoice(mint, cli_prefix): + runner = CliRunner() + result = runner.invoke( + cli, + [*cli_prefix, "invoice", "1000"], + ) + + wallet = asyncio.run(init_wallet()) + assert wallet.available_balance >= 1000 + assert result.exit_code == 0 + + def test_invoice_return_immediately(mint, cli_prefix): runner = CliRunner() result = runner.invoke( @@ -130,6 +144,30 @@ def test_invoice_return_immediately(mint, cli_prefix): assert result.exit_code == 0 +@pytest.mark.skipif(is_deprecated_api_only, reason="only works with v1 API") +def test_invoice_with_memo(mint, cli_prefix): + runner = CliRunner() + result = runner.invoke( + cli, + [*cli_prefix, "invoice", "-n", "1000", "-m", "test memo"], + ) + assert result.exception is None + + # find word starting with ln in the output + lines = result.output.split("\n") + invoice_str = "" + for line in lines: + for word in line.split(" "): + if word.startswith("ln"): + invoice_str = word + break + if not invoice_str: + raise Exception("No invoice found in the output") + invoice_obj = bolt11.decode(invoice_str) + assert invoice_obj.amount_msat == 1000_000 + assert invoice_obj.description == "test memo" + + def test_invoice_with_split(mint, cli_prefix): runner = CliRunner() result = runner.invoke( diff --git a/tests/test_wallet_lightning.py b/tests/test_wallet_lightning.py index 3b3cdd4..5dd567f 100644 --- a/tests/test_wallet_lightning.py +++ b/tests/test_wallet_lightning.py @@ -1,5 +1,6 @@ from typing import List, Union +import bolt11 import pytest import pytest_asyncio @@ -7,7 +8,13 @@ from cashu.core.base import Proof from cashu.core.errors import CashuError from cashu.wallet.lightning import LightningWallet from tests.conftest import SERVER_ENDPOINT -from tests.helpers import get_real_invoice, is_fake, is_regtest, pay_if_regtest +from tests.helpers import ( + get_real_invoice, + is_deprecated_api_only, + is_fake, + is_regtest, + pay_if_regtest, +) async def assert_err(f, msg: Union[str, CashuError]): @@ -58,6 +65,16 @@ async def test_create_invoice(wallet: LightningWallet): assert invoice.payment_request.startswith("ln") +@pytest.mark.asyncio +@pytest.mark.skipif(is_deprecated_api_only, reason="only works with v1 API") +async def test_create_invoice_with_description(wallet: LightningWallet): + invoice = await wallet.create_invoice(64, "test description") + assert invoice.payment_request + assert invoice.payment_request.startswith("ln") + invoiceObj = bolt11.decode(invoice.payment_request) + assert invoiceObj.description == "test description" + + @pytest.mark.asyncio @pytest.mark.skipif(is_regtest, reason="only works with FakeWallet") async def test_check_invoice_internal(wallet: LightningWallet):