Merge pull request #306 from arcbtc/FastAPI

Started on extensions and projects
This commit is contained in:
Arc 2021-08-20 21:36:40 +01:00 committed by GitHub
commit 0baa34ec2f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 1311 additions and 399 deletions

View file

@ -6,11 +6,11 @@ from ecdsa import SECP256k1, SigningKey # type: ignore
from lnurl import encode as lnurl_encode # type: ignore
from typing import List, NamedTuple, Optional, Dict
from sqlite3 import Row
from pydantic import BaseModel
from lnbits.settings import WALLET
class User(NamedTuple):
class User(BaseModel):
id: str
email: str
extensions: List[str] = []
@ -26,7 +26,7 @@ class User(NamedTuple):
return w[0] if w else None
class Wallet(NamedTuple):
class Wallet(BaseModel):
id: str
name: str
user: str
@ -73,7 +73,7 @@ class Wallet(NamedTuple):
return await get_wallet_payment(self.id, payment_hash)
class Payment(NamedTuple):
class Payment(BaseModel):
checking_id: str
pending: bool
amount: int
@ -161,7 +161,7 @@ class Payment(NamedTuple):
await delete_payment(self.checking_id)
class BalanceCheck(NamedTuple):
class BalanceCheck(BaseModel):
wallet: str
service: str
url: str

View file

@ -1,3 +1,4 @@
from pydantic.types import constr
import trio
import json
import httpx
@ -5,15 +6,13 @@ import hashlib
from urllib.parse import urlparse, urlunparse, urlencode, parse_qs, ParseResult
from quart import g, current_app, make_response, url_for
from fastapi import FastAPI
from fastapi import Query
from fastapi.encoders import jsonable_encoder
from fastapi.responses import JSONResponse
from pydantic import BaseModel
from http import HTTPStatus
from binascii import unhexlify
from typing import Dict, Union
from typing import Dict, List, Optional, Union
from lnbits import bolt11, lnurl
from lnbits.decorators import api_check_wallet_key, api_validate_post_request
@ -68,53 +67,30 @@ async def api_payments():
HTTPStatus.OK,
)
class CreateInvoiceData(BaseModel):
amount: int = Query(None, ge=1)
memo: str = None
unit: Optional[str] = None
description_hash: str = None
lnurl_callback: Optional[str] = None
lnurl_balance_check: Optional[str] = None
extra: Optional[dict] = None
webhook: Optional[str] = None
@api_check_wallet_key("invoice")
@api_validate_post_request(
schema={
"amount": {"type": "number", "min": 0.001, "required": True},
"memo": {
"type": "string",
"empty": False,
"required": True,
"excludes": "description_hash",
},
"unit": {"type": "string", "empty": False, "required": False},
"description_hash": {
"type": "string",
"empty": False,
"required": True,
"excludes": "memo",
},
"lnurl_callback": {"type": "string", "nullable": True, "required": False},
"lnurl_balance_check": {"type": "string", "required": False},
"extra": {"type": "dict", "nullable": True, "required": False},
"webhook": {"type": "string", "empty": False, "required": False},
}
)
# async def api_payments_create_invoice(amount: List[str] = Query([type: str = Query(None)])):
class Memo(BaseModel):
type: str
empty: bool
required: bool
excludes: bool
excludes: bool = Query("description_hash")
async def api_payments_create_invoice(amount: int = Query(...), memo: Memo ):
if "description_hash" in g.data:
description_hash = unhexlify(g.data["description_hash"])
async def api_payments_create_invoice(data: CreateInvoiceData):
if "description_hash" in data:
description_hash = unhexlify(data.description_hash)
memo = ""
else:
description_hash = b""
memo = g.data["memo"]
memo = data.memo
if g.data.get("unit") or "sat" == "sat":
amount = g.data["amount"]
if data.unit or "sat" == "sat":
amount = data.amount
else:
price_in_sats = await fiat_amount_as_satoshis(g.data["amount"], g.data["unit"])
price_in_sats = await fiat_amount_as_satoshis(data.amount, data.unit)
amount = price_in_sats
async with db.connect() as conn:
@ -124,31 +100,31 @@ async def api_payments_create_invoice(amount: int = Query(...), memo: Memo ):
amount=amount,
memo=memo,
description_hash=description_hash,
extra=g.data.get("extra"),
webhook=g.data.get("webhook"),
extra=data.extra,
webhook=data.webhook,
conn=conn,
)
except InvoiceFailure as e:
return jsonable_encoder({"message": str(e)}), 520
return {"message": str(e)}, 520
except Exception as exc:
raise exc
invoice = bolt11.decode(payment_request)
lnurl_response: Union[None, bool, str] = None
if g.data.get("lnurl_callback"):
if data.lnurl_callback:
if "lnurl_balance_check" in g.data:
save_balance_check(g.wallet.id, g.data["lnurl_balance_check"])
save_balance_check(g.wallet.id, data.lnurl_balance_check)
async with httpx.AsyncClient() as client:
try:
r = await client.get(
g.data["lnurl_callback"],
data.lnurl_callback,
params={
"pr": payment_request,
"balanceNotify": url_for(
"core.lnurl_balance_notify",
service=urlparse(g.data["lnurl_callback"]).netloc,
service=urlparse(data.lnurl_callback).netloc,
wal=g.wallet.id,
_external=True,
),
@ -167,15 +143,13 @@ async def api_payments_create_invoice(amount: int = Query(...), memo: Memo ):
lnurl_response = False
return (
jsonable_encoder(
{
"payment_hash": invoice.payment_hash,
"payment_request": payment_request,
# maintain backwards compatibility with API clients:
"checking_id": invoice.payment_hash,
"lnurl_response": lnurl_response,
}
),
},
HTTPStatus.CREATED,
)
@ -190,97 +164,76 @@ async def api_payments_pay_invoice(
payment_request=bolt11,
)
except ValueError as e:
return jsonable_encoder({"message": str(e)}), HTTPStatus.BAD_REQUEST
return {"message": str(e)}, HTTPStatus.BAD_REQUEST
except PermissionError as e:
return jsonable_encoder({"message": str(e)}), HTTPStatus.FORBIDDEN
return {"message": str(e)}, HTTPStatus.FORBIDDEN
except PaymentFailure as e:
return jsonable_encoder({"message": str(e)}), 520
return {"message": str(e)}, 520
except Exception as exc:
raise exc
return (
jsonable_encoder(
{
"payment_hash": payment_hash,
# maintain backwards compatibility with API clients:
"checking_id": payment_hash,
}
),
},
HTTPStatus.CREATED,
)
@core_app.route("/api/v1/payments", methods=["POST"])
@api_validate_post_request(schema={"out": {"type": "boolean", "required": True}})
async def api_payments_create():
if g.data["out"] is True:
@core_app.post("/api/v1/payments")
async def api_payments_create(out: bool = True):
if out is True:
return await api_payments_pay_invoice()
return await api_payments_create_invoice()
class CreateLNURLData(BaseModel):
description_hash: str
callback: str
amount: int
comment: Optional[str] = None
description: Optional[str] = None
@core_app.route("/api/v1/payments/lnurl", methods=["POST"])
@core_app.post("/api/v1/payments/lnurl")
@api_check_wallet_key("admin")
@api_validate_post_request(
schema={
"description_hash": {"type": "string", "empty": False, "required": True},
"callback": {"type": "string", "empty": False, "required": True},
"amount": {"type": "number", "empty": False, "required": True},
"comment": {
"type": "string",
"nullable": True,
"empty": True,
"required": False,
},
"description": {
"type": "string",
"nullable": True,
"empty": True,
"required": False,
},
}
)
async def api_payments_pay_lnurl():
domain = urlparse(g.data["callback"]).netloc
async def api_payments_pay_lnurl(data: CreateLNURLData):
domain = urlparse(data.callback).netloc
async with httpx.AsyncClient() as client:
try:
r = await client.get(
g.data["callback"],
params={"amount": g.data["amount"], "comment": g.data["comment"]},
data.callback,
params={"amount": data.amount, "comment": data.comment},
timeout=40,
)
if r.is_error:
raise httpx.ConnectError
except (httpx.ConnectError, httpx.RequestError):
return (
jsonify({"message": f"Failed to connect to {domain}."}),
{"message": f"Failed to connect to {domain}."},
HTTPStatus.BAD_REQUEST,
)
params = json.loads(r.text)
if params.get("status") == "ERROR":
return (
jsonify({"message": f"{domain} said: '{params.get('reason', '')}'"}),
return ({"message": f"{domain} said: '{params.get('reason', '')}'"},
HTTPStatus.BAD_REQUEST,
)
invoice = bolt11.decode(params["pr"])
if invoice.amount_msat != g.data["amount"]:
if invoice.amount_msat != data.amount:
return (
jsonify(
{
"message": f"{domain} returned an invalid invoice. Expected {g.data['amount']} msat, got {invoice.amount_msat}."
}
),
},
HTTPStatus.BAD_REQUEST,
)
if invoice.description_hash != g.data["description_hash"]:
return (
jsonify(
{
"message": f"{domain} returned an invalid invoice. Expected description_hash == {g.data['description_hash']}, got {invoice.description_hash}."
}
),
},
HTTPStatus.BAD_REQUEST,
)
@ -288,51 +241,49 @@ async def api_payments_pay_lnurl():
if params.get("successAction"):
extra["success_action"] = params["successAction"]
if g.data["comment"]:
extra["comment"] = g.data["comment"]
if data.comment:
extra["comment"] = data.comment
payment_hash = await pay_invoice(
wallet_id=g.wallet.id,
payment_request=params["pr"],
description=g.data.get("description", ""),
description=data.description,
extra=extra,
)
return (
jsonify(
{
"success_action": params.get("successAction"),
"payment_hash": payment_hash,
# maintain backwards compatibility with API clients:
"checking_id": payment_hash,
}
),
},
HTTPStatus.CREATED,
)
@core_app.route("/api/v1/payments/<payment_hash>", methods=["GET"])
@core_app.get("/api/v1/payments/<payment_hash>")
@api_check_wallet_key("invoice")
async def api_payment(payment_hash):
payment = await g.wallet.get_payment(payment_hash)
if not payment:
return jsonify({"message": "Payment does not exist."}), HTTPStatus.NOT_FOUND
return {"message": "Payment does not exist."}, HTTPStatus.NOT_FOUND
elif not payment.pending:
return jsonify({"paid": True, "preimage": payment.preimage}), HTTPStatus.OK
return {"paid": True, "preimage": payment.preimage}, HTTPStatus.OK
try:
await payment.check_pending()
except Exception:
return jsonify({"paid": False}), HTTPStatus.OK
return {"paid": False}, HTTPStatus.OK
return (
jsonify({"paid": not payment.pending, "preimage": payment.preimage}),
{"paid": not payment.pending, "preimage": payment.preimage},
HTTPStatus.OK,
)
@core_app.route("/api/v1/payments/sse", methods=["GET"])
@core_app.get("/api/v1/payments/sse")
@api_check_wallet_key("invoice", accept_querystring=True)
async def api_payments_sse():
this_wallet_id = g.wallet.id
@ -385,7 +336,7 @@ async def api_payments_sse():
return response
@core_app.route("/api/v1/lnurlscan/<code>", methods=["GET"])
@core_app.get("/api/v1/lnurlscan/<code>")
@api_check_wallet_key("invoice")
async def api_lnurlscan(code: str):
try:
@ -404,7 +355,7 @@ async def api_lnurlscan(code: str):
)
# will proceed with these values
else:
return jsonify({"message": "invalid lnurl"}), HTTPStatus.BAD_REQUEST
return {"message": "invalid lnurl"}, HTTPStatus.BAD_REQUEST
# params is what will be returned to the client
params: Dict = {"domain": domain}
@ -420,7 +371,7 @@ async def api_lnurlscan(code: str):
r = await client.get(url, timeout=5)
if r.is_error:
return (
jsonify({"domain": domain, "message": "failed to get parameters"}),
{"domain": domain, "message": "failed to get parameters"},
HTTPStatus.SERVICE_UNAVAILABLE,
)
@ -428,12 +379,10 @@ async def api_lnurlscan(code: str):
data = json.loads(r.text)
except json.decoder.JSONDecodeError:
return (
jsonify(
{
"domain": domain,
"message": f"got invalid response '{r.text[:200]}'",
}
),
},
HTTPStatus.SERVICE_UNAVAILABLE,
)
@ -441,9 +390,7 @@ async def api_lnurlscan(code: str):
tag = data["tag"]
if tag == "channelRequest":
return (
jsonify(
{"domain": domain, "kind": "channel", "message": "unsupported"}
),
{"domain": domain, "kind": "channel", "message": "unsupported"},
HTTPStatus.BAD_REQUEST,
)
@ -490,32 +437,24 @@ async def api_lnurlscan(code: str):
params.update(commentAllowed=data.get("commentAllowed", 0))
except KeyError as exc:
return (
jsonify(
{
"domain": domain,
"message": f"lnurl JSON response invalid: {exc}",
}
),
},
HTTPStatus.SERVICE_UNAVAILABLE,
)
return jsonify(params)
return params
@core_app.route("/api/v1/lnurlauth", methods=["POST"])
@core_app.post("/api/v1/lnurlauth", methods=["POST"])
@api_check_wallet_key("admin")
@api_validate_post_request(
schema={
"callback": {"type": "string", "required": True},
}
)
async def api_perform_lnurlauth():
err = await perform_lnurlauth(g.data["callback"])
async def api_perform_lnurlauth(callback: str):
err = await perform_lnurlauth(callback)
if err:
return jsonify({"reason": err.reason}), HTTPStatus.SERVICE_UNAVAILABLE
return {"reason": err.reason}, HTTPStatus.SERVICE_UNAVAILABLE
return "", HTTPStatus.OK
@core_app.route("/api/v1/currencies", methods=["GET"])
async def api_list_currencies_available():
return jsonify(list(currencies.keys()))
return list(currencies.keys())

View file

@ -4,7 +4,6 @@ from quart import (
g,
current_app,
abort,
jsonify,
request,
redirect,
render_template,
@ -28,21 +27,21 @@ from ..crud import (
from ..services import redeem_lnurl_withdraw, pay_invoice
@core_app.route("/favicon.ico")
@core_app.get("/favicon.ico")
async def favicon():
return await send_from_directory(
path.join(core_app.root_path, "static"), "favicon.ico"
)
@core_app.route("/")
@core_app.get("/")
async def home():
return await render_template(
"core/index.html", lnurl=request.args.get("lightning", None)
)
@core_app.route("/extensions")
@core_app.get("/extensions")
@validate_uuids(["usr"], required=True)
@check_user_exists()
async def extensions():
@ -66,7 +65,7 @@ async def extensions():
return await render_template("core/extensions.html", user=await get_user(g.user.id))
@core_app.route("/wallet")
@core_app.get("/wallet")
@validate_uuids(["usr", "wal"])
async def wallet():
user_id = request.args.get("usr", type=str)
@ -108,19 +107,18 @@ async def wallet():
)
@core_app.route("/withdraw")
@core_app.get("/withdraw")
@validate_uuids(["usr", "wal"], required=True)
async def lnurl_full_withdraw():
user = await get_user(request.args.get("usr"))
if not user:
return jsonify({"status": "ERROR", "reason": "User does not exist."})
return {"status": "ERROR", "reason": "User does not exist."}
wallet = user.get_wallet(request.args.get("wal"))
if not wallet:
return jsonify({"status": "ERROR", "reason": "Wallet does not exist."})
return{"status": "ERROR", "reason": "Wallet does not exist."}
return jsonify(
{
return {
"tag": "withdrawRequest",
"callback": url_for(
"core.lnurl_full_withdraw_callback",
@ -136,19 +134,18 @@ async def lnurl_full_withdraw():
"core.lnurl_full_withdraw", usr=user.id, wal=wallet.id, _external=True
),
}
)
@core_app.route("/withdraw/cb")
@core_app.get("/withdraw/cb")
@validate_uuids(["usr", "wal"], required=True)
async def lnurl_full_withdraw_callback():
user = await get_user(request.args.get("usr"))
if not user:
return jsonify({"status": "ERROR", "reason": "User does not exist."})
return {"status": "ERROR", "reason": "User does not exist."}
wallet = user.get_wallet(request.args.get("wal"))
if not wallet:
return jsonify({"status": "ERROR", "reason": "Wallet does not exist."})
return {"status": "ERROR", "reason": "Wallet does not exist."}
pr = request.args.get("pr")
@ -164,10 +161,10 @@ async def lnurl_full_withdraw_callback():
if balance_notify:
await save_balance_notify(wallet.id, balance_notify)
return jsonify({"status": "OK"})
return {"status": "OK"}
@core_app.route("/deletewallet")
@core_app.get("/deletewallet")
@validate_uuids(["usr", "wal"], required=True)
@check_user_exists()
async def deletewallet():
@ -186,7 +183,7 @@ async def deletewallet():
return redirect(url_for("core.home"))
@core_app.route("/withdraw/notify/<service>")
@core_app.get("/withdraw/notify/<service>")
@validate_uuids(["wal"], required=True)
async def lnurl_balance_notify(service: str):
bc = await get_balance_check(request.args.get("wal"), service)
@ -194,7 +191,7 @@ async def lnurl_balance_notify(service: str):
redeem_lnurl_withdraw(bc.wallet, bc.url)
@core_app.route("/lnurlwallet")
@core_app.get("/lnurlwallet")
async def lnurlwallet():
async with db.connect() as conn:
account = await create_account(conn=conn)
@ -213,14 +210,13 @@ async def lnurlwallet():
return redirect(url_for("core.wallet", usr=user.id, wal=wallet.id))
@core_app.route("/manifest/<usr>.webmanifest")
@core_app.get("/manifest/<usr>.webmanifest")
async def manifest(usr: str):
user = await get_user(usr)
if not user:
return "", HTTPStatus.NOT_FOUND
return jsonify(
{
return {
"short_name": "LNbits",
"name": "LNbits Wallet",
"icons": [
@ -244,6 +240,4 @@ async def manifest(usr: str):
"url": "/wallet?usr=" + usr + "&wal=" + wallet.id,
}
for wallet in user.wallets
],
}
)
],}

View file

@ -10,22 +10,22 @@ from ..crud import get_standalone_payment
from ..tasks import api_invoice_listeners
@core_app.route("/public/v1/payment/<payment_hash>", methods=["GET"])
@core_app.get("/public/v1/payment/<payment_hash>")
async def api_public_payment_longpolling(payment_hash):
payment = await get_standalone_payment(payment_hash)
if not payment:
return jsonify({"message": "Payment does not exist."}), HTTPStatus.NOT_FOUND
return {"message": "Payment does not exist."}, HTTPStatus.NOT_FOUND
elif not payment.pending:
return jsonify({"status": "paid"}), HTTPStatus.OK
return {"status": "paid"}, HTTPStatus.OK
try:
invoice = bolt11.decode(payment.bolt11)
expiration = datetime.datetime.fromtimestamp(invoice.date + invoice.expiry)
if expiration < datetime.datetime.now():
return jsonify({"status": "expired"}), HTTPStatus.OK
return {"status": "expired"}, HTTPStatus.OK
except:
return jsonify({"message": "Invalid bolt11 invoice."}), HTTPStatus.BAD_REQUEST
return {"message": "Invalid bolt11 invoice."}, HTTPStatus.BAD_REQUEST
send_payment, receive_payment = trio.open_memory_channel(0)
@ -38,7 +38,7 @@ async def api_public_payment_longpolling(payment_hash):
async for payment in receive_payment:
if payment.payment_hash == payment_hash:
nonlocal response
response = (jsonify({"status": "paid"}), HTTPStatus.OK)
response = ({"status": "paid"}, HTTPStatus.OK)
cancel_scope.cancel()
async def timeouter(cancel_scope):
@ -52,4 +52,4 @@ async def api_public_payment_longpolling(payment_hash):
if response:
return response
else:
return jsonify({"message": "timeout"}), HTTPStatus.REQUEST_TIMEOUT
return {"message": "timeout"}, HTTPStatus.REQUEST_TIMEOUT

View file

@ -4,6 +4,9 @@ from http import HTTPStatus
from lnbits.core.crud import get_user, get_wallet
from lnbits.core.services import create_invoice, check_invoice_status
from lnbits.decorators import api_check_wallet_key, api_validate_post_request
from fastapi.encoders import jsonable_encoder
from fastapi import Query
from pydantic import BaseModel
from . import events_ext
from .crud import (
@ -25,7 +28,8 @@ from .crud import (
# Events
@events_ext.route("/api/v1/events", methods=["GET"])
@events_ext.get("/api/v1/events")
@api_check_wallet_key("invoice")
async def api_events():
wallet_ids = [g.wallet.id]
@ -34,51 +38,48 @@ async def api_events():
wallet_ids = (await get_user(g.wallet.user)).wallet_ids
return (
jsonify([event._asdict() for event in await get_events(wallet_ids)]),
[event._asdict() for event in await get_events(wallet_ids)],
HTTPStatus.OK,
)
class CreateData(BaseModel):
wallet: str = Query(...)
name: str = Query(...)
info: str = Query(...)
closing_date: str = Query(...)
event_start_date: str = Query(...)
event_end_date: str = Query(...)
amount_tickets: int = Query(..., ge=0)
price_per_ticket: int = Query(..., ge=0)
@events_ext.route("/api/v1/events", methods=["POST"])
@events_ext.route("/api/v1/events/<event_id>", methods=["PUT"])
@events_ext.post("/api/v1/events")
@events_ext.put("/api/v1/events/<event_id>")
@api_check_wallet_key("invoice")
@api_validate_post_request(
schema={
"wallet": {"type": "string", "empty": False, "required": True},
"name": {"type": "string", "empty": False, "required": True},
"info": {"type": "string", "min": 0, "required": True},
"closing_date": {"type": "string", "empty": False, "required": True},
"event_start_date": {"type": "string", "empty": False, "required": True},
"event_end_date": {"type": "string", "empty": False, "required": True},
"amount_tickets": {"type": "integer", "min": 0, "required": True},
"price_per_ticket": {"type": "integer", "min": 0, "required": True},
}
)
async def api_event_create(event_id=None):
async def api_event_create(data: CreateData, event_id=None):
if event_id:
event = await get_event(event_id)
if not event:
return jsonify({"message": "Form does not exist."}), HTTPStatus.NOT_FOUND
return jsonable_encoder({"message": "Form does not exist."}), HTTPStatus.NOT_FOUND
if event.wallet != g.wallet.id:
return jsonify({"message": "Not your event."}), HTTPStatus.FORBIDDEN
return jsonable_encoder({"message": "Not your event."}), HTTPStatus.FORBIDDEN
event = await update_event(event_id, **g.data)
event = await update_event(event_id, **data)
else:
event = await create_event(**g.data)
event = await create_event(**data)
return jsonify(event._asdict()), HTTPStatus.CREATED
return event._asdict(), HTTPStatus.CREATED
@events_ext.route("/api/v1/events/<event_id>", methods=["DELETE"])
@events_ext.delete("/api/v1/events/<event_id>")
@api_check_wallet_key("invoice")
async def api_form_delete(event_id):
event = await get_event(event_id)
if not event:
return jsonify({"message": "Event does not exist."}), HTTPStatus.NOT_FOUND
return jsonable_encoder({"message": "Event does not exist."}), HTTPStatus.NOT_FOUND
if event.wallet != g.wallet.id:
return jsonify({"message": "Not your event."}), HTTPStatus.FORBIDDEN
return jsonable_encoder({"message": "Not your event."}), HTTPStatus.FORBIDDEN
await delete_event(event_id)
return "", HTTPStatus.NO_CONTENT
@ -87,7 +88,7 @@ async def api_form_delete(event_id):
#########Tickets##########
@events_ext.route("/api/v1/tickets", methods=["GET"])
@events_ext.get("/api/v1/tickets")
@api_check_wallet_key("invoice")
async def api_tickets():
wallet_ids = [g.wallet.id]
@ -96,22 +97,19 @@ async def api_tickets():
wallet_ids = (await get_user(g.wallet.user)).wallet_ids
return (
jsonify([ticket._asdict() for ticket in await get_tickets(wallet_ids)]),
[ticket._asdict() for ticket in await get_tickets(wallet_ids)],
HTTPStatus.OK,
)
class CreateTicketData(BaseModel):
name: str = Query(...)
email: str
@events_ext.route("/api/v1/tickets/<event_id>/<sats>", methods=["POST"])
@api_validate_post_request(
schema={
"name": {"type": "string", "empty": False, "required": True},
"email": {"type": "string", "empty": False, "required": True},
}
)
async def api_ticket_make_ticket(event_id, sats):
@events_ext.post("/api/v1/tickets/<event_id>/<sats>")
async def api_ticket_make_ticket(data: CreateTicketData, event_id, sats):
event = await get_event(event_id)
if not event:
return jsonify({"message": "Event does not exist."}), HTTPStatus.NOT_FOUND
return jsonable_encoder({"message": "Event does not exist."}), HTTPStatus.NOT_FOUND
try:
payment_hash, payment_request = await create_invoice(
wallet_id=event.wallet,
@ -123,19 +121,19 @@ async def api_ticket_make_ticket(event_id, sats):
return jsonify({"message": str(e)}), HTTPStatus.INTERNAL_SERVER_ERROR
ticket = await create_ticket(
payment_hash=payment_hash, wallet=event.wallet, event=event_id, **g.data
payment_hash=payment_hash, wallet=event.wallet, event=event_id, **data
)
if not ticket:
return jsonify({"message": "Event could not be fetched."}), HTTPStatus.NOT_FOUND
return jsonable_encoder({"message": "Event could not be fetched."}), HTTPStatus.NOT_FOUND
return (
jsonify({"payment_hash": payment_hash, "payment_request": payment_request}),
jsonable_encoder({"payment_hash": payment_hash, "payment_request": payment_request}),
HTTPStatus.OK,
)
@events_ext.route("/api/v1/tickets/<payment_hash>", methods=["GET"])
@events_ext.get("/api/v1/tickets/<payment_hash>")
async def api_ticket_send_ticket(payment_hash):
ticket = await get_ticket(payment_hash)
@ -143,7 +141,7 @@ async def api_ticket_send_ticket(payment_hash):
status = await check_invoice_status(ticket.wallet, payment_hash)
is_paid = not status.pending
except Exception:
return jsonify({"message": "Not paid."}), HTTPStatus.NOT_FOUND
return jsonable_encoder({"message": "Not paid."}), HTTPStatus.NOT_FOUND
if is_paid:
wallet = await get_wallet(ticket.wallet)
@ -151,21 +149,21 @@ async def api_ticket_send_ticket(payment_hash):
await payment.set_pending(False)
ticket = await set_ticket_paid(payment_hash=payment_hash)
return jsonify({"paid": True, "ticket_id": ticket.id}), HTTPStatus.OK
return jsonable_encoder({"paid": True, "ticket_id": ticket.id}), HTTPStatus.OK
return jsonify({"paid": False}), HTTPStatus.OK
return jsonable_encoder({"paid": False}), HTTPStatus.OK
@events_ext.route("/api/v1/tickets/<ticket_id>", methods=["DELETE"])
@events_ext.delete("/api/v1/tickets/<ticket_id>")
@api_check_wallet_key("invoice")
async def api_ticket_delete(ticket_id):
ticket = await get_ticket(ticket_id)
if not ticket:
return jsonify({"message": "Ticket does not exist."}), HTTPStatus.NOT_FOUND
return jsonable_encoder({"message": "Ticket does not exist."}), HTTPStatus.NOT_FOUND
if ticket.wallet != g.wallet.id:
return jsonify({"message": "Not your ticket."}), HTTPStatus.FORBIDDEN
return jsonable_encoder({"message": "Not your ticket."}), HTTPStatus.FORBIDDEN
await delete_ticket(ticket_id)
return "", HTTPStatus.NO_CONTENT
@ -174,34 +172,26 @@ async def api_ticket_delete(ticket_id):
# Event Tickets
@events_ext.route("/api/v1/eventtickets/<wallet_id>/<event_id>", methods=["GET"])
@events_ext.get("/api/v1/eventtickets/<wallet_id>/<event_id>")
async def api_event_tickets(wallet_id, event_id):
return (
jsonify(
[
ticket._asdict()
for ticket in await get_event_tickets(
wallet_id=wallet_id, event_id=event_id
)
]
),
return ([ticket._asdict() for ticket in await get_event_tickets(wallet_id=wallet_id, event_id=event_id)],
HTTPStatus.OK,
)
@events_ext.route("/api/v1/register/ticket/<ticket_id>", methods=["GET"])
@events_ext.get("/api/v1/register/ticket/<ticket_id>")
async def api_event_register_ticket(ticket_id):
ticket = await get_ticket(ticket_id)
if not ticket:
return jsonify({"message": "Ticket does not exist."}), HTTPStatus.FORBIDDEN
return jsonable_encoder({"message": "Ticket does not exist."}), HTTPStatus.FORBIDDEN
if not ticket.paid:
return jsonify({"message": "Ticket not paid for."}), HTTPStatus.FORBIDDEN
return jsonable_encoder({"message": "Ticket not paid for."}), HTTPStatus.FORBIDDEN
if ticket.registered == True:
return jsonify({"message": "Ticket already registered"}), HTTPStatus.FORBIDDEN
return jsonable_encoder({"message": "Ticket already registered"}), HTTPStatus.FORBIDDEN
return (
jsonify([ticket._asdict() for ticket in await reg_ticket(ticket_id)]),
[ticket._asdict() for ticket in await reg_ticket(ticket_id)],
HTTPStatus.OK,
)

View file

@ -12,14 +12,14 @@ from .crud import get_jukebox
from .views_api import api_get_jukebox_device_check
@jukebox_ext.route("/")
@jukebox_ext.get("/")
@validate_uuids(["usr"], required=True)
@check_user_exists()
async def index():
return await render_template("jukebox/index.html", user=g.user)
@jukebox_ext.route("/<juke_id>")
@jukebox_ext.get("/<juke_id>")
async def connect_to_jukebox(juke_id):
jukebox = await get_jukebox(juke_id)
if not jukebox:

View file

@ -4,6 +4,9 @@ import base64
from lnbits.core.crud import get_wallet
from lnbits.core.services import create_invoice, check_invoice_status
import json
from typing import Optional
from pydantic import BaseModel
from lnbits.decorators import api_check_wallet_key, api_validate_post_request
import httpx
@ -19,16 +22,15 @@ from .crud import (
update_jukebox_payment,
)
from lnbits.core.services import create_invoice, check_invoice_status
from fastapi.encoders import jsonable_encoder
@jukebox_ext.route("/api/v1/jukebox", methods=["GET"])
@jukebox_ext.get("/api/v1/jukebox")
@api_check_wallet_key("admin")
async def api_get_jukeboxs():
try:
return (
jsonify(
[{**jukebox._asdict()} for jukebox in await get_jukeboxs(g.wallet.user)]
),
[{**jukebox._asdict()} for jukebox in await get_jukeboxs(g.wallet.user)],
HTTPStatus.OK,
)
except:
@ -38,7 +40,7 @@ async def api_get_jukeboxs():
##################SPOTIFY AUTH#####################
@jukebox_ext.route("/api/v1/jukebox/spotify/cb/<juke_id>", methods=["GET"])
@jukebox_ext.get("/api/v1/jukebox/spotify/cb/<juke_id>")
async def api_check_credentials_callbac(juke_id):
sp_code = ""
sp_access_token = ""
@ -47,7 +49,7 @@ async def api_check_credentials_callbac(juke_id):
jukebox = await get_jukebox(juke_id)
except:
return (
jsonify({"error": "No Jukebox"}),
{"error": "No Jukebox"},
HTTPStatus.FORBIDDEN,
)
if request.args.get("code"):
@ -67,48 +69,45 @@ async def api_check_credentials_callbac(juke_id):
return "<h1>Success!</h1><h2>You can close this window</h2>"
@jukebox_ext.route("/api/v1/jukebox/<juke_id>", methods=["GET"])
@jukebox_ext.get("/api/v1/jukebox/<juke_id>")
@api_check_wallet_key("admin")
async def api_check_credentials_check(juke_id):
jukebox = await get_jukebox(juke_id)
return jsonify(jukebox._asdict()), HTTPStatus.CREATED
@jukebox_ext.route("/api/v1/jukebox/", methods=["POST"])
@jukebox_ext.route("/api/v1/jukebox/<juke_id>", methods=["PUT"])
class CreateData(BaseModel):
user: str = None
title: str = None
wallet: str = None
sp_user: str = None
sp_secret: str = None
sp_access_token: Optional[str] = None
sp_refresh_token: Optional[str] = None
sp_device: Optional[str] = None
sp_playlists: Optional[str] = None
price: Optional[str] = None
@jukebox_ext.post("/api/v1/jukebox/")
@jukebox_ext.put("/api/v1/jukebox/<juke_id>")
@api_check_wallet_key("admin")
@api_validate_post_request(
schema={
"user": {"type": "string", "empty": False, "required": True},
"title": {"type": "string", "empty": False, "required": True},
"wallet": {"type": "string", "empty": False, "required": True},
"sp_user": {"type": "string", "empty": False, "required": True},
"sp_secret": {"type": "string", "required": True},
"sp_access_token": {"type": "string", "required": False},
"sp_refresh_token": {"type": "string", "required": False},
"sp_device": {"type": "string", "required": False},
"sp_playlists": {"type": "string", "required": False},
"price": {"type": "string", "required": False},
}
)
async def api_create_update_jukebox(juke_id=None):
async def api_create_update_jukebox(data: CreateData, juke_id=None):
if juke_id:
jukebox = await update_jukebox(juke_id=juke_id, inkey=g.wallet.inkey, **g.data)
jukebox = await update_jukebox(juke_id=juke_id, inkey=g.wallet.inkey, **data)
else:
jukebox = await create_jukebox(inkey=g.wallet.inkey, **g.data)
jukebox = await create_jukebox(inkey=g.wallet.inkey, **data)
return jsonify(jukebox._asdict()), HTTPStatus.CREATED
return jukebox._asdict(), HTTPStatus.CREATED
@jukebox_ext.route("/api/v1/jukebox/<juke_id>", methods=["DELETE"])
@jukebox_ext.delete("/api/v1/jukebox/<juke_id>")
@api_check_wallet_key("admin")
async def api_delete_item(juke_id):
await delete_jukebox(juke_id)
try:
return (
jsonify(
[{**jukebox._asdict()} for jukebox in await get_jukeboxs(g.wallet.user)]
),
,
HTTPStatus.OK,
)
except:
@ -120,15 +119,15 @@ async def api_delete_item(juke_id):
######GET ACCESS TOKEN######
@jukebox_ext.route(
"/api/v1/jukebox/jb/playlist/<juke_id>/<sp_playlist>", methods=["GET"]
@jukebox_ext.get(
"/api/v1/jukebox/jb/playlist/<juke_id>/<sp_playlist>"
)
async def api_get_jukebox_song(juke_id, sp_playlist, retry=False):
try:
jukebox = await get_jukebox(juke_id)
except:
return (
jsonify({"error": "No Jukebox"}),
jsonable_encoder({"error": "No Jukebox"}),
HTTPStatus.FORBIDDEN,
)
tracks = []
@ -146,7 +145,7 @@ async def api_get_jukebox_song(juke_id, sp_playlist, retry=False):
return False
elif retry:
return (
jsonify({"error": "Failed to get auth"}),
jsonable_encoder({"error": "Failed to get auth"}),
HTTPStatus.FORBIDDEN,
)
else:
@ -166,7 +165,7 @@ async def api_get_jukebox_song(juke_id, sp_playlist, retry=False):
)
except AssertionError:
something = None
return jsonify([track for track in tracks])
return [track for track in tracks]
async def api_get_token(juke_id):
@ -174,7 +173,7 @@ async def api_get_token(juke_id):
jukebox = await get_jukebox(juke_id)
except:
return (
jsonify({"error": "No Jukebox"}),
jsonable_encoder({"error": "No Jukebox"}),
HTTPStatus.FORBIDDEN,
)
@ -211,13 +210,13 @@ async def api_get_token(juke_id):
######CHECK DEVICE
@jukebox_ext.route("/api/v1/jukebox/jb/<juke_id>", methods=["GET"])
@jukebox_ext.get("/api/v1/jukebox/jb/<juke_id>")
async def api_get_jukebox_device_check(juke_id, retry=False):
try:
jukebox = await get_jukebox(juke_id)
except:
return (
jsonify({"error": "No Jukebox"}),
{"error": "No Jukebox"},
HTTPStatus.FORBIDDEN,
)
async with httpx.AsyncClient() as client:
@ -236,19 +235,19 @@ async def api_get_jukebox_device_check(juke_id, retry=False):
token = await api_get_token(juke_id)
if token == False:
return (
jsonify({"error": "No device connected"}),
jsonable_encoder({"error": "No device connected"}),
HTTPStatus.FORBIDDEN,
)
elif retry:
return (
jsonify({"error": "Failed to get auth"}),
jsonable_encoder({"error": "Failed to get auth"}),
HTTPStatus.FORBIDDEN,
)
else:
return api_get_jukebox_device_check(juke_id, retry=True)
else:
return (
jsonify({"error": "No device connected"}),
jsonable_encoder({"error": "No device connected"}),
HTTPStatus.FORBIDDEN,
)
@ -256,13 +255,13 @@ async def api_get_jukebox_device_check(juke_id, retry=False):
######GET INVOICE STUFF
@jukebox_ext.route("/api/v1/jukebox/jb/invoice/<juke_id>/<song_id>", methods=["GET"])
@jukebox_ext.get("/api/v1/jukebox/jb/invoice/<juke_id>/<song_id>")
async def api_get_jukebox_invoice(juke_id, song_id):
try:
jukebox = await get_jukebox(juke_id)
except:
return (
jsonify({"error": "No Jukebox"}),
{"error": "No Jukebox"},
HTTPStatus.FORBIDDEN,
)
try:
@ -274,12 +273,12 @@ async def api_get_jukebox_invoice(juke_id, song_id):
deviceConnected = True
if not deviceConnected:
return (
jsonify({"error": "No device connected"}),
{"error": "No device connected"},
HTTPStatus.NOT_FOUND,
)
except:
return (
jsonify({"error": "No device connected"}),
{"error": "No device connected"},
HTTPStatus.NOT_FOUND,
)
@ -292,36 +291,36 @@ async def api_get_jukebox_invoice(juke_id, song_id):
jukebox_payment = await create_jukebox_payment(song_id, invoice[0], juke_id)
return jsonify(invoice, jukebox_payment)
return invoice, jukebox_payment
@jukebox_ext.route(
"/api/v1/jukebox/jb/checkinvoice/<pay_hash>/<juke_id>", methods=["GET"]
@jukebox_ext.get(
"/api/v1/jukebox/jb/checkinvoice/<pay_hash>/<juke_id>"
)
async def api_get_jukebox_invoice_check(pay_hash, juke_id):
try:
jukebox = await get_jukebox(juke_id)
except:
return (
jsonify({"error": "No Jukebox"}),
{"error": "No Jukebox"},
HTTPStatus.FORBIDDEN,
)
try:
status = await check_invoice_status(jukebox.wallet, pay_hash)
is_paid = not status.pending
except Exception as exc:
return jsonify({"paid": False}), HTTPStatus.OK
return {"paid": False}, HTTPStatus.OK
if is_paid:
wallet = await get_wallet(jukebox.wallet)
payment = await wallet.get_payment(pay_hash)
await payment.set_pending(False)
await update_jukebox_payment(pay_hash, paid=True)
return jsonify({"paid": True}), HTTPStatus.OK
return jsonify({"paid": False}), HTTPStatus.OK
return {"paid": True}, HTTPStatus.OK
return {"paid": False}, HTTPStatus.OK
@jukebox_ext.route(
"/api/v1/jukebox/jb/invoicep/<song_id>/<juke_id>/<pay_hash>", methods=["GET"]
@jukebox_ext.get(
"/api/v1/jukebox/jb/invoicep/<song_id>/<juke_id>/<pay_hash>"
)
async def api_get_jukebox_invoice_paid(song_id, juke_id, pay_hash, retry=False):
try:
@ -360,17 +359,17 @@ async def api_get_jukebox_invoice_paid(song_id, juke_id, pay_hash, retry=False):
headers={"Authorization": "Bearer " + jukebox.sp_access_token},
)
if r.status_code == 204:
return jsonify(jukebox_payment), HTTPStatus.OK
return jukebox_payment, HTTPStatus.OK
elif r.status_code == 401 or r.status_code == 403:
token = await api_get_token(juke_id)
if token == False:
return (
jsonify({"error": "Invoice not paid"}),
{"error": "Invoice not paid"},
HTTPStatus.FORBIDDEN,
)
elif retry:
return (
jsonify({"error": "Failed to get auth"}),
{"error": "Failed to get auth"},
HTTPStatus.FORBIDDEN,
)
else:
@ -379,7 +378,7 @@ async def api_get_jukebox_invoice_paid(song_id, juke_id, pay_hash, retry=False):
)
else:
return (
jsonify({"error": "Invoice not paid"}),
{"error": "Invoice not paid"},
HTTPStatus.FORBIDDEN,
)
elif r.status_code == 200:
@ -393,18 +392,18 @@ async def api_get_jukebox_invoice_paid(song_id, juke_id, pay_hash, retry=False):
headers={"Authorization": "Bearer " + jukebox.sp_access_token},
)
if r.status_code == 204:
return jsonify(jukebox_payment), HTTPStatus.OK
return jukebox_payment, HTTPStatus.OK
elif r.status_code == 401 or r.status_code == 403:
token = await api_get_token(juke_id)
if token == False:
return (
jsonify({"error": "Invoice not paid"}),
{"error": "Invoice not paid"},
HTTPStatus.OK,
)
elif retry:
return (
jsonify({"error": "Failed to get auth"}),
{"error": "Failed to get auth"},
HTTPStatus.FORBIDDEN,
)
else:
@ -413,38 +412,38 @@ async def api_get_jukebox_invoice_paid(song_id, juke_id, pay_hash, retry=False):
)
else:
return (
jsonify({"error": "Invoice not paid"}),
{"error": "Invoice not paid"},
HTTPStatus.OK,
)
elif r.status_code == 401 or r.status_code == 403:
token = await api_get_token(juke_id)
if token == False:
return (
jsonify({"error": "Invoice not paid"}),
{"error": "Invoice not paid"},
HTTPStatus.OK,
)
elif retry:
return (
jsonify({"error": "Failed to get auth"}),
{"error": "Failed to get auth"},
HTTPStatus.FORBIDDEN,
)
else:
return await api_get_jukebox_invoice_paid(
song_id, juke_id, pay_hash
)
return jsonify({"error": "Invoice not paid"}), HTTPStatus.OK
return {"error": "Invoice not paid"}, HTTPStatus.OK
############################GET TRACKS
@jukebox_ext.route("/api/v1/jukebox/jb/currently/<juke_id>", methods=["GET"])
@jukebox_ext.get("/api/v1/jukebox/jb/currently/<juke_id>")
async def api_get_jukebox_currently(juke_id, retry=False):
try:
jukebox = await get_jukebox(juke_id)
except:
return (
jsonify({"error": "No Jukebox"}),
{"error": "No Jukebox"},
HTTPStatus.FORBIDDEN,
)
async with httpx.AsyncClient() as client:
@ -455,7 +454,7 @@ async def api_get_jukebox_currently(juke_id, retry=False):
headers={"Authorization": "Bearer " + jukebox.sp_access_token},
)
if r.status_code == 204:
return jsonify({"error": "Nothing"}), HTTPStatus.OK
return {"error": "Nothing"}, HTTPStatus.OK
elif r.status_code == 200:
try:
response = r.json()
@ -467,25 +466,25 @@ async def api_get_jukebox_currently(juke_id, retry=False):
"artist": response["item"]["artists"][0]["name"],
"image": response["item"]["album"]["images"][0]["url"],
}
return jsonify(track), HTTPStatus.OK
return track, HTTPStatus.OK
except:
return jsonify("Something went wrong"), HTTPStatus.NOT_FOUND
return "Something went wrong", HTTPStatus.NOT_FOUND
elif r.status_code == 401:
token = await api_get_token(juke_id)
if token == False:
return (
jsonify({"error": "Invoice not paid"}),
{"error": "Invoice not paid"},
HTTPStatus.FORBIDDEN,
)
elif retry:
return (
jsonify({"error": "Failed to get auth"}),
{"error": "Failed to get auth"},
HTTPStatus.FORBIDDEN,
)
else:
return await api_get_jukebox_currently(juke_id, retry=True)
else:
return jsonify("Something went wrong"), HTTPStatus.NOT_FOUND
return "Something went wrong", HTTPStatus.NOT_FOUND
except AssertionError:
return jsonify("Something went wrong"), HTTPStatus.NOT_FOUND
return "Something went wrong", HTTPStatus.NOT_FOUND

View file

@ -10,8 +10,9 @@ from .crud import create_paywall, get_paywall, get_paywalls, delete_paywall
from typing import Optional
from pydantic import BaseModel
from fastapi import FastAPI, Query
from fastapi.encoders import jsonable_encoder
@paywall_ext.route("/api/v1/paywalls", methods=["GET"])
@paywall_ext.get("/api/v1/paywalls")
@api_check_wallet_key("invoice")
async def api_paywalls():
wallet_ids = [g.wallet.id]
@ -20,57 +21,54 @@ async def api_paywalls():
wallet_ids = (await get_user(g.wallet.user)).wallet_ids
return (
jsonify([paywall._asdict() for paywall in await get_paywalls(wallet_ids)]),
jsonable_encoder([paywall._asdict() for paywall in await get_paywalls(wallet_ids)]),
HTTPStatus.OK,
)
class CreateData(BaseModel):
url: Optional[str] = Query(...),
memo: Optional[str] = Query(...),
description: str = Query(None),
amount: int = Query(None),
url: Optional[str] = Query(...)
memo: Optional[str] = Query(...)
description: str = Query(None)
amount: int = Query(None)
remembers: bool = Query(None)
@paywall_ext.route("/api/v1/paywalls", methods=["POST"])
@paywall_ext.post("/api/v1/paywalls")
@api_check_wallet_key("invoice")
async def api_paywall_create(data: CreateData):
paywall = await create_paywall(wallet_id=g.wallet.id, **data)
return paywall, HTTPStatus.CREATED
@paywall_ext.route("/api/v1/paywalls/<paywall_id>", methods=["DELETE"])
@paywall_ext.delete("/api/v1/paywalls/<paywall_id>")
@api_check_wallet_key("invoice")
async def api_paywall_delete(paywall_id):
paywall = await get_paywall(paywall_id)
if not paywall:
return jsonify({"message": "Paywall does not exist."}), HTTPStatus.NOT_FOUND
return jsonable_encoder({"message": "Paywall does not exist."}), HTTPStatus.NOT_FOUND
if paywall.wallet != g.wallet.id:
return jsonify({"message": "Not your paywall."}), HTTPStatus.FORBIDDEN
return jsonable_encoder({"message": "Not your paywall."}), HTTPStatus.FORBIDDEN
await delete_paywall(paywall_id)
return "", HTTPStatus.NO_CONTENT
@paywall_ext.route("/api/v1/paywalls/<paywall_id>/invoice", methods=["POST"])
@api_validate_post_request(
schema={"amount": {"type": "integer", "min": 1, "required": True}}
)
async def api_paywall_create_invoice(paywall_id):
@paywall_ext.post("/api/v1/paywalls/<paywall_id>/invoice")
async def api_paywall_create_invoice(amount: int = Query(..., ge=1), paywall_id = None):
paywall = await get_paywall(paywall_id)
if g.data["amount"] < paywall.amount:
if amount < paywall.amount:
return (
jsonify({"message": f"Minimum amount is {paywall.amount} sat."}),
jsonable_encoder({"message": f"Minimum amount is {paywall.amount} sat."}),
HTTPStatus.BAD_REQUEST,
)
try:
amount = (
g.data["amount"] if g.data["amount"] > paywall.amount else paywall.amount
amount if amount > paywall.amount else paywall.amount
)
payment_hash, payment_request = await create_invoice(
wallet_id=paywall.wallet,
@ -79,38 +77,35 @@ async def api_paywall_create_invoice(paywall_id):
extra={"tag": "paywall"},
)
except Exception as e:
return jsonify({"message": str(e)}), HTTPStatus.INTERNAL_SERVER_ERROR
return jsonable_encoder({"message": str(e)}), HTTPStatus.INTERNAL_SERVER_ERROR
return (
jsonify({"payment_hash": payment_hash, "payment_request": payment_request}),
jsonable_encoder({"payment_hash": payment_hash, "payment_request": payment_request}),
HTTPStatus.CREATED,
)
@paywall_ext.route("/api/v1/paywalls/<paywall_id>/check_invoice", methods=["POST"])
@api_validate_post_request(
schema={"payment_hash": {"type": "string", "empty": False, "required": True}}
)
async def api_paywal_check_invoice(paywall_id):
@paywall_ext.post("/api/v1/paywalls/<paywall_id>/check_invoice")
async def api_paywal_check_invoice(payment_hash: str = Query(...), paywall_id = None):
paywall = await get_paywall(paywall_id)
if not paywall:
return jsonify({"message": "Paywall does not exist."}), HTTPStatus.NOT_FOUND
return jsonable_encoder({"message": "Paywall does not exist."}), HTTPStatus.NOT_FOUND
try:
status = await check_invoice_status(paywall.wallet, g.data["payment_hash"])
status = await check_invoice_status(paywall.wallet, payment_hash)
is_paid = not status.pending
except Exception:
return jsonify({"paid": False}), HTTPStatus.OK
return jsonable_encoder({"paid": False}), HTTPStatus.OK
if is_paid:
wallet = await get_wallet(paywall.wallet)
payment = await wallet.get_payment(g.data["payment_hash"])
payment = await wallet.get_payment(payment_hash)
await payment.set_pending(False)
return (
jsonify({"paid": True, "url": paywall.url, "remembers": paywall.remembers}),
jsonable_encoder({"paid": True, "url": paywall.url, "remembers": paywall.remembers}),
HTTPStatus.OK,
)
return jsonify({"paid": False}), HTTPStatus.OK
return jsonable_encoder({"paid": False}), HTTPStatus.OK

View file

@ -0,0 +1,15 @@
# TPoS
## A Shareable PoS (Point of Sale) that doesn't need to be installed and can run in the browser!
An easy, fast and secure way to accept Bitcoin, over Lightning Network, at your business. The PoS is isolated from the wallet, so it's safe for any employee to use. You can create as many TPOS's as you need, for example one for each employee, or one for each branch of your business.
### Usage
1. Enable extension
2. Create a TPOS\
![create](https://imgur.com/8jNj8Zq.jpg)
3. Open TPOS on the browser\
![open](https://imgur.com/LZuoWzb.jpg)
4. Present invoice QR to costumer\
![pay](https://imgur.com/tOwxn77.jpg)

View file

@ -0,0 +1,12 @@
from quart import Blueprint
from lnbits.db import Database
db = Database("ext_tpos")
tpos_ext: Blueprint = Blueprint(
"tpos", __name__, static_folder="static", template_folder="templates"
)
from .views_api import * # noqa
from .views import * # noqa

View file

@ -0,0 +1,6 @@
{
"name": "TPoS",
"short_description": "A shareable PoS terminal!",
"icon": "dialpad",
"contributors": ["talvasconcelos", "arcbtc"]
}

View file

@ -0,0 +1,42 @@
from typing import List, Optional, Union
from lnbits.helpers import urlsafe_short_hash
from . import db
from .models import TPoS
async def create_tpos(*, wallet_id: str, name: str, currency: str) -> TPoS:
tpos_id = urlsafe_short_hash()
await db.execute(
"""
INSERT INTO tpos.tposs (id, wallet, name, currency)
VALUES (?, ?, ?, ?)
""",
(tpos_id, wallet_id, name, currency),
)
tpos = await get_tpos(tpos_id)
assert tpos, "Newly created tpos couldn't be retrieved"
return tpos
async def get_tpos(tpos_id: str) -> Optional[TPoS]:
row = await db.fetchone("SELECT * FROM tpos.tposs WHERE id = ?", (tpos_id,))
return TPoS.from_row(row) if row else None
async def get_tposs(wallet_ids: Union[str, List[str]]) -> List[TPoS]:
if isinstance(wallet_ids, str):
wallet_ids = [wallet_ids]
q = ",".join(["?"] * len(wallet_ids))
rows = await db.fetchall(
f"SELECT * FROM tpos.tposs WHERE wallet IN ({q})", (*wallet_ids,)
)
return [TPoS.from_row(row) for row in rows]
async def delete_tpos(tpos_id: str) -> None:
await db.execute("DELETE FROM tpos.tposs WHERE id = ?", (tpos_id,))

View file

@ -0,0 +1,14 @@
async def m001_initial(db):
"""
Initial tposs table.
"""
await db.execute(
"""
CREATE TABLE tpos.tposs (
id TEXT PRIMARY KEY,
wallet TEXT NOT NULL,
name TEXT NOT NULL,
currency TEXT NOT NULL
);
"""
)

View file

@ -0,0 +1,14 @@
from sqlite3 import Row
from pydantic import BaseModel
#from typing import NamedTuple
class TPoS(BaseModel):
id: str
wallet: str
name: str
currency: str
@classmethod
def from_row(cls, row: Row) -> "TPoS":
return cls(**dict(row))

View file

@ -0,0 +1,78 @@
<q-expansion-item
group="extras"
icon="swap_vertical_circle"
label="API info"
:content-inset-level="0.5"
>
<q-expansion-item group="api" dense expand-separator label="List TPoS">
<q-card>
<q-card-section>
<code><span class="text-blue">GET</span> /tpos/api/v1/tposs</code>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;invoice_key&gt;}</code><br />
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 OK (application/json)
</h5>
<code>[&lt;tpos_object&gt;, ...]</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X GET {{ request.url_root }}api/v1/tposs -H "X-Api-Key:
&lt;invoice_key&gt;"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item group="api" dense expand-separator label="Create a TPoS">
<q-card>
<q-card-section>
<code><span class="text-green">POST</span> /tpos/api/v1/tposs</code>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;invoice_key&gt;}</code><br />
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<code
>{"name": &lt;string&gt;, "currency": &lt;string*ie USD*&gt;}</code
>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 201 CREATED (application/json)
</h5>
<code
>{"currency": &lt;string&gt;, "id": &lt;string&gt;, "name":
&lt;string&gt;, "wallet": &lt;string&gt;}</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X POST {{ request.url_root }}api/v1/tposs -d '{"name":
&lt;string&gt;, "currency": &lt;string&gt;}' -H "Content-type:
application/json" -H "X-Api-Key: &lt;admin_key&gt;"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item
group="api"
dense
expand-separator
label="Delete a TPoS"
class="q-pb-md"
>
<q-card>
<q-card-section>
<code
><span class="text-pink">DELETE</span>
/tpos/api/v1/tposs/&lt;tpos_id&gt;</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;admin_key&gt;}</code><br />
<h5 class="text-caption q-mt-sm q-mb-none">Returns 204 NO CONTENT</h5>
<code></code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X DELETE {{ request.url_root }}api/v1/tposs/&lt;tpos_id&gt; -H
"X-Api-Key: &lt;admin_key&gt;"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
</q-expansion-item>

View file

@ -0,0 +1,18 @@
<q-expansion-item group="extras" icon="info" label="About TPoS">
<q-card>
<q-card-section>
<p>
Thiago's Point of Sale is a secure, mobile-ready, instant and shareable
point of sale terminal (PoS) for merchants. The PoS is linked to your
LNbits wallet but completely air-gapped so users can ONLY create
invoices. To share the TPoS hit the hash on the terminal.
</p>
<small
>Created by
<a href="https://github.com/talvasconcelos" target="_blank"
>Tiago Vasconcelos</a
>.</small
>
</q-card-section>
</q-card>
</q-expansion-item>

View file

@ -0,0 +1,423 @@
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
%} {% block page %}
<div class="row q-col-gutter-md">
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
<q-card>
<q-card-section>
<q-btn unelevated color="primary" @click="formDialog.show = true"
>New TPoS</q-btn
>
</q-card-section>
</q-card>
<q-card>
<q-card-section>
<div class="row items-center no-wrap q-mb-md">
<div class="col">
<h5 class="text-subtitle1 q-my-none">TPoS</h5>
</div>
<div class="col-auto">
<q-btn flat color="grey" @click="exportCSV">Export to CSV</q-btn>
</div>
</div>
<q-table
dense
flat
:data="tposs"
row-key="id"
:columns="tpossTable.columns"
:pagination.sync="tpossTable.pagination"
>
{% raw %}
<template v-slot:header="props">
<q-tr :props="props">
<q-th auto-width></q-th>
<q-th v-for="col in props.cols" :key="col.name" :props="props">
{{ col.label }}
</q-th>
<q-th auto-width></q-th>
</q-tr>
</template>
<template v-slot:body="props">
<q-tr :props="props">
<q-td auto-width>
<q-btn
unelevated
dense
size="xs"
icon="launch"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
type="a"
:href="props.row.tpos"
target="_blank"
></q-btn>
</q-td>
<q-td v-for="col in props.cols" :key="col.name" :props="props">
{{ col.value }}
</q-td>
<q-td auto-width>
<q-btn
flat
dense
size="xs"
@click="deleteTPoS(props.row.id)"
icon="cancel"
color="pink"
></q-btn>
</q-td>
</q-tr>
</template>
{% endraw %}
</q-table>
</q-card-section>
</q-card>
</div>
<div class="col-12 col-md-5 q-gutter-y-md">
<q-card>
<q-card-section>
<h6 class="text-subtitle1 q-my-none">{{SITE_TITLE}} TPoS extension</h6>
</q-card-section>
<q-card-section class="q-pa-none">
<q-separator></q-separator>
<q-list>
{% include "tpos/_api_docs.html" %}
<q-separator></q-separator>
{% include "tpos/_tpos.html" %}
</q-list>
</q-card-section>
</q-card>
</div>
<q-dialog v-model="formDialog.show" position="top" @hide="closeFormDialog">
<q-card class="q-pa-lg q-pt-xl" style="width: 500px">
<q-form @submit="createTPoS" class="q-gutter-md">
<q-input
filled
dense
v-model.trim="formDialog.data.name"
label="Name"
placeholder="Tiago's PoS"
></q-input>
<q-select
filled
dense
emit-value
v-model="formDialog.data.wallet"
:options="g.user.walletOptions"
label="Wallet *"
></q-select>
<q-select
filled
dense
emit-value
v-model="formDialog.data.currency"
:options="currencyOptions"
label="Currency *"
></q-select>
<div class="row q-mt-lg">
<q-btn
unelevated
color="primary"
:disable="formDialog.data.currency == null || formDialog.data.name == null"
type="submit"
>Create TPoS</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
</div>
</q-form>
</q-card>
</q-dialog>
</div>
{% endblock %} {% block scripts %} {{ window_vars(user) }}
<script>
var mapTPoS = function (obj) {
obj.date = Quasar.utils.date.formatDate(
new Date(obj.time * 1000),
'YYYY-MM-DD HH:mm'
)
obj.fsat = new Intl.NumberFormat(LOCALE).format(obj.amount)
obj.tpos = ['/tpos/', obj.id].join('')
return obj
}
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
return {
tposs: [],
currencyOptions: [
'USD',
'EUR',
'GBP',
'AED',
'AFN',
'ALL',
'AMD',
'ANG',
'AOA',
'ARS',
'AUD',
'AWG',
'AZN',
'BAM',
'BBD',
'BDT',
'BGN',
'BHD',
'BIF',
'BMD',
'BND',
'BOB',
'BRL',
'BSD',
'BTN',
'BWP',
'BYN',
'BZD',
'CAD',
'CDF',
'CHF',
'CLF',
'CLP',
'CNH',
'CNY',
'COP',
'CRC',
'CUC',
'CUP',
'CVE',
'CZK',
'DJF',
'DKK',
'DOP',
'DZD',
'EGP',
'ERN',
'ETB',
'EUR',
'FJD',
'FKP',
'GBP',
'GEL',
'GGP',
'GHS',
'GIP',
'GMD',
'GNF',
'GTQ',
'GYD',
'HKD',
'HNL',
'HRK',
'HTG',
'HUF',
'IDR',
'ILS',
'IMP',
'INR',
'IQD',
'IRR',
'ISK',
'JEP',
'JMD',
'JOD',
'JPY',
'KES',
'KGS',
'KHR',
'KMF',
'KPW',
'KRW',
'KWD',
'KYD',
'KZT',
'LAK',
'LBP',
'LKR',
'LRD',
'LSL',
'LYD',
'MAD',
'MDL',
'MGA',
'MKD',
'MMK',
'MNT',
'MOP',
'MRO',
'MUR',
'MVR',
'MWK',
'MXN',
'MYR',
'MZN',
'NAD',
'NGN',
'NIO',
'NOK',
'NPR',
'NZD',
'OMR',
'PAB',
'PEN',
'PGK',
'PHP',
'PKR',
'PLN',
'PYG',
'QAR',
'RON',
'RSD',
'RUB',
'RWF',
'SAR',
'SBD',
'SCR',
'SDG',
'SEK',
'SGD',
'SHP',
'SLL',
'SOS',
'SRD',
'SSP',
'STD',
'SVC',
'SYP',
'SZL',
'THB',
'TJS',
'TMT',
'TND',
'TOP',
'TRY',
'TTD',
'TWD',
'TZS',
'UAH',
'UGX',
'USD',
'UYU',
'UZS',
'VEF',
'VES',
'VND',
'VUV',
'WST',
'XAF',
'XAG',
'XAU',
'XCD',
'XDR',
'XOF',
'XPD',
'XPF',
'XPT',
'YER',
'ZAR',
'ZMW',
'ZWL'
],
tpossTable: {
columns: [
{name: 'id', align: 'left', label: 'ID', field: 'id'},
{name: 'name', align: 'left', label: 'Name', field: 'name'},
{
name: 'currency',
align: 'left',
label: 'Currency',
field: 'currency'
}
],
pagination: {
rowsPerPage: 10
}
},
formDialog: {
show: false,
data: {}
}
}
},
methods: {
closeFormDialog: function () {
this.formDialog.data = {}
},
getTPoSs: function () {
var self = this
LNbits.api
.request(
'GET',
'/tpos/api/v1/tposs?all_wallets',
this.g.user.wallets[0].inkey
)
.then(function (response) {
self.tposs = response.data.map(function (obj) {
return mapTPoS(obj)
})
})
},
createTPoS: function () {
var data = {
name: this.formDialog.data.name,
currency: this.formDialog.data.currency
}
var self = this
LNbits.api
.request(
'POST',
'/tpos/api/v1/tposs',
_.findWhere(this.g.user.wallets, {id: this.formDialog.data.wallet})
.inkey,
data
)
.then(function (response) {
self.tposs.push(mapTPoS(response.data))
self.formDialog.show = false
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
deleteTPoS: function (tposId) {
var self = this
var tpos = _.findWhere(this.tposs, {id: tposId})
LNbits.utils
.confirmDialog('Are you sure you want to delete this TPoS?')
.onOk(function () {
LNbits.api
.request(
'DELETE',
'/tpos/api/v1/tposs/' + tposId,
_.findWhere(self.g.user.wallets, {id: tpos.wallet}).adminkey
)
.then(function (response) {
self.tposs = _.reject(self.tposs, function (obj) {
return obj.id == tposId
})
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
})
},
exportCSV: function () {
LNbits.utils.exportCSV(this.tpossTable.columns, this.tposs)
}
},
created: function () {
if (this.g.user.wallets.length) {
this.getTPoSs()
}
}
})
</script>
{% endblock %}

View file

@ -0,0 +1,264 @@
{% extends "public.html" %} {% block toolbar_title %}{{ tpos.name }}{% endblock
%} {% block footer %}{% endblock %} {% block page_container %}
<q-page-container>
<q-page>
<q-page-sticky v-if="exchangeRate" expand position="top">
<div class="row justify-center full-width">
<div class="col-12 col-sm-8 col-md-6 col-lg-4 text-center">
<h3 class="q-mb-md">{% raw %}{{ famount }}{% endraw %}</h3>
<h5 class="q-mt-none">
{% raw %}{{ fsat }}{% endraw %} <small>sat</small>
</h5>
</div>
</div>
</q-page-sticky>
<q-page-sticky expand position="bottom">
<div class="row justify-center full-width">
<div class="col-12 col-sm-8 col-md-6 col-lg-4">
<div class="keypad q-pa-sm">
<q-btn unelevated @click="stack.push(1)" size="xl" color="grey-8"
>1</q-btn
>
<q-btn unelevated @click="stack.push(2)" size="xl" color="grey-8"
>2</q-btn
>
<q-btn unelevated @click="stack.push(3)" size="xl" color="grey-8"
>3</q-btn
>
<q-btn
unelevated
@click="stack = []"
size="xl"
color="pink"
class="btn-cancel"
>C</q-btn
>
<q-btn unelevated @click="stack.push(4)" size="xl" color="grey-8"
>4</q-btn
>
<q-btn unelevated @click="stack.push(5)" size="xl" color="grey-8"
>5</q-btn
>
<q-btn unelevated @click="stack.push(6)" size="xl" color="grey-8"
>6</q-btn
>
<q-btn unelevated @click="stack.push(7)" size="xl" color="grey-8"
>7</q-btn
>
<q-btn unelevated @click="stack.push(8)" size="xl" color="grey-8"
>8</q-btn
>
<q-btn unelevated @click="stack.push(9)" size="xl" color="grey-8"
>9</q-btn
>
<q-btn
unelevated
:disabled="amount == 0"
@click="showInvoice()"
size="xl"
color="green"
class="btn-confirm"
>OK</q-btn
>
<q-btn
unelevated
@click="stack.splice(-1, 1)"
size="xl"
color="grey-7"
>DEL</q-btn
>
<q-btn unelevated @click="stack.push(0)" size="xl" color="grey-8"
>0</q-btn
>
<q-btn
unelevated
@click="urlDialog.show = true"
size="xl"
color="grey-7"
>#</q-btn
>
</div>
</div>
</div>
</q-page-sticky>
<q-dialog
v-model="invoiceDialog.show"
position="top"
@hide="closeInvoiceDialog"
>
<q-card
v-if="invoiceDialog.data"
class="q-pa-lg q-pt-xl lnbits__dialog-card"
>
<q-responsive :ratio="1" class="q-mx-xl q-mb-md">
<qrcode
:value="invoiceDialog.data.payment_request"
:options="{width: 800}"
class="rounded-borders"
></qrcode>
</q-responsive>
<div class="text-center">
<h3 class="q-my-md">{% raw %}{{ famount }}{% endraw %}</h3>
<h5 class="q-mt-none">
{% raw %}{{ fsat }}{% endraw %} <small>sat</small>
</h5>
</div>
<div class="row q-mt-lg">
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
</div>
</q-card>
</q-dialog>
<q-dialog v-model="urlDialog.show" position="top">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<q-responsive :ratio="1" class="q-mx-xl q-mb-md">
<qrcode
value="{{ request.url }}"
:options="{width: 800}"
class="rounded-borders"
></qrcode>
</q-responsive>
<div class="text-center q-mb-xl">
<p style="word-break: break-all">
<strong>{{ tpos.name }}</strong><br />{{ request.url }}
</p>
</div>
<div class="row q-mt-lg">
<q-btn
outline
color="grey"
@click="copyText('{{ request.url }}', 'TPoS URL copied to clipboard!')"
>Copy URL</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
</div>
</q-card>
</q-dialog>
</q-page>
</q-page-container>
{% endblock %} {% block styles %}
<style>
.keypad {
display: grid;
grid-gap: 8px;
grid-template-columns: repeat(4, 1fr);
grid-template-rows: repeat(4, 1fr);
}
.keypad .btn {
height: 100%;
}
.btn-cancel,
.btn-confirm {
grid-row: auto/span 2;
}
</style>
{% endblock %} {% block scripts %}
<script>
Vue.component(VueQrcode.name, VueQrcode)
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
return {
tposId: '{{ tpos.id }}',
currency: '{{ tpos.currency }}',
exchangeRate: null,
stack: [],
invoiceDialog: {
show: false,
data: null,
dismissMsg: null,
paymentChecker: null
},
urlDialog: {
show: false
}
}
},
computed: {
amount: function () {
if (!this.stack.length) return 0.0
return (Number(this.stack.join('')) / 100).toFixed(2)
},
famount: function () {
return LNbits.utils.formatCurrency(this.amount, this.currency)
},
sat: function () {
if (!this.exchangeRate) return 0
return Math.ceil((this.amount / this.exchangeRate) * 100000000)
},
fsat: function () {
return LNbits.utils.formatSat(this.sat)
}
},
methods: {
closeInvoiceDialog: function () {
this.stack = []
var dialog = this.invoiceDialog
setTimeout(function () {
clearInterval(dialog.paymentChecker)
dialog.dismissMsg()
}, 3000)
},
showInvoice: function () {
var self = this
var dialog = this.invoiceDialog
axios
.post('/tpos/api/v1/tposs/' + this.tposId + '/invoices/', {
amount: this.sat
})
.then(function (response) {
dialog.data = response.data
dialog.show = true
dialog.dismissMsg = self.$q.notify({
timeout: 0,
message: 'Waiting for payment...'
})
dialog.paymentChecker = setInterval(function () {
axios
.get(
'/tpos/api/v1/tposs/' +
self.tposId +
'/invoices/' +
response.data.payment_hash
)
.then(function (res) {
if (res.data.paid) {
clearInterval(dialog.paymentChecker)
dialog.dismissMsg()
dialog.show = false
self.$q.notify({
type: 'positive',
message: self.fsat + ' sat received!',
icon: null
})
}
})
}, 3000)
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
getRates: function () {
var self = this
axios.get('https://api.opennode.co/v1/rates').then(function (response) {
self.exchangeRate =
response.data.data['BTC' + self.currency][self.currency]
})
}
},
created: function () {
var getRates = this.getRates
getRates()
setInterval(function () {
getRates()
}, 20000)
}
})
</script>
{% endblock %}

View file

@ -0,0 +1,23 @@
from quart import g, abort, render_template
from http import HTTPStatus
from lnbits.decorators import check_user_exists, validate_uuids
from . import tpos_ext
from .crud import get_tpos
@tpos_ext.get("/")
@validate_uuids(["usr"], required=True)
@check_user_exists()
async def index():
return await render_template("tpos/index.html", user=g.user)
@tpos_ext.get("/{tpos_id}")
async def tpos(tpos_id):
tpos = await get_tpos(tpos_id)
if not tpos:
abort(HTTPStatus.NOT_FOUND, "TPoS does not exist.")
return await render_template("tpos/tpos.html", tpos=tpos)

View file

@ -0,0 +1,107 @@
from quart import g, jsonify, request
from http import HTTPStatus
from fastapi import FastAPI, Query
from fastapi.encoders import jsonable_encoder
from fastapi.responses import JSONResponse
from pydantic import BaseModel
from lnbits.core.crud import get_user, get_wallet
from lnbits.core.services import create_invoice, check_invoice_status
from lnbits.decorators import api_check_wallet_key, api_validate_post_request
from . import tpos_ext
from .crud import create_tpos, get_tpos, get_tposs, delete_tpos
from .models import TPoS
@tpos_ext.get("/api/v1/tposs")
@api_check_wallet_key("invoice")
async def api_tposs(all_wallets: bool = Query(None)):
wallet_ids = [g.wallet.id]
if all_wallets:
wallet_ids = wallet_ids = (await get_user(g.wallet.user)).wallet_ids(await get_user(g.wallet.user)).wallet_ids
# if "all_wallets" in request.args:
# wallet_ids = (await get_user(g.wallet.user)).wallet_ids
return [tpos._asdict() for tpos in await get_tposs(wallet_ids)], HTTPStatus.OK
class CreateData(BaseModel):
name: str
currency: str
@tpos_ext.post("/api/v1/tposs")
@api_check_wallet_key("invoice")
# @api_validate_post_request(
# schema={
# "name": {"type": "string", "empty": False, "required": True},
# "currency": {"type": "string", "empty": False, "required": True},
# }
# )
async def api_tpos_create(data: CreateData):
tpos = await create_tpos(wallet_id=g.wallet.id, **data)
return tpos._asdict(), HTTPStatus.CREATED
@tpos_ext.delete("/api/v1/tposs/{tpos_id}")
@api_check_wallet_key("admin")
async def api_tpos_delete(tpos_id: str):
tpos = await get_tpos(tpos_id)
if not tpos:
return {"message": "TPoS does not exist."}, HTTPStatus.NOT_FOUND
if tpos.wallet != g.wallet.id:
return {"message": "Not your TPoS."}, HTTPStatus.FORBIDDEN
await delete_tpos(tpos_id)
return "", HTTPStatus.NO_CONTENT
@tpos_ext.post("/api/v1/tposs/{tpos_id}/invoices/")
# @api_validate_post_request(
# schema={"amount": {"type": "integer", "min": 1, "required": True}}
# )
async def api_tpos_create_invoice(amount: int = Query(..., ge=1), tpos_id: str = None):
tpos = await get_tpos(tpos_id)
if not tpos:
return {"message": "TPoS does not exist."}, HTTPStatus.NOT_FOUND
try:
payment_hash, payment_request = await create_invoice(
wallet_id=tpos.wallet,
amount=amount,
memo=f"{tpos.name}",
extra={"tag": "tpos"},
)
except Exception as e:
return {"message": str(e)}, HTTPStatus.INTERNAL_SERVER_ERROR
return {"payment_hash": payment_hash, "payment_request": payment_request}, HTTPStatus.CREATED
@tpos_ext.get("/api/v1/tposs/{tpos_id}/invoices/{payment_hash}")
async def api_tpos_check_invoice(tpos_id: str, payment_hash: str):
tpos = await get_tpos(tpos_id)
if not tpos:
return {"message": "TPoS does not exist."}, HTTPStatus.NOT_FOUND
try:
status = await check_invoice_status(tpos.wallet, payment_hash)
is_paid = not status.pending
except Exception as exc:
print(exc)
return {"paid": False}, HTTPStatus.OK
if is_paid:
wallet = await get_wallet(tpos.wallet)
payment = await wallet.get_payment(payment_hash)
await payment.set_pending(False)
return {"paid": True}, HTTPStatus.OK
return {"paid": False}, HTTPStatus.OK

View file

@ -6,17 +6,9 @@ from lnbits.decorators import check_user_exists, validate_uuids
from . import watchonly_ext
@watchonly_ext.route("/")
@watchonly_ext.get("/")
@validate_uuids(["usr"], required=True)
@check_user_exists()
async def index():
return await render_template("watchonly/index.html", user=g.user)
@watchonly_ext.route("/<charge_id>")
async def display(charge_id):
link = get_payment(charge_id) or abort(
HTTPStatus.NOT_FOUND, "Charge link does not exist."
)
return await render_template("watchonly/display.html", link=link)

View file

@ -24,86 +24,78 @@ from .crud import (
###################WALLETS#############################
@watchonly_ext.route("/api/v1/wallet", methods=["GET"])
@watchonly_ext.get("/api/v1/wallet")
@api_check_wallet_key("invoice")
async def api_wallets_retrieve():
try:
return (
jsonify(
[wallet._asdict() for wallet in await get_watch_wallets(g.wallet.user)]
),
[wallet._asdict() for wallet in await get_watch_wallets(g.wallet.user)],
HTTPStatus.OK,
)
except:
return ""
@watchonly_ext.route("/api/v1/wallet/<wallet_id>", methods=["GET"])
@watchonly_ext.get("/api/v1/wallet/<wallet_id>")
@api_check_wallet_key("invoice")
async def api_wallet_retrieve(wallet_id):
wallet = await get_watch_wallet(wallet_id)
if not wallet:
return jsonify({"message": "wallet does not exist"}), HTTPStatus.NOT_FOUND
return {"message": "wallet does not exist"}, HTTPStatus.NOT_FOUND
return jsonify(wallet._asdict()), HTTPStatus.OK
return wallet._asdict(), HTTPStatus.OK
@watchonly_ext.route("/api/v1/wallet", methods=["POST"])
@watchonly_ext.post("/api/v1/wallet")
@api_check_wallet_key("admin")
@api_validate_post_request(
schema={
"masterpub": {"type": "string", "empty": False, "required": True},
"title": {"type": "string", "empty": False, "required": True},
}
)
async def api_wallet_create_or_update(wallet_id=None):
async def api_wallet_create_or_update(masterPub: str, Title: str, wallet_id=None):
try:
wallet = await create_watch_wallet(
user=g.wallet.user, masterpub=g.data["masterpub"], title=g.data["title"]
user=g.wallet.user, masterpub=masterPub, title=Title
)
except Exception as e:
return jsonify({"message": str(e)}), HTTPStatus.BAD_REQUEST
return {"message": str(e)}, HTTPStatus.BAD_REQUEST
mempool = await get_mempool(g.wallet.user)
if not mempool:
create_mempool(user=g.wallet.user)
return jsonify(wallet._asdict()), HTTPStatus.CREATED
return wallet._asdict(), HTTPStatus.CREATED
@watchonly_ext.route("/api/v1/wallet/<wallet_id>", methods=["DELETE"])
@watchonly_ext.delete("/api/v1/wallet/<wallet_id>")
@api_check_wallet_key("admin")
async def api_wallet_delete(wallet_id):
wallet = await get_watch_wallet(wallet_id)
if not wallet:
return jsonify({"message": "Wallet link does not exist."}), HTTPStatus.NOT_FOUND
return {"message": "Wallet link does not exist."}, HTTPStatus.NOT_FOUND
await delete_watch_wallet(wallet_id)
return jsonify({"deleted": "true"}), HTTPStatus.NO_CONTENT
return {"deleted": "true"}, HTTPStatus.NO_CONTENT
#############################ADDRESSES##########################
@watchonly_ext.route("/api/v1/address/<wallet_id>", methods=["GET"])
@watchonly_ext.get("/api/v1/address/<wallet_id>")
@api_check_wallet_key("invoice")
async def api_fresh_address(wallet_id):
await get_fresh_address(wallet_id)
addresses = await get_addresses(wallet_id)
return jsonify([address._asdict() for address in addresses]), HTTPStatus.OK
return [address._asdict() for address in addresses], HTTPStatus.OK
@watchonly_ext.route("/api/v1/addresses/<wallet_id>", methods=["GET"])
@watchonly_ext.get("/api/v1/addresses/<wallet_id>")
@api_check_wallet_key("invoice")
async def api_get_addresses(wallet_id):
wallet = await get_watch_wallet(wallet_id)
if not wallet:
return jsonify({"message": "wallet does not exist"}), HTTPStatus.NOT_FOUND
return {"message": "wallet does not exist"}, HTTPStatus.NOT_FOUND
addresses = await get_addresses(wallet_id)
@ -111,28 +103,23 @@ async def api_get_addresses(wallet_id):
await get_fresh_address(wallet_id)
addresses = await get_addresses(wallet_id)
return jsonify([address._asdict() for address in addresses]), HTTPStatus.OK
return [address._asdict() for address in addresses], HTTPStatus.OK
#############################MEMPOOL##########################
@watchonly_ext.route("/api/v1/mempool", methods=["PUT"])
@watchonly_ext.put("/api/v1/mempool")
@api_check_wallet_key("admin")
@api_validate_post_request(
schema={
"endpoint": {"type": "string", "empty": False, "required": True},
}
)
async def api_update_mempool():
mempool = await update_mempool(user=g.wallet.user, **g.data)
return jsonify(mempool._asdict()), HTTPStatus.OK
async def api_update_mempool(endpoint: str):
mempool = await update_mempool(user=g.wallet.user, **endpoint)
return mempool._asdict(), HTTPStatus.OK
@watchonly_ext.route("/api/v1/mempool", methods=["GET"])
@watchonly_ext.get("/api/v1/mempool")
@api_check_wallet_key("admin")
async def api_get_mempool():
mempool = await get_mempool(g.wallet.user)
if not mempool:
mempool = await create_mempool(user=g.wallet.user)
return jsonify(mempool._asdict()), HTTPStatus.OK
return mempool._asdict(), HTTPStatus.OK

View file

@ -8,14 +8,14 @@ from . import withdraw_ext
from .crud import get_withdraw_link, chunks
@withdraw_ext.route("/")
@withdraw_ext.get("/")
@validate_uuids(["usr"], required=True)
@check_user_exists()
async def index():
return await render_template("withdraw/index.html", user=g.user)
@withdraw_ext.route("/<link_id>")
@withdraw_ext.get("/<link_id>")
async def display(link_id):
link = await get_withdraw_link(link_id, 0) or abort(
HTTPStatus.NOT_FOUND, "Withdraw link does not exist."
@ -23,7 +23,7 @@ async def display(link_id):
return await render_template("withdraw/display.html", link=link, unique=True)
@withdraw_ext.route("/img/<link_id>")
@withdraw_ext.get("/img/<link_id>")
async def img(link_id):
link = await get_withdraw_link(link_id, 0) or abort(
HTTPStatus.NOT_FOUND, "Withdraw link does not exist."
@ -43,7 +43,7 @@ async def img(link_id):
)
@withdraw_ext.route("/print/<link_id>")
@withdraw_ext.get("/print/<link_id>")
async def print_qr(link_id):
link = await get_withdraw_link(link_id) or abort(
HTTPStatus.NOT_FOUND, "Withdraw link does not exist."

View file

@ -20,7 +20,7 @@ from .crud import (
)
@withdraw_ext.route("/api/v1/links", methods=["GET"])
@withdraw_ext.get("/api/v1/links")
@api_check_wallet_key("invoice")
async def api_links():
wallet_ids = [g.wallet.id]
@ -51,7 +51,7 @@ async def api_links():
)
@withdraw_ext.route("/api/v1/links/<link_id>", methods=["GET"])
@withdraw_ext.get("/api/v1/links/<link_id>")
@api_check_wallet_key("invoice")
async def api_link_retrieve(link_id):
link = await get_withdraw_link(link_id, 0)
@ -68,15 +68,15 @@ async def api_link_retrieve(link_id):
return jsonable_encoder({**link, **{"lnurl": link.lnurl}}), HTTPStatus.OK
class CreateData(BaseModel):
title: str = Query(...),
min_withdrawable: int = Query(..., ge=1),
max_withdrawable: int = Query(..., ge=1),
uses: int = Query(..., ge=1),
wait_time: int = Query(..., ge=1),
title: str = Query(...)
min_withdrawable: int = Query(..., ge=1)
max_withdrawable: int = Query(..., ge=1)
uses: int = Query(..., ge=1)
wait_time: int = Query(..., ge=1)
is_unique: bool
@withdraw_ext.route("/api/v1/links", methods=["POST"])
@withdraw_ext.route("/api/v1/links/<link_id>", methods=["PUT"])
@withdraw_ext.post("/api/v1/links")
@withdraw_ext.put("/api/v1/links/<link_id>")
@api_check_wallet_key("admin")
async def api_link_create_or_update(data: CreateData, link_id: str = None):
if data.max_withdrawable < data.min_withdrawable:
@ -118,7 +118,7 @@ async def api_link_create_or_update(data: CreateData, link_id: str = None):
)
@withdraw_ext.route("/api/v1/links/<link_id>", methods=["DELETE"])
@withdraw_ext.delete("/api/v1/links/<link_id>")
@api_check_wallet_key("admin")
async def api_link_delete(link_id):
link = await get_withdraw_link(link_id)
@ -137,8 +137,8 @@ async def api_link_delete(link_id):
return "", HTTPStatus.NO_CONTENT
@withdraw_ext.route("/api/v1/links/<the_hash>/<lnurl_id>", methods=["GET"])
@withdraw_ext.get("/api/v1/links/<the_hash>/<lnurl_id>")
@api_check_wallet_key("invoice")
async def api_hash_retrieve(the_hash, lnurl_id):
hashCheck = await get_hash_check(the_hash, lnurl_id)
return jsonify(hashCheck), HTTPStatus.OK
return jsonable_encoder(hashCheck), HTTPStatus.OK