Merge pull request #1018 from blackcoffeexbt/gerty

Gerty
This commit is contained in:
Arc 2022-09-30 09:03:36 +01:00 committed by GitHub
commit e084495814
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 1115 additions and 628 deletions

62
lnbits/Pipfile Normal file
View file

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

View file

@ -2,5 +2,5 @@
"name": "Gerty", "name": "Gerty",
"short_description": "Desktop bitcoin Assistant", "short_description": "Desktop bitcoin Assistant",
"icon": "sentiment_satisfied", "icon": "sentiment_satisfied",
"contributors": ["arcbtc"] "contributors": ["arcbtc", "blackcoffeebtc"]
} }

View file

@ -10,8 +10,16 @@ async def create_gerty(wallet_id: str, data: Gerty) -> Gerty:
gerty_id = urlsafe_short_hash() gerty_id = urlsafe_short_hash()
await db.execute( await db.execute(
""" """
INSERT INTO gerty.gertys (id, name, wallet, lnbits_wallets, mempool_endpoint, sats_quote, exchange, onchain_stats, ln_stats) INSERT INTO gerty.gertys (
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) id,
name,
wallet,
lnbits_wallets,
mempool_endpoint,
exchange,
display_preferences
)
VALUES (?, ?, ?, ?, ?, ?, ?)
""", """,
( (
gerty_id, gerty_id,
@ -19,10 +27,8 @@ async def create_gerty(wallet_id: str, data: Gerty) -> Gerty:
data.wallet, data.wallet,
data.lnbits_wallets, data.lnbits_wallets,
data.mempool_endpoint, data.mempool_endpoint,
data.sats_quote,
data.exchange, data.exchange,
data.onchain_stats, data.display_preferences
data.ln_stats,
), ),
) )

View file

@ -1,19 +1,18 @@
async def m001_initial(db): async def m001_initial(db):
""" """
Initial gertys table. Initial Gertys table.
""" """
await db.execute( await db.execute(
""" """
CREATE TABLE gerty.gertys ( CREATE TABLE gerty.gertys (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
refresh_time INT,
name TEXT NOT NULL, name TEXT NOT NULL,
wallet TEXT NOT NULL, wallet TEXT NOT NULL,
lnbits_wallets TEXT, lnbits_wallets TEXT,
mempool_endpoint TEXT, mempool_endpoint TEXT,
sats_quote BOOL,
exchange TEXT, exchange TEXT,
onchain_stats BOOL, display_preferences TEXT
ln_stats BOOL
); );
""" """
) )

View file

@ -4,18 +4,15 @@ from typing import Optional
from fastapi import Query from fastapi import Query
from pydantic import BaseModel from pydantic import BaseModel
class Gerty(BaseModel): class Gerty(BaseModel):
id: str = Query(None) id: str = Query(None)
name: str name: str
wallet: str wallet: str
refresh_time: int = Query(None)
lnbits_wallets: str = Query(None) # Wallets to keep an eye on, {"wallet-id": "wallet-read-key, etc"} lnbits_wallets: str = Query(None) # Wallets to keep an eye on, {"wallet-id": "wallet-read-key, etc"}
mempool_endpoint: str = Query(None) # Mempool endpoint to use mempool_endpoint: str = Query(None) # Mempool endpoint to use
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 exchange: str = Query(None) # BTC <-> Fiat exchange rate to pull ie "USD", in 0.0001 and sats
onchain_stats: bool = Query(False) # Onchain stats display_preferences: str = Query(None)
ln_stats: bool = Query(False) # ln Sats
@classmethod @classmethod
def from_row(cls, row: Row) -> "Gerty": def from_row(cls, row: Row) -> "Gerty":

View file

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

File diff suppressed because it is too large Load diff

View file

@ -1,15 +1,17 @@
import math
from http import HTTPStatus from http import HTTPStatus
import json import json
import httpx import httpx
import random import random
import os import os
import time
from datetime import datetime
from fastapi import Query from fastapi import Query
from fastapi.params import Depends from fastapi.params import Depends
from lnurl import decode as decode_lnurl from lnurl import decode as decode_lnurl
from loguru import logger from loguru import logger
from starlette.exceptions import HTTPException from starlette.exceptions import HTTPException
from lnbits.core.crud import get_wallet_for_key from lnbits.core.crud import get_wallet_for_key
from lnbits.core.crud import get_user from lnbits.core.crud import get_user
from lnbits.core.services import create_invoice 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) @gerty_ext.get("/api/v1/gerty", status_code=HTTPStatus.OK)
async def api_gertys( 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] wallet_ids = [wallet.wallet.id]
if all_wallets: if all_wallets:
@ -39,9 +41,9 @@ async def api_gertys(
@gerty_ext.post("/api/v1/gerty", status_code=HTTPStatus.CREATED) @gerty_ext.post("/api/v1/gerty", status_code=HTTPStatus.CREATED)
@gerty_ext.put("/api/v1/gerty/{gerty_id}", status_code=HTTPStatus.OK) @gerty_ext.put("/api/v1/gerty/{gerty_id}", status_code=HTTPStatus.OK)
async def api_link_create_or_update( async def api_link_create_or_update(
data: Gerty, data: Gerty,
wallet: WalletTypeInfo = Depends(get_key_type), wallet: WalletTypeInfo = Depends(get_key_type),
gerty_id: str = Query(None), gerty_id: str = Query(None),
): ):
if gerty_id: if gerty_id:
gerty = await get_gerty(gerty_id) gerty = await get_gerty(gerty_id)
@ -63,9 +65,10 @@ async def api_link_create_or_update(
return {**gerty.dict()} return {**gerty.dict()}
@gerty_ext.delete("/api/v1/gerty/{gerty_id}") @gerty_ext.delete("/api/v1/gerty/{gerty_id}")
async def api_gerty_delete( 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) 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) @gerty_ext.get("/api/v1/gerty/satoshiquote", status_code=HTTPStatus.OK)
async def api_gerty_satoshi(): async def api_gerty_satoshi():
with open(os.path.join(LNBITS_PATH, 'extensions/gerty/static/satoshi.json')) as fd:
satoshiQuotes = json.load(fd)
return satoshiQuotes[random.randint(0, 100)] 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( async def api_gerty_json(
gerty_id: str gerty_id: str,
p: int = None # page number
): ):
gerty = await get_gerty(gerty_id) gerty = await get_gerty(gerty_id)
logger.debug(gerty.wallet)
if not gerty: if not gerty:
raise HTTPException( raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Gerty does not exist." 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 # Get Wallet info
wallets = [] wallets = []
text = []
if gerty.lnbits_wallets != "": if gerty.lnbits_wallets != "":
for lnbits_wallet in json.loads(gerty.lnbits_wallets): for lnbits_wallet in json.loads(gerty.lnbits_wallets):
wallet = await get_wallet_for_key(key=lnbits_wallet) wallet = await get_wallet_for_key(key=lnbits_wallet)
logger.debug(wallet)
if wallet: if wallet:
wallets.append({ wallets.append({
"name": wallet.name, "name": wallet.name,
"balance": wallet.balance_msat, "balance": wallet.balance_msat,
"inkey": wallet.inkey, "inkey": wallet.inkey,
}) })
text.append(get_text_item_dict(wallet.name, 20))
text.append(get_text_item_dict(wallet.balance, 40))
return 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 async def get_placeholder_text():
exchange = [] 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 != "": if gerty.exchange != "":
try: try:
amount = await satoshis_amount_as_fiat(100000000, gerty.exchange) amount = await satoshis_amount_as_fiat(100000000, gerty.exchange)
if amount: if amount:
exchange.append({ price = ('{0} {1}').format(format_number(amount), gerty.exchange)
"fiat": gerty.exchange, text.append(get_text_item_dict("Current BTC price", 15))
"amount": amount, text.append(get_text_item_dict(price, 80))
})
except: except:
pass 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: async with httpx.AsyncClient() as client:
difficulty = [] if (
r = await client.get(gerty.mempool_endpoint + "/api/v1/difficulty-adjustment") stat_slug == "onchain_difficulty_epoch_progress" or
if r: stat_slug == "onchain_difficulty_retarget_date" or
difficulty.append(r.json()) stat_slug == "onchain_difficulty_blocks_remaining" or
onchain.append({"difficulty":difficulty}) stat_slug == "onchain_difficulty_epoch_time_remaining"
mempool = [] ):
r = await client.get(gerty.mempool_endpoint + "/api/v1/fees/mempool-blocks") r = await client.get(gerty.mempool_endpoint + "/api/v1/difficulty-adjustment")
if r: if stat_slug == "onchain_difficulty_epoch_progress":
mempool.append(r.json()) stat = round(r.json()['progressPercent'])
onchain.append({"mempool":mempool}) text.append(get_text_item_dict("Progress through current difficulty epoch", 15))
threed = [] text.append(get_text_item_dict("{0}%".format(stat), 80))
r = await client.get(gerty.mempool_endpoint + "/api/v1/mining/hashrate/3d") elif stat_slug == "onchain_difficulty_retarget_date":
if r: stat = r.json()['estimatedRetargetDate']
threed.append(r.json()) dt = datetime.fromtimestamp(stat / 1000).strftime("%e %b %Y at %H:%M")
onchain.append({"threed":threed}) 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 = [] async def get_mempool_stat(stat_slug: str, gerty):
if gerty.ln_stats and isinstance(gerty.mempool_endpoint, str): text = []
if isinstance(gerty.mempool_endpoint, str):
async with httpx.AsyncClient() as client: async with httpx.AsyncClient() as client:
r = await client.get(gerty.mempool_endpoint + "/api/v1/lightning/statistics/latest") if (
if r: stat_slug == "mempool_tx_count"
ln.append(r.json()) ):
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])