diff --git a/.gitignore b/.gitignore index f73a8c4..d8ad3fb 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,5 @@ cache/ build/ dist/ hsd/ +hsd-data/ +hsd.lock diff --git a/FireWalletBrowser.bsdesign b/FireWalletBrowser.bsdesign index 3e627ef..6eb4241 100644 Binary files a/FireWalletBrowser.bsdesign and b/FireWalletBrowser.bsdesign differ diff --git a/README.md b/README.md index dbe2829..46745c4 100644 --- a/README.md +++ b/README.md @@ -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) EXPLORER_TX: URL for exploring transactions (default https://shakeshift.com/transaction/) 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) ``` diff --git a/account.py b/account.py index 0392fd6..e74b423 100644 --- a/account.py +++ b/account.py @@ -7,6 +7,11 @@ import re import domainLookup import json import time +import subprocess +import atexit +import signal +import sys + dotenv.load_dotenv() @@ -29,17 +34,33 @@ elif HSD_NETWORK == "regtest": HSD_WALLET_PORT = 14039 HSD_NODE_PORT = 14037 -INTERNAL_NODE = os.getenv("INTERNAL_HSD","false").lower() in ["1","true","yes"] -if INTERNAL_NODE: +HSD_INTERNAL_NODE = os.getenv("INTERNAL_HSD","false").lower() in ["1","true","yes"] +if HSD_INTERNAL_NODE: if HSD_API == "": # Use a random API KEY HSD_API = "firewallet-" + str(int(time.time())) - + HSD_IP = "localhost" SHOW_EXPIRED = os.getenv("SHOW_EXPIRED") if SHOW_EXPIRED is None: 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) 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): return value/1000000 - return value/1000000 def get_node_api_url(path=''): @@ -1461,3 +1481,188 @@ def get_wallet_api_url(path=''): path = f'/{path}' return f"{base_url}{path}" 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 \ No newline at end of file diff --git a/hsdconfig.json b/hsdconfig.json new file mode 100644 index 0000000..9484276 --- /dev/null +++ b/hsdconfig.json @@ -0,0 +1,8 @@ +{ +"version": "v8.0.0", +"chainMigrate":4, +"walletMigrate":7, +"minNodeVersion":20, +"minNpmVersion":8, +"spv": true +} \ No newline at end of file diff --git a/main.py b/main.py index 4b2300b..911a1ba 100644 --- a/main.py +++ b/main.py @@ -1150,14 +1150,13 @@ def settings(): if not os.path.exists(".git"): return render_template("settings.html", account=account, - 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() if not info: return render_template("settings.html", account=account, 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'] if branch != "main": @@ -1172,7 +1171,7 @@ def settings(): version += ' (New version available)' return render_template("settings.html", account=account, 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/') def settings_action(action): @@ -1189,19 +1188,21 @@ def settings_action(action): if 'error' in resp: return redirect("/settings?error=" + str(resp['error'])) return redirect("/settings?success=Rescan started") - elif action == "resend": + + if action == "resend": resp = account_module.resendTXs() if 'error' in resp: return redirect("/settings?error=" + str(resp['error'])) return redirect("/settings?success=Resent transactions") - elif action == "zap": + if action == "zap": resp = account_module.zapTXs(request.cookies.get("account")) if type(resp) == dict and 'error' in resp: return redirect("/settings?error=" + str(resp['error'])) return redirect("/settings?success=Zapped transactions") - elif action == "xpub": + + if action == "xpub": xpub = account_module.getxPub(request.cookies.get("account")) content = "

" content += f"" @@ -1212,6 +1213,12 @@ def settings_action(action): title="xPub Key", content=f"{xpub}{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") @app.route('/settings/upload', methods=['POST']) @@ -1259,6 +1266,9 @@ def login(): wallets = account_module.listWallets() 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: return render_template("login.html", diff --git a/server.py b/server.py index 396a7a7..f8e1153 100644 --- a/server.py +++ b/server.py @@ -32,7 +32,7 @@ def gunicornServer(): gunicorn_app.run() -if __name__ == '__main__': +if __name__ == '__main__': # Check if --gunicorn is in the command line arguments if "--gunicorn" in sys.argv: gunicornServer() diff --git a/templates/settings.html b/templates/settings.html index c3499bd..e7cba1d 100644 --- a/templates/settings.html +++ b/templates/settings.html @@ -69,24 +69,30 @@

Node Settings

HSD Version: v{{hsd_version}} -
Settings that affect all wallets
-
    -
  • -
    Rescan -

    Rescan

    Rescan the blockchain for transactions -
    -
  • -
  • -
    Resend -

    Resend unconfirmed transactions

    Resend any transactions that haven't been mined yet. -
    -
  • -
  • -
    Zap -

    Delete unconfirmed transactions

    This will only remove pending tx from the wallet older than 20 minutes (~ 2 blocks) -
    -
  • -
+
Settings that affect all wallets
    +
  • +
    Rescan +

    Rescan

    Rescan the blockchain for transactions +
    +
  • +
  • +
    Resend +

    Resend unconfirmed transactions

    Resend any transactions that haven't been mined yet. +
    +
  • +
  • +
    Zap +

    Delete unconfirmed transactions

    This will only remove pending tx from the wallet older than 20 minutes (~ 2 blocks) +
    +
  • + {% if internal %} +
  • +
    Restart Node +

    Restart Internal Node

    This will attempt to restart the HSD node +
    +
  • + {% endif %} +
diff --git a/templates/welcome.html b/templates/welcome.html new file mode 100644 index 0000000..44b8944 --- /dev/null +++ b/templates/welcome.html @@ -0,0 +1,47 @@ + + + + + + + Welcome to FireWallet + + + + + + + + + + + +
+
+
+

{{error}}

+
+
+
+
+ +
+
+
+
+

Welcome to FireWallet!

+
+ +
+
+
+
+
+
+
+
+ + + + + \ No newline at end of file