generated from nathanwoodburn/python-webserver-template
Compare commits
9 Commits
feat/gemin
...
f0d233b2f6
| Author | SHA1 | Date | |
|---|---|---|---|
|
f0d233b2f6
|
|||
|
9a6748b156
|
|||
|
90de6042b1
|
|||
|
eea558361c
|
|||
|
b6662f400a
|
|||
|
1c51e97354
|
|||
|
206b323be6
|
|||
|
400897319f
|
|||
|
a36e467bd4
|
2
.gitignore
vendored
2
.gitignore
vendored
@@ -4,3 +4,5 @@ __pycache__/
|
|||||||
.env
|
.env
|
||||||
.vs/
|
.vs/
|
||||||
.venv/
|
.venv/
|
||||||
|
fireexplorer.db
|
||||||
|
.vscode
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ RUN --mount=type=cache,target=/root/.cache/uv \
|
|||||||
|
|
||||||
# Add mount point for data volume
|
# Add mount point for data volume
|
||||||
ENV BASE_DIR=/data
|
ENV BASE_DIR=/data
|
||||||
|
ENV DATABASE_PATH=/data/fireexplorer.db
|
||||||
VOLUME /data
|
VOLUME /data
|
||||||
|
|
||||||
EXPOSE 5000
|
EXPOSE 5000
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "python-webserver-template"
|
name = "fireexplorer"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
description = "Add your description here"
|
description = "A hot new Handshake Blockchain Explorer"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.13"
|
requires-python = ">=3.13"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ cryptography==46.0.3 \
|
|||||||
--hash=sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849 \
|
--hash=sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849 \
|
||||||
--hash=sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963 \
|
--hash=sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963 \
|
||||||
--hash=sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018
|
--hash=sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018
|
||||||
# via python-webserver-template
|
# via fireexplorer
|
||||||
distlib==0.4.0 \
|
distlib==0.4.0 \
|
||||||
--hash=sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16 \
|
--hash=sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16 \
|
||||||
--hash=sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d
|
--hash=sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d
|
||||||
@@ -163,11 +163,11 @@ filelock==3.20.0 \
|
|||||||
flask==3.1.2 \
|
flask==3.1.2 \
|
||||||
--hash=sha256:bf656c15c80190ed628ad08cdfd3aaa35beb087855e2f494910aa3774cc4fd87 \
|
--hash=sha256:bf656c15c80190ed628ad08cdfd3aaa35beb087855e2f494910aa3774cc4fd87 \
|
||||||
--hash=sha256:ca1d8112ec8a6158cc29ea4858963350011b5c846a414cdb7a954aa9e967d03c
|
--hash=sha256:ca1d8112ec8a6158cc29ea4858963350011b5c846a414cdb7a954aa9e967d03c
|
||||||
# via python-webserver-template
|
# via fireexplorer
|
||||||
gunicorn==23.0.0 \
|
gunicorn==23.0.0 \
|
||||||
--hash=sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d \
|
--hash=sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d \
|
||||||
--hash=sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec
|
--hash=sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec
|
||||||
# via python-webserver-template
|
# via fireexplorer
|
||||||
h11==0.16.0 \
|
h11==0.16.0 \
|
||||||
--hash=sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1 \
|
--hash=sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1 \
|
||||||
--hash=sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86
|
--hash=sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86
|
||||||
@@ -289,7 +289,7 @@ pysocks==1.7.1 \
|
|||||||
python-dotenv==1.2.1 \
|
python-dotenv==1.2.1 \
|
||||||
--hash=sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6 \
|
--hash=sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6 \
|
||||||
--hash=sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61
|
--hash=sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61
|
||||||
# via python-webserver-template
|
# via fireexplorer
|
||||||
pyyaml==6.0.3 \
|
pyyaml==6.0.3 \
|
||||||
--hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \
|
--hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \
|
||||||
--hash=sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3 \
|
--hash=sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3 \
|
||||||
@@ -328,7 +328,7 @@ requests==2.32.3 \
|
|||||||
requests-doh==1.0.0 \
|
requests-doh==1.0.0 \
|
||||||
--hash=sha256:6ce8bc96245030a198ef20d2100b4dcb3b120a05a58df703f8be121a79f8f2fb \
|
--hash=sha256:6ce8bc96245030a198ef20d2100b4dcb3b120a05a58df703f8be121a79f8f2fb \
|
||||||
--hash=sha256:eea6583b792b7d3dfde74fd28eedc2b95d6ea896368119eede31f0d6ff2c838c
|
--hash=sha256:eea6583b792b7d3dfde74fd28eedc2b95d6ea896368119eede31f0d6ff2c838c
|
||||||
# via python-webserver-template
|
# via fireexplorer
|
||||||
ruff==0.14.5 \
|
ruff==0.14.5 \
|
||||||
--hash=sha256:2d1fa985a42b1f075a098fa1ab9d472b712bdb17ad87a8ec86e45e7fa6273e68 \
|
--hash=sha256:2d1fa985a42b1f075a098fa1ab9d472b712bdb17ad87a8ec86e45e7fa6273e68 \
|
||||||
--hash=sha256:3676cb02b9061fee7294661071c4709fa21419ea9176087cb77e64410926eb78 \
|
--hash=sha256:3676cb02b9061fee7294661071c4709fa21419ea9176087cb77e64410926eb78 \
|
||||||
|
|||||||
457
server.py
457
server.py
@@ -5,10 +5,15 @@ from flask import (
|
|||||||
send_from_directory,
|
send_from_directory,
|
||||||
send_file,
|
send_file,
|
||||||
jsonify,
|
jsonify,
|
||||||
|
g,
|
||||||
|
request,
|
||||||
)
|
)
|
||||||
import os
|
import os
|
||||||
import requests
|
import requests
|
||||||
|
import sqlite3
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from urllib.parse import urlencode
|
||||||
|
from xml.sax.saxutils import escape
|
||||||
import dotenv
|
import dotenv
|
||||||
from tools import hip2, wallet_txt
|
from tools import hip2, wallet_txt
|
||||||
|
|
||||||
@@ -16,6 +21,42 @@ dotenv.load_dotenv()
|
|||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
DATABASE = os.getenv("DATABASE_PATH", "fireexplorer.db")
|
||||||
|
HSD_API_BASE = os.getenv("HSD_API_BASE", "https://hsd.hns.au/api/v1")
|
||||||
|
PUBLIC_BASE_URL = os.getenv("PUBLIC_BASE_URL", "").rstrip("/")
|
||||||
|
|
||||||
|
|
||||||
|
def get_db():
|
||||||
|
db = getattr(g, "_database", None)
|
||||||
|
if db is None:
|
||||||
|
db = g._database = sqlite3.connect(DATABASE)
|
||||||
|
db.row_factory = sqlite3.Row
|
||||||
|
return db
|
||||||
|
|
||||||
|
|
||||||
|
@app.teardown_appcontext
|
||||||
|
def close_connection(exception):
|
||||||
|
db = getattr(g, "_database", None)
|
||||||
|
if db is not None:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
|
def init_db():
|
||||||
|
with app.app_context():
|
||||||
|
db = get_db()
|
||||||
|
db.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS names (
|
||||||
|
namehash TEXT PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
|
||||||
|
init_db()
|
||||||
|
|
||||||
|
|
||||||
def find(name, path):
|
def find(name, path):
|
||||||
for root, dirs, files in os.walk(path):
|
for root, dirs, files in os.walk(path):
|
||||||
@@ -23,6 +64,175 @@ def find(name, path):
|
|||||||
return os.path.join(root, name)
|
return os.path.join(root, name)
|
||||||
|
|
||||||
|
|
||||||
|
def _truncate(text: str, max_length: int) -> str:
|
||||||
|
if len(text) <= max_length:
|
||||||
|
return text
|
||||||
|
return text[: max_length - 1] + "…"
|
||||||
|
|
||||||
|
|
||||||
|
def _safe_hns_value(value) -> str:
|
||||||
|
try:
|
||||||
|
return f"{float(value) / 1e6:,.2f} HNS"
|
||||||
|
except Exception:
|
||||||
|
return "Unknown"
|
||||||
|
|
||||||
|
|
||||||
|
def _format_int(value, fallback: str = "?") -> str:
|
||||||
|
try:
|
||||||
|
return f"{int(value):,}"
|
||||||
|
except Exception:
|
||||||
|
return fallback
|
||||||
|
|
||||||
|
|
||||||
|
def _ellipsize_middle(text: str, max_length: int = 48) -> str:
|
||||||
|
if len(text) <= max_length:
|
||||||
|
return text
|
||||||
|
if max_length < 7:
|
||||||
|
return _truncate(text, max_length)
|
||||||
|
left = (max_length - 1) // 2
|
||||||
|
right = max_length - left - 1
|
||||||
|
return f"{text[:left]}…{text[-right:]}"
|
||||||
|
|
||||||
|
|
||||||
|
def _fetch_explorer_json(endpoint: str):
|
||||||
|
try:
|
||||||
|
req = requests.get(f"{HSD_API_BASE}/{endpoint}", timeout=4)
|
||||||
|
if req.status_code == 200:
|
||||||
|
return req.json(), None
|
||||||
|
return None, f"HTTP {req.status_code}"
|
||||||
|
except Exception as e:
|
||||||
|
return None, str(e)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_public_base_url() -> str:
|
||||||
|
if PUBLIC_BASE_URL:
|
||||||
|
return PUBLIC_BASE_URL
|
||||||
|
return request.url_root.rstrip("/")
|
||||||
|
|
||||||
|
|
||||||
|
def _build_og_context(
|
||||||
|
search_type: str | None = None, search_value: str | None = None
|
||||||
|
) -> dict:
|
||||||
|
default_title = "Fire Explorer"
|
||||||
|
default_description = "A hot new Handshake Blockchain Explorer"
|
||||||
|
|
||||||
|
if not search_type or not search_value:
|
||||||
|
return {
|
||||||
|
"title": default_title,
|
||||||
|
"description": default_description,
|
||||||
|
"image_query": {"title": default_title, "subtitle": default_description},
|
||||||
|
}
|
||||||
|
|
||||||
|
type_labels = {
|
||||||
|
"block": "Block",
|
||||||
|
"header": "Header",
|
||||||
|
"tx": "Transaction",
|
||||||
|
"address": "Address",
|
||||||
|
"name": "Name",
|
||||||
|
}
|
||||||
|
label = type_labels.get(search_type, "Search")
|
||||||
|
|
||||||
|
fallback_title = f"{label} {search_value} | Fire Explorer"
|
||||||
|
fallback_description = f"View Handshake {label.lower()} details for {search_value}"
|
||||||
|
|
||||||
|
og = {
|
||||||
|
"title": _truncate(fallback_title, 100),
|
||||||
|
"description": _truncate(fallback_description, 200),
|
||||||
|
"image_query": {
|
||||||
|
"type": label,
|
||||||
|
"value": search_value,
|
||||||
|
"title": fallback_title,
|
||||||
|
"subtitle": fallback_description,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if search_type == "block":
|
||||||
|
block, err = _fetch_explorer_json(f"block/{search_value}")
|
||||||
|
if not err and block:
|
||||||
|
tx_count = len(block.get("txs", []))
|
||||||
|
height = block.get("height", "?")
|
||||||
|
timestamp = block.get("time")
|
||||||
|
time_str = (
|
||||||
|
datetime.utcfromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M UTC")
|
||||||
|
if timestamp
|
||||||
|
else "Unknown time"
|
||||||
|
)
|
||||||
|
height_fmt = _format_int(height)
|
||||||
|
tx_count_fmt = _format_int(tx_count, "0")
|
||||||
|
subtitle = f"Height {height_fmt} • {tx_count_fmt} txs • {time_str}"
|
||||||
|
og["title"] = _truncate(f"Block {height_fmt} | Fire Explorer", 100)
|
||||||
|
og["description"] = _truncate(subtitle, 200)
|
||||||
|
og["image_query"]["subtitle"] = subtitle
|
||||||
|
|
||||||
|
elif search_type == "header":
|
||||||
|
header, err = _fetch_explorer_json(f"header/{search_value}")
|
||||||
|
if not err and header:
|
||||||
|
height = header.get("height", "?")
|
||||||
|
bits = header.get("bits", "?")
|
||||||
|
subtitle = f"Height {height} • Bits {bits}"
|
||||||
|
og["title"] = _truncate(f"Header {height} | Fire Explorer", 100)
|
||||||
|
og["description"] = _truncate(subtitle, 200)
|
||||||
|
og["image_query"]["subtitle"] = subtitle
|
||||||
|
|
||||||
|
elif search_type == "tx":
|
||||||
|
tx, err = _fetch_explorer_json(f"tx/{search_value}")
|
||||||
|
if not err and tx:
|
||||||
|
inputs = len(tx.get("inputs", []))
|
||||||
|
outputs = len(tx.get("outputs", []))
|
||||||
|
fee = _safe_hns_value(tx.get("fee"))
|
||||||
|
subtitle = f"{inputs} inputs • {outputs} outputs • Fee {fee}"
|
||||||
|
og["title"] = _truncate("Transaction | Fire Explorer", 100)
|
||||||
|
og["description"] = _truncate(subtitle, 200)
|
||||||
|
og["image_query"]["subtitle"] = subtitle
|
||||||
|
|
||||||
|
elif search_type == "address":
|
||||||
|
txs, err = _fetch_explorer_json(f"tx/address/{search_value}")
|
||||||
|
if not err and isinstance(txs, list):
|
||||||
|
subtitle = f"{len(txs)} related transactions found"
|
||||||
|
og["title"] = _truncate("Address | Fire Explorer", 100)
|
||||||
|
og["description"] = _truncate(subtitle, 200)
|
||||||
|
og["image_query"]["subtitle"] = subtitle
|
||||||
|
|
||||||
|
elif search_type == "name":
|
||||||
|
name, err = _fetch_explorer_json(f"name/{search_value}")
|
||||||
|
if not err and name:
|
||||||
|
info = name.get("info", name)
|
||||||
|
state = info.get("state", "Unknown")
|
||||||
|
owner = info.get("owner", {}).get("hash", "Unknown")
|
||||||
|
subtitle = f"State: {state} • Owner: {_truncate(owner, 26)}"
|
||||||
|
og["title"] = _truncate(f"Name {search_value} | Fire Explorer", 100)
|
||||||
|
og["description"] = _truncate(subtitle, 200)
|
||||||
|
og["image_query"]["subtitle"] = subtitle
|
||||||
|
|
||||||
|
og["image_query"]["title"] = og["title"]
|
||||||
|
return og
|
||||||
|
|
||||||
|
|
||||||
|
def _render_index(search_type: str | None = None, search_value: str | None = None):
|
||||||
|
current_datetime = datetime.now().strftime("%d %b %Y %I:%M %p")
|
||||||
|
og_context = _build_og_context(search_type, search_value)
|
||||||
|
image_query = og_context["image_query"]
|
||||||
|
public_base_url = _get_public_base_url()
|
||||||
|
og_image_url = public_base_url + "/og-image?" + urlencode(image_query)
|
||||||
|
og_url = public_base_url + request.full_path
|
||||||
|
if og_url.endswith("?"):
|
||||||
|
og_url = og_url[:-1]
|
||||||
|
|
||||||
|
twitter_domain = request.host
|
||||||
|
if PUBLIC_BASE_URL and "//" in PUBLIC_BASE_URL:
|
||||||
|
twitter_domain = PUBLIC_BASE_URL.split("//", 1)[1]
|
||||||
|
|
||||||
|
return render_template(
|
||||||
|
"index.html",
|
||||||
|
datetime=current_datetime,
|
||||||
|
meta_title=og_context["title"],
|
||||||
|
meta_description=og_context["description"],
|
||||||
|
og_url=og_url,
|
||||||
|
og_image=og_image_url,
|
||||||
|
twitter_domain=twitter_domain,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# Assets routes
|
# Assets routes
|
||||||
@app.route("/assets/<path:path>")
|
@app.route("/assets/<path:path>")
|
||||||
def send_assets(path):
|
def send_assets(path):
|
||||||
@@ -71,44 +281,125 @@ def wellknown(path):
|
|||||||
# region Main routes
|
# region Main routes
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
def index():
|
def index():
|
||||||
current_datetime = datetime.now().strftime("%d %b %Y %I:%M %p")
|
return _render_index()
|
||||||
return render_template("index.html", datetime=current_datetime)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/tx/<path:tx_hash>")
|
@app.route("/tx/<path:tx_hash>")
|
||||||
def tx_route(tx_hash):
|
def tx_route(tx_hash):
|
||||||
current_datetime = datetime.now().strftime("%d %b %Y %I:%M %p")
|
return _render_index("tx", tx_hash)
|
||||||
return render_template("index.html", datetime=current_datetime)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/block/<path:block_id>")
|
@app.route("/block/<path:block_id>")
|
||||||
def block_route(block_id):
|
def block_route(block_id):
|
||||||
current_datetime = datetime.now().strftime("%d %b %Y %I:%M %p")
|
return _render_index("block", block_id)
|
||||||
return render_template("index.html", datetime=current_datetime)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/header/<path:block_id>")
|
@app.route("/header/<path:block_id>")
|
||||||
def header_route(block_id):
|
def header_route(block_id):
|
||||||
current_datetime = datetime.now().strftime("%d %b %Y %I:%M %p")
|
return _render_index("header", block_id)
|
||||||
return render_template("index.html", datetime=current_datetime)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/address/<path:address>")
|
@app.route("/address/<path:address>")
|
||||||
def address_route(address):
|
def address_route(address):
|
||||||
current_datetime = datetime.now().strftime("%d %b %Y %I:%M %p")
|
return _render_index("address", address)
|
||||||
return render_template("index.html", datetime=current_datetime)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/name/<path:name>")
|
@app.route("/name/<path:name>")
|
||||||
def name_route(name):
|
def name_route(name):
|
||||||
current_datetime = datetime.now().strftime("%d %b %Y %I:%M %p")
|
return _render_index("name", name)
|
||||||
return render_template("index.html", datetime=current_datetime)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/coin/<path:coin_hash>/<int:index>")
|
@app.route("/coin/<path:coin_hash>/<int:index>")
|
||||||
def coin_route(coin_hash, index):
|
def coin_route(coin_hash, index):
|
||||||
current_datetime = datetime.now().strftime("%d %b %Y %I:%M %p")
|
return _render_index("coin", f"{coin_hash}:{index}")
|
||||||
return render_template("index.html", datetime=current_datetime)
|
|
||||||
|
|
||||||
|
@app.route("/og-image")
|
||||||
|
def og_image():
|
||||||
|
title = _truncate(request.args.get("title", "Fire Explorer"), 90)
|
||||||
|
subtitle = _truncate(
|
||||||
|
request.args.get("subtitle", "A hot new Handshake Blockchain Explorer"),
|
||||||
|
140,
|
||||||
|
)
|
||||||
|
search_type = _truncate(request.args.get("type", "Explorer"), 24)
|
||||||
|
search_value = request.args.get("value", "")
|
||||||
|
|
||||||
|
normalized_type = search_type.strip().lower()
|
||||||
|
value_max_length = 44
|
||||||
|
if normalized_type in {"transaction", "tx"}:
|
||||||
|
value_max_length = 30
|
||||||
|
|
||||||
|
display_value = _ellipsize_middle(search_value, value_max_length)
|
||||||
|
value_font_size = "30"
|
||||||
|
if len(display_value) > 34:
|
||||||
|
value_font_size = "26"
|
||||||
|
is_default_card = not search_value.strip() and normalized_type in {
|
||||||
|
"explorer",
|
||||||
|
"search",
|
||||||
|
"",
|
||||||
|
}
|
||||||
|
|
||||||
|
type_text = escape(search_type)
|
||||||
|
value_text = escape(display_value)
|
||||||
|
title_text = escape(title)
|
||||||
|
subtitle_text = escape(subtitle)
|
||||||
|
|
||||||
|
if is_default_card:
|
||||||
|
svg = f"""<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"1200\" height=\"630\" viewBox=\"0 0 1200 630\">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id=\"bg\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\">
|
||||||
|
<stop offset=\"0%\" stop-color=\"#0b1220\" />
|
||||||
|
<stop offset=\"100%\" stop-color=\"#1f2937\" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id=\"accent\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"0\">
|
||||||
|
<stop offset=\"0%\" stop-color=\"#ef4444\" />
|
||||||
|
<stop offset=\"100%\" stop-color=\"#f97316\" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<rect width=\"1200\" height=\"630\" fill=\"url(#bg)\" />
|
||||||
|
<circle cx=\"1100\" cy=\"80\" r=\"180\" fill=\"#ef4444\" opacity=\"0.16\" />
|
||||||
|
<circle cx=\"120\" cy=\"610\" r=\"240\" fill=\"#f97316\" opacity=\"0.14\" />
|
||||||
|
<rect x=\"86\" y=\"74\" width=\"1028\" height=\"482\" rx=\"28\" fill=\"#0f172a\" stroke=\"#374151\" />
|
||||||
|
<rect x=\"130\" y=\"126\" width=\"176\" height=\"42\" rx=\"21\" fill=\"url(#accent)\" />
|
||||||
|
<text x=\"218\" y=\"153\" text-anchor=\"middle\" fill=\"#ffffff\" font-size=\"19\" font-family=\"Inter, Arial, sans-serif\" font-weight=\"700\">Handshake</text>
|
||||||
|
<text x=\"130\" y=\"246\" fill=\"#ffffff\" font-size=\"82\" font-family=\"Inter, Arial, sans-serif\" font-weight=\"800\">{title_text}</text>
|
||||||
|
<text x=\"130\" y=\"302\" fill=\"#d1d5db\" font-size=\"32\" font-family=\"Inter, Arial, sans-serif\">{subtitle_text}</text>
|
||||||
|
<line x1=\"130\" y1=\"338\" x2=\"1070\" y2=\"338\" stroke=\"#374151\" />
|
||||||
|
<text x=\"130\" y=\"402\" fill=\"#f9fafb\" font-size=\"34\" font-family=\"Inter, Arial, sans-serif\" font-weight=\"600\">Blocks • Transactions • Addresses • Names</text>
|
||||||
|
<text x=\"130\" y=\"452\" fill=\"#9ca3af\" font-size=\"28\" font-family=\"Inter, Arial, sans-serif\">Real-time status, searchable history, and rich on-chain data.</text>
|
||||||
|
<text x=\"86\" y=\"608\" fill=\"#9ca3af\" font-size=\"24\" font-family=\"Inter, Arial, sans-serif\">explorer.hns.au</text>
|
||||||
|
</svg>"""
|
||||||
|
else:
|
||||||
|
svg = f"""<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"1200\" height=\"630\" viewBox=\"0 0 1200 630\">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id=\"bg\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\">
|
||||||
|
<stop offset=\"0%\" stop-color=\"#111827\" />
|
||||||
|
<stop offset=\"100%\" stop-color=\"#1f2937\" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id=\"accent\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"0\">
|
||||||
|
<stop offset=\"0%\" stop-color=\"#ef4444\" />
|
||||||
|
<stop offset=\"100%\" stop-color=\"#f97316\" />
|
||||||
|
</linearGradient>
|
||||||
|
<clipPath id=\"valueClip\">
|
||||||
|
<rect x=\"96\" y=\"496\" width=\"1008\" height=\"52\" rx=\"4\" />
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
<rect width=\"1200\" height=\"630\" fill=\"url(#bg)\" />
|
||||||
|
<circle cx=\"1040\" cy=\"120\" r=\"180\" fill=\"#ef4444\" opacity=\"0.14\" />
|
||||||
|
<circle cx=\"160\" cy=\"580\" r=\"220\" fill=\"#f97316\" opacity=\"0.12\" />
|
||||||
|
<rect x=\"72\" y=\"64\" width=\"240\" height=\"44\" rx=\"22\" fill=\"url(#accent)\" />
|
||||||
|
<text x=\"192\" y=\"92\" text-anchor=\"middle\" fill=\"#ffffff\" font-size=\"20\" font-family=\"Inter, Arial, sans-serif\" font-weight=\"700\">{type_text}</text>
|
||||||
|
<text x=\"72\" y=\"188\" fill=\"#ffffff\" font-size=\"62\" font-family=\"Inter, Arial, sans-serif\" font-weight=\"700\">{title_text}</text>
|
||||||
|
<text x=\"72\" y=\"252\" fill=\"#d1d5db\" font-size=\"33\" font-family=\"Inter, Arial, sans-serif\">{subtitle_text}</text>
|
||||||
|
<rect x=\"72\" y=\"472\" width=\"1056\" height=\"96\" rx=\"16\" fill=\"#0b1220\" stroke=\"#374151\" />
|
||||||
|
<text x=\"96\" y=\"530\" clip-path=\"url(#valueClip)\" fill=\"#f9fafb\" font-size=\"{value_font_size}\" font-family=\"'JetBrains Mono', 'Consolas', monospace\">{value_text}</text>
|
||||||
|
<text x=\"72\" y=\"610\" fill=\"#9ca3af\" font-size=\"24\" font-family=\"Inter, Arial, sans-serif\">explorer.hns.au</text>
|
||||||
|
</svg>"""
|
||||||
|
|
||||||
|
response = make_response(svg)
|
||||||
|
response.headers["Content-Type"] = "image/svg+xml; charset=utf-8"
|
||||||
|
response.headers["Cache-Control"] = "public, max-age=300"
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
@app.route("/<path:path>")
|
@app.route("/<path:path>")
|
||||||
@@ -133,13 +424,46 @@ def catch_all(path: str):
|
|||||||
return render_template("404.html"), 404
|
return render_template("404.html"), 404
|
||||||
|
|
||||||
|
|
||||||
|
# endregion
|
||||||
|
|
||||||
|
|
||||||
|
# region API routes
|
||||||
|
@app.route("/api/v1/namehash/<namehash>")
|
||||||
|
def namehash_api(namehash):
|
||||||
|
db = get_db()
|
||||||
|
cur = db.execute("SELECT * FROM names WHERE namehash = ?", (namehash,))
|
||||||
|
row = cur.fetchone()
|
||||||
|
if row is None:
|
||||||
|
# Get namehash from hsd.hns.au
|
||||||
|
req = requests.get(f"https://hsd.hns.au/api/v1/namehash/{namehash}")
|
||||||
|
if req.status_code == 200:
|
||||||
|
name = req.json().get("result")
|
||||||
|
if not name:
|
||||||
|
return jsonify({"name": "Error", "namehash": namehash})
|
||||||
|
# Insert into database
|
||||||
|
db.execute(
|
||||||
|
"INSERT OR REPLACE INTO names (namehash, name) VALUES (?, ?)",
|
||||||
|
(namehash, name),
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
return jsonify({"name": name, "namehash": namehash})
|
||||||
|
return jsonify(dict(row))
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/v1/status")
|
@app.route("/api/v1/status")
|
||||||
def api_status():
|
def api_status():
|
||||||
|
# Count number of names in database
|
||||||
|
db = get_db()
|
||||||
|
cur = db.execute("SELECT COUNT(*) as count FROM names")
|
||||||
|
row = cur.fetchone()
|
||||||
|
name_count = row["count"] if row else 0
|
||||||
|
|
||||||
return jsonify(
|
return jsonify(
|
||||||
{
|
{
|
||||||
"status": "ok",
|
"status": "ok",
|
||||||
"service": "FireExplorer",
|
"service": "FireExplorer",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
|
"names_cached": name_count,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -153,6 +477,7 @@ def hip02(domain: str):
|
|||||||
"success": True,
|
"success": True,
|
||||||
"address": hip2_record,
|
"address": hip2_record,
|
||||||
"method": "hip02",
|
"method": "hip02",
|
||||||
|
"name": domain,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -163,16 +488,120 @@ def hip02(domain: str):
|
|||||||
"success": True,
|
"success": True,
|
||||||
"address": wallet_record,
|
"address": wallet_record,
|
||||||
"method": "wallet_txt",
|
"method": "wallet_txt",
|
||||||
|
"name": domain,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return jsonify(
|
return jsonify(
|
||||||
{
|
{
|
||||||
"success": False,
|
"success": False,
|
||||||
|
"name": domain,
|
||||||
"error": "No HIP02 or WALLET record found for this domain",
|
"error": "No HIP02 or WALLET record found for this domain",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/v1/covenant", methods=["POST"])
|
||||||
|
def covenant_api():
|
||||||
|
data = request.get_json()
|
||||||
|
|
||||||
|
if isinstance(data, list):
|
||||||
|
covenants = data
|
||||||
|
results = []
|
||||||
|
|
||||||
|
# Collect all namehashes needed
|
||||||
|
namehashes = set()
|
||||||
|
for cov in covenants:
|
||||||
|
items = cov.get("items", [])
|
||||||
|
if items:
|
||||||
|
namehashes.add(items[0])
|
||||||
|
|
||||||
|
# Batch DB lookup
|
||||||
|
db = get_db()
|
||||||
|
known_names = {}
|
||||||
|
if namehashes:
|
||||||
|
placeholders = ",".join("?" for _ in namehashes)
|
||||||
|
cur = db.execute(
|
||||||
|
f"SELECT namehash, name FROM names WHERE namehash IN ({placeholders})",
|
||||||
|
list(namehashes),
|
||||||
|
)
|
||||||
|
for row in cur:
|
||||||
|
known_names[row["namehash"]] = row["name"]
|
||||||
|
|
||||||
|
# Identify missing namehashes
|
||||||
|
missing_hashes = [nh for nh in namehashes if nh not in known_names]
|
||||||
|
|
||||||
|
# Fetch missing from HSD
|
||||||
|
session = requests.Session()
|
||||||
|
for nh in missing_hashes:
|
||||||
|
try:
|
||||||
|
req = session.get(f"https://hsd.hns.au/api/v1/namehash/{nh}")
|
||||||
|
if req.status_code == 200:
|
||||||
|
name = req.json().get("result")
|
||||||
|
if name:
|
||||||
|
known_names[nh] = name
|
||||||
|
# Update DB
|
||||||
|
db.execute(
|
||||||
|
"INSERT OR REPLACE INTO names (namehash, name) VALUES (?, ?)",
|
||||||
|
(nh, name),
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error fetching namehash {nh}: {e}")
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
# Build results
|
||||||
|
for cov in covenants:
|
||||||
|
action = cov.get("action")
|
||||||
|
items = cov.get("items", [])
|
||||||
|
|
||||||
|
if not action:
|
||||||
|
results.append({"covenant": cov, "display": "Unknown"})
|
||||||
|
continue
|
||||||
|
|
||||||
|
display = f"{action}"
|
||||||
|
if items:
|
||||||
|
nh = items[0]
|
||||||
|
if nh in known_names:
|
||||||
|
name = known_names[nh]
|
||||||
|
display += f' <a href="/name/{name}">{name}</a>'
|
||||||
|
|
||||||
|
results.append({"covenant": cov, "display": display})
|
||||||
|
|
||||||
|
return jsonify(results)
|
||||||
|
|
||||||
|
# Get the covenant data
|
||||||
|
action = data.get("action")
|
||||||
|
items = data.get("items", [])
|
||||||
|
|
||||||
|
if not action:
|
||||||
|
return jsonify({"success": False, "data": data})
|
||||||
|
|
||||||
|
display = f"{action}"
|
||||||
|
if len(items) > 0:
|
||||||
|
name_hash = items[0]
|
||||||
|
# Lookup name from database
|
||||||
|
db = get_db()
|
||||||
|
cur = db.execute("SELECT * FROM names WHERE namehash = ?", (name_hash,))
|
||||||
|
row = cur.fetchone()
|
||||||
|
if row:
|
||||||
|
name = row["name"]
|
||||||
|
display += f' <a href="/name/{name}">{name}</a>'
|
||||||
|
else:
|
||||||
|
req = requests.get(f"https://hsd.hns.au/api/v1/namehash/{name_hash}")
|
||||||
|
if req.status_code == 200:
|
||||||
|
name = req.json().get("result")
|
||||||
|
if name:
|
||||||
|
display += f" {name}"
|
||||||
|
# Insert into database
|
||||||
|
db.execute(
|
||||||
|
"INSERT OR REPLACE INTO names (namehash, name) VALUES (?, ?)",
|
||||||
|
(name_hash, name),
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
return jsonify({"success": True, "data": data, "display": display})
|
||||||
|
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -109,6 +109,7 @@ section {
|
|||||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||||
animation: slideUp 0.6s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
animation: slideUp 0.6s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
||||||
opacity: 0; /* Start hidden for animation */
|
opacity: 0; /* Start hidden for animation */
|
||||||
|
min-width: 0; /* Prevent grid overflow */
|
||||||
}
|
}
|
||||||
|
|
||||||
.card:hover {
|
.card:hover {
|
||||||
@@ -159,7 +160,7 @@ section {
|
|||||||
/* Info Grid */
|
/* Info Grid */
|
||||||
.info-grid {
|
.info-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,6 +169,7 @@ section {
|
|||||||
background: rgba(15, 23, 42, 0.4);
|
background: rgba(15, 23, 42, 0.4);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
border: 1px solid var(--card-border);
|
border: 1px solid var(--card-border);
|
||||||
|
min-width: 0; /* Prevent grid overflow */
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-item.no-border {
|
.info-item.no-border {
|
||||||
@@ -265,6 +267,7 @@ section {
|
|||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
animation: staggerFade 0.4s ease forwards;
|
animation: staggerFade 0.4s ease forwards;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
min-width: 0; /* Prevent flex overflow */
|
||||||
}
|
}
|
||||||
|
|
||||||
.tx-item:hover {
|
.tx-item:hover {
|
||||||
@@ -282,6 +285,7 @@ section {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
min-width: 0; /* Prevent flex overflow */
|
||||||
}
|
}
|
||||||
|
|
||||||
.tx-view-btn {
|
.tx-view-btn {
|
||||||
@@ -388,6 +392,7 @@ section {
|
|||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Transaction Details */
|
/* Transaction Details */
|
||||||
@@ -418,6 +423,7 @@ section {
|
|||||||
border: 1px solid var(--card-border);
|
border: 1px solid var(--card-border);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
|
min-width: 0; /* Prevent overflow */
|
||||||
}
|
}
|
||||||
|
|
||||||
.tx-io-header {
|
.tx-io-header {
|
||||||
@@ -611,6 +617,7 @@ section {
|
|||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
|
|
||||||
.result-box .error {
|
.result-box .error {
|
||||||
@@ -622,6 +629,16 @@ section {
|
|||||||
border: 1px solid rgba(239, 68, 68, 0.2);
|
border: 1px solid rgba(239, 68, 68, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.success-message {
|
||||||
|
color: var(--success-color);
|
||||||
|
background: rgba(16, 185, 129, 0.1);
|
||||||
|
border: 1px solid rgba(16, 185, 129, 0.2);
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
/* Scrollbar */
|
/* Scrollbar */
|
||||||
.result-box::-webkit-scrollbar {
|
.result-box::-webkit-scrollbar {
|
||||||
width: 8px;
|
width: 8px;
|
||||||
@@ -700,6 +717,26 @@ a:hover {
|
|||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.info-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.card {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
padding: 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tx-io-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Loading Animation */
|
/* Loading Animation */
|
||||||
@@ -799,3 +836,7 @@ a:hover {
|
|||||||
.tx-item {
|
.tx-item {
|
||||||
animation: staggerFade 0.4s ease forwards;
|
animation: staggerFade 0.4s ease forwards;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
small {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 224 KiB After Width: | Height: | Size: 34 KiB |
@@ -4,30 +4,30 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Fire Explorer</title>
|
<title>{{ meta_title or 'Fire Explorer' }}</title>
|
||||||
<link rel="icon" href="/assets/img/favicon.png" type="image/png">
|
<link rel="icon" href="/assets/img/favicon.png" type="image/png">
|
||||||
<link rel="stylesheet" href="/assets/css/index.css">
|
<link rel="stylesheet" href="/assets/css/index.css">
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||||
<meta name="description" content="A hot new Handshake Blockchain Explorer">
|
<meta name="description" content="{{ meta_description or 'A hot new Handshake Blockchain Explorer' }}">
|
||||||
|
|
||||||
<!-- Open Graph Meta Tags -->
|
<!-- Open Graph Meta Tags -->
|
||||||
<meta property="og:url" content="https://explorer.hns.au/">
|
<meta property="og:url" content="{{ og_url or 'https://explorer.hns.au/' }}">
|
||||||
<meta property="og:type" content="website">
|
<meta property="og:type" content="website">
|
||||||
<meta property="og:title" content="Fire Explorer">
|
<meta property="og:title" content="{{ meta_title or 'Fire Explorer' }}">
|
||||||
<meta property="og:description" content="A hot new Handshake Blockchain Explorer">
|
<meta property="og:description" content="{{ meta_description or 'A hot new Handshake Blockchain Explorer' }}">
|
||||||
<meta property="og:image" content="https://explorer.hns.au/assets/img/og.png">
|
<meta property="og:image" content="{{ og_image or 'https://explorer.hns.au/assets/img/og.png' }}">
|
||||||
<meta property="og:image:width" content="1200">
|
<meta property="og:image:width" content="1200">
|
||||||
<meta property="og:image:height" content="630">
|
<meta property="og:image:height" content="630">
|
||||||
|
|
||||||
<!-- Twitter Meta Tags -->
|
<!-- Twitter Meta Tags -->
|
||||||
<meta name="twitter:card" content="summary_large_image">
|
<meta name="twitter:card" content="summary_large_image">
|
||||||
<meta property="twitter:domain" content="explorer.hns.au">
|
<meta property="twitter:domain" content="{{ twitter_domain or 'explorer.hns.au' }}">
|
||||||
<meta property="twitter:url" content="https://explorer.hns.au/">
|
<meta property="twitter:url" content="{{ og_url or 'https://explorer.hns.au/' }}">
|
||||||
<meta name="twitter:title" content="Fire Explorer">
|
<meta name="twitter:title" content="{{ meta_title or 'Fire Explorer' }}">
|
||||||
<meta name="twitter:description" content="A hot new Handshake Blockchain Explorer">
|
<meta name="twitter:description" content="{{ meta_description or 'A hot new Handshake Blockchain Explorer' }}">
|
||||||
<meta name="twitter:image" content="https://explorer.hns.au/assets/img/og.png">
|
<meta name="twitter:image" content="{{ og_image or 'https://explorer.hns.au/assets/img/og.png' }}">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
@@ -119,6 +119,7 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<p>Fire Explorer - Handshake Blockchain Explorer | Powered by <a href="https://hns.au" target="_blank">HNSAU</a> & <a href="https://hsd.hns.au" target="_blank">Fire HSD</a></p>
|
<p>Fire Explorer - Handshake Blockchain Explorer | Powered by <a href="https://hns.au" target="_blank">HNSAU</a> & <a href="https://hsd.hns.au" target="_blank">Fire HSD</a></p>
|
||||||
<p class="timestamp">Last updated: {{ datetime }}</p>
|
<p class="timestamp">Last updated: {{ datetime }}</p>
|
||||||
|
<small>Note: Date and times are in UTC</small>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
@@ -469,7 +470,7 @@
|
|||||||
<div style="display: flex; justify-content: space-between; margin-top: 0.5rem; font-size: 0.85rem; color: #b0b0b0;">
|
<div style="display: flex; justify-content: space-between; margin-top: 0.5rem; font-size: 0.85rem; color: #b0b0b0;">
|
||||||
<span>Height: ${coin.height.toLocaleString()}</span>
|
<span>Height: ${coin.height.toLocaleString()}</span>
|
||||||
<span>Coinbase: ${coin.coinbase ? 'Yes' : 'No'}</span>
|
<span>Coinbase: ${coin.coinbase ? 'Yes' : 'No'}</span>
|
||||||
<span>Covenant: ${coin.covenant.action}</span>
|
<span>Covenant: <span data-covenant-action="${coin.covenant.action}" data-covenant="${encodeURIComponent(JSON.stringify(coin.covenant))}">${coin.covenant.action}</span></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`).join('')}
|
`).join('')}
|
||||||
@@ -521,7 +522,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="info-item">
|
<div class="info-item">
|
||||||
<label>Covenant:</label>
|
<label>Covenant:</label>
|
||||||
<span>${coin.covenant.action}</span>
|
<span data-covenant-action="${coin.covenant.action}" data-covenant="${encodeURIComponent(JSON.stringify(coin.covenant))}">${coin.covenant.action}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -696,7 +697,7 @@
|
|||||||
recordDetails += `<br><span style="font-size: 0.9rem;">KeyTag: ${record.keyTag}, Algorithm: ${record.algorithm}, DigestType: ${record.digestType}</span><br><span class="mono" style="font-size: 0.85rem; color: #b0b0b0; word-break: break-all;">${record.digest}</span>`;
|
recordDetails += `<br><span style="font-size: 0.9rem;">KeyTag: ${record.keyTag}, Algorithm: ${record.algorithm}, DigestType: ${record.digestType}</span><br><span class="mono" style="font-size: 0.85rem; color: #b0b0b0; word-break: break-all;">${record.digest}</span>`;
|
||||||
break;
|
break;
|
||||||
case 'TXT':
|
case 'TXT':
|
||||||
recordDetails += `<br><span style="color: #b0b0b0;">${record.txt.map(t => `"${t}"`).join('<br>')}</span>`;
|
recordDetails += `<br><span style="color: #b0b0b0;">${record.txt.join('<br>')}</span>`;
|
||||||
break;
|
break;
|
||||||
case 'GLUE4':
|
case 'GLUE4':
|
||||||
case 'GLUE6':
|
case 'GLUE6':
|
||||||
@@ -779,7 +780,7 @@
|
|||||||
recordDetails += `<br><span style="font-size: 0.9rem;">KeyTag: ${record.keyTag}, Algorithm: ${record.algorithm}</span>`;
|
recordDetails += `<br><span style="font-size: 0.9rem;">KeyTag: ${record.keyTag}, Algorithm: ${record.algorithm}</span>`;
|
||||||
break;
|
break;
|
||||||
case 'TXT':
|
case 'TXT':
|
||||||
recordDetails += `<br><span style="color: #b0b0b0;">${record.txt.map(t => `"${t}"`).join('<br>')}</span>`;
|
recordDetails += `<br><span style="color: #b0b0b0;">${record.txt.join('<br>')}</span>`;
|
||||||
break;
|
break;
|
||||||
case 'GLUE4':
|
case 'GLUE4':
|
||||||
case 'GLUE6':
|
case 'GLUE6':
|
||||||
@@ -861,7 +862,7 @@
|
|||||||
<span class="tx-io-value">${input.coin ? formatValue(input.coin.value) : 'Unknown'}</span>
|
<span class="tx-io-value">${input.coin ? formatValue(input.coin.value) : 'Unknown'}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="tx-io-address">${input.coin ? input.coin.address : (input.address || 'Unknown')}</div>
|
<div class="tx-io-address">${input.coin ? input.coin.address : (input.address || 'Unknown')}</div>
|
||||||
${input.coin && input.coin.covenant.action !== 'NONE' ? `<div class="tx-covenant">Covenant: ${input.coin.covenant.action}</div>` : ''}
|
${input.coin && input.coin.covenant.action !== 'NONE' ? `<div class="tx-covenant" data-covenant-action="${input.coin.covenant.action}" data-covenant="${encodeURIComponent(JSON.stringify(input.coin.covenant))}">Covenant: ${input.coin.covenant.action}</div>` : ''}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -879,7 +880,7 @@
|
|||||||
<span class="tx-io-value">${formatValue(output.value)}</span>
|
<span class="tx-io-value">${formatValue(output.value)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="tx-io-address">${output.address}</div>
|
<div class="tx-io-address">${output.address}</div>
|
||||||
${output.covenant.action !== 'NONE' ? `<div class="tx-covenant">Covenant: ${output.covenant.action}</div>` : ''}
|
${output.covenant.action !== 'NONE' ? `<div class="tx-covenant" data-covenant-action="${output.covenant.action}" data-covenant="${encodeURIComponent(JSON.stringify(output.covenant))}">Covenant: ${output.covenant.action}</div>` : ''}
|
||||||
</div>
|
</div>
|
||||||
`).join('')}
|
`).join('')}
|
||||||
</div>
|
</div>
|
||||||
@@ -919,6 +920,7 @@
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
document.body.appendChild(modal);
|
document.body.appendChild(modal);
|
||||||
|
if (!data.error) updateCovenants();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Display helper
|
// Display helper
|
||||||
@@ -940,6 +942,74 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update covenant information from API
|
||||||
|
async function updateCovenants() {
|
||||||
|
const elements = document.querySelectorAll('[data-covenant-action]');
|
||||||
|
const covenantsToFetch = [];
|
||||||
|
const elementMap = new Map(); // Map JSON string -> Array of elements
|
||||||
|
|
||||||
|
for (const el of elements) {
|
||||||
|
const action = el.dataset.covenantAction;
|
||||||
|
if (action === 'NONE') continue;
|
||||||
|
|
||||||
|
// Skip if already updated
|
||||||
|
if (el.dataset.covenantUpdated) continue;
|
||||||
|
|
||||||
|
// Get full covenant data
|
||||||
|
let covenantData = null;
|
||||||
|
if (el.dataset.covenant) {
|
||||||
|
try {
|
||||||
|
covenantData = JSON.parse(decodeURIComponent(el.dataset.covenant));
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to parse covenant data:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!covenantData) continue;
|
||||||
|
|
||||||
|
const key = JSON.stringify(covenantData);
|
||||||
|
if (!elementMap.has(key)) {
|
||||||
|
elementMap.set(key, []);
|
||||||
|
covenantsToFetch.push(covenantData);
|
||||||
|
}
|
||||||
|
elementMap.get(key).push(el);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (covenantsToFetch.length === 0) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/v1/covenant`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(covenantsToFetch)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
const results = await res.json();
|
||||||
|
|
||||||
|
for (const result of results) {
|
||||||
|
const key = JSON.stringify(result.covenant);
|
||||||
|
const els = elementMap.get(key);
|
||||||
|
|
||||||
|
if (els) {
|
||||||
|
for (const el of els) {
|
||||||
|
if (el.classList.contains('tx-covenant')) {
|
||||||
|
el.innerHTML = `Covenant: ${result.display}`;
|
||||||
|
} else {
|
||||||
|
el.innerHTML = result.display;
|
||||||
|
}
|
||||||
|
el.dataset.covenantUpdated = "true";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to fetch covenant info:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Show loading animation
|
// Show loading animation
|
||||||
function showLoading(elementId) {
|
function showLoading(elementId) {
|
||||||
const element = document.getElementById(elementId);
|
const element = document.getElementById(elementId);
|
||||||
@@ -1031,6 +1101,7 @@
|
|||||||
resultElement.innerHTML = `<div class="error">Error: ${data.error}</div>`;
|
resultElement.innerHTML = `<div class="error">Error: ${data.error}</div>`;
|
||||||
} else {
|
} else {
|
||||||
resultElement.innerHTML = formatTransactionData(data);
|
resultElement.innerHTML = formatTransactionData(data);
|
||||||
|
updateCovenants();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1055,7 +1126,7 @@
|
|||||||
const note = document.createElement('div');
|
const note = document.createElement('div');
|
||||||
note.className = 'success-message';
|
note.className = 'success-message';
|
||||||
note.style.marginBottom = '1rem';
|
note.style.marginBottom = '1rem';
|
||||||
note.innerHTML = `Resolved alias <strong>${hip02Result.name || address}</strong> to address`;
|
note.innerHTML = `Resolved <strong>${hip02Result.name || address}</strong> to address <br><span class="mono">${address}</span>`;
|
||||||
resultElement.parentNode.insertBefore(note, resultElement);
|
resultElement.parentNode.insertBefore(note, resultElement);
|
||||||
setTimeout(() => note.remove(), 5000);
|
setTimeout(() => note.remove(), 5000);
|
||||||
} else {
|
} else {
|
||||||
@@ -1106,6 +1177,7 @@
|
|||||||
resultElement.innerHTML = `<div class="error">Error: ${data.error}</div>`;
|
resultElement.innerHTML = `<div class="error">Error: ${data.error}</div>`;
|
||||||
} else {
|
} else {
|
||||||
resultElement.innerHTML = formatAddressCoins(data);
|
resultElement.innerHTML = formatAddressCoins(data);
|
||||||
|
updateCovenants();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1178,17 +1250,23 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
showLoading('name-result');
|
showLoading('name-result');
|
||||||
const data = await apiCall(`namehash/${nameHash}`);
|
|
||||||
|
|
||||||
// Check if result is valid and redirect to name page
|
try {
|
||||||
const resultElement = document.getElementById('name-result');
|
const response = await fetch(`/api/v1/namehash/${nameHash}`);
|
||||||
if (data.error) {
|
const data = await response.json();
|
||||||
resultElement.innerHTML = `<div class="error">Error: ${data.error.message ? data.error.message : "Failed to lookup hash"}</div>`;
|
|
||||||
} else if (data.result && typeof data.result === 'string') {
|
const resultElement = document.getElementById('name-result');
|
||||||
// Valid name found, redirect to name page
|
if (data.error) {
|
||||||
window.location.href = `/name/${data.result}`;
|
resultElement.innerHTML = `<div class="error">Error: ${data.error}</div>`;
|
||||||
} else {
|
} else if (data.name) {
|
||||||
resultElement.innerHTML = `<div class="error">No name found for this hash</div>`;
|
// Valid name found, redirect to name page
|
||||||
|
window.location.href = `/name/${data.name}`;
|
||||||
|
} else {
|
||||||
|
resultElement.innerHTML = `<div class="error">No name found for this hash</div>`;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
const resultElement = document.getElementById('name-result');
|
||||||
|
resultElement.innerHTML = `<div class="error">Error: ${e.message}</div>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
66
uv.lock
generated
66
uv.lock
generated
@@ -239,6 +239,39 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl", hash = "sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2", size = 16054, upload-time = "2025-10-08T18:03:48.35Z" },
|
{ url = "https://files.pythonhosted.org/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl", hash = "sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2", size = 16054, upload-time = "2025-10-08T18:03:48.35Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fireexplorer"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = { virtual = "." }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "cryptography" },
|
||||||
|
{ name = "flask" },
|
||||||
|
{ name = "gunicorn" },
|
||||||
|
{ name = "python-dotenv" },
|
||||||
|
{ name = "requests-doh" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dev-dependencies]
|
||||||
|
dev = [
|
||||||
|
{ name = "pre-commit" },
|
||||||
|
{ name = "ruff" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.metadata]
|
||||||
|
requires-dist = [
|
||||||
|
{ name = "cryptography", specifier = ">=46.0.3" },
|
||||||
|
{ name = "flask", specifier = ">=3.1.2" },
|
||||||
|
{ name = "gunicorn", specifier = ">=23.0.0" },
|
||||||
|
{ name = "python-dotenv", specifier = ">=1.2.1" },
|
||||||
|
{ name = "requests-doh", specifier = ">=1.0.0" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.metadata.requires-dev]
|
||||||
|
dev = [
|
||||||
|
{ name = "pre-commit", specifier = ">=4.4.0" },
|
||||||
|
{ name = "ruff", specifier = ">=0.14.5" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "flask"
|
name = "flask"
|
||||||
version = "3.1.2"
|
version = "3.1.2"
|
||||||
@@ -497,39 +530,6 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" },
|
{ url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "python-webserver-template"
|
|
||||||
version = "0.1.0"
|
|
||||||
source = { virtual = "." }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "cryptography" },
|
|
||||||
{ name = "flask" },
|
|
||||||
{ name = "gunicorn" },
|
|
||||||
{ name = "python-dotenv" },
|
|
||||||
{ name = "requests-doh" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.dev-dependencies]
|
|
||||||
dev = [
|
|
||||||
{ name = "pre-commit" },
|
|
||||||
{ name = "ruff" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.metadata]
|
|
||||||
requires-dist = [
|
|
||||||
{ name = "cryptography", specifier = ">=46.0.3" },
|
|
||||||
{ name = "flask", specifier = ">=3.1.2" },
|
|
||||||
{ name = "gunicorn", specifier = ">=23.0.0" },
|
|
||||||
{ name = "python-dotenv", specifier = ">=1.2.1" },
|
|
||||||
{ name = "requests-doh", specifier = ">=1.0.0" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.metadata.requires-dev]
|
|
||||||
dev = [
|
|
||||||
{ name = "pre-commit", specifier = ">=4.4.0" },
|
|
||||||
{ name = "ruff", specifier = ">=0.14.5" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyyaml"
|
name = "pyyaml"
|
||||||
version = "6.0.3"
|
version = "6.0.3"
|
||||||
|
|||||||
Reference in New Issue
Block a user