12 Commits

Author SHA1 Message Date
c4cd2bc443 fix: Check if blocksSinceExpired to get expired domains
All checks were successful
Build Docker / Build Images (map[dockerfile:Dockerfile.hsd tag_suffix:-hsd target:hsd]) (push) Successful in 2m56s
Build Docker / Build Images (map[dockerfile:Dockerfile tag_suffix: target:default]) (push) Successful in 3m4s
Tests and Linting / Tests-Linting (3.11) (push) Successful in 3m6s
Tests and Linting / Tests-Linting (3.10) (push) Successful in 3m24s
Tests and Linting / Tests-Linting (3.13) (push) Successful in 3m24s
2025-09-18 16:35:34 +10:00
608933c228 fix: Calculate balance with expired domains 2025-09-18 16:34:45 +10:00
d9e847a995 feat: Add support for verbose sync status and don't require auth for some api routes
All checks were successful
Tests and Linting / Tests-Linting (3.11) (push) Successful in 28s
Tests and Linting / Tests-Linting (3.10) (push) Successful in 30s
Tests and Linting / Tests-Linting (3.13) (push) Successful in 30s
Build Docker / Build Images (map[dockerfile:Dockerfile.hsd tag_suffix:-hsd target:hsd]) (push) Successful in 45s
Build Docker / Build Images (map[dockerfile:Dockerfile tag_suffix: target:default]) (push) Successful in 48s
2025-09-16 22:16:29 +10:00
15d919ca97 feat: Add option to use http basic auth for api routes
All checks were successful
Build Docker / Build Images (map[dockerfile:Dockerfile.hsd tag_suffix:-hsd target:hsd]) (push) Successful in 3m2s
Build Docker / Build Images (map[dockerfile:Dockerfile tag_suffix: target:default]) (push) Successful in 3m8s
Tests and Linting / Tests-Linting (3.11) (push) Successful in 3m12s
Tests and Linting / Tests-Linting (3.13) (push) Successful in 3m19s
Tests and Linting / Tests-Linting (3.10) (push) Successful in 3m24s
This shoudl make it easier to use curl to access info
2025-09-16 16:42:09 +10:00
aa52911823 fix: Style settings links as inline-blocks
All checks were successful
Build Docker / Build Images (map[dockerfile:Dockerfile tag_suffix: target:default]) (push) Successful in 2m53s
Build Docker / Build Images (map[dockerfile:Dockerfile.hsd tag_suffix:-hsd target:hsd]) (push) Successful in 2m59s
Tests and Linting / Tests-Linting (3.13) (push) Successful in 3m2s
Tests and Linting / Tests-Linting (3.10) (push) Successful in 3m5s
Tests and Linting / Tests-Linting (3.11) (push) Successful in 3m13s
This fixes mobile views from splitting the text and icons for the links
2025-09-15 11:07:17 +10:00
2ee294cab8 feat: Add git version to status route
All checks were successful
Tests and Linting / Tests-Linting (3.11) (push) Successful in 30s
Tests and Linting / Tests-Linting (3.10) (push) Successful in 35s
Tests and Linting / Tests-Linting (3.13) (push) Successful in 36s
Build Docker / Build Images (map[dockerfile:Dockerfile.hsd tag_suffix:-hsd target:hsd]) (push) Successful in 46s
Build Docker / Build Images (map[dockerfile:Dockerfile tag_suffix: target:default]) (push) Successful in 52s
2025-09-12 15:40:39 +10:00
19771fe30d feat: Add better status check for internal nodes and error logging
All checks were successful
Tests and Linting / Tests-Linting (3.10) (push) Successful in 27s
Tests and Linting / Tests-Linting (3.11) (push) Successful in 28s
Tests and Linting / Tests-Linting (3.13) (push) Successful in 28s
Build Docker / Build Images (map[dockerfile:Dockerfile tag_suffix: target:default]) (push) Successful in 46s
Build Docker / Build Images (map[dockerfile:Dockerfile.hsd tag_suffix:-hsd target:hsd]) (push) Successful in 46s
2025-09-12 15:23:56 +10:00
12d3958b9d fix: Auction sort crashes on 0 bids
All checks were successful
Build Docker / Build Images (map[dockerfile:Dockerfile tag_suffix: target:default]) (push) Successful in 2m14s
Tests and Linting / Tests-Linting (3.13) (push) Successful in 2m17s
Tests and Linting / Tests-Linting (3.10) (push) Successful in 2m23s
Build Docker / Build Images (map[dockerfile:Dockerfile.hsd tag_suffix:-hsd target:hsd]) (push) Successful in 2m36s
Tests and Linting / Tests-Linting (3.11) (push) Successful in 2m39s
2025-09-12 12:07:16 +10:00
d20fc1eb55 feat: Use inline link for logs to improve readability
All checks were successful
Tests and Linting / Tests-Linting (3.11) (push) Successful in 31s
Tests and Linting / Tests-Linting (3.10) (push) Successful in 36s
Tests and Linting / Tests-Linting (3.13) (push) Successful in 40s
Build Docker / Build Images (map[dockerfile:Dockerfile.hsd tag_suffix:-hsd target:hsd]) (push) Successful in 44s
Build Docker / Build Images (map[dockerfile:Dockerfile tag_suffix: target:default]) (push) Successful in 54s
2025-09-11 16:55:51 +10:00
148e5f325a fix: Move logger before imports to ensure it is initialized before logs start
All checks were successful
Tests and Linting / Tests-Linting (3.11) (push) Successful in 29s
Tests and Linting / Tests-Linting (3.10) (push) Successful in 31s
Tests and Linting / Tests-Linting (3.13) (push) Successful in 32s
Build Docker / Build Images (map[dockerfile:Dockerfile tag_suffix: target:default]) (push) Successful in 46s
Build Docker / Build Images (map[dockerfile:Dockerfile.hsd tag_suffix:-hsd target:hsd]) (push) Successful in 47s
2025-09-11 16:48:43 +10:00
6442aa4df6 fix: Return error message if logs are nonexistent or empty 2025-09-11 15:28:37 +10:00
2e86e64dd0 Merge pull request 'Add logging system' (#8) from feat/logging into main
All checks were successful
Tests and Linting / Tests-Linting (3.10) (push) Successful in 31s
Tests and Linting / Tests-Linting (3.13) (push) Successful in 32s
Tests and Linting / Tests-Linting (3.11) (push) Successful in 39s
Build Docker / Build Images (map[dockerfile:Dockerfile tag_suffix: target:default]) (push) Successful in 49s
Build Docker / Build Images (map[dockerfile:Dockerfile.hsd tag_suffix:-hsd target:hsd]) (push) Successful in 54s
Reviewed-on: #8
2025-09-11 15:19:24 +10:00
5 changed files with 211 additions and 78 deletions

Binary file not shown.

View File

@@ -45,9 +45,7 @@ if HSD_INTERNAL_NODE:
HSD_API = "firewallet-" + str(int(time.time()))
HSD_IP = "localhost"
SHOW_EXPIRED = os.getenv("SHOW_EXPIRED")
if SHOW_EXPIRED is None:
SHOW_EXPIRED = False
SHOW_EXPIRED = os.getenv("SHOW_EXPIRED","false").lower() in ["1","true","yes"]
HSD_PROCESS = None
SPV_MODE = None
@@ -95,6 +93,7 @@ def hsdConnected():
def hsdVersion(format=True):
info = hsd.getInfo()
if 'error' in info:
logger.error(f"HSD connection error: {info.get('error', 'Unknown error')}")
return -1
# Check if SPV mode is enabled
@@ -118,9 +117,12 @@ def check_account(cookie: str | None):
return False
account = cookie.split(":")[0]
if len(account) < 1:
return False
# Check if the account is valid
info = hsw.getAccountInfo(account, 'default')
if 'error' in info:
logger.error(f"HSW error checking account {account}: {info.get('error', 'Unknown error')}")
return False
return account
@@ -364,7 +366,7 @@ def getBalance(account: str):
available = available / 1000000
logger.debug(f"Initial balance for account {account}: total={total}, available={available}, locked={locked}")
domains = getDomains(account)
domains = getDomains(account,True,True)
domainValue = 0
domains_to_update = [] # Track domains that need cache updates
@@ -471,22 +473,27 @@ def getPendingTX(account: str):
return pending
def getDomains(account, own=True):
def getDomains(account, own: bool = True, expired: bool = SHOW_EXPIRED):
if own:
response = requests.get(get_wallet_api_url(f"/wallet/{account}/name?own=true"))
else:
response = requests.get(get_wallet_api_url(f"/wallet/{account}/name"))
info = response.json()
if SHOW_EXPIRED:
if expired:
return info
# Remove any expired domains
domains = []
for domain in info:
if 'stats' in domain:
if 'stats' in domain and domain['stats'] is not None:
if 'daysUntilExpire' in domain['stats']:
if domain['stats']['daysUntilExpire'] < 0:
logger.debug(f"Excluding expired domain: {domain['name']} due to daysUntilExpire")
continue
if 'blocksSinceExpired' in domain['stats']:
if domain['stats']['blocksSinceExpired'] > 0:
logger.debug(f"Excluding expired domain: {domain['name']} due to blocksSinceExpired")
continue
domains.append(domain)
@@ -583,6 +590,7 @@ def getTransactions(account, page=1, limit=100):
return []
info = hsw.getWalletTxHistory(account)
if 'error' in info:
logger.error(f"Error getting transactions for account {account}: {info['error']}")
return []
return info[::-1]
@@ -916,6 +924,7 @@ def register(account, domain):
def getNodeSync():
response = hsd.getInfo()
if 'error' in response:
logger.error(f"Error getting node sync status: {response['error']}")
return 0
sync = response['chain']['progress']*100
@@ -923,11 +932,14 @@ def getNodeSync():
return sync
def getWalletStatus():
def getWalletStatus(verbose: bool = False):
response = hsw.rpc_getWalletInfo()
if 'error' in response and response['error'] is not None:
return "Error"
if verbose:
return response.get('result', {})
# return response
walletHeight = response['result']['height']
# Get the current block height
@@ -1564,9 +1576,15 @@ def getMempoolBids():
# region settingsAPIs
def rescan():
def rescan(height:int = 0):
try:
response = hsw.walletRescan(0)
response = hsw.walletRescan(height)
if 'success' in response and response['success'] is False:
return {
"error": {
"message": "Rescan already in progress"
}
}
return response
except Exception as e:
return {
@@ -1941,6 +1959,7 @@ def hsdStart():
cmd.append(flag)
# Launch process
try:
HSD_PROCESS = subprocess.Popen(
cmd,
cwd=os.getcwd(),
@@ -1948,6 +1967,9 @@ def hsdStart():
)
logger.info(f"HSD started with PID {HSD_PROCESS.pid}")
except Exception as e:
logger.error(f"Failed to start HSD: {str(e)}", exc_info=True)
return
atexit.register(hsdStop)
@@ -1959,6 +1981,19 @@ def hsdStart():
logger.error(f"Failed to set signal handlers: {str(e)}", exc_info=True)
pass
def hsdRunning() -> bool:
global HSD_PROCESS
if not HSD_INTERNAL_NODE:
return False
if HSD_PROCESS is None:
return False
# Check if process has terminated
poll_result = HSD_PROCESS.poll()
if poll_result is not None:
logger.error(f"HSD process has terminated with exit code: {poll_result}")
return False
return True
def hsdStop():
global HSD_PROCESS

View File

@@ -5,3 +5,4 @@ SHOW_EXPIRED=false
EXPLORER_TX=https://shakeshift.com/transaction/
DISABLE_WALLETDNS=false
INTERNAL_HSD=false
LOG_LEVEL=WARNING

189
main.py
View File

@@ -7,13 +7,9 @@ from flask import Flask, make_response, redirect, request, jsonify, render_templ
import os
import dotenv
import requests
import account as account_module
import render
import re
from flask_qrcode import QRcode
import domainLookup
import urllib.parse
import plugin as plugins_module
import gitinfo
import datetime
import time
@@ -22,18 +18,6 @@ from logging.handlers import RotatingFileHandler
dotenv.load_dotenv()
app = Flask(__name__)
qrcode = QRcode(app)
# Change this if network fees change
fees = 0.02
revokeCheck = random.randint(100000,999999)
THEME = os.getenv("THEME", "black")
# Setup logging
if not os.path.exists('logs'):
os.makedirs('logs')
@@ -42,15 +26,33 @@ 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()
log_level = os.getenv("LOG_LEVEL", "WARNING").upper()
if log_level in ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]:
logger.setLevel(getattr(logging, log_level))
else:
logger.setLevel(logging.WARNING)
# Disable werkzeug logging
logging.getLogger('werkzeug').setLevel(logging.INFO)
logging.getLogger("urllib3").setLevel(logging.ERROR)
logging.getLogger("requests").setLevel(logging.ERROR)
logger.addHandler(handler)
# Import after logger setup to capture logs from these modules
import render # noqa: E402
import domainLookup # noqa: E402
import account as account_module # noqa: E402
import plugin as plugins_module # noqa: E402
app = Flask(__name__)
qrcode = QRcode(app)
# Change this if network fees change
fees = 0.02
revokeCheck = random.randint(100000,999999)
THEME = os.getenv("THEME", "black")
@app.route('/')
def index():
# Check if the user is logged in
@@ -315,6 +317,7 @@ def auctions():
sort_time_next = reverseDirection(direction)
# If older HSD version sort by domain height
if len(bids) > 0:
if bids[0]['height'] == 0:
domains = sorted(domains, key=lambda k: k['height'],reverse=reverse)
sortbyDomain = True
@@ -1235,12 +1238,18 @@ def settings_action(action):
if action == "logs":
if not os.path.exists(log_file):
return jsonify({"error": "Log file not found"}), 404
return redirect("/settings?error=No log file found")
# Check if log file is empty
if os.path.getsize(log_file) == 0:
return redirect("/settings?error=Log file is empty")
try:
with open(log_file, 'rb') as f:
response = requests.put(f"https://upload.woodburn.au/{os.path.basename(log_file)}", data=f)
response = requests.put(f"https://upload.woodburn.au/{os.path.basename(log_file)}", data=f,
headers={"Max-Days": "5"})
if response.status_code == 200 or response.status_code == 201:
url = response.text.strip().split('\n')[-1]
token = response.text.strip().split('\n')[-1].split('/')[-2:]
url = f"https://upload.woodburn.au/inline/{token[0]}/{token[1]}"
logger.info(f"Log upload successful: {url}")
return redirect(url)
else:
@@ -1598,13 +1607,6 @@ def plugin_function(ptype,plugin,function):
#region API Routes
@app.route('/api/v1/hsd/<function>', methods=["GET"])
def api_hsd(function):
# Check if the user is logged in
if request.cookies.get("account") is None:
return jsonify({"error": "Not logged in"})
account = account_module.check_account(request.cookies.get("account"))
if not account:
return jsonify({"error": "Invalid account"})
if function == "sync":
return jsonify({"result": account_module.getNodeSync()})
@@ -1614,8 +1616,22 @@ def api_hsd(function):
return jsonify({"result": account_module.getBlockHeight()})
if function == "mempool":
return jsonify({"result": account_module.getMempoolTxs()})
if function == "mempoolBids":
# Check if the user is logged in for all other functions
account = None
if request.cookies.get("account") is not None:
account = account_module.check_account(request.cookies.get("account"))
# Allow login using http basic auth
if account is None and request.authorization is not None:
account = account_module.check_account(f"{request.authorization.username}:{request.authorization.password}")
if not account:
return jsonify({"error": "Not logged in"})
if function == "mempoolBids": # This is a heavy function so only allow for logged in users
return jsonify({"result": account_module.getMempoolBids()})
if function == "nextAuctionState":
# Get the domain from the query parameters
domain = request.args.get('domain')
@@ -1699,20 +1715,29 @@ def api_hsd_mobile(function):
@app.route('/api/v1/wallet/<function>', methods=["GET"])
def api_wallet(function):
# Check if the user is logged in
if request.cookies.get("account") is None:
return jsonify({"error": "Not logged in"})
account = account_module.check_account(request.cookies.get("account"))
if not account:
return jsonify({"error": "Invalid account"})
password = request.cookies.get("account","").split(":")[1]
if not account:
return jsonify({"error": "Invalid account"})
if function == "sync":
return jsonify({"result": account_module.getWalletStatus()})
# Check if arg verbose is set
verbose = request.args.get('verbose', 'false').lower() == 'true'
return jsonify({"result": account_module.getWalletStatus(verbose)})
# Check if the user is logged in for all other functions
account = None
password = None
if request.cookies.get("account") is not None:
account = account_module.check_account(request.cookies.get("account"))
password = request.cookies.get("account","").split(":")[1]
# Allow login using http basic auth
if account is None and request.authorization is not None:
account = account_module.check_account(f"{request.authorization.username}:{request.authorization.password}")
password = request.authorization.password
if not account:
return jsonify({"error": "Not logged in"})
if function == "balance":
return jsonify({"result": account_module.getBalance(account)})
if function == "available":
return jsonify({"result": account_module.getBalance(account)['available']})
@@ -1859,9 +1884,43 @@ def api_icon(account):
def api_status():
# This doesn't require a login
# Check if the node is connected
if not account_module.hsdConnected():
return jsonify({"status":503,"error": "Node not connected"}), 503
return jsonify({"status": 200,"result": "FireWallet is running"})
node_status = {
"connected": account_module.hsdConnected(),
"internal": account_module.HSD_INTERNAL_NODE,
"internal_running": False,
"version": "N/A"
}
status = 200
error = None
node_status['version'] = account_module.hsdVersion(False)
if node_status['internal']:
node_status['internal_running'] = account_module.hsdRunning()
# If the node is not connected, return an error
if not node_status['connected']:
error = "Node not connected"
status = 503
if node_status['version'] == -1:
error = "Error connecting to HSD"
status = 503
if node_status['internal'] and not node_status['internal_running']:
error = "Internal node not running"
status = 503
commit = currentCurrentCommit()
return jsonify({
"node": node_status,
"version": {
"commit": commit,
"branch": currentCurrentBranch(),
"latest": runningLatestVersion(),
"url": f"https://git.woodburn.au/nathanwoodburn/firewalletbrowser/commit/{commit}" if commit != "Error" else None
},
"error": error,
"status": status
}), status
#endregion
@@ -1906,6 +1965,45 @@ def renderDomain(name: str) -> str:
except Exception:
return f"{name}/"
def currentCurrentCommit() -> str:
"""
Get the current commit of the application.
"""
if not os.path.exists(".git"):
return "Error"
info = gitinfo.get_git_info()
if info is None:
return "Error"
commit = info['commit']
return commit
def currentCurrentBranch() -> str:
"""
Get the current branch of the application.
"""
if not os.path.exists(".git"):
return "Error"
info = gitinfo.get_git_info()
if info is None:
return "Error"
branch = info['refs']
return branch
def runningLatestVersion() -> bool:
"""
Check if the current version is the latest version.
"""
if not os.path.exists(".git"):
return False
info = gitinfo.get_git_info()
if info is None:
return False
branch = info['refs']
commit = info['commit']
if commit != latestVersion(branch):
return False
return True
def get_alerts(account:str) -> list:
"""
Get alerts to show on the dashboard.
@@ -1939,8 +2037,8 @@ def get_alerts(account:str) -> list:
wallet_status = account_module.getWalletStatus()
if wallet_status != "Ready":
alerts.append({
"name": "Wallet",
"output": f"The wallet is not synced ({wallet_status}). Please wait for it to sync."
"name": "Wallet Not Synced",
"output": "Please wait for it to sync."
})
# Try to read from notifications sqlite database
if os.path.exists("user_data/notifications.db"):
@@ -2091,7 +2189,6 @@ if __name__ == '__main__':
logger.setLevel(logging.DEBUG)
app.run(debug=True, host=host, port=port)
else:
app.run(host=host, port=port)
def tests():

View File

@@ -133,7 +133,7 @@
<div class="card-body">
<h4 class="card-title">About</h4>
<h6 class="text-muted mb-2 card-subtitle">FireWallet is a UI to allow easy connection with HSD created by <a href="https://nathan.woodburn.au" target="_blank">Nathan.Woodburn/</a> and freely available. Please contact him <a href="https://l.woodburn.au/contact" target="_blank">here</a> if you would like to request any features or report any bugs.<br>FireWallet version: <code>{{version}}</code></h6>
<div class="text-center"><a href="https://github.com/nathanwoodburn/firewalletbrowser" style="margin: 15px;color: var(--bs-emphasis-color);text-decoration:none;" target="_blank"><i class="icon ion-social-github" style="color: var(--bs-emphasis-color);"></i>&nbsp;Github</a><a href="https://firewallet.au" style="margin: 15px;color: var(--bs-emphasis-color);text-decoration:none;" target="_blank"><i class="icon ion-ios-information" style="color: var(--bs-emphasis-color);"></i>&nbsp;Website</a><a href="https://l.woodburn.au/donate" style="margin: 15px;color: var(--bs-emphasis-color);text-decoration:none;" target="_blank"><i class="icon ion-social-usd" style="color: var(--bs-emphasis-color);"></i>&nbsp;Donate to support development</a><a href="/settings/logs" style="margin: 15px;color: var(--bs-emphasis-color);text-decoration:none;" target="_blank"><i class="icon ion-help" style="color: var(--bs-emphasis-color);"></i>&nbsp;Upload logs for debugging</a></div>
<div class="text-center"><a href="https://github.com/nathanwoodburn/firewalletbrowser" style="margin: 15px;color: var(--bs-emphasis-color);text-decoration: none;display: inline-block;" target="_blank"><i class="icon ion-social-github" style="color: var(--bs-emphasis-color);"></i>&nbsp;Github</a><a href="https://firewallet.au" style="margin: 15px;color: var(--bs-emphasis-color);text-decoration: none;display: inline-block;" target="_blank"><i class="icon ion-ios-information" style="color: var(--bs-emphasis-color);"></i>&nbsp;Website</a><a href="https://l.woodburn.au/donate" style="margin: 15px;color: var(--bs-emphasis-color);text-decoration: none;display: inline-block;" target="_blank"><i class="icon ion-social-usd" style="color: var(--bs-emphasis-color);"></i>&nbsp;Donate to support development</a><a href="/settings/logs" style="margin: 15px;color: var(--bs-emphasis-color);text-decoration: none;display: inline-block;" target="_blank"><i class="icon ion-help" style="color: var(--bs-emphasis-color);"></i>&nbsp;Upload logs for debugging</a></div>
</div>
</div>
</div>