HTML TRACKET WALLET
<html lang="es">
<head>
<meta charset="UTF-8"></meta>
<title>Wallet Tracker Hedera – CRYPTOTRANSFER con grupos por fecha</title>
<style>
body { font-family: Arial; background:#f4f4f4; padding:20px; }
table { width:100%; border-collapse:collapse; background:#fff; }
th,td { border:1px solid #ccc; padding:6px; font-size:13px; vertical-align:top; }
th { background:#222; color:#fff; }
.in { color:green; font-weight:bold; }
.out { color:red; font-weight:bold; }
.controls { margin-bottom:10px; }
button, input { padding:6px; margin:0 5px 10px 0; }
.small { font-size:11px; color:#555; }
.total-row { background:#eee; font-weight:bold; }
.group-header { font-weight:bold; background:#ddd; }
.simultaneous { background:#e8f0ff; }
</style>
</head>
<body>
<h2>CRYPTOTRANSFER – Wallet Tracker Hedera</h2>
<div class="controls">
<label>Wallet: <input id="walletInput" style="width: 150px;" value="0.0.4345311" /></label>
<label>Token ID: <input id="tokenInput" placeholder="0.0.12345" style="width: 150px;" /></label>
<label>Desde: <input id="dateFrom" type="datetime-local" /></label>
<label>Hasta: <input id="dateTo" type="datetime-local" /></label>
<button onclick="loadFilteredTransactions()">Cargar</button>
</div>
<div class="controls">
<button onclick="prevPage()">⬅ PREV</button>
<button onclick="nextPage()">NEXT ➡</button>
</div>
<table>
<thead>
<tr>
<th>Fecha</th>
<th>Tx</th>
<th>Token</th>
<th>Nombre</th>
<th>Precio USD</th>
<th>Cantidad</th>
<th>USD total tx</th>
</tr>
</thead>
<tbody id="txBody"></tbody>
<tfoot>
<tr class="total-row">
<td colspan="6">Total Balance USD de esta página</td>
<td id="totalUSD">0</td>
</tr>
</tfoot>
</table>
<script>
let ACCOUNT_ID = document.getElementById("walletInput").value;
let FILTER_TOKEN = document.getElementById("tokenInput").value || null;
let DATE_FROM = null;
let DATE_TO = null;
const MIRROR = "https://mainnet-public.mirrornode.hedera.com";
const SAUCER_API_KEY = "875e1017-87b8-4b12-8301-6aa1f1aa073b";
const LIMIT = 100;
let hbarPrice = 0;
let allTxs = [];
let currentPage = 0;
const tokenInfoCache = {};
const tokenPriceCache = {};
const colors = ["#f0f8ff","#f5f5dc"];
// Formatear números
function formatNumber(value, decimals=4){
return value.toLocaleString('es-ES', {minimumFractionDigits: decimals, maximumFractionDigits: decimals});
}
// Obtener precio HBAR
async function loadHbarPrice(){
try {
const res = await fetch("https://api.coingecko.com/api/v3/simple/price?ids=hedera-hashgraph&vs_currencies=usd");
const data = await res.json();
hbarPrice = data["hedera-hashgraph"].usd || 0;
} catch { hbarPrice = 0; }
}
// Información de token
async function getTokenInfo(tokenId){
if(tokenInfoCache[tokenId]) return tokenInfoCache[tokenId];
try{
const res = await fetch(`${MIRROR}/api/v1/tokens/${tokenId}`);
const data = await res.json();
const info = { name: data.name||tokenId, decimals: data.decimals||0 };
tokenInfoCache[tokenId] = info;
return info;
} catch {
const fallback = { name: tokenId, decimals: 0 };
tokenInfoCache[tokenId] = fallback;
return fallback;
}
}
// Precio USD de token
async function getTokenUSD(tokenId){
if(tokenPriceCache[tokenId] !== undefined) return tokenPriceCache[tokenId];
try {
const res = await fetch(`https://api.saucerswap.finance/tokens/${tokenId}`, {
headers: { "x-api-key": SAUCER_API_KEY }
});
const data = await res.json();
const price = Number(data.priceUsd || 0);
tokenPriceCache[tokenId] = price;
return price;
} catch { tokenPriceCache[tokenId]=0; return 0; }
}
// Agrupar por fecha completa: año-mes-dia-hora-minuto-segundo
function getFullDateKey(ts){
const d = new Date(ts * 1000);
const pad=(n)=>n.toString().padStart(2,'0');
return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())}-${pad(d.getHours())}-${pad(d.getMinutes())}-${pad(d.getSeconds())}`;
}
// Cargar transacciones filtradas y grupos completos
async function loadFilteredTransactions(){
ACCOUNT_ID = document.getElementById("walletInput").value;
FILTER_TOKEN = document.getElementById("tokenInput").value || null;
const dateFromVal = document.getElementById("dateFrom").value;
const dateToVal = document.getElementById("dateTo").value;
DATE_FROM = dateFromVal ? new Date(dateFromVal).getTime()/1000 : null;
DATE_TO = dateToVal ? new Date(dateToVal).getTime()/1000 : null;
currentPage = 0;
let url = `${MIRROR}/api/v1/transactions?account.id=${ACCOUNT_ID}&limit=${LIMIT}&order=desc`;
let txs = [];
while(url && txs.length < 2000){
const res = await fetch(url);
const data = await res.json();
txs = txs.concat(data.transactions);
url = data.links?.next ? MIRROR + data.links.next : null;
}
// 1️⃣ Detectar todos los timestamps que tengan el token buscado
let matchingTimestamps = new Set();
txs.forEach(tx=>{
if(FILTER_TOKEN){
const tokenMatch = tx.token_transfers?.some(x=>x.account===ACCOUNT_ID && x.token_id===FILTER_TOKEN);
const hbarMatch = FILTER_TOKEN==="HBAR" && tx.transfers?.some(x=>x.account===ACCOUNT_ID);
if(tokenMatch || hbarMatch){
matchingTimestamps.add(getFullDateKey(tx.consensus_timestamp));
}
} else {
matchingTimestamps.add(getFullDateKey(tx.consensus_timestamp));
}
});
// 2️⃣ Filtrar todos los tx que tengan el timestamp completo coincidente
allTxs = txs.filter(tx=>matchingTimestamps.has(getFullDateKey(tx.consensus_timestamp)));
// 3️⃣ Aplicar filtro de fechas si existe
if(DATE_FROM || DATE_TO){
allTxs = allTxs.filter(tx=>{
const ts=parseFloat(tx.consensus_timestamp);
if(DATE_FROM && ts < DATE_FROM) return false;
if(DATE_TO && ts > DATE_TO) return false;
return true;
});
}
renderPage();
}
// Renderizado de grupos completos
async function renderPage(){
const body = document.getElementById("txBody");
body.innerHTML = "";
let totalPageUSD = 0;
const groups = {};
allTxs.forEach(tx=>{
const key = getFullDateKey(tx.consensus_timestamp);
if(!groups[key]) groups[key]=[];
groups[key].push(tx);
});
const groupKeys = Object.keys(groups).sort((a,b)=>b.localeCompare(a));
const pageGroups = groupKeys.slice(currentPage*20, (currentPage+1)*20);
let colorIndex = 0;
for(const key of pageGroups){
const group = groups[key];
const bg = colors[colorIndex%2];
colorIndex++;
const d = new Date(parseFloat(group[0].consensus_timestamp)*1000);
body.innerHTML += `<tr class="group-header" style="background:${bg}">
<td colspan="7">Grupo de transacciones: ${d.toLocaleString()}</td>
</tr>`;
for(const tx of group){
const dateStr = new Date(tx.consensus_timestamp*1000).toLocaleString();
let txTotalUSD = 0;
let isSimultaneousExtra = false;
if(tx.transfers){
const t = tx.transfers.find(x=>x.account===ACCOUNT_ID);
if(t) txTotalUSD += t.amount/1e8 * hbarPrice;
}
if(tx.token_transfers){
const promises = tx.token_transfers.filter(x=>x.account===ACCOUNT_ID).map(async t=>{
const info = await getTokenInfo(t.token_id);
const realAmount = t.amount/Math.pow(10, info.decimals);
const tokenPrice = await getTokenUSD(t.token_id);
txTotalUSD += realAmount * tokenPrice;
});
await Promise.all(promises);
}
totalPageUSD += txTotalUSD;
// Detectar si fila agregada por coincidencia de fecha
if(FILTER_TOKEN){
const tokenMatch = tx.token_transfers?.some(x=>x.account===ACCOUNT_ID && x.token_id===FILTER_TOKEN);
const hbarMatch = FILTER_TOKEN==="HBAR" && tx.transfers?.some(x=>x.account===ACCOUNT_ID);
if(!tokenMatch && !hbarMatch) isSimultaneousExtra = true;
}
const rowClass = isSimultaneousExtra ? "simultaneous" : "";
const clsHBAR = tx.transfers?.find(x=>x.account===ACCOUNT_ID)?.amount > 0 ? "in" : "out";
if(tx.transfers){
const t = tx.transfers.find(x=>x.account===ACCOUNT_ID);
if(t){
const amount = t.amount/1e8;
body.innerHTML += `<tr style="background:${bg}" class="${rowClass}">
<td>${dateStr}</td>
<td><a href="https://hashscan.io/mainnet/transaction/${tx.transaction_id}" target="_blank">${tx.transaction_id}</a></td>
<td>HBAR</td>
<td>Hedera</td>
<td>$${formatNumber(hbarPrice,6)}</td>
<td class="${clsHBAR}">${formatNumber(amount,6)}</td>
<td>$${formatNumber(txTotalUSD,2)}</td>
</tr>`;
}
}
if(tx.token_transfers){
const promises = tx.token_transfers.filter(x=>x.account===ACCOUNT_ID).map(async t=>{
const info = await getTokenInfo(t.token_id);
if(FILTER_TOKEN && t.token_id!==FILTER_TOKEN && !isSimultaneousExtra) return;
const realAmount = t.amount/Math.pow(10, info.decimals);
const cls = realAmount>0?"in":"out";
const tokenPrice = await getTokenUSD(t.token_id);
body.innerHTML += `<tr style="background:${bg}" class="${rowClass}">
<td>${dateStr}</td>
<td><a href="https://hashscan.io/mainnet/transaction/${tx.transaction_id}" target="_blank">${tx.transaction_id}</a></td>
<td>${t.token_id}</td>
<td class="small">${info.name}</td>
<td>$${tokenPrice>0?formatNumber(tokenPrice,6):'-'}</td>
<td class="${cls}">${formatNumber(realAmount,info.decimals)}</td>
<td>$${formatNumber(txTotalUSD,2)}</td>
</tr>`;
});
await Promise.all(promises);
}
}
}
document.getElementById("totalUSD").innerText = "$"+formatNumber(totalPageUSD,2);
}
function nextPage(){
const totalGroups = Object.keys(allTxs.reduce((acc,tx)=>{ acc[getFullDateKey(tx.consensus_timestamp)]=1; return acc; },{})).length;
if((currentPage+1)*20 < totalGroups){ currentPage++; renderPage(); }
}
function prevPage(){ if(currentPage>0){ currentPage--; renderPage(); }}
(async()=>{
await loadHbarPrice();
await loadFilteredTransactions();
})();
</script>
</body>
</html>
<div><br /></div><div><br /></div><div><br /></div>
Comentarios
Publicar un comentario