diff --git a/lnbits/bolt11.py b/lnbits/bolt11.py index 670397409..32b43feb6 100644 --- a/lnbits/bolt11.py +++ b/lnbits/bolt11.py @@ -216,7 +216,7 @@ def lnencode(addr, privkey): expirybits = expirybits[5:] data += tagged("x", expirybits) elif k == "h": - data += tagged_bytes("h", hashlib.sha256(v.encode("utf-8")).digest()) + data += tagged_bytes("h", v) elif k == "n": data += tagged_bytes("n", v) else: diff --git a/lnbits/core/services.py b/lnbits/core/services.py index 678c89e30..6cfad7b4e 100644 --- a/lnbits/core/services.py +++ b/lnbits/core/services.py @@ -54,6 +54,7 @@ async def create_invoice( amount: int, # in satoshis memo: str, description_hash: Optional[bytes] = None, + unhashed_description: Optional[bytes] = None, extra: Optional[Dict] = None, webhook: Optional[str] = None, internal: Optional[bool] = False, @@ -65,7 +66,10 @@ async def create_invoice( wallet = FAKE_WALLET if internal else WALLET ok, checking_id, payment_request, error_message = await wallet.create_invoice( - amount=amount, memo=invoice_memo, description_hash=description_hash + amount=amount, + memo=invoice_memo, + description_hash=description_hash, + unhashed_description=unhashed_description, ) if not ok: raise InvoiceFailure(error_message or "unexpected backend error.") diff --git a/lnbits/core/views/api.py b/lnbits/core/views/api.py index ac61e52fd..fbe5f100b 100644 --- a/lnbits/core/views/api.py +++ b/lnbits/core/views/api.py @@ -141,6 +141,7 @@ class CreateInvoiceData(BaseModel): memo: Optional[str] = None unit: Optional[str] = "sat" description_hash: Optional[str] = None + unhashed_description: Optional[str] = None lnurl_callback: Optional[str] = None lnurl_balance_check: Optional[str] = None extra: Optional[dict] = None @@ -152,9 +153,15 @@ class CreateInvoiceData(BaseModel): async def api_payments_create_invoice(data: CreateInvoiceData, wallet: Wallet): if data.description_hash: description_hash = unhexlify(data.description_hash) + unhashed_description = b"" + memo = "" + elif data.unhashed_description: + unhashed_description = unhexlify(data.unhashed_description) + description_hash = b"" memo = "" else: description_hash = b"" + unhashed_description = b"" memo = data.memo or LNBITS_SITE_TITLE if data.unit == "sat": amount = int(data.amount) @@ -170,6 +177,7 @@ async def api_payments_create_invoice(data: CreateInvoiceData, wallet: Wallet): amount=amount, memo=memo, description_hash=description_hash, + unhashed_description=unhashed_description, extra=data.extra, webhook=data.webhook, internal=data.internal, diff --git a/lnbits/wallets/cliche.py b/lnbits/wallets/cliche.py index 7c0347176..9dbe5a229 100644 --- a/lnbits/wallets/cliche.py +++ b/lnbits/wallets/cliche.py @@ -46,12 +46,19 @@ class ClicheWallet(Wallet): amount: int, memo: Optional[str] = None, description_hash: Optional[bytes] = None, + unhashed_description: Optional[bytes] = None, ) -> InvoiceResponse: - if description_hash: - description_hash_hashed = hashlib.sha256(description_hash).hexdigest() + if unhashed_description or description_hash: + description_hash_str = ( + description_hash.hex() + if description_hash + else hashlib.sha256(unhashed_description).hexdigest() + if unhashed_description + else None + ) ws = create_connection(self.endpoint) ws.send( - f"create-invoice --msatoshi {amount*1000} --description_hash {description_hash_hashed}" + f"create-invoice --msatoshi {amount*1000} --description_hash {description_hash_str}" ) r = ws.recv() else: diff --git a/lnbits/wallets/cln.py b/lnbits/wallets/cln.py index a92501f25..fe6ee2eb6 100644 --- a/lnbits/wallets/cln.py +++ b/lnbits/wallets/cln.py @@ -82,21 +82,24 @@ class CoreLightningWallet(Wallet): amount: int, memo: Optional[str] = None, description_hash: Optional[bytes] = None, + unhashed_description: Optional[bytes] = None, ) -> InvoiceResponse: label = "lbl{}".format(random.random()) msat: int = int(amount * 1000) try: - if description_hash and not self.supports_description_hash: + if description_hash: raise Unsupported("description_hash") + if unhashed_description and not self.supports_description_hash: + raise Unsupported("unhashed_description") r = self.ln.invoice( msatoshi=msat, label=label, - description=description_hash.decode("utf-8") - if description_hash + description=unhashed_description.decode("utf-8") + if unhashed_description else memo, exposeprivatechannels=True, deschashonly=True - if description_hash + if unhashed_description else False, # we can't pass None here ) diff --git a/lnbits/wallets/eclair.py b/lnbits/wallets/eclair.py index 0a4f1f3e4..247b96e1e 100644 --- a/lnbits/wallets/eclair.py +++ b/lnbits/wallets/eclair.py @@ -69,11 +69,14 @@ class EclairWallet(Wallet): amount: int, memo: Optional[str] = None, description_hash: Optional[bytes] = None, + unhashed_description: Optional[bytes] = None, ) -> InvoiceResponse: data: Dict = {"amountMsat": amount * 1000} if description_hash: - data["description_hash"] = hashlib.sha256(description_hash).hexdigest() + data["description_hash"] = description_hash.hex() + elif unhashed_description: + data["description_hash"] = hashlib.sha256(unhashed_description).hexdigest() else: data["description"] = memo or "" diff --git a/lnbits/wallets/fake.py b/lnbits/wallets/fake.py index 80a3d8c65..8424001b2 100644 --- a/lnbits/wallets/fake.py +++ b/lnbits/wallets/fake.py @@ -35,6 +35,7 @@ class FakeWallet(Wallet): amount: int, memo: Optional[str] = None, description_hash: Optional[bytes] = None, + unhashed_description: Optional[bytes] = None, ) -> InvoiceResponse: # we set a default secret since FakeWallet is used for internal=True invoices # and the user might not have configured a secret yet @@ -61,7 +62,10 @@ class FakeWallet(Wallet): data["timestamp"] = datetime.now().timestamp() if description_hash: data["tags_set"] = ["h"] - data["description_hash"] = description_hash.decode("utf-8") + data["description_hash"] = description_hash + elif unhashed_description: + data["tags_set"] = ["d"] + data["description_hash"] = hashlib.sha256(unhashed_description).digest() else: data["tags_set"] = ["d"] data["memo"] = memo diff --git a/lnbits/wallets/lnbits.py b/lnbits/wallets/lnbits.py index 677b518aa..96b7dbb63 100644 --- a/lnbits/wallets/lnbits.py +++ b/lnbits/wallets/lnbits.py @@ -57,10 +57,13 @@ class LNbitsWallet(Wallet): amount: int, memo: Optional[str] = None, description_hash: Optional[bytes] = None, + unhashed_description: Optional[bytes] = None, ) -> InvoiceResponse: data: Dict = {"out": False, "amount": amount} if description_hash: - data["description_hash"] = hashlib.sha256(description_hash).hexdigest() + data["description_hash"] = description_hash.hex() + elif unhashed_description: + data["description_hash"] = hashlib.sha256(unhashed_description).hexdigest() else: data["memo"] = memo or "" diff --git a/lnbits/wallets/lndgrpc.py b/lnbits/wallets/lndgrpc.py index fee8b955a..10bd27e7f 100644 --- a/lnbits/wallets/lndgrpc.py +++ b/lnbits/wallets/lndgrpc.py @@ -134,14 +134,15 @@ class LndWallet(Wallet): amount: int, memo: Optional[str] = None, description_hash: Optional[bytes] = None, + unhashed_description: Optional[bytes] = None, ) -> InvoiceResponse: params: Dict = {"value": amount, "expiry": 600, "private": True} - if description_hash: + params["description_hash"] = description_hash + elif unhashed_description: params["description_hash"] = hashlib.sha256( - description_hash + unhashed_description ).digest() # as bytes directly - else: params["memo"] = memo or "" diff --git a/lnbits/wallets/lndrest.py b/lnbits/wallets/lndrest.py index 9f7f95581..6bdbe5e07 100644 --- a/lnbits/wallets/lndrest.py +++ b/lnbits/wallets/lndrest.py @@ -73,11 +73,17 @@ class LndRestWallet(Wallet): amount: int, memo: Optional[str] = None, description_hash: Optional[bytes] = None, + unhashed_description: Optional[bytes] = None, + **kwargs, ) -> InvoiceResponse: data: Dict = {"value": amount, "private": True} if description_hash: + data["description_hash"] = base64.b64encode(description_hash).decode( + "ascii" + ) + elif unhashed_description: data["description_hash"] = base64.b64encode( - hashlib.sha256(description_hash).digest() + hashlib.sha256(unhashed_description).digest() ).decode("ascii") else: data["memo"] = memo or "" diff --git a/lnbits/wallets/lnpay.py b/lnbits/wallets/lnpay.py index 7ba45a228..de0a60a84 100644 --- a/lnbits/wallets/lnpay.py +++ b/lnbits/wallets/lnpay.py @@ -52,10 +52,14 @@ class LNPayWallet(Wallet): amount: int, memo: Optional[str] = None, description_hash: Optional[bytes] = None, + unhashed_description: Optional[bytes] = None, + **kwargs, ) -> InvoiceResponse: data: Dict = {"num_satoshis": f"{amount}"} if description_hash: - data["description_hash"] = hashlib.sha256(description_hash).hexdigest() + data["description_hash"] = description_hash.hex() + elif unhashed_description: + data["description_hash"] = hashlib.sha256(unhashed_description).hexdigest() else: data["memo"] = memo or "" diff --git a/lnbits/wallets/lntxbot.py b/lnbits/wallets/lntxbot.py index 9b0954e9e..ba310823a 100644 --- a/lnbits/wallets/lntxbot.py +++ b/lnbits/wallets/lntxbot.py @@ -52,10 +52,14 @@ class LntxbotWallet(Wallet): amount: int, memo: Optional[str] = None, description_hash: Optional[bytes] = None, + unhashed_description: Optional[bytes] = None, + **kwargs, ) -> InvoiceResponse: data: Dict = {"amt": str(amount)} if description_hash: - data["description_hash"] = hashlib.sha256(description_hash).hexdigest() + data["description_hash"] = description_hash.hex() + elif unhashed_description: + data["description_hash"] = hashlib.sha256(unhashed_description).hexdigest() else: data["memo"] = memo or "" diff --git a/lnbits/wallets/opennode.py b/lnbits/wallets/opennode.py index 0760d186b..9fcf374a7 100644 --- a/lnbits/wallets/opennode.py +++ b/lnbits/wallets/opennode.py @@ -54,8 +54,10 @@ class OpenNodeWallet(Wallet): amount: int, memo: Optional[str] = None, description_hash: Optional[bytes] = None, + unhashed_description: Optional[bytes] = None, + **kwargs, ) -> InvoiceResponse: - if description_hash: + if description_hash or unhashed_description: raise Unsupported("description_hash") async with httpx.AsyncClient() as client: diff --git a/lnbits/wallets/spark.py b/lnbits/wallets/spark.py index 55758aab0..a26177db9 100644 --- a/lnbits/wallets/spark.py +++ b/lnbits/wallets/spark.py @@ -93,6 +93,8 @@ class SparkWallet(Wallet): amount: int, memo: Optional[str] = None, description_hash: Optional[bytes] = None, + unhashed_description: Optional[bytes] = None, + **kwargs, ) -> InvoiceResponse: label = "lbs{}".format(random.random()) checking_id = label @@ -102,7 +104,13 @@ class SparkWallet(Wallet): r = await self.invoicewithdescriptionhash( msatoshi=amount * 1000, label=label, - description_hash=hashlib.sha256(description_hash).hexdigest(), + description_hash=description_hash.hex(), + ) + elif unhashed_description: + r = await self.invoicewithdescriptionhash( + msatoshi=amount * 1000, + label=label, + description_hash=hashlib.sha256(unhashed_description).hexdigest(), ) else: r = await self.invoice( diff --git a/lnbits/wallets/void.py b/lnbits/wallets/void.py index d6a01d3ad..0de387aae 100644 --- a/lnbits/wallets/void.py +++ b/lnbits/wallets/void.py @@ -18,6 +18,7 @@ class VoidWallet(Wallet): amount: int, memo: Optional[str] = None, description_hash: Optional[bytes] = None, + **kwargs, ) -> InvoiceResponse: raise Unsupported("") diff --git a/tests/core/views/test_api.py b/tests/core/views/test_api.py index 9dd13004c..501379b8f 100644 --- a/tests/core/views/test_api.py +++ b/tests/core/views/test_api.py @@ -11,6 +11,7 @@ from lnbits.core.views.api import ( api_payment, api_payments_create_invoice, ) +from lnbits.settings import wallet_class from ...helpers import get_random_invoice_data @@ -192,11 +193,32 @@ async def test_api_payment_with_key(invoice, inkey_headers_from): # check POST /api/v1/payments: invoice creation with a description hash +@pytest.mark.skipif( + wallet_class.__name__ in ["CoreLightningWallet"], + reason="wallet does not support description_hash", +) @pytest.mark.asyncio async def test_create_invoice_with_description_hash(client, inkey_headers_to): data = await get_random_invoice_data() descr_hash = hashlib.sha256("asdasdasd".encode("utf-8")).hexdigest() - data["description_hash"] = "asdasdasd".encode("utf-8").hex() + data["description_hash"] = descr_hash + + response = await client.post( + "/api/v1/payments", json=data, headers=inkey_headers_to + ) + invoice = response.json() + + invoice_bolt11 = bolt11.decode(invoice["payment_request"]) + assert invoice_bolt11.description_hash == descr_hash + assert invoice_bolt11.description is None + return invoice + + +@pytest.mark.asyncio +async def test_create_invoice_with_unhashed_description(client, inkey_headers_to): + data = await get_random_invoice_data() + descr_hash = hashlib.sha256("asdasdasd".encode("utf-8")).hexdigest() + data["unhashed_description"] = "asdasdasd".encode("utf-8").hex() response = await client.post( "/api/v1/payments", json=data, headers=inkey_headers_to