generated from nathanwoodburn/python-webserver-template
Compare commits
5 Commits
feat/gemin
...
b6662f400a
| Author | SHA1 | Date | |
|---|---|---|---|
|
b6662f400a
|
|||
|
1c51e97354
|
|||
|
206b323be6
|
|||
|
400897319f
|
|||
|
a36e467bd4
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,3 +4,4 @@ __pycache__/
|
|||||||
.env
|
.env
|
||||||
.vs/
|
.vs/
|
||||||
.venv/
|
.venv/
|
||||||
|
fireexplorer.db
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ RUN --mount=type=cache,target=/root/.cache/uv \
|
|||||||
|
|
||||||
# Add mount point for data volume
|
# Add mount point for data volume
|
||||||
ENV BASE_DIR=/data
|
ENV BASE_DIR=/data
|
||||||
|
ENV DATABASE_PATH=/data/fireexplorer.db
|
||||||
VOLUME /data
|
VOLUME /data
|
||||||
|
|
||||||
EXPOSE 5000
|
EXPOSE 5000
|
||||||
|
|||||||
78
server.py
78
server.py
@@ -5,9 +5,12 @@ from flask import (
|
|||||||
send_from_directory,
|
send_from_directory,
|
||||||
send_file,
|
send_file,
|
||||||
jsonify,
|
jsonify,
|
||||||
|
g,
|
||||||
|
request,
|
||||||
)
|
)
|
||||||
import os
|
import os
|
||||||
import requests
|
import requests
|
||||||
|
import sqlite3
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import dotenv
|
import dotenv
|
||||||
from tools import hip2, wallet_txt
|
from tools import hip2, wallet_txt
|
||||||
@@ -16,6 +19,40 @@ dotenv.load_dotenv()
|
|||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
DATABASE = os.getenv("DATABASE_PATH", "fireexplorer.db")
|
||||||
|
|
||||||
|
|
||||||
|
def get_db():
|
||||||
|
db = getattr(g, "_database", None)
|
||||||
|
if db is None:
|
||||||
|
db = g._database = sqlite3.connect(DATABASE)
|
||||||
|
db.row_factory = sqlite3.Row
|
||||||
|
return db
|
||||||
|
|
||||||
|
|
||||||
|
@app.teardown_appcontext
|
||||||
|
def close_connection(exception):
|
||||||
|
db = getattr(g, "_database", None)
|
||||||
|
if db is not None:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
|
def init_db():
|
||||||
|
with app.app_context():
|
||||||
|
db = get_db()
|
||||||
|
db.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS names (
|
||||||
|
namehash TEXT PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
|
||||||
|
init_db()
|
||||||
|
|
||||||
|
|
||||||
def find(name, path):
|
def find(name, path):
|
||||||
for root, dirs, files in os.walk(path):
|
for root, dirs, files in os.walk(path):
|
||||||
@@ -133,6 +170,32 @@ def catch_all(path: str):
|
|||||||
return render_template("404.html"), 404
|
return render_template("404.html"), 404
|
||||||
|
|
||||||
|
|
||||||
|
# endregion
|
||||||
|
|
||||||
|
|
||||||
|
# region API routes
|
||||||
|
@app.route("/api/v1/namehash/<namehash>")
|
||||||
|
def namehash_api(namehash):
|
||||||
|
db = get_db()
|
||||||
|
cur = db.execute("SELECT * FROM names WHERE namehash = ?", (namehash,))
|
||||||
|
row = cur.fetchone()
|
||||||
|
if row is None:
|
||||||
|
# Get namehash from hsd.hns.au
|
||||||
|
req = requests.get(f"https://hsd.hns.au/api/v1/namehash/{namehash}")
|
||||||
|
if req.status_code == 200:
|
||||||
|
name = req.json().get("result")
|
||||||
|
if not name:
|
||||||
|
return jsonify({"name": "Error", "namehash": namehash})
|
||||||
|
# Insert into database
|
||||||
|
db.execute(
|
||||||
|
"INSERT OR REPLACE INTO names (namehash, name) VALUES (?, ?)",
|
||||||
|
(namehash, name),
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
return jsonify({"name": name, "namehash": namehash})
|
||||||
|
return jsonify(dict(row))
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/v1/status")
|
@app.route("/api/v1/status")
|
||||||
def api_status():
|
def api_status():
|
||||||
return jsonify(
|
return jsonify(
|
||||||
@@ -153,6 +216,7 @@ def hip02(domain: str):
|
|||||||
"success": True,
|
"success": True,
|
||||||
"address": hip2_record,
|
"address": hip2_record,
|
||||||
"method": "hip02",
|
"method": "hip02",
|
||||||
|
"name": domain,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -163,16 +227,30 @@ def hip02(domain: str):
|
|||||||
"success": True,
|
"success": True,
|
||||||
"address": wallet_record,
|
"address": wallet_record,
|
||||||
"method": "wallet_txt",
|
"method": "wallet_txt",
|
||||||
|
"name": domain,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return jsonify(
|
return jsonify(
|
||||||
{
|
{
|
||||||
"success": False,
|
"success": False,
|
||||||
|
"name": domain,
|
||||||
"error": "No HIP02 or WALLET record found for this domain",
|
"error": "No HIP02 or WALLET record found for this domain",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/v1/covenant", methods=["POST"])
|
||||||
|
def covenant_api():
|
||||||
|
data = request.get_json()
|
||||||
|
|
||||||
|
return jsonify(
|
||||||
|
{
|
||||||
|
"success": True,
|
||||||
|
"data": data,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -109,6 +109,7 @@ section {
|
|||||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||||
animation: slideUp 0.6s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
animation: slideUp 0.6s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
||||||
opacity: 0; /* Start hidden for animation */
|
opacity: 0; /* Start hidden for animation */
|
||||||
|
min-width: 0; /* Prevent grid overflow */
|
||||||
}
|
}
|
||||||
|
|
||||||
.card:hover {
|
.card:hover {
|
||||||
@@ -159,7 +160,7 @@ section {
|
|||||||
/* Info Grid */
|
/* Info Grid */
|
||||||
.info-grid {
|
.info-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,6 +169,7 @@ section {
|
|||||||
background: rgba(15, 23, 42, 0.4);
|
background: rgba(15, 23, 42, 0.4);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
border: 1px solid var(--card-border);
|
border: 1px solid var(--card-border);
|
||||||
|
min-width: 0; /* Prevent grid overflow */
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-item.no-border {
|
.info-item.no-border {
|
||||||
@@ -265,6 +267,7 @@ section {
|
|||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
animation: staggerFade 0.4s ease forwards;
|
animation: staggerFade 0.4s ease forwards;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
min-width: 0; /* Prevent flex overflow */
|
||||||
}
|
}
|
||||||
|
|
||||||
.tx-item:hover {
|
.tx-item:hover {
|
||||||
@@ -282,6 +285,7 @@ section {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
min-width: 0; /* Prevent flex overflow */
|
||||||
}
|
}
|
||||||
|
|
||||||
.tx-view-btn {
|
.tx-view-btn {
|
||||||
@@ -388,6 +392,7 @@ section {
|
|||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Transaction Details */
|
/* Transaction Details */
|
||||||
@@ -418,6 +423,7 @@ section {
|
|||||||
border: 1px solid var(--card-border);
|
border: 1px solid var(--card-border);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
|
min-width: 0; /* Prevent overflow */
|
||||||
}
|
}
|
||||||
|
|
||||||
.tx-io-header {
|
.tx-io-header {
|
||||||
@@ -611,6 +617,7 @@ section {
|
|||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
|
|
||||||
.result-box .error {
|
.result-box .error {
|
||||||
@@ -622,6 +629,16 @@ section {
|
|||||||
border: 1px solid rgba(239, 68, 68, 0.2);
|
border: 1px solid rgba(239, 68, 68, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.success-message {
|
||||||
|
color: var(--success-color);
|
||||||
|
background: rgba(16, 185, 129, 0.1);
|
||||||
|
border: 1px solid rgba(16, 185, 129, 0.2);
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
/* Scrollbar */
|
/* Scrollbar */
|
||||||
.result-box::-webkit-scrollbar {
|
.result-box::-webkit-scrollbar {
|
||||||
width: 8px;
|
width: 8px;
|
||||||
@@ -700,6 +717,26 @@ a:hover {
|
|||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.info-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.card {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
padding: 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tx-io-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Loading Animation */
|
/* Loading Animation */
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 224 KiB After Width: | Height: | Size: 34 KiB |
@@ -1055,7 +1055,7 @@
|
|||||||
const note = document.createElement('div');
|
const note = document.createElement('div');
|
||||||
note.className = 'success-message';
|
note.className = 'success-message';
|
||||||
note.style.marginBottom = '1rem';
|
note.style.marginBottom = '1rem';
|
||||||
note.innerHTML = `Resolved alias <strong>${hip02Result.name || address}</strong> to address`;
|
note.innerHTML = `Resolved <strong>${hip02Result.name || address}</strong> to address <br><span class="mono">${address}</span>`;
|
||||||
resultElement.parentNode.insertBefore(note, resultElement);
|
resultElement.parentNode.insertBefore(note, resultElement);
|
||||||
setTimeout(() => note.remove(), 5000);
|
setTimeout(() => note.remove(), 5000);
|
||||||
} else {
|
} else {
|
||||||
@@ -1178,17 +1178,23 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
showLoading('name-result');
|
showLoading('name-result');
|
||||||
const data = await apiCall(`namehash/${nameHash}`);
|
|
||||||
|
|
||||||
// Check if result is valid and redirect to name page
|
try {
|
||||||
const resultElement = document.getElementById('name-result');
|
const response = await fetch(`/api/v1/namehash/${nameHash}`);
|
||||||
if (data.error) {
|
const data = await response.json();
|
||||||
resultElement.innerHTML = `<div class="error">Error: ${data.error.message ? data.error.message : "Failed to lookup hash"}</div>`;
|
|
||||||
} else if (data.result && typeof data.result === 'string') {
|
const resultElement = document.getElementById('name-result');
|
||||||
// Valid name found, redirect to name page
|
if (data.error) {
|
||||||
window.location.href = `/name/${data.result}`;
|
resultElement.innerHTML = `<div class="error">Error: ${data.error}</div>`;
|
||||||
} else {
|
} else if (data.name) {
|
||||||
resultElement.innerHTML = `<div class="error">No name found for this hash</div>`;
|
// Valid name found, redirect to name page
|
||||||
|
window.location.href = `/name/${data.name}`;
|
||||||
|
} else {
|
||||||
|
resultElement.innerHTML = `<div class="error">No name found for this hash</div>`;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
const resultElement = document.getElementById('name-result');
|
||||||
|
resultElement.innerHTML = `<div class="error">Error: ${e.message}</div>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user