2022-08-09 11:49:39 +02:00
|
|
|
import asyncio
|
2020-04-02 13:03:10 +02:00
|
|
|
import base64
|
2020-10-03 22:27:55 +02:00
|
|
|
import hashlib
|
2023-01-20 11:44:07 +01:00
|
|
|
from os import environ
|
2022-07-16 14:23:03 +02:00
|
|
|
from typing import AsyncGenerator, Dict, Optional
|
2022-07-07 14:30:16 +02:00
|
|
|
|
2023-09-11 15:24:37 +02:00
|
|
|
import grpc
|
2022-07-07 14:30:16 +02:00
|
|
|
from loguru import logger
|
|
|
|
|
2023-09-11 15:24:37 +02:00
|
|
|
import lnbits.wallets.lnd_grpc_files.lightning_pb2 as ln
|
|
|
|
import lnbits.wallets.lnd_grpc_files.lightning_pb2_grpc as lnrpc
|
|
|
|
import lnbits.wallets.lnd_grpc_files.router_pb2 as router
|
|
|
|
import lnbits.wallets.lnd_grpc_files.router_pb2_grpc as routerrpc
|
2022-10-05 13:01:41 +02:00
|
|
|
from lnbits.settings import settings
|
|
|
|
|
2021-03-24 04:40:32 +01:00
|
|
|
from .base import (
|
|
|
|
InvoiceResponse,
|
|
|
|
PaymentResponse,
|
|
|
|
PaymentStatus,
|
2022-07-16 14:23:03 +02:00
|
|
|
StatusResponse,
|
2021-03-24 04:40:32 +01:00
|
|
|
Wallet,
|
|
|
|
)
|
2023-08-16 12:22:14 +02:00
|
|
|
from .macaroon import AESCipher, load_macaroon
|
|
|
|
|
2020-10-10 01:55:58 +02:00
|
|
|
|
2022-08-30 13:28:58 +02:00
|
|
|
def b64_to_bytes(checking_id: str) -> bytes:
|
2021-10-17 19:33:29 +02:00
|
|
|
return base64.b64decode(checking_id.replace("_", "/"))
|
2020-10-02 22:13:33 +02:00
|
|
|
|
|
|
|
|
2022-08-30 13:28:58 +02:00
|
|
|
def bytes_to_b64(r_hash: bytes) -> str:
|
2022-12-29 16:50:05 +01:00
|
|
|
return base64.b64encode(r_hash).decode().replace("/", "_")
|
2020-10-02 22:13:33 +02:00
|
|
|
|
|
|
|
|
2022-08-30 13:28:58 +02:00
|
|
|
def hex_to_b64(hex_str: str) -> str:
|
|
|
|
try:
|
|
|
|
return base64.b64encode(bytes.fromhex(hex_str)).decode()
|
|
|
|
except ValueError:
|
|
|
|
return ""
|
|
|
|
|
|
|
|
|
|
|
|
def hex_to_bytes(hex_str: str) -> bytes:
|
|
|
|
try:
|
|
|
|
return bytes.fromhex(hex_str)
|
2023-08-16 12:17:54 +02:00
|
|
|
except Exception:
|
2022-08-30 13:28:58 +02:00
|
|
|
return b""
|
|
|
|
|
|
|
|
|
|
|
|
def bytes_to_hex(b: bytes) -> str:
|
|
|
|
return b.hex()
|
|
|
|
|
|
|
|
|
2021-11-07 17:24:22 +01:00
|
|
|
# Due to updated ECDSA generated tls.cert we need to let gprc know that
|
|
|
|
# we need to use that cipher suite otherwise there will be a handhsake
|
|
|
|
# error when we communicate with the lnd rpc server.
|
|
|
|
environ["GRPC_SSL_CIPHER_SUITES"] = "HIGH+ECDSA"
|
|
|
|
|
|
|
|
|
2020-04-02 13:03:10 +02:00
|
|
|
class LndWallet(Wallet):
|
2020-04-02 08:44:03 +02:00
|
|
|
def __init__(self):
|
2022-10-05 13:01:41 +02:00
|
|
|
endpoint = settings.lnd_grpc_endpoint
|
2020-10-08 21:03:18 +02:00
|
|
|
|
2022-02-14 17:54:05 +01:00
|
|
|
macaroon = (
|
2022-10-05 13:01:41 +02:00
|
|
|
settings.lnd_grpc_macaroon
|
|
|
|
or settings.lnd_grpc_admin_macaroon
|
|
|
|
or settings.lnd_admin_macaroon
|
|
|
|
or settings.lnd_grpc_invoice_macaroon
|
|
|
|
or settings.lnd_invoice_macaroon
|
2020-10-08 21:03:18 +02:00
|
|
|
)
|
2022-03-16 07:20:15 +01:00
|
|
|
|
2022-10-05 13:01:41 +02:00
|
|
|
encrypted_macaroon = settings.lnd_grpc_macaroon_encrypted
|
2022-02-14 17:54:05 +01:00
|
|
|
if encrypted_macaroon:
|
2022-03-16 07:20:15 +01:00
|
|
|
macaroon = AESCipher(description="macaroon decryption").decrypt(
|
|
|
|
encrypted_macaroon
|
|
|
|
)
|
2021-11-07 17:24:22 +01:00
|
|
|
|
2023-02-02 13:57:36 +01:00
|
|
|
cert_path = settings.lnd_grpc_cert or settings.lnd_cert
|
|
|
|
if not endpoint or not macaroon or not cert_path or not settings.lnd_grpc_port:
|
|
|
|
raise Exception("cannot initialize lndrest")
|
|
|
|
|
|
|
|
self.endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint
|
|
|
|
self.port = int(settings.lnd_grpc_port)
|
|
|
|
self.cert_path = settings.lnd_grpc_cert or settings.lnd_cert
|
|
|
|
|
|
|
|
self.macaroon = load_macaroon(macaroon)
|
|
|
|
self.cert_path = cert_path
|
2021-11-07 17:24:22 +01:00
|
|
|
cert = open(self.cert_path, "rb").read()
|
|
|
|
creds = grpc.ssl_channel_credentials(cert)
|
|
|
|
auth_creds = grpc.metadata_call_credentials(self.metadata_callback)
|
|
|
|
composite_creds = grpc.composite_channel_credentials(creds, auth_creds)
|
|
|
|
channel = grpc.aio.secure_channel(
|
|
|
|
f"{self.endpoint}:{self.port}", composite_creds
|
2020-10-02 22:13:33 +02:00
|
|
|
)
|
2021-11-07 17:24:22 +01:00
|
|
|
self.rpc = lnrpc.LightningStub(channel)
|
2022-08-09 11:49:39 +02:00
|
|
|
self.routerpc = routerrpc.RouterStub(channel)
|
2021-11-07 17:24:22 +01:00
|
|
|
|
|
|
|
def metadata_callback(self, _, callback):
|
|
|
|
callback([("macaroon", self.macaroon)], None)
|
2020-04-02 13:43:23 +02:00
|
|
|
|
2021-03-24 05:01:09 +01:00
|
|
|
async def status(self) -> StatusResponse:
|
2020-10-13 03:25:55 +02:00
|
|
|
try:
|
2021-11-07 17:24:22 +01:00
|
|
|
resp = await self.rpc.ChannelBalance(ln.ChannelBalanceRequest())
|
2020-10-13 03:25:55 +02:00
|
|
|
except Exception as exc:
|
|
|
|
return StatusResponse(str(exc), 0)
|
|
|
|
|
|
|
|
return StatusResponse(None, resp.balance * 1000)
|
2020-04-02 08:44:03 +02:00
|
|
|
|
2021-03-24 05:01:09 +01:00
|
|
|
async def create_invoice(
|
2021-03-24 04:40:32 +01:00
|
|
|
self,
|
|
|
|
amount: int,
|
|
|
|
memo: Optional[str] = None,
|
|
|
|
description_hash: Optional[bytes] = None,
|
2022-08-13 14:29:04 +02:00
|
|
|
unhashed_description: Optional[bytes] = None,
|
2023-01-26 11:08:40 +01:00
|
|
|
**kwargs,
|
2020-10-02 22:13:33 +02:00
|
|
|
) -> InvoiceResponse:
|
2023-02-02 13:57:36 +01:00
|
|
|
data: Dict = {
|
|
|
|
"description_hash": b"",
|
|
|
|
"value": amount,
|
|
|
|
"private": True,
|
|
|
|
"memo": memo or "",
|
|
|
|
}
|
2023-01-26 11:08:40 +01:00
|
|
|
if kwargs.get("expiry"):
|
2023-02-02 13:57:36 +01:00
|
|
|
data["expiry"] = kwargs["expiry"]
|
2020-08-31 04:48:46 +02:00
|
|
|
if description_hash:
|
2023-02-02 13:57:36 +01:00
|
|
|
data["description_hash"] = description_hash
|
2022-08-13 14:29:04 +02:00
|
|
|
elif unhashed_description:
|
2023-02-02 13:57:36 +01:00
|
|
|
data["description_hash"] = hashlib.sha256(
|
2022-08-13 14:29:04 +02:00
|
|
|
unhashed_description
|
2022-08-09 11:49:39 +02:00
|
|
|
).digest() # as bytes directly
|
2020-10-03 22:27:55 +02:00
|
|
|
|
|
|
|
try:
|
2023-02-02 13:57:36 +01:00
|
|
|
req = ln.Invoice(**data)
|
2021-11-07 17:24:22 +01:00
|
|
|
resp = await self.rpc.AddInvoice(req)
|
2020-10-03 22:27:55 +02:00
|
|
|
except Exception as exc:
|
|
|
|
error_message = str(exc)
|
|
|
|
return InvoiceResponse(False, None, None, error_message)
|
2020-04-02 08:44:03 +02:00
|
|
|
|
2022-08-30 13:28:58 +02:00
|
|
|
checking_id = bytes_to_hex(resp.r_hash)
|
2020-10-02 22:13:33 +02:00
|
|
|
payment_request = str(resp.payment_request)
|
|
|
|
return InvoiceResponse(True, checking_id, payment_request, None)
|
2020-04-02 08:44:03 +02:00
|
|
|
|
2022-03-16 07:20:15 +01:00
|
|
|
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
|
2022-08-09 11:49:39 +02:00
|
|
|
# fee_limit_fixed = ln.FeeLimit(fixed=fee_limit_msat // 1000)
|
|
|
|
req = router.SendPaymentRequest(
|
|
|
|
payment_request=bolt11,
|
|
|
|
fee_limit_msat=fee_limit_msat,
|
|
|
|
timeout_seconds=30,
|
|
|
|
no_inflight_updates=True,
|
|
|
|
)
|
|
|
|
try:
|
|
|
|
resp = await self.routerpc.SendPaymentV2(req).read()
|
|
|
|
except Exception as exc:
|
2022-08-30 13:28:58 +02:00
|
|
|
return PaymentResponse(False, None, None, None, str(exc))
|
2022-08-09 11:49:39 +02:00
|
|
|
|
|
|
|
# PaymentStatus from https://github.com/lightningnetwork/lnd/blob/master/channeldb/payments.go#L178
|
|
|
|
statuses = {
|
|
|
|
0: None, # NON_EXISTENT
|
|
|
|
1: None, # IN_FLIGHT
|
|
|
|
2: True, # SUCCEEDED
|
|
|
|
3: False, # FAILED
|
|
|
|
}
|
|
|
|
|
2022-09-22 10:15:28 +02:00
|
|
|
failure_reasons = {
|
|
|
|
0: "No error given.",
|
|
|
|
1: "Payment timed out.",
|
|
|
|
2: "No route to destination.",
|
|
|
|
3: "Error.",
|
|
|
|
4: "Incorrect payment details.",
|
|
|
|
5: "Insufficient balance.",
|
|
|
|
}
|
|
|
|
|
2022-08-30 13:28:58 +02:00
|
|
|
fee_msat = None
|
|
|
|
preimage = None
|
2022-09-22 10:15:28 +02:00
|
|
|
error_message = None
|
|
|
|
checking_id = None
|
2022-08-30 13:28:58 +02:00
|
|
|
|
2023-01-21 16:02:35 +01:00
|
|
|
if statuses[resp.status] is True: # SUCCEEDED
|
2022-08-30 13:28:58 +02:00
|
|
|
fee_msat = -resp.htlcs[-1].route.total_fees_msat
|
2022-09-22 10:15:28 +02:00
|
|
|
preimage = resp.payment_preimage
|
|
|
|
checking_id = resp.payment_hash
|
2023-01-21 16:02:35 +01:00
|
|
|
elif statuses[resp.status] is False:
|
2022-09-22 10:15:28 +02:00
|
|
|
error_message = failure_reasons[resp.failure_reason]
|
2022-08-30 13:28:58 +02:00
|
|
|
|
2022-08-09 11:49:39 +02:00
|
|
|
return PaymentResponse(
|
2022-09-22 10:15:28 +02:00
|
|
|
statuses[resp.status], checking_id, fee_msat, preimage, error_message
|
2022-08-09 11:49:39 +02:00
|
|
|
)
|
2020-04-02 08:44:03 +02:00
|
|
|
|
2021-03-24 05:01:09 +01:00
|
|
|
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
|
2020-10-03 22:27:55 +02:00
|
|
|
try:
|
2022-08-30 13:28:58 +02:00
|
|
|
r_hash = hex_to_bytes(checking_id)
|
2020-10-03 22:27:55 +02:00
|
|
|
if len(r_hash) != 32:
|
2022-12-28 12:36:39 +01:00
|
|
|
raise ValueError
|
|
|
|
except ValueError:
|
2020-10-03 22:27:55 +02:00
|
|
|
# this may happen if we switch between backend wallets
|
|
|
|
# that use different checking_id formats
|
|
|
|
return PaymentStatus(None)
|
2022-08-30 13:28:58 +02:00
|
|
|
try:
|
|
|
|
resp = await self.rpc.LookupInvoice(ln.PaymentHash(r_hash=r_hash))
|
2023-09-11 15:24:37 +02:00
|
|
|
except grpc.RpcError:
|
2022-08-30 13:28:58 +02:00
|
|
|
return PaymentStatus(None)
|
2020-10-03 22:27:55 +02:00
|
|
|
if resp.settled:
|
|
|
|
return PaymentStatus(True)
|
2020-04-02 08:44:03 +02:00
|
|
|
|
2020-04-26 13:28:19 +02:00
|
|
|
return PaymentStatus(None)
|
2020-04-02 08:44:03 +02:00
|
|
|
|
2021-03-24 05:01:09 +01:00
|
|
|
async def get_payment_status(self, checking_id: str) -> PaymentStatus:
|
2022-08-09 11:49:39 +02:00
|
|
|
"""
|
|
|
|
This routine checks the payment status using routerpc.TrackPaymentV2.
|
|
|
|
"""
|
2021-11-07 17:24:22 +01:00
|
|
|
try:
|
2022-08-30 13:28:58 +02:00
|
|
|
r_hash = hex_to_bytes(checking_id)
|
2022-08-09 11:49:39 +02:00
|
|
|
if len(r_hash) != 32:
|
2022-12-28 12:36:39 +01:00
|
|
|
raise ValueError
|
|
|
|
except ValueError:
|
2022-08-09 11:49:39 +02:00
|
|
|
# this may happen if we switch between backend wallets
|
|
|
|
# that use different checking_id formats
|
|
|
|
return PaymentStatus(None)
|
2020-10-04 05:22:37 +02:00
|
|
|
|
2022-08-09 11:49:39 +02:00
|
|
|
resp = self.routerpc.TrackPaymentV2(
|
|
|
|
router.TrackPaymentRequest(payment_hash=r_hash)
|
2022-07-07 14:30:16 +02:00
|
|
|
)
|
2022-08-09 11:49:39 +02:00
|
|
|
|
2022-09-22 10:15:28 +02:00
|
|
|
# # HTLCAttempt.HTLCStatus:
|
|
|
|
# # https://github.com/lightningnetwork/lnd/blob/master/lnrpc/lightning.proto#L3641
|
|
|
|
# htlc_statuses = {
|
|
|
|
# 0: None, # IN_FLIGHT
|
|
|
|
# 1: True, # "SUCCEEDED"
|
|
|
|
# 2: False, # "FAILED"
|
|
|
|
# }
|
2022-08-09 11:49:39 +02:00
|
|
|
statuses = {
|
2022-09-22 10:15:28 +02:00
|
|
|
0: None, # NON_EXISTENT
|
|
|
|
1: None, # IN_FLIGHT
|
|
|
|
2: True, # SUCCEEDED
|
|
|
|
3: False, # FAILED
|
2022-08-09 11:49:39 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
try:
|
|
|
|
async for payment in resp:
|
2022-09-22 10:15:28 +02:00
|
|
|
if len(payment.htlcs) and statuses[payment.status]:
|
2022-08-30 13:28:58 +02:00
|
|
|
return PaymentStatus(
|
|
|
|
True,
|
|
|
|
-payment.htlcs[-1].route.total_fees_msat,
|
|
|
|
bytes_to_hex(payment.htlcs[-1].preimage),
|
|
|
|
)
|
2022-09-22 10:15:28 +02:00
|
|
|
return PaymentStatus(statuses[payment.status])
|
2023-08-16 12:17:54 +02:00
|
|
|
except Exception: # most likely the payment wasn't found
|
2022-08-09 11:49:39 +02:00
|
|
|
return PaymentStatus(None)
|
|
|
|
|
|
|
|
return PaymentStatus(None)
|
|
|
|
|
|
|
|
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
|
|
|
while True:
|
|
|
|
try:
|
2022-08-30 13:28:58 +02:00
|
|
|
request = ln.InvoiceSubscription()
|
2022-08-09 11:49:39 +02:00
|
|
|
async for i in self.rpc.SubscribeInvoices(request):
|
|
|
|
if not i.settled:
|
|
|
|
continue
|
|
|
|
|
2022-08-30 13:28:58 +02:00
|
|
|
checking_id = bytes_to_hex(i.r_hash)
|
2022-08-09 11:49:39 +02:00
|
|
|
yield checking_id
|
|
|
|
except Exception as exc:
|
|
|
|
logger.error(
|
2023-08-24 11:26:09 +02:00
|
|
|
f"lost connection to lnd invoices stream: '{exc}', "
|
|
|
|
"retrying in 5 seconds"
|
2022-08-09 11:49:39 +02:00
|
|
|
)
|
|
|
|
await asyncio.sleep(5)
|