Merge branch 'bcgerty' into gerty

This commit is contained in:
ben 2022-10-24 15:37:52 +01:00
commit 07a95532be
45 changed files with 2592 additions and 1253 deletions

View file

@ -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

View file

@ -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)

View file

@ -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()

View file

@ -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>

View file

@ -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://")

View file

@ -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

View file

@ -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,)
) )

View file

@ -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 = ?

View file

@ -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):

View file

@ -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)

View file

@ -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
) )

View file

@ -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()}
) )

View file

@ -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:

View file

@ -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()}
) )

View file

@ -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

View file

@ -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

View 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])

View file

@ -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;"
)

View file

@ -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

View 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
)

View file

@ -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."
]
}

File diff suppressed because it is too large Load diff

View file

@ -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/&lt;gerty_id&gt; -H "X-Api-Key: &lt;admin_key&gt;" }}gerty/api/v1/gertys/&lt;gerty_id&gt; -H "X-Api-Key:
&lt;admin_key&gt;"
</code> </code>
</q-card-section> </q-card-section>
</q-card> </q-card>

View file

@ -1,25 +1,50 @@
{% 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
unelevated
flat
class="q-pa-none text-body1 blockquote"
style="background: none !important"
>
"{{gerty.sats_quote[0].text}}" <br />~ Satoshi {{gerty.sats_quote[0].date}} "{{gerty.sats_quote[0].text}}" <br />~ Satoshi {{gerty.sats_quote[0].date}}
</q-card> </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"
:style="`background-color: ${gertywallet.color2}`"
>
<i
aria-hidden="true"
role="presentation"
class="material-icons q-icon notranslate text-white"
style="font-size: 50px"
>sentiment_satisfied</i
>
</div>
</q-card-section> </q-card-section>
<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
<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> 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> </div>
</q-card-section> </q-card-section>
</q-card> </q-card>
@ -30,9 +55,18 @@
<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)

View file

@ -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
%} {% block page %}
<div class="row q-col-gutter-md"> <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,22 +235,20 @@
: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>
@ -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 %}
{% block styles %}
<style> <style>
.col__display_preferences { .col__display_preferences {
border: 1px solid red border: 1px solid red;
} }
</style> </style>
{% endblock %} {% endblock %}

View file

@ -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}
)

View file

@ -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

View file

@ -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)

View file

@ -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):

View file

@ -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(

View file

@ -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()}
) )

View file

@ -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:

View file

@ -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

View file

@ -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.
)""" )"""

View file

@ -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

View file

@ -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

View file

@ -1,7 +1,4 @@
import pytest import pytest
import pytest_asyncio
from tests.conftest import client
@pytest.mark.asyncio @pytest.mark.asyncio

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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")

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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