import { ref, onMounted, onUnmounted, reactive, computed } from "vue"; import axios from "axios"; import { chainData } from "./useWallet"; import { watchBlocks, watchChainId } from "@wagmi/core"; import { config } from "@/wagmi"; import logger from "@/utils/logger"; import type { WatchBlocksReturnType } from "viem"; import { bigInt2Number } from "@/utils/helper"; const demo = sessionStorage.getItem("demo") === "true"; const GRAPHQL_TIMEOUT_MS = 15_000; const RETRY_BASE_DELAY_MS = 1_500; const RETRY_MAX_DELAY_MS = 60_000; interface StatsRecord { burnNextHourProjected: string; burnedLastDay: string; burnedLastWeek: string; id: 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 statsRetryDelayMs = ref(RETRY_BASE_DELAY_MS); let statsRetryTimer: number | null = null; function formatGraphqlError(error: unknown): string { if (axios.isAxiosError(error)) { const responseErrors = (error.response?.data as any)?.errors; if (Array.isArray(responseErrors) && responseErrors.length > 0) { return responseErrors.map((err: any) => err?.message ?? "GraphQL error").join(", "); } if (error.response?.status) { return `GraphQL request failed with status ${error.response.status}`; } if (error.message) { return error.message; } } if (error instanceof Error && error.message) { return error.message; } return "Unknown GraphQL error"; } function clearStatsRetryTimer() { if (statsRetryTimer !== null) { clearTimeout(statsRetryTimer); statsRetryTimer = null; } } function scheduleStatsRetry() { if (typeof window === "undefined") { return; } if (statsRetryTimer !== null) { return; } const delay = statsRetryDelayMs.value; statsRetryTimer = window.setTimeout(async () => { statsRetryTimer = null; await loadStats(); }, delay); statsRetryDelayMs.value = Math.min(statsRetryDelayMs.value * 2, RETRY_MAX_DELAY_MS); } export async function loadStatsCollection(endpoint: string) { logger.info(`loadStatsCollection for chain: ${chainData.value?.path}`); const res = await axios.post(endpoint, { query: `query StatsQuery { stats(id: "0x01") { 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: any) => err?.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; } }); //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) { console.log("rawStatsCollections.value[0]", rawStatsCollections.value[0]); return stakeTotalSupply.value / 5n; } else { return 0n; } }); //maxSlots const maxSlots = computed(() => { if (rawStatsCollections.value?.length > 0 && BigInt(rawStatsCollections.value[0].kraikenTotalSupply) > 0n) { console.log("rawStatsCollections.value[0]", rawStatsCollections.value[0]); return (bigInt2Number(stakeTotalSupply.value, 18) * 0.2) / 100; } else { return 0; } }); const claimedSlots = computed(() => { if (stakeTotalSupply.value > 0n) { const stakeableSupplyNumber = bigInt2Number(stakeableSupply.value, 18); const outstandingStakeNumber = bigInt2Number(outstandingStake.value, 18); return (outstandingStakeNumber / stakeableSupplyNumber) * maxSlots.value; } else { return 0; } }); export async function loadStats() { loading.value = true; const endpoint = chainData.value?.graphql?.trim(); if (!endpoint) { rawStatsCollections.value = []; statsError.value = "GraphQL endpoint not configured for this chain."; clearStatsRetryTimer(); statsRetryDelayMs.value = RETRY_BASE_DELAY_MS; loading.value = false; initialized.value = true; return; } try { rawStatsCollections.value = await loadStatsCollection(endpoint); statsError.value = null; statsRetryDelayMs.value = RETRY_BASE_DELAY_MS; clearStatsRetryTimer(); } catch (error) { console.warn("[stats] loadStats() failed", error); rawStatsCollections.value = []; statsError.value = formatGraphqlError(error); scheduleStatsRetry(); } finally { loading.value = false; initialized.value = true; } } let unwatch: any = null; let unwatchBlock: WatchBlocksReturnType; const loadingWatchBlock = ref(false); export function useStatCollection() { onMounted(async () => { //initial loading stats if (rawStatsCollections.value?.length === 0 && !loading.value) { await loadStats(); } }); if (!unwatch) { console.log("watchChain"); //chain Switch reload stats for other chain unwatch = watchChainId(config as any, { async onChange(chainId) { await loadStats(); }, }); // const unwatchBlock = watchBlocks(config as any, { // async onBlock(block) { // console.log('Block changed!', block) // await loadStats(); // }, // }) } onUnmounted(() => { clearStatsRetryTimer(); unwatch(); }); return reactive({ profit7d, nettoToken7d, inflation7d, outstandingStake, kraikenTotalSupply, stakeTotalSupply, totalSupplyChange7d, initialized, maxSlots, stakeableSupply, claimedSlots, statsError, loading, }); }