2021-08-30 19:55:02 +02:00
|
|
|
import asyncio
|
2020-11-10 04:25:46 +01:00
|
|
|
import json
|
|
|
|
from binascii import unhexlify
|
2021-10-18 13:23:57 +02:00
|
|
|
from io import BytesIO
|
|
|
|
from typing import Dict, Optional, Tuple
|
|
|
|
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
|
|
|
|
from lnurl import LnurlErrorResponse
|
|
|
|
from lnurl import decode as decode_lnurl # type: ignore
|
2020-04-11 22:18:17 +02:00
|
|
|
|
2020-09-01 03:12:46 +02:00
|
|
|
from lnbits import bolt11
|
2021-03-26 23:10:30 +01:00
|
|
|
from lnbits.db import Connection
|
2021-08-27 20:54:42 +02:00
|
|
|
from lnbits.helpers import url_for, urlsafe_short_hash
|
|
|
|
from lnbits.requestvars import g
|
2021-10-18 13:23:57 +02:00
|
|
|
from lnbits.settings import WALLET
|
|
|
|
from lnbits.wallets.base import PaymentResponse, PaymentStatus
|
2020-04-11 22:18:17 +02:00
|
|
|
|
2020-11-21 22:04:39 +01:00
|
|
|
from . import db
|
2021-03-24 04:40:32 +01:00
|
|
|
from .crud import (
|
2021-10-18 13:23:57 +02:00
|
|
|
check_internal,
|
2021-03-24 04:40:32 +01:00
|
|
|
create_payment,
|
|
|
|
delete_payment,
|
2021-10-18 13:23:57 +02:00
|
|
|
get_wallet,
|
2021-03-24 04:40:32 +01:00
|
|
|
get_wallet_payment,
|
2021-10-18 13:23:57 +02:00
|
|
|
update_payment_status,
|
2021-03-24 04:40:32 +01:00
|
|
|
)
|
2020-04-11 22:18:17 +02:00
|
|
|
|
2021-10-18 13:23:57 +02:00
|
|
|
try:
|
|
|
|
from typing import TypedDict # type: ignore
|
|
|
|
except ImportError: # pragma: nocover
|
|
|
|
from typing_extensions import TypedDict
|
|
|
|
|
|
|
|
|
2021-04-06 19:57:51 +02:00
|
|
|
class PaymentFailure(Exception):
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
2021-04-10 22:37:48 +02:00
|
|
|
class InvoiceFailure(Exception):
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
2020-11-21 22:04:39 +01:00
|
|
|
async def create_invoice(
|
2020-09-03 23:02:15 +02:00
|
|
|
*,
|
|
|
|
wallet_id: str,
|
2020-10-21 04:19:22 +02:00
|
|
|
amount: int, # in satoshis
|
2021-11-03 12:23:16 +01:00
|
|
|
memo: str,
|
2020-09-03 23:02:15 +02:00
|
|
|
description_hash: Optional[bytes] = None,
|
|
|
|
extra: Optional[Dict] = None,
|
2020-12-24 13:38:35 +01:00
|
|
|
webhook: Optional[str] = None,
|
2021-03-26 23:10:30 +01:00
|
|
|
conn: Optional[Connection] = None,
|
2020-09-01 03:12:46 +02:00
|
|
|
) -> Tuple[str, str]:
|
2021-11-03 12:07:01 +01:00
|
|
|
invoice_memo = None if description_hash else memo
|
2021-11-03 12:14:41 +01:00
|
|
|
|
2021-03-24 05:01:09 +01:00
|
|
|
ok, checking_id, payment_request, error_message = await WALLET.create_invoice(
|
2021-11-03 12:07:01 +01:00
|
|
|
amount=amount, memo=invoice_memo, description_hash=description_hash
|
2020-09-03 02:11:08 +02:00
|
|
|
)
|
2020-04-11 22:18:17 +02:00
|
|
|
if not ok:
|
2021-04-10 22:37:48 +02:00
|
|
|
raise InvoiceFailure(error_message or "Unexpected backend error.")
|
2020-04-11 22:18:17 +02:00
|
|
|
|
2020-09-01 03:12:46 +02:00
|
|
|
invoice = bolt11.decode(payment_request)
|
2020-04-11 22:18:17 +02:00
|
|
|
|
2020-09-01 03:12:46 +02:00
|
|
|
amount_msat = amount * 1000
|
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,
|
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:
|
2021-11-13 11:42:11 +01:00
|
|
|
async with (db.reuse_conn(conn) if conn else db.connect()) as conn:
|
|
|
|
temp_id = f"temp_{urlsafe_short_hash()}"
|
|
|
|
internal_id = f"internal_{urlsafe_short_hash()}"
|
|
|
|
|
|
|
|
invoice = bolt11.decode(payment_request)
|
|
|
|
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.")
|
|
|
|
|
2021-12-29 15:55:52 +01:00
|
|
|
wallet = await get_wallet(wallet_id, conn=conn)
|
2021-12-02 22:22:30 +01:00
|
|
|
|
2021-11-13 11:42:11 +01:00
|
|
|
# put all parameters that don't change here
|
|
|
|
PaymentKwargs = TypedDict(
|
|
|
|
"PaymentKwargs",
|
|
|
|
{
|
|
|
|
"wallet_id": str,
|
|
|
|
"payment_request": str,
|
|
|
|
"payment_hash": str,
|
|
|
|
"amount": int,
|
|
|
|
"memo": str,
|
|
|
|
"extra": Optional[Dict],
|
|
|
|
},
|
2021-03-24 04:40:32 +01:00
|
|
|
)
|
2021-11-13 11:42:11 +01:00
|
|
|
payment_kwargs: PaymentKwargs = dict(
|
|
|
|
wallet_id=wallet_id,
|
|
|
|
payment_request=payment_request,
|
|
|
|
payment_hash=invoice.payment_hash,
|
|
|
|
amount=-invoice.amount_msat,
|
|
|
|
memo=description or invoice.description or "",
|
|
|
|
extra=extra,
|
2021-03-26 23:10:30 +01:00
|
|
|
)
|
|
|
|
|
2021-11-13 11:42:11 +01:00
|
|
|
# check_internal() returns the checking_id of the invoice we're waiting for
|
|
|
|
internal_checking_id = await check_internal(invoice.payment_hash, conn=conn)
|
|
|
|
if internal_checking_id:
|
|
|
|
# create a new payment from this wallet
|
|
|
|
await create_payment(
|
|
|
|
checking_id=internal_id,
|
|
|
|
fee=0,
|
|
|
|
pending=False,
|
|
|
|
conn=conn,
|
|
|
|
**payment_kwargs,
|
|
|
|
)
|
|
|
|
else:
|
|
|
|
# create a temporary payment here so we can check if
|
|
|
|
# the balance is enough in the next step
|
|
|
|
await create_payment(
|
|
|
|
checking_id=temp_id,
|
|
|
|
fee=-fee_reserve(invoice.amount_msat),
|
|
|
|
conn=conn,
|
|
|
|
**payment_kwargs,
|
|
|
|
)
|
|
|
|
|
2021-12-29 15:55:52 +01:00
|
|
|
# do the balance check if internal payment
|
|
|
|
if internal_checking_id:
|
|
|
|
wallet = await get_wallet(wallet_id, conn=conn)
|
|
|
|
assert wallet
|
|
|
|
if wallet.balance_msat < 0:
|
|
|
|
raise PermissionError("Insufficient balance.")
|
|
|
|
|
|
|
|
# do the balance check if external payment
|
|
|
|
else:
|
|
|
|
if invoice.amount_msat > wallet.balance_msat - (wallet.balance_msat / 100 * 2):
|
|
|
|
raise PermissionError("LNbits requires you keep at least 2% reserve to cover potential routing fees.")
|
2021-11-12 17:05:10 +01:00
|
|
|
|
|
|
|
if internal_checking_id:
|
|
|
|
# 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
|
|
|
|
)
|
2020-04-11 22:18:17 +02:00
|
|
|
|
2021-11-12 17:05:10 +01:00
|
|
|
# notify receiver asynchronously
|
2021-03-26 23:10:30 +01:00
|
|
|
|
2021-11-12 17:05:10 +01:00
|
|
|
from lnbits.tasks import internal_invoice_queue
|
2021-10-18 14:24:32 +02:00
|
|
|
|
2021-11-12 17:05:10 +01:00
|
|
|
await internal_invoice_queue.put(internal_checking_id)
|
|
|
|
else:
|
|
|
|
# actually pay the external invoice
|
|
|
|
payment: PaymentResponse = await WALLET.pay_invoice(payment_request)
|
|
|
|
if payment.checking_id:
|
2021-11-13 11:42:11 +01:00
|
|
|
async with db.connect() as conn:
|
2021-03-26 23:10:30 +01:00
|
|
|
await create_payment(
|
|
|
|
checking_id=payment.checking_id,
|
2021-04-01 01:09:33 +02:00
|
|
|
fee=payment.fee_msat,
|
2021-03-26 23:10:30 +01:00
|
|
|
preimage=payment.preimage,
|
|
|
|
pending=payment.ok == None,
|
|
|
|
conn=conn,
|
|
|
|
**payment_kwargs,
|
|
|
|
)
|
|
|
|
await delete_payment(temp_id, conn=conn)
|
2021-11-12 17:05:10 +01:00
|
|
|
else:
|
2021-11-29 18:10:11 +01:00
|
|
|
async with db.connect() as conn:
|
|
|
|
await delete_payment(temp_id, conn=conn)
|
2021-11-12 17:05:10 +01:00
|
|
|
raise PaymentFailure(
|
|
|
|
payment.error_message
|
|
|
|
or "Payment failed, but backend didn't give us an error message."
|
|
|
|
)
|
2021-03-26 23:10:30 +01:00
|
|
|
|
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,
|
|
|
|
)
|
|
|
|
except:
|
|
|
|
print(
|
|
|
|
f"failed to create invoice on redeem_lnurl_withdraw from {lnurl}. params: {res}"
|
|
|
|
)
|
|
|
|
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(
|
2021-10-17 19:33:29 +02:00
|
|
|
callback: str, 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)
|
|
|
|
|
|
|
|
k1 = unhexlify(parse_qs(cb.query)["k1"][0])
|
2021-08-27 20:54:42 +02:00
|
|
|
key = g().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
|
|
|
|
|
|
|
|
def encode_strict_der(r_int, s_int, order):
|
|
|
|
# if s > order/2 verification will fail sometimes
|
|
|
|
# so we must fix it here (see https://github.com/indutny/elliptic/blob/e71b2d9359c5fe9437fbf46f1f05096de447de57/lib/elliptic/ec/index.js#L146-L147)
|
|
|
|
if s_int > order // 2:
|
|
|
|
s_int = order - s_int
|
|
|
|
|
|
|
|
# now we do the strict DER encoding copied from
|
|
|
|
# https://github.com/KiriKiri/bip66 (without any checks)
|
|
|
|
r = int_to_bytes_suitable_der(r_int)
|
|
|
|
s = int_to_bytes_suitable_der(s_int)
|
|
|
|
|
|
|
|
r_len = len(r)
|
|
|
|
s_len = len(s)
|
|
|
|
sign_len = 6 + r_len + s_len
|
|
|
|
|
|
|
|
signature = BytesIO()
|
|
|
|
signature.write(0x30 .to_bytes(1, "big", signed=False))
|
|
|
|
signature.write((sign_len - 2).to_bytes(1, "big", signed=False))
|
|
|
|
signature.write(0x02 .to_bytes(1, "big", signed=False))
|
|
|
|
signature.write(r_len.to_bytes(1, "big", signed=False))
|
|
|
|
signature.write(r)
|
|
|
|
signature.write(0x02 .to_bytes(1, "big", signed=False))
|
|
|
|
signature.write(s_len.to_bytes(1, "big", signed=False))
|
|
|
|
signature.write(s)
|
|
|
|
|
|
|
|
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:
|
|
|
|
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
|
|
|
|
|
|
|
|
2021-03-26 23:10:30 +01:00
|
|
|
async def check_invoice_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:
|
|
|
|
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
|
|
|
status = await WALLET.get_invoice_status(payment.checking_id)
|
|
|
|
if not payment.pending:
|
|
|
|
return status
|
|
|
|
if payment.is_out and status.failed:
|
2021-11-24 14:31:55 +01:00
|
|
|
print(
|
|
|
|
f" - deleting outgoing failed payment {payment.checking_id}: {status}")
|
2021-10-20 04:28:31 +02:00
|
|
|
await payment.delete()
|
|
|
|
elif not status.pending:
|
|
|
|
print(
|
|
|
|
f" - marking '{'in' if payment.is_in else 'out'}' {payment.checking_id} as not pending anymore: {status}"
|
|
|
|
)
|
|
|
|
await payment.set_pending(status.pending)
|
|
|
|
return status
|
2021-04-17 23:27:15 +02:00
|
|
|
|
|
|
|
|
|
|
|
def fee_reserve(amount_msat: int) -> int:
|
|
|
|
return max(1000, int(amount_msat * 0.01))
|