20 Commits

Author SHA1 Message Date
9a8472c5cb feat: Finalize emoji rendering
All checks were successful
Build Docker / Build Image (push) Successful in 48s
2025-05-24 15:31:29 +10:00
43efac30a8 feat: Add emoji rendering to dashboard 2025-05-24 15:20:43 +10:00
a67034de27 fix: DNS save to have work correctly
All checks were successful
Build Docker / Build Image (push) Successful in 1m35s
2025-05-21 11:56:23 +10:00
11699834cf fix: Use correct http code
All checks were successful
Build Docker / Build Image (push) Successful in 49s
2025-05-08 12:59:22 +10:00
17ad73ad8b fix: Update format for failed connection
All checks were successful
Build Docker / Build Image (push) Successful in 53s
2025-05-08 12:56:55 +10:00
8ce0e94e67 feat: Add api route for status
All checks were successful
Build Docker / Build Image (push) Successful in 47s
2025-05-08 12:52:13 +10:00
d42dae3263 fix: Add curl to container
All checks were successful
Build Docker / Build Image (push) Successful in 49s
2025-05-08 12:47:59 +10:00
fd1ba1d059 fix: Allow wallet acctions from unencrypted wallets
All checks were successful
Build Docker / Build Image (push) Successful in 1m29s
2025-05-08 12:09:04 +10:00
6bbc294116 fix: Correct link for ShakeShift
All checks were successful
Build Docker / Build Image (push) Successful in 40s
2025-03-12 20:28:54 +11:00
80e380b183 feat: Update hns.cymon.de to ShakeShift
All checks were successful
Build Docker / Build Image (push) Successful in 50s
2025-03-12 20:24:48 +11:00
7bd59a0fd6 feat: Add link to debug tools
All checks were successful
Build Docker / Build Image (push) Successful in 39s
2025-02-28 13:34:43 +11:00
56016b1f6f fix: Update domain sort to stop buggy sorting
All checks were successful
Build Docker / Build Image (push) Successful in 1m15s
2025-02-28 12:57:29 +11:00
3aff724b81 fix: Version check on settings page
All checks were successful
Build Docker / Build Image (push) Successful in 43s
2025-02-05 14:57:51 +11:00
afc227b5b4 fix: Remove some unnecessary logs
All checks were successful
Build Docker / Build Image (push) Successful in 43s
2025-02-05 14:53:33 +11:00
ab7749ef93 fix: Allow desciption to have HTML in plugin output 2025-02-05 14:39:52 +11:00
a568abeb49 fix: Don't import gunicorn unless specified by args
All checks were successful
Build Docker / Build Image (push) Successful in 45s
2025-02-05 13:51:01 +11:00
4652af3a2d feat: Add new server backend to add windows support
All checks were successful
Build Docker / Build Image (push) Successful in 1m18s
2025-02-05 13:20:09 +11:00
9aa061691d fix: Update update to make it clearer
All checks were successful
Build Docker / Build Image (push) Successful in 51s
2025-02-04 18:25:27 +11:00
ce8c773db3 fix: Allow reloading modules without a restart
All checks were successful
Build Docker / Build Image (push) Successful in 53s
2025-02-04 16:59:47 +11:00
12884fe696 feat: Default to debug mode off to stop reloads
All checks were successful
Build Docker / Build Image (push) Successful in 47s
2025-02-04 15:33:56 +11:00
21 changed files with 429 additions and 300 deletions

4
.gitignore vendored
View File

@@ -13,4 +13,6 @@ plugins/signatures.json
user_data/ user_data/
customPlugins/ customPlugins/
cache/ cache/
build/
dist/

View File

@@ -10,9 +10,9 @@ COPY . /app
# Add mount point for data volume # Add mount point for data volume
# VOLUME /data # VOLUME /data
RUN apk add git openssl RUN apk add git openssl curl
ENTRYPOINT ["python3"] ENTRYPOINT ["python3"]
CMD ["server.py"] CMD ["server.py"]
FROM builder as dev-envs FROM builder as dev-envs

Binary file not shown.

View File

@@ -122,7 +122,7 @@ HSD_IP: HSD IP address
THEME: Theme to use (dark-purple, black) THEME: Theme to use (dark-purple, black)
SHOW_EXPIRED: Show expired domains (true/false) 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://niami.io/tx/) 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)
``` ```

File diff suppressed because it is too large Load Diff

View File

@@ -2,4 +2,4 @@ HSD_API=123480615465636893475aCwyaae6s45
HSD_IP=localhost HSD_IP=localhost
THEME=black THEME=black
SHOW_EXPIRED=false SHOW_EXPIRED=false
EXPLORER_TX=https://niami.io/tx/ EXPLORER_TX=https://shakeshift.com/transaction/

88
main.py
View File

@@ -1,6 +1,7 @@
import io import io
import json import json
import random import random
import sys
from flask import Flask, make_response, redirect, request, jsonify, render_template, send_from_directory,send_file from flask import Flask, make_response, redirect, request, jsonify, render_template, send_from_directory,send_file
import os import os
import dotenv import dotenv
@@ -55,7 +56,7 @@ def index():
commit = info['commit'] commit = info['commit']
if commit != latestVersion(branch): if commit != latestVersion(branch):
print("New version available",flush=True) print("New version available",flush=True)
plugins += render_template('components/dashboard-plugin.html', name='Update', output='New version available') plugins += render_template('components/dashboard-alert.html', name='Update', output='A new version of FireWallet is available')
return render_template("index.html", account=account, plugins=plugins) return render_template("index.html", account=account, plugins=plugins)
@@ -177,7 +178,15 @@ def sendConfirmed():
address = request.args.get("address") address = request.args.get("address")
amount = float(request.args.get("amount")) amount = float(request.args.get("amount"))
response = account_module.send(request.cookies.get("account"),address,amount) response = account_module.send(request.cookies.get("account"),address,amount)
if 'error' in response: if 'error' in response and response['error'] != None:
# If error is a dict get the message
if isinstance(response['error'], dict):
if 'message' in response['error']:
return redirect("/send?message=" + response['error']['message'] + "&address=" + address + "&amount=" + str(amount))
else:
return redirect("/send?message=" + str(response['error']) + "&address=" + address + "&amount=" + str(amount))
# If error is a string
return redirect("/send?message=" + response['error'] + "&address=" + address + "&amount=" + str(amount)) return redirect("/send?message=" + response['error'] + "&address=" + address + "&amount=" + str(amount))
return redirect("/success?tx=" + response['tx']) return redirect("/success?tx=" + response['tx'])
@@ -420,12 +429,12 @@ def search():
if 'error' in domain: if 'error' in domain:
return render_template("search.html", account=account, return render_template("search.html", account=account,
rendered=renderDomain(search_term),
search_term=search_term, domain=domain['error'],plugins=plugins) search_term=search_term, domain=domain['error'],plugins=plugins)
if domain['info'] is None: if domain['info'] is None:
return render_template("search.html", account=account, return render_template("search.html", account=account,
rendered=renderDomain(search_term),
search_term=search_term,domain=search_term, search_term=search_term,domain=search_term,
state="AVAILABLE", next="Available Now",plugins=plugins) state="AVAILABLE", next="Available Now",plugins=plugins)
@@ -469,7 +478,7 @@ def search():
txs = render.txs(txs) txs = render.txs(txs)
return render_template("search.html", account=account, return render_template("search.html", account=account,
rendered=renderDomain(search_term),
search_term=search_term,domain=domain['info']['name'], search_term=search_term,domain=domain['info']['name'],
raw=domain,state=state, next=next, owner=owner, raw=domain,state=state, next=next, owner=owner,
dns=dns, txs=txs,plugins=plugins) dns=dns, txs=txs,plugins=plugins)
@@ -495,7 +504,7 @@ def manage(domain: str):
domain_info = account_module.getDomain(domain) domain_info = account_module.getDomain(domain)
if 'error' in domain_info: if 'error' in domain_info:
return render_template("manage.html", account=account, return render_template("manage.html", account=account,
rendered=renderDomain(domain),
domain=domain, error=domain_info['error']) domain=domain, error=domain_info['error'])
expiry = domain_info['info']['stats']['daysUntilExpire'] expiry = domain_info['info']['stats']['daysUntilExpire']
@@ -532,7 +541,7 @@ def manage(domain: str):
return render_template("manage.html", account=account, return render_template("manage.html", account=account,
rendered=renderDomain(domain),
error=errorMessage, address=address, error=errorMessage, address=address,
domain=domain,expiry=expiry, dns=dns, domain=domain,expiry=expiry, dns=dns,
raw_dns=urllib.parse.quote(raw_dns), raw_dns=urllib.parse.quote(raw_dns),
@@ -550,7 +559,6 @@ def finalize(domain: str):
return redirect("/logout") return redirect("/logout")
domain = domain.lower() domain = domain.lower()
print(domain)
response = account_module.finalize(request.cookies.get("account"),domain) response = account_module.finalize(request.cookies.get("account"),domain)
if response['error'] != None: if response['error'] != None:
print(response) print(response)
@@ -569,7 +577,6 @@ def cancelTransfer(domain: str):
return redirect("/logout") return redirect("/logout")
domain = domain.lower() domain = domain.lower()
print(domain)
response = account_module.cancelTransfer(request.cookies.get("account"),domain) response = account_module.cancelTransfer(request.cookies.get("account"),domain)
if 'error' in response: if 'error' in response:
if response['error'] != None: if response['error'] != None:
@@ -709,7 +716,7 @@ def editPage(domain: str):
return render_template("edit.html", account=account, return render_template("edit.html", account=account,
rendered=renderDomain(domain),
domain=domain, error=errorMessage, domain=domain, error=errorMessage,
dns=dns,raw_dns=urllib.parse.quote(raw_dns)) dns=dns,raw_dns=urllib.parse.quote(raw_dns))
@@ -855,7 +862,7 @@ def auction(domain):
if 'error' in domainInfo: if 'error' in domainInfo:
return render_template("auction.html", account=account, return render_template("auction.html", account=account,
rendered=renderDomain(search_term),
search_term=search_term, domain=domainInfo['error'], search_term=search_term, domain=domainInfo['error'],
error=error) error=error)
@@ -866,7 +873,7 @@ def auction(domain):
else: else:
next_action = f'<a href="/auction/{domain}/open">Open Auction</a>' next_action = f'<a href="/auction/{domain}/open">Open Auction</a>'
return render_template("auction.html", account=account, return render_template("auction.html", account=account,
rendered=renderDomain(search_term),
search_term=search_term,domain=search_term,next_action=next_action, search_term=search_term,domain=search_term,next_action=next_action,
state="AVAILABLE", next="Open Auction", state="AVAILABLE", next="Open Auction",
error=error) error=error)
@@ -884,7 +891,6 @@ def auction(domain):
# Get TX # Get TX
revealInfo = account_module.getRevealTX(reveal) revealInfo = account_module.getRevealTX(reveal)
reveal['bid'] = revealInfo reveal['bid'] = revealInfo
print(revealInfo)
bids = render.bids(bids,reveals) bids = render.bids(bids,reveals)
@@ -928,7 +934,7 @@ def auction(domain):
return render_template("auction.html", account=account, return render_template("auction.html", account=account,
rendered=renderDomain(search_term),
search_term=search_term,domain=domainInfo['info']['name'], search_term=search_term,domain=domainInfo['info']['name'],
raw=domainInfo,state=state, next=next, raw=domainInfo,state=state, next=next,
next_action=next_action, bids=bids,error=message) next_action=next_action, bids=bids,error=message)
@@ -946,7 +952,6 @@ def rescan_auction(domain):
domain = domain.lower() domain = domain.lower()
response = account_module.rescan_auction(account,domain) response = account_module.rescan_auction(account,domain)
print(response)
return redirect("/auction/" + domain) return redirect("/auction/" + domain)
@app.route('/auction/<domain>/bid') @app.route('/auction/<domain>/bid')
@@ -1022,7 +1027,7 @@ def bid_confirm(domain):
response = account_module.bid(request.cookies.get("account"),domain, response = account_module.bid(request.cookies.get("account"),domain,
float(bid), float(bid),
float(blind)) float(blind))
print(response)
if 'error' in response: if 'error' in response:
return redirect("/auction/" + domain + "?error=" + response['error']['message']) return redirect("/auction/" + domain + "?error=" + response['error']['message'])
@@ -1044,7 +1049,7 @@ def open_auction(domain):
if 'error' in response: if 'error' in response:
if response['error'] != None: if response['error'] != None:
return redirect("/auction/" + domain + "?error=" + response['error']['message']) return redirect("/auction/" + domain + "?error=" + response['error']['message'])
print(response)
return redirect("/success?tx=" + response['hash']) return redirect("/success?tx=" + response['hash'])
@app.route('/auction/<domain>/reveal') @app.route('/auction/<domain>/reveal')
@@ -1111,8 +1116,7 @@ def settings():
# import to time from format "2024-02-13 11:24:03" # import to time from format "2024-02-13 11:24:03"
last_commit = datetime.datetime.strptime(last_commit, "%Y-%m-%d %H:%M:%S") last_commit = datetime.datetime.strptime(last_commit, "%Y-%m-%d %H:%M:%S")
version = f'{last_commit.strftime("%y-%m-%d")} {branch}' version = f'{last_commit.strftime("%y-%m-%d")} {branch}'
if info['commit'] != latestVersion(info['refs']):
if info['commit'] != latestVersion(branch):
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),
@@ -1219,8 +1223,7 @@ def login_post():
if account.count(":") > 0: if account.count(":") > 0:
wallets = account_module.listWallets() wallets = account_module.listWallets()
wallets = render.wallets(wallets) wallets = render.wallets(wallets)
return render_template("login.html", return render_template("login.html",
error="Invalid account",wallets=wallets) error="Invalid account",wallets=wallets)
account = account + ":" + password account = account + ":" + password
@@ -1474,7 +1477,6 @@ def plugin_function(ptype,plugin,function):
return render_template("plugin-output.html", account=account,name=data['name'], return render_template("plugin-output.html", account=account,name=data['name'],
description=data['description'],output=response) description=data['description'],output=response)
else: else:
return jsonify({"error": "Function not found"}) return jsonify({"error": "Function not found"})
@@ -1542,7 +1544,12 @@ def api_wallet(function):
if function == "domains": if function == "domains":
domains = account_module.getDomains(account) domains = account_module.getDomains(account)
if 'error' in domains: if 'error' in domains:
return jsonify({"result": [], "error": domains['error']}) return jsonify({"result": [], "error": domains['error']})
# Add nameRender to each domain
for domain in domains:
domain['nameRender'] = renderDomain(domain['name'])
return jsonify({"result": domains}) return jsonify({"result": domains})
if function == "icon": if function == "icon":
@@ -1568,6 +1575,35 @@ def api_icon(account):
return send_file(f'user_data/images/{file}') return send_file(f'user_data/images/{file}')
return send_file('templates/assets/img/HNS.png') return send_file('templates/assets/img/HNS.png')
@app.route('/api/v1/status')
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"})
#endregion
#region Helper functions
def renderDomain(name: str) -> str:
"""
Render a domain name with emojis and other special characters.
"""
# Convert emoji to punycode
try:
rendered = name.encode("ascii").decode("idna")
if rendered == name:
return f"{name}/"
return f"{rendered}/ ({name})"
except Exception as e:
return f"{name}/"
#endregion #endregion
@@ -1609,4 +1645,8 @@ def page_not_found(e):
#endregion #endregion
if __name__ == '__main__': if __name__ == '__main__':
app.run(debug=True,host='0.0.0.0') # Check to see if --debug is in the command line arguments
if "--debug" in sys.argv:
app.run(debug=True,host='0.0.0.0')
else:
app.run(host='0.0.0.0')

View File

@@ -6,12 +6,19 @@ import hashlib
import subprocess import subprocess
def import_module(module_name):
if module_name in sys.modules:
return importlib.reload(sys.modules[module_name])
else:
return importlib.import_module(module_name)
def listPlugins(update=False): def listPlugins(update=False):
plugins = [] plugins = []
for file in os.listdir("plugins"): for file in os.listdir("plugins"):
if file.endswith(".py"): if file.endswith(".py"):
if file != "main.py": if file != "main.py":
plugin = importlib.import_module("plugins."+file[:-3]) plugin = import_module("plugins."+file[:-3])
if "info" not in dir(plugin): if "info" not in dir(plugin):
continue continue
details = plugin.info details = plugin.info
@@ -42,7 +49,7 @@ def listPlugins(update=False):
for file in os.listdir(f"customPlugins/{importPath}"): for file in os.listdir(f"customPlugins/{importPath}"):
if file.endswith(".py"): if file.endswith(".py"):
if file != "main.py": if file != "main.py":
plugin = importlib.import_module(f"customPlugins.{importPath}."+file[:-3]) plugin = import_module(f"customPlugins.{importPath}."+file[:-3])
if "info" not in dir(plugin): if "info" not in dir(plugin):
continue continue
details = plugin.info details = plugin.info
@@ -106,7 +113,7 @@ def hashPlugin(plugin: str):
def getPluginData(pluginStr: str): def getPluginData(pluginStr: str):
plugin = importlib.import_module(pluginStr.replace("/",".")) plugin = import_module(pluginStr.replace("/","."))
# Check if the plugin is verified # Check if the plugin is verified
signatures = [] signatures = []
@@ -141,12 +148,12 @@ def getPluginData(pluginStr: str):
def getPluginFunctions(plugin: str): def getPluginFunctions(plugin: str):
plugin = importlib.import_module(plugin.replace("/",".")) plugin = import_module(plugin.replace("/","."))
return plugin.functions return plugin.functions
def runPluginFunction(plugin: str, function: str, params: dict, authentication: str): def runPluginFunction(plugin: str, function: str, params: dict, authentication: str):
plugin_module = importlib.import_module(plugin.replace("/",".")) plugin_module = import_module(plugin.replace("/","."))
if function not in plugin_module.functions: if function not in plugin_module.functions:
return {"error": "Function not found"} return {"error": "Function not found"}
@@ -182,12 +189,12 @@ def runPluginFunction(plugin: str, function: str, params: dict, authentication:
def getPluginFunctionInputs(plugin: str, function: str): def getPluginFunctionInputs(plugin: str, function: str):
plugin = importlib.import_module(plugin.replace("/",".")) plugin = import_module(plugin.replace("/","."))
return plugin.functions[function]["params"] return plugin.functions[function]["params"]
def getPluginFunctionReturns(plugin: str, function: str): def getPluginFunctionReturns(plugin: str, function: str):
plugin = importlib.import_module(plugin.replace("/",".")) plugin = import_module(plugin.replace("/","."))
return plugin.functions[function]["returns"] return plugin.functions[function]["returns"]

View File

@@ -9,7 +9,7 @@ import os
info = { info = {
"name": "Batching Functions", "name": "Batching Functions",
"description": "This is a plugin that provides multiple functions to batch transactions", "description": "This is a plugin that provides multiple functions to batch transactions",
"version": "1.0", "version": "1.1",
"author": "Nathan.Woodburn/" "author": "Nathan.Woodburn/"
} }
# https://hsd-dev.org/api-docs/?shell--cli#sendbatch # https://hsd-dev.org/api-docs/?shell--cli#sendbatch
@@ -394,7 +394,6 @@ def bid(params, authentication):
for domain in domains: for domain in domains:
batch.append(['BID', domain, bid, blind]) batch.append(['BID', domain, bid, blind])
print(batch)
response = sendBatch(batch, authentication) response = sendBatch(batch, authentication)
if 'error' in response: if 'error' in response:
return { return {

View File

@@ -3,12 +3,13 @@ import json
import urllib.parse import urllib.parse
from flask import render_template from flask import render_template
from domainLookup import punycode_to_emoji from domainLookup import punycode_to_emoji
from main import renderDomain
import os import os
# Get Explorer URL # Get Explorer URL
TX_EXPLORER_URL = os.getenv("EXPLORER_TX") TX_EXPLORER_URL = os.getenv("EXPLORER_TX")
if TX_EXPLORER_URL is None: if TX_EXPLORER_URL is None:
TX_EXPLORER_URL = "https://niami.io/tx/" TX_EXPLORER_URL = "https://shakeshift.com/transaction/"
@@ -24,10 +25,7 @@ def domains(domains, mobile=False):
paid = paid / 1000000 paid = paid / 1000000
# Handle punycodes # Handle punycodes
name = domain['name'] name = renderDomain(domain['name'])
emoji = punycode_to_emoji(name)
if emoji != name:
name = f'{emoji} ({name})'
link = f'/manage/{domain["name"]}' link = f'/manage/{domain["name"]}'
@@ -199,7 +197,7 @@ def bidDomains(bids,domains, sortbyDomains=False):
html += "<tr>" html += "<tr>"
html += f"<td><a class='text-decoration-none' style='color: var(--bs-table-color-state, var(--bs-table-color-type, var(--bs-table-color)));' href='/auction/{domain['name']}'>{domain['name']}</a></td>" html += f"<td><a class='text-decoration-none' style='color: var(--bs-table-color-state, var(--bs-table-color-type, var(--bs-table-color)));' href='/auction/{domain['name']}'>{renderDomain(domain['name'])}</a></td>"
html += f"<td>{domain['state']}</td>" html += f"<td>{domain['state']}</td>"
html += f"<td>{bidDisplay}</td>" html += f"<td>{bidDisplay}</td>"
html += f"<td>{domain['height']:,}</td>" html += f"<td>{domain['height']:,}</td>"
@@ -215,7 +213,7 @@ def bidDomains(bids,domains, sortbyDomains=False):
bidDisplay = f'<b>{bidValue:,.2f} HNS</b> + {blind:,.2f} HNS blind' bidDisplay = f'<b>{bidValue:,.2f} HNS</b> + {blind:,.2f} HNS blind'
html += "<tr>" html += "<tr>"
html += f"<td><a class='text-decoration-none' style='color: var(--bs-table-color-state, var(--bs-table-color-type, var(--bs-table-color)));' href='/auction/{domain['name']}'>{domain['name']}</a></td>" html += f"<td><a class='text-decoration-none' style='color: var(--bs-table-color-state, var(--bs-table-color-type, var(--bs-table-color)));' href='/auction/{domain['name']}'>{renderDomain(domain['name'])}</a></td>"
html += f"<td>{domain['state']}</td>" html += f"<td>{domain['state']}</td>"
html += f"<td>{bidDisplay}</td>" html += f"<td>{bidDisplay}</td>"
html += f"<td>{domain['height']:,}</td>" html += f"<td>{domain['height']:,}</td>"

View File

@@ -8,4 +8,5 @@ cryptography
requests-doh requests-doh
Flask-QRcode Flask-QRcode
PySocks PySocks
python-git-info python-git-info
waitress

View File

@@ -1,38 +1,44 @@
from flask import Flask
from main import app
import main
from gunicorn.app.base import BaseApplication
import os import os
import sys
import platform
from main import app
from waitress import serve
class GunicornApp(BaseApplication): threads = 4
def __init__(self, app, options=None):
self.options = options or {}
self.application = app
super().__init__()
def load_config(self): def gunicornServer():
for key, value in self.options.items(): from gunicorn.app.base import BaseApplication
if key in self.cfg.settings and value is not None: class GunicornApp(BaseApplication):
self.cfg.set(key.lower(), value) def __init__(self, app, options=None):
self.options = options or {}
self.application = app
super().__init__()
def load(self): def load_config(self):
return self.application for key, value in self.options.items():
if key in self.cfg.settings and value is not None:
self.cfg.set(key.lower(), value)
if __name__ == '__main__': def load(self):
workers = 1 return self.application
threads = 2
if workers is None:
workers = 1
if threads is None:
threads = 2
workers = int(workers)
threads = int(threads)
options = { options = {
'bind': '0.0.0.0:5000', 'bind': '0.0.0.0:5000',
'workers': workers, 'workers': 2,
'threads': threads, 'threads': threads,
} }
gunicorn_app = GunicornApp(app, options) gunicorn_app = GunicornApp(app, options)
print('Starting server with ' + str(workers) + ' workers and ' + str(threads) + ' threads', flush=True) print(f'Starting server with Gunicorn on {platform.system()} with {threads} threads...', flush=True)
gunicorn_app.run() gunicorn_app.run()
if __name__ == '__main__':
# Check if --gunicorn is in the command line arguments
if "--gunicorn" in sys.argv:
gunicornServer()
sys.exit()
print(f'Starting server with Waitress on {platform.system()} with {threads} threads...', flush=True)
print(f'Press Ctrl+C to stop the server', flush=True)
print(f'Serving on http://0.0.0.0:5000/', flush=True)
serve(app, host="0.0.0.0", port=5000, threads=threads)

View File

@@ -1 +1 @@
async function request(e){try{const t=await fetch(`/api/v1/${e}`);if(!t.ok)throw new Error(`HTTP error! Status: ${t.status}`);const n=await t.json();return void 0!==n.error?`Error: ${n.error}`:n.result}catch(e){return console.error("Request failed:",e),"Error"}}function sortTable(e,t=!1){const n=document.getElementById("data-table"),a=n.querySelector("tbody"),l=Array.from(a.querySelectorAll("tr")),r=n.querySelectorAll("th");let o=n.getAttribute("data-sort-order")||"asc",d=n.getAttribute("data-sort-column")||"-1";o=t||d!=e?"asc":"asc"===o?"desc":"asc",n.setAttribute("data-sort-order",o),n.setAttribute("data-sort-column",e),l.sort(((t,n)=>{let a=t.cells[e].innerText.trim(),l=n.cells[e].innerText.trim(),r=parseFloat(a.replace(/[^0-9.,]/g,"").replace(/,/g,"")),d=parseFloat(l.replace(/[^0-9.,]/g,"").replace(/,/g,""));return isNaN(r)||isNaN(d)?"asc"===o?a.localeCompare(l):l.localeCompare(a):"asc"===o?r-d:d-r})),a.innerHTML="",l.forEach((e=>a.appendChild(e))),updateSortIndicators(r,e,o)}function updateSortIndicators(e,t,n){e.forEach(((e,a)=>{let l=e.querySelector(".sort-indicator");l.innerHTML=a===t?"asc"===n?" ▲":" ▼":""}))}window.addEventListener("load",(async()=>{const e=["hsd-sync","hsd-version","hsd-height","wallet-sync","wallet-available","wallet-total","wallet-locked","wallet-pending","wallet-domainCount","wallet-bidCount","wallet-pendingReveal","wallet-pendingRegister","wallet-pendingRedeem"],t=["wallet-available","wallet-total","wallet-locked"],n=["wallet-pendingReveal","wallet-pendingRegister","wallet-pendingRedeem"];for(const a of e){const e=document.getElementById(a);if(e){const l=a.replace(/-/g,"/");let r=await request(l);n.includes(a)&&"Error"!=r&&(r=r.length),t.includes(a)&&(r=Number(r).toFixed(2)),r=r.toString().replace(/\B(?=(\d{3})+(?!\d))/g,","),e.innerHTML=r}}})),document.addEventListener("DOMContentLoaded",(function(){fetch("/api/v1/wallet/domains").then((e=>e.json())).then((e=>{const t=document.querySelector("#data-table tbody");t&&(t.innerHTML="",e.result.forEach((e=>{const n=document.createElement("tr"),a=document.createElement("td");a.textContent=e.name,n.appendChild(a);var l="Unknown";"stats"in e&&"daysUntilExpire"in e.stats&&(l=e.stats.daysUntilExpire);const r=document.createElement("td");r.textContent=`${l} days`,n.appendChild(r);const o=document.createElement("td");o.textContent=`${(e.value/1e6).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g,",")} HNS`,n.appendChild(o);const d=document.createElement("td");d.innerHTML=e.registered?"<a href='/manage/"+e.name+"'>Manage</a>":"<a href='/auction/"+e.name+"/register'>Register</a>",n.appendChild(d),t.appendChild(n)})),sortTable(0,!0))})).catch((e=>console.error("Error fetching data:",e)))})),setInterval((async function(){const e=["hsd-sync","hsd-height","wallet-sync","wallet-pending","wallet-available","wallet-total"];for(const t of e){const e=document.getElementById(t);if(e){const n=t.replace(/-/g,"/");let a=await request(n);["wallet-available","wallet-total"].includes(t)&&(a=Number(a).toFixed(2)),a=a.toString().replace(/\B(?=(\d{3})+(?!\d))/g,","),e.innerHTML=a}}}),2e4),function(){"use strict";var e=document.querySelector(".sidebar"),t=document.querySelectorAll("#sidebarToggle, #sidebarToggleTop");if(e){e.querySelector(".collapse");var n=[].slice.call(document.querySelectorAll(".sidebar .collapse")).map((function(e){return new bootstrap.Collapse(e,{toggle:!1})}));for(var a of t)a.addEventListener("click",(function(t){if(document.body.classList.toggle("sidebar-toggled"),e.classList.toggle("toggled"),e.classList.contains("toggled"))for(var a of n)a.hide()}));window.addEventListener("resize",(function(){if(Math.max(document.documentElement.clientWidth||0,window.innerWidth||0)<768)for(var e of n)e.hide()}))}var l=document.querySelector("body.fixed-nav .sidebar");l&&l.on("mousewheel DOMMouseScroll wheel",(function(e){if(Math.max(document.documentElement.clientWidth||0,window.innerWidth||0)>768){var t=e.originalEvent,n=t.wheelDelta||-t.detail;this.scrollTop+=30*(n<0?1:-1),e.preventDefault()}}));var r=document.querySelector(".scroll-to-top");r&&window.addEventListener("scroll",(function(){var e=window.pageYOffset;r.style.display=e>100?"block":"none"}))}(); async function request(e){try{const t=await fetch(`/api/v1/${e}`);if(!t.ok)throw new Error(`HTTP error! Status: ${t.status}`);const n=await t.json();return void 0!==n.error?`Error: ${n.error}`:n.result}catch(e){return console.error("Request failed:",e),"Error"}}function sortTable(e,t=!1){const n=document.getElementById("data-table"),a=n.querySelector("tbody"),l=Array.from(a.querySelectorAll("tr")),r=n.querySelectorAll("th");let o=n.getAttribute("data-sort-order")||"asc",i=n.getAttribute("data-sort-column")||"-1";o=t||i!=e?"asc":"asc"===o?"desc":"asc",n.setAttribute("data-sort-order",o),n.setAttribute("data-sort-column",e);const c=determineColumnDataType(l,e);l.sort(((t,n)=>{let a=t.cells[e].innerText.trim(),l=n.cells[e].innerText.trim();if("number"===c){let e=parseFloat(a.replace(/[^0-9.,]/g,"").replace(/,/g,"")),t=parseFloat(l.replace(/[^0-9.,]/g,"").replace(/,/g,""));return"asc"===o?e-t:t-e}if("date"===c){let e=new Date(a),t=new Date(l);return"asc"===o?e-t:t-e}return"asc"===o?a.localeCompare(l,void 0,{sensitivity:"base"}):l.localeCompare(a,void 0,{sensitivity:"base"})})),a.innerHTML="",l.forEach((e=>a.appendChild(e))),updateSortIndicators(r,e,o)}function determineColumnDataType(e,t){const n=Math.min(5,e.length);let a=0,l=0;for(let r=0;r<n&&!(r>=e.length);r++){const n=e[r].cells[t].innerText.trim(),o=parseFloat(n.replace(/[^0-9.,]/g,"").replace(/,/g,""));if(!isNaN(o)&&n.replace(/[^0-9.,\s$%]/g,"").length===n.length){a++;continue}const i=new Date(n);isNaN(i)||"Invalid Date"===i.toString()||l++}return a>=n/2?"number":l>=n/2?"date":"text"}function updateSortIndicators(e,t,n){e.forEach(((e,a)=>{let l=e.querySelector(".sort-indicator");l.innerHTML=a===t?"asc"===n?" ▲":" ▼":""}))}window.addEventListener("load",(async()=>{const e=["hsd-sync","hsd-version","hsd-height","wallet-sync","wallet-available","wallet-total","wallet-locked","wallet-pending","wallet-domainCount","wallet-bidCount","wallet-pendingReveal","wallet-pendingRegister","wallet-pendingRedeem"],t=["wallet-available","wallet-total","wallet-locked"],n=["wallet-pendingReveal","wallet-pendingRegister","wallet-pendingRedeem"];for(const a of e){const e=document.getElementById(a);if(e){const l=a.replace(/-/g,"/");let r=await request(l);n.includes(a)&&"Error"!=r&&(r=r.length),t.includes(a)&&(r=Number(r).toFixed(2)),r=r.toString().replace(/\B(?=(\d{3})+(?!\d))/g,","),e.innerHTML=r}}})),document.addEventListener("DOMContentLoaded",(function(){fetch("/api/v1/wallet/domains").then((e=>e.json())).then((e=>{const t=document.querySelector("#data-table tbody");t&&(t.innerHTML="",e.result.forEach((e=>{const n=document.createElement("tr"),a=document.createElement("td");a.textContent=e.nameRender,n.appendChild(a);var l="Unknown";"stats"in e&&"daysUntilExpire"in e.stats&&(l=e.stats.daysUntilExpire);const r=document.createElement("td");r.textContent=`${l} days`,n.appendChild(r);const o=document.createElement("td");o.textContent=`${(e.value/1e6).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g,",")} HNS`,n.appendChild(o);const i=document.createElement("td");i.innerHTML=e.registered?"<a href='/manage/"+e.name+"'>Manage</a>":"<a href='/auction/"+e.name+"/register'>Register</a>",n.appendChild(i),t.appendChild(n)})),sortTable(0,!0))})).catch((e=>console.error("Error fetching data:",e)))})),setInterval((async function(){const e=["hsd-sync","hsd-height","wallet-sync","wallet-pending","wallet-available","wallet-total"];for(const t of e){const e=document.getElementById(t);if(e){const n=t.replace(/-/g,"/");let a=await request(n);["wallet-available","wallet-total"].includes(t)&&(a=Number(a).toFixed(2)),a=a.toString().replace(/\B(?=(\d{3})+(?!\d))/g,","),e.innerHTML=a}}}),2e4),function(){"use strict";var e=document.querySelector(".sidebar"),t=document.querySelectorAll("#sidebarToggle, #sidebarToggleTop");if(e){e.querySelector(".collapse");var n=[].slice.call(document.querySelectorAll(".sidebar .collapse")).map((function(e){return new bootstrap.Collapse(e,{toggle:!1})}));for(var a of t)a.addEventListener("click",(function(t){if(document.body.classList.toggle("sidebar-toggled"),e.classList.toggle("toggled"),e.classList.contains("toggled"))for(var a of n)a.hide()}));window.addEventListener("resize",(function(){if(Math.max(document.documentElement.clientWidth||0,window.innerWidth||0)<768)for(var e of n)e.hide()}))}var l=document.querySelector("body.fixed-nav .sidebar");l&&l.on("mousewheel DOMMouseScroll wheel",(function(e){if(Math.max(document.documentElement.clientWidth||0,window.innerWidth||0)>768){var t=e.originalEvent,n=t.wheelDelta||-t.detail;this.scrollTop+=30*(n<0?1:-1),e.preventDefault()}}));var r=document.querySelector(".scroll-to-top");r&&window.addEventListener("scroll",(function(){var e=window.pageYOffset;r.style.display=e>100?"block":"none"}))}();

View File

@@ -67,7 +67,7 @@
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
<div class="stick-right">{{next_action|safe}}</div> <div class="stick-right">{{next_action|safe}}</div>
<h4 class="card-title">{{domain}}/</h4> <h4 class="card-title">{{rendered}}</h4>
<h6 class="text-muted card-subtitle mb-2">{{next}}</h6> <h6 class="text-muted card-subtitle mb-2">{{next}}</h6>
</div> </div>
</div> </div>

View File

@@ -0,0 +1,8 @@
<div class="col-md-6 col-xl-3 mb-4">
<div class="card shadow border-start-warning py-2">
<div class="card-body">
<div class="text-uppercase fw-bold text-xs mb-1"><span style="color: var(--bs-dark);">{{name}}</span></div>
<div class="text-dark fw-bold h5 mb-0"><span>{{output | safe}}</span></div>
</div>
</div>
</div>

View File

@@ -2,4 +2,4 @@
<span style="display: block;">Check your transaction on a block explorer</span> <span style="display: block;">Check your transaction on a block explorer</span>
<a class="card-link" href="https://niami.io/tx/{{tx}}" target="_blank">Niami</a> <a class="card-link" href="https://niami.io/tx/{{tx}}" target="_blank">Niami</a>
<a class="card-link" href="https://3xpl.com/handshake/transaction/{{tx}}" target="_blank">3xpl</a> <a class="card-link" href="https://3xpl.com/handshake/transaction/{{tx}}" target="_blank">3xpl</a>
<a class="card-link" href="https://hns.cymon.de/tx/{{tx}}" target="_blank">Cymon.de</a> <a class="card-link" href="https://shakeshift.com/transaction/{{tx}}" target="_blank">ShakeShift</a>

View File

@@ -66,7 +66,7 @@
<div class="container-fluid"> <div class="container-fluid">
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
<h4 class="card-title">{{domain}}/</h4> <h4 class="card-title">{{rendered}}</h4>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -66,7 +66,7 @@
<div class="container-fluid"> <div class="container-fluid">
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
<h4 class="card-title">{{domain}}/<a class="btn btn-primary stick-right" role="button" href="/manage/{{domain}}/renew">Renew</a></h4> <h4 class="card-title">{{rendered}}<a class="btn btn-primary stick-right" role="button" href="/manage/{{domain}}/renew">Renew</a></h4>
<h6 class="text-muted card-subtitle mb-2">Expires in {{expiry}} days</h6> <h6 class="text-muted card-subtitle mb-2">Expires in {{expiry}} days</h6>
</div> </div>
</div> </div>
@@ -74,7 +74,8 @@
<div class="container-fluid" style="margin-top: 50px;"> <div class="container-fluid" style="margin-top: 50px;">
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
<h4 class="card-title" style="display: inline-block;">DNS</h4><a class="btn btn-primary" role="button" style="position: absolute; right:16px;" href="/manage/{{domain}}/edit?dns={{raw_dns}}">Edit</a><div class="table-responsive"> <h4 class="card-title" style="display: inline-block;">DNS</h4>
<div style="width: fit-content;position: absolute;right: 0px;top: 16px;"><a class="btn btn-primary" role="button" href="https://tools.c.woodburn.au/?domain={{domain}}&amp;url=https://{{domain}}" style="margin: 0px 16px;" target="_blank">Debug</a><a class="btn btn-primary" role="button" href="/manage/{{domain}}/edit?dns={{raw_dns}}" style="margin: 0px 16px;">Edit</a></div><div class="table-responsive">
<table class="table"> <table class="table">
<thead> <thead>
<tr> <tr>

View File

@@ -64,7 +64,7 @@
</nav> </nav>
<div class="container-fluid" style="margin-bottom: 20px;"> <div class="container-fluid" style="margin-bottom: 20px;">
<h3 class="text-dark mb-1">{{name}}</h3> <h3 class="text-dark mb-1">{{name}}</h3>
<h4 class="text-dark mb-1">{{description}}</h4>{{output|safe}} <h4 class="text-dark mb-1">{{description|safe}}</h4>{{output|safe}}
</div> </div>
</div> </div>
<footer class="sticky-footer" style="background: var(--bs-primary-text-emphasis);"> <footer class="sticky-footer" style="background: var(--bs-primary-text-emphasis);">

View File

@@ -65,7 +65,7 @@
<div class="container-fluid"> <div class="container-fluid">
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
<h4 class="d-none d-sm-none d-md-none d-lg-inline-block d-xl-inline-block card-title">{{domain}}/<span class="stick-right">{{next}}</span></h4> <h4 class="d-none d-sm-none d-md-none d-lg-inline-block d-xl-inline-block card-title">{{rendered}}<span class="stick-right">{{next}}</span></h4>
<h4 class="d-print-none d-sm-inline-block d-md-inline-block d-lg-none d-xl-none d-xxl-none card-title">{{domain}}/<br><br><span class="stick-right">{{next}}</span></h4> <h4 class="d-print-none d-sm-inline-block d-md-inline-block d-lg-none d-xl-none d-xxl-none card-title">{{domain}}/<br><br><span class="stick-right">{{next}}</span></h4>
<h6 class="text-muted card-subtitle mb-2"><br>{{state}}</h6> <h6 class="text-muted card-subtitle mb-2"><br>{{state}}</h6>
<h6 class="text-muted card-subtitle mb-2">Owner: {{owner}}</h6><a class="btn btn-primary" role="button" style="margin-right: 25px;" href="/manage/{{domain}}">Manage</a><a class="btn btn-primary" role="button" href="/auction/{{domain}}">Auction</a> <h6 class="text-muted card-subtitle mb-2">Owner: {{owner}}</h6><a class="btn btn-primary" role="button" style="margin-right: 25px;" href="/manage/{{domain}}">Manage</a><a class="btn btn-primary" role="button" href="/auction/{{domain}}">Auction</a>

View File

@@ -67,7 +67,7 @@
</div> </div>
<div class="card" style="max-width: 500px;margin: auto;margin-top: 50px;"> <div class="card" style="max-width: 500px;margin: auto;margin-top: 50px;">
<div class="card-body"> <div class="card-body">
<h4 class="card-title">Your transaction has been sent and will be mined soon.</h4><span style="display: block;font-size: 12px;">TX: {{tx}}</span><span style="display: block;">Check your transaction on a block explorer</span><a class="card-link" href="https://niami.io/tx/{{tx}}" target="_blank">Niami</a><a class="card-link" href="https://3xpl.com/handshake/transaction/{{tx}}" target="_blank">3xpl</a><a class="card-link" href="https://hns.cymon.de/tx/{{tx}}" target="_blank">HNS.Cymon.de</a> <h4 class="card-title">Your transaction has been sent and will be mined soon.</h4><span style="display: block;font-size: 12px;">TX: {{tx}}</span><span style="display: block;">Check your transaction on a block explorer</span><a class="card-link" href="https://niami.io/tx/{{tx}}" target="_blank">Niami</a><a class="card-link" href="https://3xpl.com/handshake/transaction/{{tx}}" target="_blank">3xpl</a><a class="card-link" href="https://shakeshift.com/transaction/{{tx}}" target="_blank">ShakeShift</a>
</div> </div>
</div> </div>
</div> </div>