flowstate / src /lib /helius.ts
muthuk1's picture
feat: Helius real on-chain wallet analysis
9e22644
/**
* 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
}
}