diff --git a/services/ponder/ponder.schema.ts b/services/ponder/ponder.schema.ts index eeaedf5..d31086f 100644 --- a/services/ponder/ponder.schema.ts +++ b/services/ponder/ponder.schema.ts @@ -280,6 +280,28 @@ export const holders = onchainTable( }) ); +// Transaction history for wallet dashboard +export const transactions = onchainTable( + 'transactions', + t => ({ + id: t.text().primaryKey(), // txHash-logIndex + holder: t.hex().notNull(), + type: t.text().notNull(), // "buy" | "sell" | "stake" | "unstake" | "snatch_in" | "snatch_out" + tokenAmount: t.bigint().notNull(), + ethAmount: t + .bigint() + .notNull() + .$default(() => 0n), + timestamp: t.bigint().notNull(), + blockNumber: t.integer().notNull(), + txHash: t.hex().notNull(), + }), + table => ({ + holderIdx: index().on(table.holder), + timestampIdx: index().on(table.timestamp), + }) +); + // Helper constants export const STATS_ID = '0x01'; export const SECONDS_IN_HOUR = 3600; diff --git a/services/ponder/src/kraiken.ts b/services/ponder/src/kraiken.ts index 8158e22..1056920 100644 --- a/services/ponder/src/kraiken.ts +++ b/services/ponder/src/kraiken.ts @@ -1,6 +1,6 @@ import { ponder } from 'ponder:registry'; import { getLogger } from './helpers/logger'; -import { stats, holders, STATS_ID } from 'ponder:schema'; +import { stats, holders, transactions, STATS_ID } from 'ponder:schema'; import { ensureStatsExists, parseRingBuffer, @@ -118,6 +118,29 @@ ponder.on('Kraiken:Transfer', async ({ event, context }) => { } } + // Record buy/sell transactions + if (!isSelfTransfer && from !== ZERO_ADDRESS && to !== ZERO_ADDRESS) { + const isBuy = from.toLowerCase() === POOL_ADDRESS; + const isSell = to.toLowerCase() === POOL_ADDRESS; + + if (isBuy || isSell) { + const currentPrice = statsData.currentPriceWei ?? 0n; + const ethEstimate = currentPrice > 0n ? (value * currentPrice) / 10n ** 18n : 0n; + const txId = `${event.transaction.hash}-${event.log.logIndex}`; + + await context.db.insert(transactions).values({ + id: txId, + holder: isBuy ? to : from, + type: isBuy ? 'buy' : 'sell', + tokenAmount: value, + ethAmount: ethEstimate, + timestamp: event.block.timestamp, + blockNumber: Number(event.block.number), + txHash: event.transaction.hash, + }); + } + } + // Update holder count if changed (with underflow protection) if (holderCountDelta !== 0) { const newHolderCount = statsData.holderCount + holderCountDelta; diff --git a/services/ponder/src/stake.ts b/services/ponder/src/stake.ts index 2f912ca..a89c6b0 100644 --- a/services/ponder/src/stake.ts +++ b/services/ponder/src/stake.ts @@ -1,5 +1,5 @@ import { ponder } from 'ponder:registry'; -import { positions, stats, STATS_ID, TAX_RATES } from 'ponder:schema'; +import { positions, transactions, stats, STATS_ID, TAX_RATES } from 'ponder:schema'; import { ensureStatsExists, getStakeTotalSupply, @@ -55,6 +55,18 @@ ponder.on('Stake:PositionCreated', async ({ event, context }) => { payout: ZERO, }); + // Record stake transaction + await context.db.insert(transactions).values({ + id: `${event.transaction.hash}-${event.log.logIndex}`, + holder: event.args.owner as `0x${string}`, + type: 'stake', + tokenAmount: event.args.kraikenDeposit, + ethAmount: ZERO, + timestamp: event.block.timestamp, + blockNumber: Number(event.block.number), + txHash: event.transaction.hash, + }); + await refreshOutstandingStake(context); await markPositionsUpdated(context, event.block.timestamp); }); @@ -77,6 +89,18 @@ ponder.on('Stake:PositionRemoved', async ({ event, context }) => { stakeDeposit: ZERO, }); + // Record unstake transaction (could be voluntary unstake or snatch payout) + await context.db.insert(transactions).values({ + id: `${event.transaction.hash}-${event.log.logIndex}`, + holder: position.owner, + type: 'unstake', + tokenAmount: event.args.kraikenPayout, + ethAmount: ZERO, + timestamp: event.block.timestamp, + blockNumber: Number(event.block.number), + txHash: event.transaction.hash, + }); + await refreshOutstandingStake(context); await markPositionsUpdated(context, event.block.timestamp); diff --git a/web-app/src/components/TransactionHistory.vue b/web-app/src/components/TransactionHistory.vue new file mode 100644 index 0000000..dc14c26 --- /dev/null +++ b/web-app/src/components/TransactionHistory.vue @@ -0,0 +1,306 @@ + + + + + diff --git a/web-app/src/views/WalletView.vue b/web-app/src/views/WalletView.vue index 7f4b5d4..77cad35 100644 --- a/web-app/src/views/WalletView.vue +++ b/web-app/src/views/WalletView.vue @@ -86,6 +86,9 @@ + + +

@@ -158,6 +161,7 @@ import { computed, ref } from 'vue'; import { useRoute, RouterLink } from 'vue-router'; import { useWalletDashboard } from '@/composables/useWalletDashboard'; +import TransactionHistory from '@/components/TransactionHistory.vue'; const route = useRoute(); const addressParam = computed(() => String(route.params.address ?? ''));