invoice listeners support on lnd and other fixes around wallets/
This commit is contained in:
parent
90c640b659
commit
b3c69ad49c
|
@ -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/
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"]
|
||||||
|
|
|
@ -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 ""
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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 ""
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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:"):
|
||||||
|
|
|
@ -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 ""
|
||||||
|
|
Loading…
Reference in New Issue
Block a user