2021-08-30 19:55:02 +02:00
|
|
|
import asyncio
|
2023-10-10 12:03:24 +02:00
|
|
|
import datetime
|
2020-11-10 04:25:46 +01:00
|
|
|
import json
|
2021-10-18 13:23:57 +02:00
|
|
|
from io import BytesIO
|
2023-09-11 13:19:19 +02:00
|
|
|
from pathlib import Path
|
2023-02-02 13:58:10 +01:00
|
|
|
from typing import Dict, List, Optional, Tuple, TypedDict
|
2021-10-18 13:23:57 +02:00
|
|
|
from urllib.parse import parse_qs, urlparse
|
2020-09-04 21:24:30 +02:00
|
|
|
|
2021-10-18 13:23:57 +02:00
|
|
|
import httpx
|
2023-10-10 12:03:24 +02:00
|
|
|
from bolt11 import Bolt11
|
|
|
|
from bolt11 import decode as bolt11_decode
|
2023-09-11 15:48:49 +02:00
|
|
|
from cryptography.hazmat.primitives import serialization
|
2022-12-16 11:09:13 +01:00
|
|
|
from fastapi import Depends, WebSocket
|
2021-10-18 13:23:57 +02:00
|
|
|
from lnurl import LnurlErrorResponse
|
2023-01-09 11:14:44 +01:00
|
|
|
from lnurl import decode as decode_lnurl
|
2022-07-16 14:23:03 +02:00
|
|
|
from loguru import logger
|
2023-09-11 15:48:49 +02:00
|
|
|
from py_vapid import Vapid
|
|
|
|
from py_vapid.utils import b64urlencode
|
2020-04-11 22:18:17 +02:00
|
|
|
|
2023-09-12 12:25:05 +02:00
|
|
|
from lnbits.core.db import db
|
2021-03-26 23:10:30 +01:00
|
|
|
from lnbits.db import Connection
|
2022-12-27 15:00:41 +01:00
|
|
|
from lnbits.decorators import WalletTypeInfo, require_admin_key
|
2023-03-30 14:21:01 +02:00
|
|
|
from lnbits.helpers import url_for
|
2022-12-12 08:45:08 +01:00
|
|
|
from lnbits.settings import (
|
2022-12-19 09:10:41 +01:00
|
|
|
EditableSettings,
|
2023-02-02 13:58:10 +01:00
|
|
|
SuperSettings,
|
2022-12-12 08:45:08 +01:00
|
|
|
readonly_variables,
|
|
|
|
send_admin_user_to_saas,
|
|
|
|
settings,
|
|
|
|
)
|
2023-08-28 12:00:59 +02:00
|
|
|
from lnbits.utils.exchange_rates import fiat_amount_as_satoshis, satoshis_amount_as_fiat
|
2023-06-27 16:11:00 +02:00
|
|
|
from lnbits.wallets import FAKE_WALLET, get_wallet_class, set_wallet_class
|
2021-10-18 13:23:57 +02:00
|
|
|
from lnbits.wallets.base import PaymentResponse, PaymentStatus
|
2020-04-11 22:18:17 +02:00
|
|
|
|
2021-03-24 04:40:32 +01:00
|
|
|
from .crud import (
|
2021-10-18 13:23:57 +02:00
|
|
|
check_internal,
|
2023-04-03 14:44:17 +02:00
|
|
|
check_internal_pending,
|
2022-12-16 11:09:13 +01:00
|
|
|
create_account,
|
|
|
|
create_admin_settings,
|
2021-03-24 04:40:32 +01:00
|
|
|
create_payment,
|
2022-12-16 11:09:13 +01:00
|
|
|
create_wallet,
|
2022-09-18 15:27:03 +02:00
|
|
|
delete_wallet_payment,
|
2022-12-16 11:09:13 +01:00
|
|
|
get_account,
|
2023-05-04 17:20:37 +02:00
|
|
|
get_standalone_payment,
|
2022-12-08 14:41:52 +01:00
|
|
|
get_super_settings,
|
2023-06-20 11:26:33 +02:00
|
|
|
get_total_balance,
|
2021-10-18 13:23:57 +02:00
|
|
|
get_wallet,
|
2021-03-24 04:40:32 +01:00
|
|
|
get_wallet_payment,
|
2023-09-11 15:48:49 +02:00
|
|
|
update_admin_settings,
|
2022-08-30 13:28:58 +02:00
|
|
|
update_payment_details,
|
2021-10-18 13:23:57 +02:00
|
|
|
update_payment_status,
|
2022-12-16 10:22:32 +01:00
|
|
|
update_super_user,
|
2021-03-24 04:40:32 +01:00
|
|
|
)
|
2023-05-09 10:22:19 +02:00
|
|
|
from .helpers import to_valid_user_id
|
2023-07-26 12:08:22 +02:00
|
|
|
from .models import Payment, Wallet
|
2020-04-11 22:18:17 +02:00
|
|
|
|
2021-10-18 13:23:57 +02:00
|
|
|
|
2021-04-06 19:57:51 +02:00
|
|
|
class PaymentFailure(Exception):
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
2021-04-10 22:37:48 +02:00
|
|
|
class InvoiceFailure(Exception):
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
2023-08-28 12:00:59 +02:00
|
|
|
async def calculate_fiat_amounts(
|
|
|
|
amount: float,
|
|
|
|
wallet_id: str,
|
|
|
|
currency: Optional[str] = None,
|
|
|
|
extra: Optional[Dict] = None,
|
|
|
|
conn: Optional[Connection] = None,
|
|
|
|
) -> Tuple[int, Optional[Dict]]:
|
|
|
|
wallet = await get_wallet(wallet_id, conn=conn)
|
|
|
|
assert wallet, "invalid wallet_id"
|
|
|
|
wallet_currency = wallet.currency or settings.lnbits_default_accounting_currency
|
|
|
|
|
|
|
|
if currency and currency != "sat":
|
|
|
|
amount_sat = await fiat_amount_as_satoshis(amount, currency)
|
|
|
|
extra = extra or {}
|
|
|
|
if currency != wallet_currency:
|
|
|
|
extra["fiat_currency"] = currency
|
|
|
|
extra["fiat_amount"] = round(amount, ndigits=3)
|
|
|
|
extra["fiat_rate"] = amount_sat / amount
|
|
|
|
else:
|
|
|
|
amount_sat = int(amount)
|
|
|
|
|
|
|
|
if wallet_currency:
|
|
|
|
if wallet_currency == currency:
|
|
|
|
fiat_amount = amount
|
|
|
|
else:
|
|
|
|
fiat_amount = await satoshis_amount_as_fiat(amount_sat, wallet_currency)
|
|
|
|
extra = extra or {}
|
|
|
|
extra["wallet_fiat_currency"] = wallet_currency
|
|
|
|
extra["wallet_fiat_amount"] = round(fiat_amount, ndigits=3)
|
|
|
|
extra["wallet_fiat_rate"] = amount_sat / fiat_amount
|
|
|
|
|
2023-09-12 11:06:35 +02:00
|
|
|
logger.debug(
|
|
|
|
f"Calculated fiat amounts {wallet.id=} {amount=} {currency=}: {extra=}"
|
|
|
|
)
|
2023-08-28 12:00:59 +02:00
|
|
|
|
|
|
|
return amount_sat, extra
|
|
|
|
|
|
|
|
|
2020-11-21 22:04:39 +01:00
|
|
|
async def create_invoice(
|
2020-09-03 23:02:15 +02:00
|
|
|
*,
|
|
|
|
wallet_id: str,
|
2023-08-28 12:00:59 +02:00
|
|
|
amount: float,
|
|
|
|
currency: Optional[str] = "sat",
|
2021-11-03 12:23:16 +01:00
|
|
|
memo: str,
|
2020-09-03 23:02:15 +02:00
|
|
|
description_hash: Optional[bytes] = None,
|
2022-08-13 14:29:04 +02:00
|
|
|
unhashed_description: Optional[bytes] = None,
|
2023-01-26 11:08:40 +01:00
|
|
|
expiry: Optional[int] = None,
|
2020-09-03 23:02:15 +02:00
|
|
|
extra: Optional[Dict] = None,
|
2020-12-24 13:38:35 +01:00
|
|
|
webhook: Optional[str] = None,
|
2022-07-17 14:34:25 +02:00
|
|
|
internal: Optional[bool] = False,
|
2021-03-26 23:10:30 +01:00
|
|
|
conn: Optional[Connection] = None,
|
2020-09-01 03:12:46 +02:00
|
|
|
) -> Tuple[str, str]:
|
2023-02-22 13:30:45 +01:00
|
|
|
if not amount > 0:
|
|
|
|
raise InvoiceFailure("Amountless invoices not supported.")
|
|
|
|
|
2023-09-11 15:06:31 +02:00
|
|
|
if await get_wallet(wallet_id, conn=conn) is None:
|
|
|
|
raise InvoiceFailure("Wallet does not exist.")
|
|
|
|
|
2021-11-03 12:07:01 +01:00
|
|
|
invoice_memo = None if description_hash else memo
|
2021-11-03 12:14:41 +01:00
|
|
|
|
2022-07-17 14:34:25 +02:00
|
|
|
# use the fake wallet if the invoice is for internal use only
|
2022-10-05 09:46:59 +02:00
|
|
|
wallet = FAKE_WALLET if internal else get_wallet_class()
|
2022-07-17 14:34:25 +02:00
|
|
|
|
2023-08-28 12:00:59 +02:00
|
|
|
amount_sat, extra = await calculate_fiat_amounts(
|
|
|
|
amount, wallet_id, currency=currency, extra=extra, conn=conn
|
|
|
|
)
|
|
|
|
|
2022-07-17 14:34:25 +02:00
|
|
|
ok, checking_id, payment_request, error_message = await wallet.create_invoice(
|
2023-08-28 12:00:59 +02:00
|
|
|
amount=amount_sat,
|
2022-08-13 14:29:04 +02:00
|
|
|
memo=invoice_memo,
|
|
|
|
description_hash=description_hash,
|
|
|
|
unhashed_description=unhashed_description,
|
2023-01-26 11:08:40 +01:00
|
|
|
expiry=expiry or settings.lightning_invoice_expiry,
|
2020-09-03 02:11:08 +02:00
|
|
|
)
|
2023-06-27 16:11:00 +02:00
|
|
|
if not ok or not payment_request or not checking_id:
|
2022-07-17 14:34:25 +02:00
|
|
|
raise InvoiceFailure(error_message or "unexpected backend error.")
|
2020-04-11 22:18:17 +02:00
|
|
|
|
2023-10-10 12:03:24 +02:00
|
|
|
invoice = bolt11_decode(payment_request)
|
2020-04-11 22:18:17 +02:00
|
|
|
|
2023-08-28 12:00:59 +02:00
|
|
|
amount_msat = 1000 * amount_sat
|
2020-11-21 22:04:39 +01:00
|
|
|
await create_payment(
|
2020-09-01 03:12:46 +02:00
|
|
|
wallet_id=wallet_id,
|
|
|
|
checking_id=checking_id,
|
|
|
|
payment_request=payment_request,
|
|
|
|
payment_hash=invoice.payment_hash,
|
|
|
|
amount=amount_msat,
|
2023-10-10 12:03:24 +02:00
|
|
|
expiry=get_bolt11_expiry(invoice),
|
2021-11-03 12:14:41 +01:00
|
|
|
memo=memo,
|
2020-09-01 03:12:46 +02:00
|
|
|
extra=extra,
|
2020-12-24 13:38:35 +01:00
|
|
|
webhook=webhook,
|
2021-03-26 23:10:30 +01:00
|
|
|
conn=conn,
|
2020-09-01 03:12:46 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
return invoice.payment_hash, payment_request
|
|
|
|
|
|
|
|
|
2020-11-21 22:04:39 +01:00
|
|
|
async def pay_invoice(
|
2020-10-12 23:15:27 +02:00
|
|
|
*,
|
|
|
|
wallet_id: str,
|
|
|
|
payment_request: str,
|
|
|
|
max_sat: Optional[int] = None,
|
|
|
|
extra: Optional[Dict] = None,
|
|
|
|
description: str = "",
|
2021-03-26 23:10:30 +01:00
|
|
|
conn: Optional[Connection] = None,
|
2020-09-01 03:12:46 +02:00
|
|
|
) -> str:
|
2022-08-30 13:28:58 +02:00
|
|
|
"""
|
|
|
|
Pay a Lightning invoice.
|
2023-08-24 11:26:09 +02:00
|
|
|
First, we create a temporary payment in the database with fees set to the reserve
|
|
|
|
fee. We then check whether the balance of the payer would go negative.
|
|
|
|
We then attempt to pay the invoice through the backend. If the payment is
|
|
|
|
successful, we update the payment in the database with the payment details.
|
2022-08-30 13:28:58 +02:00
|
|
|
If the payment is unsuccessful, we delete the temporary payment.
|
2023-08-24 11:26:09 +02:00
|
|
|
If the payment is still in flight, we hope that some other process
|
|
|
|
will regularly check for the payment.
|
2022-08-30 13:28:58 +02:00
|
|
|
"""
|
2023-10-10 12:03:24 +02:00
|
|
|
invoice = bolt11_decode(payment_request)
|
2023-09-25 12:06:54 +02:00
|
|
|
|
|
|
|
if not invoice.amount_msat or not invoice.amount_msat > 0:
|
|
|
|
raise ValueError("Amountless invoices not supported.")
|
|
|
|
if max_sat and invoice.amount_msat > max_sat * 1000:
|
|
|
|
raise ValueError("Amount in invoice is too high.")
|
|
|
|
|
2023-08-24 11:26:09 +02:00
|
|
|
async with db.reuse_conn(conn) if conn else db.connect() as conn:
|
2022-08-30 13:28:58 +02:00
|
|
|
temp_id = invoice.payment_hash
|
|
|
|
internal_id = f"internal_{invoice.payment_hash}"
|
2021-11-13 11:42:11 +01:00
|
|
|
|
|
|
|
if invoice.amount_msat == 0:
|
|
|
|
raise ValueError("Amountless invoices not supported.")
|
|
|
|
if max_sat and invoice.amount_msat > max_sat * 1000:
|
|
|
|
raise ValueError("Amount in invoice is too high.")
|
|
|
|
|
2023-08-28 12:00:59 +02:00
|
|
|
_, extra = await calculate_fiat_amounts(
|
2023-09-12 11:06:35 +02:00
|
|
|
invoice.amount_msat / 1000, wallet_id, extra=extra, conn=conn
|
2023-08-28 12:00:59 +02:00
|
|
|
)
|
|
|
|
|
2021-11-13 11:42:11 +01:00
|
|
|
# put all parameters that don't change here
|
2022-07-20 09:45:08 +02:00
|
|
|
class PaymentKwargs(TypedDict):
|
|
|
|
wallet_id: str
|
|
|
|
payment_request: str
|
|
|
|
payment_hash: str
|
|
|
|
amount: int
|
|
|
|
memo: str
|
2023-10-10 12:03:24 +02:00
|
|
|
expiry: Optional[datetime.datetime]
|
2022-07-20 09:45:08 +02:00
|
|
|
extra: Optional[Dict]
|
|
|
|
|
|
|
|
payment_kwargs: PaymentKwargs = PaymentKwargs(
|
2021-11-13 11:42:11 +01:00
|
|
|
wallet_id=wallet_id,
|
|
|
|
payment_request=payment_request,
|
|
|
|
payment_hash=invoice.payment_hash,
|
|
|
|
amount=-invoice.amount_msat,
|
2023-10-10 12:03:24 +02:00
|
|
|
expiry=get_bolt11_expiry(invoice),
|
2021-11-13 11:42:11 +01:00
|
|
|
memo=description or invoice.description or "",
|
|
|
|
extra=extra,
|
2021-03-26 23:10:30 +01:00
|
|
|
)
|
|
|
|
|
2023-08-24 11:26:09 +02:00
|
|
|
# we check if an internal invoice exists that has already been paid
|
|
|
|
# (not pending anymore)
|
2023-04-03 14:44:17 +02:00
|
|
|
if not await check_internal_pending(invoice.payment_hash, conn=conn):
|
|
|
|
raise PaymentFailure("Internal invoice already paid.")
|
|
|
|
|
2023-08-24 11:26:09 +02:00
|
|
|
# check_internal() returns the checking_id of the invoice we're waiting for
|
|
|
|
# (pending only)
|
2021-11-13 11:42:11 +01:00
|
|
|
internal_checking_id = await check_internal(invoice.payment_hash, conn=conn)
|
|
|
|
if internal_checking_id:
|
2023-11-21 12:11:21 +01:00
|
|
|
fee_reserve_total_msat = fee_reserve_total(
|
|
|
|
invoice.amount_msat, internal=True
|
|
|
|
)
|
2023-05-04 17:20:37 +02:00
|
|
|
# perform additional checks on the internal payment
|
|
|
|
# the payment hash is not enough to make sure that this is the same invoice
|
|
|
|
internal_invoice = await get_standalone_payment(
|
|
|
|
internal_checking_id, incoming=True, conn=conn
|
|
|
|
)
|
|
|
|
assert internal_invoice is not None
|
|
|
|
if (
|
|
|
|
internal_invoice.amount != invoice.amount_msat
|
2023-05-19 13:41:58 +02:00
|
|
|
or internal_invoice.bolt11 != payment_request.lower()
|
2023-05-04 17:20:37 +02:00
|
|
|
):
|
|
|
|
raise PaymentFailure("Invalid invoice.")
|
|
|
|
|
2022-07-07 14:30:16 +02:00
|
|
|
logger.debug(f"creating temporary internal payment with id {internal_id}")
|
2021-11-13 11:42:11 +01:00
|
|
|
# create a new payment from this wallet
|
2023-07-26 12:08:22 +02:00
|
|
|
new_payment = await create_payment(
|
2021-11-13 11:42:11 +01:00
|
|
|
checking_id=internal_id,
|
2023-11-21 12:11:21 +01:00
|
|
|
fee=0 + abs(fee_reserve_total_msat),
|
2021-11-13 11:42:11 +01:00
|
|
|
pending=False,
|
|
|
|
conn=conn,
|
|
|
|
**payment_kwargs,
|
|
|
|
)
|
|
|
|
else:
|
2023-11-21 12:11:21 +01:00
|
|
|
fee_reserve_total_msat = fee_reserve_total(
|
|
|
|
invoice.amount_msat, internal=False
|
|
|
|
)
|
2022-07-07 14:30:16 +02:00
|
|
|
logger.debug(f"creating temporary payment with id {temp_id}")
|
2021-11-13 11:42:11 +01:00
|
|
|
# create a temporary payment here so we can check if
|
|
|
|
# the balance is enough in the next step
|
2023-04-03 15:34:55 +02:00
|
|
|
try:
|
2023-07-26 12:08:22 +02:00
|
|
|
new_payment = await create_payment(
|
2023-04-03 15:34:55 +02:00
|
|
|
checking_id=temp_id,
|
2023-11-21 12:11:21 +01:00
|
|
|
fee=-abs(fee_reserve_total_msat),
|
2023-04-03 15:34:55 +02:00
|
|
|
conn=conn,
|
|
|
|
**payment_kwargs,
|
|
|
|
)
|
|
|
|
except Exception as e:
|
|
|
|
logger.error(f"could not create temporary payment: {e}")
|
|
|
|
# happens if the same wallet tries to pay an invoice twice
|
|
|
|
raise PaymentFailure("Could not make payment.")
|
2021-11-13 11:42:11 +01:00
|
|
|
|
2022-03-16 07:20:15 +01:00
|
|
|
# do the balance check
|
|
|
|
wallet = await get_wallet(wallet_id, conn=conn)
|
2023-04-03 12:23:01 +02:00
|
|
|
assert wallet, "Wallet for balancecheck could not be fetched"
|
2022-03-16 07:20:15 +01:00
|
|
|
if wallet.balance_msat < 0:
|
2022-07-07 14:30:16 +02:00
|
|
|
logger.debug("balance is too low, deleting temporary payment")
|
2023-11-21 12:11:21 +01:00
|
|
|
if (
|
|
|
|
not internal_checking_id
|
|
|
|
and wallet.balance_msat > -fee_reserve_total_msat
|
|
|
|
):
|
2022-03-16 07:20:15 +01:00
|
|
|
raise PaymentFailure(
|
2023-11-21 12:11:21 +01:00
|
|
|
f"You must reserve at least ({round(fee_reserve_total_msat/1000)}"
|
|
|
|
" sat) to cover potential routing fees."
|
2022-01-30 20:43:30 +01:00
|
|
|
)
|
2022-03-16 07:20:15 +01:00
|
|
|
raise PermissionError("Insufficient balance.")
|
2021-11-12 17:05:10 +01:00
|
|
|
|
|
|
|
if internal_checking_id:
|
2023-11-21 12:11:21 +01:00
|
|
|
service_fee_msat = service_fee(invoice.amount_msat, internal=True)
|
2022-07-07 14:30:16 +02:00
|
|
|
logger.debug(f"marking temporary payment as not pending {internal_checking_id}")
|
2021-11-12 17:05:10 +01:00
|
|
|
# mark the invoice from the other side as not pending anymore
|
|
|
|
# so the other side only has access to his new money when we are sure
|
|
|
|
# the payer has enough to deduct from
|
2021-11-13 11:42:11 +01:00
|
|
|
async with db.connect() as conn:
|
|
|
|
await update_payment_status(
|
|
|
|
checking_id=internal_checking_id, pending=False, conn=conn
|
|
|
|
)
|
2023-07-26 12:08:22 +02:00
|
|
|
await send_payment_notification(wallet, new_payment)
|
2020-04-11 22:18:17 +02:00
|
|
|
|
2021-11-12 17:05:10 +01:00
|
|
|
# notify receiver asynchronously
|
|
|
|
from lnbits.tasks import internal_invoice_queue
|
2021-10-18 14:24:32 +02:00
|
|
|
|
2022-10-04 09:51:47 +02:00
|
|
|
logger.debug(f"enqueuing internal invoice {internal_checking_id}")
|
2021-11-12 17:05:10 +01:00
|
|
|
await internal_invoice_queue.put(internal_checking_id)
|
|
|
|
else:
|
2023-11-21 12:11:21 +01:00
|
|
|
fee_reserve_msat = fee_reserve(invoice.amount_msat, internal=False)
|
|
|
|
service_fee_msat = service_fee(invoice.amount_msat, internal=False)
|
2022-07-07 14:30:16 +02:00
|
|
|
logger.debug(f"backend: sending payment {temp_id}")
|
2021-11-12 17:05:10 +01:00
|
|
|
# actually pay the external invoice
|
2022-10-05 09:46:59 +02:00
|
|
|
WALLET = get_wallet_class()
|
2022-03-16 07:20:15 +01:00
|
|
|
payment: PaymentResponse = await WALLET.pay_invoice(
|
|
|
|
payment_request, fee_reserve_msat
|
|
|
|
)
|
2022-08-30 13:28:58 +02:00
|
|
|
|
|
|
|
if payment.checking_id and payment.checking_id != temp_id:
|
|
|
|
logger.warning(
|
2023-08-24 11:26:09 +02:00
|
|
|
f"backend sent unexpected checking_id (expected: {temp_id} got:"
|
|
|
|
f" {payment.checking_id})"
|
2022-08-30 13:28:58 +02:00
|
|
|
)
|
|
|
|
|
2022-07-07 14:30:16 +02:00
|
|
|
logger.debug(f"backend: pay_invoice finished {temp_id}")
|
2023-01-21 16:02:35 +01:00
|
|
|
if payment.checking_id and payment.ok is not False:
|
2022-08-30 13:28:58 +02:00
|
|
|
# payment.ok can be True (paid) or None (pending)!
|
|
|
|
logger.debug(f"updating payment {temp_id}")
|
2021-11-13 11:42:11 +01:00
|
|
|
async with db.connect() as conn:
|
2022-08-30 13:28:58 +02:00
|
|
|
await update_payment_details(
|
|
|
|
checking_id=temp_id,
|
2023-01-21 16:02:35 +01:00
|
|
|
pending=payment.ok is not True,
|
2023-11-21 12:11:21 +01:00
|
|
|
fee=-(
|
|
|
|
abs(payment.fee_msat if payment.fee_msat else 0)
|
|
|
|
+ abs(service_fee_msat)
|
|
|
|
),
|
2021-03-26 23:10:30 +01:00
|
|
|
preimage=payment.preimage,
|
2022-08-30 13:28:58 +02:00
|
|
|
new_checking_id=payment.checking_id,
|
2021-03-26 23:10:30 +01:00
|
|
|
conn=conn,
|
|
|
|
)
|
2023-05-22 13:38:26 +02:00
|
|
|
wallet = await get_wallet(wallet_id, conn=conn)
|
2023-07-26 12:08:22 +02:00
|
|
|
updated = await get_wallet_payment(
|
|
|
|
wallet_id, payment.checking_id, conn=conn
|
|
|
|
)
|
|
|
|
if wallet and updated:
|
|
|
|
await send_payment_notification(wallet, updated)
|
2022-08-30 13:28:58 +02:00
|
|
|
logger.debug(f"payment successful {payment.checking_id}")
|
2023-01-21 16:02:35 +01:00
|
|
|
elif payment.checking_id is None and payment.ok is False:
|
2022-08-30 13:28:58 +02:00
|
|
|
# payment failed
|
2023-01-21 16:27:15 +01:00
|
|
|
logger.warning("backend sent payment failure")
|
2021-11-29 18:10:11 +01:00
|
|
|
async with db.connect() as conn:
|
2022-07-07 14:30:16 +02:00
|
|
|
logger.debug(f"deleting temporary payment {temp_id}")
|
2022-09-18 15:27:03 +02:00
|
|
|
await delete_wallet_payment(temp_id, wallet_id, conn=conn)
|
2021-11-12 17:05:10 +01:00
|
|
|
raise PaymentFailure(
|
2022-09-22 10:15:28 +02:00
|
|
|
f"Payment failed: {payment.error_message}"
|
|
|
|
or "Payment failed, but backend didn't give us an error message."
|
2021-11-12 17:05:10 +01:00
|
|
|
)
|
2022-08-30 13:28:58 +02:00
|
|
|
else:
|
|
|
|
logger.warning(
|
2023-08-24 11:26:09 +02:00
|
|
|
"didn't receive checking_id from backend, payment may be stuck in"
|
|
|
|
f" database: {temp_id}"
|
2022-08-30 13:28:58 +02:00
|
|
|
)
|
|
|
|
|
2023-11-21 12:11:21 +01:00
|
|
|
# credit service fee wallet
|
|
|
|
if settings.lnbits_service_fee_wallet and service_fee_msat:
|
|
|
|
new_payment = await create_payment(
|
|
|
|
wallet_id=settings.lnbits_service_fee_wallet,
|
|
|
|
fee=0,
|
|
|
|
amount=abs(service_fee_msat),
|
|
|
|
memo="service fee",
|
|
|
|
checking_id="service_fee" + temp_id,
|
|
|
|
payment_request=payment_request,
|
|
|
|
payment_hash=invoice.payment_hash,
|
|
|
|
pending=False,
|
|
|
|
)
|
2021-11-12 17:05:10 +01:00
|
|
|
return invoice.payment_hash
|
2020-04-16 17:10:53 +02:00
|
|
|
|
|
|
|
|
2021-03-24 04:40:32 +01:00
|
|
|
async def redeem_lnurl_withdraw(
|
2021-03-26 23:10:30 +01:00
|
|
|
wallet_id: str,
|
2021-04-17 23:27:15 +02:00
|
|
|
lnurl_request: str,
|
2021-03-26 23:10:30 +01:00
|
|
|
memo: Optional[str] = None,
|
2021-04-17 23:27:15 +02:00
|
|
|
extra: Optional[Dict] = None,
|
2021-04-18 04:44:26 +02:00
|
|
|
wait_seconds: int = 0,
|
2021-03-26 23:10:30 +01:00
|
|
|
conn: Optional[Connection] = None,
|
2021-03-24 04:40:32 +01:00
|
|
|
) -> None:
|
2021-04-22 04:22:29 +02:00
|
|
|
if not lnurl_request:
|
|
|
|
return None
|
|
|
|
|
2021-04-17 23:27:15 +02:00
|
|
|
res = {}
|
|
|
|
|
|
|
|
async with httpx.AsyncClient() as client:
|
|
|
|
lnurl = decode_lnurl(lnurl_request)
|
|
|
|
r = await client.get(str(lnurl))
|
|
|
|
res = r.json()
|
|
|
|
|
2021-04-24 00:24:04 +02:00
|
|
|
try:
|
|
|
|
_, payment_request = await create_invoice(
|
|
|
|
wallet_id=wallet_id,
|
|
|
|
amount=int(res["maxWithdrawable"] / 1000),
|
|
|
|
memo=memo or res["defaultDescription"] or "",
|
|
|
|
extra=extra,
|
|
|
|
conn=conn,
|
|
|
|
)
|
2023-08-16 12:17:54 +02:00
|
|
|
except Exception:
|
2022-07-17 22:25:37 +02:00
|
|
|
logger.warning(
|
2023-08-24 11:26:09 +02:00
|
|
|
f"failed to create invoice on redeem_lnurl_withdraw "
|
|
|
|
f"from {lnurl}. params: {res}"
|
2021-04-24 00:24:04 +02:00
|
|
|
)
|
|
|
|
return None
|
2020-09-29 23:24:08 +02:00
|
|
|
|
2021-04-18 04:44:26 +02:00
|
|
|
if wait_seconds:
|
2021-08-30 19:55:02 +02:00
|
|
|
await asyncio.sleep(wait_seconds)
|
2021-04-18 04:44:26 +02:00
|
|
|
|
2021-10-17 19:33:29 +02:00
|
|
|
params = {"k1": res["k1"], "pr": payment_request}
|
2021-04-18 04:44:26 +02:00
|
|
|
|
|
|
|
try:
|
|
|
|
params["balanceNotify"] = url_for(
|
2021-08-27 20:54:42 +02:00
|
|
|
f"/withdraw/notify/{urlparse(lnurl_request).netloc}",
|
|
|
|
external=True,
|
2021-04-18 04:44:26 +02:00
|
|
|
wal=wallet_id,
|
|
|
|
)
|
|
|
|
except Exception:
|
|
|
|
pass
|
|
|
|
|
2020-09-29 23:24:08 +02:00
|
|
|
async with httpx.AsyncClient() as client:
|
2021-06-17 18:26:09 +02:00
|
|
|
try:
|
|
|
|
await client.get(res["callback"], params=params)
|
|
|
|
except Exception:
|
|
|
|
pass
|
2020-09-29 23:24:08 +02:00
|
|
|
|
|
|
|
|
2021-03-26 23:10:30 +01:00
|
|
|
async def perform_lnurlauth(
|
2022-07-19 11:35:28 +02:00
|
|
|
callback: str,
|
|
|
|
wallet: WalletTypeInfo = Depends(require_admin_key),
|
|
|
|
conn: Optional[Connection] = None,
|
2021-03-26 23:10:30 +01:00
|
|
|
) -> Optional[LnurlErrorResponse]:
|
2020-11-12 02:37:55 +01:00
|
|
|
cb = urlparse(callback)
|
|
|
|
|
2022-12-28 12:36:39 +01:00
|
|
|
k1 = bytes.fromhex(parse_qs(cb.query)["k1"][0])
|
2022-07-20 13:41:13 +02:00
|
|
|
|
2022-07-19 11:35:28 +02:00
|
|
|
key = wallet.wallet.lnurlauth_key(cb.netloc)
|
2020-11-11 06:41:30 +01:00
|
|
|
|
|
|
|
def int_to_bytes_suitable_der(x: int) -> bytes:
|
|
|
|
"""for strict DER we need to encode the integer with some quirks"""
|
|
|
|
b = x.to_bytes((x.bit_length() + 7) // 8, "big")
|
|
|
|
|
|
|
|
if len(b) == 0:
|
|
|
|
# ensure there's at least one byte when the int is zero
|
|
|
|
return bytes([0])
|
|
|
|
|
|
|
|
if b[0] & 0x80 != 0:
|
|
|
|
# ensure it doesn't start with a 0x80 and so it isn't
|
|
|
|
# interpreted as a negative number
|
|
|
|
return bytes([0]) + b
|
|
|
|
|
|
|
|
return b
|
|
|
|
|
2023-03-02 09:27:19 +01:00
|
|
|
def encode_strict_der(r: int, s: int, order: int):
|
2020-11-11 06:41:30 +01:00
|
|
|
# if s > order/2 verification will fail sometimes
|
2023-04-17 08:29:01 +02:00
|
|
|
# so we must fix it here see:
|
|
|
|
# https://github.com/indutny/elliptic/blob/e71b2d9359c5fe9437fbf46f1f05096de447de57/lib/elliptic/ec/index.js#L146-L147
|
2023-02-02 13:58:10 +01:00
|
|
|
if s > order // 2:
|
|
|
|
s = order - s
|
2020-11-11 06:41:30 +01:00
|
|
|
|
|
|
|
# now we do the strict DER encoding copied from
|
|
|
|
# https://github.com/KiriKiri/bip66 (without any checks)
|
2023-02-02 13:58:10 +01:00
|
|
|
r_temp = int_to_bytes_suitable_der(r)
|
|
|
|
s_temp = int_to_bytes_suitable_der(s)
|
2020-11-11 06:41:30 +01:00
|
|
|
|
2023-02-02 13:58:10 +01:00
|
|
|
r_len = len(r_temp)
|
|
|
|
s_len = len(s_temp)
|
2020-11-11 06:41:30 +01:00
|
|
|
sign_len = 6 + r_len + s_len
|
|
|
|
|
|
|
|
signature = BytesIO()
|
2022-03-16 07:20:15 +01:00
|
|
|
signature.write(0x30.to_bytes(1, "big", signed=False))
|
2020-11-11 06:41:30 +01:00
|
|
|
signature.write((sign_len - 2).to_bytes(1, "big", signed=False))
|
2022-03-16 07:20:15 +01:00
|
|
|
signature.write(0x02.to_bytes(1, "big", signed=False))
|
2020-11-11 06:41:30 +01:00
|
|
|
signature.write(r_len.to_bytes(1, "big", signed=False))
|
2023-02-02 13:58:10 +01:00
|
|
|
signature.write(r_temp)
|
2022-03-16 07:20:15 +01:00
|
|
|
signature.write(0x02.to_bytes(1, "big", signed=False))
|
2020-11-11 06:41:30 +01:00
|
|
|
signature.write(s_len.to_bytes(1, "big", signed=False))
|
2023-02-02 13:58:10 +01:00
|
|
|
signature.write(s_temp)
|
2020-11-11 06:41:30 +01:00
|
|
|
|
|
|
|
return signature.getvalue()
|
|
|
|
|
|
|
|
sig = key.sign_digest_deterministic(k1, sigencode=encode_strict_der)
|
2020-11-10 04:25:46 +01:00
|
|
|
|
|
|
|
async with httpx.AsyncClient() as client:
|
2023-04-03 12:23:01 +02:00
|
|
|
assert key.verifying_key, "LNURLauth verifying_key does not exist"
|
2020-11-10 04:25:46 +01:00
|
|
|
r = await client.get(
|
|
|
|
callback,
|
2020-12-31 18:50:16 +01:00
|
|
|
params={
|
|
|
|
"k1": k1.hex(),
|
|
|
|
"key": key.verifying_key.to_string("compressed").hex(),
|
|
|
|
"sig": sig.hex(),
|
|
|
|
},
|
2020-11-10 04:25:46 +01:00
|
|
|
)
|
|
|
|
try:
|
|
|
|
resp = json.loads(r.text)
|
|
|
|
if resp["status"] == "OK":
|
|
|
|
return None
|
|
|
|
|
2020-11-11 03:01:55 +01:00
|
|
|
return LnurlErrorResponse(reason=resp["reason"])
|
2020-11-10 04:25:46 +01:00
|
|
|
except (KeyError, json.decoder.JSONDecodeError):
|
2020-12-31 18:50:16 +01:00
|
|
|
return LnurlErrorResponse(
|
2021-10-17 19:33:29 +02:00
|
|
|
reason=r.text[:200] + "..." if len(r.text) > 200 else r.text
|
2020-12-31 18:50:16 +01:00
|
|
|
)
|
2020-11-10 04:25:46 +01:00
|
|
|
|
|
|
|
|
2022-08-09 11:49:39 +02:00
|
|
|
async def check_transaction_status(
|
2021-11-12 05:14:55 +01:00
|
|
|
wallet_id: str, payment_hash: str, conn: Optional[Connection] = None
|
2021-03-26 23:10:30 +01:00
|
|
|
) -> PaymentStatus:
|
2022-08-30 13:28:58 +02:00
|
|
|
payment: Optional[Payment] = await get_wallet_payment(
|
|
|
|
wallet_id, payment_hash, conn=conn
|
|
|
|
)
|
2020-09-03 02:11:08 +02:00
|
|
|
if not payment:
|
|
|
|
return PaymentStatus(None)
|
2021-10-20 04:28:31 +02:00
|
|
|
if not payment.pending:
|
2022-08-30 13:28:58 +02:00
|
|
|
# note: before, we still checked the status of the payment again
|
2023-03-29 19:40:52 +02:00
|
|
|
return PaymentStatus(True, fee_msat=payment.fee)
|
2022-08-30 13:28:58 +02:00
|
|
|
|
|
|
|
status: PaymentStatus = await payment.check_status()
|
2021-10-20 04:28:31 +02:00
|
|
|
return status
|
2021-04-17 23:27:15 +02:00
|
|
|
|
|
|
|
|
2023-08-24 11:26:09 +02:00
|
|
|
# WARN: this same value must be used for balance check and passed to
|
|
|
|
# WALLET.pay_invoice(), it may cause a vulnerability if the values differ
|
2023-11-21 12:11:21 +01:00
|
|
|
def fee_reserve(amount_msat: int, internal: bool = False) -> int:
|
2022-10-03 16:46:46 +02:00
|
|
|
reserve_min = settings.lnbits_reserve_fee_min
|
|
|
|
reserve_percent = settings.lnbits_reserve_fee_percent
|
|
|
|
return max(int(reserve_min), int(amount_msat * reserve_percent / 100.0))
|
2022-12-08 14:41:52 +01:00
|
|
|
|
|
|
|
|
2023-11-21 12:11:21 +01:00
|
|
|
def service_fee(amount_msat: int, internal: bool = False) -> int:
|
|
|
|
service_fee_percent = settings.lnbits_service_fee
|
|
|
|
fee_max = settings.lnbits_service_fee_max * 1000
|
|
|
|
if settings.lnbits_service_fee_wallet:
|
|
|
|
if internal and settings.lnbits_service_fee_ignore_internal:
|
|
|
|
return 0
|
|
|
|
fee_percentage = int(amount_msat / 100 * service_fee_percent)
|
|
|
|
if fee_max > 0 and fee_percentage > fee_max:
|
|
|
|
return fee_max
|
|
|
|
else:
|
|
|
|
return fee_percentage
|
|
|
|
else:
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
|
|
def fee_reserve_total(amount_msat: int, internal: bool = False) -> int:
|
|
|
|
return fee_reserve(amount_msat, internal) + service_fee(amount_msat, internal)
|
|
|
|
|
|
|
|
|
2023-07-26 12:08:22 +02:00
|
|
|
async def send_payment_notification(wallet: Wallet, payment: Payment):
|
|
|
|
await websocketUpdater(
|
|
|
|
wallet.id,
|
|
|
|
json.dumps(
|
|
|
|
{
|
|
|
|
"wallet_balance": wallet.balance,
|
|
|
|
"payment": payment.dict(),
|
|
|
|
}
|
|
|
|
),
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2022-12-08 14:41:52 +01:00
|
|
|
async def update_wallet_balance(wallet_id: str, amount: int):
|
2023-03-30 14:21:01 +02:00
|
|
|
payment_hash, _ = await create_invoice(
|
2022-12-08 14:41:52 +01:00
|
|
|
wallet_id=wallet_id,
|
2023-03-30 14:21:01 +02:00
|
|
|
amount=amount,
|
2022-12-08 14:41:52 +01:00
|
|
|
memo="Admin top up",
|
2023-03-30 14:21:01 +02:00
|
|
|
internal=True,
|
2022-12-08 14:41:52 +01:00
|
|
|
)
|
2023-03-30 14:21:01 +02:00
|
|
|
async with db.connect() as conn:
|
|
|
|
checking_id = await check_internal(payment_hash, conn=conn)
|
|
|
|
assert checking_id, "newly created checking_id cannot be retrieved"
|
|
|
|
await update_payment_status(checking_id=checking_id, pending=False, conn=conn)
|
|
|
|
# notify receiver asynchronously
|
|
|
|
from lnbits.tasks import internal_invoice_queue
|
2022-12-08 14:41:52 +01:00
|
|
|
|
2023-03-30 14:21:01 +02:00
|
|
|
await internal_invoice_queue.put(checking_id)
|
2022-12-08 14:41:52 +01:00
|
|
|
|
|
|
|
|
|
|
|
async def check_admin_settings():
|
2023-05-09 10:22:19 +02:00
|
|
|
if settings.super_user:
|
|
|
|
settings.super_user = to_valid_user_id(settings.super_user).hex
|
|
|
|
|
2022-12-08 14:41:52 +01:00
|
|
|
if settings.lnbits_admin_ui:
|
2022-12-16 10:22:32 +01:00
|
|
|
settings_db = await get_super_settings()
|
|
|
|
if not settings_db:
|
2022-12-08 14:41:52 +01:00
|
|
|
# create new settings if table is empty
|
2022-12-16 10:22:32 +01:00
|
|
|
logger.warning("Settings DB empty. Inserting default settings.")
|
|
|
|
settings_db = await init_admin_settings(settings.super_user)
|
2023-09-11 15:48:49 +02:00
|
|
|
logger.warning("Initialized settings from environment variables.")
|
2022-12-16 10:22:32 +01:00
|
|
|
|
|
|
|
if settings.super_user and settings.super_user != settings_db.super_user:
|
|
|
|
# .env super_user overwrites DB super_user
|
|
|
|
settings_db = await update_super_user(settings.super_user)
|
2022-12-08 14:41:52 +01:00
|
|
|
|
2022-12-16 10:22:32 +01:00
|
|
|
update_cached_settings(settings_db.dict())
|
2022-12-08 14:41:52 +01:00
|
|
|
|
2023-09-11 13:19:19 +02:00
|
|
|
# saving superuser to {data_dir}/.super_user file
|
|
|
|
with open(Path(settings.lnbits_data_folder) / ".super_user", "w") as file:
|
2023-04-25 10:25:50 +02:00
|
|
|
file.write(settings.super_user)
|
|
|
|
|
2022-12-08 14:41:52 +01:00
|
|
|
# callback for saas
|
|
|
|
if (
|
|
|
|
settings.lnbits_saas_callback
|
|
|
|
and settings.lnbits_saas_secret
|
|
|
|
and settings.lnbits_saas_instance_id
|
|
|
|
):
|
2022-12-12 08:45:08 +01:00
|
|
|
send_admin_user_to_saas()
|
2022-12-09 13:06:47 +01:00
|
|
|
|
2023-09-11 13:19:19 +02:00
|
|
|
logger.success(
|
|
|
|
"✔️ Admin UI is enabled. run `poetry run lnbits-cli superuser` "
|
|
|
|
"to get the superuser."
|
|
|
|
)
|
|
|
|
|
2022-12-09 13:09:16 +01:00
|
|
|
|
2023-09-11 15:48:49 +02:00
|
|
|
async def check_webpush_settings():
|
|
|
|
if not settings.lnbits_webpush_privkey:
|
|
|
|
vapid = Vapid()
|
|
|
|
vapid.generate_keys()
|
|
|
|
privkey = vapid.private_pem()
|
|
|
|
assert vapid.public_key, "VAPID public key does not exist"
|
|
|
|
pubkey = b64urlencode(
|
|
|
|
vapid.public_key.public_bytes(
|
|
|
|
serialization.Encoding.X962,
|
|
|
|
serialization.PublicFormat.UncompressedPoint,
|
|
|
|
)
|
|
|
|
)
|
|
|
|
push_settings = {
|
|
|
|
"lnbits_webpush_privkey": privkey.decode(),
|
|
|
|
"lnbits_webpush_pubkey": pubkey,
|
|
|
|
}
|
|
|
|
update_cached_settings(push_settings)
|
2023-09-12 11:59:32 +02:00
|
|
|
await update_admin_settings(EditableSettings(**push_settings))
|
2023-09-11 15:48:49 +02:00
|
|
|
|
|
|
|
logger.info("Initialized webpush settings with generated VAPID key pair.")
|
|
|
|
logger.info(f"Pubkey: {settings.lnbits_webpush_pubkey}")
|
|
|
|
|
|
|
|
|
2022-12-09 13:53:51 +01:00
|
|
|
def update_cached_settings(sets_dict: dict):
|
|
|
|
for key, value in sets_dict.items():
|
2023-01-21 16:07:40 +01:00
|
|
|
if key not in readonly_variables:
|
2022-12-09 13:53:51 +01:00
|
|
|
try:
|
|
|
|
setattr(settings, key, value)
|
2023-08-16 12:17:54 +02:00
|
|
|
except Exception:
|
2023-05-11 02:14:07 +02:00
|
|
|
logger.warning(f"Failed overriding setting: {key}, value: {value}")
|
2022-12-12 09:12:35 +01:00
|
|
|
if "super_user" in sets_dict:
|
|
|
|
setattr(settings, "super_user", sets_dict["super_user"])
|
2022-12-09 13:53:51 +01:00
|
|
|
|
|
|
|
|
2023-02-02 13:58:10 +01:00
|
|
|
async def init_admin_settings(super_user: Optional[str] = None) -> SuperSettings:
|
2022-12-16 11:09:13 +01:00
|
|
|
account = None
|
|
|
|
if super_user:
|
|
|
|
account = await get_account(super_user)
|
|
|
|
if not account:
|
2023-05-09 10:22:19 +02:00
|
|
|
account = await create_account(user_id=super_user)
|
2022-12-16 11:09:13 +01:00
|
|
|
if not account.wallets or len(account.wallets) == 0:
|
|
|
|
await create_wallet(user_id=account.id)
|
|
|
|
|
2022-12-19 09:10:41 +01:00
|
|
|
editable_settings = EditableSettings.from_dict(settings.dict())
|
2022-12-16 11:09:13 +01:00
|
|
|
|
|
|
|
return await create_admin_settings(account.id, editable_settings.dict())
|
|
|
|
|
|
|
|
|
2022-11-28 14:04:35 +01:00
|
|
|
class WebsocketConnectionManager:
|
2023-08-23 21:24:54 +02:00
|
|
|
def __init__(self) -> None:
|
2022-11-24 01:21:39 +01:00
|
|
|
self.active_connections: List[WebSocket] = []
|
|
|
|
|
2023-08-18 11:22:22 +02:00
|
|
|
async def connect(self, websocket: WebSocket, item_id: str):
|
|
|
|
logger.debug(f"Websocket connected to {item_id}")
|
2022-11-24 01:21:39 +01:00
|
|
|
await websocket.accept()
|
2022-11-29 12:09:54 +01:00
|
|
|
self.active_connections.append(websocket)
|
2022-11-24 01:21:39 +01:00
|
|
|
|
|
|
|
def disconnect(self, websocket: WebSocket):
|
|
|
|
self.active_connections.remove(websocket)
|
|
|
|
|
|
|
|
async def send_data(self, message: str, item_id: str):
|
|
|
|
for connection in self.active_connections:
|
2022-12-01 15:41:57 +01:00
|
|
|
if connection.path_params["item_id"] == item_id:
|
2022-11-24 01:21:39 +01:00
|
|
|
await connection.send_text(message)
|
|
|
|
|
2022-11-24 01:34:46 +01:00
|
|
|
|
2022-11-28 14:04:35 +01:00
|
|
|
websocketManager = WebsocketConnectionManager()
|
2022-11-24 01:21:39 +01:00
|
|
|
|
2022-11-24 01:34:46 +01:00
|
|
|
|
2022-11-24 01:21:39 +01:00
|
|
|
async def websocketUpdater(item_id, data):
|
2022-11-24 01:34:46 +01:00
|
|
|
return await websocketManager.send_data(f"{data}", item_id)
|
2023-06-20 11:26:33 +02:00
|
|
|
|
|
|
|
|
|
|
|
async def switch_to_voidwallet() -> None:
|
|
|
|
WALLET = get_wallet_class()
|
|
|
|
if WALLET.__class__.__name__ == "VoidWallet":
|
|
|
|
return
|
|
|
|
set_wallet_class("VoidWallet")
|
|
|
|
settings.lnbits_backend_wallet_class = "VoidWallet"
|
|
|
|
|
|
|
|
|
|
|
|
async def get_balance_delta() -> Tuple[int, int, int]:
|
|
|
|
WALLET = get_wallet_class()
|
|
|
|
total_balance = await get_total_balance()
|
|
|
|
error_message, node_balance = await WALLET.status()
|
|
|
|
if error_message:
|
|
|
|
raise Exception(error_message)
|
|
|
|
return node_balance - total_balance, node_balance, total_balance
|
2023-10-10 12:03:24 +02:00
|
|
|
|
|
|
|
|
|
|
|
def get_bolt11_expiry(invoice: Bolt11) -> datetime.datetime:
|
|
|
|
if invoice.expiry:
|
|
|
|
return datetime.datetime.fromtimestamp(invoice.date + invoice.expiry)
|
|
|
|
else:
|
|
|
|
# assume maximum bolt11 expiry of 31 days to be on the safe side
|
|
|
|
return datetime.datetime.now() + datetime.timedelta(days=31)
|