fix: show USD values in LiveStats, fix floor price truncation (#171)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
openhands 2026-02-22 11:02:10 +00:00
parent 6f7e07b4fc
commit c374e29817

View file

@ -3,7 +3,8 @@
<div class="stats-grid" :class="{ 'has-floor': showFloorPrice }">
<div class="stat-item">
<div class="stat-label">ETH Reserve</div>
<div class="stat-value">{{ ethReserveAmount }}</div>
<div class="stat-value">{{ ethReserveDisplay }}</div>
<div v-if="ethReserveSecondary" class="stat-secondary">{{ ethReserveSecondary }}</div>
<div v-if="growthIndicator !== null" class="growth-badge" :class="growthClass">{{ growthIndicator }}</div>
<svg v-if="ethReserveSpark.length > 1" class="sparkline" viewBox="0 0 80 24" preserveAspectRatio="none">
<polyline :points="toSvgPoints(ethReserveSpark)" fill="none" stroke="rgba(96,165,250,0.5)" stroke-width="1.5" stroke-linejoin="round" stroke-linecap="round" />
@ -11,11 +12,13 @@
</div>
<div class="stat-item">
<div class="stat-label">ETH / Token</div>
<div class="stat-value">{{ ethPerToken }}</div>
<div class="stat-value">{{ ethPerTokenDisplay }}</div>
<div v-if="ethPerTokenSecondary" class="stat-secondary">{{ ethPerTokenSecondary }}</div>
</div>
<div v-if="showFloorPrice" class="stat-item">
<div class="stat-label">Floor Price</div>
<div class="stat-value">{{ floorPriceAmount }}</div>
<div class="stat-value">{{ floorPriceDisplay }}</div>
<div v-if="floorPriceSecondary" class="stat-secondary">{{ floorPriceSecondary }}</div>
<div v-if="floorDistanceText" class="floor-distance">{{ floorDistanceText }}</div>
</div>
<div class="stat-item">
@ -78,7 +81,11 @@ interface Stats {
const stats = ref<Stats | null>(null);
const error = ref(false);
const ethUsdPrice = ref<number | null>(null);
let refreshInterval: number | null = null;
let ethPriceInterval: number | null = null;
let ethPriceCacheTime = 0;
const ETH_PRICE_CACHE_MS = 5 * 60 * 1000; // 5 minutes
// Helper: safely convert a Wei BigInt string to ETH float
function weiToEth(wei: string | null | undefined): number {
@ -191,10 +198,53 @@ const holderGrowthClass = computed(() => {
return 'growth-flat';
});
const ethReserveAmount = computed(() => {
if (!stats.value) return '0.00 ETH';
// Format tiny ETH values without truncating to zero
function formatSmallEth(eth: number): string {
if (eth === 0) return '0 ETH';
if (eth >= 1) return `${eth.toFixed(2)} ETH`;
if (eth >= 0.01) return `${eth.toFixed(4)} ETH`;
if (eth >= 0.0001) return `${eth.toFixed(6)} ETH`;
const s = eth.toPrecision(4);
return `${s} ETH`;
}
// Format USD with tiered precision
function formatUsd(usd: number): string {
if (usd >= 1000) return `$${(usd / 1000).toFixed(1)}k`;
if (usd >= 1) return `$${usd.toFixed(2)}`;
if (usd >= 0.01) return `$${usd.toFixed(3)}`;
return `$${usd.toFixed(4)}`;
}
// Fetch ETH/USD price from CoinGecko with 5-min cache
async function fetchEthPrice() {
const now = Date.now();
if (ethUsdPrice.value !== null && (now - ethPriceCacheTime) < ETH_PRICE_CACHE_MS) return;
try {
const resp = await fetch('https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=usd');
if (!resp.ok) throw new Error('ETH price fetch failed');
const data = await resp.json();
if (data.ethereum?.usd) {
ethUsdPrice.value = data.ethereum.usd;
ethPriceCacheTime = now;
}
} catch {
// Keep existing cached price or null; ETH fallback will be used
}
}
// Primary/secondary display helpers
const ethReserveDisplay = computed(() => {
if (!stats.value) return '$0.00';
const eth = weiToEth(stats.value.lastEthReserve);
return `${eth.toFixed(2)} ETH`;
if (ethUsdPrice.value) return formatUsd(eth * ethUsdPrice.value);
return formatSmallEth(eth);
});
const ethReserveSecondary = computed((): string | null => {
if (!stats.value || !ethUsdPrice.value) return null;
const eth = weiToEth(stats.value.lastEthReserve);
return `(${eth.toFixed(2)} ETH)`;
});
// Growth indicator: null = hide (< 7 days history or missing)
@ -222,9 +272,22 @@ const ethPerToken = computed(() => {
const supply = Number(stats.value.kraikenTotalSupply) / 1e18;
if (supply === 0) return '—';
const ratio = reserve / supply;
if (ratio >= 0.01) return `${ratio.toFixed(4)} ETH`;
if (ratio >= 0.0001) return `${ratio.toFixed(6)} ETH`;
return `${ratio.toExponential(2)} ETH`;
return ratio;
});
const ethPerTokenDisplay = computed(() => {
if (!stats.value) return '—';
const ratio = ethPerToken.value;
if (typeof ratio === 'string') return ratio;
if (ethUsdPrice.value) return formatUsd(ratio * ethUsdPrice.value);
return formatSmallEth(ratio);
});
const ethPerTokenSecondary = computed((): string | null => {
if (!stats.value || !ethUsdPrice.value) return null;
const ratio = ethPerToken.value;
if (typeof ratio === 'string') return null;
return `(${formatSmallEth(ratio).replace(' ETH', '')} ETH)`;
});
// Net supply change indicator (7d)
@ -292,8 +355,19 @@ const showFloorPrice = computed(() => {
const floorPriceAmount = computed(() => {
if (!showFloorPrice.value || !stats.value?.floorPriceWei) return null;
const eth = weiToEth(stats.value.floorPriceWei);
return `${eth.toFixed(4)} ETH`;
return weiToEth(stats.value.floorPriceWei);
});
const floorPriceDisplay = computed(() => {
if (floorPriceAmount.value == null) return null;
const eth = floorPriceAmount.value;
if (ethUsdPrice.value) return formatUsd(eth * ethUsdPrice.value);
return formatSmallEth(eth);
});
const floorPriceSecondary = computed((): string | null => {
if (floorPriceAmount.value == null || !ethUsdPrice.value) return null;
return `(${formatSmallEth(floorPriceAmount.value)})`;
});
const floorDistanceText = computed((): string | null => {
@ -401,13 +475,14 @@ async function fetchStats() {
onMounted(() => {
fetchStats();
fetchEthPrice();
refreshInterval = window.setInterval(fetchStats, 30000); // Refresh every 30 seconds
ethPriceInterval = window.setInterval(fetchEthPrice, ETH_PRICE_CACHE_MS); // Refresh ETH price every 5 min
});
onUnmounted(() => {
if (refreshInterval) {
clearInterval(refreshInterval);
}
if (refreshInterval) clearInterval(refreshInterval);
if (ethPriceInterval) clearInterval(ethPriceInterval);
});
</script>
@ -489,6 +564,11 @@ onUnmounted(() => {
&.growth-flat
color: rgba(240, 240, 240, 0.45)
.stat-secondary
font-size: 12px
color: rgba(240, 240, 240, 0.45)
letter-spacing: 0.3px
.floor-distance
font-size: 11px
color: rgba(240, 240, 240, 0.5)