harb/web-app/src/composables/useStatCollection.ts

305 lines
7.9 KiB
TypeScript
Raw Normal View History

2025-09-23 14:18:04 +02:00
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";
2025-09-24 09:41:28 +02:00
const GRAPHQL_TIMEOUT_MS = 15_000;
const RETRY_BASE_DELAY_MS = 1_500;
const RETRY_MAX_DELAY_MS = 60_000;
2025-09-23 19:24:05 +02:00
interface StatsRecord {
burnNextHourProjected: string;
burnedLastDay: string;
burnedLastWeek: string;
2025-09-23 14:18:04 +02:00
id: string;
2025-09-23 19:24:05 +02:00
mintNextHourProjected: string;
mintedLastDay: string;
mintedLastWeek: string;
outstandingStake: string;
kraikenTotalSupply: string;
stakeTotalSupply: string;
2025-09-23 14:18:04 +02:00
ringBufferPointer: number;
2025-09-23 19:24:05 +02:00
totalBurned: string;
totalMinted: string;
2025-09-23 14:18:04 +02:00
}
2025-09-23 19:24:05 +02:00
const rawStatsCollections = ref<Array<StatsRecord>>([]);
2025-09-23 14:18:04 +02:00
const loading = ref(false);
const initialized = ref(false);
2025-09-24 09:41:28 +02:00
const statsError = ref<string | null>(null);
const statsRetryDelayMs = ref(RETRY_BASE_DELAY_MS);
let statsRetryTimer: number | null = null;
2025-09-23 14:18:04 +02:00
2025-09-24 09:41:28 +02:00
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;
2025-09-23 14:18:04 +02:00
}
2025-09-24 09:41:28 +02:00
const delay = statsRetryDelayMs.value;
statsRetryTimer = window.setTimeout(async () => {
statsRetryTimer = null;
await loadStats();
}, delay);
statsRetryDelayMs.value = Math.min(statsRetryDelayMs.value * 2, RETRY_MAX_DELAY_MS);
}
2025-09-23 14:18:04 +02:00
2025-09-24 09:41:28 +02:00
export async function loadStatsCollection(endpoint: string) {
logger.info(`loadStatsCollection for chain: ${chainData.value?.path}`);
const res = await axios.post(endpoint, {
2025-09-23 19:24:05 +02:00
query: `query StatsQuery {
stats(id: "0x01") {
burnNextHourProjected
burnedLastDay
burnedLastWeek
id
mintNextHourProjected
mintedLastDay
mintedLastWeek
outstandingStake
kraikenTotalSupply
stakeTotalSupply
ringBufferPointer
totalBurned
totalMinted
}
}`,
2025-09-24 09:41:28 +02:00
}, { 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(", "));
}
2025-09-23 19:24:05 +02:00
const stats = res.data?.data?.stats as StatsRecord | undefined;
if (!stats) {
2025-09-24 09:41:28 +02:00
throw new Error("Stats entity not found in GraphQL response");
2025-09-23 19:24:05 +02:00
}
return [{ ...stats, kraikenTotalSupply: stats.kraikenTotalSupply }];
2025-09-23 14:18:04 +02:00
}
const profit7d = computed(() => {
if (!statsCollection.value) {
return 0n;
}
2025-09-23 19:24:05 +02:00
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;
2025-09-23 14:18:04 +02:00
});
const nettoToken7d = computed(() => {
if (!statsCollection.value) {
return 0n;
}
2025-09-23 19:24:05 +02:00
return BigInt(statsCollection.value.mintedLastWeek) - BigInt(statsCollection.value.burnedLastWeek);
2025-09-23 14:18:04 +02:00
});
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;
}
});
2025-09-23 19:24:05 +02:00
const kraikenTotalSupply = computed(() => {
2025-09-23 14:18:04 +02:00
if (rawStatsCollections.value?.length > 0) {
2025-09-23 19:24:05 +02:00
return BigInt(rawStatsCollections.value[0].kraikenTotalSupply);
2025-09-23 14:18:04 +02:00
} else {
return 0n;
}
});
const stakeTotalSupply = computed(() => {
if (rawStatsCollections.value?.length > 0) {
return BigInt(rawStatsCollections.value[0].stakeTotalSupply);
} else {
return 0n;
}
});
//Total Supply Change / 7d=mintedLastWeekburnedLastWeek
const totalSupplyChange7d = computed(() => {
if (rawStatsCollections.value?.length > 0) {
2025-09-23 19:24:05 +02:00
return (
BigInt(rawStatsCollections.value[0].mintedLastWeek) -
BigInt(rawStatsCollections.value[0].burnedLastWeek)
);
2025-09-23 14:18:04 +02:00
} else {
return 0n;
}
});
//totalsupply Change7d / harbtotalsupply
const inflation7d = computed(() => {
2025-09-23 19:24:05 +02:00
if (rawStatsCollections.value?.length > 0 && BigInt(rawStatsCollections.value[0].kraikenTotalSupply) > 0n) {
return (
BigInt(rawStatsCollections.value[0].mintedLastWeek) -
BigInt(rawStatsCollections.value[0].burnedLastWeek)
);
2025-09-23 14:18:04 +02:00
} else {
return 0n;
}
});
const stakeableSupply = computed(() => {
2025-09-23 19:24:05 +02:00
if (rawStatsCollections.value?.length > 0 && BigInt(rawStatsCollections.value[0].kraikenTotalSupply) > 0n) {
2025-09-23 14:18:04 +02:00
console.log("rawStatsCollections.value[0]", rawStatsCollections.value[0]);
return stakeTotalSupply.value / 5n;
} else {
return 0n;
}
});
//maxSlots
const maxSlots = computed(() => {
2025-09-23 19:24:05 +02:00
if (rawStatsCollections.value?.length > 0 && BigInt(rawStatsCollections.value[0].kraikenTotalSupply) > 0n) {
2025-09-23 14:18:04 +02:00
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;
2025-09-24 09:41:28 +02:00
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;
}
2025-09-23 14:18:04 +02:00
2025-09-24 09:41:28 +02:00
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;
}
2025-09-23 14:18:04 +02:00
}
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(() => {
2025-09-24 09:41:28 +02:00
clearStatsRetryTimer();
2025-09-23 14:18:04 +02:00
unwatch();
});
return reactive({
profit7d,
nettoToken7d,
inflation7d,
outstandingStake,
2025-09-23 19:24:05 +02:00
kraikenTotalSupply,
2025-09-23 14:18:04 +02:00
stakeTotalSupply,
totalSupplyChange7d,
initialized,
maxSlots,
stakeableSupply,
claimedSlots,
2025-09-24 09:41:28 +02:00
statsError,
loading,
2025-09-23 14:18:04 +02:00
});
}