168 lines
5.7 KiB
Python
168 lines
5.7 KiB
Python
from time import time
|
|
|
|
from lnurl import (
|
|
LnAddress,
|
|
Lnurl,
|
|
LnurlErrorResponse,
|
|
LnurlPayActionResponse,
|
|
LnurlPayResponse,
|
|
LnurlResponseException,
|
|
LnurlSuccessResponse,
|
|
LnurlWithdrawResponse,
|
|
execute_pay_request,
|
|
execute_withdraw,
|
|
handle,
|
|
)
|
|
from loguru import logger
|
|
|
|
from lnbits.core.crud import update_wallet
|
|
from lnbits.core.models import CreateLnurlPayment, Wallet
|
|
from lnbits.core.models.lnurl import StoredPayLink
|
|
from lnbits.helpers import check_callback_url
|
|
from lnbits.settings import settings
|
|
from lnbits.utils.exchange_rates import fiat_amount_as_satoshis
|
|
|
|
|
|
async def perform_withdraw(lnurl: str, payment_request: str) -> None:
|
|
"""
|
|
Perform an LNURL withdraw to the given LNURL-withdraw link.
|
|
:param lnurl: The LNURL-withdraw link. bech32 or lud17 format.
|
|
:param payment_request: The BOLT11 payment request to pay.
|
|
:raises LnurlResponseException: If the LNURL-withdraw process fails.
|
|
"""
|
|
res = await handle(lnurl, user_agent=settings.user_agent, timeout=10)
|
|
if isinstance(res, LnurlErrorResponse):
|
|
raise LnurlResponseException(res.reason)
|
|
if not isinstance(res, LnurlWithdrawResponse):
|
|
raise LnurlResponseException("Invalid LNURL-withdraw response.")
|
|
try:
|
|
check_callback_url(res.callback)
|
|
except ValueError as exc:
|
|
raise LnurlResponseException(f"Invalid callback URL: {exc!s}") from exc
|
|
res2 = await execute_withdraw(
|
|
res, payment_request, user_agent=settings.user_agent, timeout=10
|
|
)
|
|
if isinstance(res2, LnurlErrorResponse):
|
|
raise LnurlResponseException(res2.reason)
|
|
if not isinstance(res2, LnurlSuccessResponse):
|
|
raise LnurlResponseException("Invalid LNURL-withdraw success response.")
|
|
|
|
|
|
async def get_pr_from_lnurl(
|
|
lnurl: str, amount_msat: int, comment: str | None = None
|
|
) -> str:
|
|
res = await handle(lnurl, user_agent=settings.user_agent, timeout=10)
|
|
if isinstance(res, LnurlErrorResponse):
|
|
raise LnurlResponseException(res.reason)
|
|
if not isinstance(res, LnurlPayResponse):
|
|
raise LnurlResponseException(
|
|
"Invalid LNURL response. Expected LnurlPayResponse."
|
|
)
|
|
res2 = await execute_pay_request(
|
|
res,
|
|
msat=amount_msat,
|
|
comment=comment,
|
|
user_agent=settings.user_agent,
|
|
timeout=10,
|
|
)
|
|
if isinstance(res2, LnurlErrorResponse):
|
|
raise LnurlResponseException(res2.reason)
|
|
return res2.pr
|
|
|
|
|
|
async def fetch_lnurl_pay_request(
|
|
data: CreateLnurlPayment, wallet: Wallet | None = None
|
|
) -> tuple[LnurlPayResponse, LnurlPayActionResponse]:
|
|
"""
|
|
Pay an LNURL payment request.
|
|
optional `wallet` is used to store the pay link in the wallet's stored links.
|
|
|
|
raises `LnurlResponseException` if pay request fails
|
|
"""
|
|
if not data.res and data.lnurl:
|
|
res = await handle(data.lnurl, user_agent=settings.user_agent, timeout=5)
|
|
if isinstance(res, LnurlErrorResponse):
|
|
raise LnurlResponseException(res.reason)
|
|
if not isinstance(res, LnurlPayResponse):
|
|
raise LnurlResponseException(
|
|
"Invalid LNURL response. Expected LnurlPayResponse."
|
|
)
|
|
data.res = res
|
|
if not data.res:
|
|
raise LnurlResponseException("No LNURL pay request provided.")
|
|
|
|
if data.unit and data.unit != "sat":
|
|
# shift to float with 2 decimal places
|
|
amount = round(data.amount / 1000, 2)
|
|
amount_msat = await fiat_amount_as_satoshis(amount, data.unit)
|
|
amount_msat *= 1000
|
|
else:
|
|
amount_msat = data.amount
|
|
|
|
res2 = await execute_pay_request(
|
|
data.res,
|
|
msat=amount_msat,
|
|
comment=data.comment,
|
|
user_agent=settings.user_agent,
|
|
timeout=10,
|
|
)
|
|
|
|
if wallet:
|
|
await store_paylink(data.res, res2, wallet, data.lnurl)
|
|
|
|
return data.res, res2
|
|
|
|
|
|
async def store_paylink(
|
|
res: LnurlPayResponse,
|
|
res2: LnurlPayActionResponse,
|
|
wallet: Wallet,
|
|
lnurl: LnAddress | Lnurl | None = None,
|
|
) -> None:
|
|
|
|
if res2.disposable is not False:
|
|
return # do not store disposable LNURL pay links
|
|
|
|
logger.debug(f"storing lnurl pay link for wallet {wallet.id}. ")
|
|
|
|
stored_paylink = None
|
|
# If we have only a LnurlPayResponse, we can use its lnaddress
|
|
# because the lnurl is not available.
|
|
if not lnurl:
|
|
for _data in res.metadata.list():
|
|
if _data[0] == "text/identifier":
|
|
stored_paylink = StoredPayLink(
|
|
lnurl=LnAddress(_data[1]), label=res.metadata.text
|
|
)
|
|
if not stored_paylink:
|
|
logger.warning(
|
|
"No lnaddress found in metadata for LNURL pay link. "
|
|
"Skipping storage."
|
|
)
|
|
return # skip if lnaddress not found in metadata
|
|
else:
|
|
if isinstance(lnurl, Lnurl):
|
|
_lnurl = str(lnurl.lud17 or lnurl.bech32)
|
|
else:
|
|
_lnurl = str(lnurl)
|
|
stored_paylink = StoredPayLink(lnurl=_lnurl, label=res.metadata.text)
|
|
|
|
# update last_used if its already stored
|
|
for pl in wallet.stored_paylinks.links:
|
|
if pl.lnurl == stored_paylink.lnurl:
|
|
pl.last_used = int(time())
|
|
await update_wallet(wallet)
|
|
logger.debug(
|
|
"Updated last used time for LNURL "
|
|
f"pay link {stored_paylink.lnurl} in wallet {wallet.id}."
|
|
)
|
|
return
|
|
|
|
# if not already stored, append it
|
|
if not any(stored_paylink.lnurl == pl.lnurl for pl in wallet.stored_paylinks.links):
|
|
wallet.stored_paylinks.links.append(stored_paylink)
|
|
await update_wallet(wallet)
|
|
logger.debug(
|
|
f"Stored LNURL pay link {stored_paylink.lnurl} for wallet {wallet.id}."
|
|
)
|