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;