diff --git a/lnbits/wallets/lndrest.py b/lnbits/wallets/lndrest.py index ea519e09e..f7d4827ce 100644 --- a/lnbits/wallets/lndrest.py +++ b/lnbits/wallets/lndrest.py @@ -320,3 +320,84 @@ class LndRestWallet(Wallet): " seconds" ) await asyncio.sleep(5) + + async def create_hold_invoice( + self, + amount: int, + rhash: bytes, + memo: Optional[str] = None, + description_hash: Optional[bytes] = None, + unhashed_description: Optional[bytes] = None, + **kwargs, + ) -> InvoiceResponse: + data: Dict = { + "value": amount, + "private": True, + "hash": base64.b64encode(rhash).decode("ascii"), + } + if description_hash: + data["description_hash"] = base64.b64encode(description_hash).decode( + "ascii" + ) + elif unhashed_description: + data["description_hash"] = base64.b64encode( + hashlib.sha256(unhashed_description).digest() + ).decode("ascii") + else: + data["memo"] = memo or "" + + r = await self.client.post(url="/v2/invoices/hodl", json=data) + r.raise_for_status() + data = r.json() + + payment_request = data["payment_request"] + payment_hash = base64.b64encode(rhash).decode("ascii") + checking_id = payment_hash + + return InvoiceResponse(True, checking_id, payment_request, None) + + async def settle_hold_invoice(self, preimage: str) -> PaymentResponse: + data: Dict = {"preimage": base64.b64encode(preimage).decode("ascii")} + r = await self.client.post(url="/v2/invoices/settle", json=data) + r.raise_for_status() + + return PaymentResponse(True, None, None, None, None) + + async def cancel_hold_invoice(self, payment_hash: str) -> PaymentResponse: + data: Dict = {"payment_hash": base64.b64encode(payment_hash).decode("ascii")} + r = await self.client.post(url="/v2/invoices/cancel", json=data) + r.raise_for_status() + + return PaymentResponse(True, None, None, None, None) + + async def hold_invoices_stream(self, payment_hash: str, webhook: str): + try: + rhash_hex = bytes.fromhex(payment_hash) + rhash = base64.urlsafe_b64encode(rhash_hex) + url = f"{self.endpoint}/v2/invoices/subscribe/{rhash.decode()}" + async with self.client.stream("GET", url, timeout=None) as r: + async for line in r.aiter_lines(): + try: + inv = json.loads(line)["result"] + if inv["state"] not in ["ACCEPTED", "CANCELED"]: + continue + except Exception: + continue + + # dispatch webhook + inv["payment_hash"] = payment_hash + await LndRestWallet.dispatch_hold_webhook(webhook, inv) + + except Exception as exc: + logger.error( + f"lost connection to lnd hold invoices stream: '{exc}', retrying in 5 seconds" + ) + await asyncio.sleep(5) + + async def dispatch_hold_webhook(self, webhook, inv): + async with httpx.AsyncClient() as client: + try: + logger.debug("sending hold webhook", webhook, str(inv)) + await client.post(webhook, json=inv, timeout=40) # type: ignore + except (httpx.ConnectError, httpx.RequestError): + logger.debug("error sending hold webhook")