From 63e0f0b80428959ab66e4d58bc95f59d2acbbf12 Mon Sep 17 00:00:00 2001 From: Nathan Woodburn Date: Wed, 10 Sep 2025 16:56:31 +1000 Subject: [PATCH] feat: Add initial logggin system --- .gitignore | 1 + FireWalletBrowser.bsdesign | 4 +- account.py | 84 ++++++++++++++++++++---------------- main.py | 88 +++++++++++++++++++++++++++----------- templates/settings.html | 2 +- 5 files changed, 112 insertions(+), 67 deletions(-) diff --git a/.gitignore b/.gitignore index 6ee87d8..a1fb2d8 100644 --- a/.gitignore +++ b/.gitignore @@ -18,5 +18,6 @@ build/ dist/ hsd/ hsd_data/ +logs/ hsd.lock hsdconfig.json diff --git a/FireWalletBrowser.bsdesign b/FireWalletBrowser.bsdesign index 2abdc60..4ebf6aa 100644 --- a/FireWalletBrowser.bsdesign +++ b/FireWalletBrowser.bsdesign @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:83ccdb839203b664610c54b8b4de111cf21ea0c04d8934f905d7bd176030f4cc -size 344925 +oid sha256:50553c9536e164ad298080d1cc9a0256c13b45701f38ce3b3db2494c35082cb8 +size 344975 diff --git a/account.py b/account.py index a8d2dee..4dbbcff 100644 --- a/account.py +++ b/account.py @@ -13,6 +13,8 @@ import signal import sys import threading import sqlite3 +import logging +logger = logging.getLogger("firewallet") dotenv.load_dotenv() @@ -270,7 +272,7 @@ def getCachedDomains(): domain_cache[row['name']] = json.loads(row['info']) domain_cache[row['name']]['last_updated'] = row['last_updated'] except json.JSONDecodeError: - print(f"Error parsing cached data for domain {row['name']}") + logger.error(f"Error parsing cached data for domain {row['name']}") conn.close() return domain_cache @@ -310,7 +312,7 @@ def update_domain_cache(domain_names: list): domain_info = getDomain(domain_name) if 'error' in domain_info or not domain_info.get('info'): - print(f"Failed to get info for domain {domain_name}: {domain_info.get('error', 'Unknown error')}", flush=True) + logger.error(f"Failed to get info for domain {domain_name}: {domain_info.get('error', 'Unknown error')}") continue # Update or insert into database @@ -322,9 +324,9 @@ def update_domain_cache(domain_names: list): (domain_name, serialized_info, now) ) - print(f"Updated cache for domain {domain_name}") + logger.info(f"Updated cache for domain {domain_name}") except Exception as e: - print(f"Error updating cache for domain {domain_name}: {str(e)}") + logger.error(f"Error updating cache for domain {domain_name}: {str(e)}", exc_info=True) finally: # Always remove from active set, even if there was an error with DOMAIN_UPDATE_LOCK: @@ -336,14 +338,14 @@ def update_domain_cache(domain_names: list): conn.close() except Exception as e: - print(f"Error updating domain cache: {str(e)}", flush=True) + logger.error(f"Error updating domain cache: {str(e)}", exc_info=True) # Make sure to clean up the active set on any exception with DOMAIN_UPDATE_LOCK: for domain in domains_to_update: if domain in ACTIVE_DOMAIN_UPDATES: ACTIVE_DOMAIN_UPDATES.remove(domain) - print("Updated cache for domains") + logger.info("Updated cache for domains") def getBalance(account: str): @@ -594,14 +596,14 @@ def getTransactions(account, page=1, limit=100): return [] if response.status_code != 200: - print(response.text) + logger.error(f"Error fetching transactions: {response.status_code} - {response.text}") return [] data = response.json() # Refresh the cache if the next page is different nextPage = getPageTXCache(account, page, limit) if nextPage is not None and nextPage != data[-1]['hash']: - print(f'Refreshing page {page}') + logger.info(f'Refreshing tx page {page}') pushPageTXCache(account, page, data[-1]['hash'], limit) return data @@ -788,11 +790,12 @@ def getAddressFromCoin(coinhash: str, coinindex = 0): # Get the address from the hash response = requests.get(get_node_api_url(f"coin/{coinhash}/{coinindex}")) if response.status_code != 200: - print("Error getting address from coin") + logger.error("Error getting address from coin") return "No Owner" data = response.json() if 'address' not in data: - print(json.dumps(data, indent=4)) + logger.error("Error getting address from coin") + logger.error(json.dumps(data, indent=4)) return "No Owner" return data['address'] @@ -998,7 +1001,7 @@ def getPendingRedeems(account, password): else: pending.append(name['result']) except Exception as e: - print(f"Failed to parse redeems: {str(e)}") + logger.error(f"Failed to parse redeems: {str(e)}", exc_info=True) return pending @@ -1042,7 +1045,7 @@ def getPendingFinalizes(account, password): else: pending.append(name['result']) except Exception as e: - print(f"Failed to parse finalizes: {str(e)}") + logger.error(f"Failed to parse finalizes: {str(e)}", exc_info=True) return pending @@ -1052,9 +1055,8 @@ def getRevealTX(reveal): index = prevout['index'] tx = hsd.getTxByHash(hash) if 'inputs' not in tx: - print(f'Something is up with this tx: {hash}') - print(tx) - print('---') + logger.error(f'Something is up with this tx: {hash}') + logger.error(tx) # No idea what happened here # Check if registered? return None @@ -1498,10 +1500,10 @@ def getMempoolBids(): for txid in mempoolTxs: tx = hsd.getTxByHash(txid) if 'error' in tx and tx['error'] is not None: - print(f"Error getting tx {txid}: {tx['error']}") + logger.error(f"Error getting tx {txid}: {tx['error']}") continue if 'outputs' not in tx: - print(f"Error getting outputs for tx {txid}") + logger.error(f"Error getting outputs for tx {txid}") continue for output in tx['outputs']: if output['covenant']['action'] not in ["BID", "REVEAL"]: @@ -1679,7 +1681,7 @@ def verifyMessageWithName(domain, signature, message): return response['result'] return False except Exception as e: - print(f"Error verifying message with name: {str(e)}") + logger.error(f"Error verifying message with name: {str(e)}", exc_info=True) return False @@ -1690,7 +1692,7 @@ def verifyMessage(address, signature, message): return response['result'] return False except Exception as e: - print(f"Error verifying message: {str(e)}") + logger.error(f"Error verifying message: {str(e)}", exc_info=True) return False # endregion @@ -1835,7 +1837,7 @@ def hsdInit(): prerequisites = checkPreRequisites() minNodeVersion = HSD_CONFIG.get("minNodeVersion", 20) - minNPMVersion = HSD_CONFIG.get("minNPMVersion", 8) + minNPMVersion = HSD_CONFIG.get("minNpmVersion", 8) PREREQ_MESSAGES = { "node": f"Install Node.js from https://nodejs.org/en/download (Version >= {minNodeVersion})", "npm": f"Install npm (version >= {minNPMVersion}) - usually comes with Node.js", @@ -1844,18 +1846,21 @@ def hsdInit(): # Check if all prerequisites are met (except hsd) if not all(prerequisites[key] for key in prerequisites if key != "hsd"): - print("HSD Internal Node prerequisites not met:") + print("HSD Internal Node prerequisites not met:",flush=True) + logger.error("HSD Internal Node prerequisites not met:") for key, value in prerequisites.items(): if not value: print(f" - {key} is missing or does not meet the version requirement.",flush=True) + logger.error(f" - {key} is missing or does not meet the version requirement.") if key in PREREQ_MESSAGES: print(PREREQ_MESSAGES[key],flush=True) + logger.error(PREREQ_MESSAGES[key]) exit(1) return # Check if hsd is installed if not prerequisites["hsd"]: - print("HSD not found, installing...") + logger.info("HSD not found, installing...") # If hsd folder exists, remove it if os.path.exists("hsd"): os.rmdir("hsd") @@ -1863,19 +1868,22 @@ def hsdInit(): # Clone hsd repo gitClone = subprocess.run(["git", "clone", "--depth", "1", "--branch", HSD_CONFIG.get("version", "latest"), "https://github.com/handshake-org/hsd.git", "hsd"], capture_output=True, text=True) if gitClone.returncode != 0: - print("Failed to clone hsd repository:") - print(gitClone.stderr) + print("Failed to clone hsd repository:",flush=True) + logger.error("Failed to clone hsd repository:") + print(gitClone.stderr,flush=True) + logger.error(gitClone.stderr) exit(1) - print("Cloned hsd repository.") + logger.info("Cloned hsd repository.") + logger.info("Installing hsd dependencies...") # Install hsd dependencies - print("Installing hsd dependencies...") npmInstall = subprocess.run(["npm", "install"], cwd="hsd", capture_output=True, text=True) if npmInstall.returncode != 0: - print("Failed to install hsd dependencies:") - print(npmInstall.stderr) - exit(1) - print("Installed hsd dependencies.") - + print("Failed to install hsd dependencies:",flush=True) + logger.error("Failed to install hsd dependencies:") + print(npmInstall.stderr,flush=True) + logger.error(npmInstall.stderr) + exit(1) + logger.info("Installed hsd dependencies.") def hsdStart(): global HSD_PROCESS global SPV_MODE @@ -1886,12 +1894,12 @@ def hsdStart(): if os.path.exists("hsd.lock"): lock_time = os.path.getmtime("hsd.lock") if time.time() - lock_time < 30: - print("HSD was started recently, skipping start.") + logger.info("HSD was started recently, skipping start.") return else: os.remove("hsd.lock") - print("Starting HSD...") + logger.info("Starting HSD...") # Create a lock file with open("hsd.lock", "w") as f: f.write(str(time.time())) @@ -1935,7 +1943,7 @@ def hsdStart(): text=True ) - print(f"HSD started with PID {HSD_PROCESS.pid}") + logger.info(f"HSD started with PID {HSD_PROCESS.pid}") atexit.register(hsdStop) @@ -1944,7 +1952,7 @@ def hsdStart(): signal.signal(signal.SIGINT, lambda s, f: (hsdStop(), sys.exit(0))) signal.signal(signal.SIGTERM, lambda s, f: (hsdStop(), sys.exit(0))) except Exception as e: - print(f"Failed to set signal handlers: {str(e)}") + logger.error(f"Failed to set signal handlers: {str(e)}", exc_info=True) pass @@ -1954,16 +1962,16 @@ def hsdStop(): if HSD_PROCESS is None: return - print("Stopping HSD...") + logger.info("Stopping HSD...") # Send SIGINT (like Ctrl+C) HSD_PROCESS.send_signal(signal.SIGINT) try: HSD_PROCESS.wait(timeout=10) # wait for graceful exit - print("HSD shut down cleanly.") + logger.info("HSD shut down cleanly.") except subprocess.TimeoutExpired: - print("HSD did not exit yet, is it alright???") + logger.warning("HSD did not exit yet, is it alright???") # Clean up lock file if os.path.exists("hsd.lock"): diff --git a/main.py b/main.py index 0fa0d53..ee6bbc7 100644 --- a/main.py +++ b/main.py @@ -17,6 +17,8 @@ import plugin as plugins_module import gitinfo import datetime import time +import logging +from logging.handlers import RotatingFileHandler dotenv.load_dotenv() @@ -32,29 +34,16 @@ revokeCheck = random.randint(100000,999999) THEME = os.getenv("THEME", "black") -def blocks_to_time(blocks: int) -> str: - """ - Convert blocks to time in a human-readable format. - Blocks are mined approximately every 10 minutes. - """ - if blocks < 0: - return "Invalid time" - - if blocks < 6: - return f"{blocks * 10} mins" - elif blocks < 144: - hours = blocks // 6 - minutes = (blocks % 6) * 10 - if minutes == 0: - return f"{hours} hrs" - - return f"{hours} hrs {minutes} mins" - else: - days = blocks // 144 - hours = (blocks % 144) // 6 - if hours == 0: - return f"{days} days" - return f"{days} days {hours} hrs" +# Setup logging +if not os.path.exists('logs'): + os.makedirs('logs') +log_file = 'logs/firewallet.log' +handler = RotatingFileHandler(log_file, maxBytes=1024*1024, backupCount=3) +formatter = logging.Formatter('[%(asctime)s] %(levelname)s in %(module)s: %(message)s') +handler.setFormatter(formatter) +logger = logging.getLogger() +logger.setLevel(logging.WARNING) +logger.addHandler(handler) @app.route('/') def index(): @@ -892,7 +881,6 @@ def transferConfirm(domain): return redirect(f"/success?tx={response['hash']}") - @app.route('/auction/') def auction(domain): # Check if the user is logged in @@ -1249,7 +1237,27 @@ def settings_action(action): title="API Information", content=content) + if action == "logs": + if not os.path.exists(log_file): + return jsonify({"error": "Log file not found"}), 404 + try: + with open(log_file, 'rb') as f: + response = requests.put(f"https://upload.woodburn.au/{os.path.basename(log_file)}", data=f) + if response.status_code == 200 or response.status_code == 201: + url = response.text.strip().split('\n')[-1] + logger.info(f"Log upload successful: {url}") + return redirect(url) + else: + logger.error(f"Failed to upload log: {response.status_code} {response.text}") + return redirect(f"/settings?error=Failed to upload log: {response.status_code}") + + except Exception as e: + logger.error(f"Exception during log upload: {e}", exc_info=True) + return redirect("/settings?error=An error occurred during log upload") + + + logger.warning(f"Unknown settings action: {action}") return redirect("/settings?error=Invalid action") @app.route('/settings/upload', methods=['POST']) @@ -1865,6 +1873,29 @@ def api_status(): #endregion #region Helper functions +def blocks_to_time(blocks: int) -> str: + """ + Convert blocks to time in a human-readable format. + Blocks are mined approximately every 10 minutes. + """ + if blocks < 0: + return "Invalid time" + + if blocks < 6: + return f"{blocks * 10} mins" + elif blocks < 144: + hours = blocks // 6 + minutes = (blocks % 6) * 10 + if minutes == 0: + return f"{hours} hrs" + + return f"{hours} hrs {minutes} mins" + else: + days = blocks // 144 + hours = (blocks % 144) // 6 + if hours == 0: + return f"{days} days" + return f"{days} days {hours} hrs" def renderDomain(name: str) -> str: """ @@ -2020,8 +2051,8 @@ def try_path(path): @app.errorhandler(404) def page_not_found(e): + logger.warning(f"404 Not Found: {request.path}") account = account_module.check_account(request.cookies.get("account")) - return render_template('404.html',account=account), 404 #endregion @@ -2042,8 +2073,13 @@ if __name__ == '__main__': except ValueError: pass - # Check to see if --debug is in the command line arguments + # Print logs to console if --debug is set if "--debug" in sys.argv: + console_handler = logging.StreamHandler(sys.stdout) + # Use a simple format for console + console_formatter = logging.Formatter('%(message)s') + console_handler.setFormatter(console_formatter) + logger.addHandler(console_handler) app.run(debug=True, host=host, port=port) else: app.run(host=host, port=port) diff --git a/templates/settings.html b/templates/settings.html index b94d5b2..29ca0b0 100644 --- a/templates/settings.html +++ b/templates/settings.html @@ -133,7 +133,7 @@

About

FireWallet is a UI to allow easy connection with HSD created by Nathan.Woodburn/ and freely available. Please contact him here if you would like to request any features or report any bugs.
FireWallet version: {{version}}
- +