| |
| |
| |
| |
|
|
| const HELIUS_BASE = 'https://api.helius.xyz/v0' |
| const KEY = process.env.HELIUS_API_KEY || '' |
|
|
| export function isHeliusConfigured() { |
| return KEY !== '' && !KEY.startsWith('your') |
| } |
|
|
| |
| const SOURCE_MAP: Record<string, string> = { |
| JUPITER: 'Jupiter', |
| RAYDIUM: 'Raydium', |
| DRIFT: 'Drift', |
| KAMINO: 'Kamino', |
| KAMINO_FINANCE: 'Kamino', |
| MARGINFI: 'Marginfi', |
| TENSOR: 'Tensor', |
| ORCA: 'Orca', |
| METEORA: 'Meteora', |
| PHOENIX: 'Phoenix', |
| LIFINITY: 'Lifinity', |
| MARINADE: 'Marinade', |
| } |
|
|
| interface HeliusTx { |
| timestamp: number |
| source: string |
| type: string |
| nativeTransfers?: Array<{ amount: number }> |
| tokenTransfers?: Array<{ tokenAmount: number }> |
| } |
|
|
| export interface WalletSignals { |
| address: string |
| daysInactive: number |
| protocols: string[] |
| uniqueProtocols: number |
| currentStreak: number |
| volumeDropPct: number |
| hasLiquidation: boolean |
| totalTxLast30d: number |
| lastActiveDaysAgo: string |
| source: 'helius' | 'mock' |
| } |
|
|
| export async function fetchWalletSignals(address: string): Promise<WalletSignals | null> { |
| if (!isHeliusConfigured()) return null |
| try { |
| const res = await fetch( |
| `${HELIUS_BASE}/addresses/${address}/transactions?api-key=${KEY}&limit=50`, |
| { next: { revalidate: 60 } } |
| ) |
| if (!res.ok) return null |
| const txs: HeliusTx[] = await res.json() |
| if (!txs.length) return null |
|
|
| const nowSec = Date.now() / 1000 |
| const sorted = txs.sort((a, b) => b.timestamp - a.timestamp) |
|
|
| |
| const daysInactive = Math.round((nowSec - sorted[0].timestamp) / 86400) |
|
|
| |
| const protocolSet = new Set<string>() |
| for (const tx of sorted) { |
| const mapped = SOURCE_MAP[tx.source] |
| if (mapped) protocolSet.add(mapped) |
| } |
| const protocols = Array.from(protocolSet) |
|
|
| |
| const sevenDaysAgo = nowSec - 7 * 86400 |
| const activeDays = new Set( |
| sorted |
| .filter(tx => tx.timestamp >= sevenDaysAgo) |
| .map(tx => new Date(tx.timestamp * 1000).toDateString()) |
| ) |
| const currentStreak = activeDays.size |
|
|
| |
| const fourteenDaysAgo = nowSec - 14 * 86400 |
| const recentCount = sorted.filter(tx => tx.timestamp >= sevenDaysAgo).length |
| const priorCount = sorted.filter(tx => tx.timestamp >= fourteenDaysAgo && tx.timestamp < sevenDaysAgo).length |
| const volumeDropPct = priorCount > 0 |
| ? Math.round(Math.max(0, (priorCount - recentCount) / priorCount * 100)) |
| : 0 |
|
|
| |
| const hasLiquidation = sorted.some(tx => tx.type?.includes('LIQUIDAT')) |
|
|
| |
| const thirtyDaysAgo = nowSec - 30 * 86400 |
| const totalTxLast30d = sorted.filter(tx => tx.timestamp >= thirtyDaysAgo).length |
|
|
| const lastActiveDaysAgo = daysInactive === 0 ? 'today' |
| : daysInactive === 1 ? '1d ago' |
| : `${daysInactive}d ago` |
|
|
| return { |
| address, |
| daysInactive, |
| protocols, |
| uniqueProtocols: protocols.length, |
| currentStreak, |
| volumeDropPct, |
| hasLiquidation, |
| totalTxLast30d, |
| lastActiveDaysAgo, |
| source: 'helius', |
| } |
| } catch { |
| return null |
| } |
| } |
|
|