2025-02-06 22:50:56 +11:00
|
|
|
from decimal import Decimal
|
2025-02-05 17:15:44 +11:00
|
|
|
from functools import cache
|
|
|
|
import json
|
|
|
|
from flask import (
|
|
|
|
Flask,
|
|
|
|
make_response,
|
|
|
|
redirect,
|
|
|
|
request,
|
|
|
|
jsonify,
|
|
|
|
render_template,
|
|
|
|
send_from_directory,
|
|
|
|
send_file,
|
|
|
|
)
|
|
|
|
import os
|
|
|
|
import json
|
|
|
|
import requests
|
|
|
|
from datetime import datetime
|
|
|
|
import dotenv
|
2025-02-05 18:08:31 +11:00
|
|
|
import hsd
|
2025-02-06 22:50:56 +11:00
|
|
|
import indexerClasses
|
|
|
|
import db
|
|
|
|
from datetime import datetime, timezone
|
|
|
|
from indexerClasses import Block, Transaction, Covenant
|
|
|
|
|
2025-02-05 17:15:44 +11:00
|
|
|
|
|
|
|
dotenv.load_dotenv()
|
|
|
|
|
|
|
|
app = Flask(__name__)
|
|
|
|
|
2025-02-06 22:50:56 +11:00
|
|
|
dbCon = db.DBConnection()
|
2025-02-05 17:15:44 +11:00
|
|
|
|
|
|
|
def find(name, path):
|
|
|
|
for root, dirs, files in os.walk(path):
|
|
|
|
if name in files:
|
|
|
|
return os.path.join(root, name)
|
|
|
|
|
|
|
|
# region Main routes
|
|
|
|
@app.route("/")
|
|
|
|
def index():
|
2025-02-06 22:50:56 +11:00
|
|
|
# txs = hsd.get_mempool()
|
2025-02-05 18:37:13 +11:00
|
|
|
|
2025-02-06 22:50:56 +11:00
|
|
|
# mempool_info = hsd.get_mempool_info()
|
|
|
|
# if mempool_info['result']:
|
|
|
|
# mempool_info = mempool_info['result']
|
2025-02-05 18:37:13 +11:00
|
|
|
|
2025-02-06 22:50:56 +11:00
|
|
|
# mempool_info['txs'] = txs
|
|
|
|
return render_template("index.html")
|
2025-02-05 18:37:13 +11:00
|
|
|
|
2025-02-06 22:50:56 +11:00
|
|
|
@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)
|
|
|
|
|
2025-02-05 17:15:44 +11:00
|
|
|
|
2025-02-05 18:08:31 +11:00
|
|
|
@app.route("/name")
|
|
|
|
def name():
|
|
|
|
if request.args.get("name"):
|
|
|
|
name = request.args.get("name")
|
|
|
|
data = hsd.get_name(name)
|
|
|
|
dns = hsd.get_name_resource(name)
|
|
|
|
data = json.dumps(data, indent=4) + "<br><br>" + json.dumps(dns, indent=4)
|
|
|
|
return render_template("data.html", data=data)
|
|
|
|
else:
|
|
|
|
return render_template("index.html")
|
|
|
|
|
2025-02-06 22:50:56 +11:00
|
|
|
@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")
|
2025-02-05 18:08:31 +11:00
|
|
|
|
2025-02-06 22:50:56 +11:00
|
|
|
@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")
|
2025-02-05 18:08:31 +11:00
|
|
|
|
2025-02-05 17:15:44 +11:00
|
|
|
|
|
|
|
@app.route("/<path:path>")
|
|
|
|
def catch_all(path: str):
|
|
|
|
if os.path.isfile("templates/" + path):
|
|
|
|
return render_template(path)
|
|
|
|
|
|
|
|
# Try with .html
|
|
|
|
if os.path.isfile("templates/" + path + ".html"):
|
|
|
|
return render_template(path + ".html")
|
|
|
|
|
|
|
|
if os.path.isfile("templates/" + path.strip("/") + ".html"):
|
|
|
|
return render_template(path.strip("/") + ".html")
|
|
|
|
|
|
|
|
# Try to find a file matching
|
|
|
|
if path.count("/") < 1:
|
|
|
|
# Try to find a file matching
|
|
|
|
filename = find(path, "templates")
|
|
|
|
if filename:
|
|
|
|
return send_file(filename)
|
|
|
|
|
|
|
|
return render_template("404.html"), 404
|
|
|
|
|
|
|
|
|
|
|
|
# endregion
|
|
|
|
|
|
|
|
|
2025-02-05 18:08:31 +11:00
|
|
|
# region API routes
|
|
|
|
@app.route("/api/v1/version")
|
|
|
|
def api_version():
|
|
|
|
return jsonify({"version": "1.0.0"})
|
|
|
|
|
|
|
|
@app.route("/api/v1/tx/<txid>")
|
|
|
|
def api_tx(txid):
|
2025-02-06 22:50:56 +11:00
|
|
|
tx = dbCon.getTransaction(txid)
|
2025-02-05 18:08:31 +11:00
|
|
|
if tx:
|
2025-02-06 22:50:56 +11:00
|
|
|
return jsonify(tx.toJSON())
|
2025-02-05 18:08:31 +11:00
|
|
|
else:
|
|
|
|
return jsonify({"error": "tx not found"}), 404
|
|
|
|
|
2025-02-06 22:50:56 +11:00
|
|
|
@app.route("/api/v1/block/<blockheight>")
|
|
|
|
def api_block(blockheight):
|
|
|
|
block = dbCon.getBlock(blockheight)
|
2025-02-05 18:08:31 +11:00
|
|
|
if block:
|
2025-02-06 22:50:56 +11:00
|
|
|
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
|
2025-02-05 18:08:31 +11:00
|
|
|
|
|
|
|
@app.route("/api/v1/name/<name>")
|
|
|
|
def api_name(name):
|
|
|
|
name = hsd.get_name(name)
|
|
|
|
if name:
|
|
|
|
return jsonify(name)
|
|
|
|
else:
|
|
|
|
return jsonify({"error": "name not found"}), 404
|
|
|
|
|
|
|
|
@app.route("/api/v1/name/<name>/resource")
|
|
|
|
@app.route("/api/v1/name/<name>/dns")
|
|
|
|
@app.route("/api/v1/resource/<name>")
|
|
|
|
@app.route("/api/v1/dns/<name>")
|
|
|
|
def api_name_resource(name):
|
|
|
|
name = hsd.get_name_resource(name)
|
|
|
|
if name:
|
|
|
|
return jsonify(name)
|
|
|
|
else:
|
|
|
|
return jsonify({"error": "name not found"}), 404
|
|
|
|
|
|
|
|
@app.route("/api/v1/address/<address>")
|
|
|
|
def api_address(address):
|
|
|
|
address = hsd.get_address(address)
|
|
|
|
if address:
|
|
|
|
return jsonify(address)
|
|
|
|
else:
|
|
|
|
return jsonify({"error": "address not found"}), 404
|
|
|
|
|
2025-02-05 18:37:13 +11:00
|
|
|
@app.route("/api/v1/mempool")
|
|
|
|
def api_mempool():
|
|
|
|
mempool = hsd.get_mempool()
|
|
|
|
if mempool:
|
|
|
|
return jsonify(mempool)
|
|
|
|
else:
|
|
|
|
return jsonify({"error": "mempool not found"}), 404
|
|
|
|
|
|
|
|
@app.route("/api/v1/mempool/info")
|
|
|
|
def api_mempool_info():
|
|
|
|
mempool_info = hsd.get_mempool_info()
|
|
|
|
if mempool_info:
|
|
|
|
return jsonify(mempool_info)
|
|
|
|
else:
|
|
|
|
return jsonify({"error": "mempool info not found"}), 404
|
2025-02-05 18:08:31 +11:00
|
|
|
|
|
|
|
# endregion
|
|
|
|
|
2025-02-05 17:15:44 +11:00
|
|
|
# region Error Catching
|
|
|
|
# 404 catch all
|
|
|
|
@app.errorhandler(404)
|
|
|
|
def not_found(e):
|
|
|
|
return render_template("404.html"), 404
|
|
|
|
|
2025-02-06 22:50:56 +11:00
|
|
|
# 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"
|
|
|
|
|
2025-02-05 17:15:44 +11:00
|
|
|
|
|
|
|
# endregion
|
2025-02-06 22:50:56 +11:00
|
|
|
|
2025-02-05 17:15:44 +11:00
|
|
|
if __name__ == "__main__":
|
|
|
|
app.run(debug=True, port=5000, host="0.0.0.0")
|