From 3a653630f1e4d0d41540bc06018ef0e027ada228 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Wed, 23 Aug 2023 08:59:39 +0200 Subject: [PATCH] Wallets: add cln-rest (#1775) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * receive and pay works * fix linter issues * import Paymentstatus from core.models * fix test real payment * fix get_payment_status check in lnbits * fix tests? * simplify * refactor AsyncClient * inline import of get_wallet_class fixes the previous cyclic import * invoice stream working * add notes as a reminder to get rid of labels when cln-rest supports payment_hash * create Payment dummy classmethod * remove unnecessary fields from dummy * fixes tests? * fix model * fix cln bug (#1814) * auth header * rename cln to corelightning * add clnrest to admin_ui * add to clnrest allowed sources * add allowed sources to .env.example * allow macaroon files * add corelightning rest to workflow * proper env names * cleanup routine * log wallet connection errors and fix macaroon clnrest * print error on connection fails * clnrest: handle disconnects faster * fix test use of get_payment_status * make format * clnrest: add unhashed_description * add unhashed_description to test * description_hash test * unhashed_description not supported by clnrest * fix checking_id return in api_payments_create_invoice * refactor test to use client instead of api_payments * formatting, some errorlogging * fix test 1 * fix other tests, paid statuses was missing * error handling * revert unnecessary changes (#1854) * apply review of motorina0 --------- Co-authored-by: jackstar12 Co-authored-by: jackstar12 <62219658+jackstar12@users.noreply.github.com> Co-authored-by: dni ⚡ --- .env.example | 6 +- .github/workflows/regtest.yml | 44 ++++ lnbits/app.py | 3 +- lnbits/core/templates/admin/index.html | 17 ++ lnbits/core/views/api.py | 9 +- lnbits/settings.py | 8 + lnbits/wallets/__init__.py | 5 +- lnbits/wallets/{cln.py => corelightning.py} | 28 +-- lnbits/wallets/corelightningrest.py | 255 ++++++++++++++++++++ lnbits/wallets/macaroon/macaroon.py | 9 +- tests/core/views/test_api.py | 54 +++-- 11 files changed, 396 insertions(+), 42 deletions(-) rename lnbits/wallets/{cln.py => corelightning.py} (85%) create mode 100644 lnbits/wallets/corelightningrest.py diff --git a/.env.example b/.env.example index 47558d4ee..6771412d3 100644 --- a/.env.example +++ b/.env.example @@ -69,9 +69,9 @@ LNBITS_SITE_DESCRIPTION="Some description about your service, will display if ti LNBITS_THEME_OPTIONS="classic, bitcoin, flamingo, freedom, mint, autumn, monochrome, salvador, cyber" # LNBITS_CUSTOM_LOGO="https://lnbits.com/assets/images/logo/logo.svg" -# Choose from LNPayWallet, OpenNodeWallet, ClicheWallet, -# LndWallet, LndRestWallet, CoreLightningWallet, EclairWallet, -# LnTipsWallet, LNbitsWallet, SparkWallet, FakeWallet, +# which fundingsources are allowed in the admin ui +LNBITS_ALLOWED_FUNDING_SOURCES="VoidWallet, FakeWallet, CoreLightningWallet, CoreLightningRestWallet, LndRestWallet, EclairWallet, LndWallet, LnTipsWallet, LNPayWallet, LNbitsWallet, OpenNodeWallet" + 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. diff --git a/.github/workflows/regtest.yml b/.github/workflows/regtest.yml index 594766457..2e72175fd 100644 --- a/.github/workflows/regtest.yml +++ b/.github/workflows/regtest.yml @@ -134,6 +134,50 @@ jobs: uses: codecov/codecov-action@v3 with: file: ./coverage.xml + CoreLightningRestWallet: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.9"] + poetry-version: ["1.3.1"] + steps: + - uses: actions/checkout@v3 + - name: Set up Poetry ${{ matrix.poetry-version }} + uses: abatilo/actions-poetry@v2 + with: + poetry-version: ${{ matrix.poetry-version }} + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + cache: "poetry" + - name: Setup Regtest + run: | + docker build -t lnbitsdocker/lnbits-legend . + git clone https://github.com/lnbits/legend-regtest-enviroment.git docker + cd docker + chmod +x ./tests + ./tests + sudo chmod -R a+rwx . + - name: Install dependencies + run: | + poetry install + - name: Run tests + env: + PYTHONUNBUFFERED: 1 + PORT: 5123 + LNBITS_DATA_FOLDER: ./data + LNBITS_BACKEND_WALLET_CLASS: CoreLightningRestWallet + CORELIGHTNING_REST_URL: https://localhost:3001 + CORELIGHTNING_REST_MACAROON: ./docker/data/clightning-2-rest/access.macaroon + CORELIGHTNING_REST_CERT: ./docker/data/clightning-2-rest/certificate.pem + run: | + sudo chmod -R a+rwx . && rm -rf ./data && mkdir -p ./data + make test-real-wallet + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v3 + with: + file: ./coverage.xml LNbitsWallet: runs-on: ubuntu-latest strategy: diff --git a/lnbits/app.py b/lnbits/app.py index 5b54251cd..feb69ed80 100644 --- a/lnbits/app.py +++ b/lnbits/app.py @@ -140,7 +140,8 @@ async def check_funding_source() -> None: f"working properly: '{error_message}'", RuntimeWarning, ) - except Exception: + except Exception as e: + logger.error(f"Error connecting to {WALLET.__class__.__name__}: {e}") pass if settings.lnbits_admin_ui and retry_counter == timeout: diff --git a/lnbits/core/templates/admin/index.html b/lnbits/core/templates/admin/index.html index 48c7c95d8..a41310a3f 100644 --- a/lnbits/core/templates/admin/index.html +++ b/lnbits/core/templates/admin/index.html @@ -236,6 +236,23 @@ } } ], + [ + 'CoreLightningRestWallet', + { + corelightning_rest_url: { + value: null, + label: 'Endpoint' + }, + corelightning_rest_cert: { + value: null, + label: 'Certificate' + }, + corelightning_rest_macaroon: { + value: null, + label: 'Macaroon' + } + } + ], [ 'LndRestWallet', { diff --git a/lnbits/core/views/api.py b/lnbits/core/views/api.py index 958d2b164..08c552f18 100644 --- a/lnbits/core/views/api.py +++ b/lnbits/core/views/api.py @@ -219,7 +219,7 @@ async def api_payments_create_invoice(data: CreateInvoice, wallet: Wallet): async with db.connect() as conn: try: - _, payment_request = await create_invoice( + payment_hash, payment_request = await create_invoice( wallet_id=wallet.id, amount=amount, memo=memo, @@ -231,6 +231,11 @@ async def api_payments_create_invoice(data: CreateInvoice, wallet: Wallet): internal=data.internal, conn=conn, ) + # NOTE: we get the checking_id with a seperate query because create_invoice does not return it + # and it would be a big hustle to change its return type (used across extensions) + payment_db = await get_standalone_payment(payment_hash, conn=conn) + assert payment_db is not None, "payment not found" + checking_id = payment_db.checking_id except InvoiceFailure as e: raise HTTPException(status_code=520, detail=str(e)) except Exception as exc: @@ -273,7 +278,7 @@ async def api_payments_create_invoice(data: CreateInvoice, wallet: Wallet): "payment_hash": invoice.payment_hash, "payment_request": payment_request, # maintain backwards compatibility with API clients: - "checking_id": invoice.payment_hash, + "checking_id": checking_id, "lnurl_response": lnurl_response, } diff --git a/lnbits/settings.py b/lnbits/settings.py index 9975cf6ed..1c5db56dc 100644 --- a/lnbits/settings.py +++ b/lnbits/settings.py @@ -137,6 +137,12 @@ class CoreLightningFundingSource(LNbitsSettings): clightning_rpc: Optional[str] = Field(default=None) +class CoreLightningRestFundingSource(LNbitsSettings): + corelightning_rest_url: Optional[str] = Field(default=None) + corelightning_rest_macaroon: Optional[str] = Field(default=None) + corelightning_rest_cert: Optional[str] = Field(default=None) + + class EclairFundingSource(LNbitsSettings): eclair_url: Optional[str] = Field(default=None) eclair_pass: Optional[str] = Field(default=None) @@ -207,6 +213,7 @@ class FundingSourcesSettings( LNbitsFundingSource, ClicheFundingSource, CoreLightningFundingSource, + CoreLightningRestFundingSource, EclairFundingSource, LndRestFundingSource, LndGrpcFundingSource, @@ -282,6 +289,7 @@ class SuperUserSettings(LNbitsSettings): "VoidWallet", "FakeWallet", "CoreLightningWallet", + "CoreLightningRestWallet", "LndRestWallet", "EclairWallet", "LndWallet", diff --git a/lnbits/wallets/__init__.py b/lnbits/wallets/__init__.py index 7233b1d76..838d7e270 100644 --- a/lnbits/wallets/__init__.py +++ b/lnbits/wallets/__init__.py @@ -7,8 +7,9 @@ from lnbits.settings import settings from lnbits.wallets.base import Wallet from .cliche import ClicheWallet -from .cln import CoreLightningWallet -from .cln import CoreLightningWallet as CLightningWallet +from .corelightning import CoreLightningWallet +from .corelightning import CoreLightningWallet as CLightningWallet +from .corelightningrest import CoreLightningRestWallet from .eclair import EclairWallet from .fake import FakeWallet from .lnbits import LNbitsWallet diff --git a/lnbits/wallets/cln.py b/lnbits/wallets/corelightning.py similarity index 85% rename from lnbits/wallets/cln.py rename to lnbits/wallets/corelightning.py index 51ccb51cf..e29b08094 100644 --- a/lnbits/wallets/cln.py +++ b/lnbits/wallets/corelightning.py @@ -43,14 +43,14 @@ class CoreLightningWallet(Wallet): self.rpc = settings.corelightning_rpc or settings.clightning_rpc self.ln = LightningRpc(self.rpc) - # check if description_hash is supported (from CLN>=v0.11.0) + # check if description_hash is supported (from corelightning>=v0.11.0) self.supports_description_hash = ( - "deschashonly" in self.ln.help("invoice")["help"][0]["command"] + "deschashonly" in self.ln.help("invoice")["help"][0]["command"] # type: ignore ) # check last payindex so we can listen from that point on self.last_pay_index = 0 - invoices = self.ln.listinvoices() + invoices: dict = self.ln.listinvoices() # type: ignore for inv in invoices["invoices"][::-1]: if "pay_index" in inv: self.last_pay_index = inv["pay_index"] @@ -58,7 +58,7 @@ class CoreLightningWallet(Wallet): async def status(self) -> StatusResponse: try: - funds = self.ln.listfunds() + funds: dict = self.ln.listfunds() # type: ignore return StatusResponse( None, sum([int(ch["our_amount_msat"]) for ch in funds["channels"]]) ) @@ -79,11 +79,11 @@ class CoreLightningWallet(Wallet): try: if description_hash and not unhashed_description: raise Unsupported( - "'description_hash' unsupported by CLN, provide 'unhashed_description'" + "'description_hash' unsupported by CoreLightning, provide 'unhashed_description'" ) if unhashed_description and not self.supports_description_hash: raise Unsupported("unhashed_description") - r = self.ln.invoice( + r: dict = self.ln.invoice( # type: ignore msatoshi=msat, label=label, description=unhashed_description.decode() @@ -96,12 +96,12 @@ class CoreLightningWallet(Wallet): expiry=kwargs.get("expiry"), ) - if r.get("code") and r.get("code") < 0: + if r.get("code") and r.get("code") < 0: # type: ignore raise Exception(r.get("message")) return InvoiceResponse(True, r["payment_hash"], r["bolt11"], "") except RpcError as exc: - error_message = f"CLN method '{exc.method}' failed with '{exc.error.get('message') or exc.error}'." + error_message = f"CoreLightning method '{exc.method}' failed with '{exc.error.get('message') or exc.error}'." # type: ignore return InvoiceResponse(False, None, None, error_message) except Exception as e: return InvoiceResponse(False, None, None, str(e)) @@ -125,9 +125,9 @@ class CoreLightningWallet(Wallet): r = await wrapped(self.ln, payload) except RpcError as exc: try: - error_message = exc.error["attempts"][-1]["fail_reason"] + error_message = exc.error["attempts"][-1]["fail_reason"] # type: ignore except Exception: - error_message = f"CLN method '{exc.method}' failed with '{exc.error.get('message') or exc.error}'." + error_message = f"CoreLightning method '{exc.method}' failed with '{exc.error.get('message') or exc.error}'." # type: ignore return PaymentResponse(False, None, None, None, error_message) except Exception as exc: return PaymentResponse(False, None, None, None, str(exc)) @@ -139,8 +139,8 @@ class CoreLightningWallet(Wallet): async def get_invoice_status(self, checking_id: str) -> PaymentStatus: try: - r = self.ln.listinvoices(payment_hash=checking_id) - except Exception: + r: dict = self.ln.listinvoices(payment_hash=checking_id) # type: ignore + except RpcError: return PaymentStatus(None) if not r["invoices"]: return PaymentStatus(None) @@ -160,7 +160,7 @@ class CoreLightningWallet(Wallet): async def get_payment_status(self, checking_id: str) -> PaymentStatus: try: - r = self.ln.listpays(payment_hash=checking_id) + r: dict = self.ln.listpays(payment_hash=checking_id) # type: ignore except Exception: return PaymentStatus(None) if "pays" not in r or not r["pays"]: @@ -192,6 +192,6 @@ class CoreLightningWallet(Wallet): yield paid["payment_hash"] except Exception as exc: logger.error( - f"lost connection to cln invoices stream: '{exc}', retrying in 5 seconds" + f"lost connection to corelightning invoices stream: '{exc}', retrying in 5 seconds" ) await asyncio.sleep(5) diff --git a/lnbits/wallets/corelightningrest.py b/lnbits/wallets/corelightningrest.py new file mode 100644 index 000000000..3e8e2a39b --- /dev/null +++ b/lnbits/wallets/corelightningrest.py @@ -0,0 +1,255 @@ +import asyncio +import json +import random +from typing import AsyncGenerator, Dict, Optional + +import httpx +from loguru import logger + +from lnbits import bolt11 as lnbits_bolt11 +from lnbits.settings import settings + +from .base import ( + InvoiceResponse, + PaymentResponse, + PaymentStatus, + StatusResponse, + Unsupported, + Wallet, +) +from .macaroon import load_macaroon + + +class CoreLightningRestWallet(Wallet): + def __init__(self): + macaroon = settings.corelightning_rest_macaroon + assert macaroon, "missing cln-rest macaroon" + + self.macaroon = load_macaroon(macaroon) + + url = settings.corelightning_rest_url + if not url: + raise Exception("missing url for corelightning-rest") + if not macaroon: + raise Exception("missing macaroon for corelightning-rest") + + self.url = url[:-1] if url.endswith("/") else url + self.url = ( + f"https://{self.url}" if not self.url.startswith("http") else self.url + ) + self.auth = { + "macaroon": self.macaroon, + "encodingtype": "hex", + "accept": "application/json", + } + + self.cert = settings.corelightning_rest_cert or False + self.client = httpx.AsyncClient(verify=self.cert, headers=self.auth) + self.last_pay_index = 0 + self.statuses = { + "paid": True, + "complete": True, + "failed": False, + "pending": None, + } + + async def cleanup(self): + try: + await self.client.aclose() + except RuntimeError as e: + logger.warning(f"Error closing wallet connection: {e}") + + async def status(self) -> StatusResponse: + r = await self.client.get(f"{self.url}/v1/channel/localremotebal", timeout=5) + r.raise_for_status() + if r.is_error or "error" in r.json(): + try: + data = r.json() + error_message = data["error"] + except Exception: + error_message = r.text + return StatusResponse( + f"Failed to connect to {self.url}, got: '{error_message}...'", 0 + ) + + data = r.json() + if len(data) == 0: + return StatusResponse("no data", 0) + + return StatusResponse(None, int(data.get("localBalance") * 1000)) + + async def create_invoice( + self, + amount: int, + memo: Optional[str] = None, + description_hash: Optional[bytes] = None, + unhashed_description: Optional[bytes] = None, + **kwargs, + ) -> InvoiceResponse: + label = f"lbl{random.random()}" + data: Dict = { + "amount": amount * 1000, + "description": memo, + "label": label, + } + if description_hash and not unhashed_description: + raise Unsupported( + "'description_hash' unsupported by CoreLightningRest, " + "provide 'unhashed_description'" + ) + + if unhashed_description: + data["description"] = unhashed_description.decode("utf-8") + + if kwargs.get("expiry"): + data["expiry"] = kwargs["expiry"] + + if kwargs.get("preimage"): + data["preimage"] = kwargs["preimage"] + + r = await self.client.post( + f"{self.url}/v1/invoice/genInvoice", + data=data, + ) + + if r.is_error or "error" in r.json(): + try: + data = r.json() + error_message = data["error"] + except Exception: + error_message = r.text + + return InvoiceResponse(False, None, None, error_message) + + data = r.json() + assert "payment_hash" in data + assert "bolt11" in data + # NOTE: use payment_hash when corelightning-rest updates and supports it + # return InvoiceResponse(True, data["payment_hash"], data["bolt11"], None) + return InvoiceResponse(True, label, data["bolt11"], None) + + async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse: + invoice = lnbits_bolt11.decode(bolt11) + fee_limit_percent = fee_limit_msat / invoice.amount_msat * 100 + r = await self.client.post( + f"{self.url}/v1/pay", + data={ + "invoice": bolt11, + "maxfeepercent": f"{fee_limit_percent:.11}", + "exemptfee": 0, # so fee_limit_percent is applied even on payments + # with fee < 5000 millisatoshi (which is default value of exemptfee) + }, + timeout=None, + ) + + if r.is_error or "error" in r.json(): + try: + data = r.json() + error_message = data["error"] + except Exception: + error_message = r.text + return PaymentResponse(False, None, None, None, error_message) + + data = r.json() + + if data["status"] != "complete": + return PaymentResponse(False, None, None, None, "payment failed") + + checking_id = data["payment_hash"] + preimage = data["payment_preimage"] + fee_msat = data["msatoshi_sent"] - data["msatoshi"] + + return PaymentResponse( + self.statuses.get(data["status"]), checking_id, fee_msat, preimage, None + ) + + async def get_invoice_status(self, checking_id: str) -> PaymentStatus: + # get invoice bolt11 from checking_id + # corelightning-rest wants the "label" here.... + # NOTE: We can get rid of all labels and use payment_hash when + # corelightning-rest updates and supports it + r = await self.client.get( + f"{self.url}/v1/invoice/listInvoices", + params={"label": checking_id}, + ) + try: + r.raise_for_status() + data = r.json() + + if r.is_error or "error" in data or data.get("invoices") is None: + raise Exception("error in cln response") + return PaymentStatus(self.statuses.get(data["invoices"][0]["status"])) + except Exception as e: + logger.error(f"Error getting invoice status: {e}") + return PaymentStatus(None) + + async def get_payment_status(self, checking_id: str) -> PaymentStatus: + from lnbits.core import get_standalone_payment + + payment = await get_standalone_payment(checking_id) + if not payment: + raise ValueError(f"Payment with checking_id {checking_id} not found") + r = await self.client.get( + f"{self.url}/v1/pay/listPays", + params={"invoice": payment.bolt11}, + ) + try: + r.raise_for_status() + data = r.json() + + if r.is_error or "error" in data or not data.get("pays"): + raise Exception("error in corelightning-rest response") + + pay = data["pays"][0] + + fee_msat, preimage = None, None + if self.statuses[pay["status"]]: + # cut off "msat" and convert to int + fee_msat = -int(pay["amount_sent_msat"][:-4]) - int( + pay["amount_msat"][:-4] + ) + preimage = pay["preimage"] + + return PaymentStatus(self.statuses.get(pay["status"]), fee_msat, preimage) + except Exception as e: + logger.error(f"Error getting payment status: {e}") + return PaymentStatus(None) + + async def paid_invoices_stream(self) -> AsyncGenerator[str, None]: + while True: + try: + url = f"{self.url}/v1/invoice/waitAnyInvoice/{self.last_pay_index}" + async with self.client.stream("GET", url, timeout=None) as r: + async for line in r.aiter_lines(): + inv = json.loads(line) + if "error" in inv and "message" in inv["error"]: + logger.error("Error in paid_invoices_stream:", inv) + raise Exception(inv["error"]["message"]) + try: + paid = inv["status"] == "paid" + self.last_pay_index = inv["pay_index"] + if not paid: + continue + except Exception: + continue + logger.trace(f"paid invoice: {inv}") + yield inv["label"] + # NOTE: use payment_hash when corelightning-rest updates + # and supports it + # payment_hash = inv["payment_hash"] + # yield payment_hash + # hack to return payment_hash if the above shouldn't work + # r = await self.client.get( + # f"{self.url}/v1/invoice/listInvoices", + # params={"label": inv["label"]}, + # ) + # paid_invoce = r.json() + # logger.trace(f"paid invoice: {paid_invoce}") + # yield paid_invoce["invoices"][0]["payment_hash"] + + except Exception as exc: + logger.debug( + f"lost connection to corelightning-rest invoices stream: '{exc}', " + "reconnecting..." + ) + await asyncio.sleep(0.02) diff --git a/lnbits/wallets/macaroon/macaroon.py b/lnbits/wallets/macaroon/macaroon.py index c27602a52..0bb28d56d 100644 --- a/lnbits/wallets/macaroon/macaroon.py +++ b/lnbits/wallets/macaroon/macaroon.py @@ -18,12 +18,19 @@ def load_macaroon(macaroon: str) -> str: :rtype: str """ - # if the macaroon is a file path, load it + # if the macaroon is a file path, load it and return hex version if macaroon.split(".")[-1] == "macaroon": with open(macaroon, "rb") as f: macaroon_bytes = f.read() return macaroon_bytes.hex() else: + # if macaroon is a provided string + # check if it is hex, if so, return + try: + bytes.fromhex(macaroon) + return macaroon + except ValueError: + pass # convert the bas64 macaroon to hex try: macaroon = base64.b64decode(macaroon).hex() diff --git a/tests/core/views/test_api.py b/tests/core/views/test_api.py index 9cd4c881e..7ff95c7e9 100644 --- a/tests/core/views/test_api.py +++ b/tests/core/views/test_api.py @@ -305,26 +305,30 @@ async def test_api_payment_with_key(invoice, inkey_headers_from): # check POST /api/v1/payments: invoice creation with a description hash @pytest.mark.skipif( - WALLET.__class__.__name__ in ["CoreLightningWallet"], + WALLET.__class__.__name__ in ["CoreLightningWallet", "CoreLightningRestWallet"], reason="wallet does not support description_hash", ) @pytest.mark.asyncio async def test_create_invoice_with_description_hash(client, inkey_headers_to): data = await get_random_invoice_data() - descr_hash = hashlib.sha256("asdasdasd".encode()).hexdigest() + description = "asdasdasd" + descr_hash = hashlib.sha256(description.encode()).hexdigest() data["description_hash"] = descr_hash - + data["unhashed_description"] = description.encode().hex() response = await client.post( "/api/v1/payments", json=data, headers=inkey_headers_to ) invoice = response.json() - invoice_bolt11 = bolt11.decode(invoice["payment_request"]) + assert invoice_bolt11.description_hash == descr_hash - assert invoice_bolt11.description is None return invoice +@pytest.mark.skipif( + WALLET.__class__.__name__ in ["CoreLightningRestWallet"], + reason="wallet does not support unhashed_description", +) @pytest.mark.asyncio async def test_create_invoice_with_unhashed_description(client, inkey_headers_to): data = await get_random_invoice_data() @@ -367,10 +371,12 @@ async def test_pay_real_invoice( assert payment.payment_hash == invoice["payment_hash"] # check the payment status - response = await api_payment( - invoice["payment_hash"], inkey_headers_from["X-Api-Key"] + response = await client.get( + f'/api/v1/payments/{invoice["payment_hash"]}', headers=inkey_headers_from ) - assert response["paid"] + assert response.status_code < 300 + payment_status = response.json() + assert payment_status["paid"] status = await WALLET.get_payment_status(invoice["payment_hash"]) assert status.paid @@ -392,23 +398,32 @@ async def test_create_real_invoice(client, adminkey_headers_from, inkey_headers_ ) assert response.status_code < 300 invoice = response.json() - response = await api_payment( - invoice["payment_hash"], inkey_headers_from["X-Api-Key"] + + response = await client.get( + f'/api/v1/payments/{invoice["payment_hash"]}', headers=inkey_headers_from ) - assert not response["paid"] + assert response.status_code < 300 + payment_status = response.json() + assert not payment_status["paid"] async def listen(): - async for payment_hash in get_wallet_class().paid_invoices_stream(): - assert payment_hash == invoice["payment_hash"] - return + found_checking_id = False + async for checking_id in get_wallet_class().paid_invoices_stream(): + if checking_id == invoice["checking_id"]: + found_checking_id = True + return + assert found_checking_id task = asyncio.create_task(listen()) pay_real_invoice(invoice["payment_request"]) await asyncio.wait_for(task, timeout=3) - response = await api_payment( - invoice["payment_hash"], inkey_headers_from["X-Api-Key"] + + response = await client.get( + f'/api/v1/payments/{invoice["payment_hash"]}', headers=inkey_headers_from ) - assert response["paid"] + assert response.status_code < 300 + payment_status = response.json() + assert payment_status["paid"] await asyncio.sleep(0.3) balance = await get_node_balance_sats() @@ -442,6 +457,7 @@ async def test_pay_real_invoice_set_pending_and_check_state( ) assert response["paid"] + # make sure that the backend also thinks it's paid status = await WALLET.get_payment_status(invoice["payment_hash"]) assert status.paid @@ -618,8 +634,8 @@ async def test_receive_real_invoice_set_pending_and_check_state( assert not response["paid"] async def listen(): - async for payment_hash in get_wallet_class().paid_invoices_stream(): - assert payment_hash == invoice["payment_hash"] + async for checking_id in get_wallet_class().paid_invoices_stream(): + assert checking_id == invoice["checking_id"] return task = asyncio.create_task(listen())