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

989 lines
32 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 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, 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
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,
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
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 (
ConversionData,
CreateInvoice,
CreateLnurl,
2023-08-24 11:52:12 +02:00
CreateLnurlAuth,
DecodePayment,
Payment,
PaymentFilters,
User,
Wallet,
WalletType,
)
from lnbits.db import Filters, Page
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,
fetch_github_release_config,
2023-01-11 11:25:18 +02:00
get_valid_extensions,
)
from lnbits.helpers import generate_filter_params_openapi, url_for
from lnbits.settings import 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,
delete_dbversion,
2023-01-25 09:56:05 +02:00
delete_installed_extension,
delete_tinyurl,
drop_extension_db,
2023-01-25 09:56:05 +02:00
get_dbversions,
get_payments,
get_payments_paginated,
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_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)):
if wallet.wallet_type == WalletType.admin:
2021-11-09 17:44:05 +00:00
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}")
async def api_update_wallet_name(
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.patch("/api/v1/wallet", response_model=Wallet)
async def api_update_wallet(
name: Optional[str] = Body(None),
currency: Optional[str] = Body(None),
wallet: WalletTypeInfo = Depends(require_admin_key),
):
return await update_wallet(wallet.wallet.id, name, currency)
@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(PaymentFilters),
)
async def api_payments(
wallet: WalletTypeInfo = Depends(get_key_type),
filters: Filters = Depends(parse_filters(PaymentFilters)),
):
pending_payments = 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 pending_payments:
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
@core_app.get(
"/api/v1/payments/paginated",
name="Payment List",
summary="get paginated list of payments",
response_description="list of payments",
response_model=Page[Payment],
openapi_extra=generate_filter_params_openapi(PaymentFilters),
)
async def api_payments_paginated(
wallet: WalletTypeInfo = Depends(get_key_type),
filters: Filters = Depends(parse_filters(PaymentFilters)),
):
pending = await get_payments_paginated(
wallet_id=wallet.wallet.id,
pending=True,
exclude_uncheckable=True,
filters=filters,
)
for payment in pending.data:
await check_transaction_status(
wallet_id=payment.wallet_id, payment_hash=payment.payment_hash
)
page = await get_payments_paginated(
wallet_id=wallet.wallet.id,
pending=True,
complete=True,
filters=filters,
)
return page
async def api_payments_create_invoice(data: CreateInvoice, wallet: Wallet):
description_hash = b""
unhashed_description = b""
memo = data.memo or settings.lnbits_site_title
if data.description_hash or data.unhashed_description:
if data.description_hash:
try:
description_hash = bytes.fromhex(data.description_hash)
except ValueError:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail="'description_hash' must be a valid hex string",
)
if data.unhashed_description:
try:
unhashed_description = bytes.fromhex(data.unhashed_description)
except ValueError:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail="'unhashed_description' must be a valid hex string",
)
# do not save memo if description_hash or unhashed_description is set
memo = ""
async with db.connect() as conn:
try:
Wallets: add cln-rest (#1775) * receive and pay works * fix linter issues * import Paymentstatus from core.models * fix test real payment * fix get_payment_status check in lnbits * fix tests? * simplify * refactor AsyncClient * inline import of get_wallet_class fixes the previous cyclic import * invoice stream working * add notes as a reminder to get rid of labels when cln-rest supports payment_hash * create Payment dummy classmethod * remove unnecessary fields from dummy * fixes tests? * fix model * fix cln bug (#1814) * auth header * rename cln to corelightning * add clnrest to admin_ui * add to clnrest allowed sources * add allowed sources to .env.example * allow macaroon files * add corelightning rest to workflow * proper env names * cleanup routine * log wallet connection errors and fix macaroon clnrest * print error on connection fails * clnrest: handle disconnects faster * fix test use of get_payment_status * make format * clnrest: add unhashed_description * add unhashed_description to test * description_hash test * unhashed_description not supported by clnrest * fix checking_id return in api_payments_create_invoice * refactor test to use client instead of api_payments * formatting, some errorlogging * fix test 1 * fix other tests, paid statuses was missing * error handling * revert unnecessary changes (#1854) * apply review of motorina0 --------- Co-authored-by: jackstar12 <jkranawetter05@gmail.com> Co-authored-by: jackstar12 <62219658+jackstar12@users.noreply.github.com> Co-authored-by: dni ⚡ <office@dnilabs.com>
2023-08-23 08:59:39 +02:00
payment_hash, payment_request = await create_invoice(
2021-08-29 19:38:42 +02:00
wallet_id=wallet.id,
amount=data.amount,
memo=memo,
currency=data.unit,
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,
)
# NOTE: we get the checking_id with a seperate query because create_invoice
# does not return it and it would be a big hustle to change its return type
# (used across extensions)
Wallets: add cln-rest (#1775) * receive and pay works * fix linter issues * import Paymentstatus from core.models * fix test real payment * fix get_payment_status check in lnbits * fix tests? * simplify * refactor AsyncClient * inline import of get_wallet_class fixes the previous cyclic import * invoice stream working * add notes as a reminder to get rid of labels when cln-rest supports payment_hash * create Payment dummy classmethod * remove unnecessary fields from dummy * fixes tests? * fix model * fix cln bug (#1814) * auth header * rename cln to corelightning * add clnrest to admin_ui * add to clnrest allowed sources * add allowed sources to .env.example * allow macaroon files * add corelightning rest to workflow * proper env names * cleanup routine * log wallet connection errors and fix macaroon clnrest * print error on connection fails * clnrest: handle disconnects faster * fix test use of get_payment_status * make format * clnrest: add unhashed_description * add unhashed_description to test * description_hash test * unhashed_description not supported by clnrest * fix checking_id return in api_payments_create_invoice * refactor test to use client instead of api_payments * formatting, some errorlogging * fix test 1 * fix other tests, paid statuses was missing * error handling * revert unnecessary changes (#1854) * apply review of motorina0 --------- Co-authored-by: jackstar12 <jkranawetter05@gmail.com> Co-authored-by: jackstar12 <62219658+jackstar12@users.noreply.github.com> Co-authored-by: dni ⚡ <office@dnilabs.com>
2023-08-23 08:59:39 +02:00
payment_db = await get_standalone_payment(payment_hash, conn=conn)
assert payment_db is not None, "payment not found"
checking_id = payment_db.checking_id
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:
Wallets: add cln-rest (#1775) * receive and pay works * fix linter issues * import Paymentstatus from core.models * fix test real payment * fix get_payment_status check in lnbits * fix tests? * simplify * refactor AsyncClient * inline import of get_wallet_class fixes the previous cyclic import * invoice stream working * add notes as a reminder to get rid of labels when cln-rest supports payment_hash * create Payment dummy classmethod * remove unnecessary fields from dummy * fixes tests? * fix model * fix cln bug (#1814) * auth header * rename cln to corelightning * add clnrest to admin_ui * add to clnrest allowed sources * add allowed sources to .env.example * allow macaroon files * add corelightning rest to workflow * proper env names * cleanup routine * log wallet connection errors and fix macaroon clnrest * print error on connection fails * clnrest: handle disconnects faster * fix test use of get_payment_status * make format * clnrest: add unhashed_description * add unhashed_description to test * description_hash test * unhashed_description not supported by clnrest * fix checking_id return in api_payments_create_invoice * refactor test to use client instead of api_payments * formatting, some errorlogging * fix test 1 * fix other tests, paid statuses was missing * error handling * revert unnecessary changes (#1854) * apply review of motorina0 --------- Co-authored-by: jackstar12 <jkranawetter05@gmail.com> Co-authored-by: jackstar12 <62219658+jackstar12@users.noreply.github.com> Co-authored-by: dni ⚡ <office@dnilabs.com>
2023-08-23 08:59:39 +02:00
"checking_id": checking_id,
"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",
summary="Create or pay an invoice",
description="""
This endpoint can be used both to generate and pay a BOLT11 invoice.
To generate a new invoice for receiving funds into the authorized account,
specify at least the first four fields in the POST body: `out: false`,
`amount`, `unit`, and `memo`. To pay an arbitrary invoice from the funds
already in the authorized account, specify `out: true` and use the `bolt11`
field to supply the BOLT11 invoice to be paid.
""",
2021-10-17 18:33:29 +01:00
status_code=HTTPStatus.CREATED,
)
async def api_payments_create(
wallet: WalletTypeInfo = Depends(require_invoice_key),
invoiceData: CreateInvoice = Body(...),
2021-10-17 18:33:29 +01:00
):
if invoiceData.out is True and wallet.wallet_type == WalletType.admin:
2021-09-19 09:31:16 +02:00
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-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: CreateLnurl, 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,
detail=(
(
f"{domain} returned an invalid invoice. Expected"
f" {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"
f" == {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
try:
while True:
if await request.is_disconnected():
await request.close()
break
payment: Payment = await payment_queue.get()
if payment.wallet_id == this_wallet_id:
logger.debug("sse listener: payment received", payment)
yield dict(data=payment.json(), event="payment-received")
except asyncio.CancelledError:
2022-12-16 17:18:02 +01:00
logger.debug(f"removing listener for wallet {uid}")
api_invoice_listeners.pop(uid)
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 isinstance(X_Api_Key, str) else None
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 Exception:
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"
)
# 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(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:
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-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 Exception:
2022-11-29 10:28:19 +00:00
response.status_code = HTTPStatus.BAD_REQUEST
return {"message": "Failed to decode"}
@core_app.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 ""
2021-08-29 19:38:42 +02:00
@core_app.get("/api/v1/currencies")
async def api_list_currencies_available():
if len(settings.lnbits_allowed_currencies) > 0:
return [
item
for item in currencies.keys()
if item.upper() in settings.lnbits_allowed_currencies
]
2021-08-20 21:31:01 +01:00
return list(currencies.keys())
@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(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",
},
)
@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):
await websocketManager.connect(websocket, item_id)
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 Exception:
2022-11-23 22:31:11 +00:00
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 Exception:
2022-11-23 22:31:11 +00:00
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"
)
if not release.is_version_compatible:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, detail="Incompatible extension version"
)
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=(
f"Failed to install extension {ext_info.id} "
f"({ext_info.installed_version})."
),
)
@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,
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)
logger.success(f"Extension '{ext_id}' uninstalled.")
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
)
@core_app.get(
"/api/v1/extension/release/{org}/{repo}/{tag_name}",
dependencies=[Depends(check_admin)],
)
async def get_extension_release(org: str, repo: str, tag_name: str):
try:
config = await fetch_github_release_config(org, repo, tag_name)
if not config:
return {}
return {
"min_lnbits_version": config.min_lnbits_version,
"is_version_compatible": config.is_version_compatible(),
"warning": config.warning,
}
except Exception as ex:
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(ex)
)
@core_app.delete(
"/api/v1/extension/{ext_id}/db",
dependencies=[Depends(check_admin)],
)
async def delete_extension_db(ext_id: str):
try:
db_version = (await get_dbversions()).get(ext_id, None)
if not db_version:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail=f"Unknown extension id: {ext_id}",
)
await drop_extension_db(ext_id=ext_id)
await delete_dbversion(ext_id=ext_id)
logger.success(f"Database removed for extension '{ext_id}'")
except HTTPException as ex:
logger.error(ex)
raise ex
except Exception as ex:
logger.error(ex)
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
detail=f"Cannot delete data for extension '{ext_id}'",
)
2023-04-17 08:11:07 +02:00
# TINYURL
2023-01-12 15:16:37 +00:00
2023-08-24 11:52:12 +02:00
@core_app.post(
"/api/v1/tinyurl",
name="Tinyurl",
description="creates a 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 Exception:
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-08-24 11:52:12 +02:00
@core_app.get(
"/api/v1/tinyurl/{tinyurl_id}",
name="Tinyurl",
description="get a tinyurl by 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 Exception:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Unable to fetch tinyurl"
)
2023-08-24 11:52:12 +02:00
@core_app.delete(
"/api/v1/tinyurl/{tinyurl_id}",
name="Tinyurl",
description="delete a tinyurl by 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 Exception:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, detail="Unable to delete"
)
2023-01-12 15:16:37 +00:00
2023-08-24 11:52:12 +02:00
@core_app.get(
"/t/{tinyurl_id}",
name="Tinyurl",
description="redirects a tinyurl by id",
)
2023-01-12 15:16:37 +00:00
async def api_tinyurl(tinyurl_id: str):
2023-08-24 11:52:12 +02:00
tinyurl = await get_tinyurl(tinyurl_id)
if tinyurl:
response = RedirectResponse(url=tinyurl.url)
return response
else:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="unable to find tinyurl"
2023-01-16 12:07:46 +02:00
)