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

916 lines
29 KiB
Python
Raw Normal View History

import asyncio
2021-07-30 21:01:19 -03:00
import hashlib
2021-08-29 19:38:42 +02:00
import json
import time
import uuid
2021-08-29 19:38:42 +02:00
from http import HTTPStatus
2022-07-28 11:02:49 +01:00
from io import BytesIO
from typing import Dict, List, Optional, Tuple, Union
2021-08-29 19:38:42 +02:00
from urllib.parse import ParseResult, parse_qs, urlencode, urlparse, urlunparse
2021-10-29 13:06:59 +01:00
2022-12-01 14:41:57 +00:00
import async_timeout
2021-08-29 19:38:42 +02:00
import httpx
import pyqrcode
2022-12-01 14:41:57 +00:00
from fastapi import (
Body,
2022-12-01 14:41:57 +00:00
Depends,
Header,
Query,
Request,
Response,
WebSocket,
WebSocketDisconnect,
)
2021-08-29 19:38:42 +02:00
from fastapi.exceptions import HTTPException
2022-07-16 14:23:03 +02:00
from loguru import logger
2021-08-29 19:38:42 +02:00
from pydantic import BaseModel
2022-01-14 12:19:30 +00:00
from pydantic.fields import Field
from sse_starlette.sse import EventSourceResponse
2023-01-12 20:37:03 +00:00
from starlette.responses import RedirectResponse, StreamingResponse
2021-07-30 21:01:19 -03:00
from lnbits import bolt11, lnurl
2023-02-15 17:25:58 +02:00
from lnbits.core.helpers import (
migrate_extension_database,
stop_extension_background_work,
)
from lnbits.core.models import Payment, User, Wallet
from lnbits.db import Filters
from lnbits.decorators import (
WalletTypeInfo,
check_admin,
get_key_type,
parse_filters,
require_admin_key,
require_invoice_key,
)
2023-01-20 10:06:32 +02:00
from lnbits.extension_manager import (
2023-01-17 11:16:54 +02:00
CreateExtension,
2023-01-11 11:25:18 +02:00
Extension,
ExtensionRelease,
2023-01-11 11:25:18 +02:00
InstallableExtension,
get_valid_extensions,
)
from lnbits.helpers import generate_filter_params_openapi, url_for
2022-10-05 09:46:59 +02:00
from lnbits.settings import get_wallet_class, settings
from lnbits.utils.exchange_rates import (
currencies,
fiat_amount_as_satoshis,
satoshis_amount_as_fiat,
)
from .. import core_app, core_app_extra, db
from ..crud import (
add_installed_extension,
2023-01-12 20:37:03 +00:00
create_tinyurl,
2023-01-25 09:56:05 +02:00
delete_installed_extension,
delete_tinyurl,
2023-01-25 09:56:05 +02:00
get_dbversions,
get_payments,
get_standalone_payment,
2023-01-12 20:37:03 +00:00
get_tinyurl,
2023-01-12 22:39:20 +00:00
get_tinyurl_by_url,
get_total_balance,
get_wallet_for_key,
save_balance_check,
update_wallet,
)
from ..services import (
InvoiceFailure,
PaymentFailure,
check_transaction_status,
create_invoice,
pay_invoice,
perform_lnurlauth,
websocketManager,
websocketUpdater,
)
from ..tasks import api_invoice_listeners
2022-12-25 18:49:51 +01:00
@core_app.get("/api/v1/health", status_code=HTTPStatus.OK)
async def health():
return
2021-08-29 19:38:42 +02:00
@core_app.get("/api/v1/wallet")
async def api_wallet(wallet: WalletTypeInfo = Depends(get_key_type)):
2021-11-09 17:44:05 +00:00
if wallet.wallet_type == 0:
return {
"id": wallet.wallet.id,
"name": wallet.wallet.name,
"balance": wallet.wallet.balance_msat,
}
else:
2021-11-12 04:14:55 +00:00
return {"name": wallet.wallet.name, "balance": wallet.wallet.balance_msat}
2021-10-17 18:33:29 +01:00
2021-08-16 19:27:39 +01:00
2021-08-22 23:05:39 +01:00
@core_app.put("/api/v1/wallet/{new_name}")
2022-02-16 22:42:27 +01:00
async def api_update_wallet(
2022-05-16 11:21:30 +01:00
new_name: str, wallet: WalletTypeInfo = Depends(require_admin_key)
2022-02-16 22:42:27 +01:00
):
2021-08-29 19:38:42 +02:00
await update_wallet(wallet.wallet.id, new_name)
return {
"id": wallet.wallet.id,
"name": wallet.wallet.name,
"balance": wallet.wallet.balance_msat,
}
2021-08-06 11:15:07 +01:00
@core_app.get(
"/api/v1/payments",
name="Payment List",
summary="get list of payments",
response_description="list of payments",
response_model=List[Payment],
openapi_extra=generate_filter_params_openapi(Payment),
)
async def api_payments(
wallet: WalletTypeInfo = Depends(get_key_type),
filters: Filters = Depends(parse_filters(Payment)),
):
2022-02-16 22:42:27 +01:00
pendingPayments = await get_payments(
wallet_id=wallet.wallet.id,
pending=True,
exclude_uncheckable=True,
filters=filters,
2022-02-16 22:42:27 +01:00
)
for payment in pendingPayments:
await check_transaction_status(
2022-02-16 22:42:27 +01:00
wallet_id=payment.wallet_id, payment_hash=payment.payment_hash
)
return await get_payments(
wallet_id=wallet.wallet.id,
pending=True,
complete=True,
filters=filters,
)
2021-10-17 18:33:29 +01:00
2021-08-20 21:31:01 +01:00
class CreateInvoiceData(BaseModel):
2021-09-19 09:31:16 +02:00
out: Optional[bool] = True
2022-02-16 22:41:12 +01:00
amount: float = Query(None, ge=0)
2022-03-24 12:50:57 +01:00
memo: Optional[str] = None
2021-11-12 04:14:55 +00:00
unit: Optional[str] = "sat"
2021-11-02 16:29:15 +00:00
description_hash: Optional[str] = None
unhashed_description: Optional[str] = None
expiry: Optional[int] = None
2021-10-17 18:33:29 +01:00
lnurl_callback: Optional[str] = None
lnurl_balance_check: Optional[str] = None
extra: Optional[dict] = None
webhook: Optional[str] = None
internal: Optional[bool] = False
2021-09-19 09:31:16 +02:00
bolt11: Optional[str] = None
2021-10-17 18:33:29 +01:00
2021-08-29 19:38:42 +02:00
async def api_payments_create_invoice(data: CreateInvoiceData, wallet: Wallet):
if data.description_hash or data.unhashed_description:
try:
description_hash = (
bytes.fromhex(data.description_hash) if data.description_hash else b""
)
unhashed_description = (
bytes.fromhex(data.unhashed_description)
if data.unhashed_description
else b""
)
except ValueError:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail="'description_hash' and 'unhashed_description' must be a valid hex strings",
)
memo = ""
else:
description_hash = b""
unhashed_description = b""
memo = data.memo or settings.lnbits_site_title
2021-11-12 04:14:55 +00:00
if data.unit == "sat":
2022-02-16 22:41:12 +01:00
amount = int(data.amount)
else:
assert data.unit is not None, "unit not set"
2021-08-20 21:31:01 +01:00
price_in_sats = await fiat_amount_as_satoshis(data.amount, data.unit)
amount = price_in_sats
async with db.connect() as conn:
try:
2023-01-21 12:07:19 +00:00
_, payment_request = await create_invoice(
2021-08-29 19:38:42 +02:00
wallet_id=wallet.id,
amount=amount,
memo=memo,
description_hash=description_hash,
unhashed_description=unhashed_description,
expiry=data.expiry,
2021-08-20 21:31:01 +01:00
extra=data.extra,
webhook=data.webhook,
internal=data.internal,
conn=conn,
)
except InvoiceFailure as e:
2021-10-17 18:33:29 +01:00
raise HTTPException(status_code=520, detail=str(e))
except Exception as exc:
raise exc
invoice = bolt11.decode(payment_request)
lnurl_response: Union[None, bool, str] = None
2021-08-20 21:31:01 +01:00
if data.lnurl_callback:
if data.lnurl_balance_check is not None:
2022-07-19 18:51:35 +02:00
await save_balance_check(wallet.id, data.lnurl_balance_check)
2021-04-17 18:27:15 -03:00
async with httpx.AsyncClient() as client:
try:
2020-12-31 18:50:16 +01:00
r = await client.get(
2021-08-20 21:31:01 +01:00
data.lnurl_callback,
2021-04-17 18:27:15 -03:00
params={
"pr": payment_request,
"balanceNotify": url_for(
f"/withdraw/notify/{urlparse(data.lnurl_callback).netloc}",
external=True,
2022-02-01 23:11:26 +00:00
wal=wallet.id,
2021-04-17 18:27:15 -03:00
),
},
2020-12-31 18:50:16 +01:00
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
2022-12-21 14:56:27 +02:00
except (httpx.ConnectError, httpx.RequestError) as ex:
logger.error(ex)
lnurl_response = False
return {
"payment_hash": invoice.payment_hash,
"payment_request": payment_request,
# maintain backwards compatibility with API clients:
"checking_id": invoice.payment_hash,
"lnurl_response": lnurl_response,
}
2021-08-29 19:38:42 +02:00
async def api_payments_pay_invoice(bolt11: str, wallet: Wallet):
try:
2021-10-17 18:33:29 +01:00
payment_hash = await pay_invoice(wallet_id=wallet.id, payment_request=bolt11)
2020-04-16 17:10:53 +02:00
except ValueError as e:
2021-10-17 18:33:29 +01:00
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=str(e))
2020-04-16 17:10:53 +02:00
except PermissionError as e:
2021-10-17 18:33:29 +01:00
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail=str(e))
except PaymentFailure as e:
2021-10-17 18:33:29 +01:00
raise HTTPException(status_code=520, detail=str(e))
except Exception as exc:
raise exc
return {
"payment_hash": payment_hash,
# maintain backwards compatibility with API clients:
"checking_id": payment_hash,
}
2021-10-17 18:33:29 +01:00
@core_app.post(
"/api/v1/payments",
status_code=HTTPStatus.CREATED,
)
async def api_payments_create(
wallet: WalletTypeInfo = Depends(require_invoice_key),
invoiceData: CreateInvoiceData = Body(...),
2021-10-17 18:33:29 +01:00
):
2021-09-19 09:31:16 +02:00
if invoiceData.out is True and wallet.wallet_type == 0:
if not invoiceData.bolt11:
2021-10-17 18:33:29 +01:00
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail="BOLT11 string is invalid or not given",
)
2022-02-16 22:42:27 +01:00
return await api_payments_pay_invoice(
invoiceData.bolt11, wallet.wallet
) # admin key
elif not invoiceData.out:
# invoice key
return await api_payments_create_invoice(invoiceData, wallet.wallet)
else:
raise HTTPException(
status_code=HTTPStatus.UNAUTHORIZED,
detail="Invoice (or Admin) key required.",
)
2021-10-17 18:33:29 +01:00
2021-08-20 21:31:01 +01:00
class CreateLNURLData(BaseModel):
2021-10-17 18:33:29 +01:00
description_hash: str
callback: str
amount: int
comment: Optional[str] = None
description: Optional[str] = None
2021-09-19 13:25:39 +02:00
@core_app.post("/api/v1/payments/lnurl")
2022-02-16 22:42:27 +01:00
async def api_payments_pay_lnurl(
data: CreateLNURLData, wallet: WalletTypeInfo = Depends(require_admin_key)
2022-02-16 22:42:27 +01:00
):
2021-08-20 21:31:01 +01:00
domain = urlparse(data.callback).netloc
async with httpx.AsyncClient() as client:
try:
r = await client.get(
2021-08-20 21:31:01 +01:00
data.callback,
params={"amount": data.amount, "comment": data.comment},
2020-12-31 18:50:16 +01:00
timeout=40,
)
if r.is_error:
raise httpx.ConnectError("LNURL callback connection error")
except (httpx.ConnectError, httpx.RequestError):
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
2021-10-17 18:33:29 +01:00
detail=f"Failed to connect to {domain}.",
2021-07-30 21:01:19 -03:00
)
params = json.loads(r.text)
if params.get("status") == "ERROR":
raise HTTPException(
2021-10-17 18:33:29 +01:00
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"])
2021-08-20 21:31:01 +01:00
if invoice.amount_msat != data.amount:
raise HTTPException(
2021-10-17 18:33:29 +01:00
status_code=HTTPStatus.BAD_REQUEST,
2022-02-01 21:22:20 +00:00
detail=f"{domain} returned an invalid invoice. Expected {data.amount} msat, got {invoice.amount_msat}.",
)
2021-10-17 18:33:29 +01:00
if invoice.description_hash != data.description_hash:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail=f"{domain} returned an invalid invoice. Expected description_hash == {data.description_hash}, got {invoice.description_hash}.",
)
extra = {}
if params.get("successAction"):
extra["success_action"] = params["successAction"]
2021-08-20 21:31:01 +01:00
if data.comment:
extra["comment"] = data.comment
assert data.description is not None, "description is required"
payment_hash = await pay_invoice(
2021-09-19 13:25:39 +02:00
wallet_id=wallet.wallet.id,
payment_request=params["pr"],
2021-08-20 21:31:01 +01:00
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,
}
2021-10-17 18:33:29 +01:00
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.
"""
this_wallet_id = wallet.id
payment_queue: asyncio.Queue[Payment] = asyncio.Queue(0)
uid = f"{this_wallet_id}_{str(uuid.uuid4())[:8]}"
logger.debug(f"adding sse listener for wallet: {uid}")
api_invoice_listeners[uid] = payment_queue
2022-07-19 18:51:35 +02:00
send_queue: asyncio.Queue[Tuple[str, Payment]] = asyncio.Queue(0)
async def payment_received() -> None:
while True:
try:
async with async_timeout.timeout(1):
payment: Payment = await payment_queue.get()
if payment.wallet_id == this_wallet_id:
logger.debug("sse listener: payment received", payment)
await send_queue.put(("payment-received", payment))
except asyncio.TimeoutError:
pass
task = asyncio.create_task(payment_received())
try:
while True:
if await request.is_disconnected():
await request.close()
break
typ, data = await send_queue.get()
if data:
jdata = json.dumps(dict(data.dict(), pending=False))
yield dict(data=jdata, event=typ)
except asyncio.CancelledError:
2022-12-16 17:18:02 +01:00
logger.debug(f"removing listener for wallet {uid}")
api_invoice_listeners.pop(uid)
task.cancel()
return
@core_app.get("/api/v1/payments/sse")
2022-02-16 22:42:27 +01:00
async def api_payments_sse(
request: Request, wallet: WalletTypeInfo = Depends(get_key_type)
):
return EventSourceResponse(
subscribe_wallet_invoices(request, wallet.wallet),
ping=20,
media_type="text/event-stream",
2022-02-16 22:42:27 +01:00
)
# TODO: refactor this route into a public and admin one
2021-08-29 19:38:42 +02:00
@core_app.get("/api/v1/payments/{payment_hash}")
2022-03-07 08:08:08 +00:00
async def api_payment(payment_hash, X_Api_Key: Optional[str] = Header(None)):
# We use X_Api_Key here because we want this call to work with and without keys
# If a valid key is given, we also return the field "details", otherwise not
wallet = await get_wallet_for_key(X_Api_Key) if type(X_Api_Key) == str else None # type: ignore
2022-07-20 11:21:38 +02:00
# we have to specify the wallet id here, because postgres and sqlite return internal payments in different order
# and get_standalone_payment otherwise just fetches the first one, causing unpredictable results
payment = await get_standalone_payment(
payment_hash, wallet_id=wallet.id if wallet else None
2022-07-20 11:21:38 +02:00
)
if payment is None:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Payment does not exist."
)
await check_transaction_status(payment.wallet_id, payment_hash)
payment = await get_standalone_payment(
payment_hash, wallet_id=wallet.id if wallet else None
)
2021-08-29 19:38:42 +02:00
if not payment:
2022-02-16 22:42:27 +01:00
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Payment does not exist."
)
2021-08-29 19:38:42 +02:00
elif not payment.pending:
2022-03-07 08:08:08 +00:00
if wallet and wallet.id == payment.wallet_id:
return {"paid": True, "preimage": payment.preimage, "details": payment}
2021-09-11 11:02:48 +02:00
return {"paid": True, "preimage": payment.preimage}
2021-08-29 19:38:42 +02:00
try:
await payment.check_status()
2021-08-29 19:38:42 +02:00
except Exception:
2022-03-07 08:08:08 +00:00
if wallet and wallet.id == payment.wallet_id:
return {"paid": False, "details": payment}
2021-09-11 11:02:48 +02:00
return {"paid": False}
2021-08-29 19:38:42 +02:00
2022-03-07 08:08:08 +00:00
if wallet and wallet.id == payment.wallet_id:
2022-06-01 14:53:05 +02:00
return {
"paid": not payment.pending,
"preimage": payment.preimage,
"details": payment,
}
return {"paid": not payment.pending, "preimage": payment.preimage}
2021-10-17 18:33:29 +01:00
@core_app.get("/api/v1/lnurlscan/{code}")
async def api_lnurlscan(code: str, wallet: WalletTypeInfo = Depends(get_key_type)):
try:
2021-07-30 21:01:19 -03:00
url = lnurl.decode(code)
domain = urlparse(url).netloc
except:
# 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"
)
# 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:
async with httpx.AsyncClient() as client:
2021-07-30 21:01:19 -03:00
r = await client.get(url, timeout=5)
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:
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]}'",
},
)
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
},
)
2021-08-20 21:31:01 +01:00
return params
2022-03-24 12:50:57 +01:00
class DecodePayment(BaseModel):
data: str
2022-11-29 10:28:19 +00:00
@core_app.post("/api/v1/payments/decode", status_code=HTTPStatus.OK)
async def api_payments_decode(data: DecodePayment, response: Response):
2022-03-24 12:50:57 +01:00
payment_str = data.data
try:
2022-03-24 12:50:57 +01:00
if payment_str[:5] == "LNURL":
url = lnurl.decode(payment_str)
return {"domain": url}
else:
2022-03-24 12:50:57 +01:00
invoice = bolt11.decode(payment_str)
return {
"payment_hash": invoice.payment_hash,
"amount_msat": invoice.amount_msat,
"description": invoice.description,
"description_hash": invoice.description_hash,
"payee": invoice.payee,
"date": invoice.date,
"expiry": invoice.expiry,
"secret": invoice.secret,
"route_hints": invoice.route_hints,
"min_final_cltv_expiry": invoice.min_final_cltv_expiry,
}
except:
2022-11-29 10:28:19 +00:00
response.status_code = HTTPStatus.BAD_REQUEST
return {"message": "Failed to decode"}
class Callback(BaseModel):
callback: str = Query(...)
@core_app.post("/api/v1/lnurlauth")
async def api_perform_lnurlauth(
callback: Callback, wallet: WalletTypeInfo = Depends(require_admin_key)
):
err = await perform_lnurlauth(callback.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 ""
2021-08-29 19:38:42 +02:00
@core_app.get("/api/v1/currencies")
async def api_list_currencies_available():
2021-08-20 21:31:01 +01:00
return list(currencies.keys())
class ConversionData(BaseModel):
2022-01-30 19:43:30 +00:00
from_: str = Field("sat", alias="from")
amount: float
2022-01-30 19:43:30 +00:00
to: str = Query("usd")
@core_app.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
2022-04-16 11:15:42 +01:00
@core_app.get("/api/v1/qrcode/{data}", response_class=StreamingResponse)
async def img(request: Request, 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",
},
)
2022-12-26 12:12:27 +01:00
@core_app.get("/api/v1/audit", dependencies=[Depends(check_admin)])
async def api_auditor():
WALLET = get_wallet_class()
total_balance = await get_total_balance()
error_message, node_balance = await WALLET.status()
if not error_message:
delta = node_balance - total_balance
else:
node_balance, delta = 0, 0
return {
2022-12-08 14:33:52 +01:00
"node_balance_msats": int(node_balance),
"lnbits_balance_msats": int(total_balance),
"delta_msats": int(delta),
"timestamp": int(time.time()),
}
2023-04-17 08:11:07 +02:00
# UNIVERSAL WEBSOCKET MANAGER
2022-11-23 22:42:32 +00:00
@core_app.websocket("/api/v1/ws/{item_id}")
2022-11-23 22:22:33 +00:00
async def websocket_connect(websocket: WebSocket, item_id: str):
2022-12-01 14:41:57 +00:00
await websocketManager.connect(websocket)
try:
while True:
await websocket.receive_text()
except WebSocketDisconnect:
2022-11-23 23:35:02 +00:00
websocketManager.disconnect(websocket)
2022-11-23 22:42:32 +00:00
@core_app.post("/api/v1/ws/{item_id}")
2022-11-24 00:46:39 +00:00
async def websocket_update_post(item_id: str, data: str):
2022-11-23 22:31:11 +00:00
try:
2022-11-23 23:35:02 +00:00
await websocketUpdater(item_id, data)
2022-11-23 22:31:11 +00:00
return {"sent": True, "data": data}
except:
return {"sent": False, "data": data}
2022-11-23 22:42:32 +00:00
2022-11-23 22:27:09 +00:00
@core_app.get("/api/v1/ws/{item_id}/{data}")
2022-11-24 00:46:39 +00:00
async def websocket_update_get(item_id: str, data: str):
2022-11-23 22:31:11 +00:00
try:
2022-11-23 23:35:02 +00:00
await websocketUpdater(item_id, data)
2022-11-23 22:31:11 +00:00
return {"sent": True, "data": data}
except:
return {"sent": False, "data": data}
2023-01-17 11:16:54 +02:00
@core_app.post("/api/v1/extension")
async def api_install_extension(
2023-01-17 11:16:54 +02:00
data: CreateExtension, user: User = Depends(check_admin)
):
2023-01-17 14:51:09 +02:00
release = await InstallableExtension.get_extension_release(
data.ext_id, data.source_repo, data.archive
)
2023-01-17 14:51:09 +02:00
if not release:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Release not found"
)
2023-01-17 11:16:54 +02:00
ext_info = InstallableExtension(
id=data.ext_id, name=data.ext_id, installed_release=release, icon=release.icon
)
2023-01-17 11:16:54 +02:00
ext_info.download_archive()
try:
ext_info.extract_archive()
2022-11-29 18:48:11 +02:00
2023-01-11 14:34:05 +02:00
extension = Extension.from_installable_ext(ext_info)
2023-01-17 11:16:54 +02:00
db_version = (await get_dbversions()).get(data.ext_id, 0)
2023-01-11 14:34:05 +02:00
await migrate_extension_database(extension, db_version)
2023-01-17 16:28:24 +02:00
await add_installed_extension(ext_info)
2023-02-15 17:25:58 +02:00
# call stop while the old routes are still active
await stop_extension_background_work(data.ext_id, user.id)
if data.ext_id not in settings.lnbits_deactivated_extensions:
settings.lnbits_deactivated_extensions += [data.ext_id]
2022-12-22 16:59:14 +02:00
# mount routes for the new version
core_app_extra.register_new_ext_routes(extension)
2023-01-25 14:32:41 +02:00
if extension.upgrade_hash:
ext_info.nofiy_upgrade()
2023-02-01 06:12:00 +00:00
return extension
except Exception as ex:
logger.warning(ex)
ext_info.clean_extension_files()
raise HTTPException(
2023-01-25 14:32:41 +02:00
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
detail="Failed to install extension.",
)
@core_app.delete("/api/v1/extension/{ext_id}")
2023-01-10 17:51:04 +02:00
async def api_uninstall_extension(ext_id: str, user: User = Depends(check_admin)):
2023-02-16 10:48:27 +02:00
installable_extensions = await InstallableExtension.get_installable_extensions()
2023-01-11 15:51:56 +02:00
extensions = [e for e in installable_extensions if e.id == ext_id]
if len(extensions) == 0:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail=f"Unknown extension id: {ext_id}",
)
# check that other extensions do not depend on this one
for valid_ext_id in list(map(lambda e: e.code, get_valid_extensions())):
2023-01-11 15:51:56 +02:00
installed_ext = next(
(ext for ext in installable_extensions if ext.id == valid_ext_id), None
)
2023-01-11 15:51:56 +02:00
if installed_ext and ext_id in installed_ext.dependencies:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
2023-01-11 15:51:56 +02:00
detail=f"Cannot uninstall. Extension '{installed_ext.name}' depends on this one.",
)
try:
# call stop while the old routes are still active
2023-02-15 17:25:58 +02:00
await stop_extension_background_work(ext_id, user.id)
if ext_id not in settings.lnbits_deactivated_extensions:
settings.lnbits_deactivated_extensions += [ext_id]
2023-01-11 15:51:56 +02:00
for ext_info in extensions:
ext_info.clean_extension_files()
await delete_installed_extension(ext_id=ext_info.id)
except Exception as ex:
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(ex)
)
2023-01-16 12:07:46 +02:00
@core_app.get(
"/api/v1/extension/{ext_id}/releases", dependencies=[Depends(check_admin)]
)
async def get_extension_releases(ext_id: str):
2023-01-16 12:07:46 +02:00
try:
extension_releases: List[
ExtensionRelease
] = await InstallableExtension.get_extension_releases(ext_id)
2023-01-16 12:07:46 +02:00
return extension_releases
2023-01-16 12:07:46 +02:00
except Exception as ex:
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(ex)
2023-01-25 09:56:05 +02:00
)
2023-04-17 08:11:07 +02:00
# TINYURL
2023-01-12 15:16:37 +00:00
@core_app.post("/api/v1/tinyurl")
async def api_create_tinyurl(
url: str, endless: bool = False, wallet: WalletTypeInfo = Depends(get_key_type)
):
tinyurls = await get_tinyurl_by_url(url)
try:
for tinyurl in tinyurls:
if tinyurl:
if tinyurl.wallet == wallet.wallet.inkey:
return tinyurl
return await create_tinyurl(url, endless, wallet.wallet.inkey)
except:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, detail="Unable to create tinyurl"
)
2023-01-12 15:16:37 +00:00
2023-01-12 22:52:46 +00:00
2023-01-12 15:16:37 +00:00
@core_app.get("/api/v1/tinyurl/{tinyurl_id}")
async def api_get_tinyurl(
tinyurl_id: str, wallet: WalletTypeInfo = Depends(get_key_type)
):
try:
tinyurl = await get_tinyurl(tinyurl_id)
2023-01-24 21:29:52 +00:00
if tinyurl:
if tinyurl.wallet == wallet.wallet.inkey:
return tinyurl
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN, detail="Wrong key provided."
)
except:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Unable to fetch tinyurl"
)
@core_app.delete("/api/v1/tinyurl/{tinyurl_id}")
async def api_delete_tinyurl(
tinyurl_id: str, wallet: WalletTypeInfo = Depends(get_key_type)
):
try:
tinyurl = await get_tinyurl(tinyurl_id)
2023-01-24 21:29:52 +00:00
if tinyurl:
if tinyurl.wallet == wallet.wallet.inkey:
await delete_tinyurl(tinyurl_id)
return {"deleted": True}
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN, detail="Wrong key provided."
)
except:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, detail="Unable to delete"
)
2023-01-12 15:16:37 +00:00
2023-01-12 22:39:20 +00:00
@core_app.get("/t/{tinyurl_id}")
2023-01-12 15:16:37 +00:00
async def api_tinyurl(tinyurl_id: str):
try:
tinyurl = await get_tinyurl(tinyurl_id)
if tinyurl:
response = RedirectResponse(url=tinyurl.url)
return response
else:
return
except:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="unable to find tinyurl"
2023-01-16 12:07:46 +02:00
)