feat: Add initial internal node option
All checks were successful
Build Docker / Build Image (push) Successful in 10m29s

This commit is contained in:
2025-08-26 16:44:10 +10:00
parent 4c84bc2bbe
commit 5ff8960b7b
9 changed files with 310 additions and 30 deletions

2
.gitignore vendored
View File

@@ -17,3 +17,5 @@ cache/
build/ build/
dist/ dist/
hsd/ hsd/
hsd-data/
hsd.lock

Binary file not shown.

View File

@@ -124,6 +124,8 @@ SHOW_EXPIRED: Show expired domains (true/false)
EXCLUDE: Comma separated list of wallets to exclude from the wallet list (default primary) EXCLUDE: Comma separated list of wallets to exclude from the wallet list (default primary)
EXPLORER_TX: URL for exploring transactions (default https://shakeshift.com/transaction/) EXPLORER_TX: URL for exploring transactions (default https://shakeshift.com/transaction/)
HSD_NETWORK: Network to connect to (main, regtest, simnet) HSD_NETWORK: Network to connect to (main, regtest, simnet)
DISABLE_WALLETDNS: Disable Wallet DNS records when sending HNS to domains (true/false)
INTERNAL_HSD: Use internal HSD node (true/false)
``` ```

View File

@@ -7,6 +7,11 @@ import re
import domainLookup import domainLookup
import json import json
import time import time
import subprocess
import atexit
import signal
import sys
dotenv.load_dotenv() dotenv.load_dotenv()
@@ -29,17 +34,33 @@ elif HSD_NETWORK == "regtest":
HSD_WALLET_PORT = 14039 HSD_WALLET_PORT = 14039
HSD_NODE_PORT = 14037 HSD_NODE_PORT = 14037
INTERNAL_NODE = os.getenv("INTERNAL_HSD","false").lower() in ["1","true","yes"] HSD_INTERNAL_NODE = os.getenv("INTERNAL_HSD","false").lower() in ["1","true","yes"]
if INTERNAL_NODE: if HSD_INTERNAL_NODE:
if HSD_API == "": if HSD_API == "":
# Use a random API KEY # Use a random API KEY
HSD_API = "firewallet-" + str(int(time.time())) HSD_API = "firewallet-" + str(int(time.time()))
HSD_IP = "localhost"
SHOW_EXPIRED = os.getenv("SHOW_EXPIRED") SHOW_EXPIRED = os.getenv("SHOW_EXPIRED")
if SHOW_EXPIRED is None: if SHOW_EXPIRED is None:
SHOW_EXPIRED = False SHOW_EXPIRED = False
HSD_PROCESS = None
# Get hsdconfig.json
HSD_CONFIG = {}
if not os.path.exists('hsdconfig.json'):
# Pull from the latest git
response = requests.get("https://git.woodburn.au/nathanwoodburn/firewalletbrowser/raw/branch/main/hsdconfig.json")
if response.status_code == 200:
with open('hsdconfig.json', 'w') as f:
f.write(response.text)
HSD_CONFIG = response.json()
else:
with open('hsdconfig.json') as f:
HSD_CONFIG = json.load(f)
hsd = api.hsd(HSD_API, HSD_IP, HSD_NODE_PORT) hsd = api.hsd(HSD_API, HSD_IP, HSD_NODE_PORT)
hsw = api.hsw(HSD_API, HSD_IP, HSD_WALLET_PORT) hsw = api.hsw(HSD_API, HSD_IP, HSD_WALLET_PORT)
@@ -1439,7 +1460,6 @@ def generateReport(account, format="{name},{expiry},{value},{maxBid}"):
def convertHNS(value: int): def convertHNS(value: int):
return value/1000000 return value/1000000
return value/1000000
def get_node_api_url(path=''): def get_node_api_url(path=''):
@@ -1461,3 +1481,188 @@ def get_wallet_api_url(path=''):
path = f'/{path}' path = f'/{path}'
return f"{base_url}{path}" return f"{base_url}{path}"
return base_url return base_url
# region HSD Internal Node
def checkPreRequisites() -> dict[str, bool]:
prerequisites = {
"node": False,
"npm": False,
"git": False,
"hsd": False
}
# Check if node is installed and get version
nodeSubprocess = subprocess.run(["node", "-v"], capture_output=True, text=True)
if nodeSubprocess.returncode == 0:
major_version = int(nodeSubprocess.stdout.strip().lstrip('v').split('.')[0])
if major_version >= HSD_CONFIG.get("minNodeVersion", 20):
prerequisites["node"] = True
# Check if npm is installed
npmSubprocess = subprocess.run(["npm", "-v"], capture_output=True, text=True)
if npmSubprocess.returncode == 0:
major_version = int(npmSubprocess.stdout.strip().split('.')[0])
if major_version >= HSD_CONFIG.get("minNPMVersion", 8):
prerequisites["npm"] = True
# Check if git is installed
gitSubprocess = subprocess.run(["git", "-v"], capture_output=True, text=True)
if gitSubprocess.returncode == 0:
prerequisites["git"] = True
# Check if hsd is installed
if os.path.exists("./hsd/bin/hsd"):
prerequisites["hsd"] = True
return prerequisites
def hsdInit():
if not HSD_INTERNAL_NODE:
return
prerequisites = checkPreRequisites()
PREREQ_MESSAGES = {
"node": "Install Node.js from https://nodejs.org/en/download (Version >= {minNodeVersion})",
"npm": "Install npm (version >= {minNPMVersion}) - usually comes with Node.js",
"git": "Install Git from https://git-scm.com/downloads"}
# 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:")
for key, value in prerequisites.items():
if not value:
print(f" - {key} is missing or does not meet the version requirement.")
exit(1)
return
# Check if hsd is installed
if not prerequisites["hsd"]:
print("HSD not found, installing...")
# If hsd folder exists, remove it
if os.path.exists("hsd"):
os.rmdir("hsd")
# 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)
exit(1)
print("Cloned hsd repository.")
# 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.")
def hsdStart():
global HSD_PROCESS
if not HSD_INTERNAL_NODE:
return
# Check if hsd was started in the last 30 seconds
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.")
return
else:
os.remove("hsd.lock")
print("Starting HSD...")
# Create a lock file
with open("hsd.lock", "w") as f:
f.write(str(time.time()))
# Config lookups with defaults
chain_migrate = HSD_CONFIG.get("chainMigrate", False)
wallet_migrate = HSD_CONFIG.get("walletMigrate", False)
spv = HSD_CONFIG.get("spv", False)
# Base command
cmd = [
"node",
"./hsd/bin/hsd",
f"--network={HSD_NETWORK}",
f"--prefix={os.path.join(os.getcwd(), 'hsd-data')}",
f"--api-key={HSD_API}",
"--agent=FireWallet",
"--http-host=127.0.0.1",
"--log-console=false"
]
# Conditionally add migration flags
if chain_migrate:
cmd.append(f"--chain-migrate={chain_migrate}")
if wallet_migrate:
cmd.append(f"--wallet-migrate={wallet_migrate}")
if spv:
cmd.append("--spv")
# Launch process
HSD_PROCESS = subprocess.Popen(
cmd,
cwd=os.getcwd(),
text=True
)
print(f"HSD started with PID {HSD_PROCESS.pid}")
atexit.register(hsdStop)
# Handle Ctrl+C
try:
signal.signal(signal.SIGINT, lambda s, f: (hsdStop(), sys.exit(0)))
signal.signal(signal.SIGTERM, lambda s, f: (hsdStop(), sys.exit(0)))
except:
pass
def hsdStop():
global HSD_PROCESS
if HSD_PROCESS is None:
return
print("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.")
except subprocess.TimeoutExpired:
print("HSD did not exit yet, is it alright???")
# Clean up lock file
if os.path.exists("hsd.lock"):
os.remove("hsd.lock")
HSD_PROCESS = None
def hsdRestart():
hsdStop()
time.sleep(2)
hsdStart()
checkPreRequisites()
hsdInit()
hsdStart()
# endregion

8
hsdconfig.json Normal file
View File

@@ -0,0 +1,8 @@
{
"version": "v8.0.0",
"chainMigrate":4,
"walletMigrate":7,
"minNodeVersion":20,
"minNpmVersion":8,
"spv": true
}

24
main.py
View File

@@ -1150,14 +1150,13 @@ def settings():
if not os.path.exists(".git"): if not os.path.exists(".git"):
return render_template("settings.html", account=account, return render_template("settings.html", account=account,
hsd_version=account_module.hsdVersion(False), hsd_version=account_module.hsdVersion(False),
error=error,success=success,version="Error") error=error,success=success,version="Error",internal=account_module.HSD_INTERNAL_NODE)
info = gitinfo.get_git_info() info = gitinfo.get_git_info()
if not info: if not info:
return render_template("settings.html", account=account, return render_template("settings.html", account=account,
hsd_version=account_module.hsdVersion(False), hsd_version=account_module.hsdVersion(False),
error=error,success=success,version="Error") error=error,success=success,version="Error",internal=account_module.HSD_INTERNAL_NODE)
branch = info['refs'] branch = info['refs']
if branch != "main": if branch != "main":
@@ -1172,7 +1171,7 @@ def settings():
version += ' (New version available)' version += ' (New version available)'
return render_template("settings.html", account=account, return render_template("settings.html", account=account,
hsd_version=account_module.hsdVersion(False), hsd_version=account_module.hsdVersion(False),
error=error,success=success,version=version) error=error,success=success,version=version,internal=account_module.HSD_INTERNAL_NODE)
@app.route('/settings/<action>') @app.route('/settings/<action>')
def settings_action(action): def settings_action(action):
@@ -1189,19 +1188,21 @@ def settings_action(action):
if 'error' in resp: if 'error' in resp:
return redirect("/settings?error=" + str(resp['error'])) return redirect("/settings?error=" + str(resp['error']))
return redirect("/settings?success=Rescan started") return redirect("/settings?success=Rescan started")
elif action == "resend":
if action == "resend":
resp = account_module.resendTXs() resp = account_module.resendTXs()
if 'error' in resp: if 'error' in resp:
return redirect("/settings?error=" + str(resp['error'])) return redirect("/settings?error=" + str(resp['error']))
return redirect("/settings?success=Resent transactions") return redirect("/settings?success=Resent transactions")
elif action == "zap": if action == "zap":
resp = account_module.zapTXs(request.cookies.get("account")) resp = account_module.zapTXs(request.cookies.get("account"))
if type(resp) == dict and 'error' in resp: if type(resp) == dict and 'error' in resp:
return redirect("/settings?error=" + str(resp['error'])) return redirect("/settings?error=" + str(resp['error']))
return redirect("/settings?success=Zapped transactions") return redirect("/settings?success=Zapped transactions")
elif action == "xpub":
if action == "xpub":
xpub = account_module.getxPub(request.cookies.get("account")) xpub = account_module.getxPub(request.cookies.get("account"))
content = "<br><br>" content = "<br><br>"
content += f"<textarea style='display: none;' id='data' rows='4' cols='50'>{xpub}</textarea>" content += f"<textarea style='display: none;' id='data' rows='4' cols='50'>{xpub}</textarea>"
@@ -1212,6 +1213,12 @@ def settings_action(action):
title="xPub Key", title="xPub Key",
content=f"<code>{xpub}</code>{content}") content=f"<code>{xpub}</code>{content}")
if action == "restart":
resp = account_module.hsdRestart()
return render_template("message.html", account=account,
title="Restarting",
content="The node is restarting. This may take a minute or two. You can close this window.")
return redirect("/settings?error=Invalid action") return redirect("/settings?error=Invalid action")
@app.route('/settings/upload', methods=['POST']) @app.route('/settings/upload', methods=['POST'])
@@ -1259,6 +1266,9 @@ def login():
wallets = account_module.listWallets() wallets = account_module.listWallets()
wallets = render.wallets(wallets) wallets = render.wallets(wallets)
# If there are no wallets redirect to either register or import
if len(wallets) == 0:
return redirect("/welcome")
if 'message' in request.args: if 'message' in request.args:
return render_template("login.html", return render_template("login.html",

View File

@@ -69,8 +69,7 @@
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
<h4 class="card-title">Node Settings</h4><small>HSD Version: v{{hsd_version}}</small> <h4 class="card-title">Node Settings</h4><small>HSD Version: v{{hsd_version}}</small>
<h6 class="text-muted mb-2 card-subtitle">Settings that affect all wallets</h6> <h6 class="text-muted mb-2 card-subtitle">Settings that affect all wallets</h6><ul class="list-group">
<ul class="list-group">
<li class="list-group-item"> <li class="list-group-item">
<div><a class="btn btn-primary stick-right" role="button" href="/settings/rescan">Rescan</a> <div><a class="btn btn-primary stick-right" role="button" href="/settings/rescan">Rescan</a>
<h3>Rescan</h3><span>Rescan the blockchain for transactions</span> <h3>Rescan</h3><span>Rescan the blockchain for transactions</span>
@@ -78,7 +77,7 @@
</li> </li>
<li class="list-group-item"> <li class="list-group-item">
<div><a class="btn btn-primary stick-right" role="button" href="/settings/resend">Resend</a> <div><a class="btn btn-primary stick-right" role="button" href="/settings/resend">Resend</a>
<h3>Resend&nbsp;unconfirmed transactions</h3><span>Resend any transactions that haven't been mined yet.</span> <h3>Resend unconfirmed transactions</h3><span>Resend any transactions that haven&#39;t been mined yet.</span>
</div> </div>
</li> </li>
<li class="list-group-item"> <li class="list-group-item">
@@ -86,7 +85,14 @@
<h3>Delete unconfirmed transactions</h3><span>This will only remove pending tx from the wallet older than 20 minutes (~ 2 blocks)</span> <h3>Delete unconfirmed transactions</h3><span>This will only remove pending tx from the wallet older than 20 minutes (~ 2 blocks)</span>
</div> </div>
</li> </li>
</ul> {% if internal %}
<li class="list-group-item">
<div><a class="btn btn-primary stick-right" role="button" href="/settings/restart">Restart Node</a>
<h3>Restart Internal Node</h3><span>This will attempt to restart the HSD node</span>
</div>
</li>
{% endif %}
</ul>
</div> </div>
</div> </div>
</div> </div>

47
templates/welcome.html Normal file
View File

@@ -0,0 +1,47 @@
<!DOCTYPE html>
<html data-bs-theme="dark" lang="en-au" style="height: 100%;">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<title>Welcome to FireWallet</title>
<link rel="icon" type="image/png" sizes="900x768" href="/assets/img/favicon.png">
<link rel="icon" type="image/png" sizes="900x768" href="/assets/img/favicon.png">
<link rel="icon" type="image/png" sizes="900x768" href="/assets/img/favicon.png">
<link rel="icon" type="image/png" sizes="900x768" href="/assets/img/favicon.png">
<link rel="icon" type="image/png" sizes="900x768" href="/assets/img/favicon.png">
<link rel="stylesheet" href="/assets/bootstrap/css/bootstrap.min.css">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Nunito:200,200i,300,300i,400,400i,600,600i,700,700i,800,800i,900,900i&amp;display=swap">
<link rel="stylesheet" href="/assets/css/styles.min.css">
</head>
<body class="d-flex align-items-center bg-gradient-primary" style="height: 100%;">
<div class="container">
<div class="row justify-content-center">
<div class="col-md-9 col-lg-12 col-xl-10">
<h1 class="text-center" style="color: var(--bs-danger);background: var(--bs-primary);">{{error}}</h1>
<div class="card shadow-lg my-5 o-hidden border-0" style="padding-top: 50px;padding-bottom: 50px;">
<div class="card-body p-0">
<div class="row">
<div class="col-lg-6 d-none d-lg-flex">
<div class="flex-grow-1 bg-login-image" style="background: url(&quot;/assets/img/favicon.png&quot;) center / contain no-repeat;"></div>
</div>
<div class="col-lg-6">
<div class="text-center p-5">
<div class="text-center">
<h4 class="mb-4">Welcome to FireWallet!</h4>
</div>
<div class="btn-group-vertical btn-group-lg gap-1" role="group"><a class="btn btn-primary" role="button" href="/register">Create a new wallet</a><a class="btn btn-primary" role="button" href="/import-wallet">Import an existing wallet</a></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="/assets/bootstrap/js/bootstrap.min.js"></script>
<script src="/assets/js/script.min.js"></script>
</body>
</html>