diff --git a/lnbits/core/services.py b/lnbits/core/services.py index d5924c0..f36ee54 100644 --- a/lnbits/core/services.py +++ b/lnbits/core/services.py @@ -1,15 +1,18 @@ from typing import Optional, Tuple -from lnbits.bolt11 import decode as bolt11_decode # type: ignore +from lnbits.bolt11 import decode as bolt11_decode # type: ignore from lnbits.helpers import urlsafe_short_hash from lnbits.settings import WALLET from .crud import get_wallet, create_payment, delete_payment -def create_invoice(*, wallet_id: str, amount: int, memo: str) -> Tuple[str, str]: +def create_invoice(*, wallet_id: str, amount: int, memo: str, description_hash: bytes) -> Tuple[str, str]: + try: - ok, checking_id, payment_request, error_message = WALLET.create_invoice(amount=amount, memo=memo) + ok, checking_id, payment_request, error_message = WALLET.create_invoice( + amount=amount, memo=memo, description_hash=description_hash + ) except Exception as e: ok, error_message = False, str(e) @@ -35,11 +38,7 @@ def pay_invoice(*, wallet_id: str, bolt11: str, max_sat: Optional[int] = None) - fee_reserve = max(1000, int(invoice.amount_msat * 0.01)) create_payment( - wallet_id=wallet_id, - checking_id=temp_id, - amount=-invoice.amount_msat, - fee=-fee_reserve, - memo=temp_id, + wallet_id=wallet_id, checking_id=temp_id, amount=-invoice.amount_msat, fee=-fee_reserve, memo=temp_id, ) wallet = get_wallet(wallet_id) diff --git a/lnbits/core/views/api.py b/lnbits/core/views/api.py index b6c47b0..ae2b842 100644 --- a/lnbits/core/views/api.py +++ b/lnbits/core/views/api.py @@ -1,5 +1,6 @@ from flask import g, jsonify, request from http import HTTPStatus +from binascii import unhexlify from lnbits.core import core_app from lnbits.decorators import api_check_wallet_key, api_validate_post_request @@ -27,13 +28,21 @@ def api_payments(): @api_validate_post_request( schema={ "amount": {"type": "integer", "min": 1, "required": True}, - "memo": {"type": "string", "empty": False, "required": True}, + "memo": {"type": "string", "empty": False, "required": False}, + "description_hash": {"type": "string", "empty": False, "required": False}, } ) def api_payments_create_invoice(): + if "description_hash" in g.data: + description_hash = unhexlify(g.data["description_hash"]) + memo = "" + else: + description_hash = b"" + memo = g.data["memo"] + try: checking_id, payment_request = create_invoice( - wallet_id=g.wallet.id, amount=g.data["amount"], memo=g.data["memo"] + wallet_id=g.wallet.id, amount=g.data["amount"], memo=memo, description_hash=description_hash ) except Exception as e: return jsonify({"message": str(e)}), HTTPStatus.INTERNAL_SERVER_ERROR diff --git a/lnbits/wallets/base.py b/lnbits/wallets/base.py index ced0013..2547d58 100644 --- a/lnbits/wallets/base.py +++ b/lnbits/wallets/base.py @@ -26,7 +26,7 @@ class PaymentStatus(NamedTuple): class Wallet(ABC): @abstractmethod - def create_invoice(self, amount: int, memo: str = "") -> InvoiceResponse: + def create_invoice(self, amount: int, memo: str = "", description_hash: bytes = b"") -> InvoiceResponse: pass @abstractmethod @@ -40,3 +40,7 @@ class Wallet(ABC): @abstractmethod def get_payment_status(self, checking_id: str) -> PaymentStatus: pass + + +class Unsupported(Exception): + pass diff --git a/lnbits/wallets/clightning.py b/lnbits/wallets/clightning.py index ae4486d..051a4ff 100644 --- a/lnbits/wallets/clightning.py +++ b/lnbits/wallets/clightning.py @@ -7,20 +7,22 @@ import random from os import getenv -from .base import InvoiceResponse, PaymentResponse, PaymentStatus, Wallet +from .base import InvoiceResponse, PaymentResponse, PaymentStatus, Wallet, Unsupported class CLightningWallet(Wallet): - def __init__(self): if LightningRpc is None: # pragma: nocover raise ImportError("The `pylightning` library must be installed to use `CLightningWallet`.") self.l1 = LightningRpc(getenv("CLIGHTNING_RPC")) - def create_invoice(self, amount: int, memo: str = "") -> InvoiceResponse: + def create_invoice(self, amount: int, memo: str = "", description_hash: bytes = b"") -> InvoiceResponse: + if description_hash: + raise Unsupported("description_hash") + label = "lbl{}".format(random.random()) - r = self.l1.invoice(amount*1000, label, memo, exposeprivatechannels=True) + r = self.l1.invoice(amount * 1000, label, memo, exposeprivatechannels=True) ok, checking_id, payment_request, error_message = True, r["payment_hash"], r["bolt11"], None return InvoiceResponse(ok, checking_id, payment_request, error_message) @@ -31,7 +33,7 @@ class CLightningWallet(Wallet): def get_invoice_status(self, checking_id: str) -> PaymentStatus: r = self.l1.listinvoices(checking_id) - if r['invoices'][0]['status'] == 'unpaid': + if r["invoices"][0]["status"] == "unpaid": return PaymentStatus(False) return PaymentStatus(True) diff --git a/lnbits/wallets/lnbits.py b/lnbits/wallets/lnbits.py index c3e0a0d..2da71f5 100644 --- a/lnbits/wallets/lnbits.py +++ b/lnbits/wallets/lnbits.py @@ -1,5 +1,6 @@ from os import getenv from requests import get, post +from binascii import hexlify from .base import InvoiceResponse, PaymentResponse, PaymentStatus, Wallet @@ -12,11 +13,16 @@ class LNbitsWallet(Wallet): self.auth_admin = {"X-Api-Key": getenv("LNBITS_ADMIN_KEY")} self.auth_invoice = {"X-Api-Key": getenv("LNBITS_INVOICE_KEY")} - def create_invoice(self, amount: int, memo: str = "") -> InvoiceResponse: + def create_invoice(self, amount: int, memo: str = "", description_hash: bytes = b"") -> InvoiceResponse: r = post( url=f"{self.endpoint}/api/v1/payments", headers=self.auth_invoice, - json={"out": False, "amount": amount, "memo": memo} + json={ + "out": False, + "amount": amount, + "memo": memo, + "description_hash": hexlify(description_hash).decode("ascii"), + }, ) ok, checking_id, payment_request, error_message = r.ok, None, None, None @@ -29,11 +35,7 @@ class LNbitsWallet(Wallet): return InvoiceResponse(ok, checking_id, payment_request, error_message) def pay_invoice(self, bolt11: str) -> PaymentResponse: - r = post( - url=f"{self.endpoint}/api/v1/payments", - headers=self.auth_admin, - json={"out": True, "bolt11": bolt11} - ) + r = post(url=f"{self.endpoint}/api/v1/payments", headers=self.auth_admin, json={"out": True, "bolt11": bolt11}) ok, checking_id, fee_msat, error_message = True, None, 0, None if r.ok: @@ -50,7 +52,7 @@ class LNbitsWallet(Wallet): if not r.ok: return PaymentStatus(None) - return PaymentStatus(r.json()['paid']) + return PaymentStatus(r.json()["paid"]) def get_payment_status(self, checking_id: str) -> PaymentStatus: r = get(url=f"{self.endpoint}/api/v1/payments/{checking_id}", headers=self.auth_invoice) @@ -58,4 +60,4 @@ class LNbitsWallet(Wallet): if not r.ok: return PaymentStatus(None) - return PaymentStatus(r.json()['paid']) + return PaymentStatus(r.json()["paid"]) diff --git a/lnbits/wallets/lndgrpc.py b/lnbits/wallets/lndgrpc.py index 62578f2..63df176 100644 --- a/lnbits/wallets/lndgrpc.py +++ b/lnbits/wallets/lndgrpc.py @@ -23,7 +23,7 @@ class LndWallet(Wallet): self.auth_read = getenv("LND_READ_MACAROON") self.auth_cert = getenv("LND_CERT") - def create_invoice(self, amount: int, memo: str = "") -> InvoiceResponse: + def create_invoice(self, amount: int, memo: str = "", description_hash: bytes = b"") -> InvoiceResponse: lnd_rpc = lnd_grpc.Client( lnd_dir=None, macaroon_path=self.auth_invoice, @@ -33,7 +33,13 @@ class LndWallet(Wallet): grpc_port=self.port, ) - lndResponse = lnd_rpc.add_invoice(memo=memo, value=amount, expiry=600, private=True) + lndResponse = lnd_rpc.add_invoice( + memo=memo, + description_hash=base64.b64encode(description_hash).decode("ascii"), + value=amount, + expiry=600, + private=True, + ) decoded_hash = base64.b64encode(lndResponse.r_hash).decode("utf-8").replace("/", "_") print(lndResponse.r_hash) ok, checking_id, payment_request, error_message = True, decoded_hash, str(lndResponse.payment_request), None diff --git a/lnbits/wallets/lndrest.py b/lnbits/wallets/lndrest.py index f9c98a2..1e7187b 100644 --- a/lnbits/wallets/lndrest.py +++ b/lnbits/wallets/lndrest.py @@ -1,5 +1,4 @@ from os import getenv -import os import base64 from requests import get, post from .base import InvoiceResponse, PaymentResponse, PaymentStatus, Wallet @@ -18,13 +17,17 @@ class LndRestWallet(Wallet): self.auth_read = {"Grpc-Metadata-macaroon": getenv("LND_REST_READ_MACAROON")} self.auth_cert = getenv("LND_REST_CERT") - - def create_invoice(self, amount: int, memo: str = "") -> InvoiceResponse: - + def create_invoice(self, amount: int, memo: str = "", description_hash: bytes = b"") -> InvoiceResponse: r = post( url=f"{self.endpoint}/v1/invoices", - headers=self.auth_invoice, verify=self.auth_cert, - json={"value": amount, "memo": memo, "private": True}, + headers=self.auth_invoice, + verify=self.auth_cert, + json={ + "value": amount, + "memo": memo, + "description_hash": base64.b64encode(description_hash).decode("ascii"), + "private": True, + }, ) print(self.auth_invoice) @@ -37,17 +40,19 @@ class LndRestWallet(Wallet): r = get(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("/","_") + 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: r = post( - url=f"{self.endpoint}/v1/channels/transactions", headers=self.auth_admin, verify=self.auth_cert, json={"payment_request": bolt11} + url=f"{self.endpoint}/v1/channels/transactions", + headers=self.auth_admin, + verify=self.auth_cert, + 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,) @@ -59,9 +64,8 @@ class LndRestWallet(Wallet): return PaymentResponse(ok, checking_id, fee_msat, error_message) - def get_invoice_status(self, checking_id: str) -> PaymentStatus: - checking_id = checking_id.replace("_","/") + checking_id = checking_id.replace("_", "/") print(checking_id) r = get(url=f"{self.endpoint}/v1/invoice/{checking_id}", headers=self.auth_invoice, verify=self.auth_cert,) print(r.json()["settled"]) @@ -71,7 +75,12 @@ class LndRestWallet(Wallet): return PaymentStatus(r.json()["settled"]) def get_payment_status(self, checking_id: str) -> PaymentStatus: - r = get(url=f"{self.endpoint}/v1/payments", headers=self.auth_admin, verify=self.auth_cert, params={"include_incomplete": "True", "max_payments": "20"}) + r = get( + url=f"{self.endpoint}/v1/payments", + headers=self.auth_admin, + verify=self.auth_cert, + params={"include_incomplete": "True", "max_payments": "20"}, + ) if not r.ok: return PaymentStatus(None) diff --git a/lnbits/wallets/lnpay.py b/lnbits/wallets/lnpay.py index 4b3aced..859b702 100644 --- a/lnbits/wallets/lnpay.py +++ b/lnbits/wallets/lnpay.py @@ -1,3 +1,4 @@ +import base64 from os import getenv from requests import get, post @@ -15,11 +16,15 @@ class LNPayWallet(Wallet): self.auth_read = getenv("LNPAY_READ_KEY") self.auth_api = {"X-Api-Key": getenv("LNPAY_API_KEY")} - def create_invoice(self, amount: int, memo: str = "") -> InvoiceResponse: + def create_invoice(self, amount: int, memo: str = "", description_hash: bytes = b"") -> InvoiceResponse: r = post( url=f"{self.endpoint}/user/wallet/{self.auth_invoice}/invoice", headers=self.auth_api, - json={"num_satoshis": f"{amount}", "memo": memo}, + json={ + "num_satoshis": f"{amount}", + "memo": memo, + "description_hash": base64.b64encode(description_hash).decode("ascii"), + }, ) ok, checking_id, payment_request, error_message = r.status_code == 201, None, None, None diff --git a/lnbits/wallets/lntxbot.py b/lnbits/wallets/lntxbot.py index 57bcdbf..6611c8e 100644 --- a/lnbits/wallets/lntxbot.py +++ b/lnbits/wallets/lntxbot.py @@ -1,5 +1,6 @@ from os import getenv from requests import post +from binascii import hexlify from .base import InvoiceResponse, PaymentResponse, PaymentStatus, Wallet @@ -13,8 +14,12 @@ class LntxbotWallet(Wallet): self.auth_admin = {"Authorization": f"Basic {getenv('LNTXBOT_ADMIN_KEY')}"} self.auth_invoice = {"Authorization": f"Basic {getenv('LNTXBOT_INVOICE_KEY')}"} - def create_invoice(self, amount: int, memo: str = "") -> InvoiceResponse: - r = post(url=f"{self.endpoint}/addinvoice", headers=self.auth_invoice, json={"amt": str(amount), "memo": memo}) + def create_invoice(self, amount: int, memo: str = "", description_hash: bytes = b"") -> InvoiceResponse: + r = post( + url=f"{self.endpoint}/addinvoice", + headers=self.auth_invoice, + json={"amt": str(amount), "memo": memo, "description_hash": hexlify(description_hash).decode("ascii")}, + ) ok, checking_id, payment_request, error_message = r.ok, None, None, None if r.ok: diff --git a/lnbits/wallets/opennode.py b/lnbits/wallets/opennode.py index 679db77..8a8f096 100644 --- a/lnbits/wallets/opennode.py +++ b/lnbits/wallets/opennode.py @@ -1,7 +1,7 @@ from os import getenv from requests import get, post -from .base import InvoiceResponse, PaymentResponse, PaymentStatus, Wallet +from .base import InvoiceResponse, PaymentResponse, PaymentStatus, Wallet, Unsupported class OpenNodeWallet(Wallet): @@ -13,7 +13,10 @@ class OpenNodeWallet(Wallet): self.auth_admin = {"Authorization": getenv("OPENNODE_ADMIN_KEY")} self.auth_invoice = {"Authorization": getenv("OPENNODE_INVOICE_KEY")} - def create_invoice(self, amount: int, memo: str = "") -> InvoiceResponse: + def create_invoice(self, amount: int, memo: str = "", description_hash: bytes = b"") -> InvoiceResponse: + if description_hash: + raise Unsupported("description_hash") + r = post( url=f"{self.endpoint}/v1/charges", headers=self.auth_invoice,