lnbits-legend/lnbits/core/views/api.py

245 lines
7.9 KiB
Python
Raw Normal View History

2021-07-30 21:01:19 -03:00
import hashlib
2021-08-29 19:38:42 +02:00
import json
from http import HTTPStatus
2022-07-28 11:02:49 +01:00
from io import BytesIO
from typing import Dict, List
from urllib.parse import ParseResult, parse_qs, urlencode, urlparse, urlunparse
2021-10-29 13:06:59 +01:00
2021-08-29 19:38:42 +02:00
import httpx
import pyqrcode
2022-12-01 14:41:57 +00:00
from fastapi import (
APIRouter,
2022-12-01 14:41:57 +00:00
Depends,
)
2021-08-29 19:38:42 +02:00
from fastapi.exceptions import HTTPException
from starlette.responses import StreamingResponse
from lnbits.core.models import (
BaseWallet,
ConversionData,
2023-08-24 11:52:12 +02:00
CreateLnurlAuth,
CreateWallet,
User,
Wallet,
)
from lnbits.decorators import (
WalletTypeInfo,
check_user_exists,
require_admin_key,
require_invoice_key,
2023-01-11 11:25:18 +02:00
)
from lnbits.lnurl import decode as lnurl_decode
from lnbits.settings import settings
from lnbits.utils.exchange_rates import (
allowed_currencies,
fiat_amount_as_satoshis,
satoshis_amount_as_fiat,
)
from ..services import create_user_account, perform_lnurlauth
# backwards compatibility for extension
# TODO: remove api_payment and pay_invoice imports from extensions
from .payment_api import api_payment, pay_invoice # noqa: F401
api_router = APIRouter(tags=["Core"])
@api_router.get("/api/v1/health", status_code=HTTPStatus.OK)
2022-12-25 18:49:51 +01:00
async def health():
return
@api_router.get(
"/api/v1/wallets",
name="Wallets",
description="Get basic info for all of user's wallets.",
)
async def api_wallets(user: User = Depends(check_user_exists)) -> List[BaseWallet]:
return [BaseWallet(**w.dict()) for w in user.wallets]
@api_router.post("/api/v1/account", response_model=Wallet)
async def api_create_account(data: CreateWallet) -> Wallet:
if not settings.new_accounts_allowed:
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN,
detail="Account creation is disabled.",
)
account = await create_user_account(wallet_name=data.name)
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))
2021-07-30 21:01:19 -03:00
domain = urlparse(url).netloc
except Exception as exc:
2021-07-30 21:01:19 -03:00
# parse internet identifier (user@domain.com)
name_domain = code.split("@")
if len(name_domain) == 2 and len(name_domain[1].split(".")) >= 2:
2021-07-30 21:01:19 -03:00
name, domain = name_domain
2022-02-16 22:42:27 +01:00
url = (
("http://" if domain.endswith(".onion") else "https://")
+ domain
+ "/.well-known/lnurlp/"
+ name
)
2021-07-30 21:01:19 -03:00
# will proceed with these values
else:
2022-02-16 22:42:27 +01:00
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}
2021-07-30 21:01:19 -03:00
if "tag=login" in url:
params.update(kind="auth")
2021-07-30 21:01:19 -03:00
params.update(callback=url) # with k1 already in it
lnurlauth_key = wallet.wallet.lnurlauth_key(domain)
assert lnurlauth_key.verifying_key
2022-01-30 19:43:30 +00:00
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:
2021-07-30 21:01:19 -03:00
r = await client.get(url, timeout=5)
r.raise_for_status()
if r.is_error:
raise HTTPException(
2021-10-17 18:33:29 +01:00
status_code=HTTPStatus.SERVICE_UNAVAILABLE,
2022-01-30 19:43:30 +00:00
detail={"domain": domain, "message": "failed to get parameters"},
)
try:
2021-07-30 21:01:19 -03:00
data = json.loads(r.text)
except json.decoder.JSONDecodeError as exc:
raise HTTPException(
2021-10-17 18:33:29 +01:00
status_code=HTTPStatus.SERVICE_UNAVAILABLE,
detail={
"domain": domain,
"message": f"got invalid response '{r.text[:200]}'",
},
) from exc
2021-07-30 21:01:19 -03:00
try:
tag: str = data.get("tag")
params.update(**data)
2021-07-30 21:01:19 -03:00
if tag == "channelRequest":
raise HTTPException(
2021-10-17 18:33:29 +01:00
status_code=HTTPStatus.BAD_REQUEST,
detail={
"domain": domain,
"kind": "channel",
"message": "unsupported",
},
2021-07-30 21:01:19 -03:00
)
elif tag == "withdrawRequest":
2021-07-30 21:01:19 -03:00
params.update(kind="withdraw")
2022-01-30 19:43:30 +00:00
params.update(fixed=data["minWithdrawable"] == data["maxWithdrawable"])
2021-07-30 21:01:19 -03:00
# 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
2022-02-16 22:42:27 +01:00
parsed_callback = parsed_callback._replace(
query=urlencode(qs, doseq=True)
)
2021-07-30 21:01:19 -03:00
params.update(callback=urlunparse(parsed_callback))
elif tag == "payRequest":
2021-07-30 21:01:19 -03:00
params.update(kind="pay")
params.update(fixed=data["minSendable"] == data["maxSendable"])
2022-02-16 22:42:27 +01:00
params.update(
description_hash=hashlib.sha256(
data["metadata"].encode()
2022-02-16 22:42:27 +01:00
).hexdigest()
)
2021-07-30 21:01:19 -03:00
metadata = json.loads(data["metadata"])
for [k, v] in metadata:
if k == "text/plain":
params.update(description=v)
2023-01-21 12:11:45 +00:00
if k in ("image/jpeg;base64", "image/png;base64"):
data_uri = f"data:{k},{v}"
2021-07-30 21:01:19 -03:00
params.update(image=data_uri)
2023-01-21 12:11:45 +00:00
if k in ("text/email", "text/identifier"):
2021-07-30 21:01:19 -03:00
params.update(targetUser=v)
params.update(commentAllowed=data.get("commentAllowed", 0))
2021-07-30 21:01:19 -03:00
except KeyError as exc:
raise HTTPException(
status_code=HTTPStatus.SERVICE_UNAVAILABLE,
detail={
"domain": domain,
"message": f"lnurl JSON response invalid: {exc}",
2021-10-17 18:33:29 +01:00
},
) from exc
2021-10-17 18:33:29 +01:00
2021-08-20 21:31:01 +01:00
return params
@api_router.post("/api/v1/lnurlauth")
async def api_perform_lnurlauth(
2023-08-24 11:52:12 +02:00
data: CreateLnurlAuth, wallet: WalletTypeInfo = Depends(require_admin_key)
):
err = await perform_lnurlauth(data.callback, wallet=wallet)
2020-11-10 23:01:55 -03:00
if err:
2022-02-16 22:42:27 +01:00
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()
@api_router.post("/api/v1/conversion")
async def api_fiat_as_sats(data: ConversionData):
2022-01-14 12:19:30 +00:00
output = {}
2022-01-30 19:43:30 +00:00
if data.from_ == "sat":
2022-01-14 12:19:30 +00:00
output["BTC"] = data.amount / 100000000
2022-07-26 12:46:43 +02:00
output["sats"] = int(data.amount)
2022-01-30 19:43:30 +00:00
for currency in data.to.split(","):
2022-02-16 22:42:27 +01:00
output[currency.strip().upper()] = await satoshis_amount_as_fiat(
data.amount, currency.strip()
)
2022-01-14 12:19:30 +00:00
return output
else:
output[data.from_.upper()] = data.amount
2022-03-10 12:16:51 +00:00
output["sats"] = await fiat_amount_as_satoshis(data.amount, data.from_)
2022-01-14 12:19:30 +00:00
output["BTC"] = output["sats"] / 100000000
return output
2022-07-28 11:02:49 +01:00
@api_router.get("/api/v1/qrcode/{data}", response_class=StreamingResponse)
async def img(data):
qr = pyqrcode.create(data)
stream = BytesIO()
qr.svg(stream, scale=3)
stream.seek(0)
async def _generator(stream: BytesIO):
yield stream.getvalue()
return StreamingResponse(
_generator(stream),
headers={
"Content-Type": "image/svg+xml",
"Cache-Control": "no-cache, no-store, must-revalidate",
"Pragma": "no-cache",
"Expires": "0",
},
)