Files
FireExplorer/templates/index.html
Nathan Woodburn 206b323be6
All checks were successful
Check Code Quality / RuffCheck (push) Successful in 2m0s
Build Docker / BuildImage (push) Successful in 2m13s
feat: Update mobile layout
2025-11-21 12:45:09 +11:00

1206 lines
62 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Fire Explorer</title>
<link rel="icon" href="/assets/img/favicon.png" type="image/png">
<link rel="stylesheet" href="/assets/css/index.css">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<meta name="description" content="A hot new Handshake Blockchain Explorer">
<!-- Open Graph Meta Tags -->
<meta property="og:url" content="https://explorer.hns.au/">
<meta property="og:type" content="website">
<meta property="og:title" content="Fire Explorer">
<meta property="og:description" content="A hot new Handshake Blockchain Explorer">
<meta property="og:image" content="https://explorer.hns.au/assets/img/og.png">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<!-- Twitter Meta Tags -->
<meta name="twitter:card" content="summary_large_image">
<meta property="twitter:domain" content="explorer.hns.au">
<meta property="twitter:url" content="https://explorer.hns.au/">
<meta name="twitter:title" content="Fire Explorer">
<meta name="twitter:description" content="A hot new Handshake Blockchain Explorer">
<meta name="twitter:image" content="https://explorer.hns.au/assets/img/og.png">
</head>
<body>
<header>
<div class="container">
<div class="brand">
<a href="/"><h1><img src="/assets/img/favicon.png" alt="Fire Icon" style="height: 1.2em; vertical-align: middle;"> Fire Explorer</h1></a>
<span class="subtitle">Handshake Blockchain Explorer</span>
</div>
</div>
</header>
<main class="container">
<!-- Search Section -->
<section class="search-section">
<div class="card">
<h2>Search Blockchain</h2>
<div class="search-tabs">
<button class="tab-btn active" data-tab="block">Block</button>
<button class="tab-btn" data-tab="tx">Transaction</button>
<button class="tab-btn" data-tab="address">Address</button>
<button class="tab-btn" data-tab="name">Name</button>
</div>
<!-- Block Search -->
<div class="tab-content active" id="block-tab">
<div class="search-box">
<input type="text" id="block-input" placeholder="Enter block height or hash" onkeypress="if(event.key === 'Enter') searchBlock()">
<button onclick="searchBlock()">Search Block</button>
<button onclick="searchHeader()">Get Header</button>
</div>
<div id="block-result" class="result-box"></div>
</div>
<!-- Transaction Search -->
<div class="tab-content" id="tx-tab">
<div class="search-box">
<input type="text" id="tx-input" placeholder="Enter transaction ID" onkeypress="if(event.key === 'Enter') searchTx()">
<button onclick="searchTx()">Search Transaction</button>
</div>
<div id="tx-result" class="result-box"></div>
</div>
<!-- Address Search -->
<div class="tab-content" id="address-tab">
<div class="search-box">
<input type="text" id="address-input" placeholder="Enter Handshake address" onkeypress="if(event.key === 'Enter') searchAddressTx()">
<button onclick="searchAddressTx()">Get Transactions</button>
<button onclick="searchAddressCoins()">Get Coins</button>
</div>
<p style="font-size: 0.85rem; color: var(--text-secondary); margin-top: -0.5rem; margin-bottom: 1.5rem; padding-left: 0.5rem;">
<span style="color: var(--accent-color);">Tip:</span> Use <span class="mono" style="color: var(--text-primary);">@name</span> to search via HIP02 alias
</p>
<div id="address-result" class="result-box"></div>
</div>
<!-- Name Search -->
<div class="tab-content" id="name-tab">
<div class="search-box">
<input type="text" id="name-input" placeholder="Enter Handshake name" onkeypress="if(event.key === 'Enter') searchName()">
<button onclick="searchName()">Name Info</button>
<button onclick="searchNameResource()">Get Resource</button>
<button onclick="searchNameSummary()">Get Summary</button>
</div>
<div class="search-box">
<input type="text" id="namehash-input" placeholder="Enter name hash" onkeypress="if(event.key === 'Enter') searchNameHash()">
<button onclick="searchNameHash()">Search by Hash</button>
</div>
<div id="name-result" class="result-box"></div>
</div>
</div>
</section>
<!-- Status Cards -->
<section class="status-section">
<div class="card status-card">
<h3>Chain Info</h3>
<div id="chain-status" class="status-content">Loading...</div>
</div>
<div class="card status-card">
<h3>Mempool</h3>
<div id="mempool-status" class="status-content">Loading...</div>
<div id="mempool-txs" class="mempool-txs-container"></div>
</div>
</section>
</main>
<footer>
<div class="container">
<p>Fire Explorer - Handshake Blockchain Explorer | Powered by <a href="https://hns.au" target="_blank">HNSAU</a> & <a href="https://hsd.hns.au" target="_blank">Fire HSD</a></p>
<p class="timestamp">Last updated: {{ datetime }}</p>
</div>
</footer>
<script>
// Tab switching
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.addEventListener('click', () => {
const tab = btn.dataset.tab;
// Update active tab button
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
// Update active tab content
document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
document.getElementById(tab + '-tab').classList.add('active');
});
});
// URL routing and history management
function updateURL(type, value) {
const url = `/${type}/${value}`;
window.history.pushState({type, value}, '', url);
}
function handleRoute() {
const path = window.location.pathname;
const parts = path.split('/').filter(p => p);
if (parts.length === 2) {
const [type, value] = parts;
switch(type) {
case 'block':
document.getElementById('block-input').value = value;
document.querySelector('[data-tab="block"]').click();
searchBlock();
break;
case 'header':
document.getElementById('block-input').value = value;
document.querySelector('[data-tab="block"]').click();
searchHeader();
break;
case 'tx':
document.getElementById('tx-input').value = value;
document.querySelector('[data-tab="tx"]').click();
searchTx();
break;
case 'address':
document.getElementById('address-input').value = value;
document.querySelector('[data-tab="address"]').click();
searchAddressTx();
break;
case 'name':
document.getElementById('name-input').value = value;
document.querySelector('[data-tab="name"]').click();
searchName();
break;
}
}
}
// Handle browser back/forward
window.addEventListener('popstate', (e) => {
if (e.state) {
handleRoute();
}
});
// API call helper
async function apiCall(endpoint) {
try {
const response = await fetch(`https://hsd.hns.au/api/v1/${endpoint}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
return { error: error.message };
}
}
// Convert unicode/emoji to punycode for Handshake names
function toPunycode(name) {
// Check if name contains non-ASCII characters
if (!/[^\x00-\x7F]/.test(name)) {
return name; // Already ASCII, no conversion needed
}
try {
// Use the browser's built-in URL API to convert to punycode
const url = new URL(`http://${name}`);
return url.hostname;
} catch {
// Fallback: return original if conversion fails
return name;
}
}
// Resolve HIP02 alias
async function resolveHip02(input) {
if (!input.startsWith('@')) return { success: true, address: input };
const name = input.substring(1);
try {
// Try HTTPS first
const response = await fetch(`/api/v1/hip02/${name}`);
const data = await response.json();
return data;
} catch (e) {
return { success: false, error: 'Failed to resolve HIP02 alias. ' + e.message };
}
}
// Format chain data nicely
function formatChainData(chain) {
return `
<div class="info-grid">
<div class="info-item"><strong>Height:</strong> ${chain.height.toLocaleString()}</div>
<div class="info-item"><strong>Progress:</strong> ${(chain.progress * 100).toFixed(2)}%</div>
<div class="info-item"><strong>TX Count:</strong> ${chain.state.tx.toLocaleString()}</div>
<div class="info-item"><strong>Coins:</strong> ${chain.state.coin.toLocaleString()}</div>
<div class="info-item"><strong>Value:</strong> ${(chain.state.value / 1e6).toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})} HNS</div>
<div class="info-item"><strong>Burned:</strong> ${(chain.state.burned / 1e6).toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})} HNS</div>
<div class="info-item full-width"><strong>Tip:</strong> <span class="mono">${chain.tip}</span></div>
<div class="info-item full-width"><strong>Tree Root:</strong> <span class="mono">${chain.treeRoot}</span></div>
</div>
`;
}
// Open transaction in search tab
function openTx(txId) {
document.getElementById('tx-input').value = txId;
document.querySelector('[data-tab="tx"]').click();
searchTx();
}
// Format mempool data nicely
function formatMempoolData(mempool) {
// Check if mempool is an array of transaction IDs
if (Array.isArray(mempool)) {
// Hide the loading status
document.getElementById('mempool-status').style.display = 'none';
// Display transaction count and list
const mempoolTxsContainer = document.getElementById('mempool-txs');
mempoolTxsContainer.innerHTML = `
<div class="mempool-header">
<strong>Transactions: ${mempool.length.toLocaleString()}</strong>
</div>
<div class="tx-list">
${mempool.map(txId => `
<div class="tx-item" onclick="openTx('${txId}')">
<span class="tx-hash mono">${txId}</span>
</div>
`).join('')}
</div>
`;
return '';
}
// Fallback for object format
return `
<div class="info-grid">
<div class="info-item"><strong>Transactions:</strong> ${mempool.tx || 0}</div>
<div class="info-item"><strong>Size:</strong> ${mempool.size ? (mempool.size / 1024).toFixed(2) + ' KB' : '0 KB'}</div>
<div class="info-item"><strong>Orphans:</strong> ${mempool.orphans || 0}</div>
<div class="info-item"><strong>Claims:</strong> ${mempool.claims || 0}</div>
<div class="info-item"><strong>Airdrops:</strong> ${mempool.airdrops || 0}</div>
</div>
`;
}
// Format block data nicely
function formatBlockData(block) {
if (!block || block.error) {
return `<div class="error">Error: ${block.error || 'Invalid block data'}</div>`;
}
const formatValue = (value) => (value / 1e6).toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2}) + ' HNS';
const formatTime = (timestamp) => new Date(timestamp * 1000).toLocaleString();
let html = `
<div class="tx-details">
<div class="tx-section">
<h4>Block Info</h4>
<div class="info-grid">
<div class="info-item full-width"><strong>Hash:</strong> <span class="mono">${block.hash}</span></div>
<div class="info-item"><strong>Height:</strong> ${block.height.toLocaleString()}</div>
<div class="info-item"><strong>Confirmations:</strong> ${block.depth.toLocaleString()}</div>
<div class="info-item"><strong>Time:</strong> ${formatTime(block.time)}</div>
<div class="info-item"><strong>Transactions:</strong> ${block.txs.length.toLocaleString()}</div>
<div class="info-item full-width"><strong>Previous Block:</strong> <span class="mono">${block.prevBlock}</span></div>
<div class="info-item full-width"><strong>Merkle Root:</strong> <span class="mono">${block.merkleRoot}</span></div>
<div class="info-item full-width"><strong>Witness Root:</strong> <span class="mono">${block.witnessRoot}</span></div>
<div class="info-item full-width"><strong>Tree Root:</strong> <span class="mono">${block.treeRoot}</span></div>
<div class="info-item"><strong>Version:</strong> ${block.version}</div>
<div class="info-item"><strong>Bits:</strong> ${block.bits}</div>
<div class="info-item"><strong>Nonce:</strong> ${block.nonce.toLocaleString()}</div>
</div>
</div>
<div class="tx-section">
<h4>Transactions (${block.txs.length})</h4>
<div class="tx-io-list">
${block.txs.map((tx, i) => {
const totalOutput = tx.outputs.reduce((sum, out) => sum + out.value, 0);
return `
<div class="tx-io-item" style="cursor: pointer;" onclick="window.location.href='/tx/${tx.hash}'">
<div class="tx-io-header">
<span class="tx-io-index">#${i}</span>
<span class="tx-io-value">${formatValue(totalOutput)}</span>
</div>
<div class="tx-io-hash mono">${tx.hash}</div>
<div style="display: flex; justify-content: space-between; margin-top: 0.5rem; font-size: 0.85rem; color: #b0b0b0;">
<span>${tx.inputs.length} input${tx.inputs.length !== 1 ? 's' : ''}</span>
<span>${tx.outputs.length} output${tx.outputs.length !== 1 ? 's' : ''}</span>
<span>Fee: ${formatValue(tx.fee)}</span>
</div>
</div>
`;
}).join('')}
</div>
</div>
<div class="tx-section">
<button class="secondary-btn" onclick="this.nextElementSibling.style.display = this.nextElementSibling.style.display === 'none' ? 'block' : 'none'">Show Raw JSON</button>
<pre style="display: none;">${JSON.stringify(block, null, 2)}</pre>
</div>
</div>
`;
return html;
}
// Format block header data nicely
function formatHeaderData(header) {
if (!header || header.error) {
return `<div class="error">Error: ${header.error || 'Invalid header data'}</div>`;
}
const formatTime = (timestamp) => new Date(timestamp * 1000).toLocaleString();
let html = `
<div class="tx-details">
<div class="tx-section">
<h4>Block Header Info</h4>
<div class="info-grid">
<div class="info-item full-width"><strong>Hash:</strong> <span class="mono">${header.hash}</span></div>
<div class="info-item"><strong>Height:</strong> ${header.height.toLocaleString()}</div>
<div class="info-item"><strong>Version:</strong> ${header.version}</div>
<div class="info-item"><strong>Time:</strong> ${formatTime(header.time)}</div>
<div class="info-item"><strong>Bits:</strong> ${header.bits}</div>
<div class="info-item"><strong>Nonce:</strong> ${header.nonce.toLocaleString()}</div>
<div class="info-item full-width"><strong>Previous Block:</strong> <span class="mono">${header.prevBlock}</span></div>
<div class="info-item full-width"><strong>Merkle Root:</strong> <span class="mono">${header.merkleRoot}</span></div>
<div class="info-item full-width"><strong>Witness Root:</strong> <span class="mono">${header.witnessRoot}</span></div>
<div class="info-item full-width"><strong>Tree Root:</strong> <span class="mono">${header.treeRoot}</span></div>
<div class="info-item full-width"><strong>Reserved Root:</strong> <span class="mono">${header.reservedRoot}</span></div>
<div class="info-item full-width"><strong>Extra Nonce:</strong> <span class="mono">${header.extraNonce}</span></div>
<div class="info-item full-width"><strong>Mask:</strong> <span class="mono">${header.mask}</span></div>
<div class="info-item full-width"><strong>Chainwork:</strong> <span class="mono">${header.chainwork}</span></div>
</div>
</div>
<div class="tx-section">
<button class="secondary-btn" onclick="this.nextElementSibling.style.display = this.nextElementSibling.style.display === 'none' ? 'block' : 'none'">Show Raw JSON</button>
<pre style="display: none;">${JSON.stringify(header, null, 2)}</pre>
</div>
</div>
`;
return html;
}
// Format address transactions nicely
function formatAddressTransactions(txs) {
if (!txs || txs.error) {
return `<div class="error">Error: ${txs.error || 'Invalid transaction data'}</div>`;
}
if (!Array.isArray(txs) || txs.length === 0) {
return `<div class="error">No transactions found for this address</div>`;
}
const formatValue = (value) => (value / 1e6).toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2}) + ' HNS';
const formatTime = (timestamp) => new Date(timestamp * 1000).toLocaleString();
let html = `
<div class="tx-details">
<div class="tx-section">
<h4>Transactions (${txs.length})</h4>
<div class="tx-io-list">
${txs.map((tx, i) => {
const totalInput = tx.inputs.reduce((sum, inp) => sum + (inp.coin?.value || 0), 0);
const totalOutput = tx.outputs.reduce((sum, out) => sum + out.value, 0);
return `
<div class="tx-io-item" style="cursor: pointer;" onclick="window.location.href='/tx/${tx.hash}'">
<div class="tx-io-header">
<span class="tx-io-index">${formatTime(tx.time || tx.mtime)}</span>
<span class="tx-io-value">${tx.confirmations.toLocaleString()} confirms</span>
</div>
<div class="tx-io-hash mono">${tx.hash}</div>
<div style="display: flex; justify-content: space-between; margin-top: 0.5rem; font-size: 0.85rem; color: #b0b0b0;">
<span>Block: ${tx.height >= 0 ? tx.height.toLocaleString() : 'Pending'}</span>
<span>Fee: ${formatValue(tx.fee)}</span>
<span>${tx.inputs.length} in → ${tx.outputs.length} out</span>
</div>
</div>
`;
}).join('')}
</div>
</div>
</div>
`;
return html;
}
// Format address coins nicely
function formatAddressCoins(coins) {
if (!coins || coins.error) {
return `<div class="error">Error: ${coins.error || 'Invalid coin data'}</div>`;
}
if (!Array.isArray(coins) || coins.length === 0) {
return `<div class="error">No coins found for this address</div>`;
}
const formatValue = (value) => (value / 1e6).toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2}) + ' HNS';
const totalValue = coins.reduce((sum, coin) => sum + coin.value, 0);
let html = `
<div class="tx-details">
<div class="tx-section">
<h4>Coins (${coins.length}) - Total: ${formatValue(totalValue)}</h4>
<div class="tx-io-list">
${coins.map((coin, i) => `
<div class="tx-io-item">
<div class="tx-io-header">
<span class="tx-io-index">Output #${coin.index}</span>
<span class="tx-io-value">${formatValue(coin.value)}</span>
</div>
<div class="tx-io-address">${coin.address}</div>
<div style="cursor: pointer;" onclick="window.location.href='/tx/${coin.hash}'">
<div class="tx-io-hash mono">${coin.hash}</div>
</div>
<div style="display: flex; justify-content: space-between; margin-top: 0.5rem; font-size: 0.85rem; color: #b0b0b0;">
<span>Height: ${coin.height.toLocaleString()}</span>
<span>Coinbase: ${coin.coinbase ? 'Yes' : 'No'}</span>
<span>Covenant: ${coin.covenant.action}</span>
</div>
</div>
`).join('')}
</div>
</div>
<div class="tx-section">
<button class="secondary-btn" onclick="this.nextElementSibling.style.display = this.nextElementSibling.style.display === 'none' ? 'block' : 'none'">Show Raw JSON</button>
<pre style="display: none;">${JSON.stringify(coins, null, 2)}</pre>
</div>
</div>
`;
return html;
}
// Format individual coin nicely
function formatCoin(coin) {
if (!coin || coin.error) {
return `<div class="error">Error: ${coin.error || 'Invalid coin data'}</div>`;
}
const formatValue = (value) => (value / 1e6).toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2}) + ' HNS';
let html = `
<div class="tx-details">
<div class="tx-section">
<h4>Coin Information</h4>
<div class="info-grid">
<div class="info-item">
<label>Value:</label>
<span>${formatValue(coin.value)}</span>
</div>
<div class="info-item">
<label>Output Index:</label>
<span>${coin.index}</span>
</div>
<div class="info-item">
<label>Height:</label>
<span>${coin.height.toLocaleString()}</span>
</div>
<div class="info-item">
<label>Coinbase:</label>
<span>${coin.coinbase ? 'Yes' : 'No'}</span>
</div>
<div class="info-item">
<label>Version:</label>
<span>${coin.version}</span>
</div>
<div class="info-item">
<label>Covenant:</label>
<span>${coin.covenant.action}</span>
</div>
</div>
</div>
<div class="tx-section">
<h4>Transaction Hash</h4>
<div style="cursor: pointer; padding: 1rem; background: rgba(255, 107, 53, 0.1); border-radius: 8px;" onclick="window.location.href='/tx/${coin.hash}'">
<div class="mono" style="word-break: break-all; color: #ff6b35;">${coin.hash}</div>
</div>
</div>
<div class="tx-section">
<h4>Address</h4>
<div style="cursor: pointer; padding: 1rem; background: rgba(255, 107, 53, 0.1); border-radius: 8px;" onclick="window.location.href='/address/${coin.address}'">
<div class="mono" style="word-break: break-all; color: #ff6b35;">${coin.address}</div>
</div>
</div>
${coin.covenant.items && coin.covenant.items.length > 0 ? `
<div class="tx-section">
<h4>Covenant Items</h4>
<div style="background: rgba(255, 107, 53, 0.05); padding: 1rem; border-radius: 8px;">
${coin.covenant.items.map((item, i) => `
<div style="margin-bottom: 0.5rem;">
<span style="color: #b0b0b0;">Item ${i}:</span>
<span class="mono" style="word-break: break-all;">${item}</span>
</div>
`).join('')}
</div>
</div>
` : ''}
<div class="tx-section">
<button class="secondary-btn" onclick="this.nextElementSibling.style.display = this.nextElementSibling.style.display === 'none' ? 'block' : 'none'">Show Raw JSON</button>
<pre style="display: none;">${JSON.stringify(coin, null, 2)}</pre>
</div>
</div>
`;
return html;
}
// Format name info nicely
function formatNameInfo(data) {
if (!data || data.error) {
return `<div class="error">Error: ${data.error || 'Invalid name data'}</div>`;
}
const info = data.info;
const start = data.start;
const formatValue = (value) => (value / 1e6).toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2}) + ' HNS';
const formatDate = (timestamp) => new Date(timestamp * 1000).toLocaleString();
// If name doesn't exist (no info), show error
if (!info) {
return `<div class="error">Name not found</div>`;
}
let html = `
<div class="tx-details">
<div class="tx-section">
<h4>Name: ${info.name}</h4>
<div class="info-grid">
<div class="info-item"><strong>State:</strong> <span style="color: #f7931e;">${info.state}</span></div>
<div class="info-item"><strong>Registered:</strong> ${info.registered ? 'Yes' : 'No'}</div>
<div class="info-item"><strong>Height:</strong> ${info.height.toLocaleString()}</div>
<div class="info-item"><strong>Value:</strong> ${formatValue(info.value)}</div>
<div class="info-item"><strong>Highest Bid:</strong> ${formatValue(info.highest)}</div>
<div class="info-item"><strong>Renewals:</strong> ${info.renewals}</div>
${info.stats.renewalPeriodEnd ? `
<div class="info-item"><strong>Expiry Block:</strong> ${info.stats.renewalPeriodEnd.toLocaleString()}</div>
` : ''}
<div class="info-item"><strong>Expired:</strong> ${info.expired ? 'Yes' : 'No'}</div>
<div class="info-item"><strong>Revoked:</strong> ${info.revoked > 0 ? 'Yes' : 'No'}</div>
<div class="info-item"><strong>Weak:</strong> ${info.weak ? 'Yes' : 'No'}</div>
</div>
</div>
${info.stats ? `
<div class="tx-section">
<h4>Additional Info</h4>
<div class="info-grid">
${Object.entries(info.stats).map(([key, value]) => {
// Format the key from camelCase to Title Case
const formattedKey = key.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase());
// Format the value based on type
let formattedValue;
if (typeof value === 'number') {
// Check if it looks like a block number (large integer) or a decimal (days/hours)
if ((Number.isInteger(value) && value > 100) || key.toLowerCase().includes('height') || key.toLowerCase().includes('block')) {
formattedValue = value.toLocaleString();
} else {
formattedValue = value.toFixed(2);
}
} else {
formattedValue = value;
}
return `<div class="info-item"><strong>${formattedKey}:</strong> ${formattedValue}</div>`;
}).join('')}
</div>
</div>
` : ''}
<div class="tx-section">
<h4>Owner</h4>
${ info.owner.hash == '0000000000000000000000000000000000000000000000000000000000000000' ? `
<div style="padding: 1rem; background: rgba(255, 107, 53, 0.1); border-radius: 8px;">
<div class="mono" style="word-break: break-all; color: #ff6b35;">This name is unowned</div>
</div>
` : `<div style="cursor: pointer; padding: 1rem; background: rgba(255, 107, 53, 0.1); border-radius: 8px;" onclick="window.location.href='/tx/${info.owner.hash}'">
<div class="mono" style="word-break: break-all; color: #ff6b35;">TX: ${info.owner.hash}</div>
<div style="color: #b0b0b0; margin-top: 0.5rem;">Output Index: ${info.owner.index}</div>
</div>
`}
</div>
<div class="tx-section">
<h4>Name Hash</h4>
<div class="mono" style="word-break: break-all; padding: 1rem; background: rgba(255, 107, 53, 0.05); border-radius: 8px;">${info.nameHash}</div>
</div>
${start ? `
<div class="tx-section">
<h4>Auction Info</h4>
<div class="info-grid">
<div class="info-item"><strong>Start Height:</strong> ${start.start.toLocaleString()}</div>
<div class="info-item"><strong>Week:</strong> ${start.week}</div>
<div class="info-item"><strong>Reserved:</strong> ${start.reserved ? 'Yes' : 'No'}</div>
<div class="info-item"><strong>Locked:</strong> ${start.locked ? 'Yes' : 'No'}</div>
</div>
</div>
` : ''}
<div class="tx-section">
<button class="secondary-btn" onclick="this.nextElementSibling.style.display = this.nextElementSibling.style.display === 'none' ? 'block' : 'none'">Show Raw JSON</button>
<pre style="display: none;">${JSON.stringify(data, null, 2)}</pre>
</div>
</div>
`;
return html;
}
// Format name resource records nicely
function formatNameResource(data) {
if (!data || data.error) {
return `<div class="error">Error: ${data.error || 'Invalid resource data'}</div>`;
}
if (!data.records || data.records.length === 0) {
return `<div class="error">No resource records found</div>`;
}
const records = data.records;
let html = `
<div class="tx-details">
<div class="tx-section">
<h4>DNS Records (${records.length})</h4>
<div class="tx-io-list">
${records.map((record, i) => {
let recordDetails = `<strong>${record.type}</strong>`;
switch(record.type) {
case 'NS':
recordDetails += `${record.ns}`;
break;
case 'DS':
recordDetails += `<br><span style="font-size: 0.9rem;">KeyTag: ${record.keyTag}, Algorithm: ${record.algorithm}, DigestType: ${record.digestType}</span><br><span class="mono" style="font-size: 0.85rem; color: #b0b0b0; word-break: break-all;">${record.digest}</span>`;
break;
case 'TXT':
recordDetails += `<br><span style="color: #b0b0b0;">${record.txt.map(t => `"${t}"`).join('<br>')}</span>`;
break;
case 'GLUE4':
case 'GLUE6':
recordDetails += `${record.ns}<br><span style="color: #f7931e;">${record.address}</span>`;
break;
case 'SYNTH4':
case 'SYNTH6':
recordDetails += ` → <span style="color: #f7931e;">${record.address}</span>`;
break;
}
return `
<div class="tx-io-item">
<div class="tx-io-header">
<span class="tx-io-index">Record #${i}</span>
</div>
<div style="margin-top: 0.5rem;">${recordDetails}</div>
</div>
`;
}).join('')}
</div>
</div>
<div class="tx-section">
<button class="secondary-btn" onclick="this.nextElementSibling.style.display = this.nextElementSibling.style.display === 'none' ? 'block' : 'none'">Show Raw JSON</button>
<pre style="display: none;">${JSON.stringify(data, null, 2)}</pre>
</div>
</div>
`;
return html;
}
// Format name summary nicely
function formatNameSummary(data) {
if (!data || data.error) {
return `<div class="error">Error: ${data.error || 'Invalid summary data'}</div>`;
}
const formatValue = (value) => (value).toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2}) + ' HNS';
const formatDate = (timestamp) => new Date(timestamp * 1000).toLocaleString();
let html = `
<div class="tx-details">
<div class="tx-section">
<h4>Name: ${data.name}</h4>
<div class="info-grid">
<div class="info-item"><strong>State:</strong> <span style="color: #f7931e;">${data.state}</span></div>
<div class="info-item"><strong>Height:</strong> ${data.height.toLocaleString()}</div>
<div class="info-item"><strong>Value:</strong> ${formatValue(data.value)}</div>
<div class="info-item"><strong>Blocks Until Expire:</strong> ${data.blocksUntilExpire.toLocaleString()}</div>
<div class="info-item"><strong>Mint Date:</strong> ${formatDate(data.mintTimestamp)}</div>
</div>
</div>
<div class="tx-section">
<h4>Owner Address</h4>
<div style="cursor: pointer; padding: 1rem; background: rgba(255, 107, 53, 0.1); border-radius: 8px;" onclick="window.location.href='/address/${data.owner}'">
<div class="mono" style="word-break: break-all; color: #ff6b35;">${data.owner}</div>
</div>
</div>
<div class="tx-section">
<h4>Name Hash</h4>
<div class="mono" style="word-break: break-all; padding: 1rem; background: rgba(255, 107, 53, 0.05); border-radius: 8px;">${data.hash}</div>
</div>
${data.resources && data.resources[0] && data.resources[0].records ? `
<div class="tx-section">
<h4>DNS Records (${data.resources[0].records.length})</h4>
<div class="tx-io-list">
${data.resources[0].records.map((record, i) => {
let recordDetails = `<strong>${record.type}</strong>`;
switch(record.type) {
case 'NS':
recordDetails += `${record.ns}`;
break;
case 'DS':
recordDetails += `<br><span style="font-size: 0.9rem;">KeyTag: ${record.keyTag}, Algorithm: ${record.algorithm}</span>`;
break;
case 'TXT':
recordDetails += `<br><span style="color: #b0b0b0;">${record.txt.map(t => `"${t}"`).join('<br>')}</span>`;
break;
case 'GLUE4':
case 'GLUE6':
recordDetails += `${record.ns}<br><span style="color: #f7931e;">${record.address}</span>`;
break;
case 'SYNTH4':
case 'SYNTH6':
recordDetails += ` → <span style="color: #f7931e;">${record.address}</span>`;
break;
}
return `
<div class="tx-io-item">
<div class="tx-io-header">
<span class="tx-io-index">Record #${i}</span>
</div>
<div style="margin-top: 0.5rem;">${recordDetails}</div>
</div>
`;
}).join('')}
</div>
</div>
` : ''}
<div class="tx-section">
<button class="secondary-btn" onclick="this.nextElementSibling.style.display = this.nextElementSibling.style.display === 'none' ? 'block' : 'none'">Show Raw JSON</button>
<pre style="display: none;">${JSON.stringify(data, null, 2)}</pre>
</div>
</div>
`;
return html;
}
// Format transaction data nicely
function formatTransactionData(tx) {
if (!tx || tx.error) {
return `<div class="error">Error: ${tx.error || 'Invalid transaction data'}</div>`;
}
const formatValue = (value) => (value / 1e6).toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2}) + ' HNS';
const formatRate = (value) => (value / 1e3).toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2}) + ' doo/vB';
let html = `
<div class="tx-details">
<div class="tx-section">
<h4>Transaction Info</h4>
<div class="info-grid">
<div class="info-item full-width"><strong>Hash:</strong> <span class="mono">${tx.hash}</span></div>
<div class="info-item full-width"><strong>Block:</strong> <span class="mono tx-block-hash">${tx.block || 'Unconfirmed'}</span></div>
<div class="info-item"><strong>Height:</strong> ${tx.height >= 0 ? tx.height.toLocaleString() : 'Pending'}</div>
<div class="info-item"><strong>Confirmations:</strong> ${tx.confirmations.toLocaleString()}</div>
<div class="info-item"><strong>Fee:</strong> ${formatValue(tx.fee)}</div>
<div class="info-item"><strong>Rate:</strong> ${formatRate(tx.rate)}</div>
</div>
</div>
<div class="tx-section">
<h4>Inputs (${tx.inputs.length})</h4>
<div class="tx-io-list">
${tx.inputs.map((input, i) => {
// Check if this is a coinbase input
const isCoinbase = input.prevout && input.prevout.hash === '0000000000000000000000000000000000000000000000000000000000000000';
if (isCoinbase) {
return `
<div class="tx-io-item">
<div class="tx-io-header">
<span class="tx-io-index">#${i}</span>
<span class="tx-io-value" style="color: #f7931e;">COINBASE</span>
</div>
</div>
`;
} else {
return `
<div class="tx-io-item">
<div class="tx-io-header">
<span class="tx-io-index">#${i}</span>
<span class="tx-io-value">${input.coin ? formatValue(input.coin.value) : 'Unknown'}</span>
</div>
<div class="tx-io-address">${input.coin ? input.coin.address : (input.address || 'Unknown')}</div>
${input.coin && input.coin.covenant.action !== 'NONE' ? `<div class="tx-covenant">Covenant: ${input.coin.covenant.action}</div>` : ''}
</div>
`;
}
}).join('')}
</div>
</div>
<div class="tx-section">
<h4>Outputs (${tx.outputs.length})</h4>
<div class="tx-io-list">
${tx.outputs.map((output, i) => `
<div class="tx-io-item">
<div class="tx-io-header">
<span class="tx-io-index">#${i}</span>
<span class="tx-io-value">${formatValue(output.value)}</span>
</div>
<div class="tx-io-address">${output.address}</div>
${output.covenant.action !== 'NONE' ? `<div class="tx-covenant">Covenant: ${output.covenant.action}</div>` : ''}
</div>
`).join('')}
</div>
</div>
<div class="tx-section">
<button class="secondary-btn" onclick="this.nextElementSibling.style.display = this.nextElementSibling.style.display === 'none' ? 'block' : 'none'">Show Raw JSON</button>
<pre style="display: none;">${JSON.stringify(tx, null, 2)}</pre>
</div>
</div>
`;
return html;
}
// View transaction details
async function viewTransaction(txId) {
const data = await apiCall(`tx/${txId}`);
// Create a modal or use the search result area
const modal = document.createElement('div');
modal.className = 'tx-modal';
modal.onclick = (e) => {
if (e.target === modal) {
modal.remove();
}
};
modal.innerHTML = `
<div class="tx-modal-content">
<div class="tx-modal-header">
<h3>Transaction Details</h3>
<button class="tx-modal-close" onclick="this.parentElement.parentElement.parentElement.remove()">×</button>
</div>
<div class="tx-modal-body">
${data.error ? `<div class="error">Error: ${data.error}</div>` : formatTransactionData(data)}
</div>
</div>
`;
document.body.appendChild(modal);
}
// Display helper
function displayResult(elementId, data, key = null) {
const element = document.getElementById(elementId);
if (data.error) {
element.innerHTML = `<div class="error">Error: ${data.error}</div>`;
} else {
if (key && data[key] !== undefined) {
// If the value is a simple string, display it without JSON formatting
if (typeof data[key] === 'string') {
element.innerHTML = `<pre>${data[key]}</pre>`;
} else {
element.innerHTML = `<pre>${JSON.stringify(data[key], null, 2)}</pre>`;
}
return;
}
element.innerHTML = `<pre>${JSON.stringify(data, null, 2)}</pre>`;
}
}
// Show loading animation
function showLoading(elementId) {
const element = document.getElementById(elementId);
element.style.display = 'block';
element.innerHTML = `
<div class="loading-container">
<div class="loading-spinner"></div>
<div class="loading-text">Lighing Fire...</div>
</div>
`;
}
// Load status on page load
async function loadStatus() {
const chainStatus = await apiCall('chain');
if (chainStatus.chain) {
document.getElementById('chain-status').innerHTML = formatChainData(chainStatus.chain);
} else {
displayResult('chain-status', chainStatus);
}
const mempoolStatus = await apiCall('mempool');
if (mempoolStatus && !mempoolStatus.error) {
document.getElementById('mempool-status').innerHTML = formatMempoolData(mempoolStatus);
} else {
displayResult('mempool-status', mempoolStatus);
}
}
// Search functions
async function searchBlock() {
const blockId = document.getElementById('block-input').value.trim().replace(/,/g, '');
if (!blockId) {
alert('Please enter a block height or hash');
return;
}
showLoading('block-result');
updateURL('block', blockId);
const data = await apiCall(`block/${blockId}`);
// Use formatted display instead of raw JSON
const resultElement = document.getElementById('block-result');
if (data.error) {
resultElement.innerHTML = `<div class="error">Error: ${data.error}</div>`;
} else {
resultElement.innerHTML = formatBlockData(data);
}
}
async function searchHeader() {
const blockId = document.getElementById('block-input').value.trim().replace(/,/g, '');
if (!blockId) {
alert('Please enter a block height or hash');
return;
}
showLoading('block-result');
updateURL('header', blockId);
const data = await apiCall(`header/${blockId}`);
// Use formatted display instead of raw JSON
const resultElement = document.getElementById('block-result');
if (data.error) {
resultElement.innerHTML = `<div class="error">Error: ${data.error}</div>`;
} else {
resultElement.innerHTML = formatHeaderData(data);
}
}
async function searchTx() {
const txId = document.getElementById('tx-input').value.trim().replace(/,/g, '');
if (!txId) {
alert('Please enter a transaction ID');
return;
}
const resultElement = document.getElementById('tx-result');
showLoading('tx-result');
// Scroll to the search section
document.querySelector('.search-section').scrollIntoView({ behavior: 'smooth', block: 'start' });
updateURL('tx', txId);
const data = await apiCall(`tx/${txId}`);
// Use formatted display instead of raw JSON
if (data.error) {
resultElement.innerHTML = `<div class="error">Error: ${data.error}</div>`;
} else {
resultElement.innerHTML = formatTransactionData(data);
}
}
async function searchAddressTx() {
let address = document.getElementById('address-input').value.trim().replace(/,/g, '');
if (!address) {
alert('Please enter an address');
return;
}
showLoading('address-result');
const resultElement = document.getElementById('address-result');
// Check for HIP02 alias
if (address.startsWith('@')) {
const hip02Result = await resolveHip02(address);
if (hip02Result.success && hip02Result.address) {
address = hip02Result.address;
// Update input to show resolved address
document.getElementById('address-input').value = address;
// Add a note about resolution
const note = document.createElement('div');
note.className = 'success-message';
note.style.marginBottom = '1rem';
note.innerHTML = `Resolved <strong>${hip02Result.name || address}</strong> to address <br><span class="mono">${address}</span>`;
resultElement.parentNode.insertBefore(note, resultElement);
setTimeout(() => note.remove(), 5000);
} else {
resultElement.innerHTML = `<div class="error">HIP02 Error: ${hip02Result.error || 'Could not resolve alias'}</div>`;
return;
}
}
updateURL('address', address);
const data = await apiCall(`tx/address/${address}`);
// Use formatted display instead of raw JSON
if (data.error) {
resultElement.innerHTML = `<div class="error">Error: ${data.error}</div>`;
} else {
resultElement.innerHTML = formatAddressTransactions(data);
}
}
async function searchAddressCoins() {
let address = document.getElementById('address-input').value.trim().replace(/,/g, '');
if (!address) {
alert('Please enter an address');
return;
}
showLoading('address-result');
const resultElement = document.getElementById('address-result');
// Check for HIP02 alias
if (address.startsWith('@')) {
const hip02Result = await resolveHip02(address);
if (hip02Result.success && hip02Result.address) {
address = hip02Result.address;
// Update input to show resolved address
document.getElementById('address-input').value = address;
} else {
resultElement.innerHTML = `<div class="error">HIP02 Error: ${hip02Result.error || 'Could not resolve alias'}</div>`;
return;
}
}
updateURL('address', address);
const data = await apiCall(`coin/address/${address}`);
// Use formatted display instead of raw JSON
if (data.error) {
resultElement.innerHTML = `<div class="error">Error: ${data.error}</div>`;
} else {
resultElement.innerHTML = formatAddressCoins(data);
}
}
async function searchName() {
const name = document.getElementById('name-input').value.trim().replace(/,/g, '');
if (!name) {
alert('Please enter a name');
return;
}
showLoading('name-result');
const punyName = toPunycode(name);
updateURL('name', punyName);
const data = await apiCall(`name/${punyName}`);
// Use formatted display
const resultElement = document.getElementById('name-result');
if (data.error) {
resultElement.innerHTML = `<div class="error">Error: ${data.error}</div>`;
} else {
resultElement.innerHTML = formatNameInfo(data);
}
}
async function searchNameResource() {
const name = document.getElementById('name-input').value.trim().replace(/,/g, '');
if (!name) {
alert('Please enter a name');
return;
}
showLoading('name-result');
const punyName = toPunycode(name);
const data = await apiCall(`nameresource/${punyName}`);
// Use formatted display
const resultElement = document.getElementById('name-result');
if (data.error) {
resultElement.innerHTML = `<div class="error">Error: ${data.error}</div>`;
} else {
resultElement.innerHTML = formatNameResource(data);
}
}
async function searchNameSummary() {
const name = document.getElementById('name-input').value.trim().replace(/,/g, '');
if (!name) {
alert('Please enter a name');
return;
}
showLoading('name-result');
const punyName = toPunycode(name);
const data = await apiCall(`namesummary/${punyName}`);
// Use formatted display
const resultElement = document.getElementById('name-result');
if (data.error) {
resultElement.innerHTML = `<div class="error">Error: ${data.error}</div>`;
} else {
resultElement.innerHTML = formatNameSummary(data);
}
}
async function searchNameHash() {
const nameHash = document.getElementById('namehash-input').value.trim().replace(/,/g, '');
if (!nameHash) {
alert('Please enter a name hash');
return;
}
showLoading('name-result');
const data = await apiCall(`namehash/${nameHash}`);
// Check if result is valid and redirect to name page
const resultElement = document.getElementById('name-result');
if (data.error) {
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') {
// Valid name found, redirect to name page
window.location.href = `/name/${data.result}`;
} else {
resultElement.innerHTML = `<div class="error">No name found for this hash</div>`;
}
}
// Load status when page loads
window.addEventListener('load', () => {
loadStatus();
handleRoute();
});
// Refresh status every 30 seconds
setInterval(loadStatus, 30000);
</script>
</body>
</html>