mirror of
https://github.com/lnbits/lnbits-legend.git
synced 2025-02-26 07:31:22 +01:00
Merge branch 'bcgerty' into gerty
This commit is contained in:
commit
07a95532be
45 changed files with 2592 additions and 1253 deletions
|
@ -79,3 +79,8 @@ For the invoice to work you must have a publicly accessible URL in your LNbits.
|
||||||
- `LNBITS_BACKEND_WALLET_CLASS`: **OpenNodeWallet**
|
- `LNBITS_BACKEND_WALLET_CLASS`: **OpenNodeWallet**
|
||||||
- `OPENNODE_API_ENDPOINT`: https://api.opennode.com/
|
- `OPENNODE_API_ENDPOINT`: https://api.opennode.com/
|
||||||
- `OPENNODE_KEY`: opennodeAdminApiKey
|
- `OPENNODE_KEY`: opennodeAdminApiKey
|
||||||
|
|
||||||
|
|
||||||
|
### Cliche Wallet
|
||||||
|
|
||||||
|
- `CLICHE_ENDPOINT`: ws://127.0.0.1:12000
|
||||||
|
|
|
@ -91,7 +91,6 @@ def create_app(config_object="lnbits.settings") -> FastAPI:
|
||||||
)
|
)
|
||||||
|
|
||||||
app.add_middleware(GZipMiddleware, minimum_size=1000)
|
app.add_middleware(GZipMiddleware, minimum_size=1000)
|
||||||
# app.add_middleware(ASGIProxyFix)
|
|
||||||
|
|
||||||
check_funding_source(app)
|
check_funding_source(app)
|
||||||
register_assets(app)
|
register_assets(app)
|
||||||
|
|
|
@ -361,6 +361,35 @@ new Vue({
|
||||||
this.receive.status = 'pending'
|
this.receive.status = 'pending'
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
onInitQR: async function (promise) {
|
||||||
|
try {
|
||||||
|
await promise
|
||||||
|
} catch (error) {
|
||||||
|
let mapping = {
|
||||||
|
NotAllowedError: 'ERROR: you need to grant camera access permission',
|
||||||
|
NotFoundError: 'ERROR: no camera on this device',
|
||||||
|
NotSupportedError:
|
||||||
|
'ERROR: secure context required (HTTPS, localhost)',
|
||||||
|
NotReadableError: 'ERROR: is the camera already in use?',
|
||||||
|
OverconstrainedError: 'ERROR: installed cameras are not suitable',
|
||||||
|
StreamApiNotSupportedError:
|
||||||
|
'ERROR: Stream API is not supported in this browser',
|
||||||
|
InsecureContextError:
|
||||||
|
'ERROR: Camera access is only permitted in secure context. Use HTTPS or localhost rather than HTTP.'
|
||||||
|
}
|
||||||
|
let valid_error = Object.keys(mapping).filter(key => {
|
||||||
|
return error.name === key
|
||||||
|
})
|
||||||
|
let camera_error = valid_error
|
||||||
|
? mapping[valid_error]
|
||||||
|
: `ERROR: Camera error (${error.name})`
|
||||||
|
this.parse.camera.show = false
|
||||||
|
this.$q.notify({
|
||||||
|
message: camera_error,
|
||||||
|
type: 'negative'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
decodeQR: function (res) {
|
decodeQR: function (res) {
|
||||||
this.parse.data.request = res
|
this.parse.data.request = res
|
||||||
this.decodeRequest()
|
this.decodeRequest()
|
||||||
|
|
|
@ -653,6 +653,7 @@
|
||||||
<q-responsive :ratio="1">
|
<q-responsive :ratio="1">
|
||||||
<qrcode-stream
|
<qrcode-stream
|
||||||
@decode="decodeQR"
|
@decode="decodeQR"
|
||||||
|
@init="onInitQR"
|
||||||
class="rounded-borders"
|
class="rounded-borders"
|
||||||
></qrcode-stream>
|
></qrcode-stream>
|
||||||
</q-responsive>
|
</q-responsive>
|
||||||
|
@ -671,6 +672,7 @@
|
||||||
<div class="text-center q-mb-lg">
|
<div class="text-center q-mb-lg">
|
||||||
<qrcode-stream
|
<qrcode-stream
|
||||||
@decode="decodeQR"
|
@decode="decodeQR"
|
||||||
|
@init="onInitQR"
|
||||||
class="rounded-borders"
|
class="rounded-borders"
|
||||||
></qrcode-stream>
|
></qrcode-stream>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -6,7 +6,7 @@ import time
|
||||||
import uuid
|
import uuid
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from typing import Dict, List, Optional, Tuple, Union
|
from typing import Dict, Optional, Tuple, Union
|
||||||
from urllib.parse import ParseResult, parse_qs, urlencode, urlparse, urlunparse
|
from urllib.parse import ParseResult, parse_qs, urlencode, urlparse, urlunparse
|
||||||
|
|
||||||
import async_timeout
|
import async_timeout
|
||||||
|
@ -476,7 +476,7 @@ async def api_lnurlscan(code: str, wallet: WalletTypeInfo = Depends(get_key_type
|
||||||
except:
|
except:
|
||||||
# parse internet identifier (user@domain.com)
|
# parse internet identifier (user@domain.com)
|
||||||
name_domain = code.split("@")
|
name_domain = code.split("@")
|
||||||
if len(name_domain) == 2 and len(name_domain[1].split(".")) == 2:
|
if len(name_domain) == 2 and len(name_domain[1].split(".")) >= 2:
|
||||||
name, domain = name_domain
|
name, domain = name_domain
|
||||||
url = (
|
url = (
|
||||||
("http://" if domain.endswith(".onion") else "https://")
|
("http://" if domain.endswith(".onion") else "https://")
|
||||||
|
|
|
@ -6,7 +6,6 @@ from urllib.parse import urlparse
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from starlette.requests import Request
|
from starlette.requests import Request
|
||||||
from starlette.responses import HTMLResponse
|
|
||||||
|
|
||||||
from lnbits import bolt11
|
from lnbits import bolt11
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ from .models import Copilots, CreateCopilotData
|
||||||
|
|
||||||
async def create_copilot(
|
async def create_copilot(
|
||||||
data: CreateCopilotData, inkey: Optional[str] = ""
|
data: CreateCopilotData, inkey: Optional[str] = ""
|
||||||
) -> Copilots:
|
) -> Optional[Copilots]:
|
||||||
copilot_id = urlsafe_short_hash()
|
copilot_id = urlsafe_short_hash()
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"""
|
"""
|
||||||
|
@ -67,19 +67,19 @@ async def create_copilot(
|
||||||
|
|
||||||
|
|
||||||
async def update_copilot(
|
async def update_copilot(
|
||||||
data: CreateCopilotData, copilot_id: Optional[str] = ""
|
data: CreateCopilotData, copilot_id: str
|
||||||
) -> Optional[Copilots]:
|
) -> Optional[Copilots]:
|
||||||
q = ", ".join([f"{field[0]} = ?" for field in data])
|
q = ", ".join([f"{field[0]} = ?" for field in data])
|
||||||
items = [f"{field[1]}" for field in data]
|
items = [f"{field[1]}" for field in data]
|
||||||
items.append(copilot_id)
|
items.append(copilot_id)
|
||||||
await db.execute(f"UPDATE copilot.newer_copilots SET {q} WHERE id = ?", (items))
|
await db.execute(f"UPDATE copilot.newer_copilots SET {q} WHERE id = ?", (items,))
|
||||||
row = await db.fetchone(
|
row = await db.fetchone(
|
||||||
"SELECT * FROM copilot.newer_copilots WHERE id = ?", (copilot_id,)
|
"SELECT * FROM copilot.newer_copilots WHERE id = ?", (copilot_id,)
|
||||||
)
|
)
|
||||||
return Copilots(**row) if row else None
|
return Copilots(**row) if row else None
|
||||||
|
|
||||||
|
|
||||||
async def get_copilot(copilot_id: str) -> Copilots:
|
async def get_copilot(copilot_id: str) -> Optional[Copilots]:
|
||||||
row = await db.fetchone(
|
row = await db.fetchone(
|
||||||
"SELECT * FROM copilot.newer_copilots WHERE id = ?", (copilot_id,)
|
"SELECT * FROM copilot.newer_copilots WHERE id = ?", (copilot_id,)
|
||||||
)
|
)
|
||||||
|
|
|
@ -26,7 +26,7 @@ async def wait_for_paid_invoices():
|
||||||
async def on_invoice_paid(payment: Payment) -> None:
|
async def on_invoice_paid(payment: Payment) -> None:
|
||||||
webhook = None
|
webhook = None
|
||||||
data = None
|
data = None
|
||||||
if payment.extra.get("tag") != "copilot":
|
if not payment.extra or payment.extra.get("tag") != "copilot":
|
||||||
# not an copilot invoice
|
# not an copilot invoice
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -71,8 +71,8 @@ async def on_invoice_paid(payment: Payment) -> None:
|
||||||
|
|
||||||
|
|
||||||
async def mark_webhook_sent(payment: Payment, status: int) -> None:
|
async def mark_webhook_sent(payment: Payment, status: int) -> None:
|
||||||
|
if payment.extra:
|
||||||
payment.extra["wh_status"] = status
|
payment.extra["wh_status"] = status
|
||||||
|
|
||||||
await core_db.execute(
|
await core_db.execute(
|
||||||
"""
|
"""
|
||||||
UPDATE apipayments SET extra = ?
|
UPDATE apipayments SET extra = ?
|
||||||
|
|
|
@ -15,7 +15,9 @@ templates = Jinja2Templates(directory="templates")
|
||||||
|
|
||||||
|
|
||||||
@copilot_ext.get("/", response_class=HTMLResponse)
|
@copilot_ext.get("/", response_class=HTMLResponse)
|
||||||
async def index(request: Request, user: User = Depends(check_user_exists)):
|
async def index(
|
||||||
|
request: Request, user: User = Depends(check_user_exists) # type: ignore
|
||||||
|
):
|
||||||
return copilot_renderer().TemplateResponse(
|
return copilot_renderer().TemplateResponse(
|
||||||
"copilot/index.html", {"request": request, "user": user.dict()}
|
"copilot/index.html", {"request": request, "user": user.dict()}
|
||||||
)
|
)
|
||||||
|
@ -44,7 +46,7 @@ class ConnectionManager:
|
||||||
|
|
||||||
async def connect(self, websocket: WebSocket, copilot_id: str):
|
async def connect(self, websocket: WebSocket, copilot_id: str):
|
||||||
await websocket.accept()
|
await websocket.accept()
|
||||||
websocket.id = copilot_id
|
websocket.id = copilot_id # type: ignore
|
||||||
self.active_connections.append(websocket)
|
self.active_connections.append(websocket)
|
||||||
|
|
||||||
def disconnect(self, websocket: WebSocket):
|
def disconnect(self, websocket: WebSocket):
|
||||||
|
@ -52,7 +54,7 @@ class ConnectionManager:
|
||||||
|
|
||||||
async def send_personal_message(self, message: str, copilot_id: str):
|
async def send_personal_message(self, message: str, copilot_id: str):
|
||||||
for connection in self.active_connections:
|
for connection in self.active_connections:
|
||||||
if connection.id == copilot_id:
|
if connection.id == copilot_id: # type: ignore
|
||||||
await connection.send_text(message)
|
await connection.send_text(message)
|
||||||
|
|
||||||
async def broadcast(self, message: str):
|
async def broadcast(self, message: str):
|
||||||
|
|
|
@ -23,7 +23,7 @@ from .views import updater
|
||||||
|
|
||||||
@copilot_ext.get("/api/v1/copilot")
|
@copilot_ext.get("/api/v1/copilot")
|
||||||
async def api_copilots_retrieve(
|
async def api_copilots_retrieve(
|
||||||
req: Request, wallet: WalletTypeInfo = Depends(get_key_type)
|
req: Request, wallet: WalletTypeInfo = Depends(get_key_type) # type: ignore
|
||||||
):
|
):
|
||||||
wallet_user = wallet.wallet.user
|
wallet_user = wallet.wallet.user
|
||||||
copilots = [copilot.dict() for copilot in await get_copilots(wallet_user)]
|
copilots = [copilot.dict() for copilot in await get_copilots(wallet_user)]
|
||||||
|
@ -37,7 +37,7 @@ async def api_copilots_retrieve(
|
||||||
async def api_copilot_retrieve(
|
async def api_copilot_retrieve(
|
||||||
req: Request,
|
req: Request,
|
||||||
copilot_id: str = Query(None),
|
copilot_id: str = Query(None),
|
||||||
wallet: WalletTypeInfo = Depends(get_key_type),
|
wallet: WalletTypeInfo = Depends(get_key_type), # type: ignore
|
||||||
):
|
):
|
||||||
copilot = await get_copilot(copilot_id)
|
copilot = await get_copilot(copilot_id)
|
||||||
if not copilot:
|
if not copilot:
|
||||||
|
@ -54,7 +54,7 @@ async def api_copilot_retrieve(
|
||||||
async def api_copilot_create_or_update(
|
async def api_copilot_create_or_update(
|
||||||
data: CreateCopilotData,
|
data: CreateCopilotData,
|
||||||
copilot_id: str = Query(None),
|
copilot_id: str = Query(None),
|
||||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
wallet: WalletTypeInfo = Depends(require_admin_key), # type: ignore
|
||||||
):
|
):
|
||||||
data.user = wallet.wallet.user
|
data.user = wallet.wallet.user
|
||||||
data.wallet = wallet.wallet.id
|
data.wallet = wallet.wallet.id
|
||||||
|
@ -67,7 +67,8 @@ async def api_copilot_create_or_update(
|
||||||
|
|
||||||
@copilot_ext.delete("/api/v1/copilot/{copilot_id}")
|
@copilot_ext.delete("/api/v1/copilot/{copilot_id}")
|
||||||
async def api_copilot_delete(
|
async def api_copilot_delete(
|
||||||
copilot_id: str = Query(None), wallet: WalletTypeInfo = Depends(require_admin_key)
|
copilot_id: str = Query(None),
|
||||||
|
wallet: WalletTypeInfo = Depends(require_admin_key), # type: ignore
|
||||||
):
|
):
|
||||||
copilot = await get_copilot(copilot_id)
|
copilot = await get_copilot(copilot_id)
|
||||||
|
|
||||||
|
|
|
@ -98,21 +98,21 @@ async def get_discordbot_wallet(wallet_id: str) -> Optional[Wallets]:
|
||||||
return Wallets(**row) if row else None
|
return Wallets(**row) if row else None
|
||||||
|
|
||||||
|
|
||||||
async def get_discordbot_wallets(admin_id: str) -> Optional[Wallets]:
|
async def get_discordbot_wallets(admin_id: str) -> List[Wallets]:
|
||||||
rows = await db.fetchall(
|
rows = await db.fetchall(
|
||||||
"SELECT * FROM discordbot.wallets WHERE admin = ?", (admin_id,)
|
"SELECT * FROM discordbot.wallets WHERE admin = ?", (admin_id,)
|
||||||
)
|
)
|
||||||
return [Wallets(**row) for row in rows]
|
return [Wallets(**row) for row in rows]
|
||||||
|
|
||||||
|
|
||||||
async def get_discordbot_users_wallets(user_id: str) -> Optional[Wallets]:
|
async def get_discordbot_users_wallets(user_id: str) -> List[Wallets]:
|
||||||
rows = await db.fetchall(
|
rows = await db.fetchall(
|
||||||
"""SELECT * FROM discordbot.wallets WHERE "user" = ?""", (user_id,)
|
"""SELECT * FROM discordbot.wallets WHERE "user" = ?""", (user_id,)
|
||||||
)
|
)
|
||||||
return [Wallets(**row) for row in rows]
|
return [Wallets(**row) for row in rows]
|
||||||
|
|
||||||
|
|
||||||
async def get_discordbot_wallet_transactions(wallet_id: str) -> Optional[Payment]:
|
async def get_discordbot_wallet_transactions(wallet_id: str) -> List[Payment]:
|
||||||
return await get_payments(
|
return await get_payments(
|
||||||
wallet_id=wallet_id, complete=True, pending=False, outgoing=True, incoming=True
|
wallet_id=wallet_id, complete=True, pending=False, outgoing=True, incoming=True
|
||||||
)
|
)
|
||||||
|
|
|
@ -9,7 +9,9 @@ from . import discordbot_ext, discordbot_renderer
|
||||||
|
|
||||||
|
|
||||||
@discordbot_ext.get("/", response_class=HTMLResponse)
|
@discordbot_ext.get("/", response_class=HTMLResponse)
|
||||||
async def index(request: Request, user: User = Depends(check_user_exists)):
|
async def index(
|
||||||
|
request: Request, user: User = Depends(check_user_exists) # type: ignore
|
||||||
|
):
|
||||||
return discordbot_renderer().TemplateResponse(
|
return discordbot_renderer().TemplateResponse(
|
||||||
"discordbot/index.html", {"request": request, "user": user.dict()}
|
"discordbot/index.html", {"request": request, "user": user.dict()}
|
||||||
)
|
)
|
||||||
|
|
|
@ -27,32 +27,37 @@ from .models import CreateUserData, CreateUserWallet
|
||||||
|
|
||||||
|
|
||||||
@discordbot_ext.get("/api/v1/users", status_code=HTTPStatus.OK)
|
@discordbot_ext.get("/api/v1/users", status_code=HTTPStatus.OK)
|
||||||
async def api_discordbot_users(wallet: WalletTypeInfo = Depends(get_key_type)):
|
async def api_discordbot_users(
|
||||||
|
wallet: WalletTypeInfo = Depends(get_key_type), # type: ignore
|
||||||
|
):
|
||||||
user_id = wallet.wallet.user
|
user_id = wallet.wallet.user
|
||||||
return [user.dict() for user in await get_discordbot_users(user_id)]
|
return [user.dict() for user in await get_discordbot_users(user_id)]
|
||||||
|
|
||||||
|
|
||||||
@discordbot_ext.get("/api/v1/users/{user_id}", status_code=HTTPStatus.OK)
|
@discordbot_ext.get("/api/v1/users/{user_id}", status_code=HTTPStatus.OK)
|
||||||
async def api_discordbot_user(user_id, wallet: WalletTypeInfo = Depends(get_key_type)):
|
async def api_discordbot_user(
|
||||||
|
user_id, wallet: WalletTypeInfo = Depends(get_key_type) # type: ignore
|
||||||
|
):
|
||||||
user = await get_discordbot_user(user_id)
|
user = await get_discordbot_user(user_id)
|
||||||
|
if user:
|
||||||
return user.dict()
|
return user.dict()
|
||||||
|
|
||||||
|
|
||||||
@discordbot_ext.post("/api/v1/users", status_code=HTTPStatus.CREATED)
|
@discordbot_ext.post("/api/v1/users", status_code=HTTPStatus.CREATED)
|
||||||
async def api_discordbot_users_create(
|
async def api_discordbot_users_create(
|
||||||
data: CreateUserData, wallet: WalletTypeInfo = Depends(get_key_type)
|
data: CreateUserData, wallet: WalletTypeInfo = Depends(get_key_type) # type: ignore
|
||||||
):
|
):
|
||||||
user = await create_discordbot_user(data)
|
user = await create_discordbot_user(data)
|
||||||
full = user.dict()
|
full = user.dict()
|
||||||
full["wallets"] = [
|
wallets = await get_discordbot_users_wallets(user.id)
|
||||||
wallet.dict() for wallet in await get_discordbot_users_wallets(user.id)
|
if wallets:
|
||||||
]
|
full["wallets"] = [wallet for wallet in wallets]
|
||||||
return full
|
return full
|
||||||
|
|
||||||
|
|
||||||
@discordbot_ext.delete("/api/v1/users/{user_id}")
|
@discordbot_ext.delete("/api/v1/users/{user_id}")
|
||||||
async def api_discordbot_users_delete(
|
async def api_discordbot_users_delete(
|
||||||
user_id, wallet: WalletTypeInfo = Depends(get_key_type)
|
user_id, wallet: WalletTypeInfo = Depends(get_key_type) # type: ignore
|
||||||
):
|
):
|
||||||
user = await get_discordbot_user(user_id)
|
user = await get_discordbot_user(user_id)
|
||||||
if not user:
|
if not user:
|
||||||
|
@ -75,7 +80,7 @@ async def api_discordbot_activate_extension(
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.NOT_FOUND, detail="User does not exist."
|
status_code=HTTPStatus.NOT_FOUND, detail="User does not exist."
|
||||||
)
|
)
|
||||||
update_user_extension(user_id=userid, extension=extension, active=active)
|
await update_user_extension(user_id=userid, extension=extension, active=active)
|
||||||
return {"extension": "updated"}
|
return {"extension": "updated"}
|
||||||
|
|
||||||
|
|
||||||
|
@ -84,7 +89,7 @@ async def api_discordbot_activate_extension(
|
||||||
|
|
||||||
@discordbot_ext.post("/api/v1/wallets")
|
@discordbot_ext.post("/api/v1/wallets")
|
||||||
async def api_discordbot_wallets_create(
|
async def api_discordbot_wallets_create(
|
||||||
data: CreateUserWallet, wallet: WalletTypeInfo = Depends(get_key_type)
|
data: CreateUserWallet, wallet: WalletTypeInfo = Depends(get_key_type) # type: ignore
|
||||||
):
|
):
|
||||||
user = await create_discordbot_wallet(
|
user = await create_discordbot_wallet(
|
||||||
user_id=data.user_id, wallet_name=data.wallet_name, admin_id=data.admin_id
|
user_id=data.user_id, wallet_name=data.wallet_name, admin_id=data.admin_id
|
||||||
|
@ -93,28 +98,30 @@ async def api_discordbot_wallets_create(
|
||||||
|
|
||||||
|
|
||||||
@discordbot_ext.get("/api/v1/wallets")
|
@discordbot_ext.get("/api/v1/wallets")
|
||||||
async def api_discordbot_wallets(wallet: WalletTypeInfo = Depends(get_key_type)):
|
async def api_discordbot_wallets(
|
||||||
|
wallet: WalletTypeInfo = Depends(get_key_type), # type: ignore
|
||||||
|
):
|
||||||
admin_id = wallet.wallet.user
|
admin_id = wallet.wallet.user
|
||||||
return [wallet.dict() for wallet in await get_discordbot_wallets(admin_id)]
|
return await get_discordbot_wallets(admin_id)
|
||||||
|
|
||||||
|
|
||||||
@discordbot_ext.get("/api/v1/transactions/{wallet_id}")
|
@discordbot_ext.get("/api/v1/transactions/{wallet_id}")
|
||||||
async def api_discordbot_wallet_transactions(
|
async def api_discordbot_wallet_transactions(
|
||||||
wallet_id, wallet: WalletTypeInfo = Depends(get_key_type)
|
wallet_id, wallet: WalletTypeInfo = Depends(get_key_type) # type: ignore
|
||||||
):
|
):
|
||||||
return await get_discordbot_wallet_transactions(wallet_id)
|
return await get_discordbot_wallet_transactions(wallet_id)
|
||||||
|
|
||||||
|
|
||||||
@discordbot_ext.get("/api/v1/wallets/{user_id}")
|
@discordbot_ext.get("/api/v1/wallets/{user_id}")
|
||||||
async def api_discordbot_users_wallets(
|
async def api_discordbot_users_wallets(
|
||||||
user_id, wallet: WalletTypeInfo = Depends(get_key_type)
|
user_id, wallet: WalletTypeInfo = Depends(get_key_type) # type: ignore
|
||||||
):
|
):
|
||||||
return [s_wallet.dict() for s_wallet in await get_discordbot_users_wallets(user_id)]
|
return await get_discordbot_users_wallets(user_id)
|
||||||
|
|
||||||
|
|
||||||
@discordbot_ext.delete("/api/v1/wallets/{wallet_id}")
|
@discordbot_ext.delete("/api/v1/wallets/{wallet_id}")
|
||||||
async def api_discordbot_wallets_delete(
|
async def api_discordbot_wallets_delete(
|
||||||
wallet_id, wallet: WalletTypeInfo = Depends(get_key_type)
|
wallet_id, wallet: WalletTypeInfo = Depends(get_key_type) # type: ignore
|
||||||
):
|
):
|
||||||
get_wallet = await get_discordbot_wallet(wallet_id)
|
get_wallet = await get_discordbot_wallet(wallet_id)
|
||||||
if not get_wallet:
|
if not get_wallet:
|
||||||
|
|
|
@ -12,7 +12,10 @@ templates = Jinja2Templates(directory="templates")
|
||||||
|
|
||||||
|
|
||||||
@example_ext.get("/", response_class=HTMLResponse)
|
@example_ext.get("/", response_class=HTMLResponse)
|
||||||
async def index(request: Request, user: User = Depends(check_user_exists)):
|
async def index(
|
||||||
|
request: Request,
|
||||||
|
user: User = Depends(check_user_exists), # type: ignore
|
||||||
|
):
|
||||||
return example_renderer().TemplateResponse(
|
return example_renderer().TemplateResponse(
|
||||||
"example/index.html", {"request": request, "user": user.dict()}
|
"example/index.html", {"request": request, "user": user.dict()}
|
||||||
)
|
)
|
||||||
|
|
|
@ -9,12 +9,12 @@ from lnbits.tasks import catch_everything_and_restart
|
||||||
|
|
||||||
db = Database("ext_gerty")
|
db = Database("ext_gerty")
|
||||||
|
|
||||||
|
|
||||||
gerty_ext: APIRouter = APIRouter(prefix="/gerty", tags=["Gerty"])
|
gerty_ext: APIRouter = APIRouter(prefix="/gerty", tags=["Gerty"])
|
||||||
|
|
||||||
|
|
||||||
def gerty_renderer():
|
def gerty_renderer():
|
||||||
return template_renderer(["lnbits/extensions/gerty/templates"])
|
return template_renderer(["lnbits/extensions/gerty/templates"])
|
||||||
|
|
||||||
|
|
||||||
from .views import * # noqa
|
from .views import * # noqa
|
||||||
from .views_api import * # noqa
|
from .views_api import * # noqa
|
||||||
|
|
|
@ -14,21 +14,25 @@ async def create_gerty(wallet_id: str, data: Gerty) -> Gerty:
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
wallet,
|
wallet,
|
||||||
|
utc_offset,
|
||||||
lnbits_wallets,
|
lnbits_wallets,
|
||||||
mempool_endpoint,
|
mempool_endpoint,
|
||||||
exchange,
|
exchange,
|
||||||
display_preferences
|
display_preferences,
|
||||||
|
refresh_time
|
||||||
)
|
)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
""",
|
""",
|
||||||
(
|
(
|
||||||
gerty_id,
|
gerty_id,
|
||||||
data.name,
|
data.name,
|
||||||
data.wallet,
|
data.wallet,
|
||||||
|
data.utc_offset,
|
||||||
data.lnbits_wallets,
|
data.lnbits_wallets,
|
||||||
data.mempool_endpoint,
|
data.mempool_endpoint,
|
||||||
data.exchange,
|
data.exchange,
|
||||||
data.display_preferences
|
data.display_preferences,
|
||||||
|
data.refresh_time,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -36,6 +40,7 @@ async def create_gerty(wallet_id: str, data: Gerty) -> Gerty:
|
||||||
assert gerty, "Newly created gerty couldn't be retrieved"
|
assert gerty, "Newly created gerty couldn't be retrieved"
|
||||||
return gerty
|
return gerty
|
||||||
|
|
||||||
|
|
||||||
async def update_gerty(gerty_id: str, **kwargs) -> Gerty:
|
async def update_gerty(gerty_id: str, **kwargs) -> Gerty:
|
||||||
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
|
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
|
||||||
await db.execute(
|
await db.execute(
|
||||||
|
@ -43,6 +48,7 @@ async def update_gerty(gerty_id: str, **kwargs) -> Gerty:
|
||||||
)
|
)
|
||||||
return await get_gerty(gerty_id)
|
return await get_gerty(gerty_id)
|
||||||
|
|
||||||
|
|
||||||
async def get_gerty(gerty_id: str) -> Optional[Gerty]:
|
async def get_gerty(gerty_id: str) -> Optional[Gerty]:
|
||||||
row = await db.fetchone("SELECT * FROM gerty.gertys WHERE id = ?", (gerty_id,))
|
row = await db.fetchone("SELECT * FROM gerty.gertys WHERE id = ?", (gerty_id,))
|
||||||
return Gerty(**row) if row else None
|
return Gerty(**row) if row else None
|
||||||
|
|
243
lnbits/extensions/gerty/helpers.py
Normal file
243
lnbits/extensions/gerty/helpers.py
Normal file
|
@ -0,0 +1,243 @@
|
||||||
|
import textwrap
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
from .number_prefixer import *
|
||||||
|
|
||||||
|
|
||||||
|
def get_percent_difference(current, previous, precision=4):
|
||||||
|
difference = (current - previous) / current * 100
|
||||||
|
return "{0}{1}%".format("+" if difference > 0 else "", round(difference, precision))
|
||||||
|
|
||||||
|
|
||||||
|
# A helper function get a nicely formated dict for the text
|
||||||
|
def get_text_item_dict(text: str, font_size: int, x_pos: int = None, y_pos: int = None):
|
||||||
|
# Get line size by font size
|
||||||
|
line_width = 60
|
||||||
|
if font_size <= 12:
|
||||||
|
line_width = 75
|
||||||
|
elif font_size <= 15:
|
||||||
|
line_width = 58
|
||||||
|
elif font_size <= 20:
|
||||||
|
line_width = 40
|
||||||
|
elif font_size <= 40:
|
||||||
|
line_width = 30
|
||||||
|
else:
|
||||||
|
line_width = 20
|
||||||
|
|
||||||
|
# wrap the text
|
||||||
|
wrapper = textwrap.TextWrapper(width=line_width)
|
||||||
|
word_list = wrapper.wrap(text=text)
|
||||||
|
# logger.debug("number of chars = {0}".format(len(text)))
|
||||||
|
|
||||||
|
multilineText = "\n".join(word_list)
|
||||||
|
# logger.debug("number of lines = {0}".format(len(word_list)))
|
||||||
|
|
||||||
|
# logger.debug('multilineText')
|
||||||
|
# logger.debug(multilineText)
|
||||||
|
|
||||||
|
text = {"value": multilineText, "size": font_size}
|
||||||
|
if x_pos is None and y_pos is None:
|
||||||
|
text["position"] = "center"
|
||||||
|
else:
|
||||||
|
text["x"] = x_pos
|
||||||
|
text["y"] = y_pos
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
# format a number for nice display output
|
||||||
|
def format_number(number, precision=None):
|
||||||
|
return "{:,}".format(round(number, precision))
|
||||||
|
|
||||||
|
|
||||||
|
async def get_mempool_recommended_fees(gerty):
|
||||||
|
if isinstance(gerty.mempool_endpoint, str):
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
r = await client.get(gerty.mempool_endpoint + "/api/v1/fees/recommended")
|
||||||
|
return r.json()
|
||||||
|
|
||||||
|
|
||||||
|
async def get_mining_dashboard(gerty):
|
||||||
|
areas = []
|
||||||
|
if isinstance(gerty.mempool_endpoint, str):
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
# current hashrate
|
||||||
|
r = await client.get(gerty.mempool_endpoint + "/api/v1/mining/hashrate/1w")
|
||||||
|
data = r.json()
|
||||||
|
hashrateNow = data["currentHashrate"]
|
||||||
|
hashrateOneWeekAgo = data["hashrates"][6]["avgHashrate"]
|
||||||
|
|
||||||
|
text = []
|
||||||
|
text.append(get_text_item_dict("Current mining hashrate", 12))
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
"{0}hash".format(si_format(hashrateNow, 6, True, " ")), 20
|
||||||
|
)
|
||||||
|
)
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
"{0} vs 7 days ago".format(
|
||||||
|
get_percent_difference(hashrateNow, hashrateOneWeekAgo, 3)
|
||||||
|
),
|
||||||
|
12,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
areas.append(text)
|
||||||
|
|
||||||
|
r = await client.get(
|
||||||
|
gerty.mempool_endpoint + "/api/v1/difficulty-adjustment"
|
||||||
|
)
|
||||||
|
|
||||||
|
# timeAvg
|
||||||
|
text = []
|
||||||
|
progress = "{0}%".format(round(r.json()["progressPercent"], 2))
|
||||||
|
text.append(get_text_item_dict("Progress through current epoch", 12))
|
||||||
|
text.append(get_text_item_dict(progress, 60))
|
||||||
|
areas.append(text)
|
||||||
|
|
||||||
|
# difficulty adjustment
|
||||||
|
text = []
|
||||||
|
stat = r.json()["remainingTime"]
|
||||||
|
text.append(get_text_item_dict("Time to next difficulty adjustment", 12))
|
||||||
|
text.append(get_text_item_dict(get_time_remaining(stat / 1000, 3), 12))
|
||||||
|
areas.append(text)
|
||||||
|
|
||||||
|
# difficultyChange
|
||||||
|
text = []
|
||||||
|
difficultyChange = round(r.json()["difficultyChange"], 2)
|
||||||
|
text.append(get_text_item_dict("Estimated difficulty change", 12))
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
"{0}{1}%".format(
|
||||||
|
"+" if difficultyChange > 0 else "", round(difficultyChange, 2)
|
||||||
|
),
|
||||||
|
60,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
areas.append(text)
|
||||||
|
|
||||||
|
r = await client.get(gerty.mempool_endpoint + "/api/v1/mining/hashrate/1m")
|
||||||
|
data = r.json()
|
||||||
|
stat = {}
|
||||||
|
stat["current"] = data["currentDifficulty"]
|
||||||
|
stat["previous"] = data["difficulty"][len(data["difficulty"]) - 2][
|
||||||
|
"difficulty"
|
||||||
|
]
|
||||||
|
return areas
|
||||||
|
|
||||||
|
|
||||||
|
async def api_get_lightning_stats(gerty):
|
||||||
|
stat = {}
|
||||||
|
if isinstance(gerty.mempool_endpoint, str):
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
r = await client.get(
|
||||||
|
gerty.mempool_endpoint + "/api/v1/lightning/statistics/latest"
|
||||||
|
)
|
||||||
|
data = r.json()
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
async def get_lightning_stats(gerty):
|
||||||
|
data = await api_get_lightning_stats(gerty)
|
||||||
|
areas = []
|
||||||
|
|
||||||
|
text = []
|
||||||
|
text.append(get_text_item_dict("Channel Count", 12))
|
||||||
|
text.append(get_text_item_dict(format_number(data["latest"]["channel_count"]), 20))
|
||||||
|
difference = get_percent_difference(
|
||||||
|
current=data["latest"]["channel_count"],
|
||||||
|
previous=data["previous"]["channel_count"],
|
||||||
|
)
|
||||||
|
text.append(get_text_item_dict("{0} in last 7 days".format(difference), 12))
|
||||||
|
areas.append(text)
|
||||||
|
|
||||||
|
text = []
|
||||||
|
text.append(get_text_item_dict("Number of Nodes", 12))
|
||||||
|
text.append(get_text_item_dict(format_number(data["latest"]["node_count"]), 20))
|
||||||
|
difference = get_percent_difference(
|
||||||
|
current=data["latest"]["node_count"], previous=data["previous"]["node_count"]
|
||||||
|
)
|
||||||
|
text.append(get_text_item_dict("{0} in last 7 days".format(difference), 12))
|
||||||
|
areas.append(text)
|
||||||
|
|
||||||
|
text = []
|
||||||
|
text.append(get_text_item_dict("Total Capacity", 12))
|
||||||
|
avg_capacity = float(data["latest"]["total_capacity"]) / float(100000000)
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict("{0} BTC".format(format_number(avg_capacity, 2)), 20)
|
||||||
|
)
|
||||||
|
difference = get_percent_difference(
|
||||||
|
current=data["latest"]["total_capacity"],
|
||||||
|
previous=data["previous"]["total_capacity"],
|
||||||
|
)
|
||||||
|
text.append(get_text_item_dict("{0} in last 7 days".format(difference), 12))
|
||||||
|
areas.append(text)
|
||||||
|
|
||||||
|
text = []
|
||||||
|
text.append(get_text_item_dict("Average Channel Capacity", 12))
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
"{0} sats".format(format_number(data["latest"]["avg_capacity"])), 20
|
||||||
|
)
|
||||||
|
)
|
||||||
|
difference = get_percent_difference(
|
||||||
|
current=data["latest"]["avg_capacity"],
|
||||||
|
previous=data["previous"]["avg_capacity"],
|
||||||
|
)
|
||||||
|
text.append(get_text_item_dict("{0} in last 7 days".format(difference), 12))
|
||||||
|
areas.append(text)
|
||||||
|
|
||||||
|
return areas
|
||||||
|
|
||||||
|
|
||||||
|
def get_next_update_time(sleep_time_seconds: int = 0, utc_offset: int = 0):
|
||||||
|
utc_now = datetime.utcnow()
|
||||||
|
next_refresh_time = utc_now + timedelta(0, sleep_time_seconds)
|
||||||
|
local_refresh_time = next_refresh_time + timedelta(hours=utc_offset)
|
||||||
|
return "{0} {1}".format(
|
||||||
|
"I'll wake up at" if gerty_should_sleep(utc_offset) else "Next update at",
|
||||||
|
local_refresh_time.strftime("%H:%M on %e %b %Y"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def gerty_should_sleep(utc_offset: int = 0):
|
||||||
|
utc_now = datetime.utcnow()
|
||||||
|
local_time = utc_now + timedelta(hours=utc_offset)
|
||||||
|
hours = local_time.strftime("%H")
|
||||||
|
hours = int(hours)
|
||||||
|
logger.debug("HOURS")
|
||||||
|
logger.debug(hours)
|
||||||
|
if hours >= 22 and hours <= 23:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def get_date_suffix(dayNumber):
|
||||||
|
if 4 <= dayNumber <= 20 or 24 <= dayNumber <= 30:
|
||||||
|
return "th"
|
||||||
|
else:
|
||||||
|
return ["st", "nd", "rd"][dayNumber % 10 - 1]
|
||||||
|
|
||||||
|
|
||||||
|
def get_time_remaining(seconds, granularity=2):
|
||||||
|
intervals = (
|
||||||
|
# ('weeks', 604800), # 60 * 60 * 24 * 7
|
||||||
|
("days", 86400), # 60 * 60 * 24
|
||||||
|
("hours", 3600), # 60 * 60
|
||||||
|
("minutes", 60),
|
||||||
|
("seconds", 1),
|
||||||
|
)
|
||||||
|
|
||||||
|
result = []
|
||||||
|
|
||||||
|
for name, count in intervals:
|
||||||
|
value = seconds // count
|
||||||
|
if value:
|
||||||
|
seconds -= value * count
|
||||||
|
if value == 1:
|
||||||
|
name = name.rstrip("s")
|
||||||
|
result.append("{} {}".format(round(value), name))
|
||||||
|
return ", ".join(result[:granularity])
|
|
@ -16,3 +16,11 @@ async def m001_initial(db):
|
||||||
);
|
);
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def m002_add_utc_offset_col(db):
|
||||||
|
"""
|
||||||
|
support for UTC offset
|
||||||
|
"""
|
||||||
|
await db.execute(
|
||||||
|
"ALTER TABLE gerty.gertys ADD COLUMN utc_offset INT;"
|
||||||
|
)
|
||||||
|
|
|
@ -4,14 +4,20 @@ from typing import Optional
|
||||||
from fastapi import Query
|
from fastapi import Query
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
class Gerty(BaseModel):
|
class Gerty(BaseModel):
|
||||||
id: str = Query(None)
|
id: str = Query(None)
|
||||||
name: str
|
name: str
|
||||||
wallet: str
|
wallet: str
|
||||||
refresh_time: int = Query(None)
|
refresh_time: int = Query(None)
|
||||||
lnbits_wallets: str = Query(None) # Wallets to keep an eye on, {"wallet-id": "wallet-read-key, etc"}
|
utc_offset: int = Query(None)
|
||||||
|
lnbits_wallets: str = Query(
|
||||||
|
None
|
||||||
|
) # Wallets to keep an eye on, {"wallet-id": "wallet-read-key, etc"}
|
||||||
mempool_endpoint: str = Query(None) # Mempool endpoint to use
|
mempool_endpoint: str = Query(None) # Mempool endpoint to use
|
||||||
exchange: str = Query(None) # BTC <-> Fiat exchange rate to pull ie "USD", in 0.0001 and sats
|
exchange: str = Query(
|
||||||
|
None
|
||||||
|
) # BTC <-> Fiat exchange rate to pull ie "USD", in 0.0001 and sats
|
||||||
display_preferences: str = Query(None)
|
display_preferences: str = Query(None)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
66
lnbits/extensions/gerty/number_prefixer.py
Normal file
66
lnbits/extensions/gerty/number_prefixer.py
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
import math
|
||||||
|
|
||||||
|
|
||||||
|
def si_classifier(val):
|
||||||
|
suffixes = {
|
||||||
|
24: {"long_suffix": "yotta", "short_suffix": "Y", "scalar": 10**24},
|
||||||
|
21: {"long_suffix": "zetta", "short_suffix": "Z", "scalar": 10**21},
|
||||||
|
18: {"long_suffix": "exa", "short_suffix": "E", "scalar": 10**18},
|
||||||
|
15: {"long_suffix": "peta", "short_suffix": "P", "scalar": 10**15},
|
||||||
|
12: {"long_suffix": "tera", "short_suffix": "T", "scalar": 10**12},
|
||||||
|
9: {"long_suffix": "giga", "short_suffix": "G", "scalar": 10**9},
|
||||||
|
6: {"long_suffix": "mega", "short_suffix": "M", "scalar": 10**6},
|
||||||
|
3: {"long_suffix": "kilo", "short_suffix": "k", "scalar": 10**3},
|
||||||
|
0: {"long_suffix": "", "short_suffix": "", "scalar": 10**0},
|
||||||
|
-3: {"long_suffix": "milli", "short_suffix": "m", "scalar": 10**-3},
|
||||||
|
-6: {"long_suffix": "micro", "short_suffix": "µ", "scalar": 10**-6},
|
||||||
|
-9: {"long_suffix": "nano", "short_suffix": "n", "scalar": 10**-9},
|
||||||
|
-12: {"long_suffix": "pico", "short_suffix": "p", "scalar": 10**-12},
|
||||||
|
-15: {"long_suffix": "femto", "short_suffix": "f", "scalar": 10**-15},
|
||||||
|
-18: {"long_suffix": "atto", "short_suffix": "a", "scalar": 10**-18},
|
||||||
|
-21: {"long_suffix": "zepto", "short_suffix": "z", "scalar": 10**-21},
|
||||||
|
-24: {"long_suffix": "yocto", "short_suffix": "y", "scalar": 10**-24},
|
||||||
|
}
|
||||||
|
exponent = int(math.floor(math.log10(abs(val)) / 3.0) * 3)
|
||||||
|
return suffixes.get(exponent, None)
|
||||||
|
|
||||||
|
|
||||||
|
def si_formatter(value):
|
||||||
|
"""
|
||||||
|
Return a triple of scaled value, short suffix, long suffix, or None if
|
||||||
|
the value cannot be classified.
|
||||||
|
"""
|
||||||
|
classifier = si_classifier(value)
|
||||||
|
if classifier == None:
|
||||||
|
# Don't know how to classify this value
|
||||||
|
return None
|
||||||
|
|
||||||
|
scaled = value / classifier["scalar"]
|
||||||
|
return (scaled, classifier["short_suffix"], classifier["long_suffix"])
|
||||||
|
|
||||||
|
|
||||||
|
def si_format(value, precision=4, long_form=False, separator=""):
|
||||||
|
"""
|
||||||
|
"SI prefix" formatted string: return a string with the given precision
|
||||||
|
and an appropriate order-of-3-magnitudes suffix, e.g.:
|
||||||
|
si_format(1001.0) => '1.00K'
|
||||||
|
si_format(0.00000000123, long_form=True, separator=' ') => '1.230 nano'
|
||||||
|
"""
|
||||||
|
scaled, short_suffix, long_suffix = si_formatter(value)
|
||||||
|
|
||||||
|
if scaled == None:
|
||||||
|
# Don't know how to format this value
|
||||||
|
return value
|
||||||
|
|
||||||
|
suffix = long_suffix if long_form else short_suffix
|
||||||
|
|
||||||
|
if abs(scaled) < 10:
|
||||||
|
precision = precision - 1
|
||||||
|
elif abs(scaled) < 100:
|
||||||
|
precision = precision - 2
|
||||||
|
else:
|
||||||
|
precision = precision - 3
|
||||||
|
|
||||||
|
return "{scaled:.{precision}f}{separator}{suffix}".format(
|
||||||
|
scaled=scaled, precision=precision, separator=separator, suffix=suffix
|
||||||
|
)
|
|
@ -1,14 +0,0 @@
|
||||||
{
|
|
||||||
"facts": [
|
|
||||||
"When a woman asked Pieter Wuille to talk dirty to her, he described the OpenSSL DER implementation.",
|
|
||||||
"Pieter Wuille recently visited an event horizon and escaped with a cryptographic proof.",
|
|
||||||
"Pieter Wuille's PhD thesis defence in full: \"Pieter Wuille, thank you\".",
|
|
||||||
"Pieter Wuille is an acronym for Programmatic Intelligent Encrypted Telemetric Encapsulated Recursive Witness Upscaling Integrated Load-Balancing Logical Entity.",
|
|
||||||
"Dan Bernstein only trusts one source of random numbers: Pieter Wuille.",
|
|
||||||
"Putting Pieter Wuille in the title of an r/Bitcoin submission gets more upvotes than the same post from Pieter Wuille himself.",
|
|
||||||
"Pieter Wuille won the underhanded crypto contest but his entry was so underhanded nobody even knows he entered.",
|
|
||||||
"Greg Maxwell is a bot created by Pieter Wuille to argue on reddit while he can get code done.",
|
|
||||||
"Pieter Wuille doesn't need the public key to calculate the corresponding private key.",
|
|
||||||
"When the Wikipedia servers corrupted all data including backups, Pieter Wuille had to stay awake all night to retype it."
|
|
||||||
]
|
|
||||||
}
|
|
1099
lnbits/extensions/gerty/static/satoshi_long.json
Normal file
1099
lnbits/extensions/gerty/static/satoshi_long.json
Normal file
File diff suppressed because it is too large
Load diff
|
@ -71,7 +71,8 @@
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||||
<code
|
<code
|
||||||
>curl -X DELETE {{ request.base_url
|
>curl -X DELETE {{ request.base_url
|
||||||
}}gerty/api/v1/gertys/<gerty_id> -H "X-Api-Key: <admin_key>"
|
}}gerty/api/v1/gertys/<gerty_id> -H "X-Api-Key:
|
||||||
|
<admin_key>"
|
||||||
</code>
|
</code>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
|
|
|
@ -1,38 +1,72 @@
|
||||||
{% extends "public.html" %} {% block toolbar_title %} {{ gerty.name }}{% endblock %}{% block page %}
|
{% extends "public.html" %} {% block toolbar_title %} {{ gerty.name }}{%
|
||||||
{% raw %}
|
endblock %}{% block page %} {% raw %}
|
||||||
<div class="q-pa-md row items-start q-gutter-md">
|
<div class="q-pa-md row items-start q-gutter-md">
|
||||||
<q-card unelevated flat class="q-pa-none text-body1 blockquote" style="background: none !important;">
|
<q-card
|
||||||
"{{gerty.sats_quote[0].text}}" <br/>~ Satoshi {{gerty.sats_quote[0].date}}
|
unelevated
|
||||||
</q-card>
|
flat
|
||||||
|
class="q-pa-none text-body1 blockquote"
|
||||||
|
style="background: none !important"
|
||||||
|
>
|
||||||
|
"{{gerty.sats_quote[0].text}}" <br />~ Satoshi {{gerty.sats_quote[0].date}}
|
||||||
|
</q-card>
|
||||||
</div>
|
</div>
|
||||||
<div class="q-pa-md row items-start q-gutter-md">
|
<div class="q-pa-md row items-start q-gutter-md">
|
||||||
<q-card unelevated flat class="q-pa-none" style="background: none !important;">
|
<q-card unelevated flat class="q-pa-none" style="background: none !important">
|
||||||
<q-card-section class="text-h1 q-pa-none">
|
<q-card-section class="text-h1 q-pa-none">
|
||||||
{{gerty.exchange[0].amount.toFixed(2)}} {{gerty.exchange[0].fiat}}
|
{{gerty.exchange[0].amount.toFixed(2)}} {{gerty.exchange[0].fiat}}
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
<q-card v-for="gertywallet in gertywallets" style="width:380px" flat>
|
<q-card v-for="gertywallet in gertywallets" style="width: 380px" flat>
|
||||||
<q-card-section horizontal class="q-pa-none" :style="`background-color: ${gertywallet.color1}`">
|
<q-card-section
|
||||||
|
horizontal
|
||||||
|
class="q-pa-none"
|
||||||
|
:style="`background-color: ${gertywallet.color1}`"
|
||||||
|
>
|
||||||
<q-card-section class="q-pa-none">
|
<q-card-section class="q-pa-none">
|
||||||
<div class="q-item__section column q-pa-lg q-mr-none text-white q-item__section--side justify-center" :style="`background-color: ${gertywallet.color2}`" >
|
<div
|
||||||
<i aria-hidden="true" role="presentation" class="material-icons q-icon notranslate text-white" style="font-size: 50px;">sentiment_satisfied</i></div>
|
class="q-item__section column q-pa-lg q-mr-none text-white q-item__section--side justify-center"
|
||||||
</q-card-section>
|
:style="`background-color: ${gertywallet.color2}`"
|
||||||
|
>
|
||||||
<div class="q-item__section column q-pa-md q-ml-none text-white q-item__section--main justify-center" style="min-width:200px;">
|
<i
|
||||||
<div class="q-item__label text-white text-h6 text-weight-bolder">{{gertywallet.amount}}</div><div class="q-item__label"><b>{{gertywallet.name}}</b></div>
|
aria-hidden="true"
|
||||||
|
role="presentation"
|
||||||
|
class="material-icons q-icon notranslate text-white"
|
||||||
|
style="font-size: 50px"
|
||||||
|
>sentiment_satisfied</i
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
|
||||||
|
<div
|
||||||
|
class="q-item__section column q-pa-md q-ml-none text-white q-item__section--main justify-center"
|
||||||
|
style="min-width: 200px"
|
||||||
|
>
|
||||||
|
<div class="q-item__label text-white text-h6 text-weight-bolder">
|
||||||
|
{{gertywallet.amount}}
|
||||||
|
</div>
|
||||||
|
<div class="q-item__label"><b>{{gertywallet.name}}</b></div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row q-col-gutter-md ">
|
<div class="row q-col-gutter-md">
|
||||||
<div v-if="gerty.onchain[0]" class="col-12 col-sm-6 col-md-5 col-lg-6">
|
<div v-if="gerty.onchain[0]" class="col-12 col-sm-6 col-md-5 col-lg-6">
|
||||||
<q-card class="q-pa-lg">
|
<q-card class="q-pa-lg">
|
||||||
<p class="text-h4">Onchain Stats</p>
|
<p class="text-h4">Onchain Stats</p>
|
||||||
Difficulty Progress Percent
|
Difficulty Progress Percent
|
||||||
<q-linear-progress size="20px" :value="gerty.onchain[0].difficulty[0].progressPercent/100" color="primary" class="q-mt-sm">
|
<q-linear-progress
|
||||||
|
size="20px"
|
||||||
|
:value="gerty.onchain[0].difficulty[0].progressPercent/100"
|
||||||
|
color="primary"
|
||||||
|
class="q-mt-sm"
|
||||||
|
>
|
||||||
<div class="absolute-full flex flex-center">
|
<div class="absolute-full flex flex-center">
|
||||||
<q-badge color="white" text-color="accent" :label="gerty.onchain[0].difficulty[0].progressPercent.toFixed() + '%'" />
|
<q-badge
|
||||||
|
color="white"
|
||||||
|
text-color="accent"
|
||||||
|
:label="gerty.onchain[0].difficulty[0].progressPercent.toFixed() + '%'"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</q-linear-progress>
|
</q-linear-progress>
|
||||||
|
|
||||||
|
@ -48,15 +82,12 @@
|
||||||
<q-card class="q-pa-lg">
|
<q-card class="q-pa-lg">
|
||||||
<p class="text-h4">LN Stats</p>
|
<p class="text-h4">LN Stats</p>
|
||||||
<q-card-section class="q-pa-none">
|
<q-card-section class="q-pa-none">
|
||||||
<div class="row q-mt-lg q-gutter-sm">
|
<div class="row q-mt-lg q-gutter-sm">{{gerty.ln}}</div>
|
||||||
{{gerty.ln}}
|
|
||||||
</div>
|
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endraw %}
|
{% endraw %} {% endblock %} {% block scripts %}
|
||||||
{% endblock %} {% block scripts %}
|
|
||||||
<script>
|
<script>
|
||||||
Vue.component(VueQrcode.name, VueQrcode)
|
Vue.component(VueQrcode.name, VueQrcode)
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context %} {% block page %}
|
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
|
||||||
<div class="row q-col-gutter-md">
|
%} {% block page %}
|
||||||
|
<div class="row q-col-gutter-md">
|
||||||
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
|
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
|
||||||
<q-card>
|
<q-card>
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<q-btn unelevated color="primary" @click="formDialog.show = true"
|
<q-btn unelevated color="primary" @click="formDialog.show = true"
|
||||||
>New Gerty
|
>New Gerty
|
||||||
</q-btn
|
</q-btn>
|
||||||
>
|
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
<q-card>
|
<q-card>
|
||||||
|
@ -31,8 +31,12 @@
|
||||||
<template v-slot:header="props">
|
<template v-slot:header="props">
|
||||||
<q-tr :props="props">
|
<q-tr :props="props">
|
||||||
<q-th auto-width></q-th>
|
<q-th auto-width></q-th>
|
||||||
<q-th v-for="col in props.cols" :key="col.name" :props="props"
|
<q-th
|
||||||
:class="`col__${col.name} text-truncate elipsis`">
|
v-for="col in props.cols"
|
||||||
|
:key="col.name"
|
||||||
|
:props="props"
|
||||||
|
:class="`col__${col.name} text-truncate elipsis`"
|
||||||
|
>
|
||||||
{{ col.label }}
|
{{ col.label }}
|
||||||
</q-th>
|
</q-th>
|
||||||
<q-th auto-width></q-th>
|
<q-th auto-width></q-th>
|
||||||
|
@ -65,7 +69,7 @@
|
||||||
:href="props.row.gertyJson"
|
:href="props.row.gertyJson"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<q-tooltip>Launch software Gerty</q-tooltip>
|
<q-tooltip>View Gerty API</q-tooltip>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
</q-td>
|
</q-td>
|
||||||
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
||||||
|
@ -103,13 +107,13 @@
|
||||||
<div class="col-12 col-md-5 q-gutter-y-md">
|
<div class="col-12 col-md-5 q-gutter-y-md">
|
||||||
<q-card>
|
<q-card>
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<h6 class="text-subtitle1 q-my-none">{{ SITE_TITLE }} Gerty extension</h6>
|
<h6 class="text-subtitle1 q-my-none">
|
||||||
|
{{ SITE_TITLE }} Gerty extension
|
||||||
|
</h6>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
<q-card-section class="q-pa-none">
|
<q-card-section class="q-pa-none">
|
||||||
<q-separator></q-separator>
|
<q-separator></q-separator>
|
||||||
<q-list>
|
<q-list> {% include "gerty/_api_docs.html" %} </q-list>
|
||||||
{% include "gerty/_api_docs.html" %}
|
|
||||||
</q-list>
|
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
</div>
|
</div>
|
||||||
|
@ -170,172 +174,60 @@
|
||||||
v-model.trim="formDialog.data.refresh_time"
|
v-model.trim="formDialog.data.refresh_time"
|
||||||
label="Refresh time in seconds"
|
label="Refresh time in seconds"
|
||||||
>
|
>
|
||||||
<q-tooltip>The amount of time in seconds between screen updates</q-tooltip>
|
<q-tooltip
|
||||||
|
>The amount of time in seconds between screen updates</q-tooltip
|
||||||
|
>
|
||||||
|
</q-input>
|
||||||
|
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.trim="formDialog.data.utc_offset"
|
||||||
|
label="UTC Time Offset (e.g. -1)"
|
||||||
|
>
|
||||||
|
<q-tooltip
|
||||||
|
>Enter a UTC time offset value (e.g. -1)</q-tooltip
|
||||||
|
>
|
||||||
</q-input>
|
</q-input>
|
||||||
|
|
||||||
<p>Use the toggles below to control what your Gerty will display</p>
|
<p>Use the toggles below to control what your Gerty will display</p>
|
||||||
|
|
||||||
<q-expansion-item
|
|
||||||
expand-separator
|
|
||||||
icon="perm_identity"
|
|
||||||
label="LNbits Wallets"
|
|
||||||
>
|
|
||||||
<q-toggle
|
<q-toggle
|
||||||
v-model="formDialog.data.display_preferences.lnbits_wallets_balance"
|
v-model="formDialog.data.display_preferences.dashboard"
|
||||||
label="Show LNbits wallet balances"
|
label="LNbits Dashboard"
|
||||||
></q-toggle>
|
></q-toggle>
|
||||||
|
|
||||||
</q-expansion-item>
|
|
||||||
<q-expansion-item
|
|
||||||
expand-separator
|
|
||||||
icon="celebration"
|
|
||||||
label="The Fun Stuff"
|
|
||||||
>
|
|
||||||
<q-toggle
|
|
||||||
v-model="toggleStates.fun"
|
|
||||||
label="Toggle all"
|
|
||||||
>
|
|
||||||
<q-tooltip>Toggle all</q-tooltip>
|
|
||||||
</q-toggle>
|
|
||||||
<br>
|
|
||||||
<q-toggle
|
<q-toggle
|
||||||
v-model="formDialog.data.display_preferences.fun_satoshi_quotes"
|
v-model="formDialog.data.display_preferences.fun_satoshi_quotes"
|
||||||
label="Satoshi Quotes"
|
label="Satoshi Quotes"
|
||||||
>
|
>
|
||||||
<q-tooltip>Displays random quotes from Satoshi</q-tooltip>
|
<q-tooltip>Displays random quotes from Satoshi</q-tooltip>
|
||||||
</q-toggle>
|
</q-toggle>
|
||||||
<q-toggle
|
|
||||||
v-model="formDialog.data.display_preferences.fun_pieter_wuille_facts"
|
|
||||||
label="Pieter Wuille Facts"
|
|
||||||
>
|
|
||||||
<q-tooltip>Show accurate facts about Pieter Wuille</q-tooltip>
|
|
||||||
</q-toggle>
|
|
||||||
<q-toggle
|
<q-toggle
|
||||||
v-model="formDialog.data.display_preferences.fun_exchange_market_rate"
|
v-model="formDialog.data.display_preferences.fun_exchange_market_rate"
|
||||||
label="Current dirty fiat to BTC price"
|
label="Fiat to BTC price"
|
||||||
></q-toggle>
|
|
||||||
</q-expansion-item>
|
|
||||||
<q-expansion-item
|
|
||||||
expand-separator
|
|
||||||
icon="link"
|
|
||||||
label="Onchain Information"
|
|
||||||
>
|
|
||||||
<q-toggle
|
|
||||||
v-model="toggleStates.onchain"
|
|
||||||
label="Toggle all"
|
|
||||||
>
|
|
||||||
<q-tooltip>Toggle all</q-tooltip>
|
|
||||||
</q-toggle>
|
|
||||||
<br>
|
|
||||||
<q-toggle
|
|
||||||
v-model="formDialog.data.display_preferences.onchain_difficulty_epoch_progress"
|
|
||||||
label="Percent of current difficulty epoch complete"
|
|
||||||
></q-toggle>
|
></q-toggle>
|
||||||
|
|
||||||
<q-toggle
|
<q-toggle
|
||||||
v-model="formDialog.data.display_preferences.onchain_difficulty_retarget_date"
|
v-model="formDialog.data.display_preferences.onchain_dashboard"
|
||||||
label="Estimated retarget date"
|
label="Onchain Dashboard"
|
||||||
></q-toggle>
|
></q-toggle>
|
||||||
|
|
||||||
<q-toggle
|
|
||||||
v-model="formDialog.data.display_preferences.onchain_difficulty_blocks_remaining"
|
|
||||||
label="Blocks until next difficulty adjustment"
|
|
||||||
></q-toggle>
|
|
||||||
|
|
||||||
<q-toggle
|
|
||||||
v-model="formDialog.data.display_preferences.onchain_difficulty_epoch_time_remaining"
|
|
||||||
label="Estimated time until next difficulty adjustment"
|
|
||||||
></q-toggle>
|
|
||||||
</q-expansion-item>
|
|
||||||
<q-expansion-item
|
|
||||||
expand-separator
|
|
||||||
icon="psychology"
|
|
||||||
label="The Mempool"
|
|
||||||
>
|
|
||||||
<q-toggle
|
|
||||||
v-model="toggleStates.mempool"
|
|
||||||
label="Toggle all"
|
|
||||||
>
|
|
||||||
<q-tooltip>Toggle all</q-tooltip>
|
|
||||||
</q-toggle>
|
|
||||||
<br>
|
|
||||||
|
|
||||||
<q-toggle
|
<q-toggle
|
||||||
v-model="formDialog.data.display_preferences.mempool_recommended_fees"
|
v-model="formDialog.data.display_preferences.mempool_recommended_fees"
|
||||||
label="Recommended fees"
|
label="mempool.space Recommended Fees"
|
||||||
></q-toggle>
|
></q-toggle>
|
||||||
|
|
||||||
<q-toggle
|
<q-toggle
|
||||||
v-model="formDialog.data.display_preferences.mempool_tx_count"
|
v-model="formDialog.data.display_preferences.mining_dashboard"
|
||||||
label="Number of transactions in the mempool"
|
label="Mining Dashboard"
|
||||||
></q-toggle>
|
|
||||||
</q-expansion-item>
|
|
||||||
|
|
||||||
<q-expansion-item
|
|
||||||
expand-separator
|
|
||||||
icon="money"
|
|
||||||
label="Mining Data"
|
|
||||||
>
|
|
||||||
<q-toggle
|
|
||||||
v-model="toggleStates.mining"
|
|
||||||
label="Toggle all"
|
|
||||||
>
|
|
||||||
<q-tooltip>Toggle all</q-tooltip>
|
|
||||||
</q-toggle>
|
|
||||||
<br>
|
|
||||||
<q-toggle
|
|
||||||
v-model="formDialog.data.display_preferences.mining_current_hash_rate"
|
|
||||||
label="Current mining hashrate"
|
|
||||||
></q-toggle>
|
></q-toggle>
|
||||||
|
|
||||||
<q-toggle
|
<q-toggle
|
||||||
v-model="formDialog.data.display_preferences.mining_current_difficulty"
|
v-model="formDialog.data.display_preferences.lightning_dashboard"
|
||||||
label="Current mining difficulty"
|
label="Lightning Network Dashboard"
|
||||||
></q-toggle>
|
|
||||||
</q-expansion-item>
|
|
||||||
|
|
||||||
<q-expansion-item
|
|
||||||
expand-separator
|
|
||||||
icon="bolt"
|
|
||||||
label="Lightning Network"
|
|
||||||
>
|
|
||||||
<q-toggle
|
|
||||||
v-model="toggleStates.lightning"
|
|
||||||
label="Toggle all"
|
|
||||||
>
|
|
||||||
<q-tooltip>Toggle all</q-tooltip>
|
|
||||||
</q-toggle>
|
|
||||||
<br>
|
|
||||||
<q-toggle
|
|
||||||
v-model="formDialog.data.display_preferences.lightning_channel_count"
|
|
||||||
label="Channel count"
|
|
||||||
></q-toggle>
|
></q-toggle>
|
||||||
|
|
||||||
<q-toggle
|
|
||||||
v-model="formDialog.data.display_preferences.lightning_node_count"
|
|
||||||
label="Number of nodes"
|
|
||||||
></q-toggle>
|
|
||||||
|
|
||||||
<q-toggle
|
|
||||||
v-model="formDialog.data.display_preferences.lightning_tor_node_count"
|
|
||||||
label="Number of Tor nodes"
|
|
||||||
></q-toggle>
|
|
||||||
|
|
||||||
<q-toggle
|
|
||||||
v-model="formDialog.data.display_preferences.lightning_clearnet_nodes"
|
|
||||||
label="Number of clearnet nodes"
|
|
||||||
></q-toggle>
|
|
||||||
|
|
||||||
<q-toggle
|
|
||||||
v-model="formDialog.data.display_preferences.lightning_unannounced_nodes"
|
|
||||||
label="Number of unannounced nodes"
|
|
||||||
></q-toggle>
|
|
||||||
|
|
||||||
<q-toggle
|
|
||||||
v-model="formDialog.data.display_preferences.lightning_average_channel_capacity"
|
|
||||||
label="Average channel capacity"
|
|
||||||
></q-toggle>
|
|
||||||
</q-expansion-item>
|
|
||||||
<div class="row q-mt-lg">
|
<div class="row q-mt-lg">
|
||||||
<q-btn
|
<q-btn
|
||||||
unelevated
|
unelevated
|
||||||
|
@ -343,29 +235,27 @@
|
||||||
:disable="formDialog.data.wallet == null || formDialog.data.name == null"
|
:disable="formDialog.data.wallet == null || formDialog.data.name == null"
|
||||||
type="submit"
|
type="submit"
|
||||||
class="q-mr-md"
|
class="q-mr-md"
|
||||||
|
v-if="!formDialog.data.id"
|
||||||
>Create Gerty
|
>Create Gerty
|
||||||
</q-btn
|
</q-btn>
|
||||||
>
|
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="formDialog.data.id"
|
v-else
|
||||||
unelevated
|
unelevated
|
||||||
color="primary"
|
color="primary"
|
||||||
:disable="formDialog.data.wallet == null || formDialog.data.name == null"
|
:disable="formDialog.data.wallet == null || formDialog.data.name == null"
|
||||||
type="submit"
|
type="submit"
|
||||||
>Update Gerty
|
>Update Gerty
|
||||||
</q-btn
|
</q-btn>
|
||||||
>
|
|
||||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
|
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
|
||||||
>Cancel
|
>Cancel
|
||||||
</q-btn
|
</q-btn>
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
</q-form>
|
</q-form>
|
||||||
</q-card>
|
</q-card>
|
||||||
</q-dialog>
|
</q-dialog>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
||||||
<script>
|
<script>
|
||||||
var mapGerty = function (obj) {
|
var mapGerty = function (obj) {
|
||||||
obj.date = Quasar.utils.date.formatDate(
|
obj.date = Quasar.utils.date.formatDate(
|
||||||
new Date(obj.time * 1000),
|
new Date(obj.time * 1000),
|
||||||
|
@ -373,7 +263,7 @@
|
||||||
)
|
)
|
||||||
obj.fsat = new Intl.NumberFormat(LOCALE).format(obj.amount)
|
obj.fsat = new Intl.NumberFormat(LOCALE).format(obj.amount)
|
||||||
obj.gerty = ['/gerty/', obj.id].join('')
|
obj.gerty = ['/gerty/', obj.id].join('')
|
||||||
obj.gertyJson = ['/gerty/api/v1/gerty/', obj.id].join('')
|
obj.gertyJson = ['/gerty/api/v1/gerty/', obj.id, '/0'].join('')
|
||||||
return obj
|
return obj
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -382,14 +272,6 @@
|
||||||
mixins: [windowMixin],
|
mixins: [windowMixin],
|
||||||
data: function () {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
toggleStates: {
|
|
||||||
fun: true,
|
|
||||||
onchain: true,
|
|
||||||
mempool: true,
|
|
||||||
mining: true,
|
|
||||||
lightning: true
|
|
||||||
},
|
|
||||||
oldToggleStates: {},
|
|
||||||
gertys: [],
|
gertys: [],
|
||||||
currencyOptions: [
|
currencyOptions: [
|
||||||
'USD',
|
'USD',
|
||||||
|
@ -567,14 +449,7 @@
|
||||||
],
|
],
|
||||||
gertysTable: {
|
gertysTable: {
|
||||||
columns: [
|
columns: [
|
||||||
{name: 'id', align: 'left', label: 'ID', field: 'id'},
|
|
||||||
{name: 'name', align: 'left', label: 'Name', field: 'name'},
|
{name: 'name', align: 'left', label: 'Name', field: 'name'},
|
||||||
{
|
|
||||||
name: 'lnbits_wallets',
|
|
||||||
align: 'left',
|
|
||||||
label: 'Wallets',
|
|
||||||
field: 'lnbits_wallets'
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'exchange',
|
name: 'exchange',
|
||||||
align: 'left',
|
align: 'left',
|
||||||
|
@ -586,7 +461,9 @@
|
||||||
align: 'left',
|
align: 'left',
|
||||||
label: 'Mempool Endpoint',
|
label: 'Mempool Endpoint',
|
||||||
field: 'mempool_endpoint'
|
field: 'mempool_endpoint'
|
||||||
}
|
},
|
||||||
|
{name: 'id', align: 'left', label: 'Gerty ID', field: 'id'},
|
||||||
|
|
||||||
],
|
],
|
||||||
pagination: {
|
pagination: {
|
||||||
rowsPerPage: 10
|
rowsPerPage: 10
|
||||||
|
@ -595,25 +472,15 @@
|
||||||
formDialog: {
|
formDialog: {
|
||||||
show: false,
|
show: false,
|
||||||
data: {
|
data: {
|
||||||
|
utc_offset: 0,
|
||||||
display_preferences: {
|
display_preferences: {
|
||||||
lnbits_wallets_balance: true,
|
dashboard: true,
|
||||||
fun_satoshi_quotes: true,
|
fun_satoshi_quotes: true,
|
||||||
fun_pieter_wuille_facts: true,
|
|
||||||
fun_exchange_market_rate: true,
|
fun_exchange_market_rate: true,
|
||||||
onchain_difficulty_epoch_progress: true,
|
onchain_dashboard: true,
|
||||||
onchain_difficulty_retarget_date: true,
|
|
||||||
onchain_difficulty_blocks_remaining: true,
|
|
||||||
onchain_difficulty_epoch_time_remaining: true,
|
|
||||||
mempool_recommended_fees: true,
|
mempool_recommended_fees: true,
|
||||||
mempool_tx_count: true,
|
mining_dashboard: true,
|
||||||
mining_current_hash_rate: true,
|
lightning_dashboard: true
|
||||||
mining_current_difficulty: true,
|
|
||||||
lightning_channel_count: true,
|
|
||||||
lightning_node_count: true,
|
|
||||||
lightning_tor_node_count: true,
|
|
||||||
lightning_clearnet_nodes: true,
|
|
||||||
lightning_unannounced_nodes: true,
|
|
||||||
lightning_average_channel_capacity: true,
|
|
||||||
},
|
},
|
||||||
lnbits_wallets: [],
|
lnbits_wallets: [],
|
||||||
mempool_endpoint: "https://mempool.space",
|
mempool_endpoint: "https://mempool.space",
|
||||||
|
@ -629,6 +496,7 @@
|
||||||
methods: {
|
methods: {
|
||||||
closeFormDialog: function () {
|
closeFormDialog: function () {
|
||||||
this.formDialog.data = {
|
this.formDialog.data = {
|
||||||
|
utc_offset: 0,
|
||||||
lnbits_wallets: [],
|
lnbits_wallets: [],
|
||||||
mempool_endpoint: "https://mempool.space",
|
mempool_endpoint: "https://mempool.space",
|
||||||
refresh_time: 300,
|
refresh_time: 300,
|
||||||
|
@ -655,6 +523,7 @@
|
||||||
this.formDialog.data.id = gerty.id
|
this.formDialog.data.id = gerty.id
|
||||||
this.formDialog.data.name = gerty.name
|
this.formDialog.data.name = gerty.name
|
||||||
this.formDialog.data.wallet = gerty.wallet
|
this.formDialog.data.wallet = gerty.wallet
|
||||||
|
this.formDialog.data.utc_offset = gerty.utc_offset
|
||||||
this.formDialog.data.lnbits_wallets = JSON.parse(gerty.lnbits_wallets)
|
this.formDialog.data.lnbits_wallets = JSON.parse(gerty.lnbits_wallets)
|
||||||
this.formDialog.data.exchange = gerty.exchange,
|
this.formDialog.data.exchange = gerty.exchange,
|
||||||
this.formDialog.data.mempool_endpoint = gerty.mempool_endpoint,
|
this.formDialog.data.mempool_endpoint = gerty.mempool_endpoint,
|
||||||
|
@ -680,6 +549,7 @@
|
||||||
var data = {
|
var data = {
|
||||||
name: this.formDialog.data.name,
|
name: this.formDialog.data.name,
|
||||||
wallet: this.formDialog.data.wallet,
|
wallet: this.formDialog.data.wallet,
|
||||||
|
utc_offset: this.formDialog.data.utc_offset,
|
||||||
lnbits_wallets: JSON.stringify(this.formDialog.data.lnbits_wallets),
|
lnbits_wallets: JSON.stringify(this.formDialog.data.lnbits_wallets),
|
||||||
exchange: this.formDialog.data.exchange,
|
exchange: this.formDialog.data.exchange,
|
||||||
mempool_endpoint: this.formDialog.data.mempool_endpoint,
|
mempool_endpoint: this.formDialog.data.mempool_endpoint,
|
||||||
|
@ -698,8 +568,8 @@
|
||||||
data
|
data
|
||||||
)
|
)
|
||||||
.then(function (response) {
|
.then(function (response) {
|
||||||
self.gertys.push(mapGerty(response.data))
|
|
||||||
self.formDialog.show = false
|
self.formDialog.show = false
|
||||||
|
self.gertys.push(mapGerty(response.data))
|
||||||
})
|
})
|
||||||
.catch(function (error) {
|
.catch(function (error) {
|
||||||
LNbits.utils.notifyApiError(error)
|
LNbits.utils.notifyApiError(error)
|
||||||
|
@ -707,6 +577,7 @@
|
||||||
},
|
},
|
||||||
updateGerty: function (wallet, data) {
|
updateGerty: function (wallet, data) {
|
||||||
var self = this
|
var self = this
|
||||||
|
data.utc_offset = this.formDialog.data.utc_offset
|
||||||
data.lnbits_wallets = JSON.stringify(this.formDialog.data.lnbits_wallets)
|
data.lnbits_wallets = JSON.stringify(this.formDialog.data.lnbits_wallets)
|
||||||
data.display_preferences = JSON.stringify(this.formDialog.data.display_preferences)
|
data.display_preferences = JSON.stringify(this.formDialog.data.display_preferences)
|
||||||
LNbits.api
|
LNbits.api
|
||||||
|
@ -720,8 +591,8 @@
|
||||||
self.gertys = _.reject(self.gertys, function (obj) {
|
self.gertys = _.reject(self.gertys, function (obj) {
|
||||||
return obj.id == data.id
|
return obj.id == data.id
|
||||||
})
|
})
|
||||||
self.gertys.push(mapGerty(response.data))
|
|
||||||
self.formDialog.show = false
|
self.formDialog.show = false
|
||||||
|
self.gertys.push(mapGerty(response.data))
|
||||||
})
|
})
|
||||||
.catch(function (error) {
|
.catch(function (error) {
|
||||||
LNbits.utils.notifyApiError(error)
|
LNbits.utils.notifyApiError(error)
|
||||||
|
@ -758,36 +629,13 @@
|
||||||
if (this.g.user.wallets.length) {
|
if (this.g.user.wallets.length) {
|
||||||
this.getGertys()
|
this.getGertys()
|
||||||
}
|
}
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
toggleStates: {
|
|
||||||
handler(toggleStatesValue) {
|
|
||||||
// Switch all the toggles in each section to the relevant state
|
|
||||||
for (const [toggleKey, toggleValue] of Object.entries(toggleStatesValue)) {
|
|
||||||
if (this.oldToggleStates[toggleKey] !== toggleValue) {
|
|
||||||
for (const [dpKey, dpValue] of Object.entries(this.formDialog.data.display_preferences)) {
|
|
||||||
if (dpKey.indexOf(toggleKey) === 0) {
|
|
||||||
this.formDialog.data.display_preferences[dpKey] = toggleValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// This is a weird hack we have to use to get VueJS to persist the previous toggle state between
|
|
||||||
// watches. VueJS passes the old and new values by reference so when comparing objects they
|
|
||||||
// will have the same values unless we do this
|
|
||||||
this.oldToggleStates = JSON.parse(JSON.stringify(toggleStatesValue))
|
|
||||||
},
|
|
||||||
deep: true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %} {% block styles %}
|
||||||
|
<style>
|
||||||
{% block styles %}
|
|
||||||
<style>
|
|
||||||
.col__display_preferences {
|
.col__display_preferences {
|
||||||
border: 1px solid red
|
border: 1px solid red;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
|
@ -1,8 +1,10 @@
|
||||||
|
import json
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
|
|
||||||
from fastapi import Request
|
from fastapi import Request
|
||||||
from fastapi.params import Depends
|
from fastapi.params import Depends
|
||||||
from fastapi.templating import Jinja2Templates
|
from fastapi.templating import Jinja2Templates
|
||||||
|
from loguru import logger
|
||||||
from starlette.exceptions import HTTPException
|
from starlette.exceptions import HTTPException
|
||||||
from starlette.responses import HTMLResponse
|
from starlette.responses import HTMLResponse
|
||||||
|
|
||||||
|
@ -14,18 +16,16 @@ from . import gerty_ext, gerty_renderer
|
||||||
from .crud import get_gerty
|
from .crud import get_gerty
|
||||||
from .views_api import api_gerty_json
|
from .views_api import api_gerty_json
|
||||||
|
|
||||||
import json
|
|
||||||
|
|
||||||
from loguru import logger
|
|
||||||
|
|
||||||
templates = Jinja2Templates(directory="templates")
|
templates = Jinja2Templates(directory="templates")
|
||||||
|
|
||||||
|
|
||||||
@gerty_ext.get("/", response_class=HTMLResponse)
|
@gerty_ext.get("/", response_class=HTMLResponse)
|
||||||
async def index(request: Request, user: User = Depends(check_user_exists)):
|
async def index(request: Request, user: User = Depends(check_user_exists)):
|
||||||
return gerty_renderer().TemplateResponse(
|
return gerty_renderer().TemplateResponse(
|
||||||
"gerty/index.html", {"request": request, "user": user.dict()}
|
"gerty/index.html", {"request": request, "user": user.dict()}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@gerty_ext.get("/{gerty_id}", response_class=HTMLResponse)
|
@gerty_ext.get("/{gerty_id}", response_class=HTMLResponse)
|
||||||
async def display(request: Request, gerty_id):
|
async def display(request: Request, gerty_id):
|
||||||
gerty = await get_gerty(gerty_id)
|
gerty = await get_gerty(gerty_id)
|
||||||
|
@ -34,4 +34,6 @@ async def display(request: Request, gerty_id):
|
||||||
status_code=HTTPStatus.NOT_FOUND, detail="Gerty does not exist."
|
status_code=HTTPStatus.NOT_FOUND, detail="Gerty does not exist."
|
||||||
)
|
)
|
||||||
gertyData = await api_gerty_json(gerty_id)
|
gertyData = await api_gerty_json(gerty_id)
|
||||||
return gerty_renderer().TemplateResponse("gerty/gerty.html", {"request": request, "gerty": gertyData})
|
return gerty_renderer().TemplateResponse(
|
||||||
|
"gerty/gerty.html", {"request": request, "gerty": gertyData}
|
||||||
|
)
|
||||||
|
|
|
@ -1,30 +1,30 @@
|
||||||
import math
|
|
||||||
from http import HTTPStatus
|
|
||||||
import json
|
import json
|
||||||
import httpx
|
import math
|
||||||
import random
|
|
||||||
import os
|
import os
|
||||||
|
import random
|
||||||
import time
|
import time
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from http import HTTPStatus
|
||||||
|
|
||||||
|
import httpx
|
||||||
from fastapi import Query
|
from fastapi import Query
|
||||||
from fastapi.params import Depends
|
from fastapi.params import Depends
|
||||||
|
from fastapi.templating import Jinja2Templates
|
||||||
from lnurl import decode as decode_lnurl
|
from lnurl import decode as decode_lnurl
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from starlette.exceptions import HTTPException
|
from starlette.exceptions import HTTPException
|
||||||
|
|
||||||
from lnbits.core.crud import get_wallet_for_key
|
from lnbits.core.crud import get_user, get_wallet_for_key
|
||||||
from lnbits.core.crud import get_user
|
|
||||||
from lnbits.core.services import create_invoice
|
from lnbits.core.services import create_invoice
|
||||||
from lnbits.core.views.api import api_payment, api_wallet
|
from lnbits.core.views.api import api_payment, api_wallet
|
||||||
from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key
|
from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key
|
||||||
from fastapi.templating import Jinja2Templates
|
|
||||||
|
|
||||||
from . import gerty_ext
|
|
||||||
from .crud import create_gerty, update_gerty, delete_gerty, get_gerty, get_gertys
|
|
||||||
from .models import Gerty
|
|
||||||
|
|
||||||
from lnbits.utils.exchange_rates import satoshis_amount_as_fiat
|
from lnbits.utils.exchange_rates import satoshis_amount_as_fiat
|
||||||
|
|
||||||
from ...settings import LNBITS_PATH
|
from ...settings import LNBITS_PATH
|
||||||
|
from . import gerty_ext
|
||||||
|
from .crud import create_gerty, delete_gerty, get_gerty, get_gertys, update_gerty
|
||||||
|
from .helpers import *
|
||||||
|
from .models import Gerty
|
||||||
|
|
||||||
|
|
||||||
@gerty_ext.get("/api/v1/gerty", status_code=HTTPStatus.OK)
|
@gerty_ext.get("/api/v1/gerty", status_code=HTTPStatus.OK)
|
||||||
|
@ -86,25 +86,23 @@ async def api_gerty_delete(
|
||||||
|
|
||||||
#######################
|
#######################
|
||||||
|
|
||||||
|
|
||||||
@gerty_ext.get("/api/v1/gerty/satoshiquote", status_code=HTTPStatus.OK)
|
@gerty_ext.get("/api/v1/gerty/satoshiquote", status_code=HTTPStatus.OK)
|
||||||
async def api_gerty_satoshi():
|
async def api_gerty_satoshi():
|
||||||
with open(os.path.join(LNBITS_PATH, 'extensions/gerty/static/satoshi.json')) as fd:
|
maxQuoteLength = 186
|
||||||
|
with open(os.path.join(LNBITS_PATH, "extensions/gerty/static/satoshi.json")) as fd:
|
||||||
satoshiQuotes = json.load(fd)
|
satoshiQuotes = json.load(fd)
|
||||||
return satoshiQuotes[random.randint(0, 100)]
|
quote = satoshiQuotes[random.randint(0, len(satoshiQuotes) - 1)]
|
||||||
|
# logger.debug(quote.text)
|
||||||
|
if len(quote["text"]) > maxQuoteLength:
|
||||||
@gerty_ext.get("/api/v1/gerty/pieterwielliequote", status_code=HTTPStatus.OK)
|
logger.debug("Quote is too long, getting another")
|
||||||
async def api_gerty_wuille():
|
return await api_gerty_satoshi()
|
||||||
with open(os.path.join(LNBITS_PATH, 'extensions/gerty/static/pieter_wuille.json')) as fd:
|
else:
|
||||||
data = json.load(fd)
|
return quote
|
||||||
return data['facts'][random.randint(0, (len(data['facts']) - 1))]
|
|
||||||
|
|
||||||
|
|
||||||
@gerty_ext.get("/api/v1/gerty/{gerty_id}/{p}")
|
@gerty_ext.get("/api/v1/gerty/{gerty_id}/{p}")
|
||||||
async def api_gerty_json(
|
async def api_gerty_json(gerty_id: str, p: int = None): # page number
|
||||||
gerty_id: str,
|
|
||||||
p: int = None # page number
|
|
||||||
):
|
|
||||||
gerty = await get_gerty(gerty_id)
|
gerty = await get_gerty(gerty_id)
|
||||||
|
|
||||||
if not gerty:
|
if not gerty:
|
||||||
|
@ -124,108 +122,139 @@ async def api_gerty_json(
|
||||||
enabled_screen_count += 1
|
enabled_screen_count += 1
|
||||||
enabled_screens.append(screen_slug)
|
enabled_screens.append(screen_slug)
|
||||||
|
|
||||||
text = await get_screen_text(p, enabled_screens, gerty)
|
logger.debug("Screeens " + str(enabled_screens))
|
||||||
|
data = await get_screen_data(p, enabled_screens, gerty)
|
||||||
|
|
||||||
next_screen_number = 0 if ((p + 1) >= enabled_screen_count) else p + 1;
|
next_screen_number = 0 if ((p + 1) >= enabled_screen_count) else p + 1
|
||||||
|
|
||||||
# ln = []
|
# get the sleep time
|
||||||
# if gerty.ln_stats and isinstance(gerty.mempool_endpoint, str):
|
sleep_time = gerty.refresh_time if gerty.refresh_time else 300
|
||||||
# async with httpx.AsyncClient() as client:
|
utc_offset = gerty.utc_offset if gerty.utc_offset else 0
|
||||||
# r = await client.get(gerty.mempool_endpoint + "/api/v1/lightning/statistics/latest")
|
if gerty_should_sleep(utc_offset):
|
||||||
# if r:
|
sleep_time_hours = 8
|
||||||
# ln.append(r.json())
|
sleep_time = 60 * 60 * sleep_time_hours
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"settings": {
|
"settings": {
|
||||||
"refreshTime": gerty.refresh_time,
|
"refreshTime": sleep_time,
|
||||||
"requestTimestamp": round(time.time()),
|
"requestTimestamp": get_next_update_time(sleep_time, utc_offset),
|
||||||
"nextScreenNumber": next_screen_number,
|
"nextScreenNumber": next_screen_number,
|
||||||
"showTextBoundRect": True,
|
"showTextBoundRect": False,
|
||||||
"name": gerty.name
|
"name": gerty.name,
|
||||||
},
|
},
|
||||||
"screen": {
|
"screen": {
|
||||||
"slug": get_screen_slug_by_index(p, enabled_screens),
|
"slug": get_screen_slug_by_index(p, enabled_screens),
|
||||||
"group": get_screen_slug_by_index(p, enabled_screens),
|
"group": get_screen_slug_by_index(p, enabled_screens),
|
||||||
"text": text
|
"title": data["title"],
|
||||||
}
|
"areas": data["areas"],
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# Get a screen slug by its position in the screens_list
|
# Get a screen slug by its position in the screens_list
|
||||||
def get_screen_slug_by_index(index: int, screens_list):
|
def get_screen_slug_by_index(index: int, screens_list):
|
||||||
|
if(index < len(screens_list) - 1):
|
||||||
return list(screens_list)[index]
|
return list(screens_list)[index]
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
# Get a list of text items for the screen number
|
# Get a list of text items for the screen number
|
||||||
async def get_screen_text(screen_num: int, screens_list: dict, gerty):
|
async def get_screen_data(screen_num: int, screens_list: dict, gerty):
|
||||||
screen_slug = get_screen_slug_by_index(screen_num, screens_list)
|
screen_slug = get_screen_slug_by_index(screen_num, screens_list)
|
||||||
# first get the relevant slug from the display_preferences
|
# first get the relevant slug from the display_preferences
|
||||||
logger.debug('screen_slug')
|
logger.debug("screen_slug")
|
||||||
logger.debug(screen_slug)
|
logger.debug(screen_slug)
|
||||||
# text = []
|
areas = []
|
||||||
if screen_slug == "lnbits_wallets_balance":
|
title = ""
|
||||||
text = await get_lnbits_wallet_balances(gerty)
|
|
||||||
|
if screen_slug == "dashboard":
|
||||||
|
title = gerty.name
|
||||||
|
areas = await get_dashboard(gerty)
|
||||||
elif screen_slug == "fun_satoshi_quotes":
|
elif screen_slug == "fun_satoshi_quotes":
|
||||||
text = await get_satoshi_quotes()
|
areas.append(await get_satoshi_quotes())
|
||||||
elif screen_slug == "fun_pieter_wuille_facts":
|
|
||||||
text = await get_pieter_wuille_fact()
|
|
||||||
elif screen_slug == "fun_exchange_market_rate":
|
elif screen_slug == "fun_exchange_market_rate":
|
||||||
text = await get_exchange_rate(gerty)
|
areas.append(await get_exchange_rate(gerty))
|
||||||
elif screen_slug == "onchain_difficulty_epoch_progress":
|
elif screen_slug == "onchain_dashboard":
|
||||||
text = await get_onchain_stat(screen_slug, gerty)
|
title = "Onchain Data"
|
||||||
elif screen_slug == "onchain_difficulty_retarget_date":
|
areas = await get_onchain_dashboard(gerty)
|
||||||
text = await get_onchain_stat(screen_slug, gerty)
|
|
||||||
elif screen_slug == "onchain_difficulty_blocks_remaining":
|
|
||||||
text = await get_onchain_stat(screen_slug, gerty)
|
|
||||||
elif screen_slug == "onchain_difficulty_epoch_time_remaining":
|
|
||||||
text = await get_onchain_stat(screen_slug, gerty)
|
|
||||||
elif screen_slug == "mempool_recommended_fees":
|
elif screen_slug == "mempool_recommended_fees":
|
||||||
text = await get_placeholder_text()
|
areas.append(await get_mempool_stat(screen_slug, gerty))
|
||||||
elif screen_slug == "mempool_tx_count":
|
elif screen_slug == "mining_dashboard":
|
||||||
text = await get_mempool_stat(screen_slug, gerty)
|
title = "Mining Data"
|
||||||
elif screen_slug == "mining_current_hash_rate":
|
areas = await get_mining_dashboard(gerty)
|
||||||
text = await get_placeholder_text()
|
elif screen_slug == "lightning_dashboard":
|
||||||
elif screen_slug == "mining_current_difficulty":
|
title = "Lightning Network"
|
||||||
text = await get_placeholder_text()
|
areas = await get_lightning_stats(gerty)
|
||||||
elif screen_slug == "lightning_channel_count":
|
|
||||||
text = await get_placeholder_text()
|
data = {}
|
||||||
elif screen_slug == "lightning_node_count":
|
data["title"] = title
|
||||||
text = await get_placeholder_text()
|
data["areas"] = areas
|
||||||
elif screen_slug == "lightning_tor_node_count":
|
|
||||||
text = await get_placeholder_text()
|
return data
|
||||||
elif screen_slug == "lightning_clearnet_nodes":
|
|
||||||
text = await get_placeholder_text()
|
|
||||||
elif screen_slug == "lightning_unannounced_nodes":
|
# Get the dashboard screen
|
||||||
text = await get_placeholder_text()
|
async def get_dashboard(gerty):
|
||||||
elif screen_slug == "lightning_average_channel_capacity":
|
areas = []
|
||||||
text = await get_placeholder_text()
|
# XC rate
|
||||||
return text
|
text = []
|
||||||
|
amount = await satoshis_amount_as_fiat(100000000, gerty.exchange)
|
||||||
|
text.append(get_text_item_dict(format_number(amount), 40))
|
||||||
|
text.append(get_text_item_dict("BTC{0} price".format(gerty.exchange), 15))
|
||||||
|
areas.append(text)
|
||||||
|
# balance
|
||||||
|
text = []
|
||||||
|
wallets = await get_lnbits_wallet_balances(gerty)
|
||||||
|
text = []
|
||||||
|
for wallet in wallets:
|
||||||
|
text.append(get_text_item_dict("{0}".format(wallet["name"]), 15))
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict("{0} sats".format(format_number(wallet["balance"])), 20)
|
||||||
|
)
|
||||||
|
areas.append(text)
|
||||||
|
|
||||||
|
# Mempool fees
|
||||||
|
text = []
|
||||||
|
text.append(get_text_item_dict(format_number(await get_block_height(gerty)), 40))
|
||||||
|
text.append(get_text_item_dict("Current block height", 15))
|
||||||
|
areas.append(text)
|
||||||
|
|
||||||
|
# difficulty adjustment time
|
||||||
|
text = []
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
await get_time_remaining_next_difficulty_adjustment(gerty), 15
|
||||||
|
)
|
||||||
|
)
|
||||||
|
text.append(get_text_item_dict("until next difficulty adjustment", 12))
|
||||||
|
areas.append(text)
|
||||||
|
|
||||||
|
return areas
|
||||||
|
|
||||||
|
|
||||||
async def get_lnbits_wallet_balances(gerty):
|
async def get_lnbits_wallet_balances(gerty):
|
||||||
# Get Wallet info
|
# Get Wallet info
|
||||||
wallets = []
|
wallets = []
|
||||||
text = []
|
|
||||||
if gerty.lnbits_wallets != "":
|
if gerty.lnbits_wallets != "":
|
||||||
for lnbits_wallet in json.loads(gerty.lnbits_wallets):
|
for lnbits_wallet in json.loads(gerty.lnbits_wallets):
|
||||||
|
|
||||||
wallet = await get_wallet_for_key(key=lnbits_wallet)
|
wallet = await get_wallet_for_key(key=lnbits_wallet)
|
||||||
logger.debug(wallet)
|
logger.debug(wallet.name)
|
||||||
if wallet:
|
if wallet:
|
||||||
wallets.append({
|
wallets.append(
|
||||||
|
{
|
||||||
"name": wallet.name,
|
"name": wallet.name,
|
||||||
"balance": wallet.balance_msat,
|
"balance": wallet.balance_msat / 1000,
|
||||||
"inkey": wallet.inkey,
|
"inkey": wallet.inkey,
|
||||||
})
|
}
|
||||||
text.append(get_text_item_dict(wallet.name, 20))
|
)
|
||||||
text.append(get_text_item_dict(wallet.balance, 40))
|
return wallets
|
||||||
return text
|
|
||||||
|
|
||||||
|
|
||||||
async def get_placeholder_text():
|
async def get_placeholder_text():
|
||||||
return [
|
return [
|
||||||
get_text_item_dict("Some placeholder text", 15, 10, 50),
|
get_text_item_dict("Some placeholder text", 15, 10, 50),
|
||||||
get_text_item_dict("Some placeholder text", 15, 10, 50)
|
get_text_item_dict("Some placeholder text", 15, 10, 50),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -234,19 +263,12 @@ async def get_satoshi_quotes():
|
||||||
text = []
|
text = []
|
||||||
quote = await api_gerty_satoshi()
|
quote = await api_gerty_satoshi()
|
||||||
if quote:
|
if quote:
|
||||||
if quote['text']:
|
if quote["text"]:
|
||||||
text.append(get_text_item_dict(quote['text'], 15))
|
text.append(get_text_item_dict(quote["text"], 15))
|
||||||
if quote['date']:
|
if quote["date"]:
|
||||||
text.append(get_text_item_dict(quote['date'], 15))
|
text.append(
|
||||||
return text
|
get_text_item_dict("Satoshi Nakamoto - {0}".format(quote["date"]), 15)
|
||||||
|
)
|
||||||
|
|
||||||
async def get_pieter_wuille_fact():
|
|
||||||
text = []
|
|
||||||
quote = await api_gerty_wuille()
|
|
||||||
if quote:
|
|
||||||
text.append(get_text_item_dict(quote, 15))
|
|
||||||
text.append(get_text_item_dict("Pieter Wuille facts", 15))
|
|
||||||
return text
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
@ -257,100 +279,174 @@ async def get_exchange_rate(gerty):
|
||||||
try:
|
try:
|
||||||
amount = await satoshis_amount_as_fiat(100000000, gerty.exchange)
|
amount = await satoshis_amount_as_fiat(100000000, gerty.exchange)
|
||||||
if amount:
|
if amount:
|
||||||
price = ('{0} {1}').format(format_number(amount), gerty.exchange)
|
price = format_number(amount)
|
||||||
text.append(get_text_item_dict("Current BTC price", 15))
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
"Current {0}/BTC price".format(gerty.exchange), 15
|
||||||
|
)
|
||||||
|
)
|
||||||
text.append(get_text_item_dict(price, 80))
|
text.append(get_text_item_dict(price, 80))
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
return text
|
return text
|
||||||
|
|
||||||
|
|
||||||
# A helper function get a nicely formated dict for the text
|
async def get_onchain_dashboard(gerty):
|
||||||
def get_text_item_dict(text: str, font_size: int, x_pos: int = None, y_pos: int = None):
|
areas = []
|
||||||
text = {
|
|
||||||
"value": text,
|
|
||||||
"size": font_size
|
|
||||||
}
|
|
||||||
if x_pos is None and y_pos is None:
|
|
||||||
text['position'] = 'center'
|
|
||||||
else:
|
|
||||||
text['x'] = x_pos
|
|
||||||
text['y'] = y_pos
|
|
||||||
return text
|
|
||||||
|
|
||||||
|
|
||||||
async def get_onchain_stat(stat_slug: str, gerty):
|
|
||||||
text = []
|
|
||||||
if isinstance(gerty.mempool_endpoint, str):
|
if isinstance(gerty.mempool_endpoint, str):
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
if (
|
r = await client.get(
|
||||||
stat_slug == "onchain_difficulty_epoch_progress" or
|
gerty.mempool_endpoint + "/api/v1/difficulty-adjustment"
|
||||||
stat_slug == "onchain_difficulty_retarget_date" or
|
)
|
||||||
stat_slug == "onchain_difficulty_blocks_remaining" or
|
text = []
|
||||||
stat_slug == "onchain_difficulty_epoch_time_remaining"
|
stat = round(r.json()["progressPercent"])
|
||||||
):
|
text.append(
|
||||||
r = await client.get(gerty.mempool_endpoint + "/api/v1/difficulty-adjustment")
|
get_text_item_dict("Progress through current epoch", 12)
|
||||||
if stat_slug == "onchain_difficulty_epoch_progress":
|
)
|
||||||
stat = round(r.json()['progressPercent'])
|
text.append(get_text_item_dict("{0}%".format(stat), 60))
|
||||||
text.append(get_text_item_dict("Progress through current difficulty epoch", 15))
|
areas.append(text)
|
||||||
text.append(get_text_item_dict("{0}%".format(stat), 80))
|
|
||||||
elif stat_slug == "onchain_difficulty_retarget_date":
|
text = []
|
||||||
stat = r.json()['estimatedRetargetDate']
|
stat = r.json()["estimatedRetargetDate"]
|
||||||
dt = datetime.fromtimestamp(stat / 1000).strftime("%e %b %Y at %H:%M")
|
dt = datetime.fromtimestamp(stat / 1000).strftime("%e %b %Y at %H:%M")
|
||||||
text.append(get_text_item_dict("Estimated date of next difficulty adjustment", 15))
|
text.append(
|
||||||
text.append(get_text_item_dict(dt, 40))
|
get_text_item_dict("Date of next difficulty adjustment", 12)
|
||||||
elif stat_slug == "onchain_difficulty_blocks_remaining":
|
)
|
||||||
stat = r.json()['remainingBlocks']
|
text.append(get_text_item_dict(dt, 20))
|
||||||
text.append(get_text_item_dict("Blocks remaining until next difficulty adjustment", 15))
|
areas.append(text)
|
||||||
text.append(get_text_item_dict("{0}".format(format_number(stat)), 80))
|
|
||||||
elif stat_slug == "onchain_difficulty_epoch_time_remaining":
|
text = []
|
||||||
stat = r.json()['remainingTime']
|
stat = r.json()["remainingBlocks"]
|
||||||
text.append(get_text_item_dict("Blocks remaining until next difficulty adjustment", 15))
|
text.append(
|
||||||
text.append(get_text_item_dict(get_time_remaining(stat / 1000, 4), 20))
|
get_text_item_dict(
|
||||||
return text
|
"Blocks until next adjustment", 12
|
||||||
|
)
|
||||||
|
)
|
||||||
|
text.append(get_text_item_dict("{0}".format(format_number(stat)), 60))
|
||||||
|
areas.append(text)
|
||||||
|
|
||||||
|
text = []
|
||||||
|
stat = r.json()["remainingTime"]
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
"Blocks until next adjustment", 12
|
||||||
|
)
|
||||||
|
)
|
||||||
|
text.append(get_text_item_dict(get_time_remaining(stat / 1000, 4), 60))
|
||||||
|
areas.append(text)
|
||||||
|
|
||||||
|
return areas
|
||||||
|
|
||||||
|
|
||||||
|
async def get_time_remaining_next_difficulty_adjustment(gerty):
|
||||||
|
if isinstance(gerty.mempool_endpoint, str):
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
r = await client.get(
|
||||||
|
gerty.mempool_endpoint + "/api/v1/difficulty-adjustment"
|
||||||
|
)
|
||||||
|
stat = r.json()["remainingTime"]
|
||||||
|
time = get_time_remaining(stat / 1000, 3)
|
||||||
|
return time
|
||||||
|
|
||||||
|
|
||||||
|
async def get_block_height(gerty):
|
||||||
|
if isinstance(gerty.mempool_endpoint, str):
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
r = await client.get(gerty.mempool_endpoint + "/api/blocks/tip/height")
|
||||||
|
|
||||||
|
return r.json()
|
||||||
|
|
||||||
|
|
||||||
async def get_mempool_stat(stat_slug: str, gerty):
|
async def get_mempool_stat(stat_slug: str, gerty):
|
||||||
text = []
|
text = []
|
||||||
if isinstance(gerty.mempool_endpoint, str):
|
if isinstance(gerty.mempool_endpoint, str):
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
if (
|
if stat_slug == "mempool_tx_count":
|
||||||
stat_slug == "mempool_tx_count"
|
|
||||||
):
|
|
||||||
r = await client.get(gerty.mempool_endpoint + "/api/mempool")
|
r = await client.get(gerty.mempool_endpoint + "/api/mempool")
|
||||||
if stat_slug == "mempool_tx_count":
|
if stat_slug == "mempool_tx_count":
|
||||||
stat = round(r.json()['count'])
|
stat = round(r.json()["count"])
|
||||||
text.append(get_text_item_dict("Transactions in the mempool", 15))
|
text.append(get_text_item_dict("Transactions in the mempool", 15))
|
||||||
text.append(get_text_item_dict("{0}".format(format_number(stat)), 80))
|
text.append(
|
||||||
return text
|
get_text_item_dict("{0}".format(format_number(stat)), 80)
|
||||||
|
)
|
||||||
|
elif stat_slug == "mempool_recommended_fees":
|
||||||
|
y_offset = 60
|
||||||
|
fees = await get_mempool_recommended_fees(gerty)
|
||||||
|
pos_y = 80 + y_offset
|
||||||
|
text.append(get_text_item_dict("mempool.space", 40, 160, pos_y))
|
||||||
|
pos_y = 180 + y_offset
|
||||||
|
text.append(get_text_item_dict("Recommended Tx Fees", 20, 240, pos_y))
|
||||||
|
|
||||||
def get_date_suffix(dayNumber):
|
pos_y = 280 + y_offset
|
||||||
if 4 <= dayNumber <= 20 or 24 <= dayNumber <= 30:
|
text.append(
|
||||||
return "th"
|
get_text_item_dict("{0}".format("No Priority"), 15, 30, pos_y)
|
||||||
else:
|
)
|
||||||
return ["st", "nd", "rd"][dayNumber % 10 - 1]
|
text.append(
|
||||||
|
get_text_item_dict("{0}".format("Low Priority"), 15, 235, pos_y)
|
||||||
# format a number for nice display output
|
)
|
||||||
def format_number(number):
|
text.append(
|
||||||
return ("{:,}".format(round(number)))
|
get_text_item_dict("{0}".format("Medium Priority"), 15, 460, pos_y)
|
||||||
|
)
|
||||||
|
text.append(
|
||||||
def get_time_remaining(seconds, granularity=2):
|
get_text_item_dict("{0}".format("High Priority"), 15, 750, pos_y)
|
||||||
|
|
||||||
intervals = (
|
|
||||||
('weeks', 604800), # 60 * 60 * 24 * 7
|
|
||||||
('days', 86400), # 60 * 60 * 24
|
|
||||||
('hours', 3600), # 60 * 60
|
|
||||||
('minutes', 60),
|
|
||||||
('seconds', 1),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
result = []
|
pos_y = 340 + y_offset
|
||||||
|
font_size = 15
|
||||||
|
fee_append = "/vB"
|
||||||
|
fee_rate = fees["economyFee"]
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
"{0} {1}{2}".format(
|
||||||
|
format_number(fee_rate),
|
||||||
|
("sat" if fee_rate == 1 else "sats"),
|
||||||
|
fee_append,
|
||||||
|
),
|
||||||
|
font_size,
|
||||||
|
30,
|
||||||
|
pos_y,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
for name, count in intervals:
|
fee_rate = fees["hourFee"]
|
||||||
value = seconds // count
|
text.append(
|
||||||
if value:
|
get_text_item_dict(
|
||||||
seconds -= value * count
|
"{0} {1}{2}".format(
|
||||||
if value == 1:
|
format_number(fee_rate),
|
||||||
name = name.rstrip('s')
|
("sat" if fee_rate == 1 else "sats"),
|
||||||
result.append("{} {}".format(round(value), name))
|
fee_append,
|
||||||
return ', '.join(result[:granularity])
|
),
|
||||||
|
font_size,
|
||||||
|
235,
|
||||||
|
pos_y,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
fee_rate = fees["halfHourFee"]
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
"{0} {1}{2}".format(
|
||||||
|
format_number(fee_rate),
|
||||||
|
("sat" if fee_rate == 1 else "sats"),
|
||||||
|
fee_append,
|
||||||
|
),
|
||||||
|
font_size,
|
||||||
|
460,
|
||||||
|
pos_y,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
fee_rate = fees["fastestFee"]
|
||||||
|
text.append(
|
||||||
|
get_text_item_dict(
|
||||||
|
"{0} {1}{2}".format(
|
||||||
|
format_number(fee_rate),
|
||||||
|
("sat" if fee_rate == 1 else "sats"),
|
||||||
|
fee_append,
|
||||||
|
),
|
||||||
|
font_size,
|
||||||
|
750,
|
||||||
|
pos_y,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return text
|
||||||
|
|
|
@ -3,10 +3,10 @@ from typing import List, Optional, Union
|
||||||
from lnbits.helpers import urlsafe_short_hash
|
from lnbits.helpers import urlsafe_short_hash
|
||||||
|
|
||||||
from . import db
|
from . import db
|
||||||
from .models import CreateDomain, Domains, Subdomains
|
from .models import CreateDomain, CreateSubdomain, Domains, Subdomains
|
||||||
|
|
||||||
|
|
||||||
async def create_subdomain(payment_hash, wallet, data: CreateDomain) -> Subdomains:
|
async def create_subdomain(payment_hash, wallet, data: CreateSubdomain) -> Subdomains:
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"""
|
"""
|
||||||
INSERT INTO subdomains.subdomain (id, domain, email, subdomain, ip, wallet, sats, duration, paid, record_type)
|
INSERT INTO subdomains.subdomain (id, domain, email, subdomain, ip, wallet, sats, duration, paid, record_type)
|
||||||
|
|
|
@ -3,24 +3,24 @@ from pydantic.main import BaseModel
|
||||||
|
|
||||||
|
|
||||||
class CreateDomain(BaseModel):
|
class CreateDomain(BaseModel):
|
||||||
wallet: str = Query(...)
|
wallet: str = Query(...) # type: ignore
|
||||||
domain: str = Query(...)
|
domain: str = Query(...) # type: ignore
|
||||||
cf_token: str = Query(...)
|
cf_token: str = Query(...) # type: ignore
|
||||||
cf_zone_id: str = Query(...)
|
cf_zone_id: str = Query(...) # type: ignore
|
||||||
webhook: str = Query("")
|
webhook: str = Query("") # type: ignore
|
||||||
description: str = Query(..., min_length=0)
|
description: str = Query(..., min_length=0) # type: ignore
|
||||||
cost: int = Query(..., ge=0)
|
cost: int = Query(..., ge=0) # type: ignore
|
||||||
allowed_record_types: str = Query(...)
|
allowed_record_types: str = Query(...) # type: ignore
|
||||||
|
|
||||||
|
|
||||||
class CreateSubdomain(BaseModel):
|
class CreateSubdomain(BaseModel):
|
||||||
domain: str = Query(...)
|
domain: str = Query(...) # type: ignore
|
||||||
subdomain: str = Query(...)
|
subdomain: str = Query(...) # type: ignore
|
||||||
email: str = Query(...)
|
email: str = Query(...) # type: ignore
|
||||||
ip: str = Query(...)
|
ip: str = Query(...) # type: ignore
|
||||||
sats: int = Query(..., ge=0)
|
sats: int = Query(..., ge=0) # type: ignore
|
||||||
duration: int = Query(...)
|
duration: int = Query(...) # type: ignore
|
||||||
record_type: str = Query(...)
|
record_type: str = Query(...) # type: ignore
|
||||||
|
|
||||||
|
|
||||||
class Domains(BaseModel):
|
class Domains(BaseModel):
|
||||||
|
|
|
@ -20,7 +20,7 @@ async def wait_for_paid_invoices():
|
||||||
|
|
||||||
|
|
||||||
async def on_invoice_paid(payment: Payment) -> None:
|
async def on_invoice_paid(payment: Payment) -> None:
|
||||||
if payment.extra.get("tag") != "lnsubdomain":
|
if not payment.extra or payment.extra.get("tag") != "lnsubdomain":
|
||||||
# not an lnurlp invoice
|
# not an lnurlp invoice
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -37,7 +37,7 @@ async def on_invoice_paid(payment: Payment) -> None:
|
||||||
)
|
)
|
||||||
|
|
||||||
### Use webhook to notify about cloudflare registration
|
### Use webhook to notify about cloudflare registration
|
||||||
if domain.webhook:
|
if domain and domain.webhook:
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
try:
|
try:
|
||||||
r = await client.post(
|
r = await client.post(
|
||||||
|
|
|
@ -16,7 +16,9 @@ templates = Jinja2Templates(directory="templates")
|
||||||
|
|
||||||
|
|
||||||
@subdomains_ext.get("/", response_class=HTMLResponse)
|
@subdomains_ext.get("/", response_class=HTMLResponse)
|
||||||
async def index(request: Request, user: User = Depends(check_user_exists)):
|
async def index(
|
||||||
|
request: Request, user: User = Depends(check_user_exists) # type:ignore
|
||||||
|
):
|
||||||
return subdomains_renderer().TemplateResponse(
|
return subdomains_renderer().TemplateResponse(
|
||||||
"subdomains/index.html", {"request": request, "user": user.dict()}
|
"subdomains/index.html", {"request": request, "user": user.dict()}
|
||||||
)
|
)
|
||||||
|
|
|
@ -29,12 +29,15 @@ from .crud import (
|
||||||
|
|
||||||
@subdomains_ext.get("/api/v1/domains")
|
@subdomains_ext.get("/api/v1/domains")
|
||||||
async def api_domains(
|
async def api_domains(
|
||||||
g: WalletTypeInfo = Depends(get_key_type), all_wallets: bool = Query(False)
|
g: WalletTypeInfo = Depends(get_key_type), # type: ignore
|
||||||
|
all_wallets: bool = Query(False),
|
||||||
):
|
):
|
||||||
wallet_ids = [g.wallet.id]
|
wallet_ids = [g.wallet.id]
|
||||||
|
|
||||||
if all_wallets:
|
if all_wallets:
|
||||||
wallet_ids = (await get_user(g.wallet.user)).wallet_ids
|
user = await get_user(g.wallet.user)
|
||||||
|
if user is not None:
|
||||||
|
wallet_ids = user.wallet_ids
|
||||||
|
|
||||||
return [domain.dict() for domain in await get_domains(wallet_ids)]
|
return [domain.dict() for domain in await get_domains(wallet_ids)]
|
||||||
|
|
||||||
|
@ -42,7 +45,9 @@ async def api_domains(
|
||||||
@subdomains_ext.post("/api/v1/domains")
|
@subdomains_ext.post("/api/v1/domains")
|
||||||
@subdomains_ext.put("/api/v1/domains/{domain_id}")
|
@subdomains_ext.put("/api/v1/domains/{domain_id}")
|
||||||
async def api_domain_create(
|
async def api_domain_create(
|
||||||
data: CreateDomain, domain_id=None, g: WalletTypeInfo = Depends(get_key_type)
|
data: CreateDomain,
|
||||||
|
domain_id=None,
|
||||||
|
g: WalletTypeInfo = Depends(get_key_type), # type: ignore
|
||||||
):
|
):
|
||||||
if domain_id:
|
if domain_id:
|
||||||
domain = await get_domain(domain_id)
|
domain = await get_domain(domain_id)
|
||||||
|
@ -63,7 +68,9 @@ async def api_domain_create(
|
||||||
|
|
||||||
|
|
||||||
@subdomains_ext.delete("/api/v1/domains/{domain_id}")
|
@subdomains_ext.delete("/api/v1/domains/{domain_id}")
|
||||||
async def api_domain_delete(domain_id, g: WalletTypeInfo = Depends(get_key_type)):
|
async def api_domain_delete(
|
||||||
|
domain_id, g: WalletTypeInfo = Depends(get_key_type) # type: ignore
|
||||||
|
):
|
||||||
domain = await get_domain(domain_id)
|
domain = await get_domain(domain_id)
|
||||||
|
|
||||||
if not domain:
|
if not domain:
|
||||||
|
@ -82,12 +89,14 @@ async def api_domain_delete(domain_id, g: WalletTypeInfo = Depends(get_key_type)
|
||||||
|
|
||||||
@subdomains_ext.get("/api/v1/subdomains")
|
@subdomains_ext.get("/api/v1/subdomains")
|
||||||
async def api_subdomains(
|
async def api_subdomains(
|
||||||
all_wallets: bool = Query(False), g: WalletTypeInfo = Depends(get_key_type)
|
all_wallets: bool = Query(False), g: WalletTypeInfo = Depends(get_key_type) # type: ignore
|
||||||
):
|
):
|
||||||
wallet_ids = [g.wallet.id]
|
wallet_ids = [g.wallet.id]
|
||||||
|
|
||||||
if all_wallets:
|
if all_wallets:
|
||||||
wallet_ids = (await get_user(g.wallet.user)).wallet_ids
|
user = await get_user(g.wallet.user)
|
||||||
|
if user is not None:
|
||||||
|
wallet_ids = user.wallet_ids
|
||||||
|
|
||||||
return [domain.dict() for domain in await get_subdomains(wallet_ids)]
|
return [domain.dict() for domain in await get_subdomains(wallet_ids)]
|
||||||
|
|
||||||
|
@ -173,7 +182,9 @@ async def api_subdomain_send_subdomain(payment_hash):
|
||||||
|
|
||||||
|
|
||||||
@subdomains_ext.delete("/api/v1/subdomains/{subdomain_id}")
|
@subdomains_ext.delete("/api/v1/subdomains/{subdomain_id}")
|
||||||
async def api_subdomain_delete(subdomain_id, g: WalletTypeInfo = Depends(get_key_type)):
|
async def api_subdomain_delete(
|
||||||
|
subdomain_id, g: WalletTypeInfo = Depends(get_key_type) # type: ignore
|
||||||
|
):
|
||||||
subdomain = await get_subdomain(subdomain_id)
|
subdomain = await get_subdomain(subdomain_id)
|
||||||
|
|
||||||
if not subdomain:
|
if not subdomain:
|
||||||
|
|
|
@ -1,95 +0,0 @@
|
||||||
from functools import partial
|
|
||||||
from typing import Callable, List, Optional
|
|
||||||
from urllib.parse import urlparse
|
|
||||||
from urllib.request import parse_http_list as _parse_list_header
|
|
||||||
|
|
||||||
from quart import Request
|
|
||||||
from quart_trio.asgi import TrioASGIHTTPConnection
|
|
||||||
from werkzeug.datastructures import Headers
|
|
||||||
|
|
||||||
|
|
||||||
class ASGIProxyFix(TrioASGIHTTPConnection):
|
|
||||||
def _create_request_from_scope(self, send: Callable) -> Request:
|
|
||||||
headers = Headers()
|
|
||||||
headers["Remote-Addr"] = (self.scope.get("client") or ["<local>"])[0]
|
|
||||||
for name, value in self.scope["headers"]:
|
|
||||||
headers.add(name.decode("latin1").title(), value.decode("latin1"))
|
|
||||||
if self.scope["http_version"] < "1.1":
|
|
||||||
headers.setdefault("Host", self.app.config["SERVER_NAME"] or "")
|
|
||||||
|
|
||||||
path = self.scope["path"]
|
|
||||||
path = path if path[0] == "/" else urlparse(path).path
|
|
||||||
|
|
||||||
x_proto = self._get_real_value(1, headers.get("X-Forwarded-Proto"))
|
|
||||||
if x_proto:
|
|
||||||
self.scope["scheme"] = x_proto
|
|
||||||
|
|
||||||
x_host = self._get_real_value(1, headers.get("X-Forwarded-Host"))
|
|
||||||
if x_host:
|
|
||||||
headers["host"] = x_host.lower()
|
|
||||||
|
|
||||||
return self.app.request_class(
|
|
||||||
self.scope["method"],
|
|
||||||
self.scope["scheme"],
|
|
||||||
path,
|
|
||||||
self.scope["query_string"],
|
|
||||||
headers,
|
|
||||||
self.scope.get("root_path", ""),
|
|
||||||
self.scope["http_version"],
|
|
||||||
max_content_length=self.app.config["MAX_CONTENT_LENGTH"],
|
|
||||||
body_timeout=self.app.config["BODY_TIMEOUT"],
|
|
||||||
send_push_promise=partial(self._send_push_promise, send),
|
|
||||||
scope=self.scope,
|
|
||||||
)
|
|
||||||
|
|
||||||
def _get_real_value(self, trusted: int, value: Optional[str]) -> Optional[str]:
|
|
||||||
"""Get the real value from a list header based on the configured
|
|
||||||
number of trusted proxies.
|
|
||||||
:param trusted: Number of values to trust in the header.
|
|
||||||
:param value: Comma separated list header value to parse.
|
|
||||||
:return: The real value, or ``None`` if there are fewer values
|
|
||||||
than the number of trusted proxies.
|
|
||||||
.. versionchanged:: 1.0
|
|
||||||
Renamed from ``_get_trusted_comma``.
|
|
||||||
.. versionadded:: 0.15
|
|
||||||
"""
|
|
||||||
if not (trusted and value):
|
|
||||||
return None
|
|
||||||
|
|
||||||
values = self.parse_list_header(value)
|
|
||||||
if len(values) >= trusted:
|
|
||||||
return values[-trusted]
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
def parse_list_header(self, value: str) -> List[str]:
|
|
||||||
result = []
|
|
||||||
for item in _parse_list_header(value):
|
|
||||||
if item[:1] == item[-1:] == '"':
|
|
||||||
item = self.unquote_header_value(item[1:-1])
|
|
||||||
result.append(item)
|
|
||||||
return result
|
|
||||||
|
|
||||||
def unquote_header_value(self, value: str, is_filename: bool = False) -> str:
|
|
||||||
r"""Unquotes a header value. (Reversal of :func:`quote_header_value`).
|
|
||||||
This does not use the real unquoting but what browsers are actually
|
|
||||||
using for quoting.
|
|
||||||
.. versionadded:: 0.5
|
|
||||||
:param value: the header value to unquote.
|
|
||||||
:param is_filename: The value represents a filename or path.
|
|
||||||
"""
|
|
||||||
if value and value[0] == value[-1] == '"':
|
|
||||||
# this is not the real unquoting, but fixing this so that the
|
|
||||||
# RFC is met will result in bugs with internet explorer and
|
|
||||||
# probably some other browsers as well. IE for example is
|
|
||||||
# uploading files with "C:\foo\bar.txt" as filename
|
|
||||||
value = value[1:-1]
|
|
||||||
|
|
||||||
# if this is a filename and the starting characters look like
|
|
||||||
# a UNC path, then just return the value without quotes. Using the
|
|
||||||
# replace sequence below on a UNC path has the effect of turning
|
|
||||||
# the leading double slash into a single slash and then
|
|
||||||
# _fix_ie_filename() doesn't work correctly. See #458.
|
|
||||||
if not is_filename or value[:2] != "\\\\":
|
|
||||||
return value.replace("\\\\", "\\").replace('\\"', '"')
|
|
||||||
return value
|
|
|
@ -89,7 +89,33 @@ profile = "black"
|
||||||
ignore_missing_imports = "True"
|
ignore_missing_imports = "True"
|
||||||
files = "lnbits"
|
files = "lnbits"
|
||||||
exclude = """(?x)(
|
exclude = """(?x)(
|
||||||
^lnbits/extensions.
|
^lnbits/extensions/bleskomat.
|
||||||
|
| ^lnbits/extensions/boltz.
|
||||||
|
| ^lnbits/extensions/boltcards.
|
||||||
|
| ^lnbits/extensions/events.
|
||||||
|
| ^lnbits/extensions/hivemind.
|
||||||
|
| ^lnbits/extensions/invoices.
|
||||||
|
| ^lnbits/extensions/jukebox.
|
||||||
|
| ^lnbits/extensions/livestream.
|
||||||
|
| ^lnbits/extensions/lnaddress.
|
||||||
|
| ^lnbits/extensions/lndhub.
|
||||||
|
| ^lnbits/extensions/lnticket.
|
||||||
|
| ^lnbits/extensions/lnurldevice.
|
||||||
|
| ^lnbits/extensions/lnurlp.
|
||||||
|
| ^lnbits/extensions/lnurlpayout.
|
||||||
|
| ^lnbits/extensions/ngrok.
|
||||||
|
| ^lnbits/extensions/offlineshop.
|
||||||
|
| ^lnbits/extensions/paywall.
|
||||||
|
| ^lnbits/extensions/satsdice.
|
||||||
|
| ^lnbits/extensions/satspay.
|
||||||
|
| ^lnbits/extensions/scrub.
|
||||||
|
| ^lnbits/extensions/splitpayments.
|
||||||
|
| ^lnbits/extensions/streamalerts.
|
||||||
|
| ^lnbits/extensions/tipjar.
|
||||||
|
| ^lnbits/extensions/tpos.
|
||||||
|
| ^lnbits/extensions/usermanager.
|
||||||
|
| ^lnbits/extensions/watchonly.
|
||||||
|
| ^lnbits/extensions/withdraw.
|
||||||
| ^lnbits/wallets/lnd_grpc_files.
|
| ^lnbits/wallets/lnd_grpc_files.
|
||||||
)"""
|
)"""
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,11 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
from typing import Tuple
|
|
||||||
|
|
||||||
import pytest_asyncio
|
import pytest_asyncio
|
||||||
from httpx import AsyncClient
|
from httpx import AsyncClient
|
||||||
|
|
||||||
from lnbits.app import create_app
|
from lnbits.app import create_app
|
||||||
from lnbits.commands import migrate_databases
|
from lnbits.commands import migrate_databases
|
||||||
from lnbits.core.crud import create_account, create_wallet, get_wallet
|
from lnbits.core.crud import create_account, create_wallet
|
||||||
from lnbits.core.models import BalanceCheck, Payment, User, Wallet
|
|
||||||
from lnbits.core.views.api import CreateInvoiceData, api_payments_create_invoice
|
from lnbits.core.views.api import CreateInvoiceData, api_payments_create_invoice
|
||||||
from lnbits.db import Database
|
from lnbits.db import Database
|
||||||
from lnbits.settings import HOST, PORT
|
from lnbits.settings import HOST, PORT
|
||||||
|
|
|
@ -1,16 +1,9 @@
|
||||||
import hashlib
|
import hashlib
|
||||||
from binascii import hexlify
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import pytest_asyncio
|
|
||||||
|
|
||||||
from lnbits import bolt11
|
from lnbits import bolt11
|
||||||
from lnbits.core.crud import get_wallet
|
from lnbits.core.views.api import api_payment
|
||||||
from lnbits.core.views.api import (
|
|
||||||
CreateInvoiceData,
|
|
||||||
api_payment,
|
|
||||||
api_payments_create_invoice,
|
|
||||||
)
|
|
||||||
from lnbits.settings import wallet_class
|
from lnbits.settings import wallet_class
|
||||||
|
|
||||||
from ...helpers import get_random_invoice_data, is_regtest
|
from ...helpers import get_random_invoice_data, is_regtest
|
||||||
|
|
|
@ -1,7 +1,4 @@
|
||||||
import pytest
|
import pytest
|
||||||
import pytest_asyncio
|
|
||||||
|
|
||||||
from tests.conftest import client
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
|
|
@ -1,7 +1,4 @@
|
||||||
import pytest
|
import pytest
|
||||||
import pytest_asyncio
|
|
||||||
|
|
||||||
from lnbits.core.crud import get_wallet
|
|
||||||
|
|
||||||
|
|
||||||
# check if the client is working
|
# check if the client is working
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import json
|
import json
|
||||||
import secrets
|
import secrets
|
||||||
|
|
||||||
import pytest
|
|
||||||
import pytest_asyncio
|
import pytest_asyncio
|
||||||
|
|
||||||
from lnbits.core.crud import create_account, create_wallet
|
from lnbits.core.crud import create_account, create_wallet
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import secrets
|
import secrets
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import pytest_asyncio
|
|
||||||
|
|
||||||
from lnbits.core.crud import get_wallet
|
from lnbits.core.crud import get_wallet
|
||||||
from lnbits.extensions.bleskomat.crud import get_bleskomat_lnurl
|
from lnbits.extensions.bleskomat.crud import get_bleskomat_lnurl
|
||||||
|
@ -10,8 +9,6 @@ from lnbits.extensions.bleskomat.helpers import (
|
||||||
query_to_signing_payload,
|
query_to_signing_payload,
|
||||||
)
|
)
|
||||||
from lnbits.settings import HOST, PORT
|
from lnbits.settings import HOST, PORT
|
||||||
from tests.conftest import client
|
|
||||||
from tests.extensions.bleskomat.conftest import bleskomat, lnurl
|
|
||||||
from tests.helpers import credit_wallet, is_regtest
|
from tests.helpers import credit_wallet, is_regtest
|
||||||
from tests.mocks import WALLET
|
from tests.mocks import WALLET
|
||||||
|
|
||||||
|
|
|
@ -1,17 +1,7 @@
|
||||||
import asyncio
|
|
||||||
import json
|
|
||||||
import secrets
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
import pytest_asyncio
|
import pytest_asyncio
|
||||||
|
|
||||||
from lnbits.core.crud import create_account, create_wallet, get_wallet
|
from lnbits.extensions.boltz.boltz import create_reverse_swap
|
||||||
from lnbits.extensions.boltz.boltz import create_reverse_swap, create_swap
|
from lnbits.extensions.boltz.models import CreateReverseSubmarineSwap
|
||||||
from lnbits.extensions.boltz.models import (
|
|
||||||
CreateReverseSubmarineSwap,
|
|
||||||
CreateSubmarineSwap,
|
|
||||||
)
|
|
||||||
from tests.mocks import WALLET
|
|
||||||
|
|
||||||
|
|
||||||
@pytest_asyncio.fixture(scope="session")
|
@pytest_asyncio.fixture(scope="session")
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import pytest
|
import pytest
|
||||||
import pytest_asyncio
|
|
||||||
|
|
||||||
from tests.helpers import is_fake, is_regtest
|
from tests.helpers import is_fake
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
|
|
@ -1,17 +1,10 @@
|
||||||
import asyncio
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import pytest_asyncio
|
|
||||||
|
|
||||||
from lnbits.extensions.boltz.boltz import create_reverse_swap, create_swap
|
|
||||||
from lnbits.extensions.boltz.crud import (
|
from lnbits.extensions.boltz.crud import (
|
||||||
create_reverse_submarine_swap,
|
create_reverse_submarine_swap,
|
||||||
create_submarine_swap,
|
|
||||||
get_reverse_submarine_swap,
|
get_reverse_submarine_swap,
|
||||||
get_submarine_swap,
|
|
||||||
)
|
)
|
||||||
from tests.extensions.boltz.conftest import reverse_swap
|
from tests.helpers import is_fake
|
||||||
from tests.helpers import is_fake, is_regtest
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import pytest
|
|
||||||
import pytest_asyncio
|
import pytest_asyncio
|
||||||
|
|
||||||
from lnbits.core.crud import create_account, create_wallet
|
from lnbits.core.crud import create_account, create_wallet
|
||||||
|
|
|
@ -1,12 +1,4 @@
|
||||||
import pytest
|
import pytest
|
||||||
import pytest_asyncio
|
|
||||||
from loguru import logger
|
|
||||||
|
|
||||||
from lnbits.core.crud import get_wallet
|
|
||||||
from tests.conftest import adminkey_headers_from, client, invoice
|
|
||||||
from tests.extensions.invoices.conftest import accounting_invoice, invoices_wallet
|
|
||||||
from tests.helpers import credit_wallet
|
|
||||||
from tests.mocks import WALLET
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
|
Loading…
Add table
Reference in a new issue