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",
"short_description": "Desktop bitcoin Assistant",
"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()
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
),
)

View file

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

View file

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

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