File size: 3,430 Bytes
9e22644
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
/**
 * Helius RPC client — fetches real Solana wallet activity for churn scoring.
 * Uses the enhanced transactions API to derive wallet signals without an indexer.
 */

const HELIUS_BASE = 'https://api.helius.xyz/v0'
const KEY = process.env.HELIUS_API_KEY || ''

export function isHeliusConfigured() {
  return KEY !== '' && !KEY.startsWith('your')
}

// Helius source → display protocol name
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)

    // Days since last tx
    const daysInactive = Math.round((nowSec - sorted[0].timestamp) / 86400)

    // Protocols used
    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)

    // Streak: count distinct calendar days active in last 7d
    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

    // Volume drop: tx count in last 7d vs prior 7d (proxy for volume)
    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

    // Liquidation check
    const hasLiquidation = sorted.some(tx => tx.type?.includes('LIQUIDAT'))

    // tx count last 30d
    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
  }
}