broken invoice listener on c-lightning and other fixes around wallets.

This commit is contained in:
fiatjaf 2020-10-03 17:27:55 -03:00
parent b3c69ad49c
commit e74cf33f90
9 changed files with 105 additions and 51 deletions

View File

@ -36,11 +36,11 @@ LNBITS_ADMIN_MACAROON=LNBITS_ADMIN_MACAROON
LND_GRPC_ENDPOINT=127.0.0.1 LND_GRPC_ENDPOINT=127.0.0.1
LND_GRPC_PORT=11009 LND_GRPC_PORT=11009
LND_GRPC_CERT="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/tls.cert" LND_GRPC_CERT="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/tls.cert"
LND_ADMIN_MACAROON="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/admin.macaroon" LND_GRPC_ADMIN_MACAROON="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/admin.macaroon"
LND_INVOICE_MACAROON="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/invoice.macaroon" LND_GRPC_INVOICE_MACAROON="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/invoice.macaroon"
# LndRestWallet # LndRestWallet
LND_REST_ENDPOINT=https://localhost:8080/ LND_REST_ENDPOINT=https://127.0.0.1:8080/
LND_REST_CERT="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/tls.cert" LND_REST_CERT="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/tls.cert"
LND_REST_ADMIN_MACAROON="HEXSTRING" LND_REST_ADMIN_MACAROON="HEXSTRING"
LND_REST_INVOICE_MACAROON="HEXSTRING" LND_REST_INVOICE_MACAROON="HEXSTRING"

View File

@ -34,7 +34,7 @@ You will need to copy `.env.example` to `.env`, then set variables there.
![Files](https://i.imgur.com/ri2zOe8.png) ![Files](https://i.imgur.com/ri2zOe8.png)
You might also need to install additional packages, depending on the [backend wallet](../guide/wallets.md) you use. You might also need to install additional packages, depending on the [backend wallet](../guide/wallets.md) you use.
E.g. when you want to use LND you have to `pipenv run pip install lnd-grpc`. E.g. when you want to use LND you have to `pipenv run pip install lndgrpc`.
Take a look at [Polar][polar] for an excellent way of spinning up a Lightning Network dev environment. Take a look at [Polar][polar] for an excellent way of spinning up a Lightning Network dev environment.

View File

@ -31,5 +31,5 @@ You might also need to install additional packages, depending on the chosen back
E.g. when you want to use LND you have to run: E.g. when you want to use LND you have to run:
```sh ```sh
./venv/bin/pip install lnd-grpc ./venv/bin/pip install lndgrpc
``` ```

View File

@ -29,7 +29,7 @@ Using this wallet requires the installation of the `pylightning` Python package.
### LND (gRPC) ### LND (gRPC)
Using this wallet requires the installation of the `lnd-grpc` Python package. Using this wallet requires the installation of the `lndgrpc` Python package.
- `LNBITS_BACKEND_WALLET_CLASS`: **LndWallet** - `LNBITS_BACKEND_WALLET_CLASS`: **LndWallet**
- `LND_GRPC_ENDPOINT`: ip_address - `LND_GRPC_ENDPOINT`: ip_address

View File

@ -3,7 +3,9 @@ try:
except ImportError: # pragma: nocover except ImportError: # pragma: nocover
LightningRpc = None LightningRpc = None
import asyncio
import random import random
import json
from os import getenv from os import getenv
from typing import Optional, AsyncGenerator from typing import Optional, AsyncGenerator
@ -15,7 +17,8 @@ class CLightningWallet(Wallet):
if LightningRpc is None: # pragma: nocover if LightningRpc is None: # pragma: nocover
raise ImportError("The `pylightning` library must be installed to use `CLightningWallet`.") raise ImportError("The `pylightning` library must be installed to use `CLightningWallet`.")
self.ln = LightningRpc(getenv("CLIGHTNING_RPC")) self.rpc = getenv("CLIGHTNING_RPC")
self.ln = LightningRpc(self.rpc)
# check description_hash support (could be provided by a plugin) # check description_hash support (could be provided by a plugin)
self.supports_description_hash = False self.supports_description_hash = False
@ -31,8 +34,10 @@ class CLightningWallet(Wallet):
# check last payindex so we can listen from that point on # check last payindex so we can listen from that point on
self.last_pay_index = 0 self.last_pay_index = 0
invoices = self.ln.listinvoices() invoices = self.ln.listinvoices()
if len(invoices["invoices"]): for inv in invoices["invoices"][::-1]:
self.last_pay_index = invoices["invoices"][-1]["pay_index"] if "pay_index" in inv:
self.last_pay_index = inv["pay_index"]
break
def create_invoice( def create_invoice(
self, amount: int, memo: Optional[str] = None, description_hash: Optional[bytes] = None self, amount: int, memo: Optional[str] = None, description_hash: Optional[bytes] = None
@ -45,7 +50,8 @@ class CLightningWallet(Wallet):
if not self.supports_description_hash: if not self.supports_description_hash:
raise Unsupported("description_hash") raise Unsupported("description_hash")
r = self.ln.call("invoicewithdescriptionhash", [msat, label, memo]) params = [msat, label, description_hash.hex()]
r = self.ln.call("invoicewithdescriptionhash", params)
return InvoiceResponse(True, label, r["bolt11"], "") return InvoiceResponse(True, label, r["bolt11"], "")
else: else:
r = self.ln.invoice(msat, label, memo, exposeprivatechannels=True) r = self.ln.invoice(msat, label, memo, exposeprivatechannels=True)
@ -56,15 +62,14 @@ class CLightningWallet(Wallet):
def pay_invoice(self, bolt11: str) -> PaymentResponse: def pay_invoice(self, bolt11: str) -> PaymentResponse:
r = self.ln.pay(bolt11) r = self.ln.pay(bolt11)
ok, checking_id, fee_msat, error_message = True, r["payment_hash"], r["msatoshi_sent"] - r["msatoshi"], None return PaymentResponse(True, r["payment_hash"], r["msatoshi_sent"] - r["msatoshi"], None)
return PaymentResponse(ok, checking_id, fee_msat, error_message)
def get_invoice_status(self, checking_id: str) -> PaymentStatus: def get_invoice_status(self, checking_id: str) -> PaymentStatus:
r = self.ln.listinvoices(checking_id) r = self.ln.listinvoices(checking_id)
if not r["invoices"]: if not r["invoices"]:
return PaymentStatus(False) return PaymentStatus(False)
if r["invoices"][0]["label"] == checking_id: if r["invoices"][0]["label"] == checking_id:
return PaymentStatus(r["pays"][0]["status"] == "paid") return PaymentStatus(r["invoices"][0]["status"] == "paid")
raise KeyError("supplied an invalid checking_id") raise KeyError("supplied an invalid checking_id")
def get_payment_status(self, checking_id: str) -> PaymentStatus: def get_payment_status(self, checking_id: str) -> PaymentStatus:
@ -81,7 +86,28 @@ class CLightningWallet(Wallet):
raise KeyError("supplied an invalid checking_id") raise KeyError("supplied an invalid checking_id")
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]: async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
reader, writer = await asyncio.open_unix_connection(self.rpc)
i = 0
while True: while True:
call = json.dumps(
{
"method": "waitanyinvoice",
"id": 0,
"params": [self.last_pay_index],
}
)
print(call)
writer.write(call.encode("ascii"))
await writer.drain()
data = await reader.readuntil(b"\n\n")
print(data)
paid = json.loads(data.decode("ascii"))
paid = self.ln.waitanyinvoice(self.last_pay_index) paid = self.ln.waitanyinvoice(self.last_pay_index)
self.last_pay_index = paid["pay_index"] self.last_pay_index = paid["pay_index"]
yield paid["label"] yield paid["label"]
i += 1

View File

@ -1,3 +1,4 @@
import asyncio
from os import getenv from os import getenv
from typing import Optional, Dict, AsyncGenerator from typing import Optional, Dict, AsyncGenerator
from requests import get, post from requests import get, post
@ -67,4 +68,5 @@ class LNbitsWallet(Wallet):
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]: async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
print("lnbits does not support paid invoices stream yet") print("lnbits does not support paid invoices stream yet")
await asyncio.sleep(5)
yield "" yield ""

View File

@ -1,9 +1,12 @@
try: try:
import lnd_grpc # type: ignore import lndgrpc # type: ignore
from lndgrpc.common import ln # type: ignore
except ImportError: # pragma: nocover except ImportError: # pragma: nocover
lnd_grpc = None lndgrpc = None
import binascii
import base64 import base64
import hashlib
from os import getenv from os import getenv
from typing import Optional, Dict, AsyncGenerator from typing import Optional, Dict, AsyncGenerator
@ -28,63 +31,82 @@ def stringify_checking_id(r_hash: bytes) -> str:
class LndWallet(Wallet): class LndWallet(Wallet):
def __init__(self): def __init__(self):
if lnd_grpc is None: # pragma: nocover if lndgrpc is None: # pragma: nocover
raise ImportError("The `lnd-grpc` library must be installed to use `LndWallet`.") raise ImportError("The `lndgrpc` library must be installed to use `LndWallet`.")
endpoint = getenv("LND_GRPC_ENDPOINT") endpoint = getenv("LND_GRPC_ENDPOINT")
endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint
port = getenv("LND_GRPC_PORT") port = getenv("LND_GRPC_PORT")
cert = getenv("LND_GRPC_CERT") or getenv("LND_CERT") cert = getenv("LND_GRPC_CERT") or getenv("LND_CERT")
auth_admin = getenv("LND_ADMIN_MACAROON") auth_admin = getenv("LND_GRPC_ADMIN_MACAROON") or getenv("LND_ADMIN_MACAROON")
auth_invoices = getenv("LND_INVOICE_MACAROON") auth_invoices = getenv("LND_GRPC_INVOICE_MACAROON") or getenv("LND_INVOICE_MACAROON")
network = getenv("LND_GRPC_NETWORK", "mainnet") network = getenv("LND_GRPC_NETWORK", "mainnet")
self.admin_rpc = lnd_grpc.Client( self.admin_rpc = lndgrpc.LNDClient(
lnd_dir=None, endpoint + ":" + port,
macaroon_path=auth_admin, cert_filepath=cert,
tls_cert_path=cert,
network=network, network=network,
grpc_host=endpoint, macaroon_filepath=auth_admin,
grpc_port=port,
) )
self.invoices_rpc = lnd_grpc.Client( self.invoices_rpc = lndgrpc.LNDClient(
lnd_dir=None, endpoint + ":" + port,
macaroon_path=auth_invoices, cert_filepath=cert,
tls_cert_path=cert,
network=network, network=network,
grpc_host=endpoint, macaroon_filepath=auth_invoices,
grpc_port=port, )
self.async_rpc = lndgrpc.AsyncLNDClient(
endpoint + ":" + port,
cert_filepath=cert,
network=network,
macaroon_filepath=auth_invoices,
) )
def create_invoice( def create_invoice(
self, amount: int, memo: Optional[str] = None, description_hash: Optional[bytes] = None self, amount: int, memo: Optional[str] = None, description_hash: Optional[bytes] = None
) -> InvoiceResponse: ) -> InvoiceResponse:
params: Dict = {"value": amount, "expiry": 600, "private": True} params: Dict = {"value": amount, "expiry": 600, "private": True}
if description_hash: if description_hash:
params["description_hash"] = description_hash # as bytes directly params["description_hash"] = description_hash # as bytes directly
else: else:
params["memo"] = memo or "" params["memo"] = memo or ""
resp = self.invoices_rpc.add_invoice(**params)
try:
req = ln.Invoice(**params)
resp = self.invoices_rpc._ln_stub.AddInvoice(req)
except Exception as exc:
error_message = str(exc)
return InvoiceResponse(False, None, None, error_message)
checking_id = stringify_checking_id(resp.r_hash) checking_id = stringify_checking_id(resp.r_hash)
payment_request = str(resp.payment_request) payment_request = str(resp.payment_request)
return InvoiceResponse(True, checking_id, payment_request, None) return InvoiceResponse(True, checking_id, payment_request, None)
def pay_invoice(self, bolt11: str) -> PaymentResponse: def pay_invoice(self, bolt11: str) -> PaymentResponse:
resp = self.admin_rpc.pay_invoice(payment_request=bolt11) resp = self.admin_rpc.send_payment(payment_request=bolt11)
if resp.payment_error: if resp.payment_error:
return PaymentResponse(False, "", 0, resp.payment_error) return PaymentResponse(False, "", 0, resp.payment_error)
checking_id = stringify_checking_id(resp.payment_hash) r_hash = hashlib.sha256(resp.payment_preimage).digest()
checking_id = stringify_checking_id(r_hash)
return PaymentResponse(True, checking_id, 0, None) return PaymentResponse(True, checking_id, 0, None)
def get_invoice_status(self, checking_id: str) -> PaymentStatus: def get_invoice_status(self, checking_id: str) -> PaymentStatus:
r_hash = parse_checking_id(checking_id) try:
for _response in self.invoices_rpc.subscribe_single_invoice(r_hash): r_hash = parse_checking_id(checking_id)
if _response.state == 1: if len(r_hash) != 32:
return PaymentStatus(True) raise binascii.Error
except binascii.Error:
# this may happen if we switch between backend wallets
# that use different checking_id formats
return PaymentStatus(None)
resp = self.invoices_rpc.lookup_invoice(r_hash.hex())
if resp.settled:
return PaymentStatus(True)
return PaymentStatus(None) return PaymentStatus(None)
@ -92,7 +114,9 @@ class LndWallet(Wallet):
return PaymentStatus(True) return PaymentStatus(True)
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]: async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
for paid in self.invoices_rpc.SubscribeInvoices(): async for inv in self.async_rpc._ln_stub.SubscribeInvoices(ln.InvoiceSubscription()):
print("PAID", paid) if not inv.settled:
checking_id = stringify_checking_id(paid.r_hash) continue
checking_id = stringify_checking_id(inv.r_hash)
yield checking_id yield checking_id

View File

@ -15,8 +15,12 @@ class LndRestWallet(Wallet):
endpoint = getenv("LND_REST_ENDPOINT") endpoint = getenv("LND_REST_ENDPOINT")
self.endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint self.endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint
self.auth_admin = {"Grpc-Metadata-macaroon": getenv("LND_REST_ADMIN_MACAROON")} self.auth_admin = {
self.auth_invoice = {"Grpc-Metadata-macaroon": getenv("LND_REST_INVOICE_MACAROON")} "Grpc-Metadata-macaroon": getenv("LND_ADMIN_MACAROON") or getenv("LND_REST_ADMIN_MACAROON"),
}
self.auth_invoice = {
"Grpc-Metadata-macaroon": getenv("LND_INVOICE_MACAROON") or getenv("LND_REST_INVOICE_MACAROON")
}
self.auth_cert = getenv("LND_REST_CERT") self.auth_cert = getenv("LND_REST_CERT")
def create_invoice( def create_invoice(
@ -111,17 +115,13 @@ class LndRestWallet(Wallet):
async with httpx.AsyncClient(timeout=None, headers=self.auth_admin, verify=self.auth_cert) as client: async with httpx.AsyncClient(timeout=None, headers=self.auth_admin, verify=self.auth_cert) as client:
async with client.stream("GET", url) as r: async with client.stream("GET", url) as r:
print("ok")
print(r)
print(r.is_error)
print("ok")
async for line in r.aiter_lines(): async for line in r.aiter_lines():
print("line", line)
try: try:
event = json.loads(line)["result"] inv = json.loads(line)["result"]
print(event) if not inv["settled"]:
continue
except: except:
continue continue
payment_hash = bolt11.decode(event["payment_request"]).payment_hash payment_hash = base64.b64decode(inv["r_hash"]).hex()
yield payment_hash yield payment_hash

View File

@ -1,3 +1,4 @@
import asyncio
from os import getenv from os import getenv
from typing import Optional, Dict, AsyncGenerator from typing import Optional, Dict, AsyncGenerator
from requests import post from requests import post
@ -78,4 +79,5 @@ class LntxbotWallet(Wallet):
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]: async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
print("lntxbot does not support paid invoices stream yet") print("lntxbot does not support paid invoices stream yet")
await asyncio.sleep(5)
yield "" yield ""