harb/web-app/src/composables/useStatCollection.ts
openhands 8c43d3890c fix: Move BigInt formatting functions from helper.ts to kraiken-lib/format (#246)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 23:00:58 +00:00

274 lines
7.6 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<Array<StatsRecord>>([]);
const loading = ref(false);
const initialized = ref(false);
const statsError = ref<string | null>(null);
const activeChainId = ref<number>(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=mintedLastWeekburnedLastWeek
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,
});
}