mirror of
https://github.com/lnbits/lnbits-legend.git
synced 2025-03-11 01:36:11 +01:00
Wallets: add cln-rest (#1775)
* 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 <jkranawetter05@gmail.com> Co-authored-by: jackstar12 <62219658+jackstar12@users.noreply.github.com> Co-authored-by: dni ⚡ <office@dnilabs.com>
This commit is contained in:
parent
bb5033d225
commit
3a653630f1
11 changed files with 396 additions and 42 deletions
|
@ -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.
|
||||
|
|
44
.github/workflows/regtest.yml
vendored
44
.github/workflows/regtest.yml
vendored
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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',
|
||||
{
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
255
lnbits/wallets/corelightningrest.py
Normal file
255
lnbits/wallets/corelightningrest.py
Normal file
|
@ -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)
|
|
@ -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()
|
||||
|
|
|
@ -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())
|
||||
|
|
Loading…
Add table
Reference in a new issue