fix: Post-purchase holder dashboard on landing page (#150)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
058451792f
commit
e89fd4013d
11 changed files with 779 additions and 5 deletions
140
packages/ui-shared/src/composables/useHolderDashboard.ts
Normal file
140
packages/ui-shared/src/composables/useHolderDashboard.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue