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"):
HSD_URL = os.getenv("HSD_URL")
print(f"Using HSD_URL: {HSD_URL}")
def get_tx(txid):
return requests.get(f"{HSD_URL}/tx/{txid}").json()
@ -48,3 +45,6 @@ def get_mempool():
def get_mempool_info():
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
requests
python-dotenv
mysql-connector-python

230
server.py
View File

@ -1,3 +1,4 @@
from decimal import Decimal
from functools import cache
import json
from flask import (
@ -16,77 +17,54 @@ import requests
from datetime import datetime
import dotenv
import hsd
import indexerClasses
import db
from datetime import datetime, timezone
from indexerClasses import Block, Transaction, Covenant
dotenv.load_dotenv()
app = Flask(__name__)
dbCon = db.DBConnection()
def find(name, path):
for root, dirs, files in os.walk(path):
if name in files:
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
@app.route("/")
def index():
txs = hsd.get_mempool()
# txs = hsd.get_mempool()
mempool_info = hsd.get_mempool_info()
if mempool_info['result']:
mempool_info = mempool_info['result']
# mempool_info = hsd.get_mempool_info()
# if mempool_info['result']:
# mempool_info = mempool_info['result']
mempool = f"Total Transactions: {len(txs)}<br><br>"
for txid in txs:
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>"
# mempool_info['txs'] = txs
return render_template("index.html")
@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")
def name():
@ -99,21 +77,25 @@ def name():
else:
return render_template("index.html")
@app.route("/tx")
def tx():
if request.args.get("tx"):
tx = hsd.get_tx(request.args.get("tx"))
return render_template("data.html", data=json.dumps(tx, indent=4))
else:
return render_template("index.html")
@app.route("/tx/<tx>")
def txPage(tx):
tx = dbCon.getTransaction(tx)
if tx:
return render_template("tx.html", tx=tx)
return render_template("index.html",message="Transaction not found")
@app.route("/block")
def block():
if request.args.get("block"):
block = hsd.get_block(request.args.get("block"))
return render_template("data.html", data=json.dumps(block, indent=4))
else:
return render_template("index.html")
@app.route("/block/<block>")
def blockPage(block):
block = dbCon.getBlock(block)
if block:
return render_template("block.html", block=block)
return render_template("index.html",message="Block not found")
@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>")
@ -148,19 +130,25 @@ def api_version():
@app.route("/api/v1/tx/<txid>")
def api_tx(txid):
tx = hsd.get_tx(txid)
tx = dbCon.getTransaction(txid)
if tx:
return jsonify(tx)
return jsonify(tx.toJSON())
else:
return jsonify({"error": "tx not found"}), 404
@app.route("/api/v1/block/<blockhash>")
def api_block(blockhash):
block = hsd.get_block(blockhash)
@app.route("/api/v1/block/<blockheight>")
def api_block(blockheight):
block = dbCon.getBlock(blockheight)
if block:
return jsonify(block)
else:
return jsonify({"error": "block not found"}), 404
print("Found block from height")
return jsonify(block.toJSON())
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>")
def api_name(name):
@ -213,7 +201,97 @@ def api_mempool_info():
def not_found(e):
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
if __name__ == "__main__":
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>
<html lang="en">
<html data-bs-theme="dark" lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Nathan.Woodburn/</title>
<link rel="icon" href="/assets/img/favicon.png" type="image/png">
<link rel="stylesheet" href="/assets/css/index.css">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<title>Home - 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>
<div class="spacer"></div>
<div class="centre">
<h1>Nathan.Woodburn/ EXPLORER ALPHA</h1>
<h2>MEMPOOL</h2>
<div>
{{ mempool |safe}}
<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 active" 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>
<h2>Search</h2>
<div>
<form action="/name" method="get" style="margin: 10px;">
<input type="text" name="name" placeholder="Name">
<button type="submit">Search</button>
</form>
<form action="/tx" method="get" style="margin: 10px;">
<input type="text" name="tx" placeholder="TXID">
<button type="submit">Search</button>
</form>
<form action="/block" method="get" style="margin: 10px;">
<input type="text" name="block" placeholder="Blockhash or Height">
<button type="submit">Search</button>
</form>
</nav>
<section class="py-5">
<h1 class="text-center">{{message}}</h1>
<div class="container py-5">
<div class="row mb-4 mb-lg-5">
<div class="col-md-8 col-xl-6 text-center mx-auto">
<p class="fw-bold text-success mb-2">Our Services</p>
<h2 class="fw-bold">All Star Talent</h2>
<p class="text-muted w-lg-50">No matter the project, our team can handle it.&nbsp;</p>
</div>
</div>
<div class="row row-cols-2 row-cols-md-3 mx-auto" style="max-width: 700px;">
<div class="col mb-4">
<div class="text-center"><img class="rounded mb-3 fit-cover" width="150" height="150" src="/assets/img/team/avatar4.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/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>
</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>

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>