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**
- `OPENNODE_API_ENDPOINT`: https://api.opennode.com/
- `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(ASGIProxyFix)
check_funding_source(app)
register_assets(app)

View file

@ -361,6 +361,35 @@ new Vue({
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) {
this.parse.data.request = res
this.decodeRequest()

View file

@ -653,6 +653,7 @@
<q-responsive :ratio="1">
<qrcode-stream
@decode="decodeQR"
@init="onInitQR"
class="rounded-borders"
></qrcode-stream>
</q-responsive>
@ -671,6 +672,7 @@
<div class="text-center q-mb-lg">
<qrcode-stream
@decode="decodeQR"
@init="onInitQR"
class="rounded-borders"
></qrcode-stream>
</div>

View file

@ -6,7 +6,7 @@ import time
import uuid
from http import HTTPStatus
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
import async_timeout
@ -476,7 +476,7 @@ async def api_lnurlscan(code: str, wallet: WalletTypeInfo = Depends(get_key_type
except:
# parse internet identifier (user@domain.com)
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
url = (
("http://" if domain.endswith(".onion") else "https://")

View file

@ -6,7 +6,6 @@ from urllib.parse import urlparse
from fastapi import HTTPException
from loguru import logger
from starlette.requests import Request
from starlette.responses import HTMLResponse
from lnbits import bolt11

View file

@ -10,7 +10,7 @@ from .models import Copilots, CreateCopilotData
async def create_copilot(
data: CreateCopilotData, inkey: Optional[str] = ""
) -> Copilots:
) -> Optional[Copilots]:
copilot_id = urlsafe_short_hash()
await db.execute(
"""
@ -67,19 +67,19 @@ async def create_copilot(
async def update_copilot(
data: CreateCopilotData, copilot_id: Optional[str] = ""
data: CreateCopilotData, copilot_id: str
) -> Optional[Copilots]:
q = ", ".join([f"{field[0]} = ?" for field in data])
items = [f"{field[1]}" for field in data]
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(
"SELECT * FROM copilot.newer_copilots WHERE id = ?", (copilot_id,)
)
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(
"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:
webhook = None
data = None
if payment.extra.get("tag") != "copilot":
if not payment.extra or payment.extra.get("tag") != "copilot":
# not an copilot invoice
return
@ -71,12 +71,12 @@ async def on_invoice_paid(payment: Payment) -> None:
async def mark_webhook_sent(payment: Payment, status: int) -> None:
payment.extra["wh_status"] = status
await core_db.execute(
"""
UPDATE apipayments SET extra = ?
WHERE hash = ?
""",
(json.dumps(payment.extra), payment.payment_hash),
)
if payment.extra:
payment.extra["wh_status"] = status
await core_db.execute(
"""
UPDATE apipayments SET extra = ?
WHERE hash = ?
""",
(json.dumps(payment.extra), payment.payment_hash),
)

View file

@ -15,7 +15,9 @@ templates = Jinja2Templates(directory="templates")
@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(
"copilot/index.html", {"request": request, "user": user.dict()}
)
@ -44,7 +46,7 @@ class ConnectionManager:
async def connect(self, websocket: WebSocket, copilot_id: str):
await websocket.accept()
websocket.id = copilot_id
websocket.id = copilot_id # type: ignore
self.active_connections.append(websocket)
def disconnect(self, websocket: WebSocket):
@ -52,7 +54,7 @@ class ConnectionManager:
async def send_personal_message(self, message: str, copilot_id: str):
for connection in self.active_connections:
if connection.id == copilot_id:
if connection.id == copilot_id: # type: ignore
await connection.send_text(message)
async def broadcast(self, message: str):

View file

@ -23,7 +23,7 @@ from .views import updater
@copilot_ext.get("/api/v1/copilot")
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
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(
req: Request,
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)
if not copilot:
@ -54,7 +54,7 @@ async def api_copilot_retrieve(
async def api_copilot_create_or_update(
data: CreateCopilotData,
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.wallet = wallet.wallet.id
@ -67,7 +67,8 @@ async def api_copilot_create_or_update(
@copilot_ext.delete("/api/v1/copilot/{copilot_id}")
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)

View file

@ -98,21 +98,21 @@ async def get_discordbot_wallet(wallet_id: str) -> Optional[Wallets]:
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(
"SELECT * FROM discordbot.wallets WHERE admin = ?", (admin_id,)
)
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(
"""SELECT * FROM discordbot.wallets WHERE "user" = ?""", (user_id,)
)
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(
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)
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(
"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)
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
return [user.dict() for user in await get_discordbot_users(user_id)]
@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)
return user.dict()
if user:
return user.dict()
@discordbot_ext.post("/api/v1/users", status_code=HTTPStatus.CREATED)
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)
full = user.dict()
full["wallets"] = [
wallet.dict() for wallet in await get_discordbot_users_wallets(user.id)
]
wallets = await get_discordbot_users_wallets(user.id)
if wallets:
full["wallets"] = [wallet for wallet in wallets]
return full
@discordbot_ext.delete("/api/v1/users/{user_id}")
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)
if not user:
@ -75,7 +80,7 @@ async def api_discordbot_activate_extension(
raise HTTPException(
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"}
@ -84,7 +89,7 @@ async def api_discordbot_activate_extension(
@discordbot_ext.post("/api/v1/wallets")
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_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")
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
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}")
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)
@discordbot_ext.get("/api/v1/wallets/{user_id}")
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}")
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)
if not get_wallet:

View file

@ -12,7 +12,10 @@ templates = Jinja2Templates(directory="templates")
@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(
"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")
gerty_ext: APIRouter = APIRouter(prefix="/gerty", tags=["Gerty"])
def gerty_renderer():
return template_renderer(["lnbits/extensions/gerty/templates"])
from .views import * # noqa
from .views_api import * # noqa

View file

@ -14,21 +14,25 @@ async def create_gerty(wallet_id: str, data: Gerty) -> Gerty:
id,
name,
wallet,
utc_offset,
lnbits_wallets,
mempool_endpoint,
exchange,
display_preferences
display_preferences,
refresh_time
)
VALUES (?, ?, ?, ?, ?, ?, ?)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
gerty_id,
data.name,
data.wallet,
data.utc_offset,
data.lnbits_wallets,
data.mempool_endpoint,
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"
return gerty
async def update_gerty(gerty_id: str, **kwargs) -> Gerty:
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
await db.execute(
@ -43,6 +48,7 @@ async def update_gerty(gerty_id: str, **kwargs) -> Gerty:
)
return await get_gerty(gerty_id)
async def get_gerty(gerty_id: str) -> Optional[Gerty]:
row = await db.fetchone("SELECT * FROM gerty.gertys WHERE id = ?", (gerty_id,))
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

@ -15,4 +15,12 @@ async def m001_initial(db):
display_preferences TEXT
);
"""
)
)
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,16 +4,22 @@ from typing import Optional
from fastapi import Query
from pydantic import BaseModel
class Gerty(BaseModel):
id: str = Query(None)
name: str
wallet: str
refresh_time: 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
exchange: str = Query(None) # BTC <-> Fiat exchange rate to pull ie "USD", in 0.0001 and sats
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
exchange: str = Query(
None
) # BTC <-> Fiat exchange rate to pull ie "USD", in 0.0001 and sats
display_preferences: str = Query(None)
@classmethod
def from_row(cls, row: Row) -> "Gerty":
return cls(**dict(row))
return cls(**dict(row))

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>
<code
>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>
</q-card-section>
</q-card>

View file

@ -1,40 +1,74 @@
{% extends "public.html" %} {% block toolbar_title %} {{ gerty.name }}{% endblock %}{% block page %}
{% raw %}
{% extends "public.html" %} {% block toolbar_title %} {{ gerty.name }}{%
endblock %}{% block page %} {% raw %}
<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;">
"{{gerty.sats_quote[0].text}}" <br/>~ Satoshi {{gerty.sats_quote[0].date}}
</q-card>
<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}}
</q-card>
</div>
<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">
{{gerty.exchange[0].amount.toFixed(2)}} {{gerty.exchange[0].fiat}}
</q-card-section>
</q-card>
<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 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}`" >
<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 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 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}`"
>
<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>
<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
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 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">
<q-card class="q-pa-lg">
<p class="text-h4">Onchain Stats</p>
Difficulty Progress Percent
<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">
<q-badge color="white" text-color="accent" :label="gerty.onchain[0].difficulty[0].progressPercent.toFixed() + '%'" />
</div>
</q-linear-progress>
<p class="text-h4">Onchain Stats</p>
Difficulty Progress Percent
<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">
<q-badge
color="white"
text-color="accent"
:label="gerty.onchain[0].difficulty[0].progressPercent.toFixed() + '%'"
/>
</div>
</q-linear-progress>
<q-card-section class="q-pa-none">
<div class="row q-mt-lg q-gutter-sm text-h6">
@ -46,17 +80,14 @@
</div>
<div v-if="gerty.ln[0]" class="col-12 col-sm-6 col-md-5 col-lg-6">
<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">
<div class="row q-mt-lg q-gutter-sm">
{{gerty.ln}}
</div>
<div class="row q-mt-lg q-gutter-sm">{{gerty.ln}}</div>
</q-card-section>
</q-card>
</div>
</div>
{% endraw %}
{% endblock %} {% block scripts %}
{% endraw %} {% endblock %} {% block scripts %}
<script>
Vue.component(VueQrcode.name, VueQrcode)

File diff suppressed because it is too large Load diff

View file

@ -1,8 +1,10 @@
import json
from http import HTTPStatus
from fastapi import Request
from fastapi.params import Depends
from fastapi.templating import Jinja2Templates
from loguru import logger
from starlette.exceptions import HTTPException
from starlette.responses import HTMLResponse
@ -14,18 +16,16 @@ from . import gerty_ext, gerty_renderer
from .crud import get_gerty
from .views_api import api_gerty_json
import json
from loguru import logger
templates = Jinja2Templates(directory="templates")
@gerty_ext.get("/", response_class=HTMLResponse)
async def index(request: Request, user: User = Depends(check_user_exists)):
return gerty_renderer().TemplateResponse(
"gerty/index.html", {"request": request, "user": user.dict()}
)
@gerty_ext.get("/{gerty_id}", response_class=HTMLResponse)
async def display(request: Request, 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."
)
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,35 +1,35 @@
import math
from http import HTTPStatus
import json
import httpx
import random
import math
import os
import random
import time
from datetime import datetime
from http import HTTPStatus
import httpx
from fastapi import Query
from fastapi.params import Depends
from fastapi.templating import Jinja2Templates
from lnurl import decode as decode_lnurl
from loguru import logger
from starlette.exceptions import HTTPException
from lnbits.core.crud import get_wallet_for_key
from lnbits.core.crud import get_user
from lnbits.core.crud import get_user, get_wallet_for_key
from lnbits.core.services import create_invoice
from lnbits.core.views.api import api_payment, api_wallet
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 ...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)
async def api_gertys(
all_wallets: bool = Query(False), wallet: WalletTypeInfo = Depends(get_key_type)
all_wallets: bool = Query(False), wallet: WalletTypeInfo = Depends(get_key_type)
):
wallet_ids = [wallet.wallet.id]
if all_wallets:
@ -41,9 +41,9 @@ async def api_gertys(
@gerty_ext.post("/api/v1/gerty", status_code=HTTPStatus.CREATED)
@gerty_ext.put("/api/v1/gerty/{gerty_id}", status_code=HTTPStatus.OK)
async def api_link_create_or_update(
data: Gerty,
wallet: WalletTypeInfo = Depends(get_key_type),
gerty_id: str = Query(None),
data: Gerty,
wallet: WalletTypeInfo = Depends(get_key_type),
gerty_id: str = Query(None),
):
if gerty_id:
gerty = await get_gerty(gerty_id)
@ -68,7 +68,7 @@ async def api_link_create_or_update(
@gerty_ext.delete("/api/v1/gerty/{gerty_id}")
async def api_gerty_delete(
gerty_id: str, wallet: WalletTypeInfo = Depends(require_admin_key)
gerty_id: str, wallet: WalletTypeInfo = Depends(require_admin_key)
):
gerty = await get_gerty(gerty_id)
@ -86,25 +86,23 @@ async def api_gerty_delete(
#######################
@gerty_ext.get("/api/v1/gerty/satoshiquote", status_code=HTTPStatus.OK)
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)
return satoshiQuotes[random.randint(0, 100)]
@gerty_ext.get("/api/v1/gerty/pieterwielliequote", status_code=HTTPStatus.OK)
async def api_gerty_wuille():
with open(os.path.join(LNBITS_PATH, 'extensions/gerty/static/pieter_wuille.json')) as fd:
data = json.load(fd)
return data['facts'][random.randint(0, (len(data['facts']) - 1))]
quote = satoshiQuotes[random.randint(0, len(satoshiQuotes) - 1)]
# logger.debug(quote.text)
if len(quote["text"]) > maxQuoteLength:
logger.debug("Quote is too long, getting another")
return await api_gerty_satoshi()
else:
return quote
@gerty_ext.get("/api/v1/gerty/{gerty_id}/{p}")
async def api_gerty_json(
gerty_id: str,
p: int = None # page number
):
async def api_gerty_json(gerty_id: str, p: int = None): # page number
gerty = await get_gerty(gerty_id)
if not gerty:
@ -124,108 +122,139 @@ async def api_gerty_json(
enabled_screen_count += 1
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 = []
# if gerty.ln_stats and isinstance(gerty.mempool_endpoint, str):
# async with httpx.AsyncClient() as client:
# r = await client.get(gerty.mempool_endpoint + "/api/v1/lightning/statistics/latest")
# if r:
# ln.append(r.json())
# get the sleep time
sleep_time = gerty.refresh_time if gerty.refresh_time else 300
utc_offset = gerty.utc_offset if gerty.utc_offset else 0
if gerty_should_sleep(utc_offset):
sleep_time_hours = 8
sleep_time = 60 * 60 * sleep_time_hours
return {
"settings": {
"refreshTime": gerty.refresh_time,
"requestTimestamp": round(time.time()),
"refreshTime": sleep_time,
"requestTimestamp": get_next_update_time(sleep_time, utc_offset),
"nextScreenNumber": next_screen_number,
"showTextBoundRect": True,
"name": gerty.name
"showTextBoundRect": False,
"name": gerty.name,
},
"screen": {
"slug": 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
def get_screen_slug_by_index(index: int, screens_list):
return list(screens_list)[index]
if(index < len(screens_list) - 1):
return list(screens_list)[index]
else:
return None
# 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)
# first get the relevant slug from the display_preferences
logger.debug('screen_slug')
logger.debug("screen_slug")
logger.debug(screen_slug)
# text = []
if screen_slug == "lnbits_wallets_balance":
text = await get_lnbits_wallet_balances(gerty)
areas = []
title = ""
if screen_slug == "dashboard":
title = gerty.name
areas = await get_dashboard(gerty)
elif screen_slug == "fun_satoshi_quotes":
text = await get_satoshi_quotes()
elif screen_slug == "fun_pieter_wuille_facts":
text = await get_pieter_wuille_fact()
areas.append(await get_satoshi_quotes())
elif screen_slug == "fun_exchange_market_rate":
text = await get_exchange_rate(gerty)
elif screen_slug == "onchain_difficulty_epoch_progress":
text = await get_onchain_stat(screen_slug, gerty)
elif screen_slug == "onchain_difficulty_retarget_date":
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)
areas.append(await get_exchange_rate(gerty))
elif screen_slug == "onchain_dashboard":
title = "Onchain Data"
areas = await get_onchain_dashboard(gerty)
elif screen_slug == "mempool_recommended_fees":
text = await get_placeholder_text()
elif screen_slug == "mempool_tx_count":
text = await get_mempool_stat(screen_slug, gerty)
elif screen_slug == "mining_current_hash_rate":
text = await get_placeholder_text()
elif screen_slug == "mining_current_difficulty":
text = await get_placeholder_text()
elif screen_slug == "lightning_channel_count":
text = await get_placeholder_text()
elif screen_slug == "lightning_node_count":
text = await get_placeholder_text()
elif screen_slug == "lightning_tor_node_count":
text = await get_placeholder_text()
elif screen_slug == "lightning_clearnet_nodes":
text = await get_placeholder_text()
elif screen_slug == "lightning_unannounced_nodes":
text = await get_placeholder_text()
elif screen_slug == "lightning_average_channel_capacity":
text = await get_placeholder_text()
return text
areas.append(await get_mempool_stat(screen_slug, gerty))
elif screen_slug == "mining_dashboard":
title = "Mining Data"
areas = await get_mining_dashboard(gerty)
elif screen_slug == "lightning_dashboard":
title = "Lightning Network"
areas = await get_lightning_stats(gerty)
data = {}
data["title"] = title
data["areas"] = areas
return data
# Get the dashboard screen
async def get_dashboard(gerty):
areas = []
# XC rate
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):
# Get Wallet info
wallets = []
text = []
if gerty.lnbits_wallets != "":
for lnbits_wallet in json.loads(gerty.lnbits_wallets):
wallet = await get_wallet_for_key(key=lnbits_wallet)
logger.debug(wallet)
logger.debug(wallet.name)
if wallet:
wallets.append({
"name": wallet.name,
"balance": wallet.balance_msat,
"inkey": wallet.inkey,
})
text.append(get_text_item_dict(wallet.name, 20))
text.append(get_text_item_dict(wallet.balance, 40))
return text
wallets.append(
{
"name": wallet.name,
"balance": wallet.balance_msat / 1000,
"inkey": wallet.inkey,
}
)
return wallets
async def get_placeholder_text():
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),
]
@ -234,19 +263,12 @@ async def get_satoshi_quotes():
text = []
quote = await api_gerty_satoshi()
if quote:
if quote['text']:
text.append(get_text_item_dict(quote['text'], 15))
if quote['date']:
text.append(get_text_item_dict(quote['date'], 15))
return text
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))
if quote["text"]:
text.append(get_text_item_dict(quote["text"], 15))
if quote["date"]:
text.append(
get_text_item_dict("Satoshi Nakamoto - {0}".format(quote["date"]), 15)
)
return text
@ -257,100 +279,174 @@ async def get_exchange_rate(gerty):
try:
amount = await satoshis_amount_as_fiat(100000000, gerty.exchange)
if amount:
price = ('{0} {1}').format(format_number(amount), gerty.exchange)
text.append(get_text_item_dict("Current BTC price", 15))
price = format_number(amount)
text.append(
get_text_item_dict(
"Current {0}/BTC price".format(gerty.exchange), 15
)
)
text.append(get_text_item_dict(price, 80))
except:
pass
return text
# 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):
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 = []
async def get_onchain_dashboard(gerty):
areas = []
if isinstance(gerty.mempool_endpoint, str):
async with httpx.AsyncClient() as client:
if (
stat_slug == "onchain_difficulty_epoch_progress" or
stat_slug == "onchain_difficulty_retarget_date" or
stat_slug == "onchain_difficulty_blocks_remaining" or
stat_slug == "onchain_difficulty_epoch_time_remaining"
):
r = await client.get(gerty.mempool_endpoint + "/api/v1/difficulty-adjustment")
if stat_slug == "onchain_difficulty_epoch_progress":
stat = round(r.json()['progressPercent'])
text.append(get_text_item_dict("Progress through current difficulty epoch", 15))
text.append(get_text_item_dict("{0}%".format(stat), 80))
elif stat_slug == "onchain_difficulty_retarget_date":
stat = r.json()['estimatedRetargetDate']
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(get_text_item_dict(dt, 40))
elif stat_slug == "onchain_difficulty_blocks_remaining":
stat = r.json()['remainingBlocks']
text.append(get_text_item_dict("Blocks remaining until next difficulty adjustment", 15))
text.append(get_text_item_dict("{0}".format(format_number(stat)), 80))
elif stat_slug == "onchain_difficulty_epoch_time_remaining":
stat = r.json()['remainingTime']
text.append(get_text_item_dict("Blocks remaining until next difficulty adjustment", 15))
text.append(get_text_item_dict(get_time_remaining(stat / 1000, 4), 20))
return text
r = await client.get(
gerty.mempool_endpoint + "/api/v1/difficulty-adjustment"
)
text = []
stat = round(r.json()["progressPercent"])
text.append(
get_text_item_dict("Progress through current epoch", 12)
)
text.append(get_text_item_dict("{0}%".format(stat), 60))
areas.append(text)
text = []
stat = r.json()["estimatedRetargetDate"]
dt = datetime.fromtimestamp(stat / 1000).strftime("%e %b %Y at %H:%M")
text.append(
get_text_item_dict("Date of next difficulty adjustment", 12)
)
text.append(get_text_item_dict(dt, 20))
areas.append(text)
text = []
stat = r.json()["remainingBlocks"]
text.append(
get_text_item_dict(
"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):
text = []
if isinstance(gerty.mempool_endpoint, str):
async with httpx.AsyncClient() as client:
if (
stat_slug == "mempool_tx_count"
):
if stat_slug == "mempool_tx_count":
r = await client.get(gerty.mempool_endpoint + "/api/mempool")
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("{0}".format(format_number(stat)), 80))
text.append(
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))
pos_y = 280 + y_offset
text.append(
get_text_item_dict("{0}".format("No Priority"), 15, 30, pos_y)
)
text.append(
get_text_item_dict("{0}".format("Low Priority"), 15, 235, pos_y)
)
text.append(
get_text_item_dict("{0}".format("Medium Priority"), 15, 460, pos_y)
)
text.append(
get_text_item_dict("{0}".format("High Priority"), 15, 750, pos_y)
)
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,
)
)
fee_rate = fees["hourFee"]
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,
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
def get_date_suffix(dayNumber):
if 4 <= dayNumber <= 20 or 24 <= dayNumber <= 30:
return "th"
else:
return ["st", "nd", "rd"][dayNumber % 10 - 1]
# format a number for nice display output
def format_number(number):
return ("{:,}".format(round(number)))
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

@ -3,10 +3,10 @@ from typing import List, Optional, Union
from lnbits.helpers import urlsafe_short_hash
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(
"""
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):
wallet: str = Query(...)
domain: str = Query(...)
cf_token: str = Query(...)
cf_zone_id: str = Query(...)
webhook: str = Query("")
description: str = Query(..., min_length=0)
cost: int = Query(..., ge=0)
allowed_record_types: str = Query(...)
wallet: str = Query(...) # type: ignore
domain: str = Query(...) # type: ignore
cf_token: str = Query(...) # type: ignore
cf_zone_id: str = Query(...) # type: ignore
webhook: str = Query("") # type: ignore
description: str = Query(..., min_length=0) # type: ignore
cost: int = Query(..., ge=0) # type: ignore
allowed_record_types: str = Query(...) # type: ignore
class CreateSubdomain(BaseModel):
domain: str = Query(...)
subdomain: str = Query(...)
email: str = Query(...)
ip: str = Query(...)
sats: int = Query(..., ge=0)
duration: int = Query(...)
record_type: str = Query(...)
domain: str = Query(...) # type: ignore
subdomain: str = Query(...) # type: ignore
email: str = Query(...) # type: ignore
ip: str = Query(...) # type: ignore
sats: int = Query(..., ge=0) # type: ignore
duration: int = Query(...) # type: ignore
record_type: str = Query(...) # type: ignore
class Domains(BaseModel):

View file

@ -20,7 +20,7 @@ async def wait_for_paid_invoices():
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
return
@ -37,7 +37,7 @@ async def on_invoice_paid(payment: Payment) -> None:
)
### Use webhook to notify about cloudflare registration
if domain.webhook:
if domain and domain.webhook:
async with httpx.AsyncClient() as client:
try:
r = await client.post(

View file

@ -16,7 +16,9 @@ templates = Jinja2Templates(directory="templates")
@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(
"subdomains/index.html", {"request": request, "user": user.dict()}
)

View file

@ -29,12 +29,15 @@ from .crud import (
@subdomains_ext.get("/api/v1/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]
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)]
@ -42,7 +45,9 @@ async def api_domains(
@subdomains_ext.post("/api/v1/domains")
@subdomains_ext.put("/api/v1/domains/{domain_id}")
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:
domain = await get_domain(domain_id)
@ -63,7 +68,9 @@ async def api_domain_create(
@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)
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")
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]
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)]
@ -173,7 +182,9 @@ async def api_subdomain_send_subdomain(payment_hash):
@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)
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,8 +89,34 @@ profile = "black"
ignore_missing_imports = "True"
files = "lnbits"
exclude = """(?x)(
^lnbits/extensions.
| ^lnbits/wallets/lnd_grpc_files.
^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.
)"""
[tool.pytest.ini_options]

View file

@ -1,13 +1,11 @@
import asyncio
from typing import Tuple
import pytest_asyncio
from httpx import AsyncClient
from lnbits.app import create_app
from lnbits.commands import migrate_databases
from lnbits.core.crud import create_account, create_wallet, get_wallet
from lnbits.core.models import BalanceCheck, Payment, User, Wallet
from lnbits.core.crud import create_account, create_wallet
from lnbits.core.views.api import CreateInvoiceData, api_payments_create_invoice
from lnbits.db import Database
from lnbits.settings import HOST, PORT

View file

@ -1,16 +1,9 @@
import hashlib
from binascii import hexlify
import pytest
import pytest_asyncio
from lnbits import bolt11
from lnbits.core.crud import get_wallet
from lnbits.core.views.api import (
CreateInvoiceData,
api_payment,
api_payments_create_invoice,
)
from lnbits.core.views.api import api_payment
from lnbits.settings import wallet_class
from ...helpers import get_random_invoice_data, is_regtest

View file

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

View file

@ -1,7 +1,4 @@
import pytest
import pytest_asyncio
from lnbits.core.crud import get_wallet
# check if the client is working

View file

@ -1,7 +1,6 @@
import json
import secrets
import pytest
import pytest_asyncio
from lnbits.core.crud import create_account, create_wallet

View file

@ -1,7 +1,6 @@
import secrets
import pytest
import pytest_asyncio
from lnbits.core.crud import get_wallet
from lnbits.extensions.bleskomat.crud import get_bleskomat_lnurl
@ -10,8 +9,6 @@ from lnbits.extensions.bleskomat.helpers import (
query_to_signing_payload,
)
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.mocks import WALLET

View file

@ -1,17 +1,7 @@
import asyncio
import json
import secrets
import pytest
import pytest_asyncio
from lnbits.core.crud import create_account, create_wallet, get_wallet
from lnbits.extensions.boltz.boltz import create_reverse_swap, create_swap
from lnbits.extensions.boltz.models import (
CreateReverseSubmarineSwap,
CreateSubmarineSwap,
)
from tests.mocks import WALLET
from lnbits.extensions.boltz.boltz import create_reverse_swap
from lnbits.extensions.boltz.models import CreateReverseSubmarineSwap
@pytest_asyncio.fixture(scope="session")

View file

@ -1,7 +1,6 @@
import pytest
import pytest_asyncio
from tests.helpers import is_fake, is_regtest
from tests.helpers import is_fake
@pytest.mark.asyncio

View file

@ -1,17 +1,10 @@
import asyncio
import pytest
import pytest_asyncio
from lnbits.extensions.boltz.boltz import create_reverse_swap, create_swap
from lnbits.extensions.boltz.crud import (
create_reverse_submarine_swap,
create_submarine_swap,
get_reverse_submarine_swap,
get_submarine_swap,
)
from tests.extensions.boltz.conftest import reverse_swap
from tests.helpers import is_fake, is_regtest
from tests.helpers import is_fake
@pytest.mark.asyncio

View file

@ -1,4 +1,3 @@
import pytest
import pytest_asyncio
from lnbits.core.crud import create_account, create_wallet

View file

@ -1,12 +1,4 @@
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