No KRK yet
diff --git a/landing/src/router/index.ts b/landing/src/router/index.ts
index 7d65c76..5e52a93 100644
--- a/landing/src/router/index.ts
+++ b/landing/src/router/index.ts
@@ -20,6 +20,11 @@ const router = createRouter({
name: 'mixed',
component: () => import('../views/HomeViewMixed.vue'),
},
+ {
+ path: '/wallet/:address',
+ name: 'wallet',
+ component: () => import('../views/HolderDashboardView.vue'),
+ },
{
path: '/docs',
name: 'Docs',
diff --git a/landing/src/views/HolderDashboardView.vue b/landing/src/views/HolderDashboardView.vue
new file mode 100644
index 0000000..6a48557
--- /dev/null
+++ b/landing/src/views/HolderDashboardView.vue
@@ -0,0 +1,288 @@
+
+
+
+
+
+
+
+
+
Loading wallet data…
+
+
⚠ {{ error }}
+
+
+
+
Unrealized P&L
+
+ {{ unrealizedPnlEth >= 0 ? '+' : '' }}{{ formatEth(unrealizedPnlEth) }} ETH
+
+
+ {{ unrealizedPnlPercent >= 0 ? '+' : '' }}{{ unrealizedPnlPercent.toFixed(1) }}%
+
+
+ Avg cost: {{ formatEth(avgCostBasis) }} ETH/KRK · Current: {{ formatEth(currentPriceEth) }} ETH/KRK
+
+
+
+
+
+
+
KRK Balance
+
{{ formatKrk(balanceKrk) }}
+
KRK
+
+
+
ETH Backing
+
{{ formatEth(ethBacking) }}
+
ETH
+
+
+
+
+
+
+
+
+
+
+
diff --git a/package-lock.json b/package-lock.json
index 65899d0..e86f862 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,5 +1,5 @@
{
- "name": "harb-worktree-196",
+ "name": "harb-worktree-150",
"lockfileVersion": 3,
"requires": true,
"packages": {
@@ -63,6 +63,7 @@
"name": "vue-kraiken",
"version": "0.0.0",
"dependencies": {
+ "@harb/ui-shared": "*",
"@harb/web3": "*",
"@tanstack/vue-query": "^5.92.9",
"@wagmi/vue": "^0.2.8",
@@ -3524,6 +3525,10 @@
"graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0"
}
},
+ "node_modules/@harb/ui-shared": {
+ "resolved": "packages/ui-shared",
+ "link": true
+ },
"node_modules/@harb/web3": {
"resolved": "packages/web3",
"link": true
@@ -21804,6 +21809,13 @@
}
}
},
+ "packages/ui-shared": {
+ "name": "@harb/ui-shared",
+ "version": "0.1.0",
+ "peerDependencies": {
+ "vue": "^3.5.0"
+ }
+ },
"packages/web3": {
"name": "@harb/web3",
"version": "0.1.0",
@@ -21819,6 +21831,7 @@
"name": "harb-staking",
"version": "0.0.0",
"dependencies": {
+ "@harb/ui-shared": "*",
"@harb/web3": "*",
"@tanstack/vue-query": "^5.64.2",
"@vue/test-utils": "^2.4.6",
diff --git a/packages/ui-shared/package.json b/packages/ui-shared/package.json
new file mode 100644
index 0000000..40bf4d5
--- /dev/null
+++ b/packages/ui-shared/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "@harb/ui-shared",
+ "version": "0.1.0",
+ "private": true,
+ "type": "module",
+ "main": "src/index.ts",
+ "types": "src/index.ts",
+ "peerDependencies": {
+ "vue": "^3.5.0"
+ }
+}
diff --git a/packages/ui-shared/src/components/TransactionHistory.vue b/packages/ui-shared/src/components/TransactionHistory.vue
new file mode 100644
index 0000000..7fe87be
--- /dev/null
+++ b/packages/ui-shared/src/components/TransactionHistory.vue
@@ -0,0 +1,312 @@
+
+
+
+ Transaction History
+ {{ visibleTransactions.length }}
+
+
+
+
+ Loading transactions…
+
+
+
No transactions found for this address.
+
+
+
+
+
+ | Date |
+ Type |
+ Amount (KRK) |
+ Value (ETH) |
+ Tx |
+
+
+
+
+ | {{ formatDate(tx.timestamp) }} |
+
+
+ {{ txTypeLabel(tx.type) }}
+
+ |
+ {{ formatKrk(tx.tokenAmount) }} |
+ {{ tx.ethAmount !== '0' ? formatEth(tx.ethAmount) : '—' }} |
+
+
+ {{ shortHash(tx.txHash) }} ↗
+
+ |
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/ui-shared/src/composables/useHolderDashboard.ts b/packages/ui-shared/src/composables/useHolderDashboard.ts
new file mode 100644
index 0000000..3da49bd
--- /dev/null
+++ b/packages/ui-shared/src/composables/useHolderDashboard.ts
@@ -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
, graphqlUrl: string | Ref = '/api/graphql') {
+ const holderBalance = ref('0');
+ const holderTotalEthSpent = ref('0');
+ const holderTotalTokensAcquired = ref('0');
+ const currentPriceWei = ref(null);
+ const lastEthReserve = ref('0');
+ const kraikenTotalSupply = ref('0');
+ const loading = ref(false);
+ const error = ref(null);
+ let pollTimer: ReturnType | 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,
+ };
+}
diff --git a/packages/ui-shared/src/index.ts b/packages/ui-shared/src/index.ts
new file mode 100644
index 0000000..efeb6d6
--- /dev/null
+++ b/packages/ui-shared/src/index.ts
@@ -0,0 +1,2 @@
+export { useHolderDashboard } from './composables/useHolderDashboard';
+export { default as TransactionHistory } from './components/TransactionHistory.vue';
diff --git a/web-app/package.json b/web-app/package.json
index 9cc8650..b3adf08 100644
--- a/web-app/package.json
+++ b/web-app/package.json
@@ -33,7 +33,8 @@
"vue-router": "^4.2.5",
"vue-tippy": "^6.6.0",
"vue-toastification": "^2.0.0-rc.5",
- "@harb/web3": "*"
+ "@harb/web3": "*",
+ "@harb/ui-shared": "*"
},
"devDependencies": {
"@iconify/vue": "^4.3.0",
diff --git a/web-app/src/views/WalletView.vue b/web-app/src/views/WalletView.vue
index 77cad35..e7d8efd 100644
--- a/web-app/src/views/WalletView.vue
+++ b/web-app/src/views/WalletView.vue
@@ -161,7 +161,7 @@
import { computed, ref } from 'vue';
import { useRoute, RouterLink } from 'vue-router';
import { useWalletDashboard } from '@/composables/useWalletDashboard';
-import TransactionHistory from '@/components/TransactionHistory.vue';
+import { TransactionHistory } from '@harb/ui-shared';
const route = useRoute();
const addressParam = computed(() => String(route.params.address ?? ''));