invoice listeners support on lnd and other fixes around wallets/

This commit is contained in:
fiatjaf 2020-10-02 17:13:33 -03:00
parent 90c640b659
commit b3c69ad49c
12 changed files with 217 additions and 151 deletions

View File

@ -35,24 +35,21 @@ LNBITS_ADMIN_MACAROON=LNBITS_ADMIN_MACAROON
# LndWallet # LndWallet
LND_GRPC_ENDPOINT=127.0.0.1 LND_GRPC_ENDPOINT=127.0.0.1
LND_GRPC_PORT=11009 LND_GRPC_PORT=11009
LND_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_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_INVOICE_MACAROON="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/invoice.macaroon"
LND_READ_MACAROON="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/read.macaroon"
# LndRestWallet # LndRestWallet
LND_REST_ENDPOINT=https://localhost:8080/ LND_REST_ENDPOINT=https://localhost: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"
LND_REST_READ_MACAROON="HEXSTRING"
# LNPayWallet # LNPayWallet
LNPAY_API_ENDPOINT=https://lnpay.co/v1/ LNPAY_API_ENDPOINT=https://lnpay.co/v1/
LNPAY_API_KEY=LNPAY_API_KEY LNPAY_API_KEY=LNPAY_API_KEY
LNPAY_ADMIN_KEY=LNPAY_ADMIN_KEY LNPAY_ADMIN_KEY=LNPAY_ADMIN_KEY
LNPAY_INVOICE_KEY=LNPAY_INVOICE_KEY LNPAY_INVOICE_KEY=LNPAY_INVOICE_KEY
LNPAY_READ_KEY=LNPAY_READ_KEY
# LntxbotWallet # LntxbotWallet
LNTXBOT_API_ENDPOINT=https://lntxbot.bigsun.xyz/ LNTXBOT_API_ENDPOINT=https://lntxbot.bigsun.xyz/

View File

@ -6,6 +6,7 @@ from .crud import get_pay_link_by_invoice, mark_webhook_sent
async def on_invoice_paid(payment: Payment) -> None: async def on_invoice_paid(payment: Payment) -> None:
print(payment)
islnurlp = "lnurlp" == payment.extra.get("tag") islnurlp = "lnurlp" == payment.extra.get("tag")
if islnurlp: if islnurlp:
pay_link = get_pay_link_by_invoice(payment.payment_hash) pay_link = get_pay_link_by_invoice(payment.payment_hash)

View File

@ -1,5 +1,5 @@
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import NamedTuple, Optional from typing import NamedTuple, Optional, AsyncGenerator
class InvoiceResponse(NamedTuple): class InvoiceResponse(NamedTuple):
@ -43,6 +43,15 @@ class Wallet(ABC):
def get_payment_status(self, checking_id: str) -> PaymentStatus: def get_payment_status(self, checking_id: str) -> PaymentStatus:
pass pass
@abstractmethod
def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
"""
this is an async function, but here it is noted without the 'async'
prefix because mypy has a bug identifying the signature of abstract
methods.
"""
pass
class Unsupported(Exception): class Unsupported(Exception):
pass pass

View File

@ -1,12 +1,12 @@
try: try:
from lightning import LightningRpc # type: ignore from lightning import LightningRpc, RpcError # type: ignore
except ImportError: # pragma: nocover except ImportError: # pragma: nocover
LightningRpc = None LightningRpc = None
import random import random
from os import getenv from os import getenv
from typing import Optional from typing import Optional, AsyncGenerator
from .base import InvoiceResponse, PaymentResponse, PaymentStatus, Wallet, Unsupported from .base import InvoiceResponse, PaymentResponse, PaymentStatus, Wallet, Unsupported
@ -15,26 +15,52 @@ 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.l1 = LightningRpc(getenv("CLIGHTNING_RPC")) self.ln = LightningRpc(getenv("CLIGHTNING_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 last payindex so we can listen from that point on
self.last_pay_index = 0
invoices = self.ln.listinvoices()
if len(invoices["invoices"]):
self.last_pay_index = invoices["invoices"][-1]["pay_index"]
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:
if description_hash:
raise Unsupported("description_hash")
label = "lbl{}".format(random.random()) label = "lbl{}".format(random.random())
r = self.l1.invoice(amount * 1000, label, memo, exposeprivatechannels=True) msat = amount * 1000
ok, checking_id, payment_request, error_message = True, r["payment_hash"], r["bolt11"], None
return InvoiceResponse(ok, checking_id, payment_request, error_message) try:
if description_hash:
if not self.supports_description_hash:
raise Unsupported("description_hash")
r = self.ln.call("invoicewithdescriptionhash", [msat, label, memo])
return InvoiceResponse(True, label, r["bolt11"], "")
else:
r = self.ln.invoice(msat, label, memo, exposeprivatechannels=True)
return InvoiceResponse(True, label, r["bolt11"], "")
except RpcError as exc:
error_message = f"lightningd '{exc.method}' failed with '{exc.error}'."
return InvoiceResponse(False, label, None, error_message)
def pay_invoice(self, bolt11: str) -> PaymentResponse: def pay_invoice(self, bolt11: str) -> PaymentResponse:
r = self.l1.pay(bolt11) r = self.ln.pay(bolt11)
ok, checking_id, fee_msat, error_message = True, r["payment_hash"], r["msatoshi_sent"] - r["msatoshi"], None ok, checking_id, fee_msat, error_message = True, r["payment_hash"], r["msatoshi_sent"] - r["msatoshi"], None
return PaymentResponse(ok, checking_id, fee_msat, error_message) 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.l1.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:
@ -42,7 +68,7 @@ class CLightningWallet(Wallet):
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:
r = self.l1.listpays(payment_hash=checking_id) r = self.ln.listpays(payment_hash=checking_id)
if not r["pays"]: if not r["pays"]:
return PaymentStatus(False) return PaymentStatus(False)
if r["pays"][0]["payment_hash"] == checking_id: if r["pays"][0]["payment_hash"] == checking_id:
@ -53,3 +79,9 @@ class CLightningWallet(Wallet):
return PaymentStatus(False) return PaymentStatus(False)
return PaymentStatus(None) return PaymentStatus(None)
raise KeyError("supplied an invalid checking_id") raise KeyError("supplied an invalid checking_id")
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
while True:
paid = self.ln.waitanyinvoice(self.last_pay_index)
self.last_pay_index = paid["pay_index"]
yield paid["label"]

View File

@ -1,5 +1,5 @@
from os import getenv from os import getenv
from typing import Optional, Dict from typing import Optional, Dict, AsyncGenerator
from requests import get, post from requests import get, post
from .base import InvoiceResponse, PaymentResponse, PaymentStatus, Wallet from .base import InvoiceResponse, PaymentResponse, PaymentStatus, Wallet
@ -64,3 +64,7 @@ class LNbitsWallet(Wallet):
return PaymentStatus(None) return PaymentStatus(None)
return PaymentStatus(r.json()["paid"]) return PaymentStatus(r.json()["paid"])
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
print("lnbits does not support paid invoices stream yet")
yield ""

View File

@ -5,88 +5,94 @@ except ImportError: # pragma: nocover
import base64 import base64
from os import getenv from os import getenv
from typing import Optional, Dict from typing import Optional, Dict, AsyncGenerator
from .base import InvoiceResponse, PaymentResponse, PaymentStatus, Wallet from .base import InvoiceResponse, PaymentResponse, PaymentStatus, Wallet
def parse_checking_id(checking_id: str) -> bytes:
return base64.b64decode(
checking_id.replace("_", "/"),
)
def stringify_checking_id(r_hash: bytes) -> str:
return (
base64.b64encode(
r_hash,
)
.decode("utf-8")
.replace("/", "_")
)
class LndWallet(Wallet): class LndWallet(Wallet):
def __init__(self): def __init__(self):
if lnd_grpc is None: # pragma: nocover if lnd_grpc is None: # pragma: nocover
raise ImportError("The `lnd-grpc` library must be installed to use `LndWallet`.") raise ImportError("The `lnd-grpc` library must be installed to use `LndWallet`.")
endpoint = getenv("LND_GRPC_ENDPOINT") endpoint = getenv("LND_GRPC_ENDPOINT")
self.endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint
self.port = getenv("LND_GRPC_PORT") port = getenv("LND_GRPC_PORT")
self.auth_admin = getenv("LND_ADMIN_MACAROON") cert = getenv("LND_GRPC_CERT") or getenv("LND_CERT")
self.auth_invoice = getenv("LND_INVOICE_MACAROON") auth_admin = getenv("LND_ADMIN_MACAROON")
self.auth_read = getenv("LND_READ_MACAROON") auth_invoices = getenv("LND_INVOICE_MACAROON")
self.auth_cert = getenv("LND_CERT") network = getenv("LND_GRPC_NETWORK", "mainnet")
self.admin_rpc = lnd_grpc.Client(
lnd_dir=None,
macaroon_path=auth_admin,
tls_cert_path=cert,
network=network,
grpc_host=endpoint,
grpc_port=port,
)
self.invoices_rpc = lnd_grpc.Client(
lnd_dir=None,
macaroon_path=auth_invoices,
tls_cert_path=cert,
network=network,
grpc_host=endpoint,
grpc_port=port,
)
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:
lnd_rpc = lnd_grpc.Client(
lnd_dir=None,
macaroon_path=self.auth_invoice,
tls_cert_path=self.auth_cert,
network="mainnet",
grpc_host=self.endpoint,
grpc_port=self.port,
)
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 ""
lndResponse = lnd_rpc.add_invoice(**params) resp = self.invoices_rpc.add_invoice(**params)
decoded_hash = base64.b64encode(lndResponse.r_hash).decode("utf-8").replace("/", "_")
ok, checking_id, payment_request, error_message = True, decoded_hash, str(lndResponse.payment_request), None checking_id = stringify_checking_id(resp.r_hash)
return InvoiceResponse(ok, checking_id, payment_request, error_message) payment_request = str(resp.payment_request)
return InvoiceResponse(True, checking_id, payment_request, None)
def pay_invoice(self, bolt11: str) -> PaymentResponse: def pay_invoice(self, bolt11: str) -> PaymentResponse:
lnd_rpc = lnd_grpc.Client( resp = self.admin_rpc.pay_invoice(payment_request=bolt11)
lnd_dir=None,
macaroon_path=self.auth_admin,
tls_cert_path=self.auth_cert,
network="mainnet",
grpc_host=self.endpoint,
grpc_port=self.port,
)
payinvoice = lnd_rpc.pay_invoice( if resp.payment_error:
payment_request=bolt11, return PaymentResponse(False, "", 0, resp.payment_error)
)
ok, checking_id, fee_msat, error_message = True, None, 0, None checking_id = stringify_checking_id(resp.payment_hash)
return PaymentResponse(True, checking_id, 0, None)
if payinvoice.payment_error:
ok, error_message = False, payinvoice.payment_error
else:
checking_id = base64.b64encode(payinvoice.payment_hash).decode("utf-8").replace("/", "_")
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_hash = parse_checking_id(checking_id)
check_id = base64.b64decode(checking_id.replace("_", "/")) for _response in self.invoices_rpc.subscribe_single_invoice(r_hash):
print(check_id)
lnd_rpc = lnd_grpc.Client(
lnd_dir=None,
macaroon_path=self.auth_invoice,
tls_cert_path=self.auth_cert,
network="mainnet",
grpc_host=self.endpoint,
grpc_port=self.port,
)
for _response in lnd_rpc.subscribe_single_invoice(check_id):
if _response.state == 1: if _response.state == 1:
return PaymentStatus(True) return PaymentStatus(True)
return PaymentStatus(None) return PaymentStatus(None)
def get_payment_status(self, checking_id: str) -> PaymentStatus: def get_payment_status(self, checking_id: str) -> PaymentStatus:
return PaymentStatus(True) return PaymentStatus(True)
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
for paid in self.invoices_rpc.SubscribeInvoices():
print("PAID", paid)
checking_id = stringify_checking_id(paid.r_hash)
yield checking_id

View File

@ -1,7 +1,10 @@
from os import getenv import httpx
from typing import Optional, Dict import json
import base64 import base64
from requests import get, post from os import getenv
from typing import Optional, Dict, AsyncGenerator
from lnbits import bolt11
from .base import InvoiceResponse, PaymentResponse, PaymentStatus, Wallet from .base import InvoiceResponse, PaymentResponse, PaymentStatus, Wallet
@ -12,10 +15,8 @@ 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
print(self.endpoint)
self.auth_admin = {"Grpc-Metadata-macaroon": getenv("LND_REST_ADMIN_MACAROON")} self.auth_admin = {"Grpc-Metadata-macaroon": getenv("LND_REST_ADMIN_MACAROON")}
self.auth_invoice = {"Grpc-Metadata-macaroon": getenv("LND_REST_INVOICE_MACAROON")} self.auth_invoice = {"Grpc-Metadata-macaroon": getenv("LND_REST_INVOICE_MACAROON")}
self.auth_read = {"Grpc-Metadata-macaroon": getenv("LND_REST_READ_MACAROON")}
self.auth_cert = getenv("LND_REST_CERT") self.auth_cert = getenv("LND_REST_CERT")
def create_invoice( def create_invoice(
@ -30,84 +31,97 @@ class LndRestWallet(Wallet):
else: else:
data["memo"] = memo or "" data["memo"] = memo or ""
r = post( r = httpx.post(
url=f"{self.endpoint}/v1/invoices", url=f"{self.endpoint}/v1/invoices",
headers=self.auth_invoice, headers=self.auth_invoice,
verify=self.auth_cert, verify=self.auth_cert,
json=data, json=data,
) )
ok, checking_id, payment_request, error_message = r.ok, None, None, None if r.is_error:
error_message = r.text
try:
error_message = r.json()["error"]
except Exception:
pass
return InvoiceResponse(False, None, None, error_message)
if r.ok: data = r.json()
data = r.json() payment_request = data["payment_request"]
payment_request = data["payment_request"] payment_hash = base64.b64decode(data["r_hash"]).hex()
checking_id = payment_hash
r = get( return InvoiceResponse(True, checking_id, payment_request, None)
url=f"{self.endpoint}/v1/payreq/{payment_request}",
headers=self.auth_read,
verify=self.auth_cert,
)
print(r)
if r.ok:
checking_id = r.json()["payment_hash"].replace("/", "_")
print(checking_id)
error_message = None
ok = True
return InvoiceResponse(ok, checking_id, payment_request, error_message)
def pay_invoice(self, bolt11: str) -> PaymentResponse: def pay_invoice(self, bolt11: str) -> PaymentResponse:
r = post( r = httpx.post(
url=f"{self.endpoint}/v1/channels/transactions", url=f"{self.endpoint}/v1/channels/transactions",
headers=self.auth_admin, headers=self.auth_admin,
verify=self.auth_cert, verify=self.auth_cert,
json={"payment_request": bolt11}, json={"payment_request": bolt11},
) )
ok, checking_id, fee_msat, error_message = r.ok, None, 0, None
r = get(
url=f"{self.endpoint}/v1/payreq/{bolt11}",
headers=self.auth_admin,
verify=self.auth_cert,
)
if r.ok: if r.is_error:
checking_id = r.json()["payment_hash"] error_message = r.text
else: try:
error_message = r.json()["error"] error_message = r.json()["error"]
except:
pass
return PaymentResponse(False, None, 0, error_message)
return PaymentResponse(ok, checking_id, fee_msat, error_message) payment_hash = r.json()["payment_hash"]
checking_id = payment_hash
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:
checking_id = checking_id.replace("_", "/") checking_id = checking_id.replace("_", "/")
print(checking_id) r = httpx.get(
r = get(
url=f"{self.endpoint}/v1/invoice/{checking_id}", url=f"{self.endpoint}/v1/invoice/{checking_id}",
headers=self.auth_invoice, headers=self.auth_invoice,
verify=self.auth_cert, verify=self.auth_cert,
) )
print(r.json()["settled"])
if not r or r.json()["settled"] == False: if not r or r.json()["settled"] == False:
return PaymentStatus(None) return PaymentStatus(None)
return PaymentStatus(r.json()["settled"]) return PaymentStatus(r.json()["settled"])
def get_payment_status(self, checking_id: str) -> PaymentStatus: def get_payment_status(self, checking_id: str) -> PaymentStatus:
r = get( r = httpx.get(
url=f"{self.endpoint}/v1/payments", url=f"{self.endpoint}/v1/payments",
headers=self.auth_admin, headers=self.auth_admin,
verify=self.auth_cert, verify=self.auth_cert,
params={"include_incomplete": "True", "max_payments": "20"}, params={"include_incomplete": "True", "max_payments": "20"},
) )
if not r.ok: if r.is_error:
return PaymentStatus(None) return PaymentStatus(None)
payments = [p for p in r.json()["payments"] if p["payment_hash"] == checking_id] payments = [p for p in r.json()["payments"] if p["payment_hash"] == checking_id]
print(checking_id)
payment = payments[0] if payments else None payment = payments[0] if payments else None
# check payment.status: https://api.lightning.community/rest/index.html?python#peersynctype # check payment.status:
# https://api.lightning.community/rest/index.html?python#peersynctype
statuses = {"UNKNOWN": None, "IN_FLIGHT": None, "SUCCEEDED": True, "FAILED": False} statuses = {"UNKNOWN": None, "IN_FLIGHT": None, "SUCCEEDED": True, "FAILED": False}
return PaymentStatus(statuses[payment["status"]]) return PaymentStatus(statuses[payment["status"]])
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
url = self.endpoint + "/v1/invoices/subscribe"
async with httpx.AsyncClient(timeout=None, headers=self.auth_admin, verify=self.auth_cert) as client:
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():
print("line", line)
try:
event = json.loads(line)["result"]
print(event)
except:
continue
payment_hash = bolt11.decode(event["payment_request"]).payment_hash
yield payment_hash

View File

@ -4,7 +4,6 @@ import httpx
from os import getenv from os import getenv
from http import HTTPStatus from http import HTTPStatus
from typing import Optional, Dict, AsyncGenerator from typing import Optional, Dict, AsyncGenerator
from requests import get, post
from quart import request from quart import request
from .base import InvoiceResponse, PaymentResponse, PaymentStatus, Wallet from .base import InvoiceResponse, PaymentResponse, PaymentStatus, Wallet
@ -14,11 +13,9 @@ class LNPayWallet(Wallet):
"""https://docs.lnpay.co/""" """https://docs.lnpay.co/"""
def __init__(self): def __init__(self):
endpoint = getenv("LNPAY_API_ENDPOINT") endpoint = getenv("LNPAY_API_ENDPOINT", "https://lnpay.co/v1")
self.endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint self.endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint
self.auth_admin = getenv("LNPAY_ADMIN_KEY") self.auth_admin = getenv("LNPAY_ADMIN_KEY")
self.auth_invoice = getenv("LNPAY_INVOICE_KEY")
self.auth_read = getenv("LNPAY_READ_KEY")
self.auth_api = {"X-Api-Key": getenv("LNPAY_API_KEY")} self.auth_api = {"X-Api-Key": getenv("LNPAY_API_KEY")}
def create_invoice( def create_invoice(
@ -33,8 +30,8 @@ class LNPayWallet(Wallet):
else: else:
data["memo"] = memo or "" data["memo"] = memo or ""
r = post( r = httpx.post(
url=f"{self.endpoint}/user/wallet/{self.auth_invoice}/invoice", url=f"{self.endpoint}/user/wallet/{self.auth_admin}/invoice",
headers=self.auth_api, headers=self.auth_api,
json=data, json=data,
) )
@ -52,7 +49,7 @@ class LNPayWallet(Wallet):
return InvoiceResponse(ok, checking_id, payment_request, error_message) return InvoiceResponse(ok, checking_id, payment_request, error_message)
def pay_invoice(self, bolt11: str) -> PaymentResponse: def pay_invoice(self, bolt11: str) -> PaymentResponse:
r = post( r = httpx.post(
url=f"{self.endpoint}/user/wallet/{self.auth_admin}/withdraw", url=f"{self.endpoint}/user/wallet/{self.auth_admin}/withdraw",
headers=self.auth_api, headers=self.auth_api,
json={"payment_request": bolt11}, json={"payment_request": bolt11},
@ -68,12 +65,12 @@ class LNPayWallet(Wallet):
return self.get_payment_status(checking_id) return self.get_payment_status(checking_id)
def get_payment_status(self, checking_id: str) -> PaymentStatus: def get_payment_status(self, checking_id: str) -> PaymentStatus:
r = get( r = httpx.get(
url=f"{self.endpoint}/user/lntx/{checking_id}?fields=settled", url=f"{self.endpoint}/user/lntx/{checking_id}?fields=settled",
headers=self.auth_api, headers=self.auth_api,
) )
if not r.ok: if r.is_error:
return PaymentStatus(None) return PaymentStatus(None)
statuses = {0: None, 1: True, -1: False} statuses = {0: None, 1: True, -1: False}
@ -82,8 +79,7 @@ class LNPayWallet(Wallet):
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]: async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
self.queue: asyncio.Queue = asyncio.Queue() self.queue: asyncio.Queue = asyncio.Queue()
while True: while True:
item = await self.queue.get() yield await self.queue.get()
yield item
self.queue.task_done() self.queue.task_done()
async def webhook_listener(self): async def webhook_listener(self):

View File

@ -1,5 +1,5 @@
from os import getenv from os import getenv
from typing import Optional, Dict from typing import Optional, Dict, AsyncGenerator
from requests import post from requests import post
from .base import InvoiceResponse, PaymentResponse, PaymentStatus, Wallet from .base import InvoiceResponse, PaymentResponse, PaymentStatus, Wallet
@ -75,3 +75,7 @@ class LntxbotWallet(Wallet):
statuses = {"complete": True, "failed": False, "pending": None, "unknown": None} statuses = {"complete": True, "failed": False, "pending": None, "unknown": None}
return PaymentStatus(statuses[r.json().get("status", "unknown")]) return PaymentStatus(statuses[r.json().get("status", "unknown")])
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
print("lntxbot does not support paid invoices stream yet")
yield ""

View File

@ -1,10 +1,10 @@
import json import json
import asyncio import asyncio
import hmac import hmac
import httpx
from http import HTTPStatus from http import HTTPStatus
from os import getenv from os import getenv
from typing import Optional, AsyncGenerator from typing import Optional, AsyncGenerator
from requests import get, post
from quart import request, url_for from quart import request, url_for
from .base import InvoiceResponse, PaymentResponse, PaymentStatus, Wallet, Unsupported from .base import InvoiceResponse, PaymentResponse, PaymentStatus, Wallet, Unsupported
@ -25,8 +25,8 @@ class OpenNodeWallet(Wallet):
if description_hash: if description_hash:
raise Unsupported("description_hash") raise Unsupported("description_hash")
r = post( r = httpx.post(
url=f"{self.endpoint}/v1/charges", f"{self.endpoint}/v1/charges",
headers=self.auth_invoice, headers=self.auth_invoice,
json={ json={
"amount": amount, "amount": amount,
@ -34,42 +34,43 @@ class OpenNodeWallet(Wallet):
"callback_url": url_for("webhook_listener", _external=True), "callback_url": url_for("webhook_listener", _external=True),
}, },
) )
ok, checking_id, payment_request, error_message = r.ok, None, None, None
if r.ok: if r.is_error:
data = r.json()["data"]
checking_id = data["id"]
payment_request = data["lightning_invoice"]["payreq"]
else:
error_message = r.json()["message"] error_message = r.json()["message"]
return InvoiceResponse(False, None, None, error_message)
return InvoiceResponse(ok, checking_id, payment_request, error_message) data = r.json()["data"]
checking_id = data["id"]
payment_request = data["lightning_invoice"]["payreq"]
return InvoiceResponse(True, checking_id, payment_request, error_message)
def pay_invoice(self, bolt11: str) -> PaymentResponse: def pay_invoice(self, bolt11: str) -> PaymentResponse:
r = post(url=f"{self.endpoint}/v2/withdrawals", headers=self.auth_admin, json={"type": "ln", "address": bolt11}) r = httpx.post(
ok, checking_id, fee_msat, error_message = r.ok, None, 0, None f"{self.endpoint}/v2/withdrawals", headers=self.auth_admin, json={"type": "ln", "address": bolt11}
)
if r.ok: if r.is_error:
data = r.json()["data"]
checking_id, fee_msat = data["id"], data["fee"] * 1000
else:
error_message = r.json()["message"] error_message = r.json()["message"]
return PaymentResponse(False, None, 0, error_message)
return PaymentResponse(ok, checking_id, fee_msat, error_message) data = r.json()["data"]
checking_id, fee_msat = data["id"]
fee_msat = data["fee"] * 1000
return PaymentResponse(True, 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 = get(url=f"{self.endpoint}/v1/charge/{checking_id}", headers=self.auth_invoice) r = httpx.get(f"{self.endpoint}/v1/charge/{checking_id}", headers=self.auth_invoice)
if not r.ok: if r.is_error:
return PaymentStatus(None) return PaymentStatus(None)
statuses = {"processing": None, "paid": True, "unpaid": False} statuses = {"processing": None, "paid": True, "unpaid": False}
return PaymentStatus(statuses[r.json()["data"]["status"]]) return PaymentStatus(statuses[r.json()["data"]["status"]])
def get_payment_status(self, checking_id: str) -> PaymentStatus: def get_payment_status(self, checking_id: str) -> PaymentStatus:
r = get(url=f"{self.endpoint}/v1/withdrawal/{checking_id}", headers=self.auth_admin) r = httpx.get(f"{self.endpoint}/v1/withdrawal/{checking_id}", headers=self.auth_admin)
if not r.ok: if r.is_error:
return PaymentStatus(None) return PaymentStatus(None)
statuses = {"initial": None, "pending": None, "confirmed": True, "error": False, "failed": False} statuses = {"initial": None, "pending": None, "confirmed": True, "error": False, "failed": False}
@ -78,8 +79,7 @@ class OpenNodeWallet(Wallet):
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]: async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
self.queue: asyncio.Queue = asyncio.Queue() self.queue: asyncio.Queue = asyncio.Queue()
while True: while True:
item = await self.queue.get() yield await self.queue.get()
yield item
self.queue.task_done() self.queue.task_done()
async def webhook_listener(self): async def webhook_listener(self):

View File

@ -34,7 +34,7 @@ class SparkWallet(Wallet):
data = r.json() data = r.json()
except: except:
raise UnknownError(r.text) raise UnknownError(r.text)
if not r.ok: if r.is_error:
raise SparkError(data["message"]) raise SparkError(data["message"])
return data return data
@ -96,7 +96,7 @@ class SparkWallet(Wallet):
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]: async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
url = self.url + "/stream?access-key=" + self.token url = self.url + "/stream?access-key=" + self.token
async with httpx.AsyncClient() as client: async with httpx.AsyncClient(timeout=None) as client:
async with client.stream("GET", url) as r: async with client.stream("GET", url) as r:
async for line in r.aiter_lines(): async for line in r.aiter_lines():
if line.startswith("data:"): if line.startswith("data:"):

View File

@ -1,4 +1,4 @@
from typing import Optional from typing import Optional, AsyncGenerator
from .base import InvoiceResponse, PaymentResponse, PaymentStatus, Wallet, Unsupported from .base import InvoiceResponse, PaymentResponse, PaymentStatus, Wallet, Unsupported
@ -17,3 +17,6 @@ class VoidWallet(Wallet):
def get_payment_status(self, checking_id: str) -> PaymentStatus: def get_payment_status(self, checking_id: str) -> PaymentStatus:
raise Unsupported("") raise Unsupported("")
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
yield ""