feat: preimages for incoming payments, fundingsource saves preimage on create_invoice (#3085)

Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com>
This commit is contained in:
dni ⚡
2025-04-30 11:03:19 +02:00
committed by GitHub
co-authored by Vlad Stan
parent f8b3644029
commit c4d0540e76
20 changed files with 183 additions and 106 deletions
+2
View File
@@ -99,10 +99,12 @@ class AlbyWallet(Wallet):
checking_id = data["payment_hash"]
payment_request = data["payment_request"]
preimage = data.get("payment_preimage")
return InvoiceResponse(
ok=True,
checking_id=checking_id,
payment_request=payment_request,
preimage=preimage,
)
except KeyError as exc:
logger.warning(exc)
+1
View File
@@ -20,6 +20,7 @@ class InvoiceResponse(NamedTuple):
checking_id: str | None = None # payment_hash, rpc_id
payment_request: str | None = None
error_message: str | None = None
preimage: str | None = None
@property
def success(self) -> bool:
+1
View File
@@ -99,6 +99,7 @@ class ClicheWallet(Wallet):
ok=True,
checking_id=checking_id,
payment_request=payment_request,
preimage=data["result"].get("preimage"),
)
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
+10 -1
View File
@@ -1,6 +1,7 @@
import asyncio
import random
from typing import Any, AsyncGenerator, Optional
from collections.abc import AsyncGenerator
from typing import Any, Optional
from bolt11.decode import decode as bolt11_decode
from bolt11.exceptions import Bolt11Exception
@@ -9,6 +10,7 @@ from pyln.client import LightningRpc, RpcError
from lnbits.nodes.cln import CoreLightningNode
from lnbits.settings import settings
from lnbits.utils.crypto import random_secret_and_hash
from .base import (
InvoiceResponse,
@@ -100,6 +102,11 @@ class CoreLightningWallet(Wallet):
)
if unhashed_description and not self.supports_description_hash:
raise UnsupportedError("unhashed_description")
preimage = kwargs.get("preimage")
if not preimage:
preimage, _ = random_secret_and_hash()
r: dict = self.ln.invoice( # type: ignore
amount_msat=msat,
label=label,
@@ -107,6 +114,7 @@ class CoreLightningWallet(Wallet):
unhashed_description.decode() if unhashed_description else memo
),
exposeprivatechannels=True,
preimage=preimage,
deschashonly=(
True if unhashed_description else False
), # we can't pass None here
@@ -119,6 +127,7 @@ class CoreLightningWallet(Wallet):
ok=True,
checking_id=r["payment_hash"],
payment_request=r["bolt11"],
preimage=preimage,
)
except RpcError as exc:
logger.warning(exc)
+8 -3
View File
@@ -1,7 +1,8 @@
import asyncio
import json
import random
from typing import AsyncGenerator, Optional
from collections.abc import AsyncGenerator
from typing import Optional
import httpx
from bolt11 import Bolt11Exception
@@ -9,6 +10,7 @@ from bolt11.decode import decode
from loguru import logger
from lnbits.settings import settings
from lnbits.utils.crypto import random_secret_and_hash
from .base import (
InvoiceResponse,
@@ -126,8 +128,10 @@ class CoreLightningRestWallet(Wallet):
if kwargs.get("expiry"):
data["expiry"] = kwargs["expiry"]
if kwargs.get("preimage"):
data["preimage"] = kwargs["preimage"]
preimage, _ = random_secret_and_hash()
# https://github.com/Ride-The-Lightning/c-lightning-REST/blob/master/controllers/invoice.js#L52C17-L52C25
data["preimage"] = preimage
try:
r = await self.client.post(
@@ -160,6 +164,7 @@ class CoreLightningRestWallet(Wallet):
ok=True,
checking_id=data["payment_hash"],
payment_request=data["bolt11"],
preimage=preimage,
)
except json.JSONDecodeError:
return InvoiceResponse(
+5
View File
@@ -10,6 +10,7 @@ from loguru import logger
from websockets.client import connect
from lnbits.settings import settings
from lnbits.utils.crypto import random_secret_and_hash
from .base import (
InvoiceResponse,
@@ -99,6 +100,9 @@ class EclairWallet(Wallet):
else:
data["description"] = memo
preimage, _ = random_secret_and_hash()
data["paymentPreimage"] = preimage
try:
r = await self.client.post("/createinvoice", data=data, timeout=40)
r.raise_for_status()
@@ -120,6 +124,7 @@ class EclairWallet(Wallet):
ok=True,
checking_id=data["paymentHash"],
payment_request=data["serialized"],
preimage=preimage,
)
except json.JSONDecodeError:
return InvoiceResponse(
+1 -1
View File
@@ -100,7 +100,7 @@ class FakeWallet(Wallet):
ok=True,
checking_id=payment_hash,
payment_request=payment_request,
# preimage=preimage.hex(),
preimage=preimage.hex(),
)
async def pay_invoice(self, bolt11: str, _: int) -> PaymentResponse:
+4 -1
View File
@@ -96,7 +96,10 @@ class LNbitsWallet(Wallet):
)
return InvoiceResponse(
ok=True, checking_id=data["checking_id"], payment_request=payment_str
ok=True,
checking_id=data["checking_id"],
payment_request=payment_str,
preimage=data.get("preimage"),
)
except json.JSONDecodeError:
return InvoiceResponse(
+34 -19
View File
@@ -1,6 +1,6 @@
import asyncio
import base64
import hashlib
from hashlib import sha256
from os import environ
from typing import AsyncGenerator, Dict, Optional
@@ -12,7 +12,7 @@ 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
from lnbits.settings import settings
from lnbits.utils.crypto import AESCipher
from lnbits.utils.crypto import AESCipher, random_secret_and_hash
from .base import (
InvoiceResponse,
@@ -139,22 +139,37 @@ class LndWallet(Wallet):
if description_hash:
data["description_hash"] = description_hash
elif unhashed_description:
data["description_hash"] = hashlib.sha256(
unhashed_description
).digest() # as bytes directly
data["description_hash"] = sha256(unhashed_description).digest()
preimage = kwargs.get("preimage")
if preimage:
payment_hash = sha256(preimage.encode()).hexdigest()
else:
preimage, payment_hash = random_secret_and_hash()
data["r_hash"] = bytes.fromhex(payment_hash)
data["r_preimage"] = bytes.fromhex(preimage)
try:
req = ln.Invoice(**data)
resp = await self.rpc.AddInvoice(req)
# response model
# {
# "r_hash": <bytes>,
# "payment_request": <string>,
# "add_index": <uint64>,
# "payment_addr": <bytes>,
# }
except Exception as exc:
logger.warning(exc)
error_message = str(exc)
return InvoiceResponse(ok=False, error_message=error_message)
return InvoiceResponse(ok=False, error_message=str(exc))
checking_id = bytes_to_hex(resp.r_hash)
payment_request = str(resp.payment_request)
return InvoiceResponse(
ok=True, checking_id=checking_id, payment_request=payment_request
ok=True,
checking_id=checking_id,
payment_request=payment_request,
preimage=preimage,
)
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
@@ -197,16 +212,18 @@ class LndWallet(Wallet):
fee_msat = -resp.htlcs[-1].route.total_fees_msat
preimage = resp.payment_preimage
checking_id = resp.payment_hash
return PaymentResponse(
ok=True, checking_id=checking_id, fee_msat=fee_msat, preimage=preimage
)
elif statuses[resp.status] is False:
error_message = failure_reasons[resp.failure_reason]
return PaymentResponse(
ok=statuses[resp.status],
checking_id=checking_id,
fee_msat=fee_msat,
preimage=preimage,
error_message=error_message,
)
return PaymentResponse(ok=False, error_message=error_message)
else:
return PaymentResponse(
ok=None,
checking_id=checking_id,
error_message="Payment in flight or non-existant.",
)
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
try:
@@ -217,10 +234,8 @@ class LndWallet(Wallet):
raise ValueError
resp = await self.rpc.LookupInvoice(ln.PaymentHash(r_hash=r_hash))
# todo: where is the FAILED status
if resp.settled:
return PaymentSuccessStatus()
return PaymentSuccessStatus(preimage=resp.r_preimage.hex())
return PaymentPendingStatus()
except grpc.RpcError as exc:
+11 -6
View File
@@ -9,7 +9,7 @@ from loguru import logger
from lnbits.nodes.lndrest import LndRestNode
from lnbits.settings import settings
from lnbits.utils.crypto import AESCipher
from lnbits.utils.crypto import AESCipher, random_secret_and_hash
from .base import (
InvoiceResponse,
@@ -110,24 +110,28 @@ class LndRestWallet(Wallet):
unhashed_description: Optional[bytes] = None,
**kwargs,
) -> InvoiceResponse:
data: Dict = {
_data: Dict = {
"value": amount,
"private": settings.lnd_rest_route_hints,
"memo": memo or "",
}
if kwargs.get("expiry"):
data["expiry"] = kwargs["expiry"]
_data["expiry"] = kwargs["expiry"]
if description_hash:
data["description_hash"] = base64.b64encode(description_hash).decode(
_data["description_hash"] = base64.b64encode(description_hash).decode(
"ascii"
)
elif unhashed_description:
data["description_hash"] = base64.b64encode(
_data["description_hash"] = base64.b64encode(
hashlib.sha256(unhashed_description).digest()
).decode("ascii")
preimage, _payment_hash = random_secret_and_hash()
_data["r_hash"] = base64.b64encode(bytes.fromhex(_payment_hash)).decode()
_data["r_preimage"] = base64.b64encode(bytes.fromhex(preimage)).decode()
try:
r = await self.client.post(url="/v1/invoices", json=data)
r = await self.client.post(url="/v1/invoices", json=_data)
r.raise_for_status()
data = r.json()
@@ -156,6 +160,7 @@ class LndRestWallet(Wallet):
ok=True,
checking_id=checking_id,
payment_request=payment_request,
preimage=preimage,
)
except json.JSONDecodeError:
+1
View File
@@ -98,6 +98,7 @@ class LnTipsWallet(Wallet):
ok=True,
checking_id=data["payment_hash"],
payment_request=data["payment_request"],
preimage=data.get("preimage"),
)
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
+2
View File
@@ -138,10 +138,12 @@ class PhoenixdWallet(Wallet):
checking_id = data["paymentHash"]
payment_request = data["serialized"]
preimage = data.get("paymentPreimage", None) # if available
return InvoiceResponse(
ok=True,
checking_id=checking_id,
payment_request=payment_request,
preimage=preimage,
)
except json.JSONDecodeError:
return InvoiceResponse(
+1
View File
@@ -140,6 +140,7 @@ class SparkWallet(Wallet):
ok=True,
payment_request=r["bolt11"],
checking_id=label,
preimage=r.get("preimage"),
)
except (SparkError, UnknownError) as e:
return InvoiceResponse(ok=False, error_message=str(e))
+2
View File
@@ -94,10 +94,12 @@ class ZBDWallet(Wallet):
data = r.json()["data"]
checking_id = data["id"] # this is a zbd id
payment_request = data["invoice"]["request"]
preimage = data["invoice"].get("preimage")
return InvoiceResponse(
ok=True,
checking_id=checking_id,
payment_request=payment_request,
preimage=preimage,
)
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse: