diff --git a/lnbits/Pipfile b/lnbits/Pipfile
new file mode 100644
index 000000000..3517399db
--- /dev/null
+++ b/lnbits/Pipfile
@@ -0,0 +1,62 @@
+[[source]]
+url = "https://pypi.org/simple"
+verify_ssl = true
+name = "pypi"
+
+[packages]
+aiofiles = "==0.8.0"
+anyio = "==3.6.1"
+asyncio = "==3.4.3"
+attrs = "==21.4.0"
+bech32 = "==1.2.0"
+bitstring = "==3.1.9"
+cerberus = "==1.3.4"
+certifi = "==2022.6.15"
+cffi = "==1.15.0"
+click = "==8.1.3"
+ecdsa = "==0.18.0"
+embit = "==0.5.0"
+environs = "==9.5.0"
+fastapi = "==0.79.0"
+h11 = "==0.12.0"
+httpcore = "==0.15.0"
+httptools = "==0.4.0"
+httpx = "==0.23.0"
+idna = "==3.3"
+jinja2 = "==3.0.1"
+lnurl = "==0.3.6"
+loguru = "==0.6.0"
+markupsafe = "==2.1.1"
+marshmallow = "==3.17.0"
+outcome = "==1.2.0"
+psycopg2-binary = "==2.9.3"
+pycparser = "==2.21"
+pycryptodomex = "==3.15.0"
+pydantic = "==1.9.1"
+pyngrok = "==5.1.0"
+pyparsing = "==3.0.9"
+pypng = "==0.20220715.0"
+pyqrcode = "==1.2.1"
+pyscss = "==1.4.0"
+python-dotenv = "==0.20.0"
+pyyaml = "==6.0"
+represent = "==1.6.0.post0"
+rfc3986 = "==1.5.0"
+secp256k1 = "==0.14.0"
+shortuuid = "==1.0.9"
+six = "==1.16.0"
+sniffio = "==1.2.0"
+sqlalchemy-aio = "==0.17.0"
+sqlalchemy = "==1.3.23"
+sse-starlette = "==0.10.3"
+starlette = "==0.19.1"
+typing-extensions = "==4.3.0"
+uvicorn = "==0.18.2"
+uvloop = "==0.16.0"
+watchfiles = "==0.16.0"
+websockets = "==10.3"
+
+[dev-packages]
+
+[requires]
+python_version = "3.9"
diff --git a/lnbits/extensions/gerty/config.json b/lnbits/extensions/gerty/config.json
index 158ac52a0..a36437beb 100644
--- a/lnbits/extensions/gerty/config.json
+++ b/lnbits/extensions/gerty/config.json
@@ -2,5 +2,5 @@
"name": "Gerty",
"short_description": "Desktop bitcoin Assistant",
"icon": "sentiment_satisfied",
- "contributors": ["arcbtc"]
+ "contributors": ["arcbtc", "blackcoffeebtc"]
}
diff --git a/lnbits/extensions/gerty/crud.py b/lnbits/extensions/gerty/crud.py
index 9eeb1a4af..a472ef37f 100644
--- a/lnbits/extensions/gerty/crud.py
+++ b/lnbits/extensions/gerty/crud.py
@@ -10,8 +10,16 @@ async def create_gerty(wallet_id: str, data: Gerty) -> Gerty:
gerty_id = urlsafe_short_hash()
await db.execute(
"""
- INSERT INTO gerty.gertys (id, name, wallet, lnbits_wallets, mempool_endpoint, sats_quote, exchange, onchain_stats, ln_stats)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
+ INSERT INTO gerty.gertys (
+ id,
+ name,
+ wallet,
+ lnbits_wallets,
+ mempool_endpoint,
+ exchange,
+ display_preferences
+ )
+ VALUES (?, ?, ?, ?, ?, ?, ?)
""",
(
gerty_id,
@@ -19,10 +27,8 @@ async def create_gerty(wallet_id: str, data: Gerty) -> Gerty:
data.wallet,
data.lnbits_wallets,
data.mempool_endpoint,
- data.sats_quote,
data.exchange,
- data.onchain_stats,
- data.ln_stats,
+ data.display_preferences
),
)
diff --git a/lnbits/extensions/gerty/migrations.py b/lnbits/extensions/gerty/migrations.py
index d3bba79cd..459fc8807 100644
--- a/lnbits/extensions/gerty/migrations.py
+++ b/lnbits/extensions/gerty/migrations.py
@@ -1,19 +1,18 @@
async def m001_initial(db):
"""
- Initial gertys table.
+ Initial Gertys table.
"""
await db.execute(
"""
CREATE TABLE gerty.gertys (
id TEXT PRIMARY KEY,
+ refresh_time INT,
name TEXT NOT NULL,
wallet TEXT NOT NULL,
lnbits_wallets TEXT,
mempool_endpoint TEXT,
- sats_quote BOOL,
exchange TEXT,
- onchain_stats BOOL,
- ln_stats BOOL
+ display_preferences TEXT
);
"""
)
\ No newline at end of file
diff --git a/lnbits/extensions/gerty/models.py b/lnbits/extensions/gerty/models.py
index c3590f032..fc7a33774 100644
--- a/lnbits/extensions/gerty/models.py
+++ b/lnbits/extensions/gerty/models.py
@@ -4,18 +4,15 @@ 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
- sats_quote: bool = Query(False) # Fetch Satoshi quotes
exchange: str = Query(None) # BTC <-> Fiat exchange rate to pull ie "USD", in 0.0001 and sats
- onchain_stats: bool = Query(False) # Onchain stats
- ln_stats: bool = Query(False) # ln Sats
+ display_preferences: str = Query(None)
@classmethod
def from_row(cls, row: Row) -> "Gerty":
diff --git a/lnbits/extensions/gerty/static/pieter_wuille.json b/lnbits/extensions/gerty/static/pieter_wuille.json
new file mode 100644
index 000000000..986150ed2
--- /dev/null
+++ b/lnbits/extensions/gerty/static/pieter_wuille.json
@@ -0,0 +1,14 @@
+{
+ "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."
+ ]
+}
\ No newline at end of file
diff --git a/lnbits/extensions/gerty/templates/gerty/index.html b/lnbits/extensions/gerty/templates/gerty/index.html
index 7ba0e0297..a59bf15d8 100644
--- a/lnbits/extensions/gerty/templates/gerty/index.html
+++ b/lnbits/extensions/gerty/templates/gerty/index.html
@@ -1,575 +1,793 @@
-{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
-%} {% block page %}
-
-
-
-
- New Gerty
-
-
-
-
-
-
-
Gerty
-
-
- Export to CSV
-
+{% extends "base.html" %} {% from "macros.jinja" import window_vars with context %} {% block page %}
+
+
+
+
+ New Gerty
+
+
+
+
+
+
+
+
Gerty
+
+
+ Export to CSV
+
+
+
+ {% raw %}
+
+
+
+
+ {{ col.label }}
+
+
+
+
+
+
+
+
+
+
+ Launch software Gerty
+
+
+ Launch software Gerty
+
+
+
+ {{ (col.name == 'tip_options' && col.value ?
+ JSON.parse(col.value).join(", ") : col.value) }}
+
+
+
+
+
+
+
+
+
+ {% endraw %}
+
+
+
-
- {% raw %}
-
-
-
-
- {{ col.label }}
-
-
-
-
-
-
-
-
- Launch software Gerty
- Launch software Gerty
-
-
- {{ (col.name == 'tip_options' && col.value ?
- JSON.parse(col.value).join(", ") : col.value) }}
-
-
-
-
-
-
-
-
-
- {% endraw %}
-
-
-
-
-
-
-
-
- {{SITE_TITLE}} Gerty extension
-
-
-
-
- {% include "gerty/_api_docs.html" %}
-
-
-
-
-
-
-
-
-
-
- Hit enter to add values
-
- Used for getting onchain/ln stats
- Gets random quotes from satoshi
- Gets Onchain Statistics
- Gets Lightning-Network Statistics
-
-
Create Gerty
-
Update Gerty
-
Cancel
+
+
+
+ {{ SITE_TITLE }} Gerty extension
+
+
+
+
+ {% include "gerty/_api_docs.html" %}
+
+
+
-
-
-
-
+
+
+
+
+
+
+
+ Hit enter to add values
+
+
+
+ Used for getting onchain/ln stats
+
+
+
+ The amount of time in seconds between screen updates
+
+
+ Use the toggles below to control what your Gerty will display
+
+
+
+
+
+
+
+ Toggle all
+
+
+
+ Displays random quotes from Satoshi
+
+
+ Show accurate facts about Pieter Wuille
+
+
+
+
+
+ Toggle all
+
+
+
+
+
+
+
+
+
+
+
+
+ Toggle all
+
+
+
+
+
+
+
+
+
+
+ Toggle all
+
+
+
+
+
+
+
+
+
+ Toggle all
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Create Gerty
+
+ Update Gerty
+
+ Cancel
+
+
+
+
+
+
{% endblock %} {% block scripts %} {{ window_vars(user) }}
-
+ })
+
{% endblock %}
+
+{% block styles %}
+
+{% endblock %}
\ No newline at end of file
diff --git a/lnbits/extensions/gerty/views_api.py b/lnbits/extensions/gerty/views_api.py
index d175bb7ce..b5852ee6e 100644
--- a/lnbits/extensions/gerty/views_api.py
+++ b/lnbits/extensions/gerty/views_api.py
@@ -1,15 +1,17 @@
+import math
from http import HTTPStatus
import json
import httpx
import random
import os
+import time
+from datetime import datetime
from fastapi import Query
from fastapi.params import Depends
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.services import create_invoice
@@ -27,7 +29,7 @@ from ...settings import LNBITS_PATH
@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:
@@ -39,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)
@@ -63,9 +65,10 @@ async def api_link_create_or_update(
return {**gerty.dict()}
+
@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)
@@ -83,83 +86,271 @@ async def api_gerty_delete(
#######################
-with open(os.path.join(LNBITS_PATH, 'extensions/gerty/static/satoshi.json')) as fd:
- satoshiQuotes = json.load(fd)
-
@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:
+ satoshiQuotes = json.load(fd)
return satoshiQuotes[random.randint(0, 100)]
-
-@gerty_ext.get("/api/v1/gerty/{gerty_id}")
+
+
+@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))]
+
+
+@gerty_ext.get("/api/v1/gerty/{gerty_id}/{p}")
async def api_gerty_json(
- gerty_id: str
+ gerty_id: str,
+ p: int = None # page number
):
gerty = await get_gerty(gerty_id)
- logger.debug(gerty.wallet)
+
if not gerty:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Gerty does not exist."
)
- gertyReturn = []
+ display_preferences = json.loads(gerty.display_preferences)
+
+ enabled_screen_count = 0
+
+ enabled_screens = []
+
+ for screen_slug in display_preferences:
+ is_screen_enabled = display_preferences[screen_slug]
+ if is_screen_enabled:
+ enabled_screen_count += 1
+ enabled_screens.append(screen_slug)
+
+ text = await get_screen_text(p, enabled_screens, gerty)
+
+ 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())
+
+ return {
+ "settings": {
+ "refreshTime": gerty.refresh_time,
+ "requestTimestamp": round(time.time()),
+ "nextScreenNumber": next_screen_number,
+ "showTextBoundRect": True,
+ "name": gerty.name
+ },
+ "screen": {
+ "slug": get_screen_slug_by_index(p, enabled_screens),
+ "group": get_screen_slug_by_index(p, enabled_screens),
+ "text": text
+ }
+ }
+
+
+# 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]
+
+
+# Get a list of text items for the screen number
+async def get_screen_text(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)
+ # text = []
+ if screen_slug == "lnbits_wallets_balance":
+ text = await get_lnbits_wallet_balances(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()
+ 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)
+ 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
+
+
+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)
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
- #Get Satoshi quotes
- satoshi = []
- if gerty.sats_quote:
- quote = await api_gerty_satoshi()
- if quote:
- satoshi.append(await api_gerty_satoshi())
- #Get Exchange Value
- exchange = []
+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)
+ ]
+
+
+async def get_satoshi_quotes():
+ # 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))
+ return text
+
+
+# Get Exchange Value
+async def get_exchange_rate(gerty):
+ text = []
if gerty.exchange != "":
try:
amount = await satoshis_amount_as_fiat(100000000, gerty.exchange)
if amount:
- exchange.append({
- "fiat": gerty.exchange,
- "amount": amount,
- })
+ price = ('{0} {1}').format(format_number(amount), gerty.exchange)
+ text.append(get_text_item_dict("Current BTC price", 15))
+ text.append(get_text_item_dict(price, 80))
except:
pass
+ return text
- onchain = []
- if gerty.onchain_stats and isinstance(gerty.mempool_endpoint, str):
+
+# 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 = []
+ if isinstance(gerty.mempool_endpoint, str):
async with httpx.AsyncClient() as client:
- difficulty = []
- r = await client.get(gerty.mempool_endpoint + "/api/v1/difficulty-adjustment")
- if r:
- difficulty.append(r.json())
- onchain.append({"difficulty":difficulty})
- mempool = []
- r = await client.get(gerty.mempool_endpoint + "/api/v1/fees/mempool-blocks")
- if r:
- mempool.append(r.json())
- onchain.append({"mempool":mempool})
- threed = []
- r = await client.get(gerty.mempool_endpoint + "/api/v1/mining/hashrate/3d")
- if r:
- threed.append(r.json())
- onchain.append({"threed":threed})
+ 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
- ln = []
- if gerty.ln_stats and isinstance(gerty.mempool_endpoint, str):
+async def get_mempool_stat(stat_slug: str, gerty):
+ text = []
+ if 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())
+ 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'])
+ text.append(get_text_item_dict("Transactions in the mempool", 15))
+ text.append(get_text_item_dict("{0}".format(format_number(stat)), 80))
+ return text
- return {"name":gerty.name, "wallets":wallets, "sats_quote":satoshi, "exchange":exchange, "onchain":onchain, "ln":ln}
+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