Browse Source

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

atmext
fiatjaf 4 years ago
parent
commit
e74cf33f90
  1. 6
      .env.example
  2. 2
      docs/devs/installation.md
  3. 2
      docs/guide/installation.md
  4. 2
      docs/guide/wallets.md
  5. 40
      lnbits/wallets/clightning.py
  6. 2
      lnbits/wallets/lnbits.py
  7. 80
      lnbits/wallets/lndgrpc.py
  8. 20
      lnbits/wallets/lndrest.py
  9. 2
      lnbits/wallets/lntxbot.py

6
.env.example

@ -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"

2
docs/devs/installation.md

@ -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.

2
docs/guide/installation.md

@ -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
``` ```

2
docs/guide/wallets.md

@ -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

40
lnbits/wallets/clightning.py

@ -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

2
lnbits/wallets/lnbits.py

@ -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 ""

80
lnbits/wallets/lndgrpc.py

@ -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

20
lnbits/wallets/lndrest.py

@ -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

2
lnbits/wallets/lntxbot.py

@ -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 ""

Loading…
Cancel
Save