mirror of
https://github.com/lnbits/lnbits-legend.git
synced 2025-02-25 07:07:48 +01:00
Merge pull request #306 from arcbtc/FastAPI
Started on extensions and projects
This commit is contained in:
commit
0baa34ec2f
24 changed files with 1311 additions and 399 deletions
|
@ -6,11 +6,11 @@ from ecdsa import SECP256k1, SigningKey # type: ignore
|
||||||
from lnurl import encode as lnurl_encode # type: ignore
|
from lnurl import encode as lnurl_encode # type: ignore
|
||||||
from typing import List, NamedTuple, Optional, Dict
|
from typing import List, NamedTuple, Optional, Dict
|
||||||
from sqlite3 import Row
|
from sqlite3 import Row
|
||||||
|
from pydantic import BaseModel
|
||||||
from lnbits.settings import WALLET
|
from lnbits.settings import WALLET
|
||||||
|
|
||||||
|
|
||||||
class User(NamedTuple):
|
class User(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
email: str
|
email: str
|
||||||
extensions: List[str] = []
|
extensions: List[str] = []
|
||||||
|
@ -26,7 +26,7 @@ class User(NamedTuple):
|
||||||
return w[0] if w else None
|
return w[0] if w else None
|
||||||
|
|
||||||
|
|
||||||
class Wallet(NamedTuple):
|
class Wallet(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
name: str
|
name: str
|
||||||
user: str
|
user: str
|
||||||
|
@ -73,7 +73,7 @@ class Wallet(NamedTuple):
|
||||||
return await get_wallet_payment(self.id, payment_hash)
|
return await get_wallet_payment(self.id, payment_hash)
|
||||||
|
|
||||||
|
|
||||||
class Payment(NamedTuple):
|
class Payment(BaseModel):
|
||||||
checking_id: str
|
checking_id: str
|
||||||
pending: bool
|
pending: bool
|
||||||
amount: int
|
amount: int
|
||||||
|
@ -161,7 +161,7 @@ class Payment(NamedTuple):
|
||||||
await delete_payment(self.checking_id)
|
await delete_payment(self.checking_id)
|
||||||
|
|
||||||
|
|
||||||
class BalanceCheck(NamedTuple):
|
class BalanceCheck(BaseModel):
|
||||||
wallet: str
|
wallet: str
|
||||||
service: str
|
service: str
|
||||||
url: str
|
url: str
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
from pydantic.types import constr
|
||||||
import trio
|
import trio
|
||||||
import json
|
import json
|
||||||
import httpx
|
import httpx
|
||||||
|
@ -5,15 +6,13 @@ import hashlib
|
||||||
from urllib.parse import urlparse, urlunparse, urlencode, parse_qs, ParseResult
|
from urllib.parse import urlparse, urlunparse, urlencode, parse_qs, ParseResult
|
||||||
from quart import g, current_app, make_response, url_for
|
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.encoders import jsonable_encoder
|
||||||
from fastapi.responses import JSONResponse
|
|
||||||
from pydantic import BaseModel
|
|
||||||
|
|
||||||
|
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from binascii import unhexlify
|
from binascii import unhexlify
|
||||||
from typing import Dict, Union
|
from typing import Dict, List, Optional, Union
|
||||||
|
|
||||||
from lnbits import bolt11, lnurl
|
from lnbits import bolt11, lnurl
|
||||||
from lnbits.decorators import api_check_wallet_key, api_validate_post_request
|
from lnbits.decorators import api_check_wallet_key, api_validate_post_request
|
||||||
|
@ -68,53 +67,30 @@ async def api_payments():
|
||||||
HTTPStatus.OK,
|
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_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)])):
|
# async def api_payments_create_invoice(amount: List[str] = Query([type: str = Query(None)])):
|
||||||
|
async def api_payments_create_invoice(data: CreateInvoiceData):
|
||||||
|
if "description_hash" in data:
|
||||||
class Memo(BaseModel):
|
description_hash = unhexlify(data.description_hash)
|
||||||
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"])
|
|
||||||
memo = ""
|
memo = ""
|
||||||
else:
|
else:
|
||||||
description_hash = b""
|
description_hash = b""
|
||||||
memo = g.data["memo"]
|
memo = data.memo
|
||||||
|
|
||||||
if g.data.get("unit") or "sat" == "sat":
|
if data.unit or "sat" == "sat":
|
||||||
amount = g.data["amount"]
|
amount = data.amount
|
||||||
else:
|
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
|
amount = price_in_sats
|
||||||
|
|
||||||
async with db.connect() as conn:
|
async with db.connect() as conn:
|
||||||
|
@ -124,31 +100,31 @@ async def api_payments_create_invoice(amount: int = Query(...), memo: Memo ):
|
||||||
amount=amount,
|
amount=amount,
|
||||||
memo=memo,
|
memo=memo,
|
||||||
description_hash=description_hash,
|
description_hash=description_hash,
|
||||||
extra=g.data.get("extra"),
|
extra=data.extra,
|
||||||
webhook=g.data.get("webhook"),
|
webhook=data.webhook,
|
||||||
conn=conn,
|
conn=conn,
|
||||||
)
|
)
|
||||||
except InvoiceFailure as e:
|
except InvoiceFailure as e:
|
||||||
return jsonable_encoder({"message": str(e)}), 520
|
return {"message": str(e)}, 520
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
raise exc
|
raise exc
|
||||||
|
|
||||||
invoice = bolt11.decode(payment_request)
|
invoice = bolt11.decode(payment_request)
|
||||||
|
|
||||||
lnurl_response: Union[None, bool, str] = None
|
lnurl_response: Union[None, bool, str] = None
|
||||||
if g.data.get("lnurl_callback"):
|
if data.lnurl_callback:
|
||||||
if "lnurl_balance_check" in g.data:
|
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:
|
async with httpx.AsyncClient() as client:
|
||||||
try:
|
try:
|
||||||
r = await client.get(
|
r = await client.get(
|
||||||
g.data["lnurl_callback"],
|
data.lnurl_callback,
|
||||||
params={
|
params={
|
||||||
"pr": payment_request,
|
"pr": payment_request,
|
||||||
"balanceNotify": url_for(
|
"balanceNotify": url_for(
|
||||||
"core.lnurl_balance_notify",
|
"core.lnurl_balance_notify",
|
||||||
service=urlparse(g.data["lnurl_callback"]).netloc,
|
service=urlparse(data.lnurl_callback).netloc,
|
||||||
wal=g.wallet.id,
|
wal=g.wallet.id,
|
||||||
_external=True,
|
_external=True,
|
||||||
),
|
),
|
||||||
|
@ -167,15 +143,13 @@ async def api_payments_create_invoice(amount: int = Query(...), memo: Memo ):
|
||||||
lnurl_response = False
|
lnurl_response = False
|
||||||
|
|
||||||
return (
|
return (
|
||||||
jsonable_encoder(
|
|
||||||
{
|
{
|
||||||
"payment_hash": invoice.payment_hash,
|
"payment_hash": invoice.payment_hash,
|
||||||
"payment_request": payment_request,
|
"payment_request": payment_request,
|
||||||
# maintain backwards compatibility with API clients:
|
# maintain backwards compatibility with API clients:
|
||||||
"checking_id": invoice.payment_hash,
|
"checking_id": invoice.payment_hash,
|
||||||
"lnurl_response": lnurl_response,
|
"lnurl_response": lnurl_response,
|
||||||
}
|
},
|
||||||
),
|
|
||||||
HTTPStatus.CREATED,
|
HTTPStatus.CREATED,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -190,97 +164,76 @@ async def api_payments_pay_invoice(
|
||||||
payment_request=bolt11,
|
payment_request=bolt11,
|
||||||
)
|
)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
return jsonable_encoder({"message": str(e)}), HTTPStatus.BAD_REQUEST
|
return {"message": str(e)}, HTTPStatus.BAD_REQUEST
|
||||||
except PermissionError as e:
|
except PermissionError as e:
|
||||||
return jsonable_encoder({"message": str(e)}), HTTPStatus.FORBIDDEN
|
return {"message": str(e)}, HTTPStatus.FORBIDDEN
|
||||||
except PaymentFailure as e:
|
except PaymentFailure as e:
|
||||||
return jsonable_encoder({"message": str(e)}), 520
|
return {"message": str(e)}, 520
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
raise exc
|
raise exc
|
||||||
|
|
||||||
return (
|
return (
|
||||||
jsonable_encoder(
|
|
||||||
{
|
{
|
||||||
"payment_hash": payment_hash,
|
"payment_hash": payment_hash,
|
||||||
# maintain backwards compatibility with API clients:
|
# maintain backwards compatibility with API clients:
|
||||||
"checking_id": payment_hash,
|
"checking_id": payment_hash,
|
||||||
}
|
},
|
||||||
),
|
|
||||||
HTTPStatus.CREATED,
|
HTTPStatus.CREATED,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@core_app.route("/api/v1/payments", methods=["POST"])
|
@core_app.post("/api/v1/payments")
|
||||||
@api_validate_post_request(schema={"out": {"type": "boolean", "required": True}})
|
async def api_payments_create(out: bool = True):
|
||||||
async def api_payments_create():
|
if out is True:
|
||||||
if g.data["out"] is True:
|
|
||||||
return await api_payments_pay_invoice()
|
return await api_payments_pay_invoice()
|
||||||
return await api_payments_create_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_check_wallet_key("admin")
|
||||||
@api_validate_post_request(
|
async def api_payments_pay_lnurl(data: CreateLNURLData):
|
||||||
schema={
|
domain = urlparse(data.callback).netloc
|
||||||
"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 with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
try:
|
try:
|
||||||
r = await client.get(
|
r = await client.get(
|
||||||
g.data["callback"],
|
data.callback,
|
||||||
params={"amount": g.data["amount"], "comment": g.data["comment"]},
|
params={"amount": data.amount, "comment": data.comment},
|
||||||
timeout=40,
|
timeout=40,
|
||||||
)
|
)
|
||||||
if r.is_error:
|
if r.is_error:
|
||||||
raise httpx.ConnectError
|
raise httpx.ConnectError
|
||||||
except (httpx.ConnectError, httpx.RequestError):
|
except (httpx.ConnectError, httpx.RequestError):
|
||||||
return (
|
return (
|
||||||
jsonify({"message": f"Failed to connect to {domain}."}),
|
{"message": f"Failed to connect to {domain}."},
|
||||||
HTTPStatus.BAD_REQUEST,
|
HTTPStatus.BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
params = json.loads(r.text)
|
params = json.loads(r.text)
|
||||||
if params.get("status") == "ERROR":
|
if params.get("status") == "ERROR":
|
||||||
return (
|
return ({"message": f"{domain} said: '{params.get('reason', '')}'"},
|
||||||
jsonify({"message": f"{domain} said: '{params.get('reason', '')}'"}),
|
|
||||||
HTTPStatus.BAD_REQUEST,
|
HTTPStatus.BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
invoice = bolt11.decode(params["pr"])
|
invoice = bolt11.decode(params["pr"])
|
||||||
if invoice.amount_msat != g.data["amount"]:
|
if invoice.amount_msat != data.amount:
|
||||||
return (
|
return (
|
||||||
jsonify(
|
|
||||||
{
|
{
|
||||||
"message": f"{domain} returned an invalid invoice. Expected {g.data['amount']} msat, got {invoice.amount_msat}."
|
"message": f"{domain} returned an invalid invoice. Expected {g.data['amount']} msat, got {invoice.amount_msat}."
|
||||||
}
|
},
|
||||||
),
|
|
||||||
HTTPStatus.BAD_REQUEST,
|
HTTPStatus.BAD_REQUEST,
|
||||||
)
|
)
|
||||||
if invoice.description_hash != g.data["description_hash"]:
|
if invoice.description_hash != g.data["description_hash"]:
|
||||||
return (
|
return (
|
||||||
jsonify(
|
|
||||||
{
|
{
|
||||||
"message": f"{domain} returned an invalid invoice. Expected description_hash == {g.data['description_hash']}, got {invoice.description_hash}."
|
"message": f"{domain} returned an invalid invoice. Expected description_hash == {g.data['description_hash']}, got {invoice.description_hash}."
|
||||||
}
|
},
|
||||||
),
|
|
||||||
HTTPStatus.BAD_REQUEST,
|
HTTPStatus.BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -288,51 +241,49 @@ async def api_payments_pay_lnurl():
|
||||||
|
|
||||||
if params.get("successAction"):
|
if params.get("successAction"):
|
||||||
extra["success_action"] = params["successAction"]
|
extra["success_action"] = params["successAction"]
|
||||||
if g.data["comment"]:
|
if data.comment:
|
||||||
extra["comment"] = g.data["comment"]
|
extra["comment"] = data.comment
|
||||||
|
|
||||||
payment_hash = await pay_invoice(
|
payment_hash = await pay_invoice(
|
||||||
wallet_id=g.wallet.id,
|
wallet_id=g.wallet.id,
|
||||||
payment_request=params["pr"],
|
payment_request=params["pr"],
|
||||||
description=g.data.get("description", ""),
|
description=data.description,
|
||||||
extra=extra,
|
extra=extra,
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
jsonify(
|
|
||||||
{
|
{
|
||||||
"success_action": params.get("successAction"),
|
"success_action": params.get("successAction"),
|
||||||
"payment_hash": payment_hash,
|
"payment_hash": payment_hash,
|
||||||
# maintain backwards compatibility with API clients:
|
# maintain backwards compatibility with API clients:
|
||||||
"checking_id": payment_hash,
|
"checking_id": payment_hash,
|
||||||
}
|
},
|
||||||
),
|
|
||||||
HTTPStatus.CREATED,
|
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")
|
@api_check_wallet_key("invoice")
|
||||||
async def api_payment(payment_hash):
|
async def api_payment(payment_hash):
|
||||||
payment = await g.wallet.get_payment(payment_hash)
|
payment = await g.wallet.get_payment(payment_hash)
|
||||||
|
|
||||||
if not payment:
|
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:
|
elif not payment.pending:
|
||||||
return jsonify({"paid": True, "preimage": payment.preimage}), HTTPStatus.OK
|
return {"paid": True, "preimage": payment.preimage}, HTTPStatus.OK
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await payment.check_pending()
|
await payment.check_pending()
|
||||||
except Exception:
|
except Exception:
|
||||||
return jsonify({"paid": False}), HTTPStatus.OK
|
return {"paid": False}, HTTPStatus.OK
|
||||||
|
|
||||||
return (
|
return (
|
||||||
jsonify({"paid": not payment.pending, "preimage": payment.preimage}),
|
{"paid": not payment.pending, "preimage": payment.preimage},
|
||||||
HTTPStatus.OK,
|
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)
|
@api_check_wallet_key("invoice", accept_querystring=True)
|
||||||
async def api_payments_sse():
|
async def api_payments_sse():
|
||||||
this_wallet_id = g.wallet.id
|
this_wallet_id = g.wallet.id
|
||||||
|
@ -385,7 +336,7 @@ async def api_payments_sse():
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
@core_app.route("/api/v1/lnurlscan/<code>", methods=["GET"])
|
@core_app.get("/api/v1/lnurlscan/<code>")
|
||||||
@api_check_wallet_key("invoice")
|
@api_check_wallet_key("invoice")
|
||||||
async def api_lnurlscan(code: str):
|
async def api_lnurlscan(code: str):
|
||||||
try:
|
try:
|
||||||
|
@ -404,7 +355,7 @@ async def api_lnurlscan(code: str):
|
||||||
)
|
)
|
||||||
# will proceed with these values
|
# will proceed with these values
|
||||||
else:
|
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 is what will be returned to the client
|
||||||
params: Dict = {"domain": domain}
|
params: Dict = {"domain": domain}
|
||||||
|
@ -420,7 +371,7 @@ async def api_lnurlscan(code: str):
|
||||||
r = await client.get(url, timeout=5)
|
r = await client.get(url, timeout=5)
|
||||||
if r.is_error:
|
if r.is_error:
|
||||||
return (
|
return (
|
||||||
jsonify({"domain": domain, "message": "failed to get parameters"}),
|
{"domain": domain, "message": "failed to get parameters"},
|
||||||
HTTPStatus.SERVICE_UNAVAILABLE,
|
HTTPStatus.SERVICE_UNAVAILABLE,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -428,12 +379,10 @@ async def api_lnurlscan(code: str):
|
||||||
data = json.loads(r.text)
|
data = json.loads(r.text)
|
||||||
except json.decoder.JSONDecodeError:
|
except json.decoder.JSONDecodeError:
|
||||||
return (
|
return (
|
||||||
jsonify(
|
|
||||||
{
|
{
|
||||||
"domain": domain,
|
"domain": domain,
|
||||||
"message": f"got invalid response '{r.text[:200]}'",
|
"message": f"got invalid response '{r.text[:200]}'",
|
||||||
}
|
},
|
||||||
),
|
|
||||||
HTTPStatus.SERVICE_UNAVAILABLE,
|
HTTPStatus.SERVICE_UNAVAILABLE,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -441,9 +390,7 @@ async def api_lnurlscan(code: str):
|
||||||
tag = data["tag"]
|
tag = data["tag"]
|
||||||
if tag == "channelRequest":
|
if tag == "channelRequest":
|
||||||
return (
|
return (
|
||||||
jsonify(
|
{"domain": domain, "kind": "channel", "message": "unsupported"},
|
||||||
{"domain": domain, "kind": "channel", "message": "unsupported"}
|
|
||||||
),
|
|
||||||
HTTPStatus.BAD_REQUEST,
|
HTTPStatus.BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -490,32 +437,24 @@ async def api_lnurlscan(code: str):
|
||||||
params.update(commentAllowed=data.get("commentAllowed", 0))
|
params.update(commentAllowed=data.get("commentAllowed", 0))
|
||||||
except KeyError as exc:
|
except KeyError as exc:
|
||||||
return (
|
return (
|
||||||
jsonify(
|
|
||||||
{
|
{
|
||||||
"domain": domain,
|
"domain": domain,
|
||||||
"message": f"lnurl JSON response invalid: {exc}",
|
"message": f"lnurl JSON response invalid: {exc}",
|
||||||
}
|
},
|
||||||
),
|
|
||||||
HTTPStatus.SERVICE_UNAVAILABLE,
|
HTTPStatus.SERVICE_UNAVAILABLE,
|
||||||
)
|
)
|
||||||
|
return params
|
||||||
return jsonify(params)
|
|
||||||
|
|
||||||
|
|
||||||
@core_app.route("/api/v1/lnurlauth", methods=["POST"])
|
@core_app.post("/api/v1/lnurlauth", methods=["POST"])
|
||||||
@api_check_wallet_key("admin")
|
@api_check_wallet_key("admin")
|
||||||
@api_validate_post_request(
|
async def api_perform_lnurlauth(callback: str):
|
||||||
schema={
|
err = await perform_lnurlauth(callback)
|
||||||
"callback": {"type": "string", "required": True},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
async def api_perform_lnurlauth():
|
|
||||||
err = await perform_lnurlauth(g.data["callback"])
|
|
||||||
if err:
|
if err:
|
||||||
return jsonify({"reason": err.reason}), HTTPStatus.SERVICE_UNAVAILABLE
|
return {"reason": err.reason}, HTTPStatus.SERVICE_UNAVAILABLE
|
||||||
return "", HTTPStatus.OK
|
return "", HTTPStatus.OK
|
||||||
|
|
||||||
|
|
||||||
@core_app.route("/api/v1/currencies", methods=["GET"])
|
@core_app.route("/api/v1/currencies", methods=["GET"])
|
||||||
async def api_list_currencies_available():
|
async def api_list_currencies_available():
|
||||||
return jsonify(list(currencies.keys()))
|
return list(currencies.keys())
|
||||||
|
|
|
@ -4,7 +4,6 @@ from quart import (
|
||||||
g,
|
g,
|
||||||
current_app,
|
current_app,
|
||||||
abort,
|
abort,
|
||||||
jsonify,
|
|
||||||
request,
|
request,
|
||||||
redirect,
|
redirect,
|
||||||
render_template,
|
render_template,
|
||||||
|
@ -28,21 +27,21 @@ from ..crud import (
|
||||||
from ..services import redeem_lnurl_withdraw, pay_invoice
|
from ..services import redeem_lnurl_withdraw, pay_invoice
|
||||||
|
|
||||||
|
|
||||||
@core_app.route("/favicon.ico")
|
@core_app.get("/favicon.ico")
|
||||||
async def favicon():
|
async def favicon():
|
||||||
return await send_from_directory(
|
return await send_from_directory(
|
||||||
path.join(core_app.root_path, "static"), "favicon.ico"
|
path.join(core_app.root_path, "static"), "favicon.ico"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@core_app.route("/")
|
@core_app.get("/")
|
||||||
async def home():
|
async def home():
|
||||||
return await render_template(
|
return await render_template(
|
||||||
"core/index.html", lnurl=request.args.get("lightning", None)
|
"core/index.html", lnurl=request.args.get("lightning", None)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@core_app.route("/extensions")
|
@core_app.get("/extensions")
|
||||||
@validate_uuids(["usr"], required=True)
|
@validate_uuids(["usr"], required=True)
|
||||||
@check_user_exists()
|
@check_user_exists()
|
||||||
async def extensions():
|
async def extensions():
|
||||||
|
@ -66,7 +65,7 @@ async def extensions():
|
||||||
return await render_template("core/extensions.html", user=await get_user(g.user.id))
|
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"])
|
@validate_uuids(["usr", "wal"])
|
||||||
async def wallet():
|
async def wallet():
|
||||||
user_id = request.args.get("usr", type=str)
|
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)
|
@validate_uuids(["usr", "wal"], required=True)
|
||||||
async def lnurl_full_withdraw():
|
async def lnurl_full_withdraw():
|
||||||
user = await get_user(request.args.get("usr"))
|
user = await get_user(request.args.get("usr"))
|
||||||
if not user:
|
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"))
|
wallet = user.get_wallet(request.args.get("wal"))
|
||||||
if not wallet:
|
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",
|
"tag": "withdrawRequest",
|
||||||
"callback": url_for(
|
"callback": url_for(
|
||||||
"core.lnurl_full_withdraw_callback",
|
"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.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)
|
@validate_uuids(["usr", "wal"], required=True)
|
||||||
async def lnurl_full_withdraw_callback():
|
async def lnurl_full_withdraw_callback():
|
||||||
user = await get_user(request.args.get("usr"))
|
user = await get_user(request.args.get("usr"))
|
||||||
if not user:
|
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"))
|
wallet = user.get_wallet(request.args.get("wal"))
|
||||||
if not wallet:
|
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")
|
pr = request.args.get("pr")
|
||||||
|
|
||||||
|
@ -164,10 +161,10 @@ async def lnurl_full_withdraw_callback():
|
||||||
if balance_notify:
|
if balance_notify:
|
||||||
await save_balance_notify(wallet.id, 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)
|
@validate_uuids(["usr", "wal"], required=True)
|
||||||
@check_user_exists()
|
@check_user_exists()
|
||||||
async def deletewallet():
|
async def deletewallet():
|
||||||
|
@ -186,7 +183,7 @@ async def deletewallet():
|
||||||
return redirect(url_for("core.home"))
|
return redirect(url_for("core.home"))
|
||||||
|
|
||||||
|
|
||||||
@core_app.route("/withdraw/notify/<service>")
|
@core_app.get("/withdraw/notify/<service>")
|
||||||
@validate_uuids(["wal"], required=True)
|
@validate_uuids(["wal"], required=True)
|
||||||
async def lnurl_balance_notify(service: str):
|
async def lnurl_balance_notify(service: str):
|
||||||
bc = await get_balance_check(request.args.get("wal"), service)
|
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)
|
redeem_lnurl_withdraw(bc.wallet, bc.url)
|
||||||
|
|
||||||
|
|
||||||
@core_app.route("/lnurlwallet")
|
@core_app.get("/lnurlwallet")
|
||||||
async def lnurlwallet():
|
async def lnurlwallet():
|
||||||
async with db.connect() as conn:
|
async with db.connect() as conn:
|
||||||
account = await create_account(conn=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))
|
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):
|
async def manifest(usr: str):
|
||||||
user = await get_user(usr)
|
user = await get_user(usr)
|
||||||
if not user:
|
if not user:
|
||||||
return "", HTTPStatus.NOT_FOUND
|
return "", HTTPStatus.NOT_FOUND
|
||||||
|
|
||||||
return jsonify(
|
return {
|
||||||
{
|
|
||||||
"short_name": "LNbits",
|
"short_name": "LNbits",
|
||||||
"name": "LNbits Wallet",
|
"name": "LNbits Wallet",
|
||||||
"icons": [
|
"icons": [
|
||||||
|
@ -244,6 +240,4 @@ async def manifest(usr: str):
|
||||||
"url": "/wallet?usr=" + usr + "&wal=" + wallet.id,
|
"url": "/wallet?usr=" + usr + "&wal=" + wallet.id,
|
||||||
}
|
}
|
||||||
for wallet in user.wallets
|
for wallet in user.wallets
|
||||||
],
|
],}
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
|
@ -10,22 +10,22 @@ from ..crud import get_standalone_payment
|
||||||
from ..tasks import api_invoice_listeners
|
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):
|
async def api_public_payment_longpolling(payment_hash):
|
||||||
payment = await get_standalone_payment(payment_hash)
|
payment = await get_standalone_payment(payment_hash)
|
||||||
|
|
||||||
if not payment:
|
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:
|
elif not payment.pending:
|
||||||
return jsonify({"status": "paid"}), HTTPStatus.OK
|
return {"status": "paid"}, HTTPStatus.OK
|
||||||
|
|
||||||
try:
|
try:
|
||||||
invoice = bolt11.decode(payment.bolt11)
|
invoice = bolt11.decode(payment.bolt11)
|
||||||
expiration = datetime.datetime.fromtimestamp(invoice.date + invoice.expiry)
|
expiration = datetime.datetime.fromtimestamp(invoice.date + invoice.expiry)
|
||||||
if expiration < datetime.datetime.now():
|
if expiration < datetime.datetime.now():
|
||||||
return jsonify({"status": "expired"}), HTTPStatus.OK
|
return {"status": "expired"}, HTTPStatus.OK
|
||||||
except:
|
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)
|
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:
|
async for payment in receive_payment:
|
||||||
if payment.payment_hash == payment_hash:
|
if payment.payment_hash == payment_hash:
|
||||||
nonlocal response
|
nonlocal response
|
||||||
response = (jsonify({"status": "paid"}), HTTPStatus.OK)
|
response = ({"status": "paid"}, HTTPStatus.OK)
|
||||||
cancel_scope.cancel()
|
cancel_scope.cancel()
|
||||||
|
|
||||||
async def timeouter(cancel_scope):
|
async def timeouter(cancel_scope):
|
||||||
|
@ -52,4 +52,4 @@ async def api_public_payment_longpolling(payment_hash):
|
||||||
if response:
|
if response:
|
||||||
return response
|
return response
|
||||||
else:
|
else:
|
||||||
return jsonify({"message": "timeout"}), HTTPStatus.REQUEST_TIMEOUT
|
return {"message": "timeout"}, HTTPStatus.REQUEST_TIMEOUT
|
||||||
|
|
|
@ -4,6 +4,9 @@ from http import HTTPStatus
|
||||||
from lnbits.core.crud import get_user, get_wallet
|
from lnbits.core.crud import get_user, get_wallet
|
||||||
from lnbits.core.services import create_invoice, check_invoice_status
|
from lnbits.core.services import create_invoice, check_invoice_status
|
||||||
from lnbits.decorators import api_check_wallet_key, api_validate_post_request
|
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 . import events_ext
|
||||||
from .crud import (
|
from .crud import (
|
||||||
|
@ -25,7 +28,8 @@ from .crud import (
|
||||||
# Events
|
# Events
|
||||||
|
|
||||||
|
|
||||||
@events_ext.route("/api/v1/events", methods=["GET"])
|
|
||||||
|
@events_ext.get("/api/v1/events")
|
||||||
@api_check_wallet_key("invoice")
|
@api_check_wallet_key("invoice")
|
||||||
async def api_events():
|
async def api_events():
|
||||||
wallet_ids = [g.wallet.id]
|
wallet_ids = [g.wallet.id]
|
||||||
|
@ -34,51 +38,48 @@ async def api_events():
|
||||||
wallet_ids = (await get_user(g.wallet.user)).wallet_ids
|
wallet_ids = (await get_user(g.wallet.user)).wallet_ids
|
||||||
|
|
||||||
return (
|
return (
|
||||||
jsonify([event._asdict() for event in await get_events(wallet_ids)]),
|
[event._asdict() for event in await get_events(wallet_ids)],
|
||||||
HTTPStatus.OK,
|
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.post("/api/v1/events")
|
||||||
@events_ext.route("/api/v1/events/<event_id>", methods=["PUT"])
|
@events_ext.put("/api/v1/events/<event_id>")
|
||||||
@api_check_wallet_key("invoice")
|
@api_check_wallet_key("invoice")
|
||||||
@api_validate_post_request(
|
async def api_event_create(data: CreateData, event_id=None):
|
||||||
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):
|
|
||||||
if event_id:
|
if event_id:
|
||||||
event = await get_event(event_id)
|
event = await get_event(event_id)
|
||||||
if not event:
|
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:
|
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:
|
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")
|
@api_check_wallet_key("invoice")
|
||||||
async def api_form_delete(event_id):
|
async def api_form_delete(event_id):
|
||||||
event = await get_event(event_id)
|
event = await get_event(event_id)
|
||||||
if not event:
|
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:
|
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)
|
await delete_event(event_id)
|
||||||
return "", HTTPStatus.NO_CONTENT
|
return "", HTTPStatus.NO_CONTENT
|
||||||
|
@ -87,7 +88,7 @@ async def api_form_delete(event_id):
|
||||||
#########Tickets##########
|
#########Tickets##########
|
||||||
|
|
||||||
|
|
||||||
@events_ext.route("/api/v1/tickets", methods=["GET"])
|
@events_ext.get("/api/v1/tickets")
|
||||||
@api_check_wallet_key("invoice")
|
@api_check_wallet_key("invoice")
|
||||||
async def api_tickets():
|
async def api_tickets():
|
||||||
wallet_ids = [g.wallet.id]
|
wallet_ids = [g.wallet.id]
|
||||||
|
@ -96,22 +97,19 @@ async def api_tickets():
|
||||||
wallet_ids = (await get_user(g.wallet.user)).wallet_ids
|
wallet_ids = (await get_user(g.wallet.user)).wallet_ids
|
||||||
|
|
||||||
return (
|
return (
|
||||||
jsonify([ticket._asdict() for ticket in await get_tickets(wallet_ids)]),
|
[ticket._asdict() for ticket in await get_tickets(wallet_ids)],
|
||||||
HTTPStatus.OK,
|
HTTPStatus.OK,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
class CreateTicketData(BaseModel):
|
||||||
|
name: str = Query(...)
|
||||||
|
email: str
|
||||||
|
|
||||||
@events_ext.route("/api/v1/tickets/<event_id>/<sats>", methods=["POST"])
|
@events_ext.post("/api/v1/tickets/<event_id>/<sats>")
|
||||||
@api_validate_post_request(
|
async def api_ticket_make_ticket(data: CreateTicketData, event_id, sats):
|
||||||
schema={
|
|
||||||
"name": {"type": "string", "empty": False, "required": True},
|
|
||||||
"email": {"type": "string", "empty": False, "required": True},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
async def api_ticket_make_ticket(event_id, sats):
|
|
||||||
event = await get_event(event_id)
|
event = await get_event(event_id)
|
||||||
if not event:
|
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:
|
try:
|
||||||
payment_hash, payment_request = await create_invoice(
|
payment_hash, payment_request = await create_invoice(
|
||||||
wallet_id=event.wallet,
|
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
|
return jsonify({"message": str(e)}), HTTPStatus.INTERNAL_SERVER_ERROR
|
||||||
|
|
||||||
ticket = await create_ticket(
|
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:
|
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 (
|
return (
|
||||||
jsonify({"payment_hash": payment_hash, "payment_request": payment_request}),
|
jsonable_encoder({"payment_hash": payment_hash, "payment_request": payment_request}),
|
||||||
HTTPStatus.OK,
|
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):
|
async def api_ticket_send_ticket(payment_hash):
|
||||||
ticket = await get_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)
|
status = await check_invoice_status(ticket.wallet, payment_hash)
|
||||||
is_paid = not status.pending
|
is_paid = not status.pending
|
||||||
except Exception:
|
except Exception:
|
||||||
return jsonify({"message": "Not paid."}), HTTPStatus.NOT_FOUND
|
return jsonable_encoder({"message": "Not paid."}), HTTPStatus.NOT_FOUND
|
||||||
|
|
||||||
if is_paid:
|
if is_paid:
|
||||||
wallet = await get_wallet(ticket.wallet)
|
wallet = await get_wallet(ticket.wallet)
|
||||||
|
@ -151,21 +149,21 @@ async def api_ticket_send_ticket(payment_hash):
|
||||||
await payment.set_pending(False)
|
await payment.set_pending(False)
|
||||||
ticket = await set_ticket_paid(payment_hash=payment_hash)
|
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")
|
@api_check_wallet_key("invoice")
|
||||||
async def api_ticket_delete(ticket_id):
|
async def api_ticket_delete(ticket_id):
|
||||||
ticket = await get_ticket(ticket_id)
|
ticket = await get_ticket(ticket_id)
|
||||||
|
|
||||||
if not ticket:
|
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:
|
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)
|
await delete_ticket(ticket_id)
|
||||||
return "", HTTPStatus.NO_CONTENT
|
return "", HTTPStatus.NO_CONTENT
|
||||||
|
@ -174,34 +172,26 @@ async def api_ticket_delete(ticket_id):
|
||||||
# Event Tickets
|
# 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):
|
async def api_event_tickets(wallet_id, event_id):
|
||||||
return (
|
return ([ticket._asdict() for ticket in await get_event_tickets(wallet_id=wallet_id, event_id=event_id)],
|
||||||
jsonify(
|
|
||||||
[
|
|
||||||
ticket._asdict()
|
|
||||||
for ticket in await get_event_tickets(
|
|
||||||
wallet_id=wallet_id, event_id=event_id
|
|
||||||
)
|
|
||||||
]
|
|
||||||
),
|
|
||||||
HTTPStatus.OK,
|
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):
|
async def api_event_register_ticket(ticket_id):
|
||||||
ticket = await get_ticket(ticket_id)
|
ticket = await get_ticket(ticket_id)
|
||||||
if not ticket:
|
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:
|
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:
|
if ticket.registered == True:
|
||||||
return jsonify({"message": "Ticket already registered"}), HTTPStatus.FORBIDDEN
|
return jsonable_encoder({"message": "Ticket already registered"}), HTTPStatus.FORBIDDEN
|
||||||
|
|
||||||
return (
|
return (
|
||||||
jsonify([ticket._asdict() for ticket in await reg_ticket(ticket_id)]),
|
[ticket._asdict() for ticket in await reg_ticket(ticket_id)],
|
||||||
HTTPStatus.OK,
|
HTTPStatus.OK,
|
||||||
)
|
)
|
||||||
|
|
|
@ -12,14 +12,14 @@ from .crud import get_jukebox
|
||||||
from .views_api import api_get_jukebox_device_check
|
from .views_api import api_get_jukebox_device_check
|
||||||
|
|
||||||
|
|
||||||
@jukebox_ext.route("/")
|
@jukebox_ext.get("/")
|
||||||
@validate_uuids(["usr"], required=True)
|
@validate_uuids(["usr"], required=True)
|
||||||
@check_user_exists()
|
@check_user_exists()
|
||||||
async def index():
|
async def index():
|
||||||
return await render_template("jukebox/index.html", user=g.user)
|
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):
|
async def connect_to_jukebox(juke_id):
|
||||||
jukebox = await get_jukebox(juke_id)
|
jukebox = await get_jukebox(juke_id)
|
||||||
if not jukebox:
|
if not jukebox:
|
||||||
|
|
|
@ -4,6 +4,9 @@ import base64
|
||||||
from lnbits.core.crud import get_wallet
|
from lnbits.core.crud import get_wallet
|
||||||
from lnbits.core.services import create_invoice, check_invoice_status
|
from lnbits.core.services import create_invoice, check_invoice_status
|
||||||
import json
|
import json
|
||||||
|
from typing import Optional
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
from lnbits.decorators import api_check_wallet_key, api_validate_post_request
|
from lnbits.decorators import api_check_wallet_key, api_validate_post_request
|
||||||
import httpx
|
import httpx
|
||||||
|
@ -19,16 +22,15 @@ from .crud import (
|
||||||
update_jukebox_payment,
|
update_jukebox_payment,
|
||||||
)
|
)
|
||||||
from lnbits.core.services import create_invoice, check_invoice_status
|
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")
|
@api_check_wallet_key("admin")
|
||||||
async def api_get_jukeboxs():
|
async def api_get_jukeboxs():
|
||||||
try:
|
try:
|
||||||
return (
|
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,
|
HTTPStatus.OK,
|
||||||
)
|
)
|
||||||
except:
|
except:
|
||||||
|
@ -38,7 +40,7 @@ async def api_get_jukeboxs():
|
||||||
##################SPOTIFY AUTH#####################
|
##################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):
|
async def api_check_credentials_callbac(juke_id):
|
||||||
sp_code = ""
|
sp_code = ""
|
||||||
sp_access_token = ""
|
sp_access_token = ""
|
||||||
|
@ -47,7 +49,7 @@ async def api_check_credentials_callbac(juke_id):
|
||||||
jukebox = await get_jukebox(juke_id)
|
jukebox = await get_jukebox(juke_id)
|
||||||
except:
|
except:
|
||||||
return (
|
return (
|
||||||
jsonify({"error": "No Jukebox"}),
|
{"error": "No Jukebox"},
|
||||||
HTTPStatus.FORBIDDEN,
|
HTTPStatus.FORBIDDEN,
|
||||||
)
|
)
|
||||||
if request.args.get("code"):
|
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>"
|
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")
|
@api_check_wallet_key("admin")
|
||||||
async def api_check_credentials_check(juke_id):
|
async def api_check_credentials_check(juke_id):
|
||||||
jukebox = await get_jukebox(juke_id)
|
jukebox = await get_jukebox(juke_id)
|
||||||
return jsonify(jukebox._asdict()), HTTPStatus.CREATED
|
return jsonify(jukebox._asdict()), HTTPStatus.CREATED
|
||||||
|
|
||||||
|
|
||||||
@jukebox_ext.route("/api/v1/jukebox/", methods=["POST"])
|
class CreateData(BaseModel):
|
||||||
@jukebox_ext.route("/api/v1/jukebox/<juke_id>", methods=["PUT"])
|
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_check_wallet_key("admin")
|
||||||
@api_validate_post_request(
|
async def api_create_update_jukebox(data: CreateData, juke_id=None):
|
||||||
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):
|
|
||||||
if juke_id:
|
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:
|
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")
|
@api_check_wallet_key("admin")
|
||||||
async def api_delete_item(juke_id):
|
async def api_delete_item(juke_id):
|
||||||
await delete_jukebox(juke_id)
|
await delete_jukebox(juke_id)
|
||||||
try:
|
try:
|
||||||
return (
|
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,
|
HTTPStatus.OK,
|
||||||
)
|
)
|
||||||
except:
|
except:
|
||||||
|
@ -120,15 +119,15 @@ async def api_delete_item(juke_id):
|
||||||
######GET ACCESS TOKEN######
|
######GET ACCESS TOKEN######
|
||||||
|
|
||||||
|
|
||||||
@jukebox_ext.route(
|
@jukebox_ext.get(
|
||||||
"/api/v1/jukebox/jb/playlist/<juke_id>/<sp_playlist>", methods=["GET"]
|
"/api/v1/jukebox/jb/playlist/<juke_id>/<sp_playlist>"
|
||||||
)
|
)
|
||||||
async def api_get_jukebox_song(juke_id, sp_playlist, retry=False):
|
async def api_get_jukebox_song(juke_id, sp_playlist, retry=False):
|
||||||
try:
|
try:
|
||||||
jukebox = await get_jukebox(juke_id)
|
jukebox = await get_jukebox(juke_id)
|
||||||
except:
|
except:
|
||||||
return (
|
return (
|
||||||
jsonify({"error": "No Jukebox"}),
|
jsonable_encoder({"error": "No Jukebox"}),
|
||||||
HTTPStatus.FORBIDDEN,
|
HTTPStatus.FORBIDDEN,
|
||||||
)
|
)
|
||||||
tracks = []
|
tracks = []
|
||||||
|
@ -146,7 +145,7 @@ async def api_get_jukebox_song(juke_id, sp_playlist, retry=False):
|
||||||
return False
|
return False
|
||||||
elif retry:
|
elif retry:
|
||||||
return (
|
return (
|
||||||
jsonify({"error": "Failed to get auth"}),
|
jsonable_encoder({"error": "Failed to get auth"}),
|
||||||
HTTPStatus.FORBIDDEN,
|
HTTPStatus.FORBIDDEN,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
@ -166,7 +165,7 @@ async def api_get_jukebox_song(juke_id, sp_playlist, retry=False):
|
||||||
)
|
)
|
||||||
except AssertionError:
|
except AssertionError:
|
||||||
something = None
|
something = None
|
||||||
return jsonify([track for track in tracks])
|
return [track for track in tracks]
|
||||||
|
|
||||||
|
|
||||||
async def api_get_token(juke_id):
|
async def api_get_token(juke_id):
|
||||||
|
@ -174,7 +173,7 @@ async def api_get_token(juke_id):
|
||||||
jukebox = await get_jukebox(juke_id)
|
jukebox = await get_jukebox(juke_id)
|
||||||
except:
|
except:
|
||||||
return (
|
return (
|
||||||
jsonify({"error": "No Jukebox"}),
|
jsonable_encoder({"error": "No Jukebox"}),
|
||||||
HTTPStatus.FORBIDDEN,
|
HTTPStatus.FORBIDDEN,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -211,13 +210,13 @@ async def api_get_token(juke_id):
|
||||||
######CHECK DEVICE
|
######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):
|
async def api_get_jukebox_device_check(juke_id, retry=False):
|
||||||
try:
|
try:
|
||||||
jukebox = await get_jukebox(juke_id)
|
jukebox = await get_jukebox(juke_id)
|
||||||
except:
|
except:
|
||||||
return (
|
return (
|
||||||
jsonify({"error": "No Jukebox"}),
|
{"error": "No Jukebox"},
|
||||||
HTTPStatus.FORBIDDEN,
|
HTTPStatus.FORBIDDEN,
|
||||||
)
|
)
|
||||||
async with httpx.AsyncClient() as client:
|
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)
|
token = await api_get_token(juke_id)
|
||||||
if token == False:
|
if token == False:
|
||||||
return (
|
return (
|
||||||
jsonify({"error": "No device connected"}),
|
jsonable_encoder({"error": "No device connected"}),
|
||||||
HTTPStatus.FORBIDDEN,
|
HTTPStatus.FORBIDDEN,
|
||||||
)
|
)
|
||||||
elif retry:
|
elif retry:
|
||||||
return (
|
return (
|
||||||
jsonify({"error": "Failed to get auth"}),
|
jsonable_encoder({"error": "Failed to get auth"}),
|
||||||
HTTPStatus.FORBIDDEN,
|
HTTPStatus.FORBIDDEN,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return api_get_jukebox_device_check(juke_id, retry=True)
|
return api_get_jukebox_device_check(juke_id, retry=True)
|
||||||
else:
|
else:
|
||||||
return (
|
return (
|
||||||
jsonify({"error": "No device connected"}),
|
jsonable_encoder({"error": "No device connected"}),
|
||||||
HTTPStatus.FORBIDDEN,
|
HTTPStatus.FORBIDDEN,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -256,13 +255,13 @@ async def api_get_jukebox_device_check(juke_id, retry=False):
|
||||||
######GET INVOICE STUFF
|
######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):
|
async def api_get_jukebox_invoice(juke_id, song_id):
|
||||||
try:
|
try:
|
||||||
jukebox = await get_jukebox(juke_id)
|
jukebox = await get_jukebox(juke_id)
|
||||||
except:
|
except:
|
||||||
return (
|
return (
|
||||||
jsonify({"error": "No Jukebox"}),
|
{"error": "No Jukebox"},
|
||||||
HTTPStatus.FORBIDDEN,
|
HTTPStatus.FORBIDDEN,
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
|
@ -274,12 +273,12 @@ async def api_get_jukebox_invoice(juke_id, song_id):
|
||||||
deviceConnected = True
|
deviceConnected = True
|
||||||
if not deviceConnected:
|
if not deviceConnected:
|
||||||
return (
|
return (
|
||||||
jsonify({"error": "No device connected"}),
|
{"error": "No device connected"},
|
||||||
HTTPStatus.NOT_FOUND,
|
HTTPStatus.NOT_FOUND,
|
||||||
)
|
)
|
||||||
except:
|
except:
|
||||||
return (
|
return (
|
||||||
jsonify({"error": "No device connected"}),
|
{"error": "No device connected"},
|
||||||
HTTPStatus.NOT_FOUND,
|
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)
|
jukebox_payment = await create_jukebox_payment(song_id, invoice[0], juke_id)
|
||||||
|
|
||||||
return jsonify(invoice, jukebox_payment)
|
return invoice, jukebox_payment
|
||||||
|
|
||||||
|
|
||||||
@jukebox_ext.route(
|
@jukebox_ext.get(
|
||||||
"/api/v1/jukebox/jb/checkinvoice/<pay_hash>/<juke_id>", methods=["GET"]
|
"/api/v1/jukebox/jb/checkinvoice/<pay_hash>/<juke_id>"
|
||||||
)
|
)
|
||||||
async def api_get_jukebox_invoice_check(pay_hash, juke_id):
|
async def api_get_jukebox_invoice_check(pay_hash, juke_id):
|
||||||
try:
|
try:
|
||||||
jukebox = await get_jukebox(juke_id)
|
jukebox = await get_jukebox(juke_id)
|
||||||
except:
|
except:
|
||||||
return (
|
return (
|
||||||
jsonify({"error": "No Jukebox"}),
|
{"error": "No Jukebox"},
|
||||||
HTTPStatus.FORBIDDEN,
|
HTTPStatus.FORBIDDEN,
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
status = await check_invoice_status(jukebox.wallet, pay_hash)
|
status = await check_invoice_status(jukebox.wallet, pay_hash)
|
||||||
is_paid = not status.pending
|
is_paid = not status.pending
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
return jsonify({"paid": False}), HTTPStatus.OK
|
return {"paid": False}, HTTPStatus.OK
|
||||||
if is_paid:
|
if is_paid:
|
||||||
wallet = await get_wallet(jukebox.wallet)
|
wallet = await get_wallet(jukebox.wallet)
|
||||||
payment = await wallet.get_payment(pay_hash)
|
payment = await wallet.get_payment(pay_hash)
|
||||||
await payment.set_pending(False)
|
await payment.set_pending(False)
|
||||||
await update_jukebox_payment(pay_hash, paid=True)
|
await update_jukebox_payment(pay_hash, paid=True)
|
||||||
return jsonify({"paid": True}), HTTPStatus.OK
|
return {"paid": True}, HTTPStatus.OK
|
||||||
return jsonify({"paid": False}), HTTPStatus.OK
|
return {"paid": False}, HTTPStatus.OK
|
||||||
|
|
||||||
|
|
||||||
@jukebox_ext.route(
|
@jukebox_ext.get(
|
||||||
"/api/v1/jukebox/jb/invoicep/<song_id>/<juke_id>/<pay_hash>", methods=["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):
|
async def api_get_jukebox_invoice_paid(song_id, juke_id, pay_hash, retry=False):
|
||||||
try:
|
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},
|
headers={"Authorization": "Bearer " + jukebox.sp_access_token},
|
||||||
)
|
)
|
||||||
if r.status_code == 204:
|
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:
|
elif r.status_code == 401 or r.status_code == 403:
|
||||||
token = await api_get_token(juke_id)
|
token = await api_get_token(juke_id)
|
||||||
if token == False:
|
if token == False:
|
||||||
return (
|
return (
|
||||||
jsonify({"error": "Invoice not paid"}),
|
{"error": "Invoice not paid"},
|
||||||
HTTPStatus.FORBIDDEN,
|
HTTPStatus.FORBIDDEN,
|
||||||
)
|
)
|
||||||
elif retry:
|
elif retry:
|
||||||
return (
|
return (
|
||||||
jsonify({"error": "Failed to get auth"}),
|
{"error": "Failed to get auth"},
|
||||||
HTTPStatus.FORBIDDEN,
|
HTTPStatus.FORBIDDEN,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
@ -379,7 +378,7 @@ async def api_get_jukebox_invoice_paid(song_id, juke_id, pay_hash, retry=False):
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return (
|
return (
|
||||||
jsonify({"error": "Invoice not paid"}),
|
{"error": "Invoice not paid"},
|
||||||
HTTPStatus.FORBIDDEN,
|
HTTPStatus.FORBIDDEN,
|
||||||
)
|
)
|
||||||
elif r.status_code == 200:
|
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},
|
headers={"Authorization": "Bearer " + jukebox.sp_access_token},
|
||||||
)
|
)
|
||||||
if r.status_code == 204:
|
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:
|
elif r.status_code == 401 or r.status_code == 403:
|
||||||
token = await api_get_token(juke_id)
|
token = await api_get_token(juke_id)
|
||||||
if token == False:
|
if token == False:
|
||||||
return (
|
return (
|
||||||
jsonify({"error": "Invoice not paid"}),
|
{"error": "Invoice not paid"},
|
||||||
HTTPStatus.OK,
|
HTTPStatus.OK,
|
||||||
)
|
)
|
||||||
elif retry:
|
elif retry:
|
||||||
return (
|
return (
|
||||||
jsonify({"error": "Failed to get auth"}),
|
{"error": "Failed to get auth"},
|
||||||
HTTPStatus.FORBIDDEN,
|
HTTPStatus.FORBIDDEN,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
@ -413,38 +412,38 @@ async def api_get_jukebox_invoice_paid(song_id, juke_id, pay_hash, retry=False):
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return (
|
return (
|
||||||
jsonify({"error": "Invoice not paid"}),
|
{"error": "Invoice not paid"},
|
||||||
HTTPStatus.OK,
|
HTTPStatus.OK,
|
||||||
)
|
)
|
||||||
elif r.status_code == 401 or r.status_code == 403:
|
elif r.status_code == 401 or r.status_code == 403:
|
||||||
token = await api_get_token(juke_id)
|
token = await api_get_token(juke_id)
|
||||||
if token == False:
|
if token == False:
|
||||||
return (
|
return (
|
||||||
jsonify({"error": "Invoice not paid"}),
|
{"error": "Invoice not paid"},
|
||||||
HTTPStatus.OK,
|
HTTPStatus.OK,
|
||||||
)
|
)
|
||||||
elif retry:
|
elif retry:
|
||||||
return (
|
return (
|
||||||
jsonify({"error": "Failed to get auth"}),
|
{"error": "Failed to get auth"},
|
||||||
HTTPStatus.FORBIDDEN,
|
HTTPStatus.FORBIDDEN,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return await api_get_jukebox_invoice_paid(
|
return await api_get_jukebox_invoice_paid(
|
||||||
song_id, juke_id, pay_hash
|
song_id, juke_id, pay_hash
|
||||||
)
|
)
|
||||||
return jsonify({"error": "Invoice not paid"}), HTTPStatus.OK
|
return {"error": "Invoice not paid"}, HTTPStatus.OK
|
||||||
|
|
||||||
|
|
||||||
############################GET TRACKS
|
############################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):
|
async def api_get_jukebox_currently(juke_id, retry=False):
|
||||||
try:
|
try:
|
||||||
jukebox = await get_jukebox(juke_id)
|
jukebox = await get_jukebox(juke_id)
|
||||||
except:
|
except:
|
||||||
return (
|
return (
|
||||||
jsonify({"error": "No Jukebox"}),
|
{"error": "No Jukebox"},
|
||||||
HTTPStatus.FORBIDDEN,
|
HTTPStatus.FORBIDDEN,
|
||||||
)
|
)
|
||||||
async with httpx.AsyncClient() as client:
|
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},
|
headers={"Authorization": "Bearer " + jukebox.sp_access_token},
|
||||||
)
|
)
|
||||||
if r.status_code == 204:
|
if r.status_code == 204:
|
||||||
return jsonify({"error": "Nothing"}), HTTPStatus.OK
|
return {"error": "Nothing"}, HTTPStatus.OK
|
||||||
elif r.status_code == 200:
|
elif r.status_code == 200:
|
||||||
try:
|
try:
|
||||||
response = r.json()
|
response = r.json()
|
||||||
|
@ -467,25 +466,25 @@ async def api_get_jukebox_currently(juke_id, retry=False):
|
||||||
"artist": response["item"]["artists"][0]["name"],
|
"artist": response["item"]["artists"][0]["name"],
|
||||||
"image": response["item"]["album"]["images"][0]["url"],
|
"image": response["item"]["album"]["images"][0]["url"],
|
||||||
}
|
}
|
||||||
return jsonify(track), HTTPStatus.OK
|
return track, HTTPStatus.OK
|
||||||
except:
|
except:
|
||||||
return jsonify("Something went wrong"), HTTPStatus.NOT_FOUND
|
return "Something went wrong", HTTPStatus.NOT_FOUND
|
||||||
|
|
||||||
elif r.status_code == 401:
|
elif r.status_code == 401:
|
||||||
token = await api_get_token(juke_id)
|
token = await api_get_token(juke_id)
|
||||||
if token == False:
|
if token == False:
|
||||||
return (
|
return (
|
||||||
jsonify({"error": "Invoice not paid"}),
|
{"error": "Invoice not paid"},
|
||||||
HTTPStatus.FORBIDDEN,
|
HTTPStatus.FORBIDDEN,
|
||||||
)
|
)
|
||||||
elif retry:
|
elif retry:
|
||||||
return (
|
return (
|
||||||
jsonify({"error": "Failed to get auth"}),
|
{"error": "Failed to get auth"},
|
||||||
HTTPStatus.FORBIDDEN,
|
HTTPStatus.FORBIDDEN,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return await api_get_jukebox_currently(juke_id, retry=True)
|
return await api_get_jukebox_currently(juke_id, retry=True)
|
||||||
else:
|
else:
|
||||||
return jsonify("Something went wrong"), HTTPStatus.NOT_FOUND
|
return "Something went wrong", HTTPStatus.NOT_FOUND
|
||||||
except AssertionError:
|
except AssertionError:
|
||||||
return jsonify("Something went wrong"), HTTPStatus.NOT_FOUND
|
return "Something went wrong", HTTPStatus.NOT_FOUND
|
||||||
|
|
|
@ -10,8 +10,9 @@ from .crud import create_paywall, get_paywall, get_paywalls, delete_paywall
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from fastapi import FastAPI, Query
|
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")
|
@api_check_wallet_key("invoice")
|
||||||
async def api_paywalls():
|
async def api_paywalls():
|
||||||
wallet_ids = [g.wallet.id]
|
wallet_ids = [g.wallet.id]
|
||||||
|
@ -20,57 +21,54 @@ async def api_paywalls():
|
||||||
wallet_ids = (await get_user(g.wallet.user)).wallet_ids
|
wallet_ids = (await get_user(g.wallet.user)).wallet_ids
|
||||||
|
|
||||||
return (
|
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,
|
HTTPStatus.OK,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class CreateData(BaseModel):
|
class CreateData(BaseModel):
|
||||||
url: Optional[str] = Query(...),
|
url: Optional[str] = Query(...)
|
||||||
memo: Optional[str] = Query(...),
|
memo: Optional[str] = Query(...)
|
||||||
description: str = Query(None),
|
description: str = Query(None)
|
||||||
amount: int = Query(None),
|
amount: int = Query(None)
|
||||||
remembers: bool = 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")
|
@api_check_wallet_key("invoice")
|
||||||
async def api_paywall_create(data: CreateData):
|
async def api_paywall_create(data: CreateData):
|
||||||
paywall = await create_paywall(wallet_id=g.wallet.id, **data)
|
paywall = await create_paywall(wallet_id=g.wallet.id, **data)
|
||||||
return paywall, HTTPStatus.CREATED
|
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")
|
@api_check_wallet_key("invoice")
|
||||||
async def api_paywall_delete(paywall_id):
|
async def api_paywall_delete(paywall_id):
|
||||||
paywall = await get_paywall(paywall_id)
|
paywall = await get_paywall(paywall_id)
|
||||||
|
|
||||||
if not paywall:
|
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:
|
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)
|
await delete_paywall(paywall_id)
|
||||||
|
|
||||||
return "", HTTPStatus.NO_CONTENT
|
return "", HTTPStatus.NO_CONTENT
|
||||||
|
|
||||||
|
|
||||||
@paywall_ext.route("/api/v1/paywalls/<paywall_id>/invoice", methods=["POST"])
|
@paywall_ext.post("/api/v1/paywalls/<paywall_id>/invoice")
|
||||||
@api_validate_post_request(
|
async def api_paywall_create_invoice(amount: int = Query(..., ge=1), paywall_id = None):
|
||||||
schema={"amount": {"type": "integer", "min": 1, "required": True}}
|
|
||||||
)
|
|
||||||
async def api_paywall_create_invoice(paywall_id):
|
|
||||||
paywall = await get_paywall(paywall_id)
|
paywall = await get_paywall(paywall_id)
|
||||||
|
|
||||||
if g.data["amount"] < paywall.amount:
|
if amount < paywall.amount:
|
||||||
return (
|
return (
|
||||||
jsonify({"message": f"Minimum amount is {paywall.amount} sat."}),
|
jsonable_encoder({"message": f"Minimum amount is {paywall.amount} sat."}),
|
||||||
HTTPStatus.BAD_REQUEST,
|
HTTPStatus.BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
amount = (
|
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(
|
payment_hash, payment_request = await create_invoice(
|
||||||
wallet_id=paywall.wallet,
|
wallet_id=paywall.wallet,
|
||||||
|
@ -79,38 +77,35 @@ async def api_paywall_create_invoice(paywall_id):
|
||||||
extra={"tag": "paywall"},
|
extra={"tag": "paywall"},
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({"message": str(e)}), HTTPStatus.INTERNAL_SERVER_ERROR
|
return jsonable_encoder({"message": str(e)}), HTTPStatus.INTERNAL_SERVER_ERROR
|
||||||
|
|
||||||
return (
|
return (
|
||||||
jsonify({"payment_hash": payment_hash, "payment_request": payment_request}),
|
jsonable_encoder({"payment_hash": payment_hash, "payment_request": payment_request}),
|
||||||
HTTPStatus.CREATED,
|
HTTPStatus.CREATED,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@paywall_ext.route("/api/v1/paywalls/<paywall_id>/check_invoice", methods=["POST"])
|
@paywall_ext.post("/api/v1/paywalls/<paywall_id>/check_invoice")
|
||||||
@api_validate_post_request(
|
async def api_paywal_check_invoice(payment_hash: str = Query(...), paywall_id = None):
|
||||||
schema={"payment_hash": {"type": "string", "empty": False, "required": True}}
|
|
||||||
)
|
|
||||||
async def api_paywal_check_invoice(paywall_id):
|
|
||||||
paywall = await get_paywall(paywall_id)
|
paywall = await get_paywall(paywall_id)
|
||||||
|
|
||||||
if not paywall:
|
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:
|
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
|
is_paid = not status.pending
|
||||||
except Exception:
|
except Exception:
|
||||||
return jsonify({"paid": False}), HTTPStatus.OK
|
return jsonable_encoder({"paid": False}), HTTPStatus.OK
|
||||||
|
|
||||||
if is_paid:
|
if is_paid:
|
||||||
wallet = await get_wallet(paywall.wallet)
|
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)
|
await payment.set_pending(False)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
jsonify({"paid": True, "url": paywall.url, "remembers": paywall.remembers}),
|
jsonable_encoder({"paid": True, "url": paywall.url, "remembers": paywall.remembers}),
|
||||||
HTTPStatus.OK,
|
HTTPStatus.OK,
|
||||||
)
|
)
|
||||||
|
|
||||||
return jsonify({"paid": False}), HTTPStatus.OK
|
return jsonable_encoder({"paid": False}), HTTPStatus.OK
|
||||||
|
|
15
lnbits/extensions/tpos/README.md
Normal file
15
lnbits/extensions/tpos/README.md
Normal 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\
|
||||||
|

|
||||||
|
3. Open TPOS on the browser\
|
||||||
|

|
||||||
|
4. Present invoice QR to costumer\
|
||||||
|

|
12
lnbits/extensions/tpos/__init__.py
Normal file
12
lnbits/extensions/tpos/__init__.py
Normal 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
|
6
lnbits/extensions/tpos/config.json
Normal file
6
lnbits/extensions/tpos/config.json
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"name": "TPoS",
|
||||||
|
"short_description": "A shareable PoS terminal!",
|
||||||
|
"icon": "dialpad",
|
||||||
|
"contributors": ["talvasconcelos", "arcbtc"]
|
||||||
|
}
|
42
lnbits/extensions/tpos/crud.py
Normal file
42
lnbits/extensions/tpos/crud.py
Normal 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,))
|
14
lnbits/extensions/tpos/migrations.py
Normal file
14
lnbits/extensions/tpos/migrations.py
Normal 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
|
||||||
|
);
|
||||||
|
"""
|
||||||
|
)
|
14
lnbits/extensions/tpos/models.py
Normal file
14
lnbits/extensions/tpos/models.py
Normal 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))
|
78
lnbits/extensions/tpos/templates/tpos/_api_docs.html
Normal file
78
lnbits/extensions/tpos/templates/tpos/_api_docs.html
Normal 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": <invoice_key>}</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>[<tpos_object>, ...]</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:
|
||||||
|
<invoice_key>"
|
||||||
|
</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": <invoice_key>}</code><br />
|
||||||
|
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
|
||||||
|
<code
|
||||||
|
>{"name": <string>, "currency": <string*ie USD*>}</code
|
||||||
|
>
|
||||||
|
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||||
|
Returns 201 CREATED (application/json)
|
||||||
|
</h5>
|
||||||
|
<code
|
||||||
|
>{"currency": <string>, "id": <string>, "name":
|
||||||
|
<string>, "wallet": <string>}</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":
|
||||||
|
<string>, "currency": <string>}' -H "Content-type:
|
||||||
|
application/json" -H "X-Api-Key: <admin_key>"
|
||||||
|
</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/<tpos_id></code
|
||||||
|
>
|
||||||
|
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||||
|
<code>{"X-Api-Key": <admin_key>}</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/<tpos_id> -H
|
||||||
|
"X-Api-Key: <admin_key>"
|
||||||
|
</code>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</q-expansion-item>
|
||||||
|
</q-expansion-item>
|
18
lnbits/extensions/tpos/templates/tpos/_tpos.html
Normal file
18
lnbits/extensions/tpos/templates/tpos/_tpos.html
Normal 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>
|
423
lnbits/extensions/tpos/templates/tpos/index.html
Normal file
423
lnbits/extensions/tpos/templates/tpos/index.html
Normal 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 %}
|
264
lnbits/extensions/tpos/templates/tpos/tpos.html
Normal file
264
lnbits/extensions/tpos/templates/tpos/tpos.html
Normal 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 %}
|
23
lnbits/extensions/tpos/views.py
Normal file
23
lnbits/extensions/tpos/views.py
Normal 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)
|
107
lnbits/extensions/tpos/views_api.py
Normal file
107
lnbits/extensions/tpos/views_api.py
Normal 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
|
|
@ -6,17 +6,9 @@ from lnbits.decorators import check_user_exists, validate_uuids
|
||||||
from . import watchonly_ext
|
from . import watchonly_ext
|
||||||
|
|
||||||
|
|
||||||
@watchonly_ext.route("/")
|
@watchonly_ext.get("/")
|
||||||
@validate_uuids(["usr"], required=True)
|
@validate_uuids(["usr"], required=True)
|
||||||
@check_user_exists()
|
@check_user_exists()
|
||||||
async def index():
|
async def index():
|
||||||
return await render_template("watchonly/index.html", user=g.user)
|
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)
|
|
||||||
|
|
|
@ -24,86 +24,78 @@ from .crud import (
|
||||||
###################WALLETS#############################
|
###################WALLETS#############################
|
||||||
|
|
||||||
|
|
||||||
@watchonly_ext.route("/api/v1/wallet", methods=["GET"])
|
@watchonly_ext.get("/api/v1/wallet")
|
||||||
@api_check_wallet_key("invoice")
|
@api_check_wallet_key("invoice")
|
||||||
async def api_wallets_retrieve():
|
async def api_wallets_retrieve():
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return (
|
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,
|
HTTPStatus.OK,
|
||||||
)
|
)
|
||||||
except:
|
except:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
@watchonly_ext.route("/api/v1/wallet/<wallet_id>", methods=["GET"])
|
@watchonly_ext.get("/api/v1/wallet/<wallet_id>")
|
||||||
@api_check_wallet_key("invoice")
|
@api_check_wallet_key("invoice")
|
||||||
async def api_wallet_retrieve(wallet_id):
|
async def api_wallet_retrieve(wallet_id):
|
||||||
wallet = await get_watch_wallet(wallet_id)
|
wallet = await get_watch_wallet(wallet_id)
|
||||||
|
|
||||||
if not wallet:
|
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_check_wallet_key("admin")
|
||||||
@api_validate_post_request(
|
async def api_wallet_create_or_update(masterPub: str, Title: str, wallet_id=None):
|
||||||
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):
|
|
||||||
try:
|
try:
|
||||||
wallet = await create_watch_wallet(
|
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:
|
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)
|
mempool = await get_mempool(g.wallet.user)
|
||||||
if not mempool:
|
if not mempool:
|
||||||
create_mempool(user=g.wallet.user)
|
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")
|
@api_check_wallet_key("admin")
|
||||||
async def api_wallet_delete(wallet_id):
|
async def api_wallet_delete(wallet_id):
|
||||||
wallet = await get_watch_wallet(wallet_id)
|
wallet = await get_watch_wallet(wallet_id)
|
||||||
|
|
||||||
if not wallet:
|
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)
|
await delete_watch_wallet(wallet_id)
|
||||||
|
|
||||||
return jsonify({"deleted": "true"}), HTTPStatus.NO_CONTENT
|
return {"deleted": "true"}, HTTPStatus.NO_CONTENT
|
||||||
|
|
||||||
|
|
||||||
#############################ADDRESSES##########################
|
#############################ADDRESSES##########################
|
||||||
|
|
||||||
|
|
||||||
@watchonly_ext.route("/api/v1/address/<wallet_id>", methods=["GET"])
|
@watchonly_ext.get("/api/v1/address/<wallet_id>")
|
||||||
@api_check_wallet_key("invoice")
|
@api_check_wallet_key("invoice")
|
||||||
async def api_fresh_address(wallet_id):
|
async def api_fresh_address(wallet_id):
|
||||||
await get_fresh_address(wallet_id)
|
await get_fresh_address(wallet_id)
|
||||||
|
|
||||||
addresses = await get_addresses(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")
|
@api_check_wallet_key("invoice")
|
||||||
async def api_get_addresses(wallet_id):
|
async def api_get_addresses(wallet_id):
|
||||||
wallet = await get_watch_wallet(wallet_id)
|
wallet = await get_watch_wallet(wallet_id)
|
||||||
|
|
||||||
if not wallet:
|
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)
|
addresses = await get_addresses(wallet_id)
|
||||||
|
|
||||||
|
@ -111,28 +103,23 @@ async def api_get_addresses(wallet_id):
|
||||||
await get_fresh_address(wallet_id)
|
await get_fresh_address(wallet_id)
|
||||||
addresses = await get_addresses(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##########################
|
#############################MEMPOOL##########################
|
||||||
|
|
||||||
|
|
||||||
@watchonly_ext.route("/api/v1/mempool", methods=["PUT"])
|
@watchonly_ext.put("/api/v1/mempool")
|
||||||
@api_check_wallet_key("admin")
|
@api_check_wallet_key("admin")
|
||||||
@api_validate_post_request(
|
async def api_update_mempool(endpoint: str):
|
||||||
schema={
|
mempool = await update_mempool(user=g.wallet.user, **endpoint)
|
||||||
"endpoint": {"type": "string", "empty": False, "required": True},
|
return mempool._asdict(), HTTPStatus.OK
|
||||||
}
|
|
||||||
)
|
|
||||||
async def api_update_mempool():
|
|
||||||
mempool = await update_mempool(user=g.wallet.user, **g.data)
|
|
||||||
return jsonify(mempool._asdict()), HTTPStatus.OK
|
|
||||||
|
|
||||||
|
|
||||||
@watchonly_ext.route("/api/v1/mempool", methods=["GET"])
|
@watchonly_ext.get("/api/v1/mempool")
|
||||||
@api_check_wallet_key("admin")
|
@api_check_wallet_key("admin")
|
||||||
async def api_get_mempool():
|
async def api_get_mempool():
|
||||||
mempool = await get_mempool(g.wallet.user)
|
mempool = await get_mempool(g.wallet.user)
|
||||||
if not mempool:
|
if not mempool:
|
||||||
mempool = await create_mempool(user=g.wallet.user)
|
mempool = await create_mempool(user=g.wallet.user)
|
||||||
return jsonify(mempool._asdict()), HTTPStatus.OK
|
return mempool._asdict(), HTTPStatus.OK
|
||||||
|
|
|
@ -8,14 +8,14 @@ from . import withdraw_ext
|
||||||
from .crud import get_withdraw_link, chunks
|
from .crud import get_withdraw_link, chunks
|
||||||
|
|
||||||
|
|
||||||
@withdraw_ext.route("/")
|
@withdraw_ext.get("/")
|
||||||
@validate_uuids(["usr"], required=True)
|
@validate_uuids(["usr"], required=True)
|
||||||
@check_user_exists()
|
@check_user_exists()
|
||||||
async def index():
|
async def index():
|
||||||
return await render_template("withdraw/index.html", user=g.user)
|
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):
|
async def display(link_id):
|
||||||
link = await get_withdraw_link(link_id, 0) or abort(
|
link = await get_withdraw_link(link_id, 0) or abort(
|
||||||
HTTPStatus.NOT_FOUND, "Withdraw link does not exist."
|
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)
|
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):
|
async def img(link_id):
|
||||||
link = await get_withdraw_link(link_id, 0) or abort(
|
link = await get_withdraw_link(link_id, 0) or abort(
|
||||||
HTTPStatus.NOT_FOUND, "Withdraw link does not exist."
|
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):
|
async def print_qr(link_id):
|
||||||
link = await get_withdraw_link(link_id) or abort(
|
link = await get_withdraw_link(link_id) or abort(
|
||||||
HTTPStatus.NOT_FOUND, "Withdraw link does not exist."
|
HTTPStatus.NOT_FOUND, "Withdraw link does not exist."
|
||||||
|
|
|
@ -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")
|
@api_check_wallet_key("invoice")
|
||||||
async def api_links():
|
async def api_links():
|
||||||
wallet_ids = [g.wallet.id]
|
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")
|
@api_check_wallet_key("invoice")
|
||||||
async def api_link_retrieve(link_id):
|
async def api_link_retrieve(link_id):
|
||||||
link = await get_withdraw_link(link_id, 0)
|
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
|
return jsonable_encoder({**link, **{"lnurl": link.lnurl}}), HTTPStatus.OK
|
||||||
|
|
||||||
class CreateData(BaseModel):
|
class CreateData(BaseModel):
|
||||||
title: str = Query(...),
|
title: str = Query(...)
|
||||||
min_withdrawable: int = Query(..., ge=1),
|
min_withdrawable: int = Query(..., ge=1)
|
||||||
max_withdrawable: int = Query(..., ge=1),
|
max_withdrawable: int = Query(..., ge=1)
|
||||||
uses: int = Query(..., ge=1),
|
uses: int = Query(..., ge=1)
|
||||||
wait_time: int = Query(..., ge=1),
|
wait_time: int = Query(..., ge=1)
|
||||||
is_unique: bool
|
is_unique: bool
|
||||||
|
|
||||||
@withdraw_ext.route("/api/v1/links", methods=["POST"])
|
@withdraw_ext.post("/api/v1/links")
|
||||||
@withdraw_ext.route("/api/v1/links/<link_id>", methods=["PUT"])
|
@withdraw_ext.put("/api/v1/links/<link_id>")
|
||||||
@api_check_wallet_key("admin")
|
@api_check_wallet_key("admin")
|
||||||
async def api_link_create_or_update(data: CreateData, link_id: str = None):
|
async def api_link_create_or_update(data: CreateData, link_id: str = None):
|
||||||
if data.max_withdrawable < data.min_withdrawable:
|
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")
|
@api_check_wallet_key("admin")
|
||||||
async def api_link_delete(link_id):
|
async def api_link_delete(link_id):
|
||||||
link = await get_withdraw_link(link_id)
|
link = await get_withdraw_link(link_id)
|
||||||
|
@ -137,8 +137,8 @@ async def api_link_delete(link_id):
|
||||||
return "", HTTPStatus.NO_CONTENT
|
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")
|
@api_check_wallet_key("invoice")
|
||||||
async def api_hash_retrieve(the_hash, lnurl_id):
|
async def api_hash_retrieve(the_hash, lnurl_id):
|
||||||
hashCheck = await get_hash_check(the_hash, lnurl_id)
|
hashCheck = await get_hash_check(the_hash, lnurl_id)
|
||||||
return jsonify(hashCheck), HTTPStatus.OK
|
return jsonable_encoder(hashCheck), HTTPStatus.OK
|
||||||
|
|
Loading…
Add table
Reference in a new issue