mirror of
https://github.com/lnbits/lnbits.git
synced 2025-04-07 03:18:10 +02:00
refactor: lnurl pay/withdraw/auth use lnurl lib more
- refactor lnurl endpoints into own router `lnurl_api.py` - add a new withdraw dialog to frontend (create invoice dialog was used before) - utilize lnurl library remove revert status remove
This commit is contained in:
parent
7298c4664b
commit
554a5c12a7
@ -8,6 +8,7 @@ from .views.extension_api import extension_router
|
||||
|
||||
# this compat is needed for usermanager extension
|
||||
from .views.generic import generic_router
|
||||
from .views.lnurl_api import lnurl_router
|
||||
from .views.node_api import node_router, public_node_router, super_node_router
|
||||
from .views.payment_api import payment_router
|
||||
from .views.public_api import public_router
|
||||
@ -38,3 +39,4 @@ def init_core_routers(app: FastAPI):
|
||||
app.include_router(tinyurl_router)
|
||||
app.include_router(webpush_router)
|
||||
app.include_router(users_router)
|
||||
app.include_router(lnurl_router)
|
||||
|
@ -1,6 +1,4 @@
|
||||
import datetime
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
@ -8,14 +6,12 @@ from enum import Enum
|
||||
from sqlite3 import Row
|
||||
from typing import Callable, Dict, List, Optional
|
||||
|
||||
from ecdsa import SECP256k1, SigningKey
|
||||
from fastapi import Query
|
||||
from lnurl import LnurlAuthResponse, LnurlPayResponse, LnurlWithdrawResponse
|
||||
from loguru import logger
|
||||
from pydantic import BaseModel
|
||||
|
||||
from lnbits.db import Connection, FilterModel, FromRowModel
|
||||
from lnbits.helpers import url_for
|
||||
from lnbits.lnurl import encode as lnurl_encode
|
||||
from lnbits.settings import settings
|
||||
from lnbits.wallets import get_funding_source
|
||||
from lnbits.wallets.base import PaymentPendingStatus, PaymentStatus
|
||||
@ -40,28 +36,6 @@ class Wallet(BaseWallet):
|
||||
def balance(self) -> int:
|
||||
return self.balance_msat // 1000
|
||||
|
||||
@property
|
||||
def withdrawable_balance(self) -> int:
|
||||
from .services import fee_reserve
|
||||
|
||||
return self.balance_msat - fee_reserve(self.balance_msat)
|
||||
|
||||
@property
|
||||
def lnurlwithdraw_full(self) -> str:
|
||||
url = url_for("/withdraw", external=True, usr=self.user, wal=self.id)
|
||||
try:
|
||||
return lnurl_encode(url)
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
def lnurlauth_key(self, domain: str) -> SigningKey:
|
||||
hashing_key = hashlib.sha256(self.id.encode()).digest()
|
||||
linking_key = hmac.digest(hashing_key, domain.encode(), "sha256")
|
||||
|
||||
return SigningKey.from_string(
|
||||
linking_key, curve=SECP256k1, hashfunc=hashlib.sha256
|
||||
)
|
||||
|
||||
async def get_payment(self, payment_hash: str) -> Optional["Payment"]:
|
||||
from .crud import get_standalone_payment
|
||||
|
||||
@ -397,15 +371,23 @@ class DecodePayment(BaseModel):
|
||||
data: str
|
||||
|
||||
|
||||
class CreateLnurl(BaseModel):
|
||||
description_hash: str
|
||||
callback: str
|
||||
class CreateLnurlAuth(BaseModel):
|
||||
auth_response: LnurlAuthResponse
|
||||
|
||||
|
||||
class CreateLnurlPay(BaseModel):
|
||||
pay_response: LnurlPayResponse
|
||||
amount: int
|
||||
comment: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
unit: Optional[str] = None
|
||||
|
||||
|
||||
class CreateLnurlWithdraw(BaseModel):
|
||||
withdraw_response: LnurlWithdrawResponse
|
||||
amount: int
|
||||
memo: Optional[str] = None
|
||||
|
||||
|
||||
class CreateInvoice(BaseModel):
|
||||
unit: str = "sat"
|
||||
internal: bool = False
|
||||
@ -418,7 +400,6 @@ class CreateInvoice(BaseModel):
|
||||
extra: Optional[dict] = None
|
||||
webhook: Optional[str] = None
|
||||
bolt11: Optional[str] = None
|
||||
lnurl_callback: Optional[str] = None
|
||||
|
||||
|
||||
class CreateTopup(BaseModel):
|
||||
@ -426,10 +407,6 @@ class CreateTopup(BaseModel):
|
||||
amount: int
|
||||
|
||||
|
||||
class CreateLnurlAuth(BaseModel):
|
||||
callback: str
|
||||
|
||||
|
||||
class CreateWallet(BaseModel):
|
||||
name: Optional[str] = None
|
||||
|
||||
|
@ -1,17 +1,13 @@
|
||||
import asyncio
|
||||
import datetime
|
||||
import json
|
||||
import time
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, Tuple, TypedDict
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
import httpx
|
||||
from bolt11 import decode as bolt11_decode
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
from fastapi import Depends, WebSocket
|
||||
from fastapi import WebSocket
|
||||
from loguru import logger
|
||||
from passlib.context import CryptContext
|
||||
from py_vapid import Vapid
|
||||
@ -20,13 +16,8 @@ from py_vapid.utils import b64urlencode
|
||||
from lnbits.core.db import db
|
||||
from lnbits.db import Connection
|
||||
from lnbits.decorators import (
|
||||
WalletTypeInfo,
|
||||
check_user_extension_access,
|
||||
require_admin_key,
|
||||
)
|
||||
from lnbits.helpers import url_for
|
||||
from lnbits.lnurl import LnurlErrorResponse
|
||||
from lnbits.lnurl import decode as decode_lnurl
|
||||
from lnbits.settings import (
|
||||
EditableSettings,
|
||||
SuperSettings,
|
||||
@ -475,140 +466,6 @@ async def check_wallet_daily_withdraw_limit(conn, wallet_id, amount_msat):
|
||||
)
|
||||
|
||||
|
||||
async def redeem_lnurl_withdraw(
|
||||
wallet_id: str,
|
||||
lnurl_request: str,
|
||||
memo: Optional[str] = None,
|
||||
extra: Optional[Dict] = None,
|
||||
wait_seconds: int = 0,
|
||||
conn: Optional[Connection] = None,
|
||||
) -> None:
|
||||
if not lnurl_request:
|
||||
return None
|
||||
|
||||
res = {}
|
||||
|
||||
headers = {"User-Agent": settings.user_agent}
|
||||
async with httpx.AsyncClient(headers=headers) as client:
|
||||
lnurl = decode_lnurl(lnurl_request)
|
||||
r = await client.get(str(lnurl))
|
||||
res = r.json()
|
||||
|
||||
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 Exception:
|
||||
logger.warning(
|
||||
f"failed to create invoice on redeem_lnurl_withdraw "
|
||||
f"from {lnurl}. params: {res}"
|
||||
)
|
||||
return None
|
||||
|
||||
if wait_seconds:
|
||||
await asyncio.sleep(wait_seconds)
|
||||
|
||||
params = {"k1": res["k1"], "pr": payment_request}
|
||||
|
||||
try:
|
||||
params["balanceNotify"] = url_for(
|
||||
f"/withdraw/notify/{urlparse(lnurl_request).netloc}",
|
||||
external=True,
|
||||
wal=wallet_id,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
headers = {"User-Agent": settings.user_agent}
|
||||
async with httpx.AsyncClient(headers=headers) as client:
|
||||
try:
|
||||
await client.get(res["callback"], params=params)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
async def perform_lnurlauth(
|
||||
callback: str,
|
||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||
) -> Optional[LnurlErrorResponse]:
|
||||
cb = urlparse(callback)
|
||||
|
||||
k1 = bytes.fromhex(parse_qs(cb.query)["k1"][0])
|
||||
|
||||
key = wallet.wallet.lnurlauth_key(cb.netloc)
|
||||
|
||||
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: int):
|
||||
# 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 > order // 2:
|
||||
s = order - s
|
||||
|
||||
# now we do the strict DER encoding copied from
|
||||
# https://github.com/KiriKiri/bip66 (without any checks)
|
||||
r_temp = int_to_bytes_suitable_der(r)
|
||||
s_temp = int_to_bytes_suitable_der(s)
|
||||
|
||||
r_len = len(r_temp)
|
||||
s_len = len(s_temp)
|
||||
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_temp)
|
||||
signature.write(0x02.to_bytes(1, "big", signed=False))
|
||||
signature.write(s_len.to_bytes(1, "big", signed=False))
|
||||
signature.write(s_temp)
|
||||
|
||||
return signature.getvalue()
|
||||
|
||||
sig = key.sign_digest_deterministic(k1, sigencode=encode_strict_der)
|
||||
|
||||
headers = {"User-Agent": settings.user_agent}
|
||||
async with httpx.AsyncClient(headers=headers) as client:
|
||||
assert key.verifying_key, "LNURLauth verifying_key does not exist"
|
||||
r = await client.get(
|
||||
callback,
|
||||
params={
|
||||
"k1": k1.hex(),
|
||||
"key": key.verifying_key.to_string("compressed").hex(),
|
||||
"sig": sig.hex(),
|
||||
},
|
||||
)
|
||||
try:
|
||||
resp = json.loads(r.text)
|
||||
if resp["status"] == "OK":
|
||||
return None
|
||||
|
||||
return LnurlErrorResponse(reason=resp["reason"])
|
||||
except (KeyError, json.decoder.JSONDecodeError):
|
||||
return LnurlErrorResponse(
|
||||
reason=r.text[:200] + "..." if len(r.text) > 200 else r.text
|
||||
)
|
||||
|
||||
|
||||
async def check_transaction_status(
|
||||
wallet_id: str, payment_hash: str, conn: Optional[Connection] = None
|
||||
) -> PaymentStatus:
|
||||
|
@ -126,26 +126,6 @@
|
||||
<q-separator></q-separator>
|
||||
|
||||
<q-list>
|
||||
{% if wallet.lnurlwithdraw_full %}
|
||||
<q-expansion-item
|
||||
group="extras"
|
||||
icon="crop_free"
|
||||
:label="$t('drain_funds')"
|
||||
>
|
||||
<q-card>
|
||||
<q-card-section class="text-center">
|
||||
<a href="lightning:{{wallet.lnurlwithdraw_full}}">
|
||||
<lnbits-qrcode
|
||||
:value="lightning:{{wallet.lnurlwithdraw_full}}"
|
||||
></lnbits-qrcode>
|
||||
</a>
|
||||
<p v-text="$t('drain_funds_desc')"></p>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-separator></q-separator>
|
||||
{% endif %}
|
||||
|
||||
<q-expansion-item
|
||||
group="extras"
|
||||
icon="settings_cell"
|
||||
@ -276,16 +256,12 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-dialog v-model="receive.show" position="top">
|
||||
<q-card
|
||||
v-if="!receive.paymentReq"
|
||||
v-if="receive.paymentReq === null"
|
||||
class="q-pa-lg q-pt-xl lnbits__dialog-card"
|
||||
>
|
||||
<q-form @submit="createInvoice" class="q-gutter-md">
|
||||
<p v-if="receive.lnurl" class="text-h6 text-center q-my-none">
|
||||
<b v-text="receive.lnurl.domain"></b> is requesting an invoice:
|
||||
</p>
|
||||
{% if LNBITS_DENOMINATION != 'sats' %}
|
||||
<q-input
|
||||
filled
|
||||
@ -297,7 +273,6 @@
|
||||
reverse-fill-mask
|
||||
:min="receive.minMax[0]"
|
||||
:max="receive.minMax[1]"
|
||||
:readonly="receive.lnurl && receive.lnurl.fixed"
|
||||
></q-input>
|
||||
{% else %}
|
||||
<q-select
|
||||
@ -320,7 +295,6 @@
|
||||
:step="receive.unit != 'sat' ? '0.01' : '1'"
|
||||
:min="receive.minMax[0]"
|
||||
:max="receive.minMax[1]"
|
||||
:readonly="receive.lnurl && receive.lnurl.fixed"
|
||||
></q-input>
|
||||
{% endif %}
|
||||
|
||||
@ -337,11 +311,7 @@
|
||||
:disable="receive.data.amount == null || receive.data.amount <= 0"
|
||||
type="submit"
|
||||
>
|
||||
<span
|
||||
v-if="receive.lnurl"
|
||||
v-text="$t('withdraw_from') + receive.lnurl.domain"
|
||||
></span>
|
||||
<span v-else v-text="$t('create_invoice')"></span>
|
||||
<span v-text="$t('create_invoice')"></span>
|
||||
</q-btn>
|
||||
<q-btn
|
||||
v-close-popup
|
||||
@ -358,8 +328,9 @@
|
||||
></q-spinner>
|
||||
</q-form>
|
||||
</q-card>
|
||||
|
||||
<q-card
|
||||
v-else-if="receive.paymentReq && receive.lnurl == null"
|
||||
v-else-if="receive.paymentReq"
|
||||
class="q-pa-lg q-pt-xl lnbits__dialog-card"
|
||||
>
|
||||
<div class="text-center q-mb-lg">
|
||||
@ -448,7 +419,7 @@
|
||||
<div v-else-if="parse.lnurlauth">
|
||||
<q-form @submit="authLnurl" class="q-gutter-md">
|
||||
<p class="q-my-none text-h6">
|
||||
Authenticate with <b v-text="parse.lnurlauth.domain"></b>?
|
||||
Authenticate with <b v-text="parse.domain"></b>?
|
||||
</p>
|
||||
<q-separator class="q-my-sm"></q-separator>
|
||||
<p>
|
||||
@ -458,9 +429,7 @@
|
||||
data will be shared with
|
||||
<span v-text="parse.lnurlauth.domain"></span>.
|
||||
</p>
|
||||
<p>
|
||||
Your public key for <b v-text="parse.lnurlauth.domain"></b> is:
|
||||
</p>
|
||||
<p>Your public key for <b v-text="parse.domain"></b> is:</p>
|
||||
<p class="q-mx-xl">
|
||||
<code class="text-wrap" v-text="parse.lnurlauth.pubkey"></code>
|
||||
</p>
|
||||
@ -481,10 +450,56 @@
|
||||
</div>
|
||||
</q-form>
|
||||
</div>
|
||||
<div v-else-if="parse.lnurlwithdraw">
|
||||
<q-form @submit="withdrawLnurl" class="q-gutter-md">
|
||||
<p class="q-my-none text-h6 text-center">
|
||||
<b v-text="parse.domain"></b> is requesting <br />
|
||||
between
|
||||
<b
|
||||
v-text="msatoshiFormat(parse.lnurlwithdraw.minWithdrawable)"
|
||||
></b>
|
||||
and
|
||||
<b
|
||||
v-text="msatoshiFormat(parse.lnurlwithdraw.maxWithdrawable)"
|
||||
></b>
|
||||
<span v-text="'{{LNBITS_DENOMINATION}}'"></span>
|
||||
</p>
|
||||
<q-separator class="q-my-sm"></q-separator>
|
||||
<div class="row">
|
||||
<p
|
||||
class="col text-justify text-italic"
|
||||
v-text="parse.lnurlwithdraw.defaultDescription"
|
||||
></p>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<q-input
|
||||
ref="setAmount"
|
||||
v-model.number="parse.data.amount"
|
||||
:min="parse.lnurlwithdraw.minWithdrawable / 1000"
|
||||
:max="parse.lnurlwithdraw.maxWithdrawable / 1000"
|
||||
:label="$t('amount') + ' *'"
|
||||
filled
|
||||
dense
|
||||
></q-input>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn unelevated color="primary" type="submit">Withdraw</q-btn>
|
||||
<q-btn
|
||||
:label="$t('cancel')"
|
||||
v-close-popup
|
||||
flat
|
||||
color="grey"
|
||||
class="q-ml-auto"
|
||||
></q-btn>
|
||||
</div>
|
||||
</q-form>
|
||||
</div>
|
||||
<div v-else-if="parse.lnurlpay">
|
||||
<q-form @submit="payLnurl" class="q-gutter-md">
|
||||
<p v-if="parse.lnurlpay.fixed" class="q-my-none text-h6">
|
||||
<b v-text="parse.lnurlpay.domain"></b> is requesting
|
||||
<b v-text="parse.domain"></b> is requesting
|
||||
<span
|
||||
v-text="msatoshiFormat(parse.lnurlpay.maxSendable)"
|
||||
></span>
|
||||
@ -497,9 +512,7 @@
|
||||
</span>
|
||||
</p>
|
||||
<p v-else class="q-my-none text-h6 text-center">
|
||||
<b
|
||||
v-text="parse.lnurlpay.targetUser || parse.lnurlpay.domain"
|
||||
></b>
|
||||
<b v-text="parse.lnurlpay.targetUser || parse.domain"></b>
|
||||
is requesting <br />
|
||||
between
|
||||
<b v-text="msatoshiFormat(parse.lnurlpay.minSendable)"></b> and
|
||||
@ -516,7 +529,7 @@
|
||||
<div class="row">
|
||||
<p
|
||||
class="col text-justify text-italic"
|
||||
v-text="parse.lnurlpay.description"
|
||||
v-text="getLnurlDescription(parse.lnurlpay)"
|
||||
></p>
|
||||
<p class="col-4 q-pl-md" v-if="parse.lnurlpay.image">
|
||||
<q-img :src="parse.lnurlpay.image" />
|
||||
@ -536,17 +549,17 @@
|
||||
<br />
|
||||
<q-input
|
||||
ref="setAmount"
|
||||
filled
|
||||
dense
|
||||
v-model.number="parse.data.amount"
|
||||
:min="parse.lnurlpay.minSendable / 1000"
|
||||
:max="parse.lnurlpay.maxSendable / 1000"
|
||||
:readonly="parse.lnurlpay && parse.lnurlpay.fixed"
|
||||
:label="$t('amount') + ' (' + parse.data.unit + ') *'"
|
||||
:mask="parse.data.unit != 'sat' ? '#.##' : '#'"
|
||||
:step="parse.data.unit != 'sat' ? '0.01' : '1'"
|
||||
fill-mask="0"
|
||||
reverse-fill-mask
|
||||
:min="parse.lnurlpay.minSendable / 1000"
|
||||
:max="parse.lnurlpay.maxSendable / 1000"
|
||||
:readonly="parse.lnurlpay && parse.lnurlpay.fixed"
|
||||
filled
|
||||
dense
|
||||
></q-input>
|
||||
</div>
|
||||
<div
|
||||
|
@ -1,11 +1,7 @@
|
||||
import hashlib
|
||||
import json
|
||||
from http import HTTPStatus
|
||||
from io import BytesIO
|
||||
from typing import Dict, List
|
||||
from urllib.parse import ParseResult, parse_qs, urlencode, urlparse, urlunparse
|
||||
from typing import List
|
||||
|
||||
import httpx
|
||||
import pyqrcode
|
||||
from fastapi import (
|
||||
APIRouter,
|
||||
@ -17,18 +13,13 @@ from starlette.responses import StreamingResponse
|
||||
from lnbits.core.models import (
|
||||
BaseWallet,
|
||||
ConversionData,
|
||||
CreateLnurlAuth,
|
||||
CreateWallet,
|
||||
User,
|
||||
Wallet,
|
||||
)
|
||||
from lnbits.decorators import (
|
||||
WalletTypeInfo,
|
||||
check_user_exists,
|
||||
require_admin_key,
|
||||
require_invoice_key,
|
||||
)
|
||||
from lnbits.lnurl import decode as lnurl_decode
|
||||
from lnbits.settings import settings
|
||||
from lnbits.utils.exchange_rates import (
|
||||
allowed_currencies,
|
||||
@ -36,7 +27,7 @@ from lnbits.utils.exchange_rates import (
|
||||
satoshis_amount_as_fiat,
|
||||
)
|
||||
|
||||
from ..services import create_user_account, perform_lnurlauth
|
||||
from ..services import create_user_account
|
||||
|
||||
# backwards compatibility for extension
|
||||
# TODO: remove api_payment and pay_invoice imports from extensions
|
||||
@ -70,136 +61,6 @@ async def api_create_account(data: CreateWallet) -> Wallet:
|
||||
return account.wallets[0]
|
||||
|
||||
|
||||
@api_router.get("/api/v1/lnurlscan/{code}")
|
||||
async def api_lnurlscan(
|
||||
code: str, wallet: WalletTypeInfo = Depends(require_invoice_key)
|
||||
):
|
||||
try:
|
||||
url = str(lnurl_decode(code))
|
||||
domain = urlparse(url).netloc
|
||||
except Exception as exc:
|
||||
# parse internet identifier (user@domain.com)
|
||||
name_domain = code.split("@")
|
||||
if len(name_domain) == 2 and len(name_domain[1].split(".")) >= 2:
|
||||
name, domain = name_domain
|
||||
url = (
|
||||
("http://" if domain.endswith(".onion") else "https://")
|
||||
+ domain
|
||||
+ "/.well-known/lnurlp/"
|
||||
+ name
|
||||
)
|
||||
# will proceed with these values
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST, detail="invalid lnurl"
|
||||
) from exc
|
||||
|
||||
# params is what will be returned to the client
|
||||
params: Dict = {"domain": domain}
|
||||
|
||||
if "tag=login" in url:
|
||||
params.update(kind="auth")
|
||||
params.update(callback=url) # with k1 already in it
|
||||
|
||||
lnurlauth_key = wallet.wallet.lnurlauth_key(domain)
|
||||
assert lnurlauth_key.verifying_key
|
||||
params.update(pubkey=lnurlauth_key.verifying_key.to_string("compressed").hex())
|
||||
else:
|
||||
headers = {"User-Agent": settings.user_agent}
|
||||
async with httpx.AsyncClient(headers=headers, follow_redirects=True) as client:
|
||||
r = await client.get(url, timeout=5)
|
||||
r.raise_for_status()
|
||||
if r.is_error:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.SERVICE_UNAVAILABLE,
|
||||
detail={"domain": domain, "message": "failed to get parameters"},
|
||||
)
|
||||
|
||||
try:
|
||||
data = json.loads(r.text)
|
||||
except json.decoder.JSONDecodeError as exc:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.SERVICE_UNAVAILABLE,
|
||||
detail={
|
||||
"domain": domain,
|
||||
"message": f"got invalid response '{r.text[:200]}'",
|
||||
},
|
||||
) from exc
|
||||
|
||||
try:
|
||||
tag: str = data.get("tag")
|
||||
params.update(**data)
|
||||
if tag == "channelRequest":
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail={
|
||||
"domain": domain,
|
||||
"kind": "channel",
|
||||
"message": "unsupported",
|
||||
},
|
||||
)
|
||||
elif tag == "withdrawRequest":
|
||||
params.update(kind="withdraw")
|
||||
params.update(fixed=data["minWithdrawable"] == data["maxWithdrawable"])
|
||||
|
||||
# callback with k1 already in it
|
||||
parsed_callback: ParseResult = urlparse(data["callback"])
|
||||
qs: Dict = parse_qs(parsed_callback.query)
|
||||
qs["k1"] = data["k1"]
|
||||
|
||||
# balanceCheck/balanceNotify
|
||||
if "balanceCheck" in data:
|
||||
params.update(balanceCheck=data["balanceCheck"])
|
||||
|
||||
# format callback url and send to client
|
||||
parsed_callback = parsed_callback._replace(
|
||||
query=urlencode(qs, doseq=True)
|
||||
)
|
||||
params.update(callback=urlunparse(parsed_callback))
|
||||
elif tag == "payRequest":
|
||||
params.update(kind="pay")
|
||||
params.update(fixed=data["minSendable"] == data["maxSendable"])
|
||||
|
||||
params.update(
|
||||
description_hash=hashlib.sha256(
|
||||
data["metadata"].encode()
|
||||
).hexdigest()
|
||||
)
|
||||
metadata = json.loads(data["metadata"])
|
||||
for [k, v] in metadata:
|
||||
if k == "text/plain":
|
||||
params.update(description=v)
|
||||
if k in ("image/jpeg;base64", "image/png;base64"):
|
||||
data_uri = f"data:{k},{v}"
|
||||
params.update(image=data_uri)
|
||||
if k in ("text/email", "text/identifier"):
|
||||
params.update(targetUser=v)
|
||||
params.update(commentAllowed=data.get("commentAllowed", 0))
|
||||
|
||||
except KeyError as exc:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.SERVICE_UNAVAILABLE,
|
||||
detail={
|
||||
"domain": domain,
|
||||
"message": f"lnurl JSON response invalid: {exc}",
|
||||
},
|
||||
) from exc
|
||||
|
||||
return params
|
||||
|
||||
|
||||
@api_router.post("/api/v1/lnurlauth")
|
||||
async def api_perform_lnurlauth(
|
||||
data: CreateLnurlAuth, wallet: WalletTypeInfo = Depends(require_admin_key)
|
||||
):
|
||||
err = await perform_lnurlauth(data.callback, wallet=wallet)
|
||||
if err:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.SERVICE_UNAVAILABLE, detail=err.reason
|
||||
)
|
||||
return ""
|
||||
|
||||
|
||||
@api_router.get("/api/v1/currencies")
|
||||
async def api_list_currencies_available() -> List[str]:
|
||||
return allowed_currencies()
|
||||
|
156
lnbits/core/views/lnurl_api.py
Normal file
156
lnbits/core/views/lnurl_api.py
Normal file
@ -0,0 +1,156 @@
|
||||
import json
|
||||
from http import HTTPStatus
|
||||
|
||||
from fastapi import (
|
||||
APIRouter,
|
||||
Depends,
|
||||
)
|
||||
from fastapi.exceptions import HTTPException
|
||||
from lnurl import (
|
||||
LnurlPayActionResponse,
|
||||
LnurlSuccessResponse,
|
||||
execute_login,
|
||||
execute_pay_request,
|
||||
)
|
||||
from lnurl import handle as lnurl_handle
|
||||
from lnurl.core import execute_withdraw
|
||||
from lnurl.exceptions import InvalidLnurl
|
||||
|
||||
from lnbits.core.models import CreateLnurlAuth, CreateLnurlPay, CreateLnurlWithdraw
|
||||
from lnbits.core.services import create_invoice, pay_invoice
|
||||
from lnbits.decorators import WalletTypeInfo, require_admin_key
|
||||
from lnbits.settings import settings
|
||||
from lnbits.utils.exchange_rates import fiat_amount_as_satoshis
|
||||
|
||||
lnurl_router = APIRouter(tags=["LNURL"])
|
||||
|
||||
|
||||
@lnurl_router.get("/api/v1/lnurlscan/{code}")
|
||||
@lnurl_router.get("/lnurl/api/v1/scan/{code}")
|
||||
async def api_lnurl_scan(code: str) -> dict:
|
||||
try:
|
||||
handle = await lnurl_handle(code, user_agent=settings.user_agent)
|
||||
return handle.dict()
|
||||
except InvalidLnurl as exc:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail="Invalid LNURL",
|
||||
) from exc
|
||||
except Exception as exc:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
detail="Error processing LNURL",
|
||||
) from exc
|
||||
|
||||
|
||||
@lnurl_router.post("/lnurl/api/v1/auth")
|
||||
async def api_lnurl_auth(
|
||||
data: CreateLnurlAuth, key_type: WalletTypeInfo = Depends(require_admin_key)
|
||||
):
|
||||
try:
|
||||
res = await execute_login(data.auth_response, key_type.wallet.adminkey)
|
||||
assert isinstance(
|
||||
res, LnurlSuccessResponse
|
||||
), "unexpected response from execute_login"
|
||||
return res
|
||||
except Exception as exc:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail="Failed to auth, try new LNURL",
|
||||
) from exc
|
||||
|
||||
|
||||
@lnurl_router.post("/lnurl/api/v1/withdraw")
|
||||
async def api_lnurl_withdraw(
|
||||
data: CreateLnurlWithdraw, key_type: WalletTypeInfo = Depends(require_admin_key)
|
||||
):
|
||||
try:
|
||||
payment_hash, payment_request = await create_invoice(
|
||||
wallet_id=key_type.wallet.id,
|
||||
amount=data.amount / 1000,
|
||||
memo=data.memo or "",
|
||||
extra={"tag": "lnurl-withdraw"},
|
||||
)
|
||||
res = await execute_withdraw(data.withdraw_response, payment_request)
|
||||
assert isinstance(
|
||||
res, LnurlSuccessResponse
|
||||
), "unexpected response from execute_withdraw"
|
||||
return {"payment_hash": payment_hash}
|
||||
except Exception as exc:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail=f"Failed to withdraw: {exc}",
|
||||
) from exc
|
||||
|
||||
|
||||
@lnurl_router.post("/api/v1/payments/lnurl")
|
||||
@lnurl_router.post("/lnurl/api/v1/pay")
|
||||
async def api_lnurl_pay(
|
||||
data: CreateLnurlPay, key_type: WalletTypeInfo = Depends(require_admin_key)
|
||||
):
|
||||
amount_msat = data.amount
|
||||
if data.unit and data.unit != "sat":
|
||||
amount_msat = await fiat_amount_as_satoshis(data.amount, data.unit)
|
||||
# no msat precision, why?
|
||||
amount_msat = int(amount_msat // 1000) * 1000
|
||||
|
||||
description = None
|
||||
metadata = json.loads(data.pay_response.metadata)
|
||||
for x in metadata:
|
||||
if x[0] == "text/plain":
|
||||
description = x[1]
|
||||
|
||||
if not description:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail="description required on LNURL pay_response.",
|
||||
)
|
||||
|
||||
# pay
|
||||
# params.update(
|
||||
# description_hash=hashlib.sha256(
|
||||
# data["metadata"].encode()
|
||||
# ).hexdigest()
|
||||
# )
|
||||
# metadata = json.loads(data["metadata"])
|
||||
# for [k, v] in metadata:
|
||||
# if k == "text/plain":
|
||||
# params.update(description=v)
|
||||
# if k in ("image/jpeg;base64", "image/png;base64"):
|
||||
# data_uri = f"data:{k},{v}"
|
||||
# params.update(image=data_uri)
|
||||
# if k in ("text/email", "text/identifier"):
|
||||
# params.update(targetUser=v)
|
||||
|
||||
try:
|
||||
res = await execute_pay_request(data.pay_response, str(amount_msat))
|
||||
assert isinstance(
|
||||
res, LnurlPayActionResponse
|
||||
), "unexpected response from execute_pay_request"
|
||||
except Exception as exc:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail=f"Failed to fetch invoice: {exc}",
|
||||
) from exc
|
||||
|
||||
extra: dict = {}
|
||||
if res.success_action:
|
||||
extra["success_action"] = res.success_action.json()
|
||||
if data.comment:
|
||||
extra["comment"] = data.comment
|
||||
if data.unit and data.unit != "sat":
|
||||
extra["fiat_currency"] = data.unit
|
||||
extra["fiat_amount"] = data.amount / 1000
|
||||
|
||||
payment_hash = await pay_invoice(
|
||||
wallet_id=key_type.wallet.id,
|
||||
payment_request=res.pr,
|
||||
description=description,
|
||||
extra=extra,
|
||||
)
|
||||
return {
|
||||
"success_action": res.success_action,
|
||||
"payment_hash": payment_hash,
|
||||
# maintain backwards compatibility with API clients:
|
||||
"checking_id": payment_hash,
|
||||
}
|
@ -1,12 +1,8 @@
|
||||
import asyncio
|
||||
import json
|
||||
import uuid
|
||||
from http import HTTPStatus
|
||||
from math import ceil
|
||||
from typing import List, Optional, Union
|
||||
from urllib.parse import urlparse
|
||||
from typing import List, Optional
|
||||
|
||||
import httpx
|
||||
from fastapi import (
|
||||
APIRouter,
|
||||
Body,
|
||||
@ -17,6 +13,7 @@ from fastapi import (
|
||||
Request,
|
||||
)
|
||||
from fastapi.responses import JSONResponse
|
||||
from lnurl import decode as lnurl_decode
|
||||
from loguru import logger
|
||||
from sse_starlette.sse import EventSourceResponse
|
||||
|
||||
@ -24,7 +21,6 @@ from lnbits import bolt11
|
||||
from lnbits.core.db import db
|
||||
from lnbits.core.models import (
|
||||
CreateInvoice,
|
||||
CreateLnurl,
|
||||
DecodePayment,
|
||||
KeyType,
|
||||
Payment,
|
||||
@ -37,13 +33,10 @@ from lnbits.decorators import (
|
||||
WalletTypeInfo,
|
||||
get_key_type,
|
||||
parse_filters,
|
||||
require_admin_key,
|
||||
require_invoice_key,
|
||||
)
|
||||
from lnbits.helpers import generate_filter_params_openapi
|
||||
from lnbits.lnurl import decode as lnurl_decode
|
||||
from lnbits.settings import settings
|
||||
from lnbits.utils.exchange_rates import fiat_amount_as_satoshis
|
||||
|
||||
from ..crud import (
|
||||
DateTrunc,
|
||||
@ -168,36 +161,9 @@ async def api_payments_create_invoice(data: CreateInvoice, wallet: Wallet):
|
||||
assert payment_db is not None, "payment not found"
|
||||
checking_id = payment_db.checking_id
|
||||
|
||||
invoice = bolt11.decode(payment_request)
|
||||
|
||||
lnurl_response: Union[None, bool, str] = None
|
||||
if data.lnurl_callback:
|
||||
headers = {"User-Agent": settings.user_agent}
|
||||
async with httpx.AsyncClient(headers=headers) as client:
|
||||
try:
|
||||
r = await client.get(
|
||||
data.lnurl_callback,
|
||||
params={
|
||||
"pr": payment_request,
|
||||
},
|
||||
timeout=10,
|
||||
)
|
||||
if r.is_error:
|
||||
lnurl_response = r.text
|
||||
else:
|
||||
resp = json.loads(r.text)
|
||||
if resp["status"] != "OK":
|
||||
lnurl_response = resp["reason"]
|
||||
else:
|
||||
lnurl_response = True
|
||||
except (httpx.ConnectError, httpx.RequestError) as ex:
|
||||
logger.error(ex)
|
||||
lnurl_response = False
|
||||
|
||||
return {
|
||||
"payment_hash": invoice.payment_hash,
|
||||
"payment_hash": payment_hash,
|
||||
"payment_request": payment_request,
|
||||
"lnurl_response": lnurl_response,
|
||||
# maintain backwards compatibility with API clients:
|
||||
"checking_id": checking_id,
|
||||
}
|
||||
@ -268,89 +234,10 @@ async def api_payments_fee_reserve(invoice: str = Query("invoice")) -> JSONRespo
|
||||
)
|
||||
|
||||
|
||||
@payment_router.post("/lnurl")
|
||||
async def api_payments_pay_lnurl(
|
||||
data: CreateLnurl, wallet: WalletTypeInfo = Depends(require_admin_key)
|
||||
):
|
||||
domain = urlparse(data.callback).netloc
|
||||
|
||||
headers = {"User-Agent": settings.user_agent}
|
||||
async with httpx.AsyncClient(headers=headers, follow_redirects=True) as client:
|
||||
try:
|
||||
if data.unit and data.unit != "sat":
|
||||
amount_msat = await fiat_amount_as_satoshis(data.amount, data.unit)
|
||||
# no msat precision
|
||||
amount_msat = ceil(amount_msat // 1000) * 1000
|
||||
else:
|
||||
amount_msat = data.amount
|
||||
r = await client.get(
|
||||
data.callback,
|
||||
params={"amount": amount_msat, "comment": data.comment},
|
||||
timeout=40,
|
||||
)
|
||||
if r.is_error:
|
||||
raise httpx.ConnectError("LNURL callback connection error")
|
||||
r.raise_for_status()
|
||||
except (httpx.ConnectError, httpx.RequestError) as exc:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail=f"Failed to connect to {domain}.",
|
||||
) from exc
|
||||
|
||||
params = json.loads(r.text)
|
||||
if params.get("status") == "ERROR":
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail=f"{domain} said: '{params.get('reason', '')}'",
|
||||
)
|
||||
|
||||
if not params.get("pr"):
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail=f"{domain} did not return a payment request.",
|
||||
)
|
||||
|
||||
invoice = bolt11.decode(params["pr"])
|
||||
if invoice.amount_msat != amount_msat:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail=(
|
||||
(
|
||||
f"{domain} returned an invalid invoice. Expected"
|
||||
f" {amount_msat} msat, got {invoice.amount_msat}."
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
extra = {}
|
||||
|
||||
if params.get("successAction"):
|
||||
extra["success_action"] = params["successAction"]
|
||||
if data.comment:
|
||||
extra["comment"] = data.comment
|
||||
if data.unit and data.unit != "sat":
|
||||
extra["fiat_currency"] = data.unit
|
||||
extra["fiat_amount"] = data.amount / 1000
|
||||
assert data.description is not None, "description is required"
|
||||
payment_hash = await pay_invoice(
|
||||
wallet_id=wallet.wallet.id,
|
||||
payment_request=params["pr"],
|
||||
description=data.description,
|
||||
extra=extra,
|
||||
)
|
||||
|
||||
return {
|
||||
"success_action": params.get("successAction"),
|
||||
"payment_hash": payment_hash,
|
||||
# maintain backwards compatibility with API clients:
|
||||
"checking_id": payment_hash,
|
||||
}
|
||||
|
||||
|
||||
async def subscribe_wallet_invoices(request: Request, wallet: Wallet):
|
||||
"""
|
||||
Subscribe to new invoices for a wallet. Can be wrapped in EventSourceResponse.
|
||||
Listenes invoming payments for a wallet and yields jsons with payment details.
|
||||
Listenes incoming payments for a wallet and yields jsons with payment details.
|
||||
"""
|
||||
this_wallet_id = wallet.id
|
||||
|
||||
|
@ -22,18 +22,11 @@ window.LNbits = {
|
||||
data: data
|
||||
})
|
||||
},
|
||||
createInvoice: async function (
|
||||
wallet,
|
||||
amount,
|
||||
memo,
|
||||
unit = 'sat',
|
||||
lnurlCallback = null
|
||||
) {
|
||||
createInvoice: async function (wallet, amount, memo, unit = 'sat') {
|
||||
return this.request('post', '/api/v1/payments', wallet.inkey, {
|
||||
out: false,
|
||||
amount: amount,
|
||||
memo: memo,
|
||||
lnurl_callback: lnurlCallback,
|
||||
unit: unit
|
||||
})
|
||||
},
|
||||
@ -43,27 +36,24 @@ window.LNbits = {
|
||||
bolt11: bolt11
|
||||
})
|
||||
},
|
||||
payLnurl: function (
|
||||
wallet,
|
||||
callback,
|
||||
description_hash,
|
||||
amount,
|
||||
description = '',
|
||||
comment = '',
|
||||
unit = ''
|
||||
) {
|
||||
return this.request('post', '/api/v1/payments/lnurl', wallet.adminkey, {
|
||||
callback,
|
||||
description_hash,
|
||||
payLnurl: function (wallet, pay_response, amount, comment = '', unit = '') {
|
||||
return this.request('post', '/lnurl/api/v1/pay', wallet.adminkey, {
|
||||
pay_response,
|
||||
amount,
|
||||
comment,
|
||||
description,
|
||||
unit
|
||||
})
|
||||
},
|
||||
authLnurl: function (wallet, callback) {
|
||||
return this.request('post', '/api/v1/lnurlauth', wallet.adminkey, {
|
||||
callback
|
||||
withdrawLnurl: function (wallet, withdraw_response, amount, memo) {
|
||||
return this.request('post', '/lnurl/api/v1/withdraw', wallet.adminkey, {
|
||||
withdraw_response,
|
||||
amount,
|
||||
memo
|
||||
})
|
||||
},
|
||||
authLnurl: function (wallet, auth_response) {
|
||||
return this.request('post', '/lnurl/api/v1/auth', wallet.adminkey, {
|
||||
auth_response
|
||||
})
|
||||
},
|
||||
createAccount: function (name) {
|
||||
|
@ -30,6 +30,7 @@ new Vue({
|
||||
invoice: null,
|
||||
lnurlpay: null,
|
||||
lnurlauth: null,
|
||||
lnurlwithdraw: null,
|
||||
data: {
|
||||
request: '',
|
||||
amount: 0,
|
||||
@ -130,6 +131,12 @@ new Vue({
|
||||
this.receive.paymentHash = null
|
||||
}
|
||||
},
|
||||
getLnurlDescription: function (lnurl) {
|
||||
const description = JSON.parse(lnurl.metadata).filter(
|
||||
item => item[0] === 'text/plain'
|
||||
)
|
||||
return description[0][1]
|
||||
},
|
||||
createInvoice: function () {
|
||||
this.receive.status = 'loading'
|
||||
if (LNBITS_DENOMINATION != 'sats') {
|
||||
@ -147,30 +154,6 @@ new Vue({
|
||||
this.receive.status = 'success'
|
||||
this.receive.paymentReq = response.data.payment_request
|
||||
this.receive.paymentHash = response.data.payment_hash
|
||||
|
||||
if (response.data.lnurl_response !== null) {
|
||||
if (response.data.lnurl_response === false) {
|
||||
response.data.lnurl_response = `Unable to connect`
|
||||
}
|
||||
|
||||
if (typeof response.data.lnurl_response === 'string') {
|
||||
// failure
|
||||
this.$q.notify({
|
||||
timeout: 5000,
|
||||
type: 'warning',
|
||||
message: `${this.receive.lnurl.domain} lnurl-withdraw call failed.`,
|
||||
caption: response.data.lnurl_response
|
||||
})
|
||||
return
|
||||
} else if (response.data.lnurl_response === true) {
|
||||
// success
|
||||
this.$q.notify({
|
||||
timeout: 5000,
|
||||
message: `Invoice sent to ${this.receive.lnurl.domain}!`,
|
||||
spinner: true
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
this.updatePayments = !this.updatePayments
|
||||
@ -232,11 +215,7 @@ new Vue({
|
||||
this.parse.data.request.match(/[\w.+-~_]+@[\w.+-~_]/)
|
||||
) {
|
||||
LNbits.api
|
||||
.request(
|
||||
'GET',
|
||||
'/api/v1/lnurlscan/' + this.parse.data.request,
|
||||
this.g.wallet.adminkey
|
||||
)
|
||||
.request('GET', '/lnurl/api/v1/scan/' + this.parse.data.request, null)
|
||||
.catch(err => {
|
||||
LNbits.utils.notifyApiError(err)
|
||||
})
|
||||
@ -253,28 +232,21 @@ new Vue({
|
||||
return
|
||||
}
|
||||
|
||||
if (data.kind === 'pay') {
|
||||
const url = new URL(data.callback)
|
||||
this.parse.domain = url.host
|
||||
if (data.tag === 'payRequest') {
|
||||
this.parse.lnurlpay = Object.freeze(data)
|
||||
this.parse.data.amount = data.minSendable / 1000
|
||||
} else if (data.kind === 'auth') {
|
||||
} else if (data.tag === 'login') {
|
||||
this.parse.lnurlauth = Object.freeze(data)
|
||||
} else if (data.kind === 'withdraw') {
|
||||
this.parse.show = false
|
||||
this.receive.show = true
|
||||
this.receive.status = 'pending'
|
||||
this.receive.paymentReq = null
|
||||
this.receive.paymentHash = null
|
||||
this.receive.data.amount = data.maxWithdrawable / 1000
|
||||
this.receive.data.memo = data.defaultDescription
|
||||
this.receive.minMax = [
|
||||
} else if (data.tag === 'withdrawRequest') {
|
||||
this.parse.lnurlwithdraw = Object.freeze(data)
|
||||
this.parse.data.amount = data.maxWithdrawable / 1000
|
||||
this.parse.data.memo = data.defaultDescription
|
||||
this.parse.minMax = [
|
||||
data.minWithdrawable / 1000,
|
||||
data.maxWithdrawable / 1000
|
||||
]
|
||||
this.receive.lnurl = {
|
||||
domain: data.domain,
|
||||
callback: data.callback,
|
||||
fixed: data.fixed
|
||||
}
|
||||
}
|
||||
})
|
||||
return
|
||||
@ -364,6 +336,42 @@ new Vue({
|
||||
this.parse.show = false
|
||||
})
|
||||
},
|
||||
withdrawLnurl: function () {
|
||||
let dismissPaymentMsg = this.$q.notify({
|
||||
timeout: 0,
|
||||
message: 'Processing withdraw...'
|
||||
})
|
||||
LNbits.api
|
||||
.withdrawLnurl(
|
||||
this.g.wallet,
|
||||
this.parse.lnurlwithdraw,
|
||||
this.parse.data.amount * 1000,
|
||||
this.parse.data.memo
|
||||
)
|
||||
.then(response => {
|
||||
this.parse.show = false
|
||||
this.parse.lnurlwithdraw = null
|
||||
clearInterval(this.parse.paymentChecker)
|
||||
setTimeout(() => {
|
||||
clearInterval(this.parse.paymentChecker)
|
||||
}, 40000)
|
||||
this.parse.paymentChecker = setInterval(() => {
|
||||
LNbits.api
|
||||
.getPayment(this.g.wallet, response.data.payment_hash)
|
||||
.then(res => {
|
||||
if (res.data.paid) {
|
||||
dismissPaymentMsg()
|
||||
clearInterval(this.parse.paymentChecker)
|
||||
this.updatePayments = !this.updatePayments
|
||||
}
|
||||
})
|
||||
}, 2000)
|
||||
})
|
||||
.catch(err => {
|
||||
dismissPaymentMsg()
|
||||
LNbits.utils.notifyApiError(err)
|
||||
})
|
||||
},
|
||||
payLnurl: function () {
|
||||
let dismissPaymentMsg = this.$q.notify({
|
||||
timeout: 0,
|
||||
@ -373,10 +381,8 @@ new Vue({
|
||||
LNbits.api
|
||||
.payLnurl(
|
||||
this.g.wallet,
|
||||
this.parse.lnurlpay.callback,
|
||||
this.parse.lnurlpay.description_hash,
|
||||
this.parse.lnurlpay,
|
||||
this.parse.data.amount * 1000,
|
||||
this.parse.lnurlpay.description.slice(0, 120),
|
||||
this.parse.data.comment,
|
||||
this.parse.data.unit
|
||||
)
|
||||
@ -453,7 +459,7 @@ new Vue({
|
||||
})
|
||||
|
||||
LNbits.api
|
||||
.authLnurl(this.g.wallet, this.parse.lnurlauth.callback)
|
||||
.authLnurl(this.g.wallet, this.parse.lnurlauth)
|
||||
.then(_ => {
|
||||
dismissAuthMsg()
|
||||
this.$q.notify({
|
||||
|
43
poetry.lock
generated
43
poetry.lock
generated
@ -1,4 +1,4 @@
|
||||
# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand.
|
||||
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "anyio"
|
||||
@ -767,13 +767,13 @@ wmi = ["wmi (>=1.5.1)"]
|
||||
|
||||
[[package]]
|
||||
name = "ecdsa"
|
||||
version = "0.18.0"
|
||||
version = "0.19.0"
|
||||
description = "ECDSA cryptographic signature library (pure python)"
|
||||
optional = false
|
||||
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.6"
|
||||
files = [
|
||||
{file = "ecdsa-0.18.0-py2.py3-none-any.whl", hash = "sha256:80600258e7ed2f16b9aa1d7c295bd70194109ad5a30fdee0eaeefef1d4c559dd"},
|
||||
{file = "ecdsa-0.18.0.tar.gz", hash = "sha256:190348041559e21b22a1d65cee485282ca11a6f81d503fddb84d5017e9ed1e49"},
|
||||
{file = "ecdsa-0.19.0-py2.py3-none-any.whl", hash = "sha256:2cea9b88407fdac7bbeca0833b189e4c9c53f2ef1e1eaa29f6224dbc809b707a"},
|
||||
{file = "ecdsa-0.19.0.tar.gz", hash = "sha256:60eaad1199659900dd0af521ed462b793bbdf867432b3948e87416ae4caf6bf8"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@ -987,39 +987,40 @@ cryptography = ">=2.5"
|
||||
|
||||
[[package]]
|
||||
name = "httpcore"
|
||||
version = "0.18.0"
|
||||
version = "1.0.5"
|
||||
description = "A minimal low-level HTTP client."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "httpcore-0.18.0-py3-none-any.whl", hash = "sha256:adc5398ee0a476567bf87467063ee63584a8bce86078bf748e48754f60202ced"},
|
||||
{file = "httpcore-0.18.0.tar.gz", hash = "sha256:13b5e5cd1dca1a6636a6aaea212b19f4f85cd88c366a2b82304181b769aab3c9"},
|
||||
{file = "httpcore-1.0.5-py3-none-any.whl", hash = "sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5"},
|
||||
{file = "httpcore-1.0.5.tar.gz", hash = "sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
anyio = ">=3.0,<5.0"
|
||||
certifi = "*"
|
||||
h11 = ">=0.13,<0.15"
|
||||
sniffio = "==1.*"
|
||||
|
||||
[package.extras]
|
||||
asyncio = ["anyio (>=4.0,<5.0)"]
|
||||
http2 = ["h2 (>=3,<5)"]
|
||||
socks = ["socksio (==1.*)"]
|
||||
trio = ["trio (>=0.22.0,<0.26.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "httpx"
|
||||
version = "0.25.0"
|
||||
version = "0.27.0"
|
||||
description = "The next generation HTTP client."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "httpx-0.25.0-py3-none-any.whl", hash = "sha256:181ea7f8ba3a82578be86ef4171554dd45fec26a02556a744db029a0a27b7100"},
|
||||
{file = "httpx-0.25.0.tar.gz", hash = "sha256:47ecda285389cb32bb2691cc6e069e3ab0205956f681c5b2ad2325719751d875"},
|
||||
{file = "httpx-0.27.0-py3-none-any.whl", hash = "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5"},
|
||||
{file = "httpx-0.27.0.tar.gz", hash = "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
anyio = "*"
|
||||
certifi = "*"
|
||||
httpcore = ">=0.18.0,<0.19.0"
|
||||
httpcore = "==1.*"
|
||||
idna = "*"
|
||||
sniffio = "*"
|
||||
|
||||
@ -1253,19 +1254,21 @@ rediscluster = ["redis (>=4.2.0,!=4.5.2,!=4.5.3)"]
|
||||
|
||||
[[package]]
|
||||
name = "lnurl"
|
||||
version = "0.4.2"
|
||||
version = "0.5.1"
|
||||
description = "LNURL implementation for Python."
|
||||
optional = false
|
||||
python-versions = ">=3.9,<4.0"
|
||||
python-versions = "<4.0,>=3.9"
|
||||
files = [
|
||||
{file = "lnurl-0.4.2-py3-none-any.whl", hash = "sha256:93f79ae7e0b0c66fed5b29ac1520e85e3e2c8648561a4b42974f0b7bffd34d84"},
|
||||
{file = "lnurl-0.4.2.tar.gz", hash = "sha256:c5e708b255d5333a0c08ceffe90ae4be6d2d09eb51dc8c35d19d8aa4cb21842a"},
|
||||
{file = "lnurl-0.5.1-py3-none-any.whl", hash = "sha256:41a03eac08c32b9ee2c6d83b9f1e88bcc5b393b36d82a41d73a02180fa04f249"},
|
||||
{file = "lnurl-0.5.1.tar.gz", hash = "sha256:a099899e622b23e6197c3f2e3ba38499658f1e6e9e2c456d55caa57373ca925b"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
bech32 = ">=1.2.0,<2.0.0"
|
||||
bolt11 = ">=2.0.5,<3.0.0"
|
||||
ecdsa = ">=0.19.0,<0.20.0"
|
||||
httpx = ">=0.27.0,<0.28.0"
|
||||
pydantic = ">=1,<2"
|
||||
requests = ">=2.31.0,<3.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "loguru"
|
||||
@ -3054,4 +3057,4 @@ liquid = ["wallycore"]
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.10 | ^3.9"
|
||||
content-hash = "33f9d6ee851ae77b6e02cc8964d1a6ea233ba3ff4cfaeeb082c327654c9cd7e0"
|
||||
content-hash = "4e316d3fcc22d41ada4de66e57049011312664f6b71853b0be441e289a402595"
|
||||
|
@ -15,11 +15,10 @@ packages = [
|
||||
python = "^3.10 | ^3.9"
|
||||
bech32 = "1.2.0"
|
||||
click = "8.1.7"
|
||||
ecdsa = "0.18.0"
|
||||
fastapi = "0.109.2"
|
||||
httpx = "0.25.0"
|
||||
httpx = "0.27.0"
|
||||
jinja2 = "3.1.4"
|
||||
lnurl = "0.4.2"
|
||||
lnurl = "0.5.1"
|
||||
psycopg2-binary = "2.9.7"
|
||||
pydantic = "1.10.17"
|
||||
pyqrcode = "1.2.1"
|
||||
@ -132,7 +131,6 @@ module = [
|
||||
"lnurl.*",
|
||||
"bolt11.*",
|
||||
"bitstring.*",
|
||||
"ecdsa.*",
|
||||
"psycopg2.*",
|
||||
"pyngrok.*",
|
||||
"pyln.client.*",
|
||||
|
Loading…
x
Reference in New Issue
Block a user