feat: Add initial front design
All checks were successful
Build Docker / BuildImage (push) Successful in 42s

This commit is contained in:
Nathan Woodburn 2025-02-06 22:50:56 +11:00
parent 5712c07b4d
commit 194b9c7d2e
Signed by: nathanwoodburn
GPG Key ID: 203B000478AD0EF1
34 changed files with 3595 additions and 112 deletions

BIN
FireExplorer.bsdesign Normal file

Binary file not shown.

74
db.py Normal file
View File

@ -0,0 +1,74 @@
import mysql.connector
import os
from dotenv import load_dotenv
load_dotenv()
import indexerClasses
DB_HOST = os.getenv("DB_HOST")
DB_USER = os.getenv("DB_USER")
DB_PASS = os.getenv("DB_PASSWORD")
DB_NAME = os.getenv("DB_NAME")
class DBConnection:
def __init__(self):
self.conn = connect()
def __enter__(self):
return self.conn
def __exit__(self, exc_type, exc_value, traceback):
self.conn.close()
def getBlock(self, blockheight) -> indexerClasses.Block | None:
with self.conn.cursor() as cursor:
try:
blockheight = int(blockheight)
cursor.execute("SELECT * FROM blocks WHERE height = %s", (str(blockheight),))
result = cursor.fetchone()
if result:
return indexerClasses.Block(result)
except Exception as e:
print(f"Error: {e}")
return None
def getBlockHash(self, blockhash) -> indexerClasses.Block | None:
with self.conn.cursor() as cursor:
cursor.execute("SELECT * FROM blocks WHERE hash = %s", (blockhash,))
result = cursor.fetchone()
if result:
return indexerClasses.Block(result)
else:
return None
def getTransaction(self, txid) -> indexerClasses.Transaction | None:
with self.conn.cursor() as cursor:
cursor.execute("SELECT * FROM transactions WHERE hash = %s", (txid,))
result = cursor.fetchone()
if result:
return indexerClasses.Transaction(result)
else:
return None
def getNameByHash(self, hash) -> str | None:
with self.conn.cursor() as cursor:
cursor.execute("SELECT name FROM names WHERE namehash = %s", (hash,))
result = cursor.fetchone()
if result:
return result[0]
else:
return None
def connect():
return mysql.connector.connect(
host=DB_HOST,
user=DB_USER,
password=DB_PASS,
database=DB_NAME,
charset='utf8mb4',
collation='utf8mb4_unicode_ci',
)

6
hsd.py
View File

@ -22,9 +22,6 @@ HSD_URL = f'http://x:{HSD_API_KEY}@{HSD_IP}:{HSD_PORT}'
if os.getenv("HSD_URL"): if os.getenv("HSD_URL"):
HSD_URL = os.getenv("HSD_URL") HSD_URL = os.getenv("HSD_URL")
print(f"Using HSD_URL: {HSD_URL}")
def get_tx(txid): def get_tx(txid):
return requests.get(f"{HSD_URL}/tx/{txid}").json() return requests.get(f"{HSD_URL}/tx/{txid}").json()
@ -48,3 +45,6 @@ def get_mempool():
def get_mempool_info(): def get_mempool_info():
return requests.post(HSD_URL, json={"method": "getmempoolinfo"}).json() return requests.post(HSD_URL, json={"method": "getmempoolinfo"}).json()
def get_name_from_hash(hash):
return requests.post(HSD_URL, json={"method": "getnamebyhash", "params": [hash]}).json()

270
indexerClasses.py Normal file
View File

@ -0,0 +1,270 @@
import json
import asyncio
import requests
class Block:
def __init__(self, data):
if isinstance(data, dict):
self.hash = data["hash"]
self.height = data["height"]
self.depth = data["depth"]
self.version = data["version"]
self.prevBlock = data["prevBlock"]
self.merkleRoot = data["merkleRoot"]
self.witnessRoot = data["witnessRoot"]
self.treeRoot = data["treeRoot"]
self.reservedRoot = data["reservedRoot"]
self.time = data["time"]
self.bits = data["bits"]
self.nonce = data["nonce"]
self.extraNonce = data["extraNonce"]
self.mask = data["mask"]
self.txs = data["txs"]
elif isinstance(data, list) or isinstance(data, tuple):
self.hash = data[0]
self.height = data[1]
self.depth = data[2]
self.version = data[3]
self.prevBlock = data[4]
self.merkleRoot = data[5]
self.witnessRoot = data[6]
self.treeRoot = data[7]
self.reservedRoot = data[8]
self.time = data[9]
self.bits = data[10]
self.nonce = data[11]
self.extraNonce = data[12]
self.mask = data[13]
self.txs = json.loads(data[14])
else:
raise ValueError("Invalid data type")
def __str__(self):
return f"Block {self.height}"
def toJSON(self) -> dict:
return {
"hash": self.hash,
"height": self.height,
"depth": self.depth,
"version": self.version,
"prevBlock": self.prevBlock,
"merkleRoot": self.merkleRoot,
"witnessRoot": self.witnessRoot,
"treeRoot": self.treeRoot,
"reservedRoot": self.reservedRoot,
"time": self.time,
"bits": self.bits,
"nonce": self.nonce,
"extraNonce": self.extraNonce,
"mask": self.mask,
"txs": self.txs
}
class Transaction:
def __init__(self, data):
if isinstance(data, dict):
self.hash = data["hash"]
self.witnessHash = data["witnessHash"]
self.fee = data["fee"]
self.rate = data["rate"]
self.mtime = data["mtime"]
self.block = data["block"]
self.index = data["index"]
self.version = data["version"]
self.inputs = data["inputs"]
self.outputs = data["outputs"]
self.locktime = data["locktime"]
self.hex = data["hex"]
elif isinstance(data, list) or isinstance(data, tuple):
self.hash = data[0]
self.witnessHash = data[1]
self.fee = data[2]
self.rate = data[3]
self.mtime = data[4]
self.block = data[5]
self.index = data[6]
self.version = data[7]
# Load inputs with Input class
self.inputs = []
for input in json.loads(data[8]):
self.inputs.append(Input(input))
self.outputs = []
for output in json.loads(data[9]):
self.outputs.append(Output(output))
self.locktime = data[10]
self.hex = data[11]
else:
raise ValueError("Invalid data type")
def __str__(self):
return f"Transaction {self.hash}"
def toJSON(self) -> dict:
return {
"hash": self.hash,
"witnessHash": self.witnessHash,
"fee": self.fee,
"rate": self.rate,
"mtime": self.mtime,
"block": self.block,
"index": self.index,
"version": self.version,
"inputs": [input.toJSON() for input in self.inputs],
"outputs": [output.toJSON() for output in self.outputs],
"locktime": self.locktime,
"hex": self.hex
}
class Input:
def __init__(self, data):
if isinstance(data, dict):
self.prevout = data["prevout"]
self.witness = data["witness"]
self.sequence = data["sequence"]
self.address = None
self.coin = None
if "address" in data:
self.address = data["address"]
if "coin" in data:
self.coin = Coin(data["coin"])
else:
raise ValueError("Invalid data type")
def __str__(self):
return f"Input {self.prevout['hash']} {self.coin}"
def toJSON(self) -> dict:
return {
"prevout": self.prevout,
"witness": self.witness,
"sequence": self.sequence,
"address": self.address,
"coin": self.coin.toJSON() if self.coin else None
}
class Output:
def __init__(self, data):
if isinstance(data, dict):
self.value = data["value"]
self.address = data["address"]
self.covenant = Covenant(data["covenant"])
else:
raise ValueError("Invalid data type")
def __str__(self):
return f"Output {self.value} {self.address} {self.covenant}"
def toJSON(self) -> dict:
return {
"value": self.value,
"address": self.address,
"covenant": self.covenant.toJSON()
}
def hex_to_ascii(hex_string):
# Convert the hex string to bytes
bytes_obj = bytes.fromhex(hex_string)
# Decode the bytes object to an ASCII string
ascii_string = bytes_obj.decode('ascii')
return ascii_string
class Covenant:
def __init__(self, data):
if isinstance(data, dict):
self.type = data["type"]
self.action = data["action"]
self.items = data["items"]
self.nameHash = None
self.height = None
self.name = None
self.flags = None
self.hash = None
self.nonce = None
self.recordData = None
self.blockHash = None
self.version = None
self.Address = None
self.claimHeight = None
self.renewalCount = None
if self.type > 0: # All but NONE
self.nameHash = self.items[0]
self.height = self.items[1]
if self.type == 1: # CLAIM
self.flags = self.items[3]
if self.type in [1,2,3]: # CLAIM, OPEN, BID
self.name = hex_to_ascii(self.items[2])
if self.type == 3: # BID
self.hash = self.items[3]
if self.type == 4: # REVEAL
self.nonce = self.items[2]
if self.type in [6,7]: # REGISTER, UPDATE
self.recordData = self.items[2]
if self.type == 6: # REGISTER
self.blockHash = self.items[3]
if self.type == 8: # RENEW
self.blockHash = self.items[2]
if self.type == 9: # TRANSFER
self.version = self.items[2]
self.Address = self.items[3]
if self.type == 10: # FINALIZE
self.name = hex_to_ascii(self.items[2])
self.flags = self.items[3]
self.claimHeight= self.items[4]
self.renewalCount = self.items[5]
self.blockHash = self.items[6]
else:
raise ValueError("Invalid data type")
def __str__(self):
return self.toString()
def toString(self):
return self.action
def toJSON(self) -> dict:
return {
"type": self.type,
"action": self.action,
"items": self.items
}
class Coin:
def __init__(self, data):
if isinstance(data, dict):
self.version = data["version"]
self.height = data["height"]
self.value = data["value"]
self.address = data["address"]
self.covenant = Covenant(data["covenant"])
self.coinbase = data["coinbase"]
else:
raise ValueError("Invalid data type")
def __str__(self):
return f"Coin {self.value} {self.address} {self.covenant}"
def toJSON(self) -> dict:
return {
"version": self.version,
"height": self.height,
"value": self.value,
"address": self.address,
"covenant": self.covenant.toJSON(),
"coinbase": self.coinbase
}

View File

@ -2,3 +2,4 @@ flask
gunicorn gunicorn
requests requests
python-dotenv python-dotenv
mysql-connector-python

230
server.py
View File

@ -1,3 +1,4 @@
from decimal import Decimal
from functools import cache from functools import cache
import json import json
from flask import ( from flask import (
@ -16,77 +17,54 @@ import requests
from datetime import datetime from datetime import datetime
import dotenv import dotenv
import hsd import hsd
import indexerClasses
import db
from datetime import datetime, timezone
from indexerClasses import Block, Transaction, Covenant
dotenv.load_dotenv() dotenv.load_dotenv()
app = Flask(__name__) app = Flask(__name__)
dbCon = db.DBConnection()
def find(name, path): def find(name, path):
for root, dirs, files in os.walk(path): for root, dirs, files in os.walk(path):
if name in files: if name in files:
return os.path.join(root, name) return os.path.join(root, name)
# Assets routes
@app.route("/assets/<path:path>")
def send_assets(path):
if path.endswith(".json"):
return send_from_directory(
"templates/assets", path, mimetype="application/json"
)
if os.path.isfile("templates/assets/" + path):
return send_from_directory("templates/assets", path)
# Try looking in one of the directories
filename: str = path.split("/")[-1]
if (
filename.endswith(".png")
or filename.endswith(".jpg")
or filename.endswith(".jpeg")
or filename.endswith(".svg")
):
if os.path.isfile("templates/assets/img/" + filename):
return send_from_directory("templates/assets/img", filename)
if os.path.isfile("templates/assets/img/favicon/" + filename):
return send_from_directory("templates/assets/img/favicon", filename)
return render_template("404.html"), 404
# region Special routes
@app.route("/favicon.png")
def faviconPNG():
return send_from_directory("templates/assets/img", "favicon.png")
@app.route("/.well-known/<path:path>")
def wellknown(path):
# Try to proxy to https://nathan.woodburn.au/.well-known/
req = requests.get(f"https://nathan.woodburn.au/.well-known/{path}")
return make_response(
req.content, 200, {"Content-Type": req.headers["Content-Type"]}
)
# endregion
# region Main routes # region Main routes
@app.route("/") @app.route("/")
def index(): def index():
txs = hsd.get_mempool() # txs = hsd.get_mempool()
mempool_info = hsd.get_mempool_info() # mempool_info = hsd.get_mempool_info()
if mempool_info['result']: # if mempool_info['result']:
mempool_info = mempool_info['result'] # mempool_info = mempool_info['result']
mempool = f"Total Transactions: {len(txs)}<br><br>" # mempool_info['txs'] = txs
for txid in txs: return render_template("index.html")
tx = hsd.get_tx(txid)
mempool += f"<a href='/tx?tx={txid}' target='_blank'>{txid}</a>: {len(tx['inputs'])} inputs, {len(tx['outputs'])} outputs, fee: {tx['fee']/1000000}, Total Value: {sum([output['value']/1000000 for output in tx['outputs']]):,.2f} HNS<br><br>" @app.route("/search")
def search():
if not request.args.get("q"):
return render_template("index.html")
query = request.args.get("q")
tx = dbCon.getTransaction(query)
if tx:
return redirect(f"/tx/{query}")
block = dbCon.getBlock(query)
if block:
return redirect(f"/block/{query}")
block = dbCon.getBlockHash(query)
if block:
return redirect(f"/block/{query}")
name = hsd.get_name(query)
if not name['error']:
return redirect(f"/name/{query}")
return render_template("index.html",message="No results found",query=query)
return render_template("index.html",mempool=mempool)
@app.route("/name") @app.route("/name")
def name(): def name():
@ -99,21 +77,25 @@ def name():
else: else:
return render_template("index.html") return render_template("index.html")
@app.route("/tx") @app.route("/tx/<tx>")
def tx(): def txPage(tx):
if request.args.get("tx"): tx = dbCon.getTransaction(tx)
tx = hsd.get_tx(request.args.get("tx")) if tx:
return render_template("data.html", data=json.dumps(tx, indent=4)) return render_template("tx.html", tx=tx)
else: return render_template("index.html",message="Transaction not found")
return render_template("index.html")
@app.route("/block") @app.route("/block/<block>")
def block(): def blockPage(block):
if request.args.get("block"): block = dbCon.getBlock(block)
block = hsd.get_block(request.args.get("block")) if block:
return render_template("data.html", data=json.dumps(block, indent=4)) return render_template("block.html", block=block)
else: return render_template("index.html",message="Block not found")
return render_template("index.html")
@app.route("/address/<address>")
def addressPage(address):
# if address:
# return render_template("address.html", address=address)
return render_template("index.html",message="Not implemented")
@app.route("/<path:path>") @app.route("/<path:path>")
@ -148,19 +130,25 @@ def api_version():
@app.route("/api/v1/tx/<txid>") @app.route("/api/v1/tx/<txid>")
def api_tx(txid): def api_tx(txid):
tx = hsd.get_tx(txid) tx = dbCon.getTransaction(txid)
if tx: if tx:
return jsonify(tx) return jsonify(tx.toJSON())
else: else:
return jsonify({"error": "tx not found"}), 404 return jsonify({"error": "tx not found"}), 404
@app.route("/api/v1/block/<blockhash>") @app.route("/api/v1/block/<blockheight>")
def api_block(blockhash): def api_block(blockheight):
block = hsd.get_block(blockhash) block = dbCon.getBlock(blockheight)
if block: if block:
return jsonify(block) print("Found block from height")
else: return jsonify(block.toJSON())
return jsonify({"error": "block not found"}), 404
block = dbCon.getBlockHash(blockheight)
if block:
print("Found block from hash")
return jsonify(block.toJSON())
return jsonify({"error": "block not found"}), 404
@app.route("/api/v1/name/<name>") @app.route("/api/v1/name/<name>")
def api_name(name): def api_name(name):
@ -213,7 +201,97 @@ def api_mempool_info():
def not_found(e): def not_found(e):
return render_template("404.html"), 404 return render_template("404.html"), 404
# endregion
# region Assets routes
@app.route("/assets/<path:path>")
def send_assets(path):
if path.endswith(".json"):
return send_from_directory(
"templates/assets", path, mimetype="application/json"
)
if os.path.isfile("templates/assets/" + path):
return send_from_directory("templates/assets", path)
# Try looking in one of the directories
filename: str = path.split("/")[-1]
if (
filename.endswith(".png")
or filename.endswith(".jpg")
or filename.endswith(".jpeg")
or filename.endswith(".svg")
):
if os.path.isfile("templates/assets/img/" + filename):
return send_from_directory("templates/assets/img", filename)
if os.path.isfile("templates/assets/img/favicon/" + filename):
return send_from_directory("templates/assets/img/favicon", filename)
return render_template("404.html"), 404
@app.route("/favicon.png")
def faviconPNG():
return send_from_directory("templates/assets/img", "favicon.png")
@app.route("/.well-known/<path:path>")
def wellknown(path):
# Try to proxy to https://nathan.woodburn.au/.well-known/
req = requests.get(f"https://nathan.woodburn.au/.well-known/{path}")
return make_response(
req.content, 200, {"Content-Type": req.headers["Content-Type"]}
)
@app.template_filter('datetimeformat')
def datetimeformat(value, format='%Y-%m-%d %H:%M:%S'):
return datetime.fromtimestamp(value, tz=timezone.utc).strftime(format)
@app.template_filter('convert_bits_to_difficulty')
def convert_bits_to_difficulty(bits):
"""Convert compact bits format to difficulty."""
bits_hex = f"{bits:08x}" # Convert to 8-char hex string
exp = int(bits_hex[:2], 16) # First byte (exponent)
coeff = int(bits_hex[2:], 16) # Remaining 3 bytes (coefficient)
# Compute target from bits
target = coeff * (256 ** (exp - 3))
# Maximum target (difficulty 1)
max_target = 0xFFFF * (256 ** (0x1D - 3))
# Compute difficulty
difficulty = Decimal(max_target) / Decimal(target)
return f"{difficulty:,.0f}"
@app.template_filter('hexToAscii')
def hexToAscii(hex_string):
# Convert the hex string to bytes
bytes_obj = bytes.fromhex(hex_string)
# Decode the bytes object to an ASCII string
ascii_string = bytes_obj.decode('ascii')
return ascii_string
@app.template_filter('getTX')
def getTX(txid):
tx = dbCon.getTransaction(txid)
if tx:
return tx.toJSON()
else:
return {"error": "tx not found"}
@app.template_filter("parse_covenant")
def parse_covenant(covenant):
covenant = Covenant(covenant)
if covenant.name:
return f"{covenant.action} {covenant.name}"
elif covenant.type == 0:
return f"{covenant.action}"
name = dbCon.getNameByHash(covenant.nameHash)
if name:
return f"{covenant.action} {name}"
return f"{covenant.action} Unknown Name"
# endregion # endregion
if __name__ == "__main__": if __name__ == "__main__":
app.run(debug=True, port=5000, host="0.0.0.0") app.run(debug=True, port=5000, host="0.0.0.0")

227
templates/address.html Normal file
View File

@ -0,0 +1,227 @@
<!DOCTYPE html>
<html data-bs-theme="dark" lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<title>Address {{address.hash}} - FireExplorer</title>
<meta property="og:type" content="website">
<meta name="description" content="The piping hot Handshake Explorer by Nathan.Woodburn/">
<meta property="og:image" content="/assets/img/favicon.png">
<link rel="icon" type="image/png" sizes="900x768" href="/assets/img/favicon.png">
<link rel="icon" type="image/png" sizes="900x768" href="/assets/img/favicon.png" media="(prefers-color-scheme: dark)">
<link rel="icon" type="image/png" sizes="900x768" href="/assets/img/favicon.png">
<link rel="icon" type="image/png" sizes="900x768" href="/assets/img/favicon.png" media="(prefers-color-scheme: dark)">
<link rel="icon" type="image/png" sizes="900x768" href="/assets/img/favicon.png">
<link rel="stylesheet" href="/assets/bootstrap/css/bootstrap.min.css">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Inter:300italic,400italic,600italic,700italic,800italic,400,300,600,700,800&amp;display=swap">
<link rel="stylesheet" href="/assets/fonts/ionicons.min.css">
</head>
<body>
<nav class="navbar navbar-expand-md sticky-top py-3 navbar-dark" id="mainNav">
<div class="container"><a class="navbar-brand d-flex align-items-center" href="/"><img src="/assets/img/favicon.png" width="64px" height="64px"><span style="margin-left: 10px;">FireExplorer</span></a><button data-bs-toggle="collapse" class="navbar-toggler" data-bs-target="#navcol-1"><span class="visually-hidden">Toggle navigation</span><span class="navbar-toggler-icon"></span></button>
<div class="collapse navbar-collapse" id="navcol-1">
<ul class="navbar-nav mx-auto">
<li class="nav-item"><a class="nav-link" href="/">Home</a></li>
<li class="nav-item"><a class="nav-link" href="/block">Blocks</a></li>
<li class="nav-item"><a class="nav-link" href="/tx">Transactions</a></li>
<li class="nav-item"><a class="nav-link" href="/name">Names</a></li>
</ul>
<form action="/search"><input class="form-control" type="search" name="q" placeholder="Search anything" value="{{query}}"></form>
</div>
</div>
</nav>
<section class="py-5" style="margin: auto;max-width: 1400px;">
<h1 class="text-center">{{message}}</h1>
<div class="card">
<div class="card-body">
<div class="container">
<div class="row">
<div class="col-md-6">
<h4>Address {{address.hash}}
<div class="btn-group btn-group-sm gap-2" role="group" style="margin-left: 10px;"><a class="btn btn-primary" role="button" href="/block/{{block.height - 1}}"><i class="icon ion-ios-arrow-back"></i></a><a class="btn btn-primary" role="button" href="/block/{{block.height + 1}}"><i class="icon ion-ios-arrow-forward"></i></a></div>
</h4>
</div>
<div class="col-md-6">
<div class="text-center toast" id="toast"><span>Copied to clipboard</span></div>
</div>
</div>
</div>
<div class="container">
<div class="row">
<div class="col-md-6">
<div class="table-responsive">
<table class="table">
<tbody>
<tr>
<td>Date</td>
<td>{{block.time | datetimeformat}}</td>
</tr>
<tr>
<td>Difficulty</td>
<td>{{block.bits | convert_bits_to_difficulty}}</td>
</tr>
<tr>
<td>Nonce</td>
<td>{{block.nonce}}</td>
</tr>
<tr>
<td>Transactions</td>
<td>{{block.txs | length}}</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="col-md-6">
<div class="table-responsive">
<table class="table">
<tbody>
<tr>
<td>Hash</td>
<td><span id="hash-display" style="cursor: pointer;text-decoration: underline;" onclick="copyToClipboard(this, &#39;{{ block.hash }}&#39;,&#39;block hash&#39;)">{{ block.hash[:6] }}...{{ block.hash[-6:] }}</span></td>
</tr>
<tr>
<td>Merkle Root</td>
<td>{{block.merkleRoot}}</td>
</tr>
<tr>
<td>Witness Root</td>
<td>{{block.witnessRoot}}</td>
</tr>
<tr>
<td>Tree Root</td>
<td>{{block.treeRoot}}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<p class="card-text">Nullam id dolor id nibh ultricies vehicula ut id elit. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Donec id elit non mi porta gravida at eget metus.</p>
<h3>Transactions</h3>{% for txid in block.txs %}
{% set tx = txid | getTX %}
<div class="card bg-dark-subtle" style="margin: 10px;">
<div class="card-body">
<h4 class="card-title">TX: #{{tx.index}}&nbsp;<span style="cursor: pointer;text-decoration: underline;" onclick="copyToClipboard(this, &#39;{{ tx.hash }}&#39;,&#39;transaction hash&#39;)">{{ tx.hash[:6] }}...{{ tx.hash[-6:] }}</span>&nbsp;&nbsp;<a href="/tx/{{tx.hash}}"><i class="icon ion-android-open"></i></a></h4>
<div class="container">
<div class="row">
<div class="col-md-6">
<h6 class="text-muted mb-2">Fee: {{"{:,.2f}".format(tx.fee / 1000000) }} HNS</h6>
</div>
<div class="col-md-6">
<h6 class="text-muted mb-2">Rate: {{"{:,.2f}".format(tx.rate/1000)}} doo/vB</h6>
</div>
</div>
</div>
<div class="container" style="border-right-width: 1px;">
<div class="row">
<div class="col-md-6" style="border-right-width: 1px;border-right-style: solid;">{% for input in tx.inputs %}
<div class="row">
<div class="col">{% if input.address %}
<a href="/address/{{input.address}}">{{ input.address }}</a>
{% elif input.coin %}
<a href="/address/{{input.coin.address}}">{{ input.coin.address }}</a>
{% else %}
<span class="text-muted">The Void</span>
{% endif %}</div>
<div class="col">{% if input.address %}
{% elif input.coin %}
<span>{{ "{:,.2f}".format(input.coin.value/1000000) }} HNS</span>
{% if input.coin.covenant %}
{% if input.coin.covenant.type != 0 %}
{{ input.coin.covenant }}
{% endif %}
{% endif %}
{% else %}
<!-- Must be a coinbase -->
{% if input.witness %}
{% if input.witness | length == 1 %}
<span>Airdrop/Name Claim</span>
<!-- {{input}} -->
{% else %}
<span>Mining Reward `{{ input.witness[0] | hexToAscii }}`</span>
{% endif %}
{% endif %}
{% endif %}</div>
</div>{% endfor %}
</div>
<div class="col-md-6">{% for output in tx.outputs %}
<div class="row">
<div class="col">{% if output.address %}
<a href="/address/{{output.address}}">{{ output.address }}</a>
{% elif output.coin %}
<a href="/address/{{output.coin.address}}">{{ output.coin.address }}</a>
{% else %}
<span class="text-muted">The Void</span>
{% endif %}</div>
<div class="col text-end">{% if output.covenant %}
{% if output.covenant.action == "NONE" %}
{{"{:,.2f}".format(output.value / 1000000) }} HNS
{% else %}
{{ output.covenant | parse_covenant }}
{% endif %}
{% else %}
{{"{:,.2f}".format(output.value / 1000000) }} HNS
{% endif %}</div>
</div>{% endfor %}
</div>
</div>
</div>
</div>
</div>{% endfor %}
</div>
</div>
</section>
<footer class="bg-dark">
<div class="container py-4 py-lg-5">
<div class="row justify-content-center">
<div class="col-sm-4 col-md-3 text-center text-lg-start d-flex flex-column">
<h3 class="fs-6 fw-bold">Services</h3>
<ul class="list-unstyled">
<li><a href="https://firewallet.au" target="_blank">FireWallet</a></li>
<li><a href="https://nathan.woodburn.au/projects" target="_blank">Development</a></li>
<li><a href="https://hnshosting.au" target="_blank">Hosting</a></li>
</ul>
</div>
<div class="col-sm-4 col-md-3 text-center text-lg-start d-flex flex-column">
<h3 class="fs-6 fw-bold">About</h3>
<ul class="list-unstyled">
<li><a href="https://nathan.woodburn.au/" target="_blank">Nathan.Woodburn/</a></li>
</ul>
</div>
<div class="col-lg-3 text-center text-lg-start d-flex flex-column align-items-center order-first align-items-lg-start order-lg-last">
<div class="fw-bold d-flex align-items-center mb-2"><span>FireExplorer</span></div>
<p class="text-muted">The piping hot Handshake Explorer by Nathan.Woodburn/</p>
</div>
</div>
<hr>
<div class="text-muted d-flex justify-content-between align-items-center pt-3">
<p class="mb-0">Copyright © 2025 FireExplorer</p>
<ul class="list-inline mb-0">
<li class="list-inline-item"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-facebook">
<path d="M16 8.049c0-4.446-3.582-8.05-8-8.05C3.58 0-.002 3.603-.002 8.05c0 4.017 2.926 7.347 6.75 7.951v-5.625h-2.03V8.05H6.75V6.275c0-2.017 1.195-3.131 3.022-3.131.876 0 1.791.157 1.791.157v1.98h-1.009c-.993 0-1.303.621-1.303 1.258v1.51h2.218l-.354 2.326H9.25V16c3.824-.604 6.75-3.934 6.75-7.951"></path>
</svg></li>
<li class="list-inline-item"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-twitter">
<path d="M5.026 15c6.038 0 9.341-5.003 9.341-9.334 0-.14 0-.282-.006-.422A6.685 6.685 0 0 0 16 3.542a6.658 6.658 0 0 1-1.889.518 3.301 3.301 0 0 0 1.447-1.817 6.533 6.533 0 0 1-2.087.793A3.286 3.286 0 0 0 7.875 6.03a9.325 9.325 0 0 1-6.767-3.429 3.289 3.289 0 0 0 1.018 4.382A3.323 3.323 0 0 1 .64 6.575v.045a3.288 3.288 0 0 0 2.632 3.218 3.203 3.203 0 0 1-.865.115 3.23 3.23 0 0 1-.614-.057 3.283 3.283 0 0 0 3.067 2.277A6.588 6.588 0 0 1 .78 13.58a6.32 6.32 0 0 1-.78-.045A9.344 9.344 0 0 0 5.026 15"></path>
</svg></li>
<li class="list-inline-item"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-instagram">
<path d="M8 0C5.829 0 5.556.01 4.703.048 3.85.088 3.269.222 2.76.42a3.917 3.917 0 0 0-1.417.923A3.927 3.927 0 0 0 .42 2.76C.222 3.268.087 3.85.048 4.7.01 5.555 0 5.827 0 8.001c0 2.172.01 2.444.048 3.297.04.852.174 1.433.372 1.942.205.526.478.972.923 1.417.444.445.89.719 1.416.923.51.198 1.09.333 1.942.372C5.555 15.99 5.827 16 8 16s2.444-.01 3.298-.048c.851-.04 1.434-.174 1.943-.372a3.916 3.916 0 0 0 1.416-.923c.445-.445.718-.891.923-1.417.197-.509.332-1.09.372-1.942C15.99 10.445 16 10.173 16 8s-.01-2.445-.048-3.299c-.04-.851-.175-1.433-.372-1.941a3.926 3.926 0 0 0-.923-1.417A3.911 3.911 0 0 0 13.24.42c-.51-.198-1.092-.333-1.943-.372C10.443.01 10.172 0 7.998 0h.003zm-.717 1.442h.718c2.136 0 2.389.007 3.232.046.78.035 1.204.166 1.486.275.373.145.64.319.92.599.28.28.453.546.598.92.11.281.24.705.275 1.485.039.843.047 1.096.047 3.231s-.008 2.389-.047 3.232c-.035.78-.166 1.203-.275 1.485a2.47 2.47 0 0 1-.599.919c-.28.28-.546.453-.92.598-.28.11-.704.24-1.485.276-.843.038-1.096.047-3.232.047s-2.39-.009-3.233-.047c-.78-.036-1.203-.166-1.485-.276a2.478 2.478 0 0 1-.92-.598 2.48 2.48 0 0 1-.6-.92c-.109-.281-.24-.705-.275-1.485-.038-.843-.046-1.096-.046-3.233 0-2.136.008-2.388.046-3.231.036-.78.166-1.204.276-1.486.145-.373.319-.64.599-.92.28-.28.546-.453.92-.598.282-.11.705-.24 1.485-.276.738-.034 1.024-.044 2.515-.045v.002zm4.988 1.328a.96.96 0 1 0 0 1.92.96.96 0 0 0 0-1.92zm-4.27 1.122a4.109 4.109 0 1 0 0 8.217 4.109 4.109 0 0 0 0-8.217zm0 1.441a2.667 2.667 0 1 1 0 5.334 2.667 2.667 0 0 1 0-5.334"></path>
</svg></li>
</ul>
</div>
</div>
</footer>
<script src="/assets/bootstrap/js/bootstrap.min.js"></script>
<script src="/assets/js/bold-and-dark.js"></script>
<script src="/assets/js/copy.js"></script>
</body>
</html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

11
templates/assets/fonts/ionicons.min.css vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 326 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

View File

@ -0,0 +1,61 @@
(function() {
"use strict"; // Start of use strict
function initParallax() {
if (!('requestAnimationFrame' in window)) return;
if (/Mobile|Android/.test(navigator.userAgent)) return;
var parallaxItems = document.querySelectorAll('[data-bss-parallax]');
if (!parallaxItems.length) return;
var defaultSpeed = 0.5;
var visible = [];
var scheduled;
window.addEventListener('scroll', scroll);
window.addEventListener('resize', scroll);
scroll();
function scroll() {
visible.length = 0;
for (var i = 0; i < parallaxItems.length; i++) {
var rect = parallaxItems[i].getBoundingClientRect();
var speed = parseFloat(parallaxItems[i].getAttribute('data-bss-parallax-speed'), 10) || defaultSpeed;
if (rect.bottom > 0 && rect.top < window.innerHeight) {
visible.push({
speed: speed,
node: parallaxItems[i]
});
}
}
cancelAnimationFrame(scheduled);
if (visible.length) {
scheduled = requestAnimationFrame(update);
}
}
function update() {
for (var i = 0; i < visible.length; i++) {
var node = visible[i].node;
var speed = visible[i].speed;
node.style.transform = 'translate3d(0, ' + (-window.scrollY * speed) + 'px, 0)';
}
}
}
initParallax();
})(); // End of use strict

View File

@ -0,0 +1,18 @@
function copyToClipboard(element, text, description) {
navigator.clipboard.writeText(text).then(function() {
showToast("Copied " + description);
}).catch(function(error) {
console.error("Copy failed!", error);
});
}
// Function to show the toast notification
function showToast(message) {
let toast = document.getElementById("toast");
toast.innerText = message;
toast.classList.add("show");
setTimeout(() => {
toast.classList.remove("show");
}, 2000);
}

227
templates/block.html Normal file
View File

@ -0,0 +1,227 @@
<!DOCTYPE html>
<html data-bs-theme="dark" lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<title>Block {{block.height}} - FireExplorer</title>
<meta property="og:type" content="website">
<meta name="description" content="The piping hot Handshake Explorer by Nathan.Woodburn/">
<meta property="og:image" content="/assets/img/favicon.png">
<link rel="icon" type="image/png" sizes="900x768" href="/assets/img/favicon.png">
<link rel="icon" type="image/png" sizes="900x768" href="/assets/img/favicon.png" media="(prefers-color-scheme: dark)">
<link rel="icon" type="image/png" sizes="900x768" href="/assets/img/favicon.png">
<link rel="icon" type="image/png" sizes="900x768" href="/assets/img/favicon.png" media="(prefers-color-scheme: dark)">
<link rel="icon" type="image/png" sizes="900x768" href="/assets/img/favicon.png">
<link rel="stylesheet" href="/assets/bootstrap/css/bootstrap.min.css">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Inter:300italic,400italic,600italic,700italic,800italic,400,300,600,700,800&amp;display=swap">
<link rel="stylesheet" href="/assets/fonts/ionicons.min.css">
</head>
<body>
<nav class="navbar navbar-expand-md sticky-top py-3 navbar-dark" id="mainNav">
<div class="container"><a class="navbar-brand d-flex align-items-center" href="/"><img src="/assets/img/favicon.png" width="64px" height="64px"><span style="margin-left: 10px;">FireExplorer</span></a><button data-bs-toggle="collapse" class="navbar-toggler" data-bs-target="#navcol-1"><span class="visually-hidden">Toggle navigation</span><span class="navbar-toggler-icon"></span></button>
<div class="collapse navbar-collapse" id="navcol-1">
<ul class="navbar-nav mx-auto">
<li class="nav-item"><a class="nav-link" href="/">Home</a></li>
<li class="nav-item"><a class="nav-link" href="/block">Blocks</a></li>
<li class="nav-item"><a class="nav-link" href="/tx">Transactions</a></li>
<li class="nav-item"><a class="nav-link" href="/name">Names</a></li>
</ul>
<form action="/search"><input class="form-control" type="search" name="q" placeholder="Search anything" value="{{query}}"></form>
</div>
</div>
</nav>
<section class="py-5" style="margin: auto;max-width: 1400px;">
<h1 class="text-center">{{message}}</h1>
<div class="card">
<div class="card-body">
<div class="container">
<div class="row">
<div class="col-md-6">
<h4>Block {{"{:,}".format(block.height)}}
<div class="btn-group btn-group-sm gap-2" role="group" style="margin-left: 10px;"><a class="btn btn-primary" role="button" href="/block/{{block.height - 1}}"><i class="icon ion-ios-arrow-back"></i></a><a class="btn btn-primary" role="button" href="/block/{{block.height + 1}}"><i class="icon ion-ios-arrow-forward"></i></a></div>
</h4>
</div>
<div class="col-md-6">
<div class="text-center toast" id="toast"><span>Copied to clipboard</span></div>
</div>
</div>
</div>
<div class="container">
<div class="row">
<div class="col-md-6">
<div class="table-responsive">
<table class="table">
<tbody>
<tr>
<td>Date</td>
<td>{{block.time | datetimeformat}}</td>
</tr>
<tr>
<td>Difficulty</td>
<td>{{block.bits | convert_bits_to_difficulty}}</td>
</tr>
<tr>
<td>Nonce</td>
<td>{{block.nonce}}</td>
</tr>
<tr>
<td>Transactions</td>
<td>{{"{:,}".format(block.txs | length)}}</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="col-md-6">
<div class="table-responsive">
<table class="table">
<tbody>
<tr>
<td>Hash</td>
<td><span id="hash-display" style="cursor: pointer;text-decoration: underline;" onclick="copyToClipboard(this, &#39;{{ block.hash }}&#39;,&#39;block hash&#39;)">{{ block.hash[:6] }}...{{ block.hash[-6:] }}</span></td>
</tr>
<tr>
<td>Merkle Root</td>
<td>{{block.merkleRoot}}</td>
</tr>
<tr>
<td>Witness Root</td>
<td>{{block.witnessRoot}}</td>
</tr>
<tr>
<td>Tree Root</td>
<td>{{block.treeRoot}}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<p class="card-text">Nullam id dolor id nibh ultricies vehicula ut id elit. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Donec id elit non mi porta gravida at eget metus.</p>
<h3>Transactions</h3>{% for txid in block.txs %}
{% set tx = txid | getTX %}
<div class="card bg-dark-subtle" style="margin: 10px;">
<div class="card-body">
<h4 class="card-title">TX: #{{tx.index}}&nbsp;<span style="cursor: pointer;text-decoration: underline;" onclick="copyToClipboard(this, &#39;{{ tx.hash }}&#39;,&#39;transaction hash&#39;)">{{ tx.hash[:6] }}...{{ tx.hash[-6:] }}</span>&nbsp;&nbsp;<a href="/tx/{{tx.hash}}"><i class="icon ion-android-open"></i></a></h4>
<div class="container">
<div class="row">
<div class="col-md-6">
<h6 class="text-muted mb-2">Fee: {{"{:,.2f}".format(tx.fee / 1000000) }} HNS</h6>
</div>
<div class="col-md-6">
<h6 class="text-muted mb-2">Rate: {{"{:,.2f}".format(tx.rate/1000)}} doo/vB</h6>
</div>
</div>
</div>
<div class="container" style="border-right-width: 1px;">
<div class="row">
<div class="col-md-6" style="border-right-width: 1px;border-right-style: solid;">{% for input in tx.inputs %}
<div class="row">
<div class="col">{% if input.address %}
<a href="/address/{{input.address}}">{{ input.address }}</a>
{% elif input.coin %}
<a href="/address/{{input.coin.address}}">{{ input.coin.address }}</a>
{% else %}
<span class="text-muted">The Void</span>
{% endif %}</div>
<div class="col">{% if input.address %}
{% elif input.coin %}
<span>{{ "{:,.2f}".format(input.coin.value/1000000) }} HNS</span>
{% if input.coin.covenant %}
{% if input.coin.covenant.type != 0 %}
{{ input.coin.covenant }}
{% endif %}
{% endif %}
{% else %}
<!-- Must be a coinbase -->
{% if input.witness %}
{% if input.witness | length == 1 %}
<span>Airdrop/Name Claim</span>
<!-- {{input}} -->
{% else %}
<span>Mining Reward `{{ input.witness[0] | hexToAscii }}`</span>
{% endif %}
{% endif %}
{% endif %}</div>
</div>{% endfor %}
</div>
<div class="col-md-6">{% for output in tx.outputs %}
<div class="row">
<div class="col">{% if output.address %}
<a href="/address/{{output.address}}">{{ output.address }}</a>
{% elif output.coin %}
<a href="/address/{{output.coin.address}}">{{ output.coin.address }}</a>
{% else %}
<span class="text-muted">The Void</span>
{% endif %}</div>
<div class="col text-end">{% if output.covenant %}
{% if output.covenant.action == "NONE" %}
{{"{:,.2f}".format(output.value / 1000000) }} HNS
{% else %}
{{ output.covenant | parse_covenant }}
{% endif %}
{% else %}
{{"{:,.2f}".format(output.value / 1000000) }} HNS
{% endif %}</div>
</div>{% endfor %}
</div>
</div>
</div>
</div>
</div>{% endfor %}
</div>
</div>
</section>
<footer class="bg-dark">
<div class="container py-4 py-lg-5">
<div class="row justify-content-center">
<div class="col-sm-4 col-md-3 text-center text-lg-start d-flex flex-column">
<h3 class="fs-6 fw-bold">Services</h3>
<ul class="list-unstyled">
<li><a href="https://firewallet.au" target="_blank">FireWallet</a></li>
<li><a href="https://nathan.woodburn.au/projects" target="_blank">Development</a></li>
<li><a href="https://hnshosting.au" target="_blank">Hosting</a></li>
</ul>
</div>
<div class="col-sm-4 col-md-3 text-center text-lg-start d-flex flex-column">
<h3 class="fs-6 fw-bold">About</h3>
<ul class="list-unstyled">
<li><a href="https://nathan.woodburn.au/" target="_blank">Nathan.Woodburn/</a></li>
</ul>
</div>
<div class="col-lg-3 text-center text-lg-start d-flex flex-column align-items-center order-first align-items-lg-start order-lg-last">
<div class="fw-bold d-flex align-items-center mb-2"><span>FireExplorer</span></div>
<p class="text-muted">The piping hot Handshake Explorer by Nathan.Woodburn/</p>
</div>
</div>
<hr>
<div class="text-muted d-flex justify-content-between align-items-center pt-3">
<p class="mb-0">Copyright © 2025 FireExplorer</p>
<ul class="list-inline mb-0">
<li class="list-inline-item"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-facebook">
<path d="M16 8.049c0-4.446-3.582-8.05-8-8.05C3.58 0-.002 3.603-.002 8.05c0 4.017 2.926 7.347 6.75 7.951v-5.625h-2.03V8.05H6.75V6.275c0-2.017 1.195-3.131 3.022-3.131.876 0 1.791.157 1.791.157v1.98h-1.009c-.993 0-1.303.621-1.303 1.258v1.51h2.218l-.354 2.326H9.25V16c3.824-.604 6.75-3.934 6.75-7.951"></path>
</svg></li>
<li class="list-inline-item"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-twitter">
<path d="M5.026 15c6.038 0 9.341-5.003 9.341-9.334 0-.14 0-.282-.006-.422A6.685 6.685 0 0 0 16 3.542a6.658 6.658 0 0 1-1.889.518 3.301 3.301 0 0 0 1.447-1.817 6.533 6.533 0 0 1-2.087.793A3.286 3.286 0 0 0 7.875 6.03a9.325 9.325 0 0 1-6.767-3.429 3.289 3.289 0 0 0 1.018 4.382A3.323 3.323 0 0 1 .64 6.575v.045a3.288 3.288 0 0 0 2.632 3.218 3.203 3.203 0 0 1-.865.115 3.23 3.23 0 0 1-.614-.057 3.283 3.283 0 0 0 3.067 2.277A6.588 6.588 0 0 1 .78 13.58a6.32 6.32 0 0 1-.78-.045A9.344 9.344 0 0 0 5.026 15"></path>
</svg></li>
<li class="list-inline-item"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-instagram">
<path d="M8 0C5.829 0 5.556.01 4.703.048 3.85.088 3.269.222 2.76.42a3.917 3.917 0 0 0-1.417.923A3.927 3.927 0 0 0 .42 2.76C.222 3.268.087 3.85.048 4.7.01 5.555 0 5.827 0 8.001c0 2.172.01 2.444.048 3.297.04.852.174 1.433.372 1.942.205.526.478.972.923 1.417.444.445.89.719 1.416.923.51.198 1.09.333 1.942.372C5.555 15.99 5.827 16 8 16s2.444-.01 3.298-.048c.851-.04 1.434-.174 1.943-.372a3.916 3.916 0 0 0 1.416-.923c.445-.445.718-.891.923-1.417.197-.509.332-1.09.372-1.942C15.99 10.445 16 10.173 16 8s-.01-2.445-.048-3.299c-.04-.851-.175-1.433-.372-1.941a3.926 3.926 0 0 0-.923-1.417A3.911 3.911 0 0 0 13.24.42c-.51-.198-1.092-.333-1.943-.372C10.443.01 10.172 0 7.998 0h.003zm-.717 1.442h.718c2.136 0 2.389.007 3.232.046.78.035 1.204.166 1.486.275.373.145.64.319.92.599.28.28.453.546.598.92.11.281.24.705.275 1.485.039.843.047 1.096.047 3.231s-.008 2.389-.047 3.232c-.035.78-.166 1.203-.275 1.485a2.47 2.47 0 0 1-.599.919c-.28.28-.546.453-.92.598-.28.11-.704.24-1.485.276-.843.038-1.096.047-3.232.047s-2.39-.009-3.233-.047c-.78-.036-1.203-.166-1.485-.276a2.478 2.478 0 0 1-.92-.598 2.48 2.48 0 0 1-.6-.92c-.109-.281-.24-.705-.275-1.485-.038-.843-.046-1.096-.046-3.233 0-2.136.008-2.388.046-3.231.036-.78.166-1.204.276-1.486.145-.373.319-.64.599-.92.28-.28.546-.453.92-.598.282-.11.705-.24 1.485-.276.738-.034 1.024-.044 2.515-.045v.002zm4.988 1.328a.96.96 0 1 0 0 1.92.96.96 0 0 0 0-1.92zm-4.27 1.122a4.109 4.109 0 1 0 0 8.217 4.109 4.109 0 0 0 0-8.217zm0 1.441a2.667 2.667 0 1 1 0 5.334 2.667 2.667 0 0 1 0-5.334"></path>
</svg></li>
</ul>
</div>
</div>
</footer>
<script src="/assets/bootstrap/js/bootstrap.min.js"></script>
<script src="/assets/js/bold-and-dark.js"></script>
<script src="/assets/js/copy.js"></script>
</body>
</html>

View File

@ -1,42 +1,128 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html data-bs-theme="dark" lang="en">
<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, shrink-to-fit=no">
<title>Nathan.Woodburn/</title> <title>Home - FireExplorer</title>
<link rel="icon" href="/assets/img/favicon.png" type="image/png"> <meta property="og:type" content="website">
<link rel="stylesheet" href="/assets/css/index.css"> <meta name="description" content="The piping hot Handshake Explorer by Nathan.Woodburn/">
<meta property="og:image" content="/assets/img/favicon.png">
<link rel="icon" type="image/png" sizes="900x768" href="/assets/img/favicon.png">
<link rel="icon" type="image/png" sizes="900x768" href="/assets/img/favicon.png" media="(prefers-color-scheme: dark)">
<link rel="icon" type="image/png" sizes="900x768" href="/assets/img/favicon.png">
<link rel="icon" type="image/png" sizes="900x768" href="/assets/img/favicon.png" media="(prefers-color-scheme: dark)">
<link rel="icon" type="image/png" sizes="900x768" href="/assets/img/favicon.png">
<link rel="stylesheet" href="/assets/bootstrap/css/bootstrap.min.css">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Inter:300italic,400italic,600italic,700italic,800italic,400,300,600,700,800&amp;display=swap">
</head> </head>
<body> <body>
<div class="spacer"></div> <nav class="navbar navbar-expand-md sticky-top py-3 navbar-dark" id="mainNav">
<div class="centre"> <div class="container"><a class="navbar-brand d-flex align-items-center" href="/"><img src="/assets/img/favicon.png" width="64px" height="64px"><span style="margin-left: 10px;">FireExplorer</span></a><button data-bs-toggle="collapse" class="navbar-toggler" data-bs-target="#navcol-1"><span class="visually-hidden">Toggle navigation</span><span class="navbar-toggler-icon"></span></button>
<h1>Nathan.Woodburn/ EXPLORER ALPHA</h1> <div class="collapse navbar-collapse" id="navcol-1">
<ul class="navbar-nav mx-auto">
<h2>MEMPOOL</h2> <li class="nav-item"><a class="nav-link active" href="/">Home</a></li>
<div> <li class="nav-item"><a class="nav-link" href="/block">Blocks</a></li>
{{ mempool |safe}} <li class="nav-item"><a class="nav-link" href="/tx">Transactions</a></li>
<li class="nav-item"><a class="nav-link" href="/name">Names</a></li>
</ul>
<form action="/search"><input class="form-control" type="search" name="q" placeholder="Search anything" value="{{query}}"></form>
</div>
</div> </div>
</nav>
<section class="py-5">
<h2>Search</h2> <h1 class="text-center">{{message}}</h1>
<div> <div class="container py-5">
<form action="/name" method="get" style="margin: 10px;"> <div class="row mb-4 mb-lg-5">
<input type="text" name="name" placeholder="Name"> <div class="col-md-8 col-xl-6 text-center mx-auto">
<button type="submit">Search</button> <p class="fw-bold text-success mb-2">Our Services</p>
</form> <h2 class="fw-bold">All Star Talent</h2>
<form action="/tx" method="get" style="margin: 10px;"> <p class="text-muted w-lg-50">No matter the project, our team can handle it.&nbsp;</p>
<input type="text" name="tx" placeholder="TXID"> </div>
<button type="submit">Search</button> </div>
</form> <div class="row row-cols-2 row-cols-md-3 mx-auto" style="max-width: 700px;">
<form action="/block" method="get" style="margin: 10px;"> <div class="col mb-4">
<input type="text" name="block" placeholder="Blockhash or Height"> <div class="text-center"><img class="rounded mb-3 fit-cover" width="150" height="150" src="/assets/img/team/avatar4.jpg">
<button type="submit">Search</button> <h5 class="fw-bold mb-0"><strong>John Smith</strong></h5>
</form> <p class="text-muted mb-2">Erat netus</p>
</div>
</div>
<div class="col mb-4">
<div class="text-center"><img class="rounded mb-3 fit-cover" width="150" height="150" src="/assets/img/team/avatar6.jpg">
<h5 class="fw-bold mb-0"><strong>John Smith</strong></h5>
<p class="text-muted mb-2">Erat netus</p>
</div>
</div>
<div class="col mb-4">
<div class="text-center"><img class="rounded mb-3 fit-cover" width="150" height="150" src="/assets/img/team/avatar5.jpg">
<h5 class="fw-bold mb-0"><strong>John Smith</strong></h5>
<p class="text-muted mb-2">Erat netus</p>
</div>
</div>
<div class="col mb-4">
<div class="text-center"><img class="rounded mb-3 fit-cover" width="150" height="150" src="/assets/img/team/avatar3.jpg">
<h5 class="fw-bold mb-0"><strong>John Smith</strong></h5>
<p class="text-muted mb-2">Erat netus</p>
</div>
</div>
<div class="col mb-4">
<div class="text-center"><img class="rounded mb-3 fit-cover" width="150" height="150" src="/assets/img/team/avatar1.jpg">
<h5 class="fw-bold mb-0"><strong>John Smith</strong></h5>
<p class="text-muted mb-2">Erat netus</p>
</div>
</div>
<div class="col mb-4">
<div class="text-center"><img class="rounded mb-3 fit-cover" width="150" height="150" src="/assets/img/team/avatar2.jpg">
<h5 class="fw-bold mb-0"><strong>John Smith</strong></h5>
<p class="text-muted mb-2">Erat netus</p>
</div>
</div>
</div>
</div> </div>
</div> </section>
<footer class="bg-dark">
<div class="container py-4 py-lg-5">
<div class="row justify-content-center">
<div class="col-sm-4 col-md-3 text-center text-lg-start d-flex flex-column">
<h3 class="fs-6 fw-bold">Services</h3>
<ul class="list-unstyled">
<li><a href="https://firewallet.au" target="_blank">FireWallet</a></li>
<li><a href="https://nathan.woodburn.au/projects" target="_blank">Development</a></li>
<li><a href="https://hnshosting.au" target="_blank">Hosting</a></li>
</ul>
</div>
<div class="col-sm-4 col-md-3 text-center text-lg-start d-flex flex-column">
<h3 class="fs-6 fw-bold">About</h3>
<ul class="list-unstyled">
<li><a href="https://nathan.woodburn.au/" target="_blank">Nathan.Woodburn/</a></li>
</ul>
</div>
<div class="col-lg-3 text-center text-lg-start d-flex flex-column align-items-center order-first align-items-lg-start order-lg-last">
<div class="fw-bold d-flex align-items-center mb-2"><span>FireExplorer</span></div>
<p class="text-muted">The piping hot Handshake Explorer by Nathan.Woodburn/</p>
</div>
</div>
<hr>
<div class="text-muted d-flex justify-content-between align-items-center pt-3">
<p class="mb-0">Copyright © 2025 FireExplorer</p>
<ul class="list-inline mb-0">
<li class="list-inline-item"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-facebook">
<path d="M16 8.049c0-4.446-3.582-8.05-8-8.05C3.58 0-.002 3.603-.002 8.05c0 4.017 2.926 7.347 6.75 7.951v-5.625h-2.03V8.05H6.75V6.275c0-2.017 1.195-3.131 3.022-3.131.876 0 1.791.157 1.791.157v1.98h-1.009c-.993 0-1.303.621-1.303 1.258v1.51h2.218l-.354 2.326H9.25V16c3.824-.604 6.75-3.934 6.75-7.951"></path>
</svg></li>
<li class="list-inline-item"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-twitter">
<path d="M5.026 15c6.038 0 9.341-5.003 9.341-9.334 0-.14 0-.282-.006-.422A6.685 6.685 0 0 0 16 3.542a6.658 6.658 0 0 1-1.889.518 3.301 3.301 0 0 0 1.447-1.817 6.533 6.533 0 0 1-2.087.793A3.286 3.286 0 0 0 7.875 6.03a9.325 9.325 0 0 1-6.767-3.429 3.289 3.289 0 0 0 1.018 4.382A3.323 3.323 0 0 1 .64 6.575v.045a3.288 3.288 0 0 0 2.632 3.218 3.203 3.203 0 0 1-.865.115 3.23 3.23 0 0 1-.614-.057 3.283 3.283 0 0 0 3.067 2.277A6.588 6.588 0 0 1 .78 13.58a6.32 6.32 0 0 1-.78-.045A9.344 9.344 0 0 0 5.026 15"></path>
</svg></li>
<li class="list-inline-item"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-instagram">
<path d="M8 0C5.829 0 5.556.01 4.703.048 3.85.088 3.269.222 2.76.42a3.917 3.917 0 0 0-1.417.923A3.927 3.927 0 0 0 .42 2.76C.222 3.268.087 3.85.048 4.7.01 5.555 0 5.827 0 8.001c0 2.172.01 2.444.048 3.297.04.852.174 1.433.372 1.942.205.526.478.972.923 1.417.444.445.89.719 1.416.923.51.198 1.09.333 1.942.372C5.555 15.99 5.827 16 8 16s2.444-.01 3.298-.048c.851-.04 1.434-.174 1.943-.372a3.916 3.916 0 0 0 1.416-.923c.445-.445.718-.891.923-1.417.197-.509.332-1.09.372-1.942C15.99 10.445 16 10.173 16 8s-.01-2.445-.048-3.299c-.04-.851-.175-1.433-.372-1.941a3.926 3.926 0 0 0-.923-1.417A3.911 3.911 0 0 0 13.24.42c-.51-.198-1.092-.333-1.943-.372C10.443.01 10.172 0 7.998 0h.003zm-.717 1.442h.718c2.136 0 2.389.007 3.232.046.78.035 1.204.166 1.486.275.373.145.64.319.92.599.28.28.453.546.598.92.11.281.24.705.275 1.485.039.843.047 1.096.047 3.231s-.008 2.389-.047 3.232c-.035.78-.166 1.203-.275 1.485a2.47 2.47 0 0 1-.599.919c-.28.28-.546.453-.92.598-.28.11-.704.24-1.485.276-.843.038-1.096.047-3.232.047s-2.39-.009-3.233-.047c-.78-.036-1.203-.166-1.485-.276a2.478 2.478 0 0 1-.92-.598 2.48 2.48 0 0 1-.6-.92c-.109-.281-.24-.705-.275-1.485-.038-.843-.046-1.096-.046-3.233 0-2.136.008-2.388.046-3.231.036-.78.166-1.204.276-1.486.145-.373.319-.64.599-.92.28-.28.546-.453.92-.598.282-.11.705-.24 1.485-.276.738-.034 1.024-.044 2.515-.045v.002zm4.988 1.328a.96.96 0 1 0 0 1.92.96.96 0 0 0 0-1.92zm-4.27 1.122a4.109 4.109 0 1 0 0 8.217 4.109 4.109 0 0 0 0-8.217zm0 1.441a2.667 2.667 0 1 1 0 5.334 2.667 2.667 0 0 1 0-5.334"></path>
</svg></li>
</ul>
</div>
</div>
</footer>
<script src="/assets/bootstrap/js/bootstrap.min.js"></script>
<script src="/assets/js/bold-and-dark.js"></script>
<script src="/assets/js/copy.js"></script>
</body> </body>
</html> </html>

188
templates/tx.html Normal file
View File

@ -0,0 +1,188 @@
<!DOCTYPE html>
<html data-bs-theme="dark" lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<title>Transaction {{tx.hash}} - FireExplorer</title>
<meta property="og:type" content="website">
<meta name="description" content="The piping hot Handshake Explorer by Nathan.Woodburn/">
<meta property="og:image" content="/assets/img/favicon.png">
<link rel="icon" type="image/png" sizes="900x768" href="/assets/img/favicon.png">
<link rel="icon" type="image/png" sizes="900x768" href="/assets/img/favicon.png" media="(prefers-color-scheme: dark)">
<link rel="icon" type="image/png" sizes="900x768" href="/assets/img/favicon.png">
<link rel="icon" type="image/png" sizes="900x768" href="/assets/img/favicon.png" media="(prefers-color-scheme: dark)">
<link rel="icon" type="image/png" sizes="900x768" href="/assets/img/favicon.png">
<link rel="stylesheet" href="/assets/bootstrap/css/bootstrap.min.css">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Inter:300italic,400italic,600italic,700italic,800italic,400,300,600,700,800&amp;display=swap">
</head>
<body>
<nav class="navbar navbar-expand-md sticky-top py-3 navbar-dark" id="mainNav">
<div class="container"><a class="navbar-brand d-flex align-items-center" href="/"><img src="/assets/img/favicon.png" width="64px" height="64px"><span style="margin-left: 10px;">FireExplorer</span></a><button data-bs-toggle="collapse" class="navbar-toggler" data-bs-target="#navcol-1"><span class="visually-hidden">Toggle navigation</span><span class="navbar-toggler-icon"></span></button>
<div class="collapse navbar-collapse" id="navcol-1">
<ul class="navbar-nav mx-auto">
<li class="nav-item"><a class="nav-link" href="/">Home</a></li>
<li class="nav-item"><a class="nav-link" href="/block">Blocks</a></li>
<li class="nav-item"><a class="nav-link" href="/tx">Transactions</a></li>
<li class="nav-item"><a class="nav-link" href="/name">Names</a></li>
</ul>
<form action="/search"><input class="form-control" type="search" name="q" placeholder="Search anything" value="{{query}}"></form>
</div>
</div>
</nav>
<section class="py-5" style="margin: auto;max-width: 1400px;">
<h1 class="text-center">{{message}}</h1>
<div class="card">
<div class="card-body">
<div class="container">
<h4>Transaction {{tx.hash}}</h4>
</div>
<div class="container">
<div class="row">
<div class="col-md-6">
<div class="table-responsive">
<table class="table">
<tbody>
<tr>
<td>Block</td>
<td>{{"{:,}".format(tx.block)}} (TX: #{{"{:,}".format(tx.index)}})</td>
</tr>
<tr>
<td>Fee</td>
<td>{{"{:,.2f}".format(tx.fee / 1000000) }} HNS</td>
</tr>
<tr>
<td>Rate</td>
<td>{{"{:,.2f}".format(tx.rate/1000)}} doo/vB</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="col-md-6">
<div class="table-responsive">
<table class="table">
<tbody>
<tr>
<td>Hash</td>
<td><span id="hash-display" style="cursor: pointer;text-decoration: underline;" onclick="copyToClipboard(this, &#39;{{ tx.hash }}&#39;,&#39;Transaction hash&#39;)">{{ tx.hash[:6] }}...{{ tx.hash[-6:] }}</span></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<p class="card-text">Nullam id dolor id nibh ultricies vehicula ut id elit. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Donec id elit non mi porta gravida at eget metus.</p>
<h3>Inputs &amp; Outputs</h3>
<div class="card bg-dark-subtle" style="margin: 10px;">
<div class="card-body">
<div class="container" style="border-right-width: 1px;">
<div class="row">
<div class="col-md-6" style="border-right-width: 1px;border-right-style: solid;">{% for input in tx.inputs %}
<div class="row">
<div class="col">{% if input.address %}
<a href="/address/{{input.address}}">{{ input.address }}</a>
{% elif input.coin %}
<a href="/address/{{input.coin.address}}">{{ input.coin.address }}</a>
{% else %}
<span class="text-muted">The Void</span>
{% endif %}</div>
<div class="col">{% if input.address %}
{% elif input.coin %}
<span>{{ "{:,.2f}".format(input.coin.value/1000000) }} HNS</span>
{% if input.coin.covenant %}
{% if input.coin.covenant.type != 0 %}
{{ input.coin.covenant }}
{% endif %}
{% endif %}
{% else %}
<!-- Must be a coinbase -->
{% if input.witness %}
{% if input.witness | length == 1 %}
<span>Airdrop/Name Claim</span>
<!-- {{input}} -->
{% else %}
<span>Mining Reward `{{ input.witness[0] | hexToAscii }}`</span>
{% endif %}
{% endif %}
{% endif %}</div>
</div>{% endfor %}
</div>
<div class="col-md-6">{% for output in tx.outputs %}
<div class="row">
<div class="col">{% if output.address %}
<a href="/address/{{output.address}}">{{ output.address }}</a>
{% elif output.coin %}
<a href="/address/{{output.coin.address}}">{{ output.coin.address }}</a>
{% else %}
<span class="text-muted">The Void</span>
{% endif %}</div>
<div class="col text-end">{% if output.covenant %}
{% if output.covenant.action == "NONE" %}
{{"{:,.2f}".format(output.value / 1000000) }} HNS
{% else %}
{{ output.covenant | parse_covenant }}
{% endif %}
{% else %}
{{"{:,.2f}".format(output.value / 1000000) }} HNS
{% endif %}</div>
</div>{% endfor %}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<footer class="bg-dark">
<div class="container py-4 py-lg-5">
<div class="row justify-content-center">
<div class="col-sm-4 col-md-3 text-center text-lg-start d-flex flex-column">
<h3 class="fs-6 fw-bold">Services</h3>
<ul class="list-unstyled">
<li><a href="https://firewallet.au" target="_blank">FireWallet</a></li>
<li><a href="https://nathan.woodburn.au/projects" target="_blank">Development</a></li>
<li><a href="https://hnshosting.au" target="_blank">Hosting</a></li>
</ul>
</div>
<div class="col-sm-4 col-md-3 text-center text-lg-start d-flex flex-column">
<h3 class="fs-6 fw-bold">About</h3>
<ul class="list-unstyled">
<li><a href="https://nathan.woodburn.au/" target="_blank">Nathan.Woodburn/</a></li>
</ul>
</div>
<div class="col-lg-3 text-center text-lg-start d-flex flex-column align-items-center order-first align-items-lg-start order-lg-last">
<div class="fw-bold d-flex align-items-center mb-2"><span>FireExplorer</span></div>
<p class="text-muted">The piping hot Handshake Explorer by Nathan.Woodburn/</p>
</div>
</div>
<hr>
<div class="text-muted d-flex justify-content-between align-items-center pt-3">
<p class="mb-0">Copyright © 2025 FireExplorer</p>
<ul class="list-inline mb-0">
<li class="list-inline-item"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-facebook">
<path d="M16 8.049c0-4.446-3.582-8.05-8-8.05C3.58 0-.002 3.603-.002 8.05c0 4.017 2.926 7.347 6.75 7.951v-5.625h-2.03V8.05H6.75V6.275c0-2.017 1.195-3.131 3.022-3.131.876 0 1.791.157 1.791.157v1.98h-1.009c-.993 0-1.303.621-1.303 1.258v1.51h2.218l-.354 2.326H9.25V16c3.824-.604 6.75-3.934 6.75-7.951"></path>
</svg></li>
<li class="list-inline-item"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-twitter">
<path d="M5.026 15c6.038 0 9.341-5.003 9.341-9.334 0-.14 0-.282-.006-.422A6.685 6.685 0 0 0 16 3.542a6.658 6.658 0 0 1-1.889.518 3.301 3.301 0 0 0 1.447-1.817 6.533 6.533 0 0 1-2.087.793A3.286 3.286 0 0 0 7.875 6.03a9.325 9.325 0 0 1-6.767-3.429 3.289 3.289 0 0 0 1.018 4.382A3.323 3.323 0 0 1 .64 6.575v.045a3.288 3.288 0 0 0 2.632 3.218 3.203 3.203 0 0 1-.865.115 3.23 3.23 0 0 1-.614-.057 3.283 3.283 0 0 0 3.067 2.277A6.588 6.588 0 0 1 .78 13.58a6.32 6.32 0 0 1-.78-.045A9.344 9.344 0 0 0 5.026 15"></path>
</svg></li>
<li class="list-inline-item"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-instagram">
<path d="M8 0C5.829 0 5.556.01 4.703.048 3.85.088 3.269.222 2.76.42a3.917 3.917 0 0 0-1.417.923A3.927 3.927 0 0 0 .42 2.76C.222 3.268.087 3.85.048 4.7.01 5.555 0 5.827 0 8.001c0 2.172.01 2.444.048 3.297.04.852.174 1.433.372 1.942.205.526.478.972.923 1.417.444.445.89.719 1.416.923.51.198 1.09.333 1.942.372C5.555 15.99 5.827 16 8 16s2.444-.01 3.298-.048c.851-.04 1.434-.174 1.943-.372a3.916 3.916 0 0 0 1.416-.923c.445-.445.718-.891.923-1.417.197-.509.332-1.09.372-1.942C15.99 10.445 16 10.173 16 8s-.01-2.445-.048-3.299c-.04-.851-.175-1.433-.372-1.941a3.926 3.926 0 0 0-.923-1.417A3.911 3.911 0 0 0 13.24.42c-.51-.198-1.092-.333-1.943-.372C10.443.01 10.172 0 7.998 0h.003zm-.717 1.442h.718c2.136 0 2.389.007 3.232.046.78.035 1.204.166 1.486.275.373.145.64.319.92.599.28.28.453.546.598.92.11.281.24.705.275 1.485.039.843.047 1.096.047 3.231s-.008 2.389-.047 3.232c-.035.78-.166 1.203-.275 1.485a2.47 2.47 0 0 1-.599.919c-.28.28-.546.453-.92.598-.28.11-.704.24-1.485.276-.843.038-1.096.047-3.232.047s-2.39-.009-3.233-.047c-.78-.036-1.203-.166-1.485-.276a2.478 2.478 0 0 1-.92-.598 2.48 2.48 0 0 1-.6-.92c-.109-.281-.24-.705-.275-1.485-.038-.843-.046-1.096-.046-3.233 0-2.136.008-2.388.046-3.231.036-.78.166-1.204.276-1.486.145-.373.319-.64.599-.92.28-.28.546-.453.92-.598.282-.11.705-.24 1.485-.276.738-.034 1.024-.044 2.515-.045v.002zm4.988 1.328a.96.96 0 1 0 0 1.92.96.96 0 0 0 0-1.92zm-4.27 1.122a4.109 4.109 0 1 0 0 8.217 4.109 4.109 0 0 0 0-8.217zm0 1.441a2.667 2.667 0 1 1 0 5.334 2.667 2.667 0 0 1 0-5.334"></path>
</svg></li>
</ul>
</div>
</div>
</footer>
<script src="/assets/bootstrap/js/bootstrap.min.js"></script>
<script src="/assets/js/bold-and-dark.js"></script>
<script src="/assets/js/copy.js"></script>
</body>
</html>