Files
FireExplorer/templates/index.html

1121 lines
58 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">
<h1><img src="/assets/img/favicon.png" alt="Fire Icon" style="height: 1.2em; vertical-align: middle;"> Fire Explorer</h1>
<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>
<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;
}
}
// 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();
document.querySelector('.search-section').scrollIntoView({ behavior: 'smooth' });
}
// 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">
<span class="tx-hash mono">${txId}</span>
<button class="tx-view-btn" onclick="openTx('${txId}')">View</button>
</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>`;
}
}
// 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;
}
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;
}
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;
}
updateURL('tx', txId);
const data = await apiCall(`tx/${txId}`);
// Use formatted display instead of raw JSON
const resultElement = document.getElementById('tx-result');
if (data.error) {
resultElement.innerHTML = `<div class="error">Error: ${data.error}</div>`;
} else {
resultElement.innerHTML = formatTransactionData(data);
}
}
async function searchAddressTx() {
const address = document.getElementById('address-input').value.trim().replace(/,/g, '');
if (!address) {
alert('Please enter an address');
return;
}
updateURL('address', address);
const data = await apiCall(`tx/address/${address}`);
// Use formatted display instead of raw JSON
const resultElement = document.getElementById('address-result');
if (data.error) {
resultElement.innerHTML = `<div class="error">Error: ${data.error}</div>`;
} else {
resultElement.innerHTML = formatAddressTransactions(data);
}
}
async function searchAddressCoins() {
const address = document.getElementById('address-input').value.trim().replace(/,/g, '');
if (!address) {
alert('Please enter an address');
return;
}
updateURL('address', address);
const data = await apiCall(`coin/address/${address}`);
// Use formatted display instead of raw JSON
const resultElement = document.getElementById('address-result');
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;
}
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;
}
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;
}
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;
}
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>