import copy import requests import subprocess import os import time import re import datetime import urllib import random import base64 from device_info import * from threading import Timer from utilities import * from bitcoin_info import * from systemctl_info import * # Variables lightning_info = None lnd_ready = False lnd_version = None loop_version = None pool_version = None lit_version = None lightning_peers = None lightning_peer_aliases = {} lightning_channels = None lightning_channel_balance = None lightning_wallet_balance = None lightning_transactions = None lightning_payments = None lightning_invoices = None lightning_watchtower_server_info = {} lightning_watchtower_client_towers = {} lightning_watchtower_client_stats = {} lightning_watchtower_client_policy = {} lightning_desync_count = 0 lightning_update_count = 0 LIGHTNING_CACHE_FILE = "/tmp/lightning_info.json" LND_FOLDER = "/mnt/hdd/mynode/lnd/" TLS_CERT_FILE = "/mnt/hdd/mynode/lnd/tls.cert" LND_REST_PORT = "10080" # Functions def run_lncli_command(cmd): try: base = "lncli " base += "--lnddir=/mnt/hdd/mynode/lnd " if is_testnet_enabled(): base += "--network=testnet " cmd = cmd.replace("lncli ", base) output = subprocess.check_output(cmd, shell=True) return output except Exception as e: log_message("ERROR in run_lncli_command: {}".format(str(e))) return None def update_lightning_info(): global lightning_info global lightning_peers global lightning_channels global lightning_channel_balance global lightning_wallet_balance global lightning_transactions global lightning_payments global lightning_invoices global lightning_watchtower_server_info global lightning_watchtower_client_towers global lightning_watchtower_client_stats global lightning_watchtower_client_policy global lightning_desync_count global lightning_update_count global lnd_ready # Check logged in #while not is_lnd_logged_in(): # lnd_ready = False # time.sleep(10) # Get latest LN info lightning_info = lnd_get("/getinfo") lightning_update_count = lightning_update_count + 1 # Set is LND ready if lightning_info != None and "synced_to_chain" in lightning_info and lightning_info['synced_to_chain']: lnd_ready = True # Check for LND de-sync (this can happen unfortunately) # See https://github.com/lightningnetwork/lnd/issues/1909 # See https://github.com/bitcoin/bitcoin/pull/14687 # Hopefully patch comes soon to enable TCP keepalive to prevent this from happening if lnd_ready and lightning_info != None and "synced_to_chain" in lightning_info and not lightning_info['synced_to_chain']: lightning_desync_count += 1 os.system("printf \"%s | LND De-sync!!! Count: {} \\n\" \"$(date)\" >> /tmp/lnd_failures".format(lightning_desync_count)) if lightning_desync_count >= 8: os.system("printf \"%s | De-sync count too high! Retarting LND... \\n\" \"$(date)\" >> /tmp/lnd_failures") restart_lnd() lightning_desync_count = 0 return True if lnd_ready: log_message("update_lightning_info - LND READY") if lightning_desync_count > 0: os.system("printf \"%s | De-sync greater than 0 (was {}), but now synced! Setting to 0. \\n\" \"$(date)\" >> /tmp/lnd_failures".format(lightning_desync_count)) lightning_desync_count = 0 log_message("update_lightning_info - GET PEERS, CHANNELS, BALANCE, WALLET") lightning_peers = lnd_get("/peers") lightning_channels = lnd_get("/channels") lightning_channel_balance = lnd_get("/balance/channels") lightning_wallet_balance = lnd_get("/balance/blockchain") log_message("update_lightning_info - GET WATCHTOWER") if is_watchtower_server_enabled(): lightning_watchtower_server_info = lnd_get_v2("/watchtower/server") towers = lnd_get_v2("/watchtower/client?include_sessions=1") log_message("update_lightning_info - TOWER DETAILS") tower_details = [] if towers != None and "towers" in towers: for tower in towers["towers"]: if "pubkey" in tower and tower["active_session_candidate"]: pubkey_decoded = base64.b64decode(tower['pubkey']) pubkey_b16 = to_string(base64.b16encode( pubkey_decoded )).lower() tower["pubkey_b16"] = pubkey_b16 tower_details.append(tower) lightning_watchtower_client_towers = tower_details log_message("update_lightning_info - GET CLIENT STATS, POLICY") lightning_watchtower_client_stats = lnd_get_v2("/watchtower/client/stats") lightning_watchtower_client_policy = lnd_get_v2("/watchtower/client/policy") # Poll slower (make sure we gather data early) if lightning_update_count < 30 or lightning_update_count % 2 == 0: log_message("update_lightning_info - GET TX INFO") update_lightning_tx_info() update_lightning_json_cache() return True def update_lightning_tx_info(): global lightning_transactions global lightning_payments global lightning_invoices if is_lnd_ready(): tx_cache_limit = 50 lightning_transactions = lnd_get("/transactions") lightning_payments = lnd_get("/payments", params={"reversed":"true", "index_offset": "0", "max_payments": tx_cache_limit}) lightning_invoices = lnd_get("/invoices", params={"reversed":"true", "index_offset": "0", "num_max_invoices": tx_cache_limit}) def get_lnd_deposit_address(): if os.path.isfile("/tmp/lnd_deposit_address"): addr = get_file_contents("/tmp/lnd_deposit_address") else: addr = get_new_lnd_deposit_address() return to_string(addr) def get_new_lnd_deposit_address(): address = "NEW_ADDR" try: addressdata = lnd_get("/newaddress") address = addressdata["address"] set_file_contents("/tmp/lnd_deposit_address", address) except: address = "ERROR" return address def get_lightning_info(): global lightning_info return copy.deepcopy(lightning_info) def get_lightning_peers(): global lightning_peers peerdata = copy.deepcopy(lightning_peers) peers = [] if peerdata != None and "peers" in peerdata: for p in peerdata["peers"]: peer = p if "bytes_recv" in p: peer["bytes_recv"] = "{:.2f}".format(float(p["bytes_recv"]) / 1000 / 1000) else: peer["bytes_recv"] = "N/A" if "bytes_sent" in p: peer["bytes_sent"] = "{:.2f}".format(float(p["bytes_sent"]) / 1000 / 1000) else: peer["bytes_sent"] = "N/A" if "sat_sent" in p: peer["sat_sent"] = format_sat_amount(peer["sat_sent"]) if "sat_recv" in p: peer["sat_recv"] = format_sat_amount(peer["sat_recv"]) if "ping_time" not in p: peer["ping_time"] = "N/A" if "pub_key" in p: peer["alias"] = get_lightning_peer_alias( p["pub_key"] ) else: peer["alias"] = "Unknown" peers.append(peer) return peers def get_lightning_node_info(pubkey): nodeinfo = lnd_get("/graph/node/{}".format(pubkey), timeout=2) return nodeinfo def get_lightning_peer_alias(pubkey): global lightning_peer_aliases if pubkey in lightning_peer_aliases: return lightning_peer_aliases[pubkey] nodeinfo = get_lightning_node_info(pubkey) if nodeinfo != None and "node" in nodeinfo: if "alias" in nodeinfo["node"]: lightning_peer_aliases[pubkey] = nodeinfo["node"]["alias"] return nodeinfo["node"]["alias"] return "UNKNOWN" def get_lightning_peer_count(): info = get_lightning_info() num_peers = 0 if info != None and "num_peers" in info: num_peers = info['num_peers'] return num_peers def get_lightning_channels(): global lightning_channels channeldata = copy.deepcopy(lightning_channels) channels = [] if channeldata != None and "channels" in channeldata: for c in channeldata["channels"]: channel = c channel["status_color"] = "gray" if "active" in channel: if channel["active"]: channel["status_color"] = "green" else: channel["status_color"] = "yellow" if "capacity" in channel: channel["capacity"] = format_sat_amount(channel["capacity"]) else: channel["capacity"] = "N/A" if "local_balance" in channel and "remote_balance" in channel: l = float(channel["local_balance"]) r = float(channel["remote_balance"]) channel["chan_percent"] = (l / (l+r)) * 100 else: channel["chan_percent"] = "0" if "local_balance" in channel: channel["local_balance"] = format_sat_amount(channel["local_balance"]) else: channel["local_balance"] = "0" if "remote_balance" in channel: channel["remote_balance"] = format_sat_amount(channel["remote_balance"]) else: channel["remote_balance"] = "0" if "remote_pubkey" in channel: channel["remote_alias"] = get_lightning_peer_alias( channel["remote_pubkey"] ) else: channel["remote_alias"] = "Unknown" if "commit_fee" in channel: channel["commit_fee"] = format_sat_amount(channel["commit_fee"]) else: channel["commit_fee"] = "0" if "lifetime" in channel: seconds = int(channel["lifetime"]) channel["age"] = "{}".format(str(datetime.timedelta(seconds=seconds))) else: channel["age"] = "N/A" channels.append(channel) return channels def get_lightning_channel_count(): channels = get_lightning_channels() return len(channels) def get_lightning_channel_balance(): global lightning_channel_balance return copy.deepcopy(lightning_channel_balance) def get_lightning_wallet_balance(): global lightning_wallet_balance return copy.deepcopy(lightning_wallet_balance) def get_lightning_balance_info(): channel_balance_data = get_lightning_channel_balance() wallet_balance_data = get_lightning_wallet_balance() balance_data = {} balance_data["channel_balance"] = "N/A" balance_data["channel_pending"] = "N/A" balance_data["wallet_balance"] = "N/A" balance_data["wallet_pending"] = "N/A" balance_data["total_balance"] = "N/A" channel_num = -1 wallet_num = -1 channel_balance_data = get_lightning_channel_balance() if channel_balance_data != None and "balance" in channel_balance_data: balance_data["channel_balance"] = format_sat_amount( channel_balance_data["balance"] ) channel_num = int(channel_balance_data["balance"]) if channel_balance_data != None and "pending_open_balance" in channel_balance_data: balance_data["channel_pending"] = format_sat_amount( channel_balance_data["pending_open_balance"] ) wallet_balance_data = get_lightning_wallet_balance() if wallet_balance_data != None and "confirmed_balance" in wallet_balance_data: balance_data["wallet_balance"] = format_sat_amount( wallet_balance_data["confirmed_balance"] ) wallet_num = int(wallet_balance_data["confirmed_balance"]) if wallet_balance_data != None and "unconfirmed_balance" in wallet_balance_data: balance_data["wallet_pending"] = format_sat_amount( wallet_balance_data["unconfirmed_balance"] ) if channel_num >= 0 and wallet_num >= 0: balance_data["total_balance"] = format_sat_amount(channel_num + wallet_num) if settings_file_exists("randomize_balances"): channel_num = random.randint(40000,1000000) wallet_num = random.randint(100000,1500000) balance_data["channel_balance"] = format_sat_amount(channel_num) balance_data["channel_pending"] = "0" balance_data["wallet_balance"] = format_sat_amount(wallet_num) balance_data["wallet_pending"] = "0" balance_data["total_balance"] = format_sat_amount(channel_num + wallet_num) return balance_data def get_lightning_transactions(): global lightning_transactions try: transactions = [] data = copy.deepcopy(lightning_transactions) for tx in data["transactions"]: tx["id"] = tx["tx_hash"] tx["amount_str"] = format_sat_amount(tx["amount"]) tx["date_str"] = time.strftime("%D %H:%M", time.localtime(int(tx["time_stamp"]))) transactions.append(tx) return transactions except: return None def get_lightning_payments(): global lightning_payments try: payments = [] data = copy.deepcopy(lightning_payments) for tx in data["payments"]: tx["id"] = tx["payment_hash"] tx["type"] = "PAYMENT" tx["value_str"] = format_sat_amount(tx["value_sat"]) tx["fee_str"] = format_sat_amount(tx["fee"]) tx["date_str"] = time.strftime("%D %H:%M", time.localtime(int(tx["creation_date"]))) tx["memo"] = "" payments.append(tx) payments.reverse() return payments except: return [] def get_lightning_invoices(): global lightning_invoices try: invoices = [] data = copy.deepcopy(lightning_invoices) for tx in data["invoices"]: tx["id"] = tx["r_hash"] tx["type"] = "INVOICE" tx["value_str"] = format_sat_amount(tx["value"]) tx["date_str"] = time.strftime("%D %H:%M", time.localtime(int(tx["creation_date"]))) tx["memo"] = unquote_plus(tx["memo"]) invoices.append(tx) invoices.reverse() return invoices except: return [] def get_lightning_payments_and_invoices(): payments = get_lightning_payments() invoices = get_lightning_invoices() txs = [] if payments == None and invoices == None: return [] elif payments == None and invoices != None: return invoices elif payments != None and invoices == None: return payments elif len(payments) == 0 and len(invoices) == 0: return [] while len(payments) or len(invoices): if len(payments) == 0: txs.insert(0, invoices.pop()) elif len(invoices) == 0: txs.insert(0, payments.pop()) else: # Prepend oldest to list p = payments[-1] i = invoices[-1] if int(p["creation_date"]) < int(i["creation_date"]): txs.insert(0, payments.pop()) else: txs.insert(0, invoices.pop()) for tx in txs: if tx["type"] == "PAYMENT": tx["value_str"] = "-" + tx["value_str"] return txs def get_lightning_watchtower_server_info(): global lightning_watchtower_server_info server_info = copy.deepcopy(lightning_watchtower_server_info) server_info["watchtower_server_uri"] = "..." if server_info != None: try: if "uris" in server_info and len(server_info['uris']) > 0: first_uri = True text = "" for uri in server_info['uris']: if first_uri: first_uri = False else: text += "
" text += uri server_info["watchtower_server_uri"] = text elif "pubkey" in server_info or "listeners" in server_info: server_info["watchtower_server_uri"] = "" if "pubkey" in server_info: server_info["watchtower_server_uri"] += server_info["pubkey"] #if "listeners": # server_info["watchtower_server_uri"] += "listeners: " + watchtower_server_info["listeners"][0] except: return server_info return server_info def get_lightning_watchtower_client_towers(): global lightning_watchtower_client_towers towers = copy.deepcopy(lightning_watchtower_client_towers) return towers def get_lightning_watchtower_client_stats(): global lightning_watchtower_client_stats stats = copy.deepcopy(lightning_watchtower_client_stats) return stats def get_lightning_watchtower_client_policy(): global lightning_watchtower_client_policy policy = copy.deepcopy(lightning_watchtower_client_policy) return policy def is_lnd_ready(): global lnd_ready return lnd_ready def lnd_get(path, timeout=10, params={}): try: macaroon = get_macaroon() headers = {"Grpc-Metadata-macaroon":macaroon} r = requests.get("https://localhost:"+LND_REST_PORT+"/v1"+path, verify=TLS_CERT_FILE,headers=headers, params=params, timeout=timeout) except Exception as e: log_message("ERROR in lnd_get: "+str(e)) return {"error": str(e)} return r.json() def lnd_get_v2(path, timeout=10): try: macaroon = get_macaroon() headers = {'Grpc-Metadata-macaroon': macaroon} r = requests.get("https://localhost:"+LND_REST_PORT+"/v2"+path, verify=TLS_CERT_FILE, headers=headers, timeout=timeout) except Exception as e: log_message("ERROR in lnd_get_v2: "+str(e)) return {"error": str(e)} return r.json() def gen_new_wallet_seed(): seed = to_string(subprocess.check_output("python3 /usr/bin/gen_seed.py", shell=True)) return seed def get_lnd_lit_password(): return to_string( get_file_contents("/mnt/hdd/mynode/settings/.litpw") ) def restart_lnd_actual(): global lnd_ready lnd_ready = False os.system("systemctl restart lnd") os.system("systemctl restart lnd_admin") def restart_lnd(): t = Timer(0.1, restart_lnd_actual) t.start() time.sleep(1) def get_lightning_wallet_file(): if is_testnet_enabled(): return "/mnt/hdd/mynode/lnd/data/chain/bitcoin/testnet/wallet.db" return "/mnt/hdd/mynode/lnd/data/chain/bitcoin/mainnet/wallet.db" def get_lightning_macaroon_file(): if is_testnet_enabled(): return "/mnt/hdd/mynode/lnd/data/chain/bitcoin/testnet/admin.macaroon" return "/mnt/hdd/mynode/lnd/data/chain/bitcoin/mainnet/admin.macaroon" def get_macaroon(): m = to_string(subprocess.check_output("xxd -ps -u -c 1000 " + get_lightning_macaroon_file(), shell=True)) return m.strip() def lnd_wallet_exists(): return os.path.isfile( get_lightning_wallet_file() ) def create_wallet(seed): try: subprocess.check_call("create_lnd_wallet.tcl \""+seed+"\"", shell=True) # Sync FS and sleep so the success redirect understands the wallet was created os.system("sync") time.sleep(2) return True except: return False def is_lnd_logged_in(): try: macaroon = get_macaroon() headers = {"Grpc-Metadata-macaroon":macaroon} r = requests.get("https://localhost:"+LND_REST_PORT+"/v1/getinfo", verify=TLS_CERT_FILE,headers=headers) if r.status_code == 200 and r.json(): return True return False except: return False def get_lnd_channel_backup_file(): if is_testnet_enabled(): return "/home/bitcoin/lnd_backup/channel_testnet.backup" return "/home/bitcoin/lnd_backup/channel.backup" def lnd_channel_backup_exists(): return os.path.isfile( get_lnd_channel_backup_file() ) def lnd_get_channel_db_size(): path = "mainnet" if is_testnet_enabled(): path = "testnet" size = "???" try: size = to_string(subprocess.check_output("ls -lsah /mnt/hdd/mynode/lnd/data/graph/"+path+"/channel.db | awk '{print $6}'", shell=True)) except: size = "ERR" return size def get_lnd_status(): #if not lnd_wallet_exists(): # return "Please create wallet..." if not is_bitcoin_synced(): return "Waiting..." if is_lnd_ready(): return "Running" try: log = get_journalctl_log("lnd") lines = log.splitlines() for line in lines: if "Waiting for wallet encryption password" in line and not lnd_wallet_exists(): return "Please create wallet..." elif "Caught up to height" in line: m = re.search("height ([0-9]+)", line) height = m.group(1) percent = 100.0 * (float(height) / bitcoin_block_height) return "Syncing... {:.2f}%".format(percent) elif "Waiting for chain backend to finish sync" in line: return "Syncing..." elif "Started rescan from block" in line: return "Scanning..." elif "Version: " in line: return "Launching..." elif "Opening the main database" in line: return "Opening DB..." elif "Database now open" in line: return "DB open..." elif "unable to create server" in line: return "Network Error" elif "Waiting for wallet encryption password" in line: return "Logging in..." elif "LightningWallet opened" in line: return "Wallet open..." elif "wallet unlock password file was specified but wallet does not exist" in line: return "Config Error" # Check if no wallet file (log may have been rotated out, so can't get more accurate message) if not lnd_wallet_exists(): return "Please create wallet..." return "Waiting..." except: return "Status Error" def get_lnd_status_color(): if not is_bitcoin_synced(): return "yellow" #if not lnd_wallet_exists(): # # This hides the restart /login attempt LND does from the GUI # return "green" lnd_status_code = get_service_status_code("lnd") if lnd_status_code != 0: lnd_status_color = "red" lnd_status = get_lnd_status() if lnd_status == "Logging in...": lnd_status_color = "yellow" return lnd_status_color return "green" def get_lnd_version(): global lnd_version if lnd_version == None: lnd_version = to_string(subprocess.check_output("lnd --version | egrep -o '[0-9]+\\.[0-9]+\\.[0-9]+' | head -n 1", shell=True)) return "v{}".format(lnd_version) def get_loop_version(): global loop_version if loop_version == None: loop_version = to_string(subprocess.check_output("loopd --version | egrep -o '[0-9]+\\.[0-9]+\\.[0-9]+' | head -n 1", shell=True)) return "v{}".format(loop_version) def get_pool_version(): global pool_version if pool_version == None: pool_version = to_string(subprocess.check_output("poold --version | egrep -o '[0-9]+\\.[0-9]+\\.[0-9]+' | head -n 1", shell=True)) return "v{}".format(pool_version) def get_lit_version(): global lit_version if lit_version == None: #lit_version = to_string(subprocess.check_output("litd --version | egrep -o '[0-9]+\\.[0-9]+\\.[0-9]+' | head -n 1", shell=True)) lit_version = "TODO" return "v{}".format(lit_version) def get_default_lnd_config(): try: with open("/usr/share/mynode/lnd.conf") as f: return f.read() except: return "ERROR" def get_lnd_config(): try: with open("/mnt/hdd/mynode/lnd/lnd.conf") as f: return f.read() except: return "ERROR" def get_lnd_custom_config(): try: with open("/mnt/hdd/mynode/settings/lnd_custom.conf") as f: return f.read() except: return "ERROR" def set_lnd_custom_config(config): try: with open("/mnt/hdd/mynode/settings/lnd_custom.conf", "w") as f: f.write(config) os.system("sync") return True except: return False def using_lnd_custom_config(): return os.path.isfile("/mnt/hdd/mynode/settings/lnd_custom.conf") def delete_lnd_custom_config(): os.system("rm -f /mnt/hdd/mynode/settings/lnd_custom.conf") def get_lnd_alias_file_data(): try: with open("/mnt/hdd/mynode/settings/.lndalias", "r") as f: return f.read().strip() except: return "ERROR" return "ERROR" def is_watchtower_server_enabled(): return settings_file_exists("watchtower_enabled") def enable_watchtower_server(): create_settings_file("watchtower_enabled") def disable_watchtower_server(): delete_settings_file("watchtower_enabled") def is_watchtower_client_enabled(): return settings_file_exists("watchtower_client_enabled") def enable_watchtower_client(): create_settings_file("watchtower_client_enabled") def disable_watchtower_client(): delete_settings_file("watchtower_client_enabled") # Only call from www process which has data def update_lightning_json_cache(): global LIGHTNING_CACHE_FILE lightning_data = {} lightning_data["info"] = get_lightning_info() lightning_data["peers"] = get_lightning_peers() lightning_data["channels"] = get_lightning_channels() lightning_data["balances"] = get_lightning_balance_info() #lightning_data["transactions"] = lightning_transactions #lightning_data["payments"] = lightning_payments #lightning_data["invoices"] = lightning_invoices #lightning_data["watchtower_server_info"] = lightning_watchtower_server_info return set_dictionary_file_cache(lightning_data, LIGHTNING_CACHE_FILE) # Can call from any process def get_lightning_json_cache(): global LIGHTNING_CACHE_FILE return get_dictionary_file_cache(LIGHTNING_CACHE_FILE)