- Onchain Stats
- Difficulty Progress Percent
-
-
-
-
-
+ Onchain Stats
+ Difficulty Progress Percent
+
+
+
+
+
@@ -46,17 +80,14 @@
- LN Stats
+ LN Stats
-
- {{gerty.ln}}
-
+ {{gerty.ln}}
-{% endraw %}
-{% endblock %} {% block scripts %}
+{% endraw %} {% endblock %} {% block scripts %}
+ LNbits.api
+ .request(
+ 'POST',
+ '/gerty/api/v1/gerty',
+ _.findWhere(this.g.user.wallets, {id: this.formDialog.data.wallet})
+ .inkey,
+ data
+ )
+ .then(function (response) {
+ self.formDialog.show = false
+ self.gertys.push(mapGerty(response.data))
+ })
+ .catch(function (error) {
+ LNbits.utils.notifyApiError(error)
+ })
+ },
+ updateGerty: function (wallet, data) {
+ var self = this
+ data.utc_offset = this.formDialog.data.utc_offset
+ data.lnbits_wallets = JSON.stringify(this.formDialog.data.lnbits_wallets)
+ data.display_preferences = JSON.stringify(this.formDialog.data.display_preferences)
+ LNbits.api
+ .request(
+ 'PUT',
+ '/gerty/api/v1/gerty/' + data.id,
+ wallet,
+ data
+ )
+ .then(function (response) {
+ self.gertys = _.reject(self.gertys, function (obj) {
+ return obj.id == data.id
+ })
+ self.formDialog.show = false
+ self.gertys.push(mapGerty(response.data))
+ })
+ .catch(function (error) {
+ LNbits.utils.notifyApiError(error)
+ })
+ },
+ deleteGerty: function (gertyId) {
+ var self = this
+ var gerty = _.findWhere(this.gertys, {id: gertyId})
+
+ LNbits.utils
+ .confirmDialog('Are you sure you want to delete this Gerty?')
+ .onOk(function () {
+ LNbits.api
+ .request(
+ 'DELETE',
+ '/gerty/api/v1/gerty/' + gertyId,
+ _.findWhere(self.g.user.wallets, {id: gerty.wallet}).adminkey
+ )
+ .then(function (response) {
+ self.gertys = _.reject(self.gertys, function (obj) {
+ return obj.id == gertyId
+ })
+ })
+ .catch(function (error) {
+ LNbits.utils.notifyApiError(error)
+ })
+ })
+ },
+ exportCSV: function () {
+ LNbits.utils.exportCSV(this.gertysTable.columns, this.gertys)
+ }
+ },
+ created: function () {
+ if (this.g.user.wallets.length) {
+ this.getGertys()
+ }
+ }
+ })
+
+{% endblock %} {% block styles %}
+
{% endblock %}
-
-{% block styles %}
-
-{% endblock %}
\ No newline at end of file
diff --git a/lnbits/extensions/gerty/views.py b/lnbits/extensions/gerty/views.py
index 630cb48bf..e05861691 100644
--- a/lnbits/extensions/gerty/views.py
+++ b/lnbits/extensions/gerty/views.py
@@ -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})
\ No newline at end of file
+ return gerty_renderer().TemplateResponse(
+ "gerty/gerty.html", {"request": request, "gerty": gertyData}
+ )
diff --git a/lnbits/extensions/gerty/views_api.py b/lnbits/extensions/gerty/views_api.py
index b5852ee6e..05a7f5d75 100644
--- a/lnbits/extensions/gerty/views_api.py
+++ b/lnbits/extensions/gerty/views_api.py
@@ -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])
\ No newline at end of file
diff --git a/lnbits/extensions/subdomains/crud.py b/lnbits/extensions/subdomains/crud.py
index 207e2d1d4..aa358d11c 100644
--- a/lnbits/extensions/subdomains/crud.py
+++ b/lnbits/extensions/subdomains/crud.py
@@ -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)
diff --git a/lnbits/extensions/subdomains/models.py b/lnbits/extensions/subdomains/models.py
index 170045040..39e176155 100644
--- a/lnbits/extensions/subdomains/models.py
+++ b/lnbits/extensions/subdomains/models.py
@@ -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):
diff --git a/lnbits/extensions/subdomains/tasks.py b/lnbits/extensions/subdomains/tasks.py
index 04ee2dd48..c5a7f47ba 100644
--- a/lnbits/extensions/subdomains/tasks.py
+++ b/lnbits/extensions/subdomains/tasks.py
@@ -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(
diff --git a/lnbits/extensions/subdomains/views.py b/lnbits/extensions/subdomains/views.py
index df387ba8c..962f850d0 100644
--- a/lnbits/extensions/subdomains/views.py
+++ b/lnbits/extensions/subdomains/views.py
@@ -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()}
)
diff --git a/lnbits/extensions/subdomains/views_api.py b/lnbits/extensions/subdomains/views_api.py
index b01e6ffbb..34d8e75be 100644
--- a/lnbits/extensions/subdomains/views_api.py
+++ b/lnbits/extensions/subdomains/views_api.py
@@ -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:
diff --git a/lnbits/proxy_fix.py b/lnbits/proxy_fix.py
deleted file mode 100644
index 897835e0d..000000000
--- a/lnbits/proxy_fix.py
+++ /dev/null
@@ -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 ["
"])[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
diff --git a/pyproject.toml b/pyproject.toml
index 19dac8600..7418de272 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -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]
diff --git a/tests/conftest.py b/tests/conftest.py
index 1e719c76a..fd8d4d424 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -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
diff --git a/tests/core/views/test_api.py b/tests/core/views/test_api.py
index e0f6b5762..c62801535 100644
--- a/tests/core/views/test_api.py
+++ b/tests/core/views/test_api.py
@@ -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
diff --git a/tests/core/views/test_generic.py b/tests/core/views/test_generic.py
index 4300b78b9..ac25e1e78 100644
--- a/tests/core/views/test_generic.py
+++ b/tests/core/views/test_generic.py
@@ -1,7 +1,4 @@
import pytest
-import pytest_asyncio
-
-from tests.conftest import client
@pytest.mark.asyncio
diff --git a/tests/core/views/test_public_api.py b/tests/core/views/test_public_api.py
index 6ebaeabd3..144cd161e 100644
--- a/tests/core/views/test_public_api.py
+++ b/tests/core/views/test_public_api.py
@@ -1,7 +1,4 @@
import pytest
-import pytest_asyncio
-
-from lnbits.core.crud import get_wallet
# check if the client is working
diff --git a/tests/extensions/bleskomat/conftest.py b/tests/extensions/bleskomat/conftest.py
index 13be2b579..595ba6b87 100644
--- a/tests/extensions/bleskomat/conftest.py
+++ b/tests/extensions/bleskomat/conftest.py
@@ -1,7 +1,6 @@
import json
import secrets
-import pytest
import pytest_asyncio
from lnbits.core.crud import create_account, create_wallet
diff --git a/tests/extensions/bleskomat/test_lnurl_api.py b/tests/extensions/bleskomat/test_lnurl_api.py
index 3f7232669..1fc4ea3ef 100644
--- a/tests/extensions/bleskomat/test_lnurl_api.py
+++ b/tests/extensions/bleskomat/test_lnurl_api.py
@@ -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
diff --git a/tests/extensions/boltz/conftest.py b/tests/extensions/boltz/conftest.py
index b9ef78875..930a1bfb1 100644
--- a/tests/extensions/boltz/conftest.py
+++ b/tests/extensions/boltz/conftest.py
@@ -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")
diff --git a/tests/extensions/boltz/test_api.py b/tests/extensions/boltz/test_api.py
index 90ce6ec16..2d64fc40c 100644
--- a/tests/extensions/boltz/test_api.py
+++ b/tests/extensions/boltz/test_api.py
@@ -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
diff --git a/tests/extensions/boltz/test_swap.py b/tests/extensions/boltz/test_swap.py
index ab5954acb..f1a820cad 100644
--- a/tests/extensions/boltz/test_swap.py
+++ b/tests/extensions/boltz/test_swap.py
@@ -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
diff --git a/tests/extensions/invoices/conftest.py b/tests/extensions/invoices/conftest.py
index 09ac42ecb..277368d6b 100644
--- a/tests/extensions/invoices/conftest.py
+++ b/tests/extensions/invoices/conftest.py
@@ -1,4 +1,3 @@
-import pytest
import pytest_asyncio
from lnbits.core.crud import create_account, create_wallet
diff --git a/tests/extensions/invoices/test_invoices_api.py b/tests/extensions/invoices/test_invoices_api.py
index eaadd07b3..5661673e5 100644
--- a/tests/extensions/invoices/test_invoices_api.py
+++ b/tests/extensions/invoices/test_invoices_api.py
@@ -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