From 9c19b61e4ac11ab84ce7dfbdb7dfed3211bad458 Mon Sep 17 00:00:00 2001 From: calle <93376500+callebtc@users.noreply.github.com> Date: Mon, 1 Aug 2022 16:41:50 +0200 Subject: [PATCH] Mega-merge 3: CLN update client lib with descriptionhash support (WIP) (#792) * CoreLightningWallet --- .env.example | 6 +- .github/workflows/regtest.yml | 10 +-- Dockerfile | 2 +- docs/guide/wallets.md | 8 +-- lnbits/wallets/__init__.py | 3 +- lnbits/wallets/{clightning.py => cln.py} | 82 ++++++++++++++---------- 6 files changed, 63 insertions(+), 48 deletions(-) rename lnbits/wallets/{clightning.py => cln.py} (63%) diff --git a/.env.example b/.env.example index 0a5e2bffc..ec980775f 100644 --- a/.env.example +++ b/.env.example @@ -35,7 +35,7 @@ LNBITS_THEME_OPTIONS="classic, bitcoin, freedom, mint, autumn, monochrome, salva # LNBITS_CUSTOM_LOGO="https://lnbits.com/assets/images/logo/logo.svg" # Choose from LNPayWallet, OpenNodeWallet, LntxbotWallet, ClicheWallet -# LndRestWallet, CLightningWallet, LNbitsWallet, SparkWallet, FakeWallet, EclairWallet +# LndRestWallet, CoreLightningWallet, LNbitsWallet, SparkWallet, FakeWallet, EclairWallet LNBITS_BACKEND_WALLET_CLASS=VoidWallet # VoidWallet is just a fallback that works without any actual Lightning capabilities, # just so you can see the UI before dealing with this file. @@ -49,8 +49,8 @@ CLICHE_ENDPOINT=ws://127.0.0.1:12000 SPARK_URL=http://localhost:9737/rpc SPARK_TOKEN=myaccesstoken -# CLightningWallet -CLIGHTNING_RPC="/home/bob/.lightning/bitcoin/lightning-rpc" +# CoreLightningWallet +CORELIGHTNING_RPC="/home/bob/.lightning/bitcoin/lightning-rpc" # LnbitsWallet LNBITS_ENDPOINT=https://legend.lnbits.com diff --git a/.github/workflows/regtest.yml b/.github/workflows/regtest.yml index ce77bba40..3df68f962 100644 --- a/.github/workflows/regtest.yml +++ b/.github/workflows/regtest.yml @@ -29,7 +29,7 @@ jobs: python -m venv ${{ env.VIRTUAL_ENV }} ./venv/bin/python -m pip install --upgrade pip ./venv/bin/pip install -r requirements.txt - ./venv/bin/pip install pylightning + ./venv/bin/pip install pyln-client ./venv/bin/pip install pytest pytest-asyncio pytest-cov requests mock - name: Run tests env: @@ -47,7 +47,7 @@ jobs: uses: codecov/codecov-action@v3 with: file: ./coverage.xml - CLightningWallet: + CoreLightningWallet: runs-on: ubuntu-latest strategy: matrix: @@ -73,15 +73,15 @@ jobs: python -m venv ${{ env.VIRTUAL_ENV }} ./venv/bin/python -m pip install --upgrade pip ./venv/bin/pip install -r requirements.txt - ./venv/bin/pip install pylightning + ./venv/bin/pip install pyln-client ./venv/bin/pip install pytest pytest-asyncio pytest-cov requests mock - name: Run tests env: PYTHONUNBUFFERED: 1 PORT: 5123 LNBITS_DATA_FOLDER: ./data - LNBITS_BACKEND_WALLET_CLASS: CLightningWallet - CLIGHTNING_RPC: ./docker/data/clightning-1/regtest/lightning-rpc + LNBITS_BACKEND_WALLET_CLASS: CoreLightningWallet + CORELIGHTNING_RPC: ./docker/data/clightning-1/regtest/lightning-rpc run: | sudo chmod -R a+rwx . && rm -rf ./data && mkdir -p ./data make test-real-wallet diff --git a/Dockerfile b/Dockerfile index c29d75010..f8a5c6926 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,7 +17,7 @@ COPY requirements.txt /tmp/requirements.txt RUN pip install -r /tmp/requirements.txt # Install c-lightning specific deps -RUN pip install pylightning +RUN pip install pyln-client # Install LND specific deps RUN pip install lndgrpc diff --git a/docs/guide/wallets.md b/docs/guide/wallets.md index dfea66aa2..9eaca8d46 100644 --- a/docs/guide/wallets.md +++ b/docs/guide/wallets.md @@ -9,17 +9,17 @@ Backend wallets =============== LNbits can run on top of many lightning-network funding sources. Currently there is support for -CLightning, LND, LNbits, LNPay, lntxbot and OpenNode, with more being added regularily. +CoreLightning, LND, LNbits, LNPay, lntxbot and OpenNode, with more being added regularily. A backend wallet can be configured using the following LNbits environment variables: -### CLightning +### CoreLightning Using this wallet requires the installation of the `pylightning` Python package. -- `LNBITS_BACKEND_WALLET_CLASS`: **CLightningWallet** -- `CLIGHTNING_RPC`: /file/path/lightning-rpc +- `LNBITS_BACKEND_WALLET_CLASS`: **CoreLightningWallet** +- `CORELIGHTNING_RPC`: /file/path/lightning-rpc ### Spark (c-lightning) diff --git a/lnbits/wallets/__init__.py b/lnbits/wallets/__init__.py index 3ce2c30f9..4cf425cf8 100644 --- a/lnbits/wallets/__init__.py +++ b/lnbits/wallets/__init__.py @@ -1,7 +1,8 @@ # flake8: noqa from .cliche import ClicheWallet -from .clightning import CLightningWallet +from .cln import CoreLightningWallet # legacy .env support +from .cln import CoreLightningWallet as CLightningWallet from .eclair import EclairWallet from .fake import FakeWallet from .lnbits import LNbitsWallet diff --git a/lnbits/wallets/clightning.py b/lnbits/wallets/cln.py similarity index 63% rename from lnbits/wallets/clightning.py rename to lnbits/wallets/cln.py index 5ea47339f..221e38616 100644 --- a/lnbits/wallets/clightning.py +++ b/lnbits/wallets/cln.py @@ -1,5 +1,5 @@ try: - from lightning import LightningRpc, RpcError # type: ignore + from pyln.client import LightningRpc, RpcError # type: ignore except ImportError: # pragma: nocover LightningRpc = None @@ -11,6 +11,8 @@ from functools import partial, wraps from os import getenv from typing import AsyncGenerator, Optional +from loguru import logger + from lnbits import bolt11 as lnbits_bolt11 from .base import ( @@ -42,26 +44,20 @@ def _paid_invoices_stream(ln, last_pay_index): return ln.waitanyinvoice(last_pay_index) -class CLightningWallet(Wallet): +class CoreLightningWallet(Wallet): def __init__(self): if LightningRpc is None: # pragma: nocover raise ImportError( - "The `pylightning` library must be installed to use `CLightningWallet`." + "The `pyln-client` library must be installed to use `CoreLightningWallet`." ) - self.rpc = getenv("CLIGHTNING_RPC") + self.rpc = getenv("CORELIGHTNING_RPC") or getenv("CLIGHTNING_RPC") self.ln = LightningRpc(self.rpc) - # check description_hash support (could be provided by a plugin) - self.supports_description_hash = False - try: - answer = self.ln.help("invoicewithdescriptionhash") - if answer["help"][0]["command"].startswith( - "invoicewithdescriptionhash msatoshi label description_hash" - ): - self.supports_description_hash = True - except: - pass + # check if description_hash is supported (from CLN>=v0.11.0) + self.supports_description_hash = ( + "deschashonly" in self.ln.help("invoice")["help"][0]["command"] + ) # check last payindex so we can listen from that point on self.last_pay_index = 0 @@ -89,21 +85,32 @@ class CLightningWallet(Wallet): ) -> InvoiceResponse: label = "lbl{}".format(random.random()) msat = amount * 1000 - try: - if description_hash: - if not self.supports_description_hash: - raise Unsupported("description_hash") + if description_hash and not self.supports_description_hash: + raise Unsupported("description_hash") + r = self.ln.invoice( + msatoshi=msat, + label=label, + description=description_hash.decode("utf-8") + if description_hash + else memo, + exposeprivatechannels=True, + deschashonly=True + if description_hash + else False, # we can't pass None here + ) - params = [msat, label, hashlib.sha256(description_hash).hexdigest()] - r = self.ln.call("invoicewithdescriptionhash", params) - return InvoiceResponse(True, label, r["bolt11"], "") - else: - r = self.ln.invoice(msat, label, memo, exposeprivatechannels=True) - return InvoiceResponse(True, label, r["bolt11"], "") + if r.get("code") and r.get("code") < 0: + raise Exception(r.get("message")) + + return InvoiceResponse(True, r["payment_hash"], r["bolt11"], "") except RpcError as exc: error_message = f"lightningd '{exc.method}' failed with '{exc.error}'." - return InvoiceResponse(False, label, None, error_message) + logger.error("RPC error:", error_message) + return InvoiceResponse(False, None, None, error_message) + except Exception as e: + logger.error("error:", e) + return InvoiceResponse(False, None, None, str(e)) async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse: invoice = lnbits_bolt11.decode(bolt11) @@ -117,18 +124,19 @@ class CLightningWallet(Wallet): try: wrapped = async_wrap(_pay_invoice) r = await wrapped(self.ln, payload) - except RpcError as exc: + except Exception as exc: return PaymentResponse(False, None, 0, None, str(exc)) fee_msat = r["msatoshi_sent"] - r["msatoshi"] - preimage = r["payment_preimage"] - return PaymentResponse(True, r["payment_hash"], fee_msat, preimage, None) + return PaymentResponse( + True, r["payment_hash"], fee_msat, r["payment_preimage"], None + ) async def get_invoice_status(self, checking_id: str) -> PaymentStatus: - r = self.ln.listinvoices(checking_id) + r = self.ln.listinvoices(payment_hash=checking_id) if not r["invoices"]: return PaymentStatus(False) - if r["invoices"][0]["label"] == checking_id: + if r["invoices"][0]["payment_hash"] == checking_id: return PaymentStatus(r["invoices"][0]["status"] == "paid") raise KeyError("supplied an invalid checking_id") @@ -147,7 +155,13 @@ class CLightningWallet(Wallet): async def paid_invoices_stream(self) -> AsyncGenerator[str, None]: while True: - wrapped = async_wrap(_paid_invoices_stream) - paid = await wrapped(self.ln, self.last_pay_index) - self.last_pay_index = paid["pay_index"] - yield paid["label"] + try: + wrapped = async_wrap(_paid_invoices_stream) + paid = await wrapped(self.ln, self.last_pay_index) + self.last_pay_index = paid["pay_index"] + yield paid["payment_hash"] + except Exception as exc: + logger.error( + f"lost connection to cln invoices stream: '{exc}', retrying in 5 seconds" + ) + await asyncio.sleep(5)