fix: Post-purchase holder dashboard on landing page (#150)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
openhands 2026-02-24 12:34:36 +00:00
parent 058451792f
commit e89fd4013d
11 changed files with 779 additions and 5 deletions

View file

@ -0,0 +1,140 @@
import { ref, computed, onMounted, onUnmounted, type Ref } from 'vue';
const POLL_INTERVAL_MS = 30_000;
function formatTokenAmount(rawWei: string, decimals = 18): number {
try {
const big = BigInt(rawWei);
const divisor = 10 ** decimals;
return Number(big) / divisor;
} catch {
return 0;
}
}
export function useHolderDashboard(address: Ref<string>, graphqlUrl: string | Ref<string> = '/api/graphql') {
const holderBalance = ref<string>('0');
const holderTotalEthSpent = ref<string>('0');
const holderTotalTokensAcquired = ref<string>('0');
const currentPriceWei = ref<string | null>(null);
const lastEthReserve = ref<string>('0');
const kraikenTotalSupply = ref<string>('0');
const loading = ref(false);
const error = ref<string | null>(null);
let pollTimer: ReturnType<typeof setInterval> | null = null;
function resolveUrl(): string {
return typeof graphqlUrl === 'string' ? graphqlUrl : graphqlUrl.value;
}
async function fetchData() {
const addr = address.value?.toLowerCase();
if (!addr) return;
loading.value = true;
error.value = null;
try {
const res = await fetch(resolveUrl(), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: `query HolderDashboard {
holders(address: "${addr}") {
balance
totalEthSpent
totalTokensAcquired
}
statss(where: { id: "0x01" }) {
items {
kraikenTotalSupply
lastEthReserve
currentPriceWei
}
}
}`,
}),
});
const json = await res.json();
if (Array.isArray(json?.errors) && json.errors.length > 0) {
const msgs = json.errors.map((e: { message?: string }) => e.message ?? 'GraphQL error').join(', ');
throw new Error(msgs);
}
const holder = json?.data?.holders;
holderBalance.value = holder?.balance ?? '0';
holderTotalEthSpent.value = holder?.totalEthSpent ?? '0';
holderTotalTokensAcquired.value = holder?.totalTokensAcquired ?? '0';
const statsItems = json?.data?.statss?.items;
const statsRow = Array.isArray(statsItems) && statsItems.length > 0 ? statsItems[0] : null;
currentPriceWei.value = statsRow?.currentPriceWei ?? null;
lastEthReserve.value = statsRow?.lastEthReserve ?? '0';
kraikenTotalSupply.value = statsRow?.kraikenTotalSupply ?? '0';
} catch (err) {
error.value = err instanceof Error ? err.message : 'Failed to load holder data';
} finally {
loading.value = false;
}
}
const balanceKrk = computed(() => formatTokenAmount(holderBalance.value));
const ethBacking = computed(() => {
const balance = balanceKrk.value;
const reserve = formatTokenAmount(lastEthReserve.value);
const totalSupply = formatTokenAmount(kraikenTotalSupply.value);
if (totalSupply === 0) return 0;
return balance * (reserve / totalSupply);
});
const avgCostBasis = computed(() => {
const spent = formatTokenAmount(holderTotalEthSpent.value);
const acquired = formatTokenAmount(holderTotalTokensAcquired.value);
if (acquired === 0) return 0;
return spent / acquired;
});
const currentPriceEth = computed(() => {
if (!currentPriceWei.value) return 0;
return formatTokenAmount(currentPriceWei.value);
});
const unrealizedPnlEth = computed(() => {
const basis = avgCostBasis.value;
if (basis === 0) return 0;
return (currentPriceEth.value - basis) * balanceKrk.value;
});
const unrealizedPnlPercent = computed(() => {
const basis = avgCostBasis.value;
if (basis === 0) return 0;
return (currentPriceEth.value / basis - 1) * 100;
});
onMounted(async () => {
await fetchData();
pollTimer = setInterval(() => void fetchData(), POLL_INTERVAL_MS);
});
onUnmounted(() => {
if (pollTimer) {
clearInterval(pollTimer);
pollTimer = null;
}
});
return {
loading,
error,
balanceKrk,
avgCostBasis,
currentPriceEth,
unrealizedPnlEth,
unrealizedPnlPercent,
ethBacking,
refresh: fetchData,
};
}