diff --git a/web-app/src/components/NotificationBell.vue b/web-app/src/components/NotificationBell.vue index 816b587..0c39726 100644 --- a/web-app/src/components/NotificationBell.vue +++ b/web-app/src/components/NotificationBell.vue @@ -18,7 +18,7 @@
Position snatched
- {{ formatKrk(event.tokenAmount) }} $KRK + {{ formatTokenAmount(event.tokenAmount, 18, 2) }} $KRK {{ relativeTime(event.timestamp) }}
@@ -35,6 +35,7 @@ import { Icon } from '@iconify/vue'; import { useSnatchNotifications } from '@/composables/useSnatchNotifications'; import { useWallet } from '@/composables/useWallet'; import { DEFAULT_CHAIN_ID } from '@/config'; +import { relativeTime, formatTokenAmount } from '@/utils/helper'; const wallet = useWallet(); const chainId = wallet.account.chainId ?? DEFAULT_CHAIN_ID; @@ -43,29 +44,6 @@ const { unseenCount, allRecentSnatches, isOpen, toggle, close } = useSnatchNotif const bellRef = ref(null); -function formatKrk(raw: string): string { - try { - const val = Number(BigInt(raw)) / 1e18; - return val.toFixed(2); - } catch { - return '?'; - } -} - -function relativeTime(ts: string): string { - try { - const sec = Number(ts); - const nowSec = Math.floor(Date.now() / 1000); - const diff = nowSec - sec; - if (diff < 60) return 'just now'; - if (diff < 3600) return `${Math.floor(diff / 60)}m ago`; - if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`; - return `${Math.floor(diff / 86400)}d ago`; - } catch { - return ''; - } -} - function handleClickOutside(event: MouseEvent) { if (bellRef.value && !bellRef.value.contains(event.target as Node)) { close(); diff --git a/web-app/src/components/collapse/CollapseHistory.vue b/web-app/src/components/collapse/CollapseHistory.vue index ba5a20e..3ccf1e8 100644 --- a/web-app/src/components/collapse/CollapseHistory.vue +++ b/web-app/src/components/collapse/CollapseHistory.vue @@ -45,7 +45,7 @@ import type { Position } from '@/composables/usePositions'; import FCollapse from '@/components/fcomponents/FCollapse.vue'; import FTag from '@/components/fcomponents/FTag.vue'; import { RouterLink } from 'vue-router'; -import { compactNumber } from '@/utils/helper'; +import { compactNumber, relativeTime, formatTokenAmount } from '@/utils/helper'; import { calculateClosedPositionProfit } from 'kraiken-lib/position'; import { computed } from 'vue'; @@ -69,29 +69,12 @@ const profit = computed(() => { const payoutKrk = computed(() => { if (!props.position.payout) return null; - try { - const raw = BigInt(props.position.payout); - // payout is stored in wei (18 decimals) - const krk = Number(raw) / 1e18; - return krk.toFixed(4); - } catch { - return null; - } + return formatTokenAmount(props.position.payout); }); const relativeClosedAt = computed(() => { if (!props.position.closedAt) return null; - try { - const ts = Number(props.position.closedAt); - const nowSec = Math.floor(Date.now() / 1000); - const diffSec = nowSec - ts; - if (diffSec < 60) return 'just now'; - if (diffSec < 3600) return `${Math.floor(diffSec / 60)}m ago`; - if (diffSec < 86400) return `${Math.floor(diffSec / 3600)}h ago`; - return `${Math.floor(diffSec / 86400)}d ago`; - } catch { - return null; - } + return relativeTime(props.position.closedAt) || null; }); diff --git a/web-app/src/composables/useSnatchNotifications.ts b/web-app/src/composables/useSnatchNotifications.ts index 85ce1dd..62afee4 100644 --- a/web-app/src/composables/useSnatchNotifications.ts +++ b/web-app/src/composables/useSnatchNotifications.ts @@ -1,4 +1,4 @@ -import { ref, computed, onMounted, onUnmounted, watch } from 'vue'; +import { ref, computed, onMounted, onUnmounted } from 'vue'; import axios from 'axios'; import { getAccount, watchAccount } from '@wagmi/core'; import type { WatchAccountReturnType } from '@wagmi/core'; @@ -38,8 +38,45 @@ function setLastSeen(address: string, timestamp: string): void { localStorage.setItem(storageKey(address), timestamp); } -async function fetchUnseenSnatches(chainId: number, address: string): Promise { - const since = getLastSeen(address); +async function fetchSnatchEvents(endpoint: string, address: string, since: string | null): Promise { + const holder = address.toLowerCase(); + const whereClause = + since !== null + ? `{ holder: "${holder}", type: "snatch_out", timestamp_gt: "${since}" }` + : `{ holder: "${holder}", type: "snatch_out" }`; + const res = await axios.post( + endpoint, + { + query: `query SnatchEvents { + transactionss( + where: ${whereClause} + orderBy: "timestamp" + orderDirection: "desc" + limit: 20 + ) { + items { id timestamp tokenAmount ethAmount txHash } + } + }`, + }, + { timeout: GRAPHQL_TIMEOUT_MS } + ); + + const errors = res.data?.errors; + if (Array.isArray(errors) && errors.length > 0) { + logger.info('useSnatchNotifications GraphQL errors', errors); + return []; + } + return (res.data?.data?.transactionss?.items ?? []) as SnatchEvent[]; +} + +async function refresh(chainId: number): Promise { + const account = getAccount(config as Config); + if (!account.address) { + unseenSnatches.value = []; + allRecentSnatches.value = []; + return; + } + let endpoint: string; try { endpoint = resolveGraphqlEndpoint(chainId); @@ -50,57 +87,28 @@ async function fetchUnseenSnatches(chainId: number, address: string): Promise 0) { - logger.info('useSnatchNotifications GraphQL errors', errors); - return; - } - - const items: SnatchEvent[] = res.data?.data?.transactionss?.items ?? []; - unseenSnatches.value = items; - allRecentSnatches.value = items; + const since = getLastSeen(account.address); + const [unseen, all] = await Promise.all([ + fetchSnatchEvents(endpoint, account.address, since), + fetchSnatchEvents(endpoint, account.address, null), + ]); + unseenSnatches.value = unseen; + allRecentSnatches.value = all; } catch (err) { logger.info('useSnatchNotifications fetch failed', err); } } -async function refresh(chainId: number): Promise { - const account = getAccount(config as Config); - if (!account.address) { - unseenSnatches.value = []; - allRecentSnatches.value = []; - return; - } - await fetchUnseenSnatches(chainId, account.address); -} - function markSeen(): void { const account = getAccount(config as Config); - if (!account.address || allRecentSnatches.value.length === 0) return; + if (!account.address) return; - // Use the most recent timestamp from current results - const latestTs = allRecentSnatches.value[0]?.timestamp; + // Advance last-seen to the most recent event timestamp + const latestTs = allRecentSnatches.value[0]?.timestamp ?? unseenSnatches.value[0]?.timestamp; if (latestTs) { setLastSeen(account.address, latestTs); } + // Clear badge; allRecentSnatches stays populated so the panel still shows history unseenSnatches.value = []; } @@ -146,11 +154,6 @@ export function useSnatchNotifications(chainId: number = DEFAULT_CHAIN_ID) { } }); - // Re-fetch when bell is closed to clear unseen after markSeen - watch(isOpen, async newVal => { - if (!newVal) return; - }); - return { unseenCount, unseenSnatches, diff --git a/web-app/src/utils/helper.ts b/web-app/src/utils/helper.ts index 7fde6c4..50cc08a 100644 --- a/web-app/src/utils/helper.ts +++ b/web-app/src/utils/helper.ts @@ -46,6 +46,28 @@ export function InsertCommaNumber(number: number) { return formattedWithOptions; } +export function relativeTime(timestamp: string | number): string { + try { + const sec = typeof timestamp === 'string' ? Number(timestamp) : timestamp; + const nowSec = Math.floor(Date.now() / 1000); + const diff = nowSec - sec; + if (diff < 60) return 'just now'; + if (diff < 3600) return `${Math.floor(diff / 60)}m ago`; + if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`; + return `${Math.floor(diff / 86400)}d ago`; + } catch { + return ''; + } +} + +export function formatTokenAmount(raw: string, decimals: number = 18, digits: number = 4): string { + try { + return Number(formatUnits(BigInt(raw), decimals)).toFixed(digits); + } catch { + return '?'; + } +} + export function formatBigIntDivision(nominator: bigint, denominator: bigint, _digits: number = 2) { if (!nominator) { return 0;