1/* ============================================================ 2 Zoltar Speaks Rebalancer â Mempool Fee Gauge + Block Context 3 Real-time sat/vB data from mempool.space 4 ============================================================ */ 5 6(function () { 7 'use strict'; 8 9 const FEE_API = 'https://mempool.space/api/v1/fees/recommended'; 10 const BLOCKS_API = 'https://mempool.space/api/v1/blocks'; 11 const REFRESH_MS = 1800000; // 30 minutes 12 const NUM_BLOCKS = 6; 13 var isFetchingFees = false; // re-entry guard 14 15 /* ---- Tier definitions ---- */ 16 const TIERS = [ 17 { max: 2, label: 'OPTIMAL', color: '#39ff9e', bg: 'rgba(57,255,158,.12)' }, 18 { max: 5, label: 'LOW', color: '#00e0c4', bg: 'rgba(0,224,196,.12)' }, 19 { max: 10, label: 'MODERATE', color: '#06b6d4', bg: 'rgba(6,182,212,.12)' }, 20 { max: 20, label: 'ELEVATED', color: '#f5a623', bg: 'rgba(245,166,35,.12)' }, 21 { max: 35, label: 'HIGH', color: '#ff8c00', bg: 'rgba(255,140,0,.12)' }, 22 { max: 50, label: 'VERY HIGH', color: '#ff6b35', bg: 'rgba(255,107,53,.12)' }, 23 { max: 75, label: 'INTENSE', color: '#ff4444', bg: 'rgba(255,68,68,.12)' }, 24 { max: 100, label: 'SEVERE', color: '#e62020', bg: 'rgba(230,32,32,.12)' }, 25 { max: 120, label: 'CRITICAL', color: '#d40000', bg: 'rgba(212,0,0,.12)' }, 26 { max: Infinity, label: 'EXTREME', color: '#cc0000', bg: 'rgba(204,0,0,.15)' } 27 ]; 28 29 function getTier(satVb) { 30 var v = Math.max(1, satVb); 31 for (var i = 0; i < TIERS.length; i++) { 32 if (v <= TIERS[i].max) return { num: i + 1, ...TIERS[i] }; 33 } 34 return { num: 10, ...TIERS[9] }; 35 } 36 37 /* ---- Logarithmic scale: 1â-90° ⦠200â+90° ---- */ 38 var LOG_MIN = Math.log(1); 39 var LOG_MAX = Math.log(200); 40 function feeToAngle(satVb) { 41 var clamped = Math.min(Math.max(satVb, 1), 200); 42 var pct = (Math.log(clamped) - LOG_MIN) / (LOG_MAX - LOG_MIN); 43 return -90 + pct * 180; 44 } 45 46 /* ---- SVG arc helper ---- */ 47 function arcPath(cx, cy, r, startDeg, endDeg) { 48 var s = (startDeg - 90) * Math.PI / 180; 49 var e = (endDeg - 90) * Math.PI / 180; 50 var x1 = cx + r * Math.cos(s), y1 = cy + r * Math.sin(s); 51 var x2 = cx + r * Math.cos(e), y2 = cy + r * Math.sin(e); 52 var large = (endDeg - startDeg > 180) ? 1 : 0; 53 return 'M ' + x1 + ' ' + y1 + ' A ' + r + ' ' + r + ' 0 ' + large + ' 1 ' + x2 + ' ' + y2; 54 } 55 56 function valToAngle(v) { 57 var c = Math.min(Math.max(v, 1), 200); 58 return -90 + ((Math.log(c) - LOG_MIN) / (LOG_MAX - LOG_MIN)) * 180; 59 } 60 61 /* ---- Build gauge SVG ---- */ 62 function buildGaugeSVG() { 63 var cx = 150, cy = 145, r = 115; 64 var segments = ''; 65 var thresholds = [1, 2, 5, 10, 20, 35, 50, 75, 100, 120, 200]; 66 for (var i = 0; i < TIERS.length; i++) { 67 var startDeg = valToAngle(thresholds[i]); 68 var endDeg = valToAngle(thresholds[i + 1]); 69 segments += '<path d="' + arcPath(cx, cy, r, startDeg, endDeg) + '" ' 70 + 'fill="none" stroke="' + TIERS[i].color + '" stroke-width="20" ' 71 + 'stroke-linecap="round" opacity="0.8"/>'; 72 } 73 74 var tickVals = [1, 2, 3, 5, 7, 10, 15, 20, 30, 50, 75, 100, 150, 200]; 75 var majorTicks = [1, 2, 5, 10, 20, 50, 100, 200]; 76 var ticks = ''; 77 for (var ti = 0; ti < tickVals.length; ti++) { 78 var tv = tickVals[ti]; 79 var isMajor = majorTicks.indexOf(tv) !== -1; 80 var deg = (valToAngle(tv) - 90) * Math.PI / 180; 81 var inner = isMajor ? r - 32 : r - 24; 82 var outer = r - 12; 83 ticks += '<line x1="' + (cx + inner * Math.cos(deg)) + '" y1="' + (cy + inner * Math.sin(deg)) + '" ' 84 + 'x2="' + (cx + outer * Math.cos(deg)) + '" y2="' + (cy + outer * Math.sin(deg)) + '" ' 85 + 'stroke="rgba(255,255,255,' + (isMajor ? '0.6' : '0.25') + ')" stroke-width="' + (isMajor ? '2' : '1') + '"/>'; 86 } 87 88 var labelVals = [1, 2, 5, 10, 20, 50, 100, 200]; 89 var labels = ''; 90 labelVals.forEach(function (v) { 91 var deg = (valToAngle(v) - 90) * Math.PI / 180; 92 var lr = r - 44; 93 labels += '<text x="' + (cx + lr * Math.cos(deg)) + '" y="' + (cy + lr * Math.sin(deg) + 4) + '" ' 94 + 'fill="rgba(255,255,255,0.6)" font-size="9" text-anchor="middle" font-family="\'Press Start 2P\', monospace">' 95 + v + '</text>'; 96 }); 97 98 // Needle â triangle pointer for visibility 99 return '<svg class="fee-gauge-svg" viewBox="0 0 300 180" xmlns="http://www.w3.org/2000/svg">' 100 + '<defs>' 101 + '<filter id="needleGlow"><feGaussianBlur stdDeviation="4" result="g"/>' 102 + '<feMerge><feMergeNode in="g"/><feMergeNode in="SourceGraphic"/></feMerge></filter>' 103 + '<linearGradient id="needleGrad" x1="0" y1="0" x2="0" y2="1">' 104 + '<stop offset="0%" stop-color="#ffffff"/>' 105 + '<stop offset="100%" stop-color="#ff6b35"/>' 106 + '</linearGradient>' 107 + '</defs>' 108 + segments + ticks + labels 109 // Needle: a tapered polygon for real visibility
110 + '<g id="feeNeedle" filter="url(#needleGlow)">' 111 + '<polygon points="' + cx + ',' + (cy - r + 24) + ' ' + (cx - 5) + ',' + cy + ' ' + (cx + 5) + ',' + cy + '" ' 112 + 'fill="url(#needleGrad)" opacity="0.95"/>' 113 + '<circle cx="' + cx + '" cy="' + cy + '" r="8" fill="#1a1a2e" stroke="#ff6b35" stroke-width="2.5"/>' 114 + '<circle cx="' + cx + '" cy="' + cy + '" r="3" fill="#ffffff"/>' 115 + '</g>' 116 + '</svg>'; 117 } 118 119 /* ==== BLOCK FEE CONTEXT CHART ==== */ 120 function fmtSats(n) { 121 if (n >= 1e6) return (n / 1e6).toFixed(2) + 'M'; 122 if (n >= 1e3) return (n / 1e3).toFixed(1) + 'K'; 123 return String(Math.round(n)); 124 } 125 function fmtK(n) { 126 return n >= 1000 ? (n / 1000).toFixed(1) + 'k' : String(n); 127 } 128 function timeAgo(ts) { 129 var s = Math.max(0, Math.floor(Date.now() / 1000) - ts); 130 if (s < 60) return 'just now'; 131 if (s < 3600) return Math.floor(s / 60) + 'm ago'; 132 return Math.floor(s / 3600) + 'h ago'; 133 } 134 135 function buildBlockChart(blocks) { 136 if (!blocks || blocks.length === 0) return ''; 137 138 var data = blocks.slice(0, NUM_BLOCKS).reverse(); 139 140 var W = 720, H = 385; 141 var pad = { top: 58, right: 72, bottom: 82, left: 92 }; 142 var plotW = W - pad.left - pad.right; 143 var plotH = H - pad.top - pad.bottom; 144 145 var fees = data.map(function (b) { 146 return (b.extras && b.extras.totalFees ? b.extras.totalFees : 0); 147 }); 148 var txCounts = data.map(function (b) { return b.tx_count || 0; }); 149 var heights = data.map(function (b) { return b.height; }); 150 var avgRates = data.map(function (b) { 151 return b.extras && b.extras.avgFeeRate ? Math.round(b.extras.avgFeeRate) : 0; 152 }); 153 var timestamps = data.map(function (b) { return b.timestamp || 0; }); 154 155 var maxFee = Math.max.apply(null, fees) * 1.35 || 100000; 156 var maxTx = Math.max.apply(null, txCounts) * 1.35 || 1; 157 var gap = plotW / data.length; 158 var barW = gap * 0.4; 159 160 // Helper: dark pill behind a text label 161 function pill(cx, cy, text, fontSize) { 162 var charW = fontSize * 0.72; 163 var pw = text.length * charW + 8; 164 var ph = fontSize + 6; 165 return '<rect x="' + (cx - pw / 2) + '" y="' + (cy - ph + 2) + '" width="' + pw + '" height="' + ph + '" rx="4" fill="rgba(10,10,30,0.85)"/>'; 166 } 167 168 var svg = '<svg class="block-chart-svg" viewBox="0 0 ' + W + ' ' + H + '" xmlns="http://www.w3.org/2000/svg">'; 169 170 // Defs 171 svg += '<defs><linearGradient id="barGrad" x1="0" y1="0" x2="0" y2="1">' 172 + '<stop offset="0%" stop-color="#39ff9e" stop-opacity="0.9"/>' 173 + '<stop offset="100%" stop-color="#06b6d4" stop-opacity="0.5"/>' 174 + '</linearGradient>' 175 + '<filter id="whiteGlow"><feDropShadow dx="0" dy="0" stdDeviation="2" flood-color="#fff" flood-opacity="0.7"/></filter>' 176 + '</defs>'; 177 178 // Grid lines + axes 179 var gridSteps = 4; 180 for (var g = 0; g <= gridSteps; g++) { 181 var gy = pad.top + plotH - (g / gridSteps) * plotH; 182 svg += '<line x1="' + pad.left + '" y1="' + gy + '" x2="' + (W - pad.right) + '" y2="' + gy + '" ' 183 + 'stroke="rgba(255,255,255,' + (g === 0 ? '0.15' : '0.06') + ')" stroke-width="1"/>'; 184 svg += '<text x="' + (pad.left - 10) + '" y="' + (gy + 4) + '" fill="rgba(57,255,158,0.6)" ' 185 + 'font-size="9" text-anchor="end" font-family="\'Press Start 2P\', monospace">' 186 + fmtSats(maxFee * g / gridSteps) + '</text>'; 187 svg += '<text x="' + (W - pad.right + 10) + '" y="' + (gy + 4) + '" fill="rgba(255,165,0,0.6)" ' 188 + 'font-size="9" text-anchor="start" font-family="\'Press Start 2P\', monospace">' 189 + fmtK(Math.round(maxTx * g / gridSteps)) + '</text>'; 190 } 191 192 // Axis titles 193 svg += '<text x="' + pad.left + '" y="' + (pad.top - 18) + '" fill="rgba(57,255,158,0.7)" ' 194 + 'font-size="9" text-anchor="start" font-family="\'Press Start 2P\', monospace">FEES (sats)</text>'; 195 svg += '<text x="' + (W - pad.right) + '" y="' + (pad.top - 18) + '" fill="rgba(255,165,0,0.7)" ' 196 + 'font-size="9" text-anchor="end" font-family="\'Press Start 2P\', monospace">TX COUNT</text>'; 197 198 // Pre-compute positions 199 var barTops = [], dotYs = []; 200 for (var p = 0; p < data.length; p++) { 201 var bH = Math.max(2, (fees[p] / maxFee) * plotH); 202 barTops.push(pad.top + plotH - bH); 203 dotYs.push(pad.top + plotH - (txCounts[p] / maxTx) * plotH); 204 } 205 206 // --- TX layer first (renders underneath) --- 207 208 // TX line + area fill
209 var linePoints = []; 210 for (var j = 0; j < data.length; j++) { 211 linePoints.push((pad.left + j * gap + gap / 2) + ',' + dotYs[j]); 212 } 213 var areaStr = linePoints.join(' ') 214 + ' ' + (pad.left + (data.length - 1) * gap + gap / 2) + ',' + (pad.top + plotH) 215 + ' ' + (pad.left + gap / 2) + ',' + (pad.top + plotH); 216 svg += '<polygon points="' + areaStr + '" fill="rgba(255,165,0,0.05)"/>'; 217 svg += '<polyline points="' + linePoints.join(' ') + '" ' 218 + 'fill="none" stroke="rgba(255,165,0,0.12)" stroke-width="8" stroke-linejoin="round" stroke-linecap="round"/>'; 219 svg += '<polyline points="' + linePoints.join(' ') + '" ' 220 + 'fill="none" stroke="#ffa500" stroke-width="2.5" stroke-linejoin="round" stroke-linecap="round" opacity="0.9"/>'; 221 222 // TX dots + labels â pinned to top band of chart 223 for (var d = 0; d < data.length; d++) { 224 var dx = pad.left + d * gap + gap / 2; 225 var dy = dotYs[d]; 226 var txStr = fmtK(txCounts[d]); 227 var txY = pad.top + 12; 228 229 svg += '<circle cx="' + dx + '" cy="' + dy + '" r="6" fill="#1a1a2e" stroke="#ffa500" stroke-width="2.5"/>'; 230 svg += '<line x1="' + dx + '" y1="' + (txY + 4) + '" x2="' + dx + '" y2="' + (dy - 8) + '" stroke="rgba(255,165,0,0.2)" stroke-width="1" stroke-dasharray="3,3"/>'; 231 svg += pill(dx, txY, txStr, 10); 232 svg += '<text x="' + dx + '" y="' + txY + '" fill="#ffa500" ' 233 + 'font-size="10" text-anchor="middle" font-family="\'Press Start 2P\', monospace" font-weight="bold">' 234 + txStr + '</text>'; 235 } 236 237 // --- Fee layer on top (renders over TX labels) --- 238 239 // Bars + fee labels 240 for (var i = 0; i < data.length; i++) { 241 var x = pad.left + i * gap + (gap - barW) / 2; 242 var barH = Math.max(2, (fees[i] / maxFee) * plotH); 243 var y = barTops[i]; 244 var tier = getTier(avgRates[i] || 1); 245 246 svg += '<rect x="' + x + '" y="' + y + '" width="' + barW + '" height="' + barH + '" ' 247 + 'rx="4" fill="' + tier.color + '" opacity="0.7">' 248 + '<animate attributeName="height" from="0" to="' + barH + '" dur="0.5s" fill="freeze"/>' 249 + '<animate attributeName="y" from="' + (pad.top + plotH) + '" to="' + y + '" dur="0.5s" fill="freeze"/>' 250 + '</rect>'; 251 252 // Fee label with dark pill 253 var feeStr = fmtSats(fees[i]); 254 var feeLblY = y - 8; 255 svg += pill(x + barW / 2, feeLblY, feeStr, 10); 256 svg += '<text x="' + (x + barW / 2) + '" y="' + feeLblY + '" fill="' + tier.color + '" ' 257 + 'font-size="10" text-anchor="middle" font-family="\'Press Start 2P\', monospace" font-weight="bold" filter="url(#whiteGlow)">' 258 + feeStr + '</text>'; 259 260 // X-axis: Block height 261 svg += '<text x="' + (x + barW / 2) + '" y="' + (pad.top + plotH + 18) + '" fill="rgba(255,255,255,0.65)" ' 262 + 'font-size="9" text-anchor="middle" font-family="\'Press Start 2P\', monospace">' 263 + heights[i] + '</text>'; 264 265 // X-axis row 2: avg rate 266 svg += '<text x="' + (x + barW / 2) + '" y="' + (pad.top + plotH + 36) + '" fill="' + tier.color + '" ' 267 + 'font-size="8" text-anchor="middle" font-family="\'Press Start 2P\', monospace" opacity="0.85">' 268 + avgRates[i] + ' sat/vB</text>'; 269 270 // X-axis row 3: time ago 271 svg += '<text x="' + (x + barW / 2) + '" y="' + (pad.top + plotH + 52) + '" fill="rgba(255,255,255,0.4)" ' 272 + 'font-size="7" text-anchor="middle" font-family="\'Press Start 2P\', monospace">' 273 + timeAgo(timestamps[i]) + '</text>'; 274 } 275 276 // Legend 277 var legendY = pad.top + plotH + 68; 278 svg += '<rect x="' + (W / 2 - 130) + '" y="' + (legendY - 7) + '" width="14" height="10" rx="2" fill="#39ff9e" opacity="0.7"/>'; 279 svg += '<text x="' + (W / 2 - 112) + '" y="' + (legendY + 2) + '" fill="rgba(255,255,255,0.5)" ' 280 + 'font-size="8" font-family="\'Press Start 2P\', monospace">Fees (sats)</text>'; 281 svg += '<line x1="' + (W / 2 + 16) + '" y1="' + (legendY - 2) + '" ' 282 + 'x2="' + (W / 2 + 38) + '" y2="' + (legendY - 2) + '" stroke="#ffa500" stroke-width="2.5"/>'; 283 svg += '<circle cx="' + (W / 2 + 27) + '" cy="' + (legendY - 2) + '" r="3.5" fill="#1a1a2e" stroke="#ffa500" stroke-width="1.5"/>'; 284 svg += '<text x="' + (W / 2 + 44) + '" y="' + (legendY + 2) + '" fill="rgba(255,255,255,0.5)" ' 285 + 'font-size="8" font-family="\'Press Start 2P\', monospace">Tx Count</text>'; 286 287 svg += '</svg>'; 288 return svg; 289 } 290 291 /* ---- Apply data-color attributes via JS (CSP-safe, no inline style= needed) ---- */ 292 function applyColors(root) { 293 root.querySelectorAll('[data-c]').forEach(function(e) { e.style.color = e.getAttribute('data-c'); }); 294 root.querySelectorAll('[data-bg]').forEach(function(e) { e.style.background = e.getAttribute('data-bg'); }); 295 root.querySelectorAll('[data-bc]').forEach(function(e) { e.style.borderColor = e.getAttribute('data-bc'); }); 296 root.querySelectorAll('[data-bs]').forEach(function(e) { e.style.boxShadow = e.getAttribute('data-bs'); }); 297 } 298 299 /* ---- Build insight summary from block data ---- */ 300 function buildInsight(blocks) { 301 if (!blocks || blocks.length < 2) return ''; 302 var data = blocks.slice(0, NUM_BLOCKS).reverse(); 303 304 var fees = data.map(function (b) { return b.extras && b.extras.totalFees ? b.extras.totalFees : 0; }); 305 var txs = data.map(function (b) { return b.tx_count || 0; }); 306 var rates = data.map(function (b) { return b.extras && b.extras.avgFeeRate ? b.extras.avgFeeRate : 0; }); 307 308 var avgTx = txs.reduce(function (a, b) { return a + b; }, 0) / txs.length; 309 var avgRate = rates.reduce(function (a, b) { return a + b; }, 0) / rates.length; 310 var latestRate = rates[rates.length - 1]; 311 var feeTrend = fees[fees.length - 1] > fees[0] * 1.1 ? 'rising' : fees[fees.length - 1] < fees[0] * 0.9 ? 'falling' : 'stable'; 312 var txTrend = txs[txs.length - 1] > txs[0] * 1.1 ? 'rising' : txs[txs.length - 1] < txs[0] * 0.9 ? 'falling' : 'stable'; 313 314 var arrows = { rising: '\u2191', falling: '\u2193', stable: '\u2192' }; 315 var trendColors = { rising: '#ff6b35', falling: '#39ff9e', stable: '#06b6d4' }; 316 317 var signal, signalColor, signalIcon; 318 if (latestRate <= 5 && feeTrend !== 'rising') { 319 signal = 'CLEAR TO TRANSACT'; 320 signalColor = '#39ff9e'; 321 signalIcon = '\u2713'; 322 } else if (latestRate <= 15) { 323 signal = 'MODERATE ACTIVITY'; 324 signalColor = '#06b6d4'; 325 signalIcon = '\u25CF'; 326 } else if (latestRate <= 35) { 327 signal = 'ELEVATED \u2014 Consider Waiting'; 328 signalColor = '#f5a623'; 329 signalIcon = '\u25B2'; 330 } else { 331 signal = 'HIGH CONGESTION \u2014 Delay'; 332 signalColor = '#ff4444'; 333 signalIcon = '\u26A0'; 334 } 335 336 return '<div class="block-insight">' 337 + '<div class="block-insight-signal" data-c="' + signalColor + '" data-bc="' + signalColor + '40" data-bg="' + signalColor + '08">' 338 + '<span class="block-insight-icon" data-bg="' + signalColor + '20">' + signalIcon + '</span>' 339 + signal 340 + '</div>' 341 + '<div class="block-insight-stats">' 342 + '<div class="block-insight-stat">' 343 + '<span class="block-insight-stat-label">Avg Fee Rate</span>' 344 + '<span class="block-insight-stat-value" data-c="' + getTier(avgRate).color + '">' + avgRate.toFixed(1) + ' <small>sat/vB</small></span>' 345 + '</div>' 346 + '<div class="block-insight-stat">' 347 + '<span class="block-insight-stat-label">Fee Trend</span>' 348 + '<span class="block-insight-stat-value" data-c="' + trendColors[feeTrend] + '">' + arrows[feeTrend] + ' ' + feeTrend + '</span>' 349 + '</div>' 350 + '<div class="block-insight-stat">' 351 + '<span class="block-insight-stat-label">Tx Volume</span>' 352 + '<span class="block-insight-stat-value" data-c="' + trendColors[txTrend] + '">' + arrows[txTrend] + ' ' + txTrend + '</span>' 353 + '</div>' 354 + '<div class="block-insight-stat">' 355 + '<span class="block-insight-stat-label">Avg Txs/Block</span>' 356 + '<span class="block-insight-stat-value" data-c="#ffa500">' + fmtK(Math.round(avgTx)) + '</span>' 357 + '</div>' 358 + '</div></div>'; 359 } 360 361 /* ---- Active tab state ---- */ 362 var activeKey = 'fastestFee'; 363 var latestData = null; 364 365 var KEY_LABELS = { 366 fastestFee: 'Next Block', 367 halfHourFee: '~30 min', 368 hourFee: '~60 min', 369 economyFee: 'Economy', 370 minimumFee: 'Minimum' 371 }; 372 373 /* ---- Render gauge ---- */ 374 function render(data) { 375 latestData = data; 376 var sat = data[activeKey] || 1; 377 var tier = getTier(sat); 378 var angle = feeToAngle(sat); 379 380 // Needle 381 var needle = document.getElementById('feeNeedle'); 382 if (needle) needle.style.transform = 'rotate(' + angle + 'deg)'; 383 384 // Center readout 385 var readout = document.getElementById('feeReadout'); 386 if (readout) { 387 readout.textContent = sat + ' sat/vB'; 388 readout.style.color = tier.color; 389 readout.style.textShadow = '0 0 20px ' + tier.color + ',0 0 40px ' + tier.color + '80'; 390 } 391 392 // Tier badge â add context when fees are very low 393 var badge = document.getElementById('feeTierBadge'); 394 if (badge) { 395 var badgeText = 'TIER ' + tier.num + ' \u2014 ' + tier.label; 396 if (sat <= 2) badgeText += ' \u26A1 Send Now!'; 397 badge.textContent = badgeText; 398 badge.style.color = tier.color; 399 badge.style.borderColor = tier.color; 400 badge.style.background = tier.bg; 401 badge.style.boxShadow = '0 0 20px ' + tier.color + '40, inset 0 0 10px ' + tier.color + '20'; 402 } 403 404 // Stats row â highlight differences even when fees are similar 405 var stats = document.getElementById('feeStatsRow'); 406 if (stats) { 407 var allSame = data.fastestFee === data.halfHourFee && data.halfHourFee === data.hourFee 408 && data.hourFee === data.economyFee && data.economyFee === data.minimumFee; 409 var html = ''; 410 if (allSame) { 411 html += '<div class="fee-stat-uniform">All priority levels: <strong data-c="' + tier.color + '">' + sat + ' sat/vB</strong> \u2014 Mempool is nearly empty</div>'; 412 } 413 ['fastestFee', 'halfHourFee', 'hourFee', 'economyFee', 'minimumFee'].forEach(function (k) { 414 var v = data[k] || '\u2014'; 415 var t = getTier(data[k] || 1); 416 var isActive = k === activeKey ? ' fee-stat-active' : ''; 417 html += '<button class="fee-stat-btn' + isActive + '" data-feekey="' + k + '">' 418 + '<span class="fee-stat-label">' + KEY_LABELS[k] + '</span>' 419 + '<span class="fee-stat-value" data-c="' + t.color + '">' + v + ' <small>sat/vB</small></span>' 420 + '</button>'; 421 }); 422 stats.innerHTML = html; 423 applyColors(stats); 424 } 425 } 426 427 /* ---- Render block chart ---- */ 428 function renderBlockChart(blocks) { 429 var el = document.getElementById('blockChartArea'); 430 if (!el) return; 431 el.innerHTML = 432 '<div class="block-chart-title">TRANSACTIONS vs. FEES â Last ' + NUM_BLOCKS + ' Blocks</div>' 433 + buildBlockChart(blocks) 434 + buildInsight(blocks); applyColors(el); } 435 436 /* ---- Fetch fees + blocks ---- */ 437 function fetchAll() { 438 if (isFetchingFees) { 439 console.log('Fee gauge already fetching, skipping duplicate call'); 440 return; 441 } 442 isFetchingFees = true; 443 var controller = typeof AbortController !== 'undefined' ? new AbortController() : null; 444 var opts = controller ? { signal: controller.signal }
444 : {}; 445 var timeout = controller ? setTimeout(function () { controller.abort(); }, 12000) : null; 446 447 Promise.all([ 448 fetch(FEE_API, opts).then(function (r) { return r.ok ? r.json() : null; }), 449 fetch(BLOCKS_API, opts).then(function (r) { return r.ok ? r.json() : null; }) 450 ]) 451 .then(function (results) { 452 var feeData = results[0]; 453 var blocksData = results[1]; 454 if (feeData) render(feeData); 455 if (blocksData) renderBlockChart(blocksData); 456 }) 457 .catch(function () { 458 var readout = document.getElementById('feeReadout'); 459 if (readout) readout.textContent = 'Awaiting dataâ¦'; 460 }) 461 .finally(function () { 462 if (timeout) clearTimeout(timeout); 463 isFetchingFees = false; 464 }); 465 } 466 467 /* ---- Init ---- */ 468 function init() { 469 var container = document.getElementById('feeGaugeWidget'); 470 if (!container) return; 471 472 container.innerHTML = 473 '<!-- Zoltar Oracle Background -->' 474 + '<div class="zoltar-bg" aria-hidden="true">' 475 + ' <div class="zoltar-crystal-ball"></div>' 476 + ' <div class="zoltar-wisp zoltar-wisp-1"></div>' 477 + ' <div class="zoltar-wisp zoltar-wisp-2"></div>' 478 + ' <div class="zoltar-wisp zoltar-wisp-3"></div>' 479 + ' <div class="zoltar-ray zoltar-ray-1"></div>' 480 + ' <div class="zoltar-ray zoltar-ray-2"></div>' 481 + ' <div class="zoltar-ray zoltar-ray-3"></div>' 482 + ' <div class="zoltar-ray zoltar-ray-4"></div>' 483 + ' <div class="zoltar-spark zoltar-spark-1"></div>' 484 + ' <div class="zoltar-spark zoltar-spark-2"></div>' 485 + ' <div class="zoltar-spark zoltar-spark-3"></div>' 486 + ' <div class="zoltar-spark zoltar-spark-4"></div>' 487 + ' <div class="zoltar-spark zoltar-spark-5"></div>' 488 + ' <div class="zoltar-spark zoltar-spark-6"></div>' 489 + ' <div class="zoltar-spark zoltar-spark-7"></div>' 490 + ' <div class="zoltar-spark zoltar-spark-8"></div>' 491 + '</div>' 492 + '<div class="fee-gauge-header">' 493 + ' <div class="fee-gauge-title">ð® ZOLTAR SPEAKS REBALANCER</div>' 494 + ' <div class="fee-gauge-subtitle">Mempool Fee Data | Live Feed</div>' 495 + '</div>' 496 + '<div class="fee-gauge-body">' 497 + ' <div class="fee-gauge-dial">' 498 + buildGaugeSVG() 499 + ' <div class="fee-gauge-readout" id="feeReadout">Loadingâ¦</div>' 500 + ' </div>' 501 + ' <div class="fee-tier-badge" id="feeTierBadge">â</div>' 502 + '</div>' 503 + '<div class="fee-stats-row" id="feeStatsRow"></div>' 504 + '<div class="block-chart-area" id="blockChartArea"></div>' 505 + '<div class="fee-gauge-footer">' 506 + ' <svg class="fee-gauge-footer-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">' 507 + ' <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>' 508 + ' </svg>' 509 + ' <span>Data Source: mempool.space | Updated every 30 min</span>' 510 + '</div>'; 511 512 // Delegate clicks on stat buttons 513 var statsRow = document.getElementById('feeStatsRow'); 514 if (statsRow) { 515 statsRow.addEventListener('click', function (e) { 516 var btn = e.target.closest('.fee-stat-btn'); 517 if (!btn) return; 518 activeKey = btn.getAttribute('data-feekey'); 519 if (latestData) render(latestData); 520 }); 521 } 522 523 fetchAll(); 524 setInterval(fetchAll, REFRESH_MS); 525 } 526 527 if (document.readyState === 'loading') { 528 document.addEventListener('DOMContentLoaded', init); 529 } else { 530 init(); 531 } 532})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.