import { ref, reactive, computed } from 'vue'; import axios from 'axios'; import { watchChainId } from '@wagmi/core'; import type { Config } from '@wagmi/core'; import { config } from '@/wagmi'; import logger from '@/utils/logger'; import type { WatchBlocksReturnType } from 'viem'; import { weiToNumber } from 'kraiken-lib/format'; import { minStake as stakeMinStake } from '@/contracts/stake'; import { DEFAULT_CHAIN_ID } from '@/config'; import { createRetryManager, formatGraphqlError, resolveGraphqlEndpoint } from '@/utils/graphqlRetry'; const demo = sessionStorage.getItem('demo') === 'true'; const GRAPHQL_TIMEOUT_MS = 15_000; interface StatsRecord { burnNextHourProjected: string; burnedLastDay: string; burnedLastWeek: string; id: string; minStake: string; mintNextHourProjected: string; mintedLastDay: string; mintedLastWeek: string; outstandingStake: string; kraikenTotalSupply: string; stakeTotalSupply: string; ringBufferPointer: number; totalBurned: string; totalMinted: string; } const rawStatsCollections = ref>([]); const loading = ref(false); const initialized = ref(false); const statsError = ref(null); const activeChainId = ref(DEFAULT_CHAIN_ID); const retryManager = createRetryManager(loadStats, activeChainId); export async function loadStatsCollection(chainId: number, endpointOverride?: string) { const endpoint = resolveGraphqlEndpoint(chainId, endpointOverride); logger.info(`loadStatsCollection for chainId: ${chainId}`); const res = await axios.post( endpoint, { query: `query StatsQuery { stats(id: "0x01") { minStake burnNextHourProjected burnedLastDay burnedLastWeek id mintNextHourProjected mintedLastDay mintedLastWeek outstandingStake kraikenTotalSupply stakeTotalSupply ringBufferPointer totalBurned totalMinted } }`, }, { timeout: GRAPHQL_TIMEOUT_MS } ); const errors = res.data?.errors; if (Array.isArray(errors) && errors.length > 0) { throw new Error(errors.map((err: unknown) => (err as { message?: string })?.message ?? 'GraphQL error').join(', ')); } const stats = res.data?.data?.stats as StatsRecord | undefined; if (!stats) { throw new Error('Stats entity not found in GraphQL response'); } return [{ ...stats, kraikenTotalSupply: stats.kraikenTotalSupply }]; } const profit7d = computed(() => { if (!statsCollection.value) { return 0n; } const mintedLastWeek = BigInt(statsCollection.value.mintedLastWeek); const burnedLastWeek = BigInt(statsCollection.value.burnedLastWeek); const totalMinted = BigInt(statsCollection.value.totalMinted); const totalBurned = BigInt(statsCollection.value.totalBurned); const denominator = totalMinted - totalBurned; if (denominator === 0n) { return 0n; } return (mintedLastWeek - burnedLastWeek) / denominator; }); const nettoToken7d = computed(() => { if (!statsCollection.value) { return 0n; } return BigInt(statsCollection.value.mintedLastWeek) - BigInt(statsCollection.value.burnedLastWeek); }); const statsCollection = computed(() => { if (rawStatsCollections.value?.length > 0) { return rawStatsCollections.value[0]; } else { return undefined; } }); const outstandingStake = computed(() => { if (demo) { // outStandingStake = 1990000000000000000000000n; return 2000000000000000000000000n; } if (rawStatsCollections.value?.length > 0) { return BigInt(rawStatsCollections.value[0].outstandingStake); } else { return 0n; } }); const kraikenTotalSupply = computed(() => { if (rawStatsCollections.value?.length > 0) { return BigInt(rawStatsCollections.value[0].kraikenTotalSupply); } else { return 0n; } }); const stakeTotalSupply = computed(() => { if (rawStatsCollections.value?.length > 0) { return BigInt(rawStatsCollections.value[0].stakeTotalSupply); } else { return 0n; } }); const minStake = computed(() => { if (rawStatsCollections.value?.length > 0) { return BigInt(rawStatsCollections.value[0].minStake); } else { return 0n; } }); //Total Supply Change / 7d=mintedLastWeek−burnedLastWeek const totalSupplyChange7d = computed(() => { if (rawStatsCollections.value?.length > 0) { return BigInt(rawStatsCollections.value[0].mintedLastWeek) - BigInt(rawStatsCollections.value[0].burnedLastWeek); } else { return 0n; } }); //totalsupply Change7d / harbtotalsupply const inflation7d = computed(() => { if (rawStatsCollections.value?.length > 0 && BigInt(rawStatsCollections.value[0].kraikenTotalSupply) > 0n) { return BigInt(rawStatsCollections.value[0].mintedLastWeek) - BigInt(rawStatsCollections.value[0].burnedLastWeek); } else { return 0n; } }); const stakeableSupply = computed(() => { if (rawStatsCollections.value?.length > 0 && BigInt(rawStatsCollections.value[0].kraikenTotalSupply) > 0n) { return stakeTotalSupply.value / 5n; } else { return 0n; } }); //maxSlots const maxSlots = computed(() => { if (rawStatsCollections.value?.length > 0 && BigInt(rawStatsCollections.value[0].kraikenTotalSupply) > 0n) { return (weiToNumber(stakeTotalSupply.value, 18) * 0.2) / 100; } else { return 0; } }); const claimedSlots = computed(() => { if (stakeTotalSupply.value > 0n) { const stakeableSupplyNumber = weiToNumber(stakeableSupply.value, 18); const outstandingStakeNumber = weiToNumber(outstandingStake.value, 18); return (outstandingStakeNumber / stakeableSupplyNumber) * maxSlots.value; } else { return 0; } }); export async function loadStats(chainId?: number) { loading.value = true; const targetChainId = typeof chainId === 'number' ? chainId : (activeChainId.value ?? DEFAULT_CHAIN_ID); activeChainId.value = targetChainId; let endpoint: string; try { endpoint = resolveGraphqlEndpoint(targetChainId); } catch (error) { rawStatsCollections.value = []; statsError.value = error instanceof Error ? error.message : 'GraphQL endpoint not configured for this chain.'; retryManager.clear(); retryManager.reset(); loading.value = false; initialized.value = true; return; } try { rawStatsCollections.value = await loadStatsCollection(targetChainId, endpoint); statsError.value = null; retryManager.reset(); retryManager.clear(); stakeMinStake.value = minStake.value; } catch (error) { rawStatsCollections.value = []; statsError.value = formatGraphqlError(error); stakeMinStake.value = 0n; retryManager.schedule(); } finally { loading.value = false; initialized.value = true; } } import { onMounted, onUnmounted } from 'vue'; let unwatch: WatchBlocksReturnType | null = null; export function useStatCollection(chainId: number = DEFAULT_CHAIN_ID) { activeChainId.value = chainId; onMounted(async () => { //initial loading stats if (rawStatsCollections.value?.length === 0 && !loading.value) { await loadStats(activeChainId.value); } }); if (!unwatch) { //chain Switch reload stats for other chain unwatch = watchChainId(config as Config, { async onChange(nextChainId) { const resolvedChainId = nextChainId ?? DEFAULT_CHAIN_ID; activeChainId.value = resolvedChainId; await loadStats(resolvedChainId); }, }); } onUnmounted(() => { retryManager.clear(); if (unwatch) { unwatch(); } }); return reactive({ profit7d, nettoToken7d, inflation7d, outstandingStake, kraikenTotalSupply, stakeTotalSupply, totalSupplyChange7d, initialized, maxSlots, stakeableSupply, claimedSlots, statsError, loading, minStake, }); }