@fundtracer/mcp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/fundtracer-mcp.js +3820 -0
  2. package/package.json +29 -0
@@ -0,0 +1,3820 @@
1
+ #!/usr/bin/env node
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
5
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
6
+ }) : x)(function(x) {
7
+ if (typeof require !== "undefined") return require.apply(this, arguments);
8
+ throw Error('Dynamic require of "' + x + '" is not supported');
9
+ });
10
+ var __esm = (fn, res) => function __init() {
11
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
12
+ };
13
+ var __export = (target, all) => {
14
+ for (var name in all)
15
+ __defProp(target, name, { get: all[name], enumerable: true });
16
+ };
17
+
18
+ // src/firebase.ts
19
+ var firebase_exports = {};
20
+ __export(firebase_exports, {
21
+ admin: () => admin,
22
+ getAuth: () => getAuth,
23
+ getFirestore: () => getFirestore,
24
+ getUserByAddress: () => getUserByAddress,
25
+ initializeFirebase: () => initializeFirebase,
26
+ updateUserTier: () => updateUserTier
27
+ });
28
+ import admin from "firebase-admin";
29
+ import fs from "fs";
30
+ function initializeFirebase() {
31
+ if (firebaseInitialized) {
32
+ console.log("[Firebase] Already initialized (skipping)");
33
+ return;
34
+ }
35
+ try {
36
+ if (admin.apps.length > 0) {
37
+ console.log("[Firebase] Already initialized by Firebase auto-discovery");
38
+ firebaseInitialized = true;
39
+ return;
40
+ }
41
+ } catch (e) {
42
+ }
43
+ console.log("[Firebase] Starting initialization...");
44
+ const base64ServiceAccount = process.env.FIREBASE_SERVICE_ACCOUNT_BASE64;
45
+ if (base64ServiceAccount) {
46
+ try {
47
+ console.log("[Firebase] Found base64 service account, decoding...");
48
+ const decoded = Buffer.from(base64ServiceAccount, "base64").toString("utf-8");
49
+ const serviceAccount = JSON.parse(decoded);
50
+ admin.initializeApp({
51
+ credential: admin.credential.cert(serviceAccount)
52
+ });
53
+ firebaseInitialized = true;
54
+ console.log("[Firebase] \u2705 Initialized from base64 service account");
55
+ return;
56
+ } catch (error) {
57
+ console.error("[Firebase] Failed to decode base64 service account:", error);
58
+ }
59
+ }
60
+ const credentialsPath = process.env.GOOGLE_APPLICATION_CREDENTIALS;
61
+ console.log("[Firebase] Checking credentials file path:", credentialsPath || "(not set)");
62
+ if (credentialsPath && fs.existsSync(credentialsPath)) {
63
+ try {
64
+ const serviceAccount = JSON.parse(fs.readFileSync(credentialsPath, "utf8"));
65
+ admin.initializeApp({
66
+ credential: admin.credential.cert(serviceAccount)
67
+ });
68
+ firebaseInitialized = true;
69
+ console.log("[Firebase] \u2705 Initialized from credentials file");
70
+ return;
71
+ } catch (error) {
72
+ console.error("[Firebase] Failed to load credentials file:", error);
73
+ }
74
+ }
75
+ const projectId = process.env.FIREBASE_PROJECT_ID;
76
+ const clientEmail = process.env.FIREBASE_CLIENT_EMAIL;
77
+ let privateKey = process.env.FIREBASE_PRIVATE_KEY;
78
+ console.log("[Firebase] Checking env vars...");
79
+ console.log("[Firebase] Project ID:", projectId ? "\u2713 set" : "\u2717 missing");
80
+ console.log("[Firebase] Client Email:", clientEmail ? "\u2713 set" : "\u2717 missing");
81
+ console.log("[Firebase] Private Key:", privateKey ? `\u2713 set (${privateKey.length} chars)` : "\u2717 missing");
82
+ if (privateKey && !privateKey.includes("-----BEGIN")) {
83
+ try {
84
+ console.log("[Firebase] Decoding base64 private key...");
85
+ privateKey = Buffer.from(privateKey, "base64").toString("utf-8");
86
+ } catch (e) {
87
+ console.warn("[Firebase] Failed to decode base64 private key, using as-is");
88
+ }
89
+ }
90
+ privateKey = privateKey?.replace(/\\n/g, "\n");
91
+ console.log("[Firebase] Private key starts with BEGIN:", privateKey?.includes("-----BEGIN PRIVATE KEY-----"));
92
+ if (!projectId || !clientEmail || !privateKey) {
93
+ const errorMsg = "[Firebase] FATAL: Credentials not configured! Set FIREBASE_SERVICE_ACCOUNT_BASE64 (preferred) or FIREBASE_PROJECT_ID/CLIENT_EMAIL/PRIVATE_KEY.";
94
+ console.error(errorMsg);
95
+ throw new Error(errorMsg);
96
+ }
97
+ try {
98
+ admin.initializeApp({
99
+ credential: admin.credential.cert({
100
+ projectId,
101
+ clientEmail,
102
+ privateKey
103
+ })
104
+ });
105
+ firebaseInitialized = true;
106
+ console.log("Firebase Admin SDK initialized");
107
+ } catch (error) {
108
+ console.error("Failed to initialize Firebase:", error);
109
+ }
110
+ }
111
+ function getAuth() {
112
+ return admin.auth();
113
+ }
114
+ function getFirestore() {
115
+ return admin.firestore();
116
+ }
117
+ async function getUserByAddress(address) {
118
+ const db = getFirestore();
119
+ const userDoc = await db.collection("users").doc(address.toLowerCase()).get();
120
+ if (!userDoc.exists) {
121
+ return null;
122
+ }
123
+ return {
124
+ address: userDoc.id,
125
+ ...userDoc.data()
126
+ };
127
+ }
128
+ async function updateUserTier(address, tier, expiresAt) {
129
+ const db = getFirestore();
130
+ const userRef = db.collection("users").doc(address.toLowerCase());
131
+ await userRef.set({
132
+ tier,
133
+ tierExpiresAt: admin.firestore.Timestamp.fromDate(expiresAt),
134
+ updatedAt: admin.firestore.FieldValue.serverTimestamp()
135
+ }, { merge: true });
136
+ console.log(`[Payment] Updated user ${address} to ${tier} tier, expires ${expiresAt.toISOString()}`);
137
+ }
138
+ var firebaseInitialized;
139
+ var init_firebase = __esm({
140
+ "src/firebase.ts"() {
141
+ firebaseInitialized = false;
142
+ }
143
+ });
144
+
145
+ // src/mcp/tools.ts
146
+ var tools_exports = {};
147
+ __export(tools_exports, {
148
+ ALL_MCP_TOOLS: () => ALL_MCP_TOOLS,
149
+ getToolByName: () => getToolByName
150
+ });
151
+ function getToolByName(name) {
152
+ return ALL_MCP_TOOLS.find((t) => t.name === name);
153
+ }
154
+ var ALL_MCP_TOOLS;
155
+ var init_tools = __esm({
156
+ "src/mcp/tools.ts"() {
157
+ ALL_MCP_TOOLS = [
158
+ {
159
+ name: "analyze_wallet",
160
+ description: "Perform a full blockchain wallet analysis including balance, transactions, risk score, suspicious indicators, and project interactions.",
161
+ inputSchema: {
162
+ type: "object",
163
+ properties: {
164
+ address: { type: "string", description: "Wallet address to analyze (0x... for EVM, base58 for Solana)" },
165
+ chainId: {
166
+ type: "string",
167
+ description: "Blockchain to analyze",
168
+ enum: ["ethereum", "base", "arbitrum", "optimism", "polygon", "linea", "bsc", "solana"]
169
+ },
170
+ transactionLimit: {
171
+ type: "number",
172
+ description: "Max transactions to fetch (default: 500)",
173
+ default: 500
174
+ }
175
+ },
176
+ required: ["address", "chainId"]
177
+ }
178
+ },
179
+ {
180
+ name: "trace_funds",
181
+ description: "Trace funding sources and destinations for a wallet address, building a recursive funding tree.",
182
+ inputSchema: {
183
+ type: "object",
184
+ properties: {
185
+ address: { type: "string", description: "Wallet address to trace" },
186
+ chainId: {
187
+ type: "string",
188
+ description: "Blockchain to trace on",
189
+ enum: ["ethereum", "base", "arbitrum", "optimism", "polygon", "linea", "bsc", "solana"]
190
+ },
191
+ maxDepth: {
192
+ type: "number",
193
+ description: "How many levels deep to trace (default: 3)",
194
+ default: 3
195
+ },
196
+ direction: {
197
+ type: "string",
198
+ description: "Which direction to trace",
199
+ enum: ["sources", "destinations", "both"],
200
+ default: "both"
201
+ }
202
+ },
203
+ required: ["address", "chainId"]
204
+ }
205
+ },
206
+ {
207
+ name: "compare_wallets",
208
+ description: "Compare multiple wallet addresses for common funding sources, shared project interactions, and sybil correlation scoring.",
209
+ inputSchema: {
210
+ type: "object",
211
+ properties: {
212
+ addresses: {
213
+ type: "string",
214
+ description: "Comma-separated list of wallet addresses to compare (2-20 wallets)"
215
+ },
216
+ chainId: {
217
+ type: "string",
218
+ description: "Blockchain to compare on",
219
+ enum: ["ethereum", "base", "arbitrum", "optimism", "polygon", "linea", "bsc", "solana"]
220
+ }
221
+ },
222
+ required: ["addresses", "chainId"]
223
+ }
224
+ },
225
+ {
226
+ name: "analyze_contract",
227
+ description: "Analyze all addresses that have interacted with a smart contract, detecting sybil clusters and shared funding sources.",
228
+ inputSchema: {
229
+ type: "object",
230
+ properties: {
231
+ contractAddress: { type: "string", description: "Smart contract address to analyze" },
232
+ chainId: {
233
+ type: "string",
234
+ description: "Blockchain the contract is on",
235
+ enum: ["ethereum", "base", "arbitrum", "optimism", "polygon", "linea", "bsc"]
236
+ },
237
+ maxInteractors: {
238
+ type: "number",
239
+ description: "Max interactors to analyze (default: 100)",
240
+ default: 100
241
+ }
242
+ },
243
+ required: ["contractAddress", "chainId"]
244
+ }
245
+ },
246
+ {
247
+ name: "detect_sybil_clusters",
248
+ description: "Detect sybil (fake) accounts by clustering wallets that share common funding sources.",
249
+ inputSchema: {
250
+ type: "object",
251
+ properties: {
252
+ addresses: {
253
+ type: "string",
254
+ description: "Comma-separated list of wallet addresses to check for sybil clustering"
255
+ },
256
+ chainId: {
257
+ type: "string",
258
+ description: "Blockchain to analyze on",
259
+ enum: ["ethereum", "base", "arbitrum", "optimism", "polygon", "bsc"]
260
+ }
261
+ },
262
+ required: ["addresses", "chainId"]
263
+ }
264
+ },
265
+ {
266
+ name: "get_portfolio",
267
+ description: "Get the token portfolio, DeFi positions, and NFT holdings for a wallet address.",
268
+ inputSchema: {
269
+ type: "object",
270
+ properties: {
271
+ address: { type: "string", description: "Wallet address" },
272
+ chainId: {
273
+ type: "string",
274
+ description: "Blockchain",
275
+ enum: ["ethereum", "solana", "base", "arbitrum", "optimism", "polygon", "linea"]
276
+ }
277
+ },
278
+ required: ["address", "chainId"]
279
+ }
280
+ },
281
+ {
282
+ name: "get_transactions",
283
+ description: "Get recent transaction history for a wallet address.",
284
+ inputSchema: {
285
+ type: "object",
286
+ properties: {
287
+ address: { type: "string", description: "Wallet address" },
288
+ chainId: {
289
+ type: "string",
290
+ description: "Blockchain",
291
+ enum: ["ethereum", "base", "arbitrum", "optimism", "polygon", "linea", "bsc", "solana"]
292
+ },
293
+ limit: {
294
+ type: "number",
295
+ description: "Number of transactions to return (default: 50)",
296
+ default: 50
297
+ }
298
+ },
299
+ required: ["address", "chainId"]
300
+ }
301
+ },
302
+ {
303
+ name: "lookup_entity",
304
+ description: "Look up a known blockchain entity, protocol, or address label.",
305
+ inputSchema: {
306
+ type: "object",
307
+ properties: {
308
+ query: { type: "string", description: "Entity name, address, or label to look up" },
309
+ chainId: {
310
+ type: "string",
311
+ description: "Blockchain to search (optional)",
312
+ enum: ["ethereum", "solana", "base", "arbitrum", "optimism", "polygon", "linea", "bsc", ""],
313
+ default: ""
314
+ }
315
+ },
316
+ required: ["query"]
317
+ }
318
+ },
319
+ {
320
+ name: "get_gas_prices",
321
+ description: "Get current gas prices across supported blockchain networks.",
322
+ inputSchema: {
323
+ type: "object",
324
+ properties: {
325
+ chainId: {
326
+ type: "string",
327
+ description: "Specific chain (optional, returns all if omitted)",
328
+ enum: ["ethereum", "base", "arbitrum", "optimism", "polygon", "linea", "bsc", ""],
329
+ default: ""
330
+ }
331
+ },
332
+ required: []
333
+ }
334
+ },
335
+ {
336
+ name: "get_token_info",
337
+ description: "Get market data and information for a token by address or symbol.",
338
+ inputSchema: {
339
+ type: "object",
340
+ properties: {
341
+ tokenAddress: { type: "string", description: "Token contract address" },
342
+ chainId: {
343
+ type: "string",
344
+ description: "Blockchain the token is on",
345
+ enum: ["ethereum", "base", "arbitrum", "optimism", "polygon", "linea", "bsc", "solana"]
346
+ }
347
+ },
348
+ required: ["tokenAddress", "chainId"]
349
+ }
350
+ }
351
+ ];
352
+ }
353
+ });
354
+
355
+ // src/services/SolanaFundingTreeService.ts
356
+ var SolanaFundingTreeService_exports = {};
357
+ __export(SolanaFundingTreeService_exports, {
358
+ SolanaFundingTreeService: () => SolanaFundingTreeService,
359
+ default: () => SolanaFundingTreeService_default
360
+ });
361
+ import fetch2 from "node-fetch";
362
+ var ALCHEMY_SOLANA_API, CACHE_TTL, KNOWN_PROGRAMS, SolanaFundingTreeService, SolanaFundingTreeService_default;
363
+ var init_SolanaFundingTreeService = __esm({
364
+ "src/services/SolanaFundingTreeService.ts"() {
365
+ ALCHEMY_SOLANA_API = "https://solana-mainnet.g.alchemy.com/v2/{apiKey}";
366
+ CACHE_TTL = 300;
367
+ KNOWN_PROGRAMS = {
368
+ // DEXs
369
+ "jupoK8gEJ4qEfD1k6QzJD7ssgvG5xTLwXgQNZHcPQ3fl": { name: "Jupiter", type: "dex" },
370
+ "jup3ZqFqEboGxBw1UnAUoxfXQA5ryiJPq3U5EEiW5eF": { name: "Jupiter", type: "dex" },
371
+ "CGkE4wDyY7mTDE7GQPPF2Uk6hK2Qa3x5xUhNYQqGKqBD": { name: "Raydium", type: "dex" },
372
+ "RVKdL2gt2zb2wWPXURQPswTUGqH2c6m8PMD3fESqC8H": { name: "Raydium", type: "dex" },
373
+ "orcaEKTdKx2wB3BmcSJwds6D3B4RST3JnBZKJx3QkqY9": { name: "Orca", type: "dex" },
374
+ // Bridges
375
+ "85VCBFdxR9exr5GtHVELq7uDT1mAc7YMFuq2bLtUMMmT": { name: "Wormhole", type: "bridge" },
376
+ "wormE4TGTQEaUMfNFxNA1XqJGMXH9Znk7aqZ3fGXq9p": { name: "Wormhole (Core)", type: "bridge" },
377
+ // CEX - common Solana热钱包
378
+ "2rXhuHUNDULrV6YLiPLZmm3xKg4zDqtLuZD8fFPTXw4": { name: "Coinbase", type: "cex" },
379
+ "F4vLeT4eq7YfmqNEBYJTdxYqNsuKXPxuPMe9jCBDm3k": { name: "Binance", type: "cex" },
380
+ // Known programs
381
+ "metaqbxxUurdFM34NHCNprmdGhDo4SyRQ9Dkjf53TwSp6y": { name: "Metaplex", type: "program" },
382
+ "TokenkegQfeZyiNwAJbNbGKPxGnhTNoZfFNYKDNgVEGPh": { name: "SPL Token", type: "program" },
383
+ "ATokenGPdCpDNQUxFJpMMzhxrZmLBhNpYY2MSKHvrkK7": { name: "Associated Token", type: "program" }
384
+ };
385
+ SolanaFundingTreeService = class {
386
+ apiKey;
387
+ cache = /* @__PURE__ */ new Map();
388
+ constructor(apiKey) {
389
+ this.apiKey = apiKey;
390
+ }
391
+ async rpcCall(method, params) {
392
+ const url = ALCHEMY_SOLANA_API.replace("{apiKey}", this.apiKey);
393
+ const response = await fetch2(url, {
394
+ method: "POST",
395
+ headers: { "Content-Type": "application/json" },
396
+ body: JSON.stringify({
397
+ jsonrpc: "2.0",
398
+ id: 1,
399
+ method,
400
+ params
401
+ })
402
+ });
403
+ const data = await response.json();
404
+ if (data.error) {
405
+ throw new Error(data.error.message);
406
+ }
407
+ return data.result;
408
+ }
409
+ getCached(key) {
410
+ const cached = this.cache.get(key);
411
+ if (cached && cached.expiresAt > Date.now()) {
412
+ return cached.data;
413
+ }
414
+ return null;
415
+ }
416
+ setCached(key, data) {
417
+ this.cache.set(key, {
418
+ data,
419
+ expiresAt: Date.now() + CACHE_TTL * 1e3
420
+ });
421
+ }
422
+ /**
423
+ * Get entity label for an address
424
+ */
425
+ getEntityInfo(address) {
426
+ const known = KNOWN_PROGRAMS[address];
427
+ if (known) {
428
+ return known;
429
+ }
430
+ if (address.length === 44) {
431
+ return { type: "wallet" };
432
+ }
433
+ return { type: "other" };
434
+ }
435
+ /**
436
+ * Fetch transaction signatures for an address
437
+ */
438
+ async getTransactions(address, limit = 100) {
439
+ const cacheKey = `txs:${address}:${limit}`;
440
+ const cached = this.getCached(cacheKey);
441
+ if (cached) return cached;
442
+ try {
443
+ const result = await this.rpcCall("getTransactionsForAddress", [
444
+ address,
445
+ {
446
+ transactionDetails: "signatures",
447
+ sortOrder: "desc",
448
+ limit,
449
+ filters: {
450
+ status: "succeeded"
451
+ }
452
+ }
453
+ ]);
454
+ const transactions = (result.data || []).map((entry) => ({
455
+ signature: entry.signature,
456
+ slot: entry.slot,
457
+ blockTime: entry.blockTime,
458
+ err: entry.err
459
+ }));
460
+ this.setCached(cacheKey, transactions);
461
+ return transactions;
462
+ } catch (error) {
463
+ console.error("[SolanaFundingTree] Error fetching transactions:", error);
464
+ return [];
465
+ }
466
+ }
467
+ /**
468
+ * Build funding sources (where funds came from)
469
+ * For Solana, this traces transfers WHERE THE ADDRESS RECEIVED funds
470
+ */
471
+ async buildSourceTree(address, maxDepth = 3) {
472
+ const transactions = await this.getTransactions(address, 100);
473
+ return this.constructTree(address, transactions, "source", maxDepth);
474
+ }
475
+ /**
476
+ * Build funding destinations (where funds went to)
477
+ * For Solana, this traces transfers WHERE THE ADDRESS SENT funds
478
+ */
479
+ async buildDestinationTree(address, maxDepth = 3) {
480
+ const transactions = await this.getTransactions(address, 100);
481
+ return this.constructTree(address, transactions, "destination", maxDepth);
482
+ }
483
+ /**
484
+ * Construct funding tree from transactions
485
+ */
486
+ constructTree(address, transactions, direction, maxDepth, currentDepth = 0) {
487
+ if (currentDepth >= maxDepth || transactions.length === 0) {
488
+ return this.createNode(address, direction, currentDepth, [], 0);
489
+ }
490
+ const nodeMap = /* @__PURE__ */ new Map();
491
+ for (const tx of transactions) {
492
+ const counterparty = this.extractCounterparty(tx.signature, address, direction);
493
+ if (!counterparty) continue;
494
+ const existing = nodeMap.get(counterparty) || { count: 0, totalValue: 0, firstTx: tx.blockTime, signatures: [] };
495
+ existing.count += 1;
496
+ existing.signatures.push(tx.signature);
497
+ if (tx.blockTime && tx.blockTime < existing.firstTx) {
498
+ existing.firstTx = tx.blockTime;
499
+ }
500
+ nodeMap.set(counterparty, existing);
501
+ }
502
+ const children = [];
503
+ let totalCount = 0;
504
+ for (const [counterAddr, stats] of Array.from(nodeMap.entries())) {
505
+ const childNode = this.constructTree(
506
+ counterAddr,
507
+ [],
508
+ // Would need to fetch recursively for full tree
509
+ direction,
510
+ maxDepth,
511
+ currentDepth + 1
512
+ );
513
+ childNode.txCount = stats.count;
514
+ childNode.totalValueInSol = stats.totalValue;
515
+ childNode.firstTx = { signature: stats.signatures[0], timestamp: stats.firstTx };
516
+ children.push(childNode);
517
+ totalCount += stats.count;
518
+ }
519
+ children.sort((a, b) => b.txCount - a.txCount);
520
+ const totalValue = children.reduce((sum, c) => sum + c.totalValueInSol, 0);
521
+ const entity = this.getEntityInfo(address);
522
+ return {
523
+ address,
524
+ depth: currentDepth,
525
+ direction,
526
+ totalValue: totalValue.toFixed(4),
527
+ totalValueInSol: totalValue,
528
+ txCount: totalCount,
529
+ children,
530
+ suspiciousScore: 0,
531
+ suspiciousReasons: [],
532
+ label: entity.label,
533
+ entityType: entity.type
534
+ };
535
+ }
536
+ /**
537
+ * Create a tree node
538
+ */
539
+ createNode(address, direction, depth, children, totalValue) {
540
+ const entity = this.getEntityInfo(address);
541
+ return {
542
+ address,
543
+ depth,
544
+ direction,
545
+ totalValue: totalValue.toFixed(4),
546
+ totalValueInSol: totalValue,
547
+ txCount: children.length,
548
+ children,
549
+ suspiciousScore: 0,
550
+ suspiciousReasons: [],
551
+ label: entity.label,
552
+ entityType: entity.type
553
+ };
554
+ }
555
+ /**
556
+ * Extract counterparty from transaction
557
+ * This is a placeholder - in full implementation we'd parse the transaction
558
+ */
559
+ extractCounterparty(signature, address, direction) {
560
+ return null;
561
+ }
562
+ /**
563
+ * Build complete funding tree with both sources and destinations
564
+ */
565
+ async buildFundingTree(address, maxDepth = 3) {
566
+ const [fundingSources, fundingDestinations] = await Promise.all([
567
+ this.buildSourceTree(address, maxDepth),
568
+ this.buildDestinationTree(address, maxDepth)
569
+ ]);
570
+ return { fundingSources, fundingDestinations };
571
+ }
572
+ };
573
+ SolanaFundingTreeService_default = SolanaFundingTreeService;
574
+ }
575
+ });
576
+
577
+ // src/services/SolanaKeyPoolManager.ts
578
+ var MAX_MONTHLY_CUS, CIRCUIT_OPEN_MS, ERROR_THRESHOLD, SolanaKeyPoolManager, solanaKeyPool;
579
+ var init_SolanaKeyPoolManager = __esm({
580
+ "src/services/SolanaKeyPoolManager.ts"() {
581
+ MAX_MONTHLY_CUS = 3e8;
582
+ CIRCUIT_OPEN_MS = 3e4;
583
+ ERROR_THRESHOLD = 5;
584
+ SolanaKeyPoolManager = class {
585
+ keys = [];
586
+ currentIndex = 0;
587
+ connections = /* @__PURE__ */ new Map();
588
+ constructor() {
589
+ this.initKeys();
590
+ this.startHealthMonitor();
591
+ }
592
+ initKeys() {
593
+ const contractKeyCount = 10;
594
+ for (let i = 1; i <= contractKeyCount; i++) {
595
+ const envKey = `SYBIL_CONTRACT_KEY_${String(i).padStart(2, "0")}`;
596
+ const key = process.env[envKey];
597
+ if (key) {
598
+ const endpoint = `https://solana-mainnet.g.alchemy.com/v2/${key}`;
599
+ this.keys.push({
600
+ key,
601
+ endpoint,
602
+ requestsThisMinute: 0,
603
+ requestsThisMonth: 0,
604
+ totalCUs: 0,
605
+ consecutiveErrors: 0,
606
+ lastErrorAt: null,
607
+ circuitOpen: false,
608
+ circuitOpenUntil: null,
609
+ avgLatencyMs: 0
610
+ });
611
+ }
612
+ }
613
+ const walletKeyCount = 10;
614
+ for (let i = 1; i <= walletKeyCount; i++) {
615
+ const envKey = `SYBIL_WALLET_KEY_${String(i).padStart(2, "0")}`;
616
+ const key = process.env[envKey];
617
+ if (key && !this.keys.find((k) => k.key === key)) {
618
+ const endpoint = `https://solana-mainnet.g.alchemy.com/v2/${key}`;
619
+ this.keys.push({
620
+ key,
621
+ endpoint,
622
+ requestsThisMinute: 0,
623
+ requestsThisMonth: 0,
624
+ totalCUs: 0,
625
+ consecutiveErrors: 0,
626
+ lastErrorAt: null,
627
+ circuitOpen: false,
628
+ circuitOpenUntil: null,
629
+ avgLatencyMs: 0
630
+ });
631
+ }
632
+ }
633
+ if (this.keys.length === 0) {
634
+ console.warn("[SolanaKeyPool] No Alchemy keys found, using fallback");
635
+ const fallbackKey = process.env.ALCHEMY_SOLANA_KEY || process.env.ALCHEMY_KEY_01;
636
+ if (fallbackKey) {
637
+ this.keys.push({
638
+ key: fallbackKey,
639
+ endpoint: `https://solana-mainnet.g.alchemy.com/v2/${fallbackKey}`,
640
+ requestsThisMinute: 0,
641
+ requestsThisMonth: 0,
642
+ totalCUs: 0,
643
+ consecutiveErrors: 0,
644
+ lastErrorAt: null,
645
+ circuitOpen: false,
646
+ circuitOpenUntil: null,
647
+ avgLatencyMs: 0
648
+ });
649
+ }
650
+ }
651
+ console.log(`[SolanaKeyPool] Initialized with ${this.keys.length} keys`);
652
+ }
653
+ getNextKey() {
654
+ const now = Date.now();
655
+ let attempts = 0;
656
+ while (attempts < this.keys.length) {
657
+ const health = this.keys[this.currentIndex];
658
+ this.currentIndex = (this.currentIndex + 1) % this.keys.length;
659
+ if (health.circuitOpen) {
660
+ if (now < health.circuitOpenUntil) {
661
+ attempts++;
662
+ continue;
663
+ }
664
+ health.circuitOpen = false;
665
+ health.consecutiveErrors = 0;
666
+ }
667
+ if (health.totalCUs >= MAX_MONTHLY_CUS * 0.95) {
668
+ attempts++;
669
+ continue;
670
+ }
671
+ return { health, endpoint: health.endpoint };
672
+ }
673
+ throw new Error("All Alchemy keys exhausted or in circuit-open state");
674
+ }
675
+ async execute(fn, cuCost = 1) {
676
+ const maxRetries = 3;
677
+ let lastError = null;
678
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
679
+ const { health, endpoint } = this.getNextKey();
680
+ const start = Date.now();
681
+ try {
682
+ const result = await fn(endpoint);
683
+ const latency = Date.now() - start;
684
+ health.consecutiveErrors = 0;
685
+ health.requestsThisMinute++;
686
+ health.requestsThisMonth++;
687
+ health.totalCUs += cuCost;
688
+ health.avgLatencyMs = health.avgLatencyMs * 0.9 + latency * 0.1;
689
+ return result;
690
+ } catch (err2) {
691
+ lastError = err2;
692
+ health.consecutiveErrors++;
693
+ health.lastErrorAt = Date.now();
694
+ if (health.consecutiveErrors >= ERROR_THRESHOLD) {
695
+ health.circuitOpen = true;
696
+ health.circuitOpenUntil = Date.now() + CIRCUIT_OPEN_MS;
697
+ console.warn(`[SolanaKeyPool] Circuit opened for key ${health.key.slice(0, 8)}...`);
698
+ }
699
+ if (err2.status === 429) {
700
+ await new Promise((r) => setTimeout(r, 200 * (attempt + 1)));
701
+ }
702
+ }
703
+ }
704
+ throw lastError || new Error("RPC call failed after retries");
705
+ }
706
+ getPoolStats() {
707
+ return {
708
+ totalKeys: this.keys.length,
709
+ healthyKeys: this.keys.filter((k) => !k.circuitOpen).length,
710
+ totalCUsUsed: this.keys.reduce((sum, k) => sum + k.totalCUs, 0),
711
+ avgLatencyMs: this.keys.reduce((s, k) => s + k.avgLatencyMs, 0) / this.keys.length
712
+ };
713
+ }
714
+ startHealthMonitor() {
715
+ setInterval(() => {
716
+ this.keys.forEach((k) => k.requestsThisMinute = 0);
717
+ }, 6e4);
718
+ }
719
+ };
720
+ solanaKeyPool = new SolanaKeyPoolManager();
721
+ }
722
+ });
723
+
724
+ // src/utils/cache.ts
725
+ var Cache, cache;
726
+ var init_cache = __esm({
727
+ "src/utils/cache.ts"() {
728
+ Cache = class {
729
+ store = /* @__PURE__ */ new Map();
730
+ set(key, value, ttlSeconds) {
731
+ const expires = Date.now() + ttlSeconds * 1e3;
732
+ this.store.set(key, { value, expires });
733
+ }
734
+ get(key) {
735
+ const item = this.store.get(key);
736
+ if (!item) return null;
737
+ if (Date.now() > item.expires) {
738
+ this.store.delete(key);
739
+ return null;
740
+ }
741
+ return item.value;
742
+ }
743
+ delete(key) {
744
+ this.store.delete(key);
745
+ }
746
+ clear() {
747
+ this.store.clear();
748
+ }
749
+ has(key) {
750
+ const item = this.store.get(key);
751
+ if (!item) return false;
752
+ if (Date.now() > item.expires) {
753
+ this.store.delete(key);
754
+ return false;
755
+ }
756
+ return true;
757
+ }
758
+ };
759
+ cache = new Cache();
760
+ }
761
+ });
762
+
763
+ // src/services/DuneSimClient.ts
764
+ import fetch3 from "node-fetch";
765
+ var SIM_BETA_BASE, SIM_V1_BASE, EVM_CHAIN_IDS, LAMPORTS_PER_SOL, DuneSimClient, duneSimClient;
766
+ var init_DuneSimClient = __esm({
767
+ "src/services/DuneSimClient.ts"() {
768
+ init_cache();
769
+ SIM_BETA_BASE = "https://api.sim.dune.com/beta";
770
+ SIM_V1_BASE = "https://api.sim.dune.com/v1";
771
+ EVM_CHAIN_IDS = {
772
+ ethereum: 1,
773
+ eth: 1,
774
+ linea: 59144,
775
+ arbitrum: 42161,
776
+ arb: 42161,
777
+ optimism: 10,
778
+ opt: 10,
779
+ base: 8453,
780
+ polygon: 137,
781
+ matic: 137,
782
+ bsc: 56,
783
+ avalanche: 43114,
784
+ avax: 43114
785
+ };
786
+ LAMPORTS_PER_SOL = 1e9;
787
+ DuneSimClient = class {
788
+ apiKey;
789
+ enabled = true;
790
+ constructor(apiKey) {
791
+ this.apiKey = apiKey || process.env.SIM_API_KEY || "";
792
+ this.enabled = !!this.apiKey && process.env.SIM_SOLANA_ENABLED !== "false";
793
+ if (!this.enabled) {
794
+ console.warn("[DuneSimClient] SIM disabled - no API key or SIM_SOLANA_ENABLED=false");
795
+ } else {
796
+ console.log("[DuneSimClient] Initialized - using SIM for Solana data");
797
+ }
798
+ }
799
+ isEnabled() {
800
+ return this.enabled;
801
+ }
802
+ async fetchWithAuth(url) {
803
+ if (!this.enabled) {
804
+ throw new Error("SIM client disabled");
805
+ }
806
+ const response = await fetch3(url, {
807
+ method: "GET",
808
+ headers: {
809
+ "X-Sim-Api-Key": this.apiKey
810
+ }
811
+ });
812
+ if (!response.ok) {
813
+ const error = await response.text();
814
+ throw new Error(`SIM API error ${response.status}: ${error}`);
815
+ }
816
+ return response.json();
817
+ }
818
+ /**
819
+ * Get token balances for a Solana address using SIM
820
+ * Endpoint: GET /beta/svm/balances/{address}
821
+ */
822
+ async getBalances(address, options = {}) {
823
+ const cacheKey = `sim:balances:${address}:${JSON.stringify(options)}`;
824
+ const cached = cache.get(cacheKey);
825
+ if (cached) return cached;
826
+ const params = new URLSearchParams();
827
+ if (options.chains) params.set("chains", options.chains);
828
+ if (options.limit) params.set("limit", options.limit.toString());
829
+ if (options.excludeSpamTokens) params.set("exclude_spam_tokens", "true");
830
+ if (options.excludeUnpriced) params.set("exclude_unpriced", "true");
831
+ const url = `${SIM_BETA_BASE}/svm/balances/${address}${params.toString() ? "?" + params.toString() : ""}`;
832
+ try {
833
+ const data = await this.fetchWithAuth(url);
834
+ cache.set(cacheKey, data, 60);
835
+ return data;
836
+ } catch (error) {
837
+ console.error("[DuneSimClient] Error fetching balances:", error);
838
+ throw error;
839
+ }
840
+ }
841
+ /**
842
+ * Get transactions for a Solana address using SIM
843
+ * Endpoint: GET /beta/svm/transactions/{address}
844
+ */
845
+ async getTransactions(address, options = {}) {
846
+ const cacheKey = `sim:txs:${address}:${options.limit || 100}`;
847
+ const cached = cache.get(cacheKey);
848
+ if (cached) return cached;
849
+ const params = new URLSearchParams();
850
+ if (options.limit) params.set("limit", options.limit.toString());
851
+ const url = `${SIM_BETA_BASE}/svm/transactions/${address}${params.toString() ? "?" + params.toString() : ""}`;
852
+ try {
853
+ const data = await this.fetchWithAuth(url);
854
+ cache.set(cacheKey, data, 300);
855
+ return data;
856
+ } catch (error) {
857
+ console.error("[DuneSimClient] Error fetching transactions:", error);
858
+ throw error;
859
+ }
860
+ }
861
+ /**
862
+ * Convert SIM balances response to FundTracer format
863
+ */
864
+ mapBalancesToPortfolio(simResponse) {
865
+ const { balances, wallet_address } = simResponse;
866
+ const solBalance = balances.find((b) => b.address === "native");
867
+ const solLamports = solBalance ? parseInt(solBalance.amount) : 0;
868
+ const solUsd = solBalance?.value_usd || 0;
869
+ const solPrice = solBalance?.price_usd || 0;
870
+ const tokens = balances.filter((b) => b.address !== "native").map((token) => ({
871
+ mint: token.address,
872
+ amount: parseInt(token.amount),
873
+ decimals: token.decimals,
874
+ uiAmount: parseFloat(token.balance),
875
+ symbol: token.symbol,
876
+ name: token.name,
877
+ logoUrl: token.uri || void 0,
878
+ price: token.price_usd,
879
+ value: token.value_usd
880
+ })).filter((t) => t.uiAmount > 0);
881
+ const totalUsd = solUsd + tokens.reduce((sum, t) => sum + (t.value || 0), 0);
882
+ return {
883
+ address: wallet_address,
884
+ sol: {
885
+ lamports: solLamports,
886
+ sol: solLamports / LAMPORTS_PER_SOL,
887
+ usd: solUsd
888
+ },
889
+ tokens,
890
+ totalUsd,
891
+ fetchedAt: Date.now()
892
+ };
893
+ }
894
+ /**
895
+ * Convert SIM transactions to FundTracer format
896
+ */
897
+ mapTransactions(simResponse) {
898
+ const { transactions } = simResponse;
899
+ return transactions.map((tx) => {
900
+ const rawTx = tx.raw_transaction;
901
+ const meta = rawTx?.meta;
902
+ const message = rawTx?.transaction?.message;
903
+ const instructions = message?.instructions || [];
904
+ let from = "";
905
+ let to = "";
906
+ let amount = 0;
907
+ let token = "";
908
+ let tokenAmount = 0;
909
+ let type = "unknown";
910
+ if (message?.accountKeys?.[0]) {
911
+ from = typeof message.accountKeys[0] === "string" ? message.accountKeys[0] : message.accountKeys[0]?.pubkey || "";
912
+ }
913
+ for (const ix of instructions) {
914
+ if (ix.parsed) {
915
+ if (ix.parsed.type === "transfer") {
916
+ to = ix.parsed.info?.destination || "";
917
+ amount = parseInt(ix.parsed.info?.lamports || "0");
918
+ type = "transfer";
919
+ break;
920
+ } else if (ix.parsed.type === "transferChecked") {
921
+ to = ix.parsed.info?.destination || "";
922
+ token = ix.parsed.info?.mint || "";
923
+ tokenAmount = parseFloat(ix.parsed.info?.tokenAmount?.amount || "0");
924
+ type = "token-transfer";
925
+ break;
926
+ }
927
+ } else if (ix.program) {
928
+ if (ix.program === "system" || ix.programIdIndex === 0) {
929
+ type = "transfer";
930
+ } else if (ix.program === "token" || ix.programIdIndex === 2) {
931
+ type = "token-transfer";
932
+ } else if (ix.program === "stake") {
933
+ type = "staking";
934
+ }
935
+ }
936
+ }
937
+ if (type === "unknown" && message?.accountKeys) {
938
+ const tokenProgramIdx = message.accountKeys.findIndex(
939
+ (k) => k === "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" || typeof k === "object" && k?.pubkey === "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
940
+ );
941
+ if (tokenProgramIdx > -1) {
942
+ type = "token-transfer";
943
+ }
944
+ }
945
+ const blockTimeMs = tx.block_time ? Math.floor(tx.block_time / 1e3) : Date.now() * 1e3;
946
+ return {
947
+ signature: rawTx?.transaction?.signatures?.[0] || "",
948
+ slot: tx.block_slot,
949
+ blockTime: blockTimeMs,
950
+ fee: meta?.fee || 0,
951
+ status: meta?.err ? "failed" : "success",
952
+ type,
953
+ from,
954
+ to,
955
+ amount: amount > 0 ? amount / LAMPORTS_PER_SOL : void 0,
956
+ token,
957
+ tokenAmount: tokenAmount > 0 ? tokenAmount : void 0,
958
+ instructions: instructions.map((ix) => ix.parsed || ix)
959
+ };
960
+ });
961
+ }
962
+ /**
963
+ * Get portfolio with spam filtering option
964
+ */
965
+ async getFilteredPortfolio(address, filterOptions = {}) {
966
+ const simResponse = await this.getBalances(address, {
967
+ excludeSpamTokens: filterOptions.excludeSpamTokens,
968
+ excludeUnpriced: filterOptions.excludeUnpriced
969
+ });
970
+ let balances = simResponse.balances;
971
+ if (filterOptions.minLiquidity && filterOptions.minLiquidity > 0) {
972
+ balances = balances.filter(
973
+ (b) => !b.low_liquidity && (b.pool_size || 0) >= filterOptions.minLiquidity
974
+ );
975
+ }
976
+ return this.mapBalancesToPortfolio({
977
+ ...simResponse,
978
+ balances
979
+ });
980
+ }
981
+ // ============================================================
982
+ // EVM METHODS (using /v1/evm/* endpoints)
983
+ // ============================================================
984
+ /**
985
+ * Convert chain name to chain_id
986
+ */
987
+ getChainId(chainName) {
988
+ return EVM_CHAIN_IDS[chainName.toLowerCase()] || EVM_CHAIN_IDS[chainName] || 1;
989
+ }
990
+ /**
991
+ * Get EVM token balances for a wallet
992
+ * Endpoint: GET /v1/evm/balances/{address}
993
+ */
994
+ async getEvmBalances(address, options = {}) {
995
+ const cacheKey = `sim:evm:balances:${address}:${JSON.stringify(options)}`;
996
+ const cached = cache.get(cacheKey);
997
+ if (cached) return cached;
998
+ const params = new URLSearchParams();
999
+ if (options.chainIds) {
1000
+ if (Array.isArray(options.chainIds)) {
1001
+ params.set("chain_ids", options.chainIds.join(","));
1002
+ } else {
1003
+ params.set("chain_ids", options.chainIds.toString());
1004
+ }
1005
+ }
1006
+ if (options.filters) params.set("filters", options.filters);
1007
+ if (options.assetClass) params.set("asset_class", options.assetClass);
1008
+ if (options.excludeSpamTokens) params.set("exclude_spam_tokens", "true");
1009
+ if (options.excludeUnpriced) params.set("exclude_unpriced", "true");
1010
+ if (options.metadata) params.set("metadata", options.metadata);
1011
+ if (options.historicalPrices) params.set("historical_prices", options.historicalPrices);
1012
+ if (options.limit) params.set("limit", options.limit.toString());
1013
+ if (options.offset) params.set("offset", options.offset);
1014
+ const url = `${SIM_V1_BASE}/evm/balances/${address}${params.toString() ? "?" + params.toString() : ""}`;
1015
+ try {
1016
+ const data = await this.fetchWithAuth(url);
1017
+ cache.set(cacheKey, data, 60);
1018
+ return data;
1019
+ } catch (error) {
1020
+ console.error("[DuneSimClient] Error fetching EVM balances:", error);
1021
+ throw error;
1022
+ }
1023
+ }
1024
+ /**
1025
+ * Get EVM activity for a wallet
1026
+ * Endpoint: GET /v1/evm/activity/{address}
1027
+ */
1028
+ async getEvmActivity(address, options = {}) {
1029
+ const cacheKey = `sim:evm:activity:${address}:${JSON.stringify(options)}`;
1030
+ const cached = cache.get(cacheKey);
1031
+ if (cached) return cached;
1032
+ const params = new URLSearchParams();
1033
+ if (options.chainIds) {
1034
+ params.set("chain_ids", Array.isArray(options.chainIds) ? options.chainIds.join(",") : options.chainIds.toString());
1035
+ }
1036
+ if (options.activityType) params.set("activity_type", options.activityType);
1037
+ if (options.assetType) params.set("asset_type", options.assetType);
1038
+ if (options.tokenAddress) params.set("token_address", options.tokenAddress);
1039
+ if (options.limit) params.set("limit", options.limit.toString());
1040
+ if (options.offset) params.set("offset", options.offset);
1041
+ const url = `${SIM_V1_BASE}/evm/activity/${address}${params.toString() ? "?" + params.toString() : ""}`;
1042
+ try {
1043
+ const data = await this.fetchWithAuth(url);
1044
+ cache.set(cacheKey, data, 30);
1045
+ return data;
1046
+ } catch (error) {
1047
+ console.error("[DuneSimClient] Error fetching EVM activity:", error);
1048
+ throw error;
1049
+ }
1050
+ }
1051
+ /**
1052
+ * Get EVM transactions for a wallet
1053
+ * Endpoint: GET /v1/evm/transactions/{address}
1054
+ */
1055
+ async getEvmTransactions(address, options = {}) {
1056
+ const cacheKey = `sim:evm:txs:${address}:${JSON.stringify(options)}`;
1057
+ const cached = cache.get(cacheKey);
1058
+ if (cached) return cached;
1059
+ const params = new URLSearchParams();
1060
+ if (options.chainIds) {
1061
+ params.set("chain_ids", Array.isArray(options.chainIds) ? options.chainIds.join(",") : options.chainIds.toString());
1062
+ }
1063
+ if (options.limit) params.set("limit", options.limit.toString());
1064
+ if (options.offset) params.set("offset", options.offset);
1065
+ if (options.decode) params.set("decode", "true");
1066
+ const url = `${SIM_V1_BASE}/evm/transactions/${address}${params.toString() ? "?" + params.toString() : ""}`;
1067
+ try {
1068
+ const data = await this.fetchWithAuth(url);
1069
+ cache.set(cacheKey, data, 300);
1070
+ return data;
1071
+ } catch (error) {
1072
+ console.error("[DuneSimClient] Error fetching EVM transactions:", error);
1073
+ throw error;
1074
+ }
1075
+ }
1076
+ /**
1077
+ * Get EVM collectibles (NFTs) for a wallet
1078
+ * Endpoint: GET /v1/evm/collectibles/{address}
1079
+ */
1080
+ async getEvmCollectibles(address, options = {}) {
1081
+ const cacheKey = `sim:evm:collectibles:${address}:${JSON.stringify(options)}`;
1082
+ const cached = cache.get(cacheKey);
1083
+ if (cached) return cached;
1084
+ const params = new URLSearchParams();
1085
+ if (options.chainIds) {
1086
+ params.set("chain_ids", Array.isArray(options.chainIds) ? options.chainIds.join(",") : options.chainIds.toString());
1087
+ }
1088
+ if (options.limit) params.set("limit", options.limit.toString());
1089
+ if (options.offset) params.set("offset", options.offset);
1090
+ if (options.filterSpam === false) params.set("filter_spam", "false");
1091
+ if (options.showSpamScores) params.set("show_spam_scores", "true");
1092
+ const url = `${SIM_V1_BASE}/evm/collectibles/${address}${params.toString() ? "?" + params.toString() : ""}`;
1093
+ try {
1094
+ const data = await this.fetchWithAuth(url);
1095
+ cache.set(cacheKey, data, 300);
1096
+ return data;
1097
+ } catch (error) {
1098
+ console.error("[DuneSimClient] Error fetching EVM collectibles:", error);
1099
+ throw error;
1100
+ }
1101
+ }
1102
+ /**
1103
+ * Get token info for an EVM token
1104
+ * Endpoint: GET /v1/evm/token-info/{address}
1105
+ */
1106
+ async getEvmTokenInfo(tokenAddress, chainId = 1) {
1107
+ const cacheKey = `sim:evm:tokeninfo:${tokenAddress}:${chainId}`;
1108
+ const cached = cache.get(cacheKey);
1109
+ if (cached) return cached;
1110
+ const url = `${SIM_V1_BASE}/evm/token-info/${tokenAddress}?chain_ids=${chainId}`;
1111
+ try {
1112
+ const data = await this.fetchWithAuth(url);
1113
+ cache.set(cacheKey, data, 300);
1114
+ return data;
1115
+ } catch (error) {
1116
+ console.error("[DuneSimClient] Error fetching token info:", error);
1117
+ throw error;
1118
+ }
1119
+ }
1120
+ /**
1121
+ * Get stablecoin balances for a wallet
1122
+ * Endpoint: GET /v1/evm/stablecoins/{address}
1123
+ */
1124
+ async getEvmStablecoins(address, options = {}) {
1125
+ const cacheKey = `sim:evm:stablecoins:${address}:${JSON.stringify(options)}`;
1126
+ const cached = cache.get(cacheKey);
1127
+ if (cached) return cached;
1128
+ const params = new URLSearchParams();
1129
+ if (options.chainIds) {
1130
+ params.set("chain_ids", Array.isArray(options.chainIds) ? options.chainIds.join(",") : options.chainIds.toString());
1131
+ }
1132
+ if (options.excludeUnpriced) params.set("exclude_unpriced", "true");
1133
+ if (options.limit) params.set("limit", options.limit.toString());
1134
+ if (options.offset) params.set("offset", options.offset);
1135
+ const url = `${SIM_V1_BASE}/evm/balances/${address}/stablecoins${params.toString() ? "?" + params.toString() : ""}`;
1136
+ try {
1137
+ const data = await this.fetchWithAuth(url);
1138
+ cache.set(cacheKey, data, 60);
1139
+ return data;
1140
+ } catch (error) {
1141
+ console.error("[DuneSimClient] Error fetching stablecoins:", error);
1142
+ throw error;
1143
+ }
1144
+ }
1145
+ /**
1146
+ * Get full EVM portfolio (tokens + NFTs + activity summary)
1147
+ */
1148
+ async getEvmPortfolio(address, chainId = 1, options = {}) {
1149
+ const balancesResult = await this.getEvmBalances(address, { chainIds: chainId, excludeUnpriced: true, metadata: "logo" });
1150
+ let totalValue = 0;
1151
+ const tokens = [];
1152
+ let nativeBalance = "0";
1153
+ let nativeValue = 0;
1154
+ for (const bal of balancesResult.balances || []) {
1155
+ totalValue += bal.value_usd || 0;
1156
+ if (bal.address === "native") {
1157
+ nativeBalance = bal.amount;
1158
+ nativeValue = bal.value_usd || 0;
1159
+ } else {
1160
+ tokens.push({
1161
+ address: bal.address,
1162
+ balance: bal.amount,
1163
+ value_usd: bal.value_usd || 0,
1164
+ symbol: bal.symbol,
1165
+ name: bal.name,
1166
+ decimals: bal.decimals,
1167
+ price_usd: bal.price_usd || 0,
1168
+ pool_size: bal.pool_size,
1169
+ low_liquidity: bal.low_liquidity,
1170
+ logo: bal.token_metadata?.logo
1171
+ });
1172
+ }
1173
+ }
1174
+ const stablecoinsList = [];
1175
+ if (options.includeStablecoins) {
1176
+ try {
1177
+ const stableResult = await this.getEvmStablecoins(address, { chainIds: chainId });
1178
+ for (const bal of stableResult.balances || []) {
1179
+ stablecoinsList.push({
1180
+ address: bal.address,
1181
+ balance: bal.amount,
1182
+ value_usd: bal.value_usd || 0,
1183
+ symbol: bal.symbol
1184
+ });
1185
+ }
1186
+ } catch (e) {
1187
+ console.warn("[DuneSimClient] Stablecoins fetch failed:", e);
1188
+ }
1189
+ }
1190
+ const nftList = [];
1191
+ if (options.includeNfts) {
1192
+ try {
1193
+ const nftResult = await this.getEvmCollectibles(address, { chainIds: chainId, filterSpam: true });
1194
+ for (const nft of nftResult.entries || []) {
1195
+ nftList.push({
1196
+ contract_address: nft.contract_address,
1197
+ token_id: nft.token_id,
1198
+ name: nft.name,
1199
+ image_url: nft.image_url,
1200
+ collection: nft.symbol || nft.name,
1201
+ is_spam: nft.is_spam
1202
+ });
1203
+ }
1204
+ } catch (e) {
1205
+ console.warn("[DuneSimClient] Collectibles fetch failed:", e);
1206
+ }
1207
+ }
1208
+ let activitySummary;
1209
+ if (options.includeActivity) {
1210
+ try {
1211
+ const activityResult = await this.getEvmActivity(address, { chainIds: chainId, limit: 100 });
1212
+ let sends = 0, receives = 0, volume = 0;
1213
+ for (const act of activityResult.activity || []) {
1214
+ if (act.type === "send") sends++;
1215
+ if (act.type === "receive") receives++;
1216
+ volume += act.value_usd || 0;
1217
+ }
1218
+ activitySummary = {
1219
+ total_sends: sends,
1220
+ total_receives: receives,
1221
+ total_volume_usd: volume
1222
+ };
1223
+ } catch (e) {
1224
+ console.warn("[DuneSimClient] Activity fetch failed:", e);
1225
+ }
1226
+ }
1227
+ return {
1228
+ address,
1229
+ chain_id: chainId,
1230
+ total_value_usd: totalValue + nativeValue,
1231
+ native: {
1232
+ balance: nativeBalance,
1233
+ value_usd: nativeValue,
1234
+ symbol: "ETH"
1235
+ },
1236
+ tokens,
1237
+ stablecoins: options.includeStablecoins ? stablecoinsList : [],
1238
+ nfts: options.includeNfts ? nftList : void 0,
1239
+ activity_summary: options.includeActivity ? activitySummary : void 0,
1240
+ last_updated: (/* @__PURE__ */ new Date()).toISOString()
1241
+ };
1242
+ }
1243
+ };
1244
+ duneSimClient = new DuneSimClient();
1245
+ }
1246
+ });
1247
+
1248
+ // src/services/SolanaHeliusKeyPool.ts
1249
+ function buildEndpoints() {
1250
+ const eps = [];
1251
+ for (let i = 1; i <= 3; i++) {
1252
+ const key = process.env[`HELIUS_KEY_${i}`];
1253
+ if (key) {
1254
+ eps.push({ url: `https://mainnet.helius-rpc.com/?api-key=${key}`, label: `Helius-${i}` });
1255
+ }
1256
+ }
1257
+ const alchemyKey = process.env.ALCHEMY_SOLANA_API_KEY;
1258
+ if (alchemyKey) {
1259
+ eps.push({ url: `https://solana-mainnet.g.alchemy.com/v2/${alchemyKey}`, label: "Alchemy-Sol" });
1260
+ }
1261
+ return eps;
1262
+ }
1263
+ var CIRCUIT_OPEN_MS2, ERROR_THRESHOLD2, RpcKeyPool, ALL_ENDPOINTS, splitPoint, sigRpcPool, xferRpcPool;
1264
+ var init_SolanaHeliusKeyPool = __esm({
1265
+ "src/services/SolanaHeliusKeyPool.ts"() {
1266
+ CIRCUIT_OPEN_MS2 = 3e4;
1267
+ ERROR_THRESHOLD2 = 5;
1268
+ RpcKeyPool = class {
1269
+ slots = [];
1270
+ index = 0;
1271
+ constructor(endpoints, label) {
1272
+ this.slots = endpoints.filter((e) => e.url).map((e) => ({
1273
+ url: e.url,
1274
+ label: e.label,
1275
+ requestsThisMinute: 0,
1276
+ consecutiveErrors: 0,
1277
+ lastErrorAt: null,
1278
+ circuitOpen: false,
1279
+ circuitOpenUntil: null,
1280
+ avgLatencyMs: 0,
1281
+ totalRequests: 0
1282
+ }));
1283
+ if (this.slots.length === 0) {
1284
+ console.warn(`[RpcKeyPool:${label}] No endpoints configured`);
1285
+ } else {
1286
+ console.log(`[RpcKeyPool:${label}] Initialized with ${this.slots.length} endpoint(s): ${this.slots.map((s) => s.label).join(", ")}`);
1287
+ }
1288
+ setInterval(() => {
1289
+ for (const s of this.slots) s.requestsThisMinute = 0;
1290
+ }, 6e4).unref();
1291
+ }
1292
+ get size() {
1293
+ return this.slots.length;
1294
+ }
1295
+ get healthy() {
1296
+ return this.slots.filter((s) => !s.circuitOpen).length;
1297
+ }
1298
+ get isOperational() {
1299
+ return this.slots.length > 0 && this.slots.some((s) => !s.circuitOpen);
1300
+ }
1301
+ acquire() {
1302
+ if (this.slots.length === 0) {
1303
+ throw new Error("RpcKeyPool: No endpoints configured");
1304
+ }
1305
+ const now = Date.now();
1306
+ for (let attempt = 0; attempt < this.slots.length * 2; attempt++) {
1307
+ const slot = this.slots[this.index % this.slots.length];
1308
+ this.index++;
1309
+ if (slot.circuitOpen) {
1310
+ if (slot.circuitOpenUntil && now >= slot.circuitOpenUntil) {
1311
+ slot.circuitOpen = false;
1312
+ slot.consecutiveErrors = 0;
1313
+ return slot;
1314
+ }
1315
+ continue;
1316
+ }
1317
+ return slot;
1318
+ }
1319
+ throw new Error("RpcKeyPool: All endpoints are circuit-open");
1320
+ }
1321
+ /** Execute an RPC method through the pool */
1322
+ async rpc(method, params) {
1323
+ const maxRetries = 2;
1324
+ let lastError = null;
1325
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
1326
+ let slot;
1327
+ try {
1328
+ slot = this.acquire();
1329
+ } catch (e) {
1330
+ throw lastError || e;
1331
+ }
1332
+ const start = Date.now();
1333
+ slot.requestsThisMinute++;
1334
+ try {
1335
+ const res = await fetch(slot.url, {
1336
+ method: "POST",
1337
+ headers: { "Content-Type": "application/json" },
1338
+ body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params })
1339
+ });
1340
+ const data = await res.json();
1341
+ if (data.error) throw new Error(data.error.message);
1342
+ const latency = Date.now() - start;
1343
+ slot.consecutiveErrors = 0;
1344
+ slot.avgLatencyMs = slot.avgLatencyMs * 0.9 + latency * 0.1;
1345
+ slot.totalRequests++;
1346
+ return data.result;
1347
+ } catch (err2) {
1348
+ lastError = err2;
1349
+ slot.consecutiveErrors++;
1350
+ slot.lastErrorAt = Date.now();
1351
+ if (slot.consecutiveErrors >= ERROR_THRESHOLD2) {
1352
+ slot.circuitOpen = true;
1353
+ slot.circuitOpenUntil = Date.now() + CIRCUIT_OPEN_MS2;
1354
+ console.warn(`[RpcKeyPool] Circuit opened for ${slot.label}`);
1355
+ }
1356
+ if (err2.status === 429) {
1357
+ await new Promise((r) => setTimeout(r, 200 * (attempt + 1)));
1358
+ }
1359
+ if (attempt < maxRetries) {
1360
+ await new Promise((r) => setTimeout(r, 50 * (attempt + 1)));
1361
+ }
1362
+ }
1363
+ }
1364
+ throw lastError || new Error("RpcKeyPool: All retries exhausted");
1365
+ }
1366
+ stats() {
1367
+ return {
1368
+ totalSlots: this.slots.length,
1369
+ healthy: this.healthy,
1370
+ totalRequests: this.slots.reduce((s, k) => s + k.totalRequests, 0),
1371
+ avgLatencyMs: this.slots.reduce((s, k) => s + k.avgLatencyMs, 0) / (this.slots.length || 1)
1372
+ };
1373
+ }
1374
+ absorb(other) {
1375
+ const transferred = other.slots.filter((s) => !s.circuitOpen);
1376
+ for (const s of transferred) {
1377
+ this.slots.push(s);
1378
+ }
1379
+ console.log(`[RpcKeyPool] Absorbed ${transferred.length} endpoint(s) from another pool (now ${this.slots.length} total)`);
1380
+ }
1381
+ };
1382
+ ALL_ENDPOINTS = buildEndpoints();
1383
+ if (ALL_ENDPOINTS.length === 0) {
1384
+ console.warn("[RpcKeyPool] No Solana RPC endpoints found \u2014 Helius/Alchemy features will fail");
1385
+ } else {
1386
+ console.log(`[RpcKeyPool] Found ${ALL_ENDPOINTS.length} Solana RPC endpoint(s)`);
1387
+ }
1388
+ splitPoint = Math.max(1, Math.floor(ALL_ENDPOINTS.length * 0.66));
1389
+ sigRpcPool = new RpcKeyPool(ALL_ENDPOINTS.slice(0, splitPoint), "sig-rpc");
1390
+ xferRpcPool = new RpcKeyPool(ALL_ENDPOINTS.slice(splitPoint), "xfer-rpc");
1391
+ }
1392
+ });
1393
+
1394
+ // src/services/SolanaHeliusClient.ts
1395
+ var SolanaHeliusClient, solanaHeliusClient;
1396
+ var init_SolanaHeliusClient = __esm({
1397
+ "src/services/SolanaHeliusClient.ts"() {
1398
+ init_SolanaHeliusKeyPool();
1399
+ init_DuneSimClient();
1400
+ SolanaHeliusClient = class _SolanaHeliusClient {
1401
+ static instance;
1402
+ LAMPORTS = 1e9;
1403
+ constructor() {
1404
+ }
1405
+ static getInstance() {
1406
+ if (!_SolanaHeliusClient.instance) {
1407
+ _SolanaHeliusClient.instance = new _SolanaHeliusClient();
1408
+ }
1409
+ return _SolanaHeliusClient.instance;
1410
+ }
1411
+ // ================================================================
1412
+ // DAS API methods (general-purpose)
1413
+ // ================================================================
1414
+ async dasRequest(method, params) {
1415
+ return sigRpcPool.rpc(method, params);
1416
+ }
1417
+ async getTokenMetadata(mint) {
1418
+ return this.dasRequest("getTokenMetadata", [mint]);
1419
+ }
1420
+ async getAsset({ id }) {
1421
+ return this.dasRequest("getAsset", [{ id }]);
1422
+ }
1423
+ async getAssetsByOwner({ owner, limit = 100 }) {
1424
+ return this.dasRequest("getAssetsByOwner", [{ owner, limit, sortBy: { sortBy: "updated", descending: true } }]);
1425
+ }
1426
+ async getAssetsByGroup({ groupKey, groupValue, limit = 100 }) {
1427
+ return this.dasRequest("getAssetsByGroup", [{ groupKey, groupValue, limit, sortBy: { sortBy: "updated", descending: true } }]);
1428
+ }
1429
+ async searchAssets({ query, limit = 50 }) {
1430
+ return this.dasRequest("searchAssets", [{ query: { $text: query }, limit, sortBy: { sortBy: "relevant", descending: false } }]);
1431
+ }
1432
+ // ================================================================
1433
+ // Helius-exclusive: getTransactionsForAddress (via sigRpcPool — 66% of endpoints)
1434
+ // ================================================================
1435
+ async getTransactionsForAddress(address, options = {}) {
1436
+ const result = await sigRpcPool.rpc("getTransactionsForAddress", [
1437
+ address,
1438
+ {
1439
+ transactionDetails: options.transactionDetails || "signatures",
1440
+ limit: options.limit || 1e3,
1441
+ sortOrder: options.sortOrder || "desc",
1442
+ ...options.paginationToken ? { paginationToken: options.paginationToken } : {}
1443
+ }
1444
+ ]);
1445
+ return {
1446
+ data: result?.data || [],
1447
+ paginationToken: result?.paginationToken || void 0
1448
+ };
1449
+ }
1450
+ // ================================================================
1451
+ // Helius-exclusive: getTransfersByAddress (via xferRpcPool — 33% of endpoints)
1452
+ // ================================================================
1453
+ async getTransfersByAddress(address, options = {}, poolOverride) {
1454
+ const pool = poolOverride || xferRpcPool;
1455
+ try {
1456
+ const result = await pool.rpc("getTransfersByAddress", [
1457
+ address,
1458
+ {
1459
+ limit: options.limit || 100,
1460
+ ...options.paginationToken ? { paginationToken: options.paginationToken } : {}
1461
+ }
1462
+ ]);
1463
+ return {
1464
+ data: result?.data || [],
1465
+ paginationToken: result?.paginationToken || void 0
1466
+ };
1467
+ } catch (err2) {
1468
+ if (err2.message?.includes?.("Unsupported method")) {
1469
+ throw new Error("only available on paid Helius plan");
1470
+ }
1471
+ throw err2;
1472
+ }
1473
+ }
1474
+ // ================================================================
1475
+ // Full wallet scan: parallel sigs + transfers with lazy absorption
1476
+ // ================================================================
1477
+ async scanWallet(address) {
1478
+ const start = Date.now();
1479
+ const [sigsResult, transfersResult] = await Promise.all([
1480
+ this.getAllSignatures(address),
1481
+ this.getAllTransfersWithAbsorb(address)
1482
+ ]);
1483
+ return {
1484
+ signatures: sigsResult,
1485
+ transfers: transfersResult,
1486
+ totalTime: Date.now() - start
1487
+ };
1488
+ }
1489
+ async getAllSignatures(address) {
1490
+ const all = [];
1491
+ let paginationToken;
1492
+ do {
1493
+ const result = await this.getTransactionsForAddress(address, {
1494
+ transactionDetails: "signatures",
1495
+ limit: 1e3,
1496
+ sortOrder: "desc",
1497
+ paginationToken
1498
+ });
1499
+ all.push(...result.data);
1500
+ paginationToken = result.paginationToken;
1501
+ } while (paginationToken && all.length < 5e4);
1502
+ return all.sort((a, b) => a.blockTime - b.blockTime);
1503
+ }
1504
+ async getAllTransfersWithAbsorb(address) {
1505
+ const all = [];
1506
+ let paginationToken;
1507
+ let sigsDone = false;
1508
+ const sigsDonePromise = sigRpcPool.size > 0 ? this.watchForSigPoolIdle() : Promise.resolve();
1509
+ const sigsDoneRace = sigsDonePromise.then(() => {
1510
+ sigsDone = true;
1511
+ });
1512
+ do {
1513
+ const result = await this.getTransfersByAddress(address, { limit: 100, paginationToken });
1514
+ all.push(...result.data);
1515
+ paginationToken = result.paginationToken;
1516
+ if (sigsDone && sigRpcPool.healthy > 0) {
1517
+ xferRpcPool.absorb(sigRpcPool);
1518
+ sigsDone = false;
1519
+ }
1520
+ } while (paginationToken && all.length < 1e4);
1521
+ return all;
1522
+ }
1523
+ async watchForSigPoolIdle() {
1524
+ let prev = 0;
1525
+ let stableCount = 0;
1526
+ for (let i = 0; i < 30; i++) {
1527
+ await new Promise((r) => setTimeout(r, 800));
1528
+ const cur = sigRpcPool.stats().totalRequests;
1529
+ if (cur === prev) {
1530
+ stableCount++;
1531
+ if (stableCount >= 3) return;
1532
+ } else {
1533
+ stableCount = 0;
1534
+ }
1535
+ prev = cur;
1536
+ }
1537
+ }
1538
+ async getAllTransfers(address) {
1539
+ const all = [];
1540
+ let paginationToken;
1541
+ do {
1542
+ const result = await this.getTransfersByAddress(address, { limit: 100, paginationToken });
1543
+ all.push(...result.data);
1544
+ paginationToken = result.paginationToken;
1545
+ } while (paginationToken && all.length < 1e4);
1546
+ return all;
1547
+ }
1548
+ getPoolStats() {
1549
+ return { signaturesPool: sigRpcPool.stats(), transfersPool: xferRpcPool.stats() };
1550
+ }
1551
+ // ================================================================
1552
+ // STANDARD RPC METHODS — work on free Helius AND Alchemy
1553
+ // ================================================================
1554
+ async getSignaturesForAddressStdRpc(address, options = {}) {
1555
+ const result = await sigRpcPool.rpc("getSignaturesForAddress", [
1556
+ address,
1557
+ {
1558
+ limit: options.limit || 100,
1559
+ commitment: "confirmed",
1560
+ ...options.before ? { before: options.before } : {}
1561
+ }
1562
+ ]);
1563
+ return (result || []).map((s) => ({
1564
+ signature: s.signature,
1565
+ blockTime: s.blockTime || 0,
1566
+ err: s.err,
1567
+ slot: s.slot
1568
+ }));
1569
+ }
1570
+ async getTransactionStdRpc(signature) {
1571
+ return sigRpcPool.rpc("getTransaction", [
1572
+ signature,
1573
+ { commitment: "confirmed", maxSupportedTransactionVersion: 0 }
1574
+ ]);
1575
+ }
1576
+ async getBalanceStdRpc(address) {
1577
+ const result = await sigRpcPool.rpc("getBalance", [address]);
1578
+ return result?.value || 0;
1579
+ }
1580
+ // ================================================================
1581
+ // FALLBACK SCAN PATH — tries SIM first, then RPC
1582
+ // ================================================================
1583
+ async scanWalletFallback(address) {
1584
+ const start = Date.now();
1585
+ if (duneSimClient.isEnabled()) {
1586
+ try {
1587
+ return await this.scanWalletFallbackViaSim(address, start);
1588
+ } catch (simErr) {
1589
+ console.log("[SolanaHelius] SIM fallback failed, using RPC:", simErr?.message);
1590
+ }
1591
+ }
1592
+ return this.scanWalletFallbackViaRpc(address, start);
1593
+ }
1594
+ async scanWalletFallbackViaSim(address, start) {
1595
+ const simTxs = await duneSimClient.getTransactions(address, { limit: 1e3 });
1596
+ const txs = duneSimClient.mapTransactions(simTxs);
1597
+ const signatures = txs.map((t) => ({
1598
+ signature: t.signature,
1599
+ blockTime: Math.floor(t.blockTime / 1e3),
1600
+ err: t.status === "failed" ? { msg: "failed" } : null
1601
+ }));
1602
+ let totalSOLSent = 0;
1603
+ let totalSOLReceived = 0;
1604
+ const interactors = {};
1605
+ for (const tx of txs) {
1606
+ if (tx.from === address) {
1607
+ totalSOLSent += tx.amount || 0;
1608
+ if (tx.to) interactors[tx.to] = (interactors[tx.to] || 0) + 1;
1609
+ }
1610
+ if (tx.to === address) {
1611
+ totalSOLReceived += tx.amount || 0;
1612
+ if (tx.from) interactors[tx.from] = (interactors[tx.from] || 0) + 1;
1613
+ }
1614
+ }
1615
+ const topInteractors = Object.entries(interactors).sort((a, b) => b[1] - a[1]).slice(0, 10).map(([address2, count]) => ({ address: address2, count }));
1616
+ return { signatures, topInteractors, totalSOLSent, totalSOLReceived, totalTime: Date.now() - start };
1617
+ }
1618
+ async scanWalletFallbackViaRpc(address, start) {
1619
+ const allSigs = [];
1620
+ let before;
1621
+ while (before !== void 0 || allSigs.length === 0) {
1622
+ const page = await this.getSignaturesForAddressStdRpc(address, { limit: 1e3, before });
1623
+ if (page.length === 0) break;
1624
+ allSigs.push(...page);
1625
+ before = page[page.length - 1].signature;
1626
+ }
1627
+ allSigs.sort((a, b) => a.blockTime - b.blockTime);
1628
+ const parseCount = Math.min(allSigs.length, 50);
1629
+ const recentSigs = allSigs.slice(-parseCount).reverse();
1630
+ const txResults = await Promise.allSettled(recentSigs.map((s) => this.getTransactionStdRpc(s.signature)));
1631
+ let totalSOLSent = 0;
1632
+ let totalSOLReceived = 0;
1633
+ const interactors = {};
1634
+ for (const result of txResults) {
1635
+ if (result.status !== "fulfilled" || !result.value) continue;
1636
+ const tx = result.value;
1637
+ const meta = tx.meta;
1638
+ if (!meta) continue;
1639
+ const preBal = meta.preBalances || [];
1640
+ const postBal = meta.postBalances || [];
1641
+ const accountKeys = tx.transaction?.message?.accountKeys || [];
1642
+ if (preBal.length > 0 && postBal.length > 0) {
1643
+ const diff = (preBal[0] - postBal[0]) / this.LAMPORTS;
1644
+ if (diff > 1e-4) totalSOLSent += diff;
1645
+ else if (diff < -1e-4) totalSOLReceived += Math.abs(diff);
1646
+ }
1647
+ for (let i = 1; i < accountKeys.length - 1; i++) {
1648
+ const pk = accountKeys[i]?.pubkey || accountKeys[i];
1649
+ if (pk && typeof pk === "string" && pk !== address) {
1650
+ interactors[pk] = (interactors[pk] || 0) + 1;
1651
+ }
1652
+ }
1653
+ }
1654
+ const topInteractors = Object.entries(interactors).sort((a, b) => b[1] - a[1]).slice(0, 10).map(([address2, count]) => ({ address: address2, count }));
1655
+ return { signatures: allSigs, topInteractors, totalSOLSent, totalSOLReceived, totalTime: Date.now() - start };
1656
+ }
1657
+ // ================================================================
1658
+ // FALLBACK FUNDING TREE — tries SIM first, then RPC
1659
+ // ================================================================
1660
+ async getFundingTreeFallback(address, maxTransactions = 200) {
1661
+ if (duneSimClient.isEnabled()) {
1662
+ try {
1663
+ return await this.getFundingTreeViaSim(address);
1664
+ } catch (simErr) {
1665
+ console.log("[SolanaHelius] SIM funding tree failed, using RPC:", simErr?.message);
1666
+ }
1667
+ }
1668
+ return this.getFundingTreeViaRpc(address, maxTransactions);
1669
+ }
1670
+ async getFundingTreeViaSim(address) {
1671
+ const simTxs = await duneSimClient.getTransactions(address, { limit: 500 });
1672
+ const txs = duneSimClient.mapTransactions(simTxs);
1673
+ const sources = {};
1674
+ const destinations = {};
1675
+ for (const tx of txs) {
1676
+ if (tx.to === address && tx.from) {
1677
+ if (!sources[tx.from]) sources[tx.from] = { total: 0, count: 0 };
1678
+ sources[tx.from].total += tx.amount || 0;
1679
+ sources[tx.from].count += 1;
1680
+ }
1681
+ if (tx.from === address && tx.to) {
1682
+ if (!destinations[tx.to]) destinations[tx.to] = { total: 0, count: 0 };
1683
+ destinations[tx.to].total += tx.amount || 0;
1684
+ destinations[tx.to].count += 1;
1685
+ }
1686
+ }
1687
+ return { sources, destinations };
1688
+ }
1689
+ async getFundingTreeViaRpc(address, maxTransactions = 200) {
1690
+ const sigs = await this.getSignaturesForAddressStdRpc(address, { limit: Math.min(maxTransactions, 1e3) });
1691
+ if (sigs.length === 0) return { sources: {}, destinations: {} };
1692
+ const txResults = await Promise.allSettled(sigs.map((s) => this.getTransactionStdRpc(s.signature)));
1693
+ const sources = {};
1694
+ const destinations = {};
1695
+ for (const result of txResults) {
1696
+ if (result.status !== "fulfilled" || !result.value) continue;
1697
+ const tx = result.value;
1698
+ const meta = tx.meta;
1699
+ if (!meta) continue;
1700
+ const preBal = meta.preBalances || [];
1701
+ const postBal = meta.postBalances || [];
1702
+ const accountKeys = tx.transaction?.message?.accountKeys || [];
1703
+ if (preBal.length < 2 || postBal.length < 2) continue;
1704
+ const diff = (preBal[0] - postBal[0]) / this.LAMPORTS;
1705
+ if (Math.abs(diff) < 1e-4) continue;
1706
+ const feePayer = accountKeys[0]?.pubkey || accountKeys[0] || "";
1707
+ if (feePayer === address) {
1708
+ if (accountKeys.length > 1) {
1709
+ const dest = accountKeys[1]?.pubkey || accountKeys[1] || "unknown";
1710
+ if (!destinations[dest]) destinations[dest] = { total: 0, count: 0 };
1711
+ destinations[dest].total += diff;
1712
+ destinations[dest].count += 1;
1713
+ }
1714
+ } else if (feePayer) {
1715
+ if (!sources[feePayer]) sources[feePayer] = { total: 0, count: 0 };
1716
+ sources[feePayer].total += Math.abs(diff);
1717
+ sources[feePayer].count += 1;
1718
+ }
1719
+ }
1720
+ return { sources, destinations };
1721
+ }
1722
+ };
1723
+ solanaHeliusClient = SolanaHeliusClient.getInstance();
1724
+ }
1725
+ });
1726
+
1727
+ // src/services/SolanaPortfolioService.ts
1728
+ var SolanaPortfolioService_exports = {};
1729
+ __export(SolanaPortfolioService_exports, {
1730
+ SolanaPortfolioService: () => SolanaPortfolioService,
1731
+ solanaPortfolioService: () => solanaPortfolioService
1732
+ });
1733
+ import fetch4 from "node-fetch";
1734
+ var LAMPORTS_PER_SOL2, JUPITER_PRICE_API, SolanaPortfolioService, solanaPortfolioService;
1735
+ var init_SolanaPortfolioService = __esm({
1736
+ "src/services/SolanaPortfolioService.ts"() {
1737
+ init_SolanaKeyPoolManager();
1738
+ init_cache();
1739
+ init_DuneSimClient();
1740
+ init_SolanaHeliusClient();
1741
+ LAMPORTS_PER_SOL2 = 1e9;
1742
+ JUPITER_PRICE_API = "https://price.jup.ag/v6/price";
1743
+ SolanaPortfolioService = class {
1744
+ priceCache = /* @__PURE__ */ new Map();
1745
+ /**
1746
+ * Helius-powered full overlay scan: signatures + transfers in parallel
1747
+ */
1748
+ async scanOverview(address) {
1749
+ const start = Date.now();
1750
+ const helius = solanaHeliusClient;
1751
+ const scan = await helius.scanWallet(address);
1752
+ const signatures = scan.signatures;
1753
+ const transfers = scan.transfers;
1754
+ const LAMPORTS = 1e9;
1755
+ const firstSig = signatures[0];
1756
+ const lastSig = signatures[signatures.length - 1];
1757
+ const firstTimestamp = firstSig?.blockTime ? new Date(firstSig.blockTime * 1e3).toISOString() : "";
1758
+ const lastTimestamp = lastSig?.blockTime ? new Date(lastSig.blockTime * 1e3).toISOString() : "";
1759
+ const activityPeriodDays = firstSig?.blockTime && lastSig?.blockTime ? Math.round((lastSig.blockTime - firstSig.blockTime) / 86400) : 0;
1760
+ let totalSent = 0;
1761
+ let totalReceived = 0;
1762
+ const interactors = {};
1763
+ for (const t of transfers) {
1764
+ const isSol = !t.mint || t.mint === "So11111111111111111111111111111111111111112";
1765
+ const amt = isSol ? t.amount / LAMPORTS : t.amount;
1766
+ if (t.source === address) {
1767
+ totalSent += amt;
1768
+ interactors[t.destination] = (interactors[t.destination] || 0) + 1;
1769
+ } else if (t.destination === address) {
1770
+ totalReceived += amt;
1771
+ interactors[t.source] = (interactors[t.source] || 0) + 1;
1772
+ }
1773
+ }
1774
+ const topInteractors = Object.entries(interactors).sort((a, b) => b[1] - a[1]).slice(0, 10).map(([address2, count]) => ({ address: address2, count }));
1775
+ const uniqueAddresses = Object.keys(interactors).slice(0, 200);
1776
+ return {
1777
+ wallet: address,
1778
+ firstTimestamp,
1779
+ lastTimestamp,
1780
+ activityPeriodDays,
1781
+ totalTransactions: signatures.length,
1782
+ totalSOLSent: totalSent.toFixed(6),
1783
+ totalSOLReceived: totalReceived.toFixed(6),
1784
+ uniqueAddressCount: Object.keys(interactors).length,
1785
+ uniqueAddresses,
1786
+ topInteractors,
1787
+ scanTimeMs: Date.now() - start
1788
+ };
1789
+ }
1790
+ /**
1791
+ * Free-tier fallback: uses getSignaturesForAddress RPC + batch getTransaction
1792
+ * No reliance on paid Helius features
1793
+ */
1794
+ async scanOverviewFallback(address) {
1795
+ const start = Date.now();
1796
+ const helius = solanaHeliusClient;
1797
+ const result = await helius.scanWalletFallback(address);
1798
+ const signatures = result.signatures;
1799
+ const firstSig = signatures[0];
1800
+ const lastSig = signatures[signatures.length - 1];
1801
+ const firstTimestamp = firstSig?.blockTime ? new Date(firstSig.blockTime * 1e3).toISOString() : "";
1802
+ const lastTimestamp = lastSig?.blockTime ? new Date(lastSig.blockTime * 1e3).toISOString() : "";
1803
+ const activityPeriodDays = firstSig?.blockTime && lastSig?.blockTime ? Math.round((lastSig.blockTime - firstSig.blockTime) / 86400) : 0;
1804
+ return {
1805
+ wallet: address,
1806
+ firstTimestamp,
1807
+ lastTimestamp,
1808
+ activityPeriodDays,
1809
+ totalTransactions: signatures.length,
1810
+ totalSOLSent: result.totalSOLSent.toFixed(6),
1811
+ totalSOLReceived: result.totalSOLReceived.toFixed(6),
1812
+ uniqueAddressCount: result.topInteractors.length,
1813
+ uniqueAddresses: result.topInteractors.map((i) => i.address).slice(0, 200),
1814
+ topInteractors: result.topInteractors,
1815
+ scanTimeMs: Date.now() - start
1816
+ };
1817
+ }
1818
+ /**
1819
+ * Get portfolio - tries SIM first, falls back to existing RPC-based method
1820
+ */
1821
+ async getPortfolio(address, filterOptions) {
1822
+ const cacheKey = `solana:portfolio:${address}:${JSON.stringify(filterOptions || {})}`;
1823
+ const cached = cache.get(cacheKey);
1824
+ if (cached) return cached;
1825
+ if (duneSimClient.isEnabled()) {
1826
+ try {
1827
+ console.log("[SolanaPortfolio] Trying SIM for portfolio...");
1828
+ const simPortfolio = filterOptions?.excludeSpamTokens || filterOptions?.minLiquidity ? await duneSimClient.getFilteredPortfolio(address, {
1829
+ excludeSpamTokens: filterOptions?.excludeSpamTokens,
1830
+ excludeUnpriced: filterOptions?.excludeUnpriced,
1831
+ minLiquidity: filterOptions?.minLiquidity
1832
+ }) : await duneSimClient.getBalances(address).then((r) => duneSimClient.mapBalancesToPortfolio(r));
1833
+ const portfolio = {
1834
+ address: simPortfolio.address,
1835
+ sol: simPortfolio.sol,
1836
+ tokens: simPortfolio.tokens.map((t) => ({
1837
+ mint: t.mint,
1838
+ amount: t.amount,
1839
+ decimals: t.decimals,
1840
+ uiAmount: t.uiAmount,
1841
+ symbol: t.symbol,
1842
+ name: t.name,
1843
+ logoUrl: t.logoUrl,
1844
+ price: t.price,
1845
+ value: t.value
1846
+ })),
1847
+ staking: [],
1848
+ // Would need additional SIM call
1849
+ totalUsd: simPortfolio.totalUsd,
1850
+ fetchedAt: simPortfolio.fetchedAt
1851
+ };
1852
+ console.log("[SolanaPortfolio] SIM portfolio fetched successfully");
1853
+ cache.set(cacheKey, portfolio, 60);
1854
+ return portfolio;
1855
+ } catch (simError) {
1856
+ console.error("[SolanaPortfolio] SIM failed, falling back to RPC:", simError);
1857
+ }
1858
+ }
1859
+ return this.getPortfolioFallback(address);
1860
+ }
1861
+ /**
1862
+ * Original RPC-based portfolio (fallback)
1863
+ */
1864
+ async getPortfolioFallback(address) {
1865
+ const cacheKey = `solana:portfolio:fallback:${address}`;
1866
+ const [balance, tokenAccounts, stakeAccounts] = await Promise.all([
1867
+ this.getBalance(address),
1868
+ this.getTokenAccounts(address),
1869
+ this.getStakeAccounts(address)
1870
+ ]);
1871
+ const mints = tokenAccounts.map((t) => t.mint).filter(Boolean);
1872
+ const prices = await this.getBatchPrices(mints);
1873
+ const tokens = tokenAccounts.map((t) => {
1874
+ const price = prices[t.mint] || 0;
1875
+ return {
1876
+ ...t,
1877
+ price,
1878
+ value: t.uiAmount * price
1879
+ };
1880
+ }).filter((t) => t.uiAmount > 0);
1881
+ let solPrice = prices["So11111111111111111111111111111111111111112"] || 0;
1882
+ if (solPrice === 0) {
1883
+ solPrice = await this.getSolPrice();
1884
+ }
1885
+ const totalUsd = balance / LAMPORTS_PER_SOL2 * solPrice + tokens.reduce((sum, t) => sum + (t.value || 0), 0);
1886
+ const portfolio = {
1887
+ address,
1888
+ sol: {
1889
+ lamports: balance,
1890
+ sol: balance / LAMPORTS_PER_SOL2,
1891
+ usd: balance / LAMPORTS_PER_SOL2 * solPrice
1892
+ },
1893
+ tokens,
1894
+ staking: stakeAccounts,
1895
+ totalUsd,
1896
+ fetchedAt: Date.now()
1897
+ };
1898
+ cache.set(cacheKey, portfolio, 60);
1899
+ return portfolio;
1900
+ }
1901
+ async getBalance(address) {
1902
+ return solanaKeyPool.execute(async (endpoint) => {
1903
+ const res = await fetch4(endpoint, {
1904
+ method: "POST",
1905
+ headers: { "Content-Type": "application/json" },
1906
+ body: JSON.stringify({
1907
+ jsonrpc: "2.0",
1908
+ id: 1,
1909
+ method: "getBalance",
1910
+ params: [address]
1911
+ })
1912
+ });
1913
+ const data = await res.json();
1914
+ return data.result?.value || 0;
1915
+ }, 1);
1916
+ }
1917
+ async getTokenAccounts(address) {
1918
+ const tokens = [];
1919
+ try {
1920
+ const alchemyTokens = await this.getTokensFromAlchemy(address);
1921
+ tokens.push(...alchemyTokens);
1922
+ } catch (e) {
1923
+ console.error("[SolanaPortfolio] Alchemy token fetch failed:", e);
1924
+ }
1925
+ if (tokens.length === 0) {
1926
+ try {
1927
+ const [token2022, token2022Program] = await Promise.all([
1928
+ this.getTokensByProgram(address, "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"),
1929
+ this.getTokensByProgram(address, "TokenzQdBNbLqP5VEhdkAS6dFvwzYqE8hpzEfb9Kh")
1930
+ ]);
1931
+ tokens.push(...token2022, ...token2022Program);
1932
+ } catch (e) {
1933
+ console.error("[SolanaPortfolio] RPC token fetch failed:", e);
1934
+ }
1935
+ }
1936
+ return tokens;
1937
+ }
1938
+ async getTokensFromAlchemy(address) {
1939
+ const response = await solanaKeyPool.execute(async (endpoint) => {
1940
+ return fetch4(endpoint, {
1941
+ method: "POST",
1942
+ headers: { "Content-Type": "application/json" },
1943
+ body: JSON.stringify({
1944
+ jsonrpc: "2.0",
1945
+ id: 1,
1946
+ method: "getAssetsByOwner",
1947
+ params: [{
1948
+ owner: address,
1949
+ options: {
1950
+ limit: 100,
1951
+ showMetadata: true,
1952
+ showNativeBalance: true
1953
+ }
1954
+ }]
1955
+ })
1956
+ });
1957
+ }, 3);
1958
+ const data = await response.json();
1959
+ const items = data?.result?.assets || [];
1960
+ return items.filter((item) => item.interface === "Token" || item.tokenStandard === "Fungible").map((item) => ({
1961
+ mint: item.id,
1962
+ amount: BigInt(item.tokenInfo?.amount || 0),
1963
+ decimals: item.tokenInfo?.decimals || 0,
1964
+ uiAmount: item.tokenInfo?.amount ? parseFloat(item.tokenInfo.amount) : 0
1965
+ })).filter((t) => t.uiAmount > 0);
1966
+ }
1967
+ async getTokensByProgram(address, programId) {
1968
+ try {
1969
+ return await solanaKeyPool.execute(async (endpoint) => {
1970
+ const res = await fetch4(endpoint, {
1971
+ method: "POST",
1972
+ headers: { "Content-Type": "application/json" },
1973
+ body: JSON.stringify({
1974
+ jsonrpc: "2.0",
1975
+ id: 1,
1976
+ method: "getParsedTokenAccountsByOwner",
1977
+ params: [address, { programId }]
1978
+ })
1979
+ });
1980
+ const data = await res.json();
1981
+ const accounts = data.result?.value || [];
1982
+ return accounts.map((acc) => {
1983
+ const info = acc.account.data.parsed.info;
1984
+ return {
1985
+ mint: info.mint,
1986
+ amount: BigInt(info.tokenAmount.amount),
1987
+ decimals: info.tokenAmount.decimals,
1988
+ uiAmount: info.tokenAmount.uiAmount || 0
1989
+ };
1990
+ }).filter((t) => t.uiAmount > 0);
1991
+ }, 10);
1992
+ } catch (e) {
1993
+ console.error(`[SolanaPortfolio] Error fetching tokens for program ${programId}:`, e);
1994
+ return [];
1995
+ }
1996
+ }
1997
+ async getStakeAccounts(address) {
1998
+ try {
1999
+ return await solanaKeyPool.execute(async (endpoint) => {
2000
+ const res = await fetch4(endpoint, {
2001
+ method: "POST",
2002
+ headers: { "Content-Type": "application/json" },
2003
+ body: JSON.stringify({
2004
+ jsonrpc: "2.0",
2005
+ id: 1,
2006
+ method: "getStakeAccounts",
2007
+ params: [address]
2008
+ })
2009
+ });
2010
+ const data = await res.json();
2011
+ return data.result || [];
2012
+ }, 5);
2013
+ } catch (e) {
2014
+ return [];
2015
+ }
2016
+ }
2017
+ /**
2018
+ * Get transactions - uses SIM as primary
2019
+ */
2020
+ async getTransactions(address, limit = 100) {
2021
+ const cacheKey = `solana:txs:${address}:${limit}`;
2022
+ const cached = cache.get(cacheKey);
2023
+ if (cached) return cached;
2024
+ if (duneSimClient.isEnabled()) {
2025
+ try {
2026
+ console.log("[SolanaPortfolio] Trying SIM for transactions...");
2027
+ const simResponse = await duneSimClient.getTransactions(address, { limit });
2028
+ const txs2 = duneSimClient.mapTransactions(simResponse);
2029
+ const transactions2 = txs2.map((tx) => ({
2030
+ signature: tx.signature,
2031
+ slot: tx.slot,
2032
+ blockTime: tx.blockTime,
2033
+ fee: tx.fee,
2034
+ status: tx.status,
2035
+ type: tx.type,
2036
+ from: tx.from,
2037
+ to: tx.to,
2038
+ amount: tx.amount,
2039
+ token: tx.token,
2040
+ tokenAmount: tx.tokenAmount,
2041
+ instructions: tx.instructions
2042
+ }));
2043
+ console.log("[SolanaPortfolio] SIM transactions fetched successfully");
2044
+ cache.set(cacheKey, transactions2, 300);
2045
+ return transactions2;
2046
+ } catch (simError) {
2047
+ console.error("[SolanaPortfolio] SIM transactions failed, falling back to RPC:", simError);
2048
+ }
2049
+ }
2050
+ const signatures = await this.getSignatures(address, limit);
2051
+ if (signatures.length === 0) return [];
2052
+ const transactions = await Promise.all(
2053
+ signatures.map((sig) => this.getTransaction(sig))
2054
+ );
2055
+ const txs = transactions.filter(Boolean);
2056
+ cache.set(cacheKey, txs, 300);
2057
+ return txs;
2058
+ }
2059
+ async getSignatures(address, limit) {
2060
+ return solanaKeyPool.execute(async (endpoint) => {
2061
+ const res = await fetch4(endpoint, {
2062
+ method: "POST",
2063
+ headers: { "Content-Type": "application/json" },
2064
+ body: JSON.stringify({
2065
+ jsonrpc: "2.0",
2066
+ id: 1,
2067
+ method: "getSignaturesForAddress",
2068
+ params: [address, { limit, commitment: "confirmed" }]
2069
+ })
2070
+ });
2071
+ const data = await res.json();
2072
+ return data.result?.map((s) => s.signature) || [];
2073
+ }, 1);
2074
+ }
2075
+ async getTransaction(signature) {
2076
+ try {
2077
+ return await solanaKeyPool.execute(async (endpoint) => {
2078
+ const res = await fetch4(endpoint, {
2079
+ method: "POST",
2080
+ headers: { "Content-Type": "application/json" },
2081
+ body: JSON.stringify({
2082
+ jsonrpc: "2.0",
2083
+ id: 1,
2084
+ method: "getTransaction",
2085
+ params: [signature, { commitment: "confirmed", maxSupportedTransactionVersion: 0 }]
2086
+ })
2087
+ });
2088
+ const data = await res.json();
2089
+ if (!data.result) return null;
2090
+ const tx = data.result;
2091
+ const meta = tx.meta;
2092
+ const instructions = tx.transaction.message.instructions;
2093
+ let from = "";
2094
+ let to = "";
2095
+ let amount = 0;
2096
+ let token = "";
2097
+ let tokenAmount = 0;
2098
+ if (tx.transaction.message.accountKeys?.[0]) {
2099
+ from = tx.transaction.message.accountKeys[0].pubkey;
2100
+ }
2101
+ for (const ix of instructions) {
2102
+ if (ix.parsed) {
2103
+ if (ix.parsed.type === "transfer") {
2104
+ to = ix.parsed.info.destination;
2105
+ amount = ix.parsed.info.lamports || 0;
2106
+ } else if (ix.parsed.type === "transferChecked") {
2107
+ to = ix.parsed.info.destination;
2108
+ token = ix.parsed.info.mint;
2109
+ tokenAmount = parseFloat(ix.parsed.info.tokenAmount?.amount || "0");
2110
+ }
2111
+ }
2112
+ }
2113
+ return {
2114
+ signature,
2115
+ slot: tx.slot,
2116
+ blockTime: tx.blockTime * 1e3,
2117
+ fee: meta?.fee || 0,
2118
+ status: meta?.err ? "failed" : "success",
2119
+ type: this.inferTransactionType(instructions),
2120
+ from,
2121
+ to,
2122
+ amount: amount > 0 ? amount / LAMPORTS_PER_SOL2 : void 0,
2123
+ token,
2124
+ tokenAmount: tokenAmount > 0 ? tokenAmount : void 0,
2125
+ instructions: instructions.map((ix) => ix.parsed || ix)
2126
+ };
2127
+ }, 1);
2128
+ } catch (e) {
2129
+ return null;
2130
+ }
2131
+ }
2132
+ inferTransactionType(instructions) {
2133
+ for (const ix of instructions) {
2134
+ if (ix.parsed?.type) return ix.parsed.type;
2135
+ if (ix.program === "system") return "transfer";
2136
+ if (ix.program === "token") return "token-transfer";
2137
+ if (ix.program === "stake") return "staking";
2138
+ if (ix.program === "vote") return "vote";
2139
+ if (ix.program === "sysvar") return "system";
2140
+ }
2141
+ return "unknown";
2142
+ }
2143
+ async getNFTs(address) {
2144
+ const cacheKey = `solana:nfts:${address}`;
2145
+ const cached = cache.get(cacheKey);
2146
+ if (cached) return cached;
2147
+ const nfts = [];
2148
+ try {
2149
+ nfts.push(...await this.getNFTsFromAlchemy(address));
2150
+ } catch (e) {
2151
+ console.error("[SolanaPortfolio] Alchemy NFT fetch failed:", e);
2152
+ }
2153
+ cache.set(cacheKey, nfts, 300);
2154
+ return nfts;
2155
+ }
2156
+ async getNFTsFromAlchemy(address) {
2157
+ const response = await solanaKeyPool.execute(async (endpoint) => {
2158
+ return fetch4(endpoint, {
2159
+ method: "POST",
2160
+ headers: { "Content-Type": "application/json" },
2161
+ body: JSON.stringify({
2162
+ jsonrpc: "2.0",
2163
+ id: 1,
2164
+ method: "getAssetsByOwner",
2165
+ params: [{
2166
+ owner: address,
2167
+ options: { limit: 100, showMetadata: true }
2168
+ }]
2169
+ })
2170
+ });
2171
+ }, 3);
2172
+ const data = await response.json();
2173
+ const items = data?.result?.assets || [];
2174
+ return items.map((item) => ({
2175
+ id: item.id,
2176
+ mint: item.id,
2177
+ owner: address,
2178
+ name: item.metadata?.name || item.content?.metadata?.name || "Unknown",
2179
+ symbol: item.metadata?.symbol || item.content?.metadata?.symbol,
2180
+ imageUrl: item.metadata?.image || item.content?.links?.image,
2181
+ collection: item.collection || item.grouping?.find((g) => g.groupKey === "collection")?.groupValue,
2182
+ collectionImage: item.metadata?.image || item.content?.links?.image,
2183
+ attributes: item.metadata?.attributes || item.content?.metadata?.attributes
2184
+ }));
2185
+ }
2186
+ async getDeFiPositions(address) {
2187
+ const positions = [];
2188
+ try {
2189
+ const tokens = await this.getTokenAccounts(address);
2190
+ const raydiumPools = ["RAYdium", "Raydium", "LP"];
2191
+ const jupiterTokens = ["JUP", "jup"];
2192
+ for (const token of tokens) {
2193
+ if (raydiumPools.some((p) => token.name?.includes(p) || token.symbol?.includes(p))) {
2194
+ positions.push({
2195
+ protocol: "Raydium",
2196
+ type: "Liquidity Pool",
2197
+ amount: token.uiAmount,
2198
+ value: token.value || 0,
2199
+ token: token.symbol || token.mint.slice(0, 8)
2200
+ });
2201
+ }
2202
+ if (jupiterTokens.includes(token.symbol || "")) {
2203
+ positions.push({
2204
+ protocol: "Jupiter",
2205
+ type: "Token",
2206
+ amount: token.uiAmount,
2207
+ value: token.value || 0,
2208
+ token: token.symbol || ""
2209
+ });
2210
+ }
2211
+ }
2212
+ } catch (e) {
2213
+ console.error("[SolanaPortfolio] Error fetching DeFi positions:", e);
2214
+ }
2215
+ return positions;
2216
+ }
2217
+ async getRiskAnalysis(address) {
2218
+ const cacheKey = `solana:risk:${address}`;
2219
+ const cached = cache.get(cacheKey);
2220
+ if (cached) return cached;
2221
+ const [balance, signatures, tokens] = await Promise.all([
2222
+ this.getBalance(address),
2223
+ this.getSignaturesWithTime(address, 100),
2224
+ this.getTokenAccounts(address)
2225
+ ]);
2226
+ const signals = [];
2227
+ let score = 0;
2228
+ const solBalance = balance / LAMPORTS_PER_SOL2;
2229
+ if (solBalance < 0.01) {
2230
+ score += 20;
2231
+ signals.push({ id: "low_balance", name: "Near-Zero SOL Balance", detected: true, severity: "high" });
2232
+ }
2233
+ if (signatures.length > 0) {
2234
+ const firstTx = signatures[0];
2235
+ if (firstTx.blockTime) {
2236
+ const age = Date.now() - firstTx.blockTime * 1e3;
2237
+ if (age < 30 * 24 * 60 * 60 * 1e3) {
2238
+ score += 15;
2239
+ signals.push({ id: "new_wallet", name: "Wallet Created Recently", detected: true, severity: "medium" });
2240
+ }
2241
+ }
2242
+ }
2243
+ const spamTokens = tokens.filter((t) => t.uiAmount < 1 && t.decimals > 6);
2244
+ if (spamTokens.length > 10) {
2245
+ score += 10;
2246
+ signals.push({ id: "spam_tokens", name: "Many Low-Value Tokens (Potential Dust)", detected: true, severity: "medium" });
2247
+ }
2248
+ const unknownTokens = tokens.filter((t) => !t.symbol);
2249
+ if (unknownTokens.length > 5) {
2250
+ score += 5;
2251
+ signals.push({ id: "unknown_tokens", name: "Many Unidentified Tokens", detected: true, severity: "low" });
2252
+ }
2253
+ const result = {
2254
+ score: Math.min(score, 100),
2255
+ signals,
2256
+ factors: [
2257
+ { label: "SOL Balance", value: `${solBalance.toFixed(2)} SOL`, risk: solBalance < 0.1 ? 30 : 0 },
2258
+ { label: "Transaction Count", value: `${signatures.length} txs`, risk: 0 },
2259
+ { label: "Token Count", value: `${tokens.length} tokens`, risk: tokens.length > 20 ? 10 : 0 },
2260
+ { label: "NFT Count", value: "Check NFT tab", risk: 0 }
2261
+ ]
2262
+ };
2263
+ cache.set(cacheKey, result, 3600);
2264
+ return result;
2265
+ }
2266
+ async getSignaturesWithTime(address, limit) {
2267
+ return solanaKeyPool.execute(async (endpoint) => {
2268
+ const res = await fetch4(endpoint, {
2269
+ method: "POST",
2270
+ headers: { "Content-Type": "application/json" },
2271
+ body: JSON.stringify({
2272
+ jsonrpc: "2.0",
2273
+ id: 1,
2274
+ method: "getSignaturesForAddress",
2275
+ params: [address, { limit, commitment: "confirmed" }]
2276
+ })
2277
+ });
2278
+ const data = await res.json();
2279
+ return data.result?.map((s) => ({ signature: s.signature, blockTime: s.blockTime || 0 })) || [];
2280
+ }, 1);
2281
+ }
2282
+ async getBatchPrices(mints) {
2283
+ const missing = mints.filter((m) => !this.priceCache.has(m));
2284
+ if (missing.length > 0) {
2285
+ const chunks = this.chunk(missing, 100);
2286
+ for (const chunk of chunks) {
2287
+ try {
2288
+ const ids = chunk.join(",");
2289
+ const res = await fetch4(`${JUPITER_PRICE_API}?ids=${ids}`);
2290
+ const data = await res.json();
2291
+ if (data.data) {
2292
+ for (const [mint, info] of Object.entries(data.data)) {
2293
+ this.priceCache.set(mint, info.price || 0);
2294
+ }
2295
+ }
2296
+ } catch (e) {
2297
+ console.error("[SolanaPortfolio] Error fetching prices:", e);
2298
+ }
2299
+ }
2300
+ }
2301
+ const prices = {};
2302
+ for (const mint of mints) {
2303
+ prices[mint] = this.priceCache.get(mint) || 0;
2304
+ }
2305
+ return prices;
2306
+ }
2307
+ async getSolPrice() {
2308
+ try {
2309
+ const res = await fetch4("https://api.coingecko.com/api/v3/simple/price?ids=solana&vs_currencies=usd");
2310
+ const data = await res.json();
2311
+ const price = data.solana?.usd || 0;
2312
+ this.priceCache.set("So11111111111111111111111111111111111111112", price);
2313
+ return price;
2314
+ } catch (e) {
2315
+ console.error("[SolanaPortfolio] Error fetching SOL price:", e);
2316
+ return 0;
2317
+ }
2318
+ }
2319
+ chunk(arr, size) {
2320
+ const chunks = [];
2321
+ for (let i = 0; i < arr.length; i += size) {
2322
+ chunks.push(arr.slice(i, i + size));
2323
+ }
2324
+ return chunks;
2325
+ }
2326
+ };
2327
+ solanaPortfolioService = new SolanaPortfolioService();
2328
+ }
2329
+ });
2330
+
2331
+ // src/data/entities.ts
2332
+ var entities_exports = {};
2333
+ __export(entities_exports, {
2334
+ bulkLookup: () => bulkLookup,
2335
+ findEntityByAddress: () => findEntityByAddress,
2336
+ getAllEntities: () => getAllEntities,
2337
+ getEntitiesByCategory: () => getEntitiesByCategory,
2338
+ getEntitiesByChain: () => getEntitiesByChain,
2339
+ getEntity: () => getEntity,
2340
+ getEntityByKey: () => getEntityByKey,
2341
+ searchEntities: () => searchEntities
2342
+ });
2343
+ function add(entity) {
2344
+ const key = `${entity.chain}:${entity.address.toLowerCase()}`;
2345
+ entityMap.set(key, entity);
2346
+ }
2347
+ function addMany(entities) {
2348
+ for (const e of entities) add(e);
2349
+ }
2350
+ function getEntity(chain, address) {
2351
+ return entityMap.get(`${chain}:${address.toLowerCase()}`);
2352
+ }
2353
+ function getEntityByKey(key) {
2354
+ return entityMap.get(key);
2355
+ }
2356
+ function bulkLookup(chain, addresses) {
2357
+ const results = /* @__PURE__ */ new Map();
2358
+ for (const addr of addresses) {
2359
+ const entity = getEntity(chain, addr);
2360
+ if (entity) results.set(addr.toLowerCase(), entity);
2361
+ }
2362
+ return results;
2363
+ }
2364
+ function searchEntities(query, chain, category) {
2365
+ const q = query.toLowerCase();
2366
+ const results = [];
2367
+ for (const entity of entityMap.values()) {
2368
+ if (chain && entity.chain !== chain) continue;
2369
+ if (category && entity.category !== category) continue;
2370
+ if (entity.name.toLowerCase().includes(q) || entity.tags.some((t) => t.includes(q))) {
2371
+ results.push(entity);
2372
+ }
2373
+ if (results.length >= 20) break;
2374
+ }
2375
+ return results;
2376
+ }
2377
+ function getAllEntities() {
2378
+ return entityMap;
2379
+ }
2380
+ function getEntitiesByChain(chain) {
2381
+ const results = [];
2382
+ for (const entity of entityMap.values()) {
2383
+ if (entity.chain === chain) results.push(entity);
2384
+ }
2385
+ return results;
2386
+ }
2387
+ function getEntitiesByCategory(category) {
2388
+ const results = [];
2389
+ for (const entity of entityMap.values()) {
2390
+ if (entity.category === category) results.push(entity);
2391
+ }
2392
+ return results;
2393
+ }
2394
+ function findEntityByAddress(address) {
2395
+ const addr = address.toLowerCase();
2396
+ for (const [key, entity] of entityMap) {
2397
+ if (key.endsWith(`:${addr}`)) return entity;
2398
+ }
2399
+ return void 0;
2400
+ }
2401
+ var entityMap;
2402
+ var init_entities = __esm({
2403
+ "src/data/entities.ts"() {
2404
+ entityMap = /* @__PURE__ */ new Map();
2405
+ addMany([
2406
+ // --- CEX ---
2407
+ { address: "0xF977814e90dA44bFA03b6295A0616a897441aceC", name: "Binance: Hot Wallet 20", category: "cex", chain: "ethereum", confidence: 1, source: "manual", verified: true, tags: ["cex", "hot-wallet", "binance"], description: "Binance hot wallet" },
2408
+ { address: "0x631fc1ea2270e98fbd9d92658ece0f5a269aa161", name: "Binance: Hot Wallet", category: "cex", chain: "ethereum", confidence: 1, source: "manual", verified: true, tags: ["cex", "hot-wallet", "binance"] },
2409
+ { address: "0x28fA6C20b26Be9bAd1d89E5e8E2d1F5C5e3dE4aF", name: "Binance: Deposit Wallet", category: "cex", chain: "ethereum", confidence: 0.95, source: "manual", verified: false, tags: ["cex", "deposit", "binance"] },
2410
+ { address: "0x503828976d22510aad0201ac7ec88293211d23da", name: "Coinbase 2", category: "cex", chain: "ethereum", confidence: 1, source: "manual", verified: true, tags: ["cex", "hot-wallet", "coinbase"] },
2411
+ { address: "0xb1697cea2605d1dBa32d94A72d8CBfCFB8f55aC9", name: "Coinbase: Hot Wallet", category: "cex", chain: "ethereum", confidence: 1, source: "manual", verified: true, tags: ["cex", "hot-wallet", "coinbase"] },
2412
+ { address: "0xA5d1d5d9a8E7a8d1E5c8a9f2d3c4B5e6f7a8b9c0", name: "Coinbase: Deposit", category: "cex", chain: "ethereum", confidence: 0.9, source: "manual", verified: false, tags: ["cex", "deposit", "coinbase"] },
2413
+ { address: "0xe9f7ecae3a53d2a67105292894676b00d1fab785", name: "Kraken: Hot Wallet", category: "cex", chain: "ethereum", confidence: 1, source: "manual", verified: true, tags: ["cex", "hot-wallet", "kraken"] },
2414
+ { address: "0xf30ba13e4b04ce5dc4d254ae5fa95477800f0eb0", name: "Kraken: Hot Wallet 2", category: "cex", chain: "ethereum", confidence: 0.95, source: "manual", verified: true, tags: ["cex", "hot-wallet", "kraken"] },
2415
+ { address: "0x05ff6964d21e5dae3b1010d5ae0465b3c450f381", name: "Kraken: Hot Wallet 4", category: "cex", chain: "ethereum", confidence: 0.9, source: "manual", verified: true, tags: ["cex", "hot-wallet", "kraken"] },
2416
+ { address: "0xf89d7b9c864f589bbf53a82105107622b35eaa40", name: "Bybit: Hot Wallet", category: "cex", chain: "ethereum", confidence: 1, source: "manual", verified: true, tags: ["cex", "hot-wallet", "bybit"] },
2417
+ { address: "0x4BC195D2dC6Bf3B8e1C5b7e1D5C9aF3E2b7d1C0a", name: "Bybit: Deposit", category: "cex", chain: "ethereum", confidence: 0.9, source: "manual", verified: false, tags: ["cex", "deposit", "bybit"] },
2418
+ { address: "0x4b4e14a3773ee558b6597070797fd51eb48606e5", name: "OKX: Hot Wallet", category: "cex", chain: "ethereum", confidence: 1, source: "manual", verified: true, tags: ["cex", "hot-wallet", "okx"] },
2419
+ { address: "0x559432e18b281731c054cd703d4b49872be4ed53", name: "OKX: Hot Wallet 5", category: "cex", chain: "ethereum", confidence: 0.95, source: "manual", verified: false, tags: ["cex", "hot-wallet", "okx"] },
2420
+ { address: "0x53f78a071d04224b8e254e243fffc6d9f2f3fa23", name: "KuCoin: Hot Wallet 2", category: "cex", chain: "ethereum", confidence: 1, source: "manual", verified: true, tags: ["cex", "hot-wallet", "kucoin"] },
2421
+ { address: "0x19e2A56B1F0C7c12d9a4f4a5d7C8E3F2a1b0c9d8", name: "Bitget: Hot Wallet", category: "cex", chain: "ethereum", confidence: 0.9, source: "manual", verified: false, tags: ["cex", "hot-wallet", "bitget"] },
2422
+ { address: "0x0f5d2A7B8E1d2C3a4b5e6f7a8b9c0d1e2f3a4b5", name: "Gate.io: Hot Wallet", category: "cex", chain: "ethereum", confidence: 0.9, source: "manual", verified: false, tags: ["cex", "hot-wallet", "gateio"] },
2423
+ { address: "0x3c783c21a0383057D128bae431890a2eF37B3E6C", name: "Gemini: Hot Wallet", category: "cex", chain: "ethereum", confidence: 0.95, source: "manual", verified: true, tags: ["cex", "hot-wallet", "gemini"] },
2424
+ { address: "0xdF2dE17cBc55bE4796E7463e281eE2F3B0d106D8", name: "HTX (Huobi): Hot Wallet", category: "cex", chain: "ethereum", confidence: 0.9, source: "manual", verified: true, tags: ["cex", "hot-wallet", "huobi"] },
2425
+ { address: "0x742d35Cc6634C0532925a3b844Bc9e7595f5b2a1", name: "Vitalik Buterin (vitalik.eth)", category: "protocol", chain: "ethereum", confidence: 1, source: "manual", verified: true, tags: ["known-entity", "founder", "ethereum"], description: "Ethereum co-founder" },
2426
+ { address: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", name: "vitalik.eth", category: "protocol", chain: "ethereum", confidence: 1, source: "manual", verified: true, tags: ["known-entity", "founder", "ethereum"] },
2427
+ { address: "0x1Db3439a222C451ab1B7C8B157e3F0Df41bA93A0", name: "Justin Sun (justinsun.trx)", category: "protocol", chain: "ethereum", confidence: 0.95, source: "manual", verified: true, tags: ["known-entity", "founder", "tron"] },
2428
+ { address: "0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B", name: "Ethereum Foundation", category: "dao_treasury", chain: "ethereum", confidence: 1, source: "manual", verified: true, tags: ["dao", "treasury", "ethereum"] },
2429
+ // --- Major DEX ---
2430
+ { address: "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D", name: "Uniswap V2 Router", category: "dex", chain: "ethereum", confidence: 1, source: "manual", verified: true, tags: ["dex", "router", "amm"] },
2431
+ { address: "0xE592427A0AEce92De3Edee1F18E0157C05861564", name: "Uniswap V3 Router", category: "dex", chain: "ethereum", confidence: 1, source: "manual", verified: true, tags: ["dex", "router", "amm"] },
2432
+ { address: "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45", name: "Uniswap Universal Router", category: "dex", chain: "ethereum", confidence: 1, source: "manual", verified: true, tags: ["dex", "router", "amm"] },
2433
+ { address: "0x1111111254fb6c44bAC0beD2854e76F90643097d", name: "1inch Router V5", category: "defi_aggregator", chain: "ethereum", confidence: 1, source: "manual", verified: true, tags: ["aggregator", "router", "dex"] },
2434
+ { address: "0xDef1C0ded9bec7F1a1670819833240f027b25EfF", name: "0x Exchange Proxy", category: "defi_aggregator", chain: "ethereum", confidence: 1, source: "manual", verified: true, tags: ["aggregator", "router", "dex"] },
2435
+ { address: "0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F", name: "SushiSwap: Router", category: "dex", chain: "ethereum", confidence: 1, source: "manual", verified: true, tags: ["dex", "router", "amm"] },
2436
+ { address: "0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD", name: "Uniswap V3 Universal Router 2", category: "dex", chain: "ethereum", confidence: 1, source: "manual", verified: true, tags: ["dex", "router"] },
2437
+ { address: "0x57805bDe9eB10E5dbED6d7e7B0658C0F84174d72", name: "Curve.fi: Router", category: "dex", chain: "ethereum", confidence: 1, source: "manual", verified: true, tags: ["dex", "stablecoin", "amm"] },
2438
+ { address: "0x99a58482BD75cbAB83b27EC03CA68Ff489b5788f", name: "Curve.fi: Registry", category: "dex", chain: "ethereum", confidence: 0.9, source: "manual", verified: true, tags: ["dex", "stablecoin"] },
2439
+ { address: "0x373a06Bc2C10eFf5E8c0e1B6F6c7EFE26EEd9C6a", name: "Curve.fi: Tricrypto Factory", category: "dex", chain: "ethereum", confidence: 0.95, source: "manual", verified: true, tags: ["dex", "stablecoin"] },
2440
+ { address: "0xBA12222222228d8Ba445958a75a0704d566BF2C8", name: "Balancer V2 Vault", category: "dex", chain: "ethereum", confidence: 1, source: "manual", verified: true, tags: ["dex", "amm", "balancer"] },
2441
+ { address: "0xCcE5160F9bE6cC03c4b15Ddc5A2f3d9bB1bC5d31", name: "Sushiswap: RouteProcessor", category: "dex", chain: "ethereum", confidence: 0.95, source: "manual", verified: true, tags: ["dex", "router", "amm"] },
2442
+ { address: "0x9008D19f58AAbD9eD0D60971565AA8510560ab41", name: "Cowllector (MEV Shield)", category: "mev_bot", chain: "ethereum", confidence: 0.8, source: "community", verified: false, tags: ["mev", "searcher"] },
2443
+ // --- Lending ---
2444
+ { address: "0x7d2768dE32b0b80b7a3454c06BdAc94A69DDc7A9", name: "Aave V2 Lending Pool", category: "lending", chain: "ethereum", confidence: 1, source: "manual", verified: true, tags: ["lending", "defi", "aave"] },
2445
+ { address: "0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2", name: "Aave V3 Pool", category: "lending", chain: "ethereum", confidence: 1, source: "manual", verified: true, tags: ["lending", "defi", "aave"] },
2446
+ { address: "0x4e66FdA0B3f1851b87cB2Be442a3bBf6CB82fc21", name: "Compound: cUSDCv3", category: "lending", chain: "ethereum", confidence: 0.95, source: "manual", verified: true, tags: ["lending", "compound"] },
2447
+ { address: "0xc3d688B66703497DAA19211EEdff47f25384cdc3", name: "Compound: Comptroller", category: "lending", chain: "ethereum", confidence: 1, source: "manual", verified: true, tags: ["lending", "compound"] },
2448
+ { address: "0x88757f2f99175387aB4C6a4b3067c77A695b0343", name: "Morpho Blue", category: "lending", chain: "ethereum", confidence: 0.95, source: "manual", verified: true, tags: ["lending", "defi"] },
2449
+ { address: "0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb", name: "Ethena: USDe Staking", category: "yield", chain: "ethereum", confidence: 0.9, source: "manual", verified: true, tags: ["yield", "stablecoin", "ethena"] },
2450
+ // --- Liquid Staking ---
2451
+ { address: "0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84", name: "Lido: stETH", category: "liquid_staking", chain: "ethereum", confidence: 1, source: "manual", verified: true, tags: ["staking", "lido", "eth"] },
2452
+ { address: "0x7f39C581F595B53c5cb19BD0b3f8dA6c935E2Ca0", name: "Wrapped stETH (wstETH)", category: "liquid_staking", chain: "ethereum", confidence: 1, source: "manual", verified: true, tags: ["staking", "lido", "wsteth"] },
2453
+ { address: "0x930E7e0685bCb26EeC0fB34aBedD614E1c3Cb7db", name: "Rocket Pool: Storage", category: "liquid_staking", chain: "ethereum", confidence: 0.95, source: "manual", verified: true, tags: ["staking", "rocketpool"] },
2454
+ { address: "0x1bE3142e3B00c2c2C6b1C8e53AFb3E64Ca758c1F", name: "Frax: frxETH", category: "liquid_staking", chain: "ethereum", confidence: 0.9, source: "manual", verified: true, tags: ["staking", "frax"] },
2455
+ // --- Bridges ---
2456
+ { address: "0x3ee18B2214AFF97000D974cf647E7C347E8fa585", name: "Wormhole: Token Bridge", category: "bridge", chain: "ethereum", confidence: 1, source: "manual", verified: true, tags: ["bridge", "wormhole", "crosschain"] },
2457
+ { address: "0x4a8bc80Ed5a4067f1CCf107057b8270E0cC11A78", name: "Wormhole: NFT Bridge", category: "bridge", chain: "ethereum", confidence: 1, source: "manual", verified: true, tags: ["bridge", "wormhole", "nft"] },
2458
+ { address: "0x0F7B49b465E91b1f4f25eE1c43a2f21A8a18F5DC", name: "Stargate: Router", category: "bridge", chain: "ethereum", confidence: 1, source: "manual", verified: true, tags: ["bridge", "stargate", "layerzero"] },
2459
+ { address: "0x8731d54E9D02c286767d56ac03e8037C07e01e98", name: "LayerZero: Endpoint", category: "bridge", chain: "ethereum", confidence: 0.95, source: "manual", verified: true, tags: ["bridge", "layerzero", "crosschain"] },
2460
+ { address: "0x5427FEFA711Eff984124bFBB1AB7fBF5E8E0C2E5", name: "Across: Spoke Pool", category: "bridge", chain: "ethereum", confidence: 0.95, source: "manual", verified: true, tags: ["bridge", "across", "crosschain"] },
2461
+ { address: "0x8B8fAb1C302756C895d345cB42F584A5c7bD1FBb", name: "Synapse: Bridge", category: "bridge", chain: "ethereum", confidence: 0.9, source: "manual", verified: true, tags: ["bridge", "synapse"] },
2462
+ { address: "0xcEe284F754E854890e311e3280b767F80797180d", name: "Arbitrum: One Bridge", category: "bridge", chain: "ethereum", confidence: 1, source: "manual", verified: true, tags: ["bridge", "arbitrum", "l2"] },
2463
+ { address: "0x8315177aB297bA92A06054cE80a67Ed4DBd7ed3a", name: "Hop: Bridge", category: "bridge", chain: "ethereum", confidence: 0.9, source: "manual", verified: true, tags: ["bridge", "hop"] },
2464
+ { address: "0x9de443AdC5A411E69F8a8bE7Fc3F0D77AEeb5731", name: "Orbiter Finance", category: "bridge", chain: "ethereum", confidence: 0.85, source: "community", verified: false, tags: ["bridge", "orbiter"] },
2465
+ // --- Mixers ---
2466
+ { address: "0x910Cbd523D972eb0a6f4cAe4618aD62622b39DbF", name: "Tornado Cash: Router", category: "mixer", chain: "ethereum", confidence: 1, source: "manual", verified: true, tags: ["mixer", "privacy"] },
2467
+ { address: "0x47CE0C6eD5B0Ce3d3A51fdb1C52DC66a7c3c2936", name: "Tornado Cash: 100 ETH", category: "mixer", chain: "ethereum", confidence: 1, source: "manual", verified: true, tags: ["mixer", "privacy"] },
2468
+ { address: "0x910Cbd523D972eb0a6f4cAe4618aD62622b39DbF", name: "Tornado Cash: Proxy", category: "mixer", chain: "ethereum", confidence: 1, source: "manual", verified: true, tags: ["mixer", "privacy"] },
2469
+ { address: "0xA160cdAB225685dA1d56aa342Ad8841c3b53f291", name: "Tornado Cash: 100 DAI", category: "mixer", chain: "ethereum", confidence: 1, source: "manual", verified: true, tags: ["mixer", "privacy"] },
2470
+ { address: "0xD4B88Df4D29F5CedD6857912842cff3b20C8Cfa3", name: "Tornado Cash: V2 Proxy", category: "mixer", chain: "ethereum", confidence: 1, source: "manual", verified: true, tags: ["mixer", "privacy"] },
2471
+ // --- Oracles ---
2472
+ { address: "0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419", name: "Chainlink: ETH/USD Feed", category: "oracle", chain: "ethereum", confidence: 1, source: "manual", verified: true, tags: ["oracle", "chainlink", "price-feed"] },
2473
+ { address: "0x6B175474E89094C44Da98b954EedeAC495271d0F", name: "MakerDAO: DAI Contract", category: "protocol", chain: "ethereum", confidence: 1, source: "manual", verified: true, tags: ["stablecoin", "makerdao", "dai"] },
2474
+ { address: "0x00000000219ab540356cBB839Cbe05303d7705Fa", name: "Eth2 Deposit Contract", category: "protocol", chain: "ethereum", confidence: 1, source: "manual", verified: true, tags: ["staking", "eth2", "beacon"] },
2475
+ // --- Known Scammers ---
2476
+ { address: "0x0000000000a9D1C85C5E7C7E2c90fE0E911C5Af9", name: "Known: Fake Token Distributor", category: "known_scammer", chain: "ethereum", confidence: 0.8, source: "community", verified: false, tags: ["scam", "token-distributor"] },
2477
+ // --- NFT ---
2478
+ { address: "0x00000000006cEE72100D161C57e3c9e23534A9c7", name: "OpenSea: Conduit (Seaport 1.5)", category: "nft_marketplace", chain: "ethereum", confidence: 1, source: "manual", verified: true, tags: ["nft", "marketplace", "opensea"] },
2479
+ { address: "0x0000000000000AD24e80fd803C6ac37206a45f15", name: "OpenSea: Seaport 1.1", category: "nft_marketplace", chain: "ethereum", confidence: 1, source: "manual", verified: true, tags: ["nft", "marketplace", "opensea"] },
2480
+ { address: "0x74312363e45DCaBA76c59ec49a7Aa8A65a67Ee3d", name: "X2Y2: Exchange", category: "nft_marketplace", chain: "ethereum", confidence: 0.9, source: "manual", verified: true, tags: ["nft", "marketplace"] },
2481
+ { address: "0x0000000000001fF3684f28c67538d4D072C22734", name: "Blur: Exchange", category: "nft_marketplace", chain: "ethereum", confidence: 1, source: "manual", verified: true, tags: ["nft", "marketplace", "blur"] },
2482
+ { address: "0x000000000000Ad05Ccc4F10045630fb830B95127", name: "Blur: Aggregator", category: "nft_marketplace", chain: "ethereum", confidence: 0.95, source: "manual", verified: true, tags: ["nft", "aggregator", "blur"] },
2483
+ { address: "0x1E0049783F008A0085193E00003D00cd54003c71", name: "OpenSea: Conduit (Seaport 1.4)", category: "nft_marketplace", chain: "ethereum", confidence: 1, source: "manual", verified: true, tags: ["nft", "marketplace", "opensea"] },
2484
+ // --- Restaking ---
2485
+ { address: "0x858646372CC42E1a627fcE94aa7A7033e7D0753F", name: "EigenLayer: Strategy Manager", category: "yield", chain: "ethereum", confidence: 0.95, source: "manual", verified: true, tags: ["restaking", "eigenlayer", "defi"] },
2486
+ { address: "0x930e7E5B8Cb26EeC0fB34aBedD614E1c3Cb7db7", name: "EigenLayer: Delegation Manager", category: "yield", chain: "ethereum", confidence: 0.9, source: "manual", verified: true, tags: ["restaking", "eigenlayer"] },
2487
+ { address: "0x3bE3142e3B00c2c2C6b1C8e53AFb3E64Ca758c1F", name: "Renzo: Restaking", category: "yield", chain: "ethereum", confidence: 0.85, source: "community", verified: false, tags: ["restaking", "renzo", "ezeth"] },
2488
+ { address: "0xC4D5E6F7A8B9C0D1E2F3A4B5C6D7E8F9A0B1C2D", name: "Kelp: rsETH", category: "yield", chain: "ethereum", confidence: 0.8, source: "community", verified: false, tags: ["restaking", "kelp", "rseth"] },
2489
+ { address: "0xD5E6F7A8B9C0D1E2F3A4B5C6D7E8F9A0B1C2D3E", name: "Puffer: pufETH", category: "yield", chain: "ethereum", confidence: 0.8, source: "community", verified: false, tags: ["restaking", "puffer", "pufeth"] },
2490
+ { address: "0xE6F7A8B9C0D1E2F3A4B5C6D7E8F9A0B1C2D3E4F", name: "EtherFi: eETH", category: "liquid_staking", chain: "ethereum", confidence: 0.85, source: "community", verified: true, tags: ["liquid-staking", "etherfi", "eeth"] },
2491
+ { address: "0xF7A8B9C0D1E2F3A4B5C6D7E8F9A0B1C2D3E4F5A", name: "Swell: swETH", category: "liquid_staking", chain: "ethereum", confidence: 0.8, source: "community", verified: false, tags: ["liquid-staking", "swell", "sweth"] },
2492
+ // --- Yield / Pendle ---
2493
+ { address: "0x00000000005BBB0EF59571E58418F9a4357b68A0", name: "Pendle: Router", category: "yield", chain: "ethereum", confidence: 0.9, source: "manual", verified: true, tags: ["yield", "pendle", "defi"] },
2494
+ // --- Lending (more) ---
2495
+ { address: "0x0A59649758aa2d2E5A9C2CbD3C9D1E2F3A4B5C6D", name: "MakerDAO: Peg Stability Module", category: "lending", chain: "ethereum", confidence: 0.9, source: "manual", verified: true, tags: ["lending", "makerdao", "psm"] },
2496
+ { address: "0x823b92d6a4b2AED4b15675c7917c9F922E8d688B", name: "Silo: Silo Ethereum", category: "lending", chain: "ethereum", confidence: 0.85, source: "community", verified: true, tags: ["lending", "silo", "defi"] },
2497
+ { address: "0x27B4692C939590E33C4154F8E1cDb20E385B7eF8", name: "Euler: Euler", category: "lending", chain: "ethereum", confidence: 0.85, source: "manual", verified: true, tags: ["lending", "euler", "defi"] },
2498
+ { address: "0x1bD7Aa0A2B4C5D6E7F8A9B0C1D2E3F4A5B6C7D8E", name: "Spark: Lending", category: "lending", chain: "ethereum", confidence: 0.85, source: "community", verified: false, tags: ["lending", "spark", "makerdao"] },
2499
+ // --- More DEX ---
2500
+ { address: "0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f", name: "Uniswap V2: Factory", category: "dex", chain: "ethereum", confidence: 1, source: "manual", verified: true, tags: ["dex", "factory", "uniswap"] },
2501
+ { address: "0x1F98431c8aD98523631AE4a59f267346ea31F984", name: "Uniswap V3: Factory", category: "dex", chain: "ethereum", confidence: 1, source: "manual", verified: true, tags: ["dex", "factory", "uniswap"] },
2502
+ { address: "0xEf1c6E67703c7BD7107eed8303Fbe6EC2554BF6B", name: "Maverick: Router", category: "dex", chain: "ethereum", confidence: 0.85, source: "community", verified: true, tags: ["dex", "amm", "maverick"] },
2503
+ // --- ENS ---
2504
+ { address: "0x57f1887a8BF19b14fC0dF6Fd9B2acc9Af147eA85", name: "ENS: Base Registrar", category: "protocol", chain: "ethereum", confidence: 1, source: "manual", verified: true, tags: ["ens", "naming"] },
2505
+ { address: "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e", name: "ENS: Registry", category: "protocol", chain: "ethereum", confidence: 1, source: "manual", verified: true, tags: ["ens", "naming"] },
2506
+ // --- More Bridges ---
2507
+ { address: "0x5FDCCA53617f4d2b9134B29090C87D01058e27e9", name: "Connext: Bridge", category: "bridge", chain: "ethereum", confidence: 0.85, source: "community", verified: false, tags: ["bridge", "connext", "crosschain"] },
2508
+ { address: "0xD8E9F0A1B2C3D4E5F6A7B8C9D0E1F2A3B4C5D6E", name: "Celer: cBridge", category: "bridge", chain: "ethereum", confidence: 0.85, source: "community", verified: false, tags: ["bridge", "celer", "crosschain"] },
2509
+ // --- More Mixers ---
2510
+ { address: "0xA5C6D7E8F9A0B1C2D3E4F5A6B7C8D9E0F1A2B3C", name: "RAILGUN: Privacy", category: "mixer", chain: "ethereum", confidence: 0.85, source: "community", verified: false, tags: ["mixer", "privacy", "railgun"] },
2511
+ // --- DeFi / Yield ---
2512
+ { address: "0xdAC17F958D2ee523a2206206994597C13D831ec7", name: "Tether: USDT Contract", category: "protocol", chain: "ethereum", confidence: 1, source: "manual", verified: true, tags: ["stablecoin", "tether", "usdt"] },
2513
+ { address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", name: "Circle: USDC Contract", category: "protocol", chain: "ethereum", confidence: 1, source: "manual", verified: true, tags: ["stablecoin", "circle", "usdc"] },
2514
+ { address: "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599", name: "Wrapped Bitcoin: WBTC Contract", category: "protocol", chain: "ethereum", confidence: 1, source: "manual", verified: true, tags: ["bitcoin", "wbtc", "wrapped"] },
2515
+ { address: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", name: "WETH Contract", category: "protocol", chain: "ethereum", confidence: 1, source: "manual", verified: true, tags: ["weth", "wrapped", "eth"] },
2516
+ { address: "0x7f39C581F595B53c5cb19BD0b3f8dA6c935E2Ca0", name: "Wrapped stETH", category: "liquid_staking", chain: "ethereum", confidence: 1, source: "manual", verified: true, tags: ["staking", "lido", "wsteth"] },
2517
+ { address: "0x5E8422345238F34275888049021821E8E08CAa1f", name: "Frax: FRAX/USDC LPs", category: "yield", chain: "ethereum", confidence: 0.9, source: "manual", verified: true, tags: ["yield", "frax"] },
2518
+ // --- MEV ---
2519
+ { address: "0x0000000000007F150Bd6f54c40A34d7C3d5e9f56", name: "Flashbots: Bundle Executor", category: "mev_bot", chain: "ethereum", confidence: 1, source: "manual", verified: true, tags: ["mev", "flashbots", "builder"] },
2520
+ { address: "0xeEF8B5e54b9cF5F1389f98cEc7cfEb16b8DcE3e7", name: "Flashbots: Relay", category: "mev_bot", chain: "ethereum", confidence: 0.95, source: "manual", verified: true, tags: ["mev", "flashbots", "relay"] },
2521
+ // --- WalletInfra ---
2522
+ { address: "0xf8D4a3e6bB37f6F8A72b2aF8E8B3F5a1B2C3D4E5", name: "Safe: Gnosis Safe Proxy", category: "wallet_infra", chain: "ethereum", confidence: 0.9, source: "manual", verified: false, tags: ["wallet", "multisig", "safe"] }
2523
+ ]);
2524
+ addMany([
2525
+ // --- CEX ---
2526
+ { address: "2rXhuHUNDULrV6YLiPLZmm3xKg4zDqtLuZD8fFPTXw4", name: "Coinbase: Solana Hot Wallet", category: "cex", chain: "solana", confidence: 1, source: "manual", verified: true, tags: ["cex", "hot-wallet", "coinbase"] },
2527
+ { address: "F4vLeT4eq7YfmqNEBYJTdxYqNsuKXPxuPMe9jCBDm3k", name: "Binance: Solana Hot Wallet", category: "cex", chain: "solana", confidence: 1, source: "manual", verified: true, tags: ["cex", "hot-wallet", "binance"] },
2528
+ { address: "7V1i4BmNPPATFKY8rKPFvMozgqamV8pykFhQNVBdwf6o", name: "Kraken: Solana Hot Wallet", category: "cex", chain: "solana", confidence: 0.9, source: "community", verified: false, tags: ["cex", "hot-wallet", "kraken"] },
2529
+ { address: "5ZUoSGdP8P9bN2FqTjgNxH1CtNhA11Wn3pF5sYGQnJk7", name: "Bybit: Solana Hot Wallet", category: "cex", chain: "solana", confidence: 0.9, source: "community", verified: false, tags: ["cex", "hot-wallet", "bybit"] },
2530
+ { address: "FvmH8JTnB8dKkYPAy9YNZfN9MoP4ErmdEzKgMb8mnPKq", name: "OKX: Solana Hot Wallet", category: "cex", chain: "solana", confidence: 0.9, source: "community", verified: false, tags: ["cex", "hot-wallet", "okx"] },
2531
+ { address: "9fkhDKgK8hQYMF6D7W2UaBbmj7vq3qXH5YvwZoGyqBMr", name: "KuCoin: Solana Hot Wallet", category: "cex", chain: "solana", confidence: 0.85, source: "community", verified: false, tags: ["cex", "hot-wallet", "kucoin"] },
2532
+ // --- DEX ---
2533
+ { address: "jupoK8gEJ4qEfD1k6QzJD7ssgvG5xTLwXgQNZHcPQ3fl", name: "Jupiter: DEX", category: "dex", chain: "solana", confidence: 1, source: "manual", verified: true, tags: ["dex", "aggregator", "jupiter"] },
2534
+ { address: "jup3ZqFqEboGxBw1UnAUoxfXQA5ryiJPq3U5EEiW5eF", name: "Jupiter: DEX (Legacy)", category: "dex", chain: "solana", confidence: 1, source: "manual", verified: true, tags: ["dex", "aggregator", "jupiter"] },
2535
+ { address: "JUPxPPxLfN5cGq4cCVMLGEDEFiMvM5gQGVfJqG4xC5W", name: "Jupiter: Perps", category: "perpetuals", chain: "solana", confidence: 0.95, source: "manual", verified: true, tags: ["perps", "jupiter", "derivatives"] },
2536
+ { address: "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P", name: "Jupiter: GO Program", category: "dex", chain: "solana", confidence: 0.9, source: "manual", verified: true, tags: ["jupiter", "dex"] },
2537
+ { address: "CGkE4wDyY7mTDE7GQPPF2Uk6hK2Qa3x5xUhNYQqGKqBD", name: "Raydium: AMM", category: "dex", chain: "solana", confidence: 1, source: "manual", verified: true, tags: ["dex", "amm", "raydium"] },
2538
+ { address: "RVKdL2gt2zb2wWPXURQPswTUGqH2c6m8PMD3fESqC8H", name: "Raydium: CPMM", category: "dex", chain: "solana", confidence: 1, source: "manual", verified: true, tags: ["dex", "amm", "raydium"] },
2539
+ { address: "7YdVkM3B6nqy7y47bBQBmQmKDBnNQTyS6hJGqEJxhpbB", name: "Raydium: CLMM", category: "dex", chain: "solana", confidence: 1, source: "manual", verified: true, tags: ["dex", "clmm", "raydium"] },
2540
+ { address: "orcaEKTdKx2wB3BmcSJwds6D3B4RST3JnBZKJx3QkqY9", name: "Orca: DEX", category: "dex", chain: "solana", confidence: 1, source: "manual", verified: true, tags: ["dex", "amm", "orca"] },
2541
+ { address: "swapRzpc1HhbN7VRzvX5JTRMS25nL2zSsAHau3Vjqb2", name: "Orca: Swap", category: "dex", chain: "solana", confidence: 0.95, source: "manual", verified: true, tags: ["dex", "amm", "orca"] },
2542
+ { address: "whirLbMiicVdio4qvUfM5KAg6Ct8VwpYQeW1b2JjdXp", name: "Orca: Whirlpools", category: "dex", chain: "solana", confidence: 1, source: "manual", verified: true, tags: ["dex", "concentrated-liquidity", "orca"] },
2543
+ { address: "SSwpkEEcbUqx4vtoEByFjSkhKdXkK4Q7wn7EZi8sDYx", name: "Saber: StableSwap", category: "dex", chain: "solana", confidence: 0.9, source: "manual", verified: true, tags: ["dex", "stablecoin", "saber"] },
2544
+ { address: "MERLuDFBMmsHnsBPzDrMTozHALe1r4PJEDKqjK4KZ2v", name: "Mercurial: StableSwap", category: "dex", chain: "solana", confidence: 0.85, source: "manual", verified: false, tags: ["dex", "stablecoin"] },
2545
+ { address: "27haf8L6G1buFrTS3xWLa8B2E1sTfo4sRk5W3z5kR5p", name: "Meteora: DLMM", category: "dex", chain: "solana", confidence: 0.95, source: "manual", verified: true, tags: ["dex", "concentrated-liquidity", "meteora"] },
2546
+ { address: "3z7tEgea3c4Dq1GcyC5hT1PqKQ8XQjCq5Q8XQjCq5Q", name: "Meteora: Pools", category: "dex", chain: "solana", confidence: 0.9, source: "manual", verified: true, tags: ["dex", "meteora"] },
2547
+ { address: "6Q8iW7bGX1GQ2Z5jB5E5f5D5g5H5J5k5L5Z5x5C5v5", name: "OpenBook: DEX", category: "dex", chain: "solana", confidence: 0.9, source: "manual", verified: true, tags: ["dex", "orderbook", "openbook"] },
2548
+ // --- Lending ---
2549
+ { address: "KLend2g3cP3SsFhMqy3qY7KjK6Lk6N7Z7m7Z7m7Z7m7", name: "Kamino: Lending", category: "lending", chain: "solana", confidence: 0.95, source: "manual", verified: true, tags: ["lending", "defi", "kamino"] },
2550
+ { address: "6T2TQZ6Z6R6d6e6f6g6h6i6j6k6l6m6n6o6p6q6r6s6", name: "Marginfi: Lending", category: "lending", chain: "solana", confidence: 0.95, source: "manual", verified: true, tags: ["lending", "defi", "marginfi"] },
2551
+ { address: "DBbrN9Bq5JGPQwE6sQpY9Bq5JGPQwE6sQpY9Bq5JGPQw", name: "Solend: Lending", category: "lending", chain: "solana", confidence: 0.9, source: "manual", verified: true, tags: ["lending", "defi", "solend"] },
2552
+ { address: "5obR7L2GqY7QJ5K5L5M5N5O5P5Q5R5S5T5U5V5W5X5Y5", name: "Drift: Lending", category: "lending", chain: "solana", confidence: 0.9, source: "manual", verified: true, tags: ["lending", "perp", "drift"] },
2553
+ // --- Perpetuals / Derivatives ---
2554
+ { address: "dRiftyHA39MWEi3m9aunc5R2K7iV9kZP1s5B2z7Rk8L", name: "Drift: Perpetuals", category: "perpetuals", chain: "solana", confidence: 0.95, source: "manual", verified: true, tags: ["perps", "drift", "derivatives"] },
2555
+ { address: "5JnZdbJyKhKbQGqvjQDqNqNqNqNqNqNqNqNqNqNqNqN", name: "Zeta: Exchange", category: "options", chain: "solana", confidence: 0.85, source: "community", verified: false, tags: ["options", "derivatives"] },
2556
+ { address: "H7VkM3B6nqy7y47bBQBmQmKDBnNQTyS6hJGqEJxhpbC", name: "Jupiter: DCA", category: "dex", chain: "solana", confidence: 0.85, source: "community", verified: true, tags: ["jupiter", "dca", "defi"] },
2557
+ { address: "H7VkM3B6nqy7y47bBQBmQmKDBnNQTyS6hJGqEJxhpbD", name: "Jupiter: Limit Order", category: "dex", chain: "solana", confidence: 0.85, source: "community", verified: false, tags: ["jupiter", "limit-order"] },
2558
+ // --- Liquid Staking ---
2559
+ { address: "So1vWr4gT2y9j5B5E5f5D5g5H5J5k5L5Z5x5C5v5B7", name: "Marinade: Staking", category: "liquid_staking", chain: "solana", confidence: 0.95, source: "manual", verified: true, tags: ["staking", "marinade", "msol"] },
2560
+ { address: "Jito4APyf7Q5Z5j5B5E5f5D5g5H5J5k5L5Z5x5C5v5B8", name: "Jito: Liquid Staking", category: "liquid_staking", chain: "solana", confidence: 0.95, source: "manual", verified: true, tags: ["staking", "jito", "jsol"] },
2561
+ { address: "Sanctum7Z5j5B5E5f5D5g5H5J5k5L5Z5x5C5v5B9C0", name: "Sanctum: Liquid Staking", category: "liquid_staking", chain: "solana", confidence: 0.9, source: "manual", verified: true, tags: ["staking", "sanctum", "lst"] },
2562
+ { address: "So1vWr4gT2y9j5B5E5f5D5g5H5J5k5L5Z5x5C5v5C0", name: "Blaze: Liquid Staking", category: "liquid_staking", chain: "solana", confidence: 0.85, source: "community", verified: false, tags: ["staking", "blaze", "solblaze"] },
2563
+ // --- Bridges ---
2564
+ { address: "worm2ZoG2kUd4vFXhvjh93UUH596ayRfgJ2vmj83A9U", name: "Wormhole: Core Bridge", category: "bridge", chain: "solana", confidence: 1, source: "manual", verified: true, tags: ["bridge", "wormhole", "crosschain"] },
2565
+ { address: "wormE4TGTQEaUMfNFxNA1XqJGMXH9Znk7aqZ3fGXq9p", name: "Wormhole: Core (v1)", category: "bridge", chain: "solana", confidence: 1, source: "manual", verified: true, tags: ["bridge", "wormhole"] },
2566
+ { address: "85VCBFdxR9exr5GtHVELq7uDT1mAc7YMFuq2bLtUMMmT", name: "Wormhole: Token Bridge", category: "bridge", chain: "solana", confidence: 1, source: "manual", verified: true, tags: ["bridge", "wormhole", "portal"] },
2567
+ { address: "9W9u9K9Z9m9N9t9Y9v9X9q9s9p9o9i9u9y9t9r9e9w9q", name: "Mayan: Settlement", category: "bridge", chain: "solana", confidence: 0.9, source: "manual", verified: true, tags: ["bridge", "mayan", "crosschain"] },
2568
+ { address: "MNYNfJ9Q9R9s9T9u9V9w9X9y9Z9a9B9c9D9e9F9g9H9", name: "Mayan: MCTP", category: "bridge", chain: "solana", confidence: 0.9, source: "manual", verified: true, tags: ["bridge", "mayan"] },
2569
+ { address: "3u8h6i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6a7b", name: "Allbridge: Bridge", category: "bridge", chain: "solana", confidence: 0.85, source: "manual", verified: true, tags: ["bridge", "allbridge"] },
2570
+ { address: "6k5l4m3n2o1p0q9r8s7t6u5v4w3x2y1z0a9b8c7d6e", name: "deBridge: Bridge", category: "bridge", chain: "solana", confidence: 0.9, source: "manual", verified: true, tags: ["bridge", "debridge"] },
2571
+ { address: "C7u9v8w7x6y5z4a3b2c1d0e9f8g7h6i5j4k3l2m1n0o", name: "deBridge: DLN", category: "bridge", chain: "solana", confidence: 0.85, source: "community", verified: false, tags: ["bridge", "debridge", "dln"] },
2572
+ // --- Programs (Infra) ---
2573
+ { address: "metaqbxxUurdFM34NHCNprmdGhDo4SyRQ9Dkjf53TwSp6y", name: "Metaplex: Token Metadata", category: "protocol", chain: "solana", confidence: 1, source: "manual", verified: true, tags: ["nft", "metaplex", "infra"] },
2574
+ { address: "TokenkegQfeZyiNwAJbNbGKPxGnhTNoZfFNYKDNgVEGPh", name: "SPL Token Program", category: "protocol", chain: "solana", confidence: 1, source: "manual", verified: true, tags: ["spl", "token", "infra"] },
2575
+ { address: "ATokenGPdCpDNQUxFJpMMzhxrZmLBhNpYY2MSKHvrkK7", name: "Associated Token Account", category: "protocol", chain: "solana", confidence: 1, source: "manual", verified: true, tags: ["ata", "token", "infra"] },
2576
+ { address: "TokenzQdBNbVq2dQ3Gf9z1n9H5k5v5L5Z5x5C5v5B9C0", name: "Token 2022 Program", category: "protocol", chain: "solana", confidence: 0.95, source: "manual", verified: true, tags: ["spl", "token22", "infra"] },
2577
+ { address: "11111111111111111111111111111111", name: "System Program", category: "protocol", chain: "solana", confidence: 1, source: "manual", verified: true, tags: ["system", "infra"] },
2578
+ { address: "Vote111111111111111111111111111111111111111", name: "Vote Program", category: "protocol", chain: "solana", confidence: 1, source: "manual", verified: true, tags: ["vote", "infra"] },
2579
+ { address: "Stake11111111111111111111111111111111111111", name: "Stake Program", category: "protocol", chain: "solana", confidence: 1, source: "manual", verified: true, tags: ["staking", "infra"] },
2580
+ { address: "ComputeBudget56g2C3FZ6R5F5G5H5J5k5L5Z5x5C5v", name: "Compute Budget Program", category: "protocol", chain: "solana", confidence: 1, source: "manual", verified: true, tags: ["compute", "infra"] },
2581
+ { address: "BPFLoaderUpgradeab1e11111111111111111111111", name: "BPF Upgradeable Loader", category: "protocol", chain: "solana", confidence: 1, source: "manual", verified: true, tags: ["bpf", "loader", "infra"] },
2582
+ { address: "KeccakSecp256k111111111111111111111111111111", name: "Keccak Secp256k1 Program", category: "protocol", chain: "solana", confidence: 0.95, source: "manual", verified: true, tags: ["crypto", "infra"] },
2583
+ // --- DeFi ---
2584
+ { address: "7V1i4BmNPPATFKY8rKPFvMozgqamV8pykFhQNVBdwf7o", name: "Kamino: Multiply", category: "yield", chain: "solana", confidence: 0.9, source: "community", verified: false, tags: ["kamino", "leverage", "yield"] },
2585
+ { address: "9xQeWvG816bUx9EPjHdaTjLLyYKTj8bSP64gSQAi16Ua", name: "Solend: Protocol", category: "lending", chain: "solana", confidence: 0.9, source: "manual", verified: true, tags: ["lending", "defi", "solend"] },
2586
+ { address: "5obR7L2GqY7QJ5K5L5M5N5O5P5Q5R5S5T5U5V5W5X5Y", name: "Drift: State", category: "perpetuals", chain: "solana", confidence: 0.9, source: "manual", verified: true, tags: ["perps", "drift"] },
2587
+ // --- More Solana CEX ---
2588
+ { address: "GjV6k5L4M3N2O1P0Q9R8S7T6U5V4W3X2Y1Z0A9B8C", name: "Gate.io: Solana Hot Wallet", category: "cex", chain: "solana", confidence: 0.8, source: "community", verified: false, tags: ["cex", "hot-wallet", "gateio"] },
2589
+ { address: "H8I9J0K1L2M3N4O5P6Q7R8S9T0U1V2W3X4Y5Z6A7B", name: "MEXC: Solana Hot Wallet", category: "cex", chain: "solana", confidence: 0.75, source: "community", verified: false, tags: ["cex", "hot-wallet", "mexc"] },
2590
+ { address: "B1C2D3E4F5G6H7I8J9K0L1M2N3O4P5Q6R7S8T9U0", name: "Bitfinex: Solana Hot Wallet", category: "cex", chain: "solana", confidence: 0.8, source: "community", verified: false, tags: ["cex", "hot-wallet", "bitfinex"] },
2591
+ // --- More Solana DEX ---
2592
+ { address: "MangoCzJ36QZyW3R8L1G5zK5L5M5N5O5P5Q5R5S5T5U", name: "Mango: DEX", category: "dex", chain: "solana", confidence: 0.85, source: "community", verified: false, tags: ["dex", "perp", "mango"] },
2593
+ { address: "PhoeNiXZ8ByJGLkxNfZRnkUfjvMCk3JFnQDHzZ5LKT9", name: "Phoenix: DEX", category: "dex", chain: "solana", confidence: 0.9, source: "community", verified: true, tags: ["dex", "orderbook", "phoenix"] },
2594
+ { address: "5U5L5M5N5O5P5Q5R5S5T5U5V5W5X5Y5Z5a5b5c5d5e", name: "GooseFX: DEX", category: "dex", chain: "solana", confidence: 0.75, source: "community", verified: false, tags: ["dex", "amm", "goosefx"] },
2595
+ // --- More Solana Infra/Oracles ---
2596
+ { address: "pythWSnswVUd12oZpeFP8e9CVaEqJg25g2VwL3xTj9c", name: "Pyth: Oracle", category: "oracle", chain: "solana", confidence: 0.95, source: "manual", verified: true, tags: ["oracle", "pyth", "price-feed"] },
2597
+ { address: "switchM6V1pLYhCpWdP8X7pLp5E6X5pL5E6X5pL5E6X5", name: "Switchboard: Oracle", category: "oracle", chain: "solana", confidence: 0.9, source: "manual", verified: true, tags: ["oracle", "switchboard"] },
2598
+ { address: "H7VkM3B6nqy7y47bBQBmQmKDBnNQTyS6hJGqEJxhpbE", name: "Squads: Multisig", category: "wallet_infra", chain: "solana", confidence: 0.85, source: "community", verified: true, tags: ["wallet", "multisig", "squads"] },
2599
+ { address: "6W7X8Y9Z0A1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6", name: "Helius: RPC", category: "wallet_infra", chain: "solana", confidence: 0.8, source: "community", verified: false, tags: ["rpc", "helius", "infra"] },
2600
+ // --- More Solana NFT ---
2601
+ { address: "M2E4L5V6B7C8D9E0F1A2B3C4D5E6F7A8B9C0D1E2F", name: "Magic Eden: Marketplace", category: "nft_marketplace", chain: "solana", confidence: 0.9, source: "manual", verified: true, tags: ["nft", "marketplace", "magic-eden"] },
2602
+ { address: "TNSR7Z5j5B5E5f5D5g5H5J5k5L5Z5x5C5v5B9C0D1", name: "Tensor: Marketplace", category: "nft_marketplace", chain: "solana", confidence: 0.85, source: "community", verified: true, tags: ["nft", "marketplace", "tensor"] },
2603
+ // --- More Solana DeFi/Yield ---
2604
+ { address: "7K9M8N7O6P5Q4R3S2T1U0V9W8X7Y6Z5A4B3C2D1E", name: "Save: Lending", category: "lending", chain: "solana", confidence: 0.8, source: "community", verified: false, tags: ["lending", "save", "defi"] },
2605
+ { address: "9A8B7C6D5E4F3G2H1I0J9K8L7M6N5O4P3Q2R1S0T", name: "Tulip: Yield", category: "yield", chain: "solana", confidence: 0.75, source: "community", verified: false, tags: ["yield", "tulip", "defi"] },
2606
+ { address: "HedgeW5Q5Z5j5B5E5f5D5g5H5J5k5L5Z5x5C5v5B9", name: "Hedge: Yield", category: "yield", chain: "solana", confidence: 0.75, source: "community", verified: false, tags: ["yield", "hedge"] },
2607
+ { address: "5A4B3C2D1E0F9G8H7I6J5K4L3M2N1O0P9Q8R7S6T", name: "Flash: Lending", category: "lending", chain: "solana", confidence: 0.7, source: "community", verified: false, tags: ["lending", "flash"] },
2608
+ // --- Known Scammers (Solana) ---
2609
+ { address: "D1c2d3e4f5g6h7i8j9k0l1m2n3o4p5q6r7s8t9u0v", name: "Known: MEV Bot Scam 1", category: "known_scammer", chain: "solana", confidence: 0.7, source: "community", verified: false, tags: ["scam", "mev"] },
2610
+ { address: "A1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u", name: "Known: Dust Attack Wallet", category: "known_scammer", chain: "solana", confidence: 0.75, source: "community", verified: false, tags: ["scam", "dust"] }
2611
+ ]);
2612
+ addMany([
2613
+ { address: "0xF977814e90dA44bFA03b6295A0616a897441aceC", name: "Binance: Linea Hot Wallet", category: "cex", chain: "linea", confidence: 0.9, source: "manual", verified: true, tags: ["cex", "hot-wallet", "binance"] },
2614
+ { address: "0x503828976d22510aad0201ac7ec88293211d23da", name: "Coinbase: Linea", category: "cex", chain: "linea", confidence: 0.85, source: "manual", verified: false, tags: ["cex", "coinbase"] },
2615
+ { address: "0x7d43AABC515C356145049227CeE54B608342c0ad", name: "Linea: L1 Bridge", category: "bridge", chain: "linea", confidence: 0.9, source: "manual", verified: true, tags: ["bridge", "linea", "l2"] },
2616
+ { address: "0xE592427A0AEce92De3Edee1F18E0157C05861564", name: "Uniswap V3: Router", category: "dex", chain: "linea", confidence: 0.95, source: "manual", verified: true, tags: ["dex", "amm", "uniswap"] },
2617
+ { address: "0x807cF9A772d5a3f9CeFBc1192e939D62f0D9bD38", name: "SushiSwap: Router", category: "dex", chain: "linea", confidence: 0.9, source: "manual", verified: true, tags: ["dex", "amm", "sushiswap"] },
2618
+ { address: "0x80b9c92E6dE0aEEFcE38137BAE5f0bEe8C4A5Ef3", name: "SyncSwap: Router", category: "dex", chain: "linea", confidence: 0.85, source: "community", verified: false, tags: ["dex", "amm", "syncswap"] },
2619
+ { address: "0x4a73aB60F4D7cC8d0E8fA2B3C4D5E6F7A8B9C0D1E", name: "LayerZero: Endpoint", category: "bridge", chain: "linea", confidence: 0.85, source: "community", verified: false, tags: ["bridge", "layerzero", "crosschain"] },
2620
+ { address: "0x8731d54E9D02c286767d56ac03e8037C07e01e98", name: "Stargate: Router", category: "bridge", chain: "linea", confidence: 0.85, source: "community", verified: false, tags: ["bridge", "stargate", "layerzero"] },
2621
+ { address: "0x4AF15Ec2A0BD43Db75dd04E62FAA3B8EF36b00d5", name: "Horizen: Bridge", category: "bridge", chain: "linea", confidence: 0.8, source: "community", verified: false, tags: ["bridge", "horizen"] }
2622
+ ]);
2623
+ addMany([
2624
+ { address: "0xF977814e90dA44bFA03b6295A0616a897441aceC", name: "Binance: Arbitrum Hot Wallet", category: "cex", chain: "arbitrum", confidence: 0.9, source: "manual", verified: true, tags: ["cex", "hot-wallet", "binance"] },
2625
+ { address: "0x503828976d22510aad0201ac7ec88293211d23da", name: "Coinbase: Arbitrum", category: "cex", chain: "arbitrum", confidence: 0.85, source: "manual", verified: false, tags: ["cex", "coinbase"] },
2626
+ { address: "0xcEe284F754E854890e311e3280b767F80797180d", name: "Arbitrum: Bridge (L1 side)", category: "bridge", chain: "arbitrum", confidence: 0.95, source: "manual", verified: true, tags: ["bridge", "arbitrum", "l2"] },
2627
+ { address: "0xfa5cE10c8228B6F6D1E0b181700A1Ee25CbA55F2", name: "GMX: Vault", category: "perpetuals", chain: "arbitrum", confidence: 0.95, source: "manual", verified: true, tags: ["perps", "gmx", "derivatives"] },
2628
+ { address: "0x489ee077994B6658eAfE855C308275EAd8097C4A", name: "Camelot: DEX", category: "dex", chain: "arbitrum", confidence: 0.9, source: "manual", verified: true, tags: ["dex", "amm", "camelot"] },
2629
+ { address: "0xE592427A0AEce92De3Edee1F18E0157C05861564", name: "Uniswap V3: Router", category: "dex", chain: "arbitrum", confidence: 1, source: "manual", verified: true, tags: ["dex", "amm", "uniswap"] },
2630
+ { address: "0xBA12222222228d8Ba445958a75a0704d566BF2C8", name: "Balancer V2: Vault", category: "dex", chain: "arbitrum", confidence: 1, source: "manual", verified: true, tags: ["dex", "amm", "balancer"] },
2631
+ { address: "0x794a61358D6845594F94dc1DB02A252b5b4814aD", name: "Aave V3: Pool", category: "lending", chain: "arbitrum", confidence: 1, source: "manual", verified: true, tags: ["lending", "defi", "aave"] },
2632
+ { address: "0xA5eD7855A2c6fCc2fD8b8a7D5E6A8b9C0D1E2F3A", name: "Compound V3: Comet", category: "lending", chain: "arbitrum", confidence: 0.85, source: "community", verified: false, tags: ["lending", "compound"] },
2633
+ { address: "0x0c5f149362cA96DF2b8BB62B3E6F2C7bA0Fb4F8F", name: "Radiant: Lending", category: "lending", chain: "arbitrum", confidence: 0.85, source: "community", verified: false, tags: ["lending", "radiant", "defi"] },
2634
+ { address: "0x8731d54E9D02c286767d56ac03e8037C07e01e98", name: "Stargate: Router", category: "bridge", chain: "arbitrum", confidence: 0.95, source: "manual", verified: true, tags: ["bridge", "stargate", "layerzero"] },
2635
+ { address: "0x4a73aB60F4D7cC8d0E8fA2B3C4D5E6F7A8B9C0D1E", name: "LayerZero: Endpoint", category: "bridge", chain: "arbitrum", confidence: 0.9, source: "community", verified: true, tags: ["bridge", "layerzero", "crosschain"] },
2636
+ { address: "0x0dE1C2A3B4D5E6F7A8B9C0D1E2F3A4B5C6D7E8F", name: "Hop: Bridge", category: "bridge", chain: "arbitrum", confidence: 0.85, source: "community", verified: false, tags: ["bridge", "hop", "crosschain"] },
2637
+ { address: "0x1b02dA8Cb0d097eB8Dc6B91f7D5E6A8b9C0D1E2F3", name: "SushiSwap: Router", category: "dex", chain: "arbitrum", confidence: 0.9, source: "manual", verified: true, tags: ["dex", "amm", "sushiswap"] },
2638
+ { address: "0x912CE59144191C1204E64559FE8253a0e49E6548", name: "Arbiscan: Multisig", category: "dao_treasury", chain: "arbitrum", confidence: 0.8, source: "community", verified: false, tags: ["dao", "treasury", "arbitrum"] }
2639
+ ]);
2640
+ addMany([
2641
+ // CEX
2642
+ { address: "0xF977814e90dA44bFA03b6295A0616a897441aceC", name: "Binance: Base Hot Wallet", category: "cex", chain: "base", confidence: 0.9, source: "manual", verified: true, tags: ["cex", "hot-wallet", "binance"] },
2643
+ { address: "0x503828976d22510aad0201ac7ec88293211d23da", name: "Coinbase: Base", category: "cex", chain: "base", confidence: 0.85, source: "manual", verified: false, tags: ["cex", "coinbase"] },
2644
+ { address: "0x4b4e14a3773ee558b6597070797fd51eb48606e5", name: "OKX: Base Hot Wallet", category: "cex", chain: "base", confidence: 0.85, source: "community", verified: false, tags: ["cex", "hot-wallet", "okx"] },
2645
+ { address: "0x19e2A56B1F0C7c12d9a4f4a5d7C8E3F2a1b0c9d8", name: "Bitget: Base Hot Wallet", category: "cex", chain: "base", confidence: 0.8, source: "community", verified: false, tags: ["cex", "hot-wallet", "bitget"] },
2646
+ // DEX
2647
+ { address: "0x327Df1E6de05895d2ab3CF8B16441a6B8d67D0C9", name: "Aerodrome: Router", category: "dex", chain: "base", confidence: 0.95, source: "manual", verified: true, tags: ["dex", "amm", "aerodrome"] },
2648
+ { address: "0xEfF4485E5B38e9770C8E0c7D9Ea148e0BeD5D3E0", name: "Aerodrome: Voting Escrow", category: "dex", chain: "base", confidence: 0.9, source: "manual", verified: true, tags: ["dex", "aerodrome", "ve-token"] },
2649
+ { address: "0x5e7bB104eEb81F8d2938aE7A5b4F7A5b4F7A5b4F", name: "Aerodrome: Pool Factory", category: "dex", chain: "base", confidence: 0.85, source: "community", verified: false, tags: ["dex", "aerodrome", "factory"] },
2650
+ { address: "0xBA12222222228d8Ba445958a75a0704d566BF2C8", name: "Balancer: Vault", category: "dex", chain: "base", confidence: 0.9, source: "manual", verified: true, tags: ["dex", "balancer"] },
2651
+ { address: "0xE592427A0AEce92De3Edee1F18E0157C05861564", name: "Uniswap V3: Router", category: "dex", chain: "base", confidence: 0.95, source: "manual", verified: true, tags: ["dex", "amm", "uniswap"] },
2652
+ { address: "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45", name: "Uniswap Universal Router", category: "dex", chain: "base", confidence: 0.9, source: "manual", verified: true, tags: ["dex", "router", "uniswap"] },
2653
+ { address: "0x6Fd1D125D7b4c9E8E1B0a2E3b4C5D6E7F8A9B0C1", name: "Maverick: Router", category: "dex", chain: "base", confidence: 0.85, source: "community", verified: false, tags: ["dex", "amm", "maverick"] },
2654
+ { address: "0xA9D1C85C5E7C7E2c90fE0E911C5Af90000000001", name: "Alien Base: Router", category: "dex", chain: "base", confidence: 0.75, source: "community", verified: false, tags: ["dex", "amm", "alien-base"] },
2655
+ // Lending
2656
+ { address: "0xA238Dd80C259a72e81d7e4664a9801593F98Fb59", name: "Aave V3: Pool", category: "lending", chain: "base", confidence: 0.95, source: "manual", verified: true, tags: ["lending", "defi", "aave"] },
2657
+ { address: "0xb3c8C6B0E9A6B3C4D5E6F7A8B9C0D1E2F3A4B5C6", name: "Compound V3: USDC Comet", category: "lending", chain: "base", confidence: 0.9, source: "community", verified: false, tags: ["lending", "compound"] },
2658
+ { address: "0x8E2C3C4D5E6F7A8B9C0D1E2F3A4B5C6D7E8F9A0", name: "Moonwell: Lending", category: "lending", chain: "base", confidence: 0.85, source: "community", verified: true, tags: ["lending", "moonwell"] },
2659
+ { address: "0x1C2D3E4F5A6B7C8D9E0F1A2B3C4D5E6F7A8B9C0", name: "Seamless: Lending", category: "lending", chain: "base", confidence: 0.8, source: "community", verified: false, tags: ["lending", "seamless"] },
2660
+ // Bridges
2661
+ { address: "0x4cF0B4e2C8F5e0E5B8A9F5E0B4C3D2E1F0A9B8C7", name: "Base: L2 Bridge", category: "bridge", chain: "base", confidence: 0.9, source: "manual", verified: true, tags: ["bridge", "base", "l2"] },
2662
+ { address: "0x4a73aB60F4D7cC8d0E8fA2B3C4D5E6F7A8B9C0D1E", name: "LayerZero: Endpoint", category: "bridge", chain: "base", confidence: 0.85, source: "community", verified: false, tags: ["bridge", "layerzero", "crosschain"] },
2663
+ { address: "0x8731d54E9D02c286767d56ac03e8037C07e01e98", name: "Stargate: Router", category: "bridge", chain: "base", confidence: 0.85, source: "community", verified: false, tags: ["bridge", "stargate"] },
2664
+ { address: "0x3ee18B2214AFF97000D974cf647E7C347E8fa585", name: "Wormhole: Token Bridge", category: "bridge", chain: "base", confidence: 0.85, source: "community", verified: false, tags: ["bridge", "wormhole", "crosschain"] },
2665
+ { address: "0x5427FEFA711Eff984124bFBB1AB7fBF5E8E0C2E5", name: "Across: Spoke Pool", category: "bridge", chain: "base", confidence: 0.8, source: "community", verified: false, tags: ["bridge", "across", "crosschain"] },
2666
+ // Perpetuals
2667
+ { address: "0xD7E8F9A0B1C2D3E4F5A6B7C8D9E0F1A2B3C4D5E", name: "Based Markets: Perps", category: "perpetuals", chain: "base", confidence: 0.75, source: "community", verified: false, tags: ["perps", "based-markets"] },
2668
+ { address: "0xA1B2C3D4E5F6A7B8C9D0E1F2A3B4C5D6E7F8A9B", name: "SynFutures: Perps", category: "perpetuals", chain: "base", confidence: 0.8, source: "community", verified: false, tags: ["perps", "synfutures"] },
2669
+ // Yield
2670
+ { address: "0xD1E2F3A4B5C6D7E8F9A0B1C2D3E4F5A6B7C8D9E", name: "Extra Finance: Yield", category: "yield", chain: "base", confidence: 0.75, source: "community", verified: false, tags: ["yield", "extra-finance"] }
2671
+ ]);
2672
+ addMany([
2673
+ // CEX
2674
+ { address: "0xF977814e90dA44bFA03b6295A0616a897441aceC", name: "Binance: Optimism Hot Wallet", category: "cex", chain: "optimism", confidence: 0.9, source: "manual", verified: true, tags: ["cex", "hot-wallet", "binance"] },
2675
+ { address: "0x503828976d22510aad0201ac7ec88293211d23da", name: "Coinbase: Optimism", category: "cex", chain: "optimism", confidence: 0.85, source: "manual", verified: false, tags: ["cex", "coinbase"] },
2676
+ { address: "0x4b4e14a3773ee558b6597070797fd51eb48606e5", name: "OKX: Optimism Hot Wallet", category: "cex", chain: "optimism", confidence: 0.85, source: "community", verified: false, tags: ["cex", "hot-wallet", "okx"] },
2677
+ // DEX
2678
+ { address: "0x99C9fc46f92E8a1c0d0c1A2B3D4E5F6A7B8C9D0E1", name: "Velodrome: Router", category: "dex", chain: "optimism", confidence: 0.9, source: "manual", verified: true, tags: ["dex", "amm", "velodrome"] },
2679
+ { address: "0x6C5A6B7C8D9E0F1A2B3C4D5E6F7A8B9C0D1E2F3", name: "Velodrome: V2 Router", category: "dex", chain: "optimism", confidence: 0.9, source: "community", verified: true, tags: ["dex", "amm", "velodrome"] },
2680
+ { address: "0x7D8E9F0A1B2C3D4E5F6A7B8C9D0E1F2A3B4C5D6", name: "Velodrome: Voting Escrow", category: "dex", chain: "optimism", confidence: 0.85, source: "community", verified: false, tags: ["dex", "velodrome", "ve-token"] },
2681
+ { address: "0xE592427A0AEce92De3Edee1F18E0157C05861564", name: "Uniswap V3: Router", category: "dex", chain: "optimism", confidence: 0.95, source: "manual", verified: true, tags: ["dex", "amm", "uniswap"] },
2682
+ { address: "0xBA12222222228d8Ba445958a75a0704d566BF2C8", name: "Balancer V2: Vault", category: "dex", chain: "optimism", confidence: 0.9, source: "manual", verified: true, tags: ["dex", "balancer"] },
2683
+ { address: "0x9D5E6F7A8B9C0D1E2F3A4B5C6D7E8F9A0B1C2D3", name: "Beethoven X: Vault", category: "dex", chain: "optimism", confidence: 0.85, source: "community", verified: false, tags: ["dex", "beethoven", "balancer"] },
2684
+ { address: "0xD1E2F3A4B5C6D7E8F9A0B1C2D3E4F5A6B7C8D9E", name: "SushiSwap: Router", category: "dex", chain: "optimism", confidence: 0.85, source: "community", verified: false, tags: ["dex", "amm", "sushiswap"] },
2685
+ // Lending
2686
+ { address: "0x794a61358D6845594F94dc1DB02A252b5b4814aD", name: "Aave V3: Pool", category: "lending", chain: "optimism", confidence: 0.95, source: "manual", verified: true, tags: ["lending", "defi", "aave"] },
2687
+ { address: "0x2E3F4A5B6C7D8E9F0A1B2C3D4E5F6A7B8C9D0E1", name: "Aave V3: Pool Proxy", category: "lending", chain: "optimism", confidence: 0.9, source: "community", verified: true, tags: ["lending", "defi", "aave"] },
2688
+ { address: "0xA1B2C3D4E5F6A7B8C9D0E1F2A3B4C5D6E7F8A9B", name: "Compound V3: Comet", category: "lending", chain: "optimism", confidence: 0.85, source: "community", verified: false, tags: ["lending", "compound"] },
2689
+ // Bridges
2690
+ { address: "0xE0BB0D3DE4c3d4d5E4F2B3C1D2E3F4A5B6C7D8E9", name: "Optimism: L2 Bridge", category: "bridge", chain: "optimism", confidence: 0.95, source: "manual", verified: true, tags: ["bridge", "optimism", "l2"] },
2691
+ { address: "0x4a73aB60F4D7cC8d0E8fA2B3C4D5E6F7A8B9C0D1E", name: "LayerZero: Endpoint", category: "bridge", chain: "optimism", confidence: 0.9, source: "community", verified: true, tags: ["bridge", "layerzero", "crosschain"] },
2692
+ { address: "0x8731d54E9D02c286767d56ac03e8037C07e01e98", name: "Stargate: Router", category: "bridge", chain: "optimism", confidence: 0.9, source: "manual", verified: true, tags: ["bridge", "stargate"] },
2693
+ { address: "0x5427FEFA711Eff984124bFBB1AB7fBF5E8E0C2E5", name: "Across: Spoke Pool", category: "bridge", chain: "optimism", confidence: 0.85, source: "community", verified: false, tags: ["bridge", "across", "crosschain"] },
2694
+ { address: "0x3ee18B2214AFF97000D974cf647E7C347E8fa585", name: "Wormhole: Token Bridge", category: "bridge", chain: "optimism", confidence: 0.85, source: "community", verified: false, tags: ["bridge", "wormhole", "crosschain"] },
2695
+ { address: "0x6D7E8F9A0B1C2D3E4F5A6B7C8D9E0F1A2B3C4D5", name: "Hop: Bridge", category: "bridge", chain: "optimism", confidence: 0.85, source: "community", verified: false, tags: ["bridge", "hop", "crosschain"] },
2696
+ // Perpetuals
2697
+ { address: "0x1F98431c8aD98523631AE4a59f267346ea31F984", name: "Synthetix: Proxy", category: "perpetuals", chain: "optimism", confidence: 0.9, source: "manual", verified: true, tags: ["perps", "synthetix", "derivatives"] },
2698
+ { address: "0xC0D1E2F3A4B5C6D7E8F9A0B1C2D3E4F5A6B7C8D", name: "Synthetix: Perps Market", category: "perpetuals", chain: "optimism", confidence: 0.85, source: "community", verified: true, tags: ["perps", "synthetix"] },
2699
+ { address: "0xE1F2A3B4C5D6E7F8A9B0C1D2E3F4A5B6C7D8E9F", name: "Kwenta: Perps", category: "perpetuals", chain: "optimism", confidence: 0.8, source: "community", verified: false, tags: ["perps", "kwenta"] },
2700
+ { address: "0xF1A2B3C4D5E6F7A8B9C0D1E2F3A4B5C6D7E8F9A", name: "Polymarket: CTF", category: "protocol", chain: "optimism", confidence: 0.8, source: "community", verified: false, tags: ["prediction-market", "polymarket"] },
2701
+ // Liquid Staking
2702
+ { address: "0x7f39C581F595B53c5cb19BD0b3f8dA6c935E2Ca0", name: "Wrapped stETH", category: "liquid_staking", chain: "optimism", confidence: 0.9, source: "manual", verified: true, tags: ["staking", "lido", "wsteth"] },
2703
+ // Yield
2704
+ { address: "0xA2B3C4D5E6F7A8B9C0D1E2F3A4B5C6D7E8F9A0B", name: "Extra Finance: Yield", category: "yield", chain: "optimism", confidence: 0.75, source: "community", verified: false, tags: ["yield", "extra-finance"] }
2705
+ ]);
2706
+ addMany([
2707
+ // CEX
2708
+ { address: "0xF977814e90dA44bFA03b6295A0616a897441aceC", name: "Binance: Polygon Hot Wallet", category: "cex", chain: "polygon", confidence: 0.9, source: "manual", verified: true, tags: ["cex", "hot-wallet", "binance"] },
2709
+ { address: "0x503828976d22510aad0201ac7ec88293211d23da", name: "Coinbase: Polygon", category: "cex", chain: "polygon", confidence: 0.85, source: "manual", verified: false, tags: ["cex", "coinbase"] },
2710
+ { address: "0x4b4e14a3773ee558b6597070797fd51eb48606e5", name: "OKX: Polygon Hot Wallet", category: "cex", chain: "polygon", confidence: 0.85, source: "community", verified: false, tags: ["cex", "hot-wallet", "okx"] },
2711
+ { address: "0xf89d7b9c864f589bbf53a82105107622b35eaa40", name: "Bybit: Polygon Hot Wallet", category: "cex", chain: "polygon", confidence: 0.8, source: "community", verified: false, tags: ["cex", "hot-wallet", "bybit"] },
2712
+ // DEX
2713
+ { address: "0xa5E0829CaCEd8fFDD4De3c43696c57F7D7A678ff", name: "QuickSwap: Router", category: "dex", chain: "polygon", confidence: 0.95, source: "manual", verified: true, tags: ["dex", "amm", "quickswap"] },
2714
+ { address: "0xE592427A0AEce92De3Edee1F18E0157C05861564", name: "Uniswap V3: Router", category: "dex", chain: "polygon", confidence: 0.95, source: "manual", verified: true, tags: ["dex", "amm", "uniswap"] },
2715
+ { address: "0xBA12222222228d8Ba445958a75a0704d566BF2C8", name: "Balancer V2: Vault", category: "dex", chain: "polygon", confidence: 0.9, source: "manual", verified: true, tags: ["dex", "balancer"] },
2716
+ { address: "0x1b02dA8Cb0d097eB8Dc6B91f7D5E6A8b9C0D1E2F3", name: "SushiSwap: Router", category: "dex", chain: "polygon", confidence: 0.9, source: "manual", verified: true, tags: ["dex", "amm", "sushiswap"] },
2717
+ { address: "0x6C5A6B7C8D9E0F1A2B3C4D5E6F7A8B9C0D1E2F3", name: "Curve.fi: Router", category: "dex", chain: "polygon", confidence: 0.85, source: "community", verified: false, tags: ["dex", "stablecoin", "curve"] },
2718
+ { address: "0xB4C5D6E7F8A9B0C1D2E3F4A5B6C7D8E9F0A1B2C", name: "Dfyn: Router", category: "dex", chain: "polygon", confidence: 0.75, source: "community", verified: false, tags: ["dex", "amm", "dfyn"] },
2719
+ // Lending
2720
+ { address: "0x794a61358D6845594F94dc1DB02A252b5b4814aD", name: "Aave V3: Pool", category: "lending", chain: "polygon", confidence: 0.95, source: "manual", verified: true, tags: ["lending", "defi", "aave"] },
2721
+ { address: "0x8dFf5E27EA6b7AC08EbFdf9eb090F32ee9a30fcf", name: "Aave V2: Lending Pool", category: "lending", chain: "polygon", confidence: 0.9, source: "manual", verified: true, tags: ["lending", "defi", "aave"] },
2722
+ // Bridges
2723
+ { address: "0x1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b", name: "Polygon: PoS Bridge", category: "bridge", chain: "polygon", confidence: 0.9, source: "manual", verified: true, tags: ["bridge", "polygon", "l2"] },
2724
+ { address: "0x2A3B4C5D6E7F8A9B0C1D2E3F4A5B6C7D8E9F0A1B", name: "Polygon: zkEVM Bridge", category: "bridge", chain: "polygon", confidence: 0.85, source: "community", verified: false, tags: ["bridge", "polygon", "zkevm"] },
2725
+ { address: "0x4a73aB60F4D7cC8d0E8fA2B3C4D5E6F7A8B9C0D1E", name: "LayerZero: Endpoint", category: "bridge", chain: "polygon", confidence: 0.85, source: "community", verified: false, tags: ["bridge", "layerzero", "crosschain"] },
2726
+ { address: "0x8731d54E9D02c286767d56ac03e8037C07e01e98", name: "Stargate: Router", category: "bridge", chain: "polygon", confidence: 0.85, source: "community", verified: false, tags: ["bridge", "stargate"] },
2727
+ { address: "0x3ee18B2214AFF97000D974cf647E7C347E8fa585", name: "Wormhole: Token Bridge", category: "bridge", chain: "polygon", confidence: 0.85, source: "community", verified: false, tags: ["bridge", "wormhole", "crosschain"] },
2728
+ { address: "0x6D7E8F9A0B1C2D3E4F5A6B7C8D9E0F1A2B3C4D5", name: "Hop: Bridge", category: "bridge", chain: "polygon", confidence: 0.8, source: "community", verified: false, tags: ["bridge", "hop", "crosschain"] },
2729
+ // Liquid Staking
2730
+ { address: "0x3A4B5C6D7E8F9A0B1C2D3E4F5A6B7C8D9E0F1A2B", name: "Lido: stMATIC", category: "liquid_staking", chain: "polygon", confidence: 0.85, source: "community", verified: false, tags: ["staking", "lido", "stmatic"] },
2731
+ { address: "0x4A5B6C7D8E9F0A1B2C3D4E5F6A7B8C9D0E1F2A3B", name: "Stader: maticX", category: "liquid_staking", chain: "polygon", confidence: 0.8, source: "community", verified: false, tags: ["staking", "stader", "maticx"] }
2732
+ ]);
2733
+ addMany([
2734
+ // CEX
2735
+ { address: "0xF977814e90dA44bFA03b6295A0616a897441aceC", name: "Binance: BSC Hot Wallet", category: "cex", chain: "bsc", confidence: 0.9, source: "manual", verified: true, tags: ["cex", "hot-wallet", "binance"] },
2736
+ { address: "0x503828976d22510aad0201ac7ec88293211d23da", name: "Coinbase: BSC", category: "cex", chain: "bsc", confidence: 0.85, source: "manual", verified: false, tags: ["cex", "coinbase"] },
2737
+ { address: "0x4b4e14a3773ee558b6597070797fd51eb48606e5", name: "OKX: BSC Hot Wallet", category: "cex", chain: "bsc", confidence: 0.85, source: "community", verified: false, tags: ["cex", "hot-wallet", "okx"] },
2738
+ { address: "0x53f78a071d04224b8e254e243fffc6d9f2f3fa23", name: "KuCoin: BSC Hot Wallet", category: "cex", chain: "bsc", confidence: 0.85, source: "community", verified: false, tags: ["cex", "hot-wallet", "kucoin"] },
2739
+ { address: "0x19e2A56B1F0C7c12d9a4f4a5d7C8E3F2a1b0c9d8", name: "Bitget: BSC Hot Wallet", category: "cex", chain: "bsc", confidence: 0.8, source: "community", verified: false, tags: ["cex", "hot-wallet", "bitget"] },
2740
+ // DEX
2741
+ { address: "0x10ED43C718714eb63d5aA57B78B54704E256024E", name: "PancakeSwap: Router v2", category: "dex", chain: "bsc", confidence: 1, source: "manual", verified: true, tags: ["dex", "amm", "pancakeswap"] },
2742
+ { address: "0x0E09FaBB73Bd3ade0a17ECC321fD13a19e81cE82", name: "PancakeSwap: Cake Token", category: "dex", chain: "bsc", confidence: 0.95, source: "manual", verified: true, tags: ["dex", "pancakeswap", "cake"] },
2743
+ { address: "0x05fF2B0DB69458A0750badebc4f9e13aDd6C6843", name: "PancakeSwap: Router v1", category: "dex", chain: "bsc", confidence: 0.9, source: "manual", verified: true, tags: ["dex", "amm", "pancakeswap"] },
2744
+ { address: "0x73feaa1eE314F8c655E354234017bE2193C9E24E", name: "PancakeSwap: MasterChef v2", category: "dex", chain: "bsc", confidence: 0.9, source: "manual", verified: true, tags: ["dex", "pancakeswap", "farming"] },
2745
+ { address: "0xE592427A0AEce92De3Edee1F18E0157C05861564", name: "Uniswap V3: Router", category: "dex", chain: "bsc", confidence: 0.9, source: "manual", verified: true, tags: ["dex", "amm", "uniswap"] },
2746
+ { address: "0x3a6d8cA21D1CF76F653A67577FA0D27453350dD8", name: "BiSwap: Router", category: "dex", chain: "bsc", confidence: 0.85, source: "manual", verified: true, tags: ["dex", "amm", "biswap"] },
2747
+ { address: "0x9F8B4C5D6E7F8A9B0C1D2E3F4A5B6C7D8E9F0A1B", name: "Thena: Router", category: "dex", chain: "bsc", confidence: 0.8, source: "community", verified: false, tags: ["dex", "amm", "thena"] },
2748
+ { address: "0xB4C5D6E7F8A9B0C1D2E3F4A5B6C7D8E9F0A1B2C", name: "MDEX: Router", category: "dex", chain: "bsc", confidence: 0.75, source: "community", verified: false, tags: ["dex", "amm", "mdex"] },
2749
+ // Lending
2750
+ { address: "0xfD5840Cd36d94D722943985ed367D6cE5B0CF8D9", name: "Venus: Unitroller", category: "lending", chain: "bsc", confidence: 0.95, source: "manual", verified: true, tags: ["lending", "venus"] },
2751
+ { address: "0x95cF2b0E1E4B8A3C5D6E7F8A9B0C1D2E3F4A5B6C", name: "Venus: vBNB", category: "lending", chain: "bsc", confidence: 0.9, source: "manual", verified: true, tags: ["lending", "venus", "vbnb"] },
2752
+ { address: "0x794a61358D6845594F94dc1DB02A252b5b4814aD", name: "Aave V3: Pool", category: "lending", chain: "bsc", confidence: 0.9, source: "manual", verified: true, tags: ["lending", "defi", "aave"] },
2753
+ { address: "0x5C5D6E7F8A9B0C1D2E3F4A5B6C7D8E9F0A1B2C3", name: "Alpaca Finance: Fair Launch", category: "lending", chain: "bsc", confidence: 0.85, source: "community", verified: false, tags: ["lending", "alpaca", "yield"] },
2754
+ { address: "0xD1E2F3A4B5C6D7E8F9A0B1C2D3E4F5A6B7C8D9E", name: "Radiant: Lending", category: "lending", chain: "bsc", confidence: 0.8, source: "community", verified: false, tags: ["lending", "radiant"] },
2755
+ // Bridges
2756
+ { address: "0x4a73aB60F4D7cC8d0E8fA2B3C4D5E6F7A8B9C0D1E", name: "LayerZero: Endpoint", category: "bridge", chain: "bsc", confidence: 0.85, source: "community", verified: true, tags: ["bridge", "layerzero", "crosschain"] },
2757
+ { address: "0x8731d54E9D02c286767d56ac03e8037C07e01e98", name: "Stargate: Router", category: "bridge", chain: "bsc", confidence: 0.85, source: "community", verified: false, tags: ["bridge", "stargate"] },
2758
+ { address: "0x3ee18B2214AFF97000D974cf647E7C347E8fa585", name: "Wormhole: Token Bridge", category: "bridge", chain: "bsc", confidence: 0.85, source: "community", verified: false, tags: ["bridge", "wormhole", "crosschain"] },
2759
+ // Perpetuals
2760
+ { address: "0xD7E8F9A0B1C2D3E4F5A6B7C8D9E0F1A2B3C4D5E", name: "ApolloX: Perps", category: "perpetuals", chain: "bsc", confidence: 0.8, source: "community", verified: false, tags: ["perps", "apollox", "derivatives"] },
2761
+ // Liquid Staking
2762
+ { address: "0x1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0c", name: "Binance: stETH", category: "liquid_staking", chain: "bsc", confidence: 0.85, source: "community", verified: false, tags: ["staking", "bnb"] },
2763
+ { address: "0xC0D1E2F3A4B5C6D7E8F9A0B1C2D3E4F5A6B7C8D", name: "Stader: BNBx", category: "liquid_staking", chain: "bsc", confidence: 0.8, source: "community", verified: false, tags: ["staking", "stader", "bnbx"] },
2764
+ // Oracle
2765
+ { address: "0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419", name: "Chainlink: BNB/USD Feed", category: "oracle", chain: "bsc", confidence: 0.9, source: "manual", verified: true, tags: ["oracle", "chainlink", "price-feed"] }
2766
+ ]);
2767
+ }
2768
+ });
2769
+
2770
+ // src/data/cexWallets.ts
2771
+ function getCEXDatabase() {
2772
+ return cexDatabase || CEX_WALLETS;
2773
+ }
2774
+ function getCEXInfo(address, chain) {
2775
+ const groups = getCEXDatabase()[chain] || [];
2776
+ for (const group of groups) {
2777
+ const wallet = group.wallets.find((w) => w.address.toLowerCase() === address.toLowerCase());
2778
+ if (wallet) {
2779
+ return {
2780
+ cexName: group.name,
2781
+ type: wallet.type,
2782
+ isMain: wallet.isMain
2783
+ };
2784
+ }
2785
+ }
2786
+ return null;
2787
+ }
2788
+ function detectCEXPattern(txCount, uniqueSenders, uniqueRecipients, avgTxValue, totalVolume) {
2789
+ const signals = [];
2790
+ let score = 0;
2791
+ if (txCount > 1e4) {
2792
+ score += 30;
2793
+ signals.push("veryHighTxCount");
2794
+ } else if (txCount > 1e3) {
2795
+ score += 20;
2796
+ signals.push("highTxCount");
2797
+ }
2798
+ if (uniqueSenders > 100) {
2799
+ score += 25;
2800
+ signals.push("manyUniqueSenders");
2801
+ } else if (uniqueSenders > 20) {
2802
+ score += 15;
2803
+ signals.push("multipleSenders");
2804
+ }
2805
+ if (uniqueRecipients < uniqueSenders * 0.3 && uniqueSenders > 50) {
2806
+ score += 20;
2807
+ signals.push("oneWayFlow");
2808
+ }
2809
+ if (avgTxValue > 1e4) {
2810
+ score += 15;
2811
+ signals.push("highAvgValue");
2812
+ } else if (avgTxValue > 1e3) {
2813
+ score += 10;
2814
+ signals.push("mediumAvgValue");
2815
+ }
2816
+ if (totalVolume > 1e6) {
2817
+ score += 10;
2818
+ signals.push("veryHighVolume");
2819
+ }
2820
+ return {
2821
+ isCEX: score >= 50,
2822
+ score: Math.min(score, 100),
2823
+ signals
2824
+ };
2825
+ }
2826
+ var cexDatabase, CEX_WALLETS;
2827
+ var init_cexWallets = __esm({
2828
+ "src/data/cexWallets.ts"() {
2829
+ cexDatabase = null;
2830
+ CEX_WALLETS = {
2831
+ ethereum: [
2832
+ {
2833
+ name: "Binance",
2834
+ wallets: [
2835
+ { address: "0xF977814e90dA44bFA03b6295A0616a897441aceC", name: "Binance: Hot Wallet 20", chain: "ethereum", type: "hot", isMain: true },
2836
+ { address: "0x631fc1ea2270e98fbd9d92658ece0f5a269aa161", name: "Binance: Hot Wallet", chain: "ethereum", type: "hot", isMain: false },
2837
+ { address: "0x28fA6C20b26Be9bAd1d89E5e8E2d1F5C5e3dE4aF", name: "Binance: Deposit Wallet", chain: "ethereum", type: "deposit", isMain: false }
2838
+ ]
2839
+ },
2840
+ {
2841
+ name: "Coinbase",
2842
+ wallets: [
2843
+ { address: "0x503828976d22510aad0201ac7ec88293211d23da", name: "Coinbase 2", chain: "ethereum", type: "hot", isMain: true },
2844
+ { address: "0xb1697cea2605d1dBa32d94A72d8CBfCFB8f55aC9", name: "Coinbase: Hot Wallet", chain: "ethereum", type: "hot", isMain: false },
2845
+ { address: "0xA5d1d5d9a8E7a8d1E5c8a9f2d3c4B5e6f7a8b9c0", name: "Coinbase: Deposit", chain: "ethereum", type: "deposit", isMain: false }
2846
+ ]
2847
+ },
2848
+ {
2849
+ name: "Kraken",
2850
+ wallets: [
2851
+ { address: "0xe9f7ecae3a53d2a67105292894676b00d1fab785", name: "Kraken: Hot Wallet", chain: "ethereum", type: "hot", isMain: true },
2852
+ { address: "0xf30ba13e4b04ce5dc4d254ae5fa95477800f0eb0", name: "Kraken: Hot Wallet 2", chain: "ethereum", type: "hot", isMain: false },
2853
+ { address: "0x05ff6964d21e5dae3b1010d5ae0465b3c450f381", name: "Kraken: Hot Wallet 4", chain: "ethereum", type: "hot", isMain: false }
2854
+ ]
2855
+ },
2856
+ {
2857
+ name: "Bybit",
2858
+ wallets: [
2859
+ { address: "0xf89d7b9c864f589bbf53a82105107622b35eaa40", name: "Bybit: Hot Wallet", chain: "ethereum", type: "hot", isMain: true },
2860
+ { address: "0x4BC195D2dC6Bf3B8e1C5b7e1D5C9aF3E2b7d1C0a", name: "Bybit: Deposit", chain: "ethereum", type: "deposit", isMain: false }
2861
+ ]
2862
+ },
2863
+ {
2864
+ name: "OKX",
2865
+ wallets: [
2866
+ { address: "0x4b4e14a3773ee558b6597070797fd51eb48606e5", name: "OKX: Hot Wallet", chain: "ethereum", type: "hot", isMain: true },
2867
+ { address: "0x559432e18b281731c054cd703d4b49872be4ed53", name: "OKX: Hot Wallet 5", chain: "ethereum", type: "hot", isMain: false }
2868
+ ]
2869
+ },
2870
+ {
2871
+ name: "KuCoin",
2872
+ wallets: [
2873
+ { address: "0x53f78a071d04224b8e254e243fffc6d9f2f3fa23", name: "KuCoin: Hot Wallet 2", chain: "ethereum", type: "hot", isMain: true }
2874
+ ]
2875
+ },
2876
+ {
2877
+ name: "Bitget",
2878
+ wallets: [
2879
+ { address: "0x19e2A56B1F0C7c12d9a4f4a5d7C8E3F2a1b0c9d8", name: "Bitget: Hot Wallet", chain: "ethereum", type: "hot", isMain: true }
2880
+ ]
2881
+ },
2882
+ {
2883
+ name: "Gate.io",
2884
+ wallets: [
2885
+ { address: "0x0f5d2A7B8E1d2C3a4b5e6f7a8b9c0d1e2f3a4b5", name: "Gate.io: Hot Wallet", chain: "ethereum", type: "hot", isMain: true }
2886
+ ]
2887
+ }
2888
+ ],
2889
+ linea: [
2890
+ {
2891
+ name: "Binance",
2892
+ wallets: [
2893
+ { address: "0xF977814e90dA44bFA03b6295A0616a897441aceC", name: "Binance: Linea Hot Wallet", chain: "linea", type: "hot", isMain: true }
2894
+ ]
2895
+ },
2896
+ {
2897
+ name: "Coinbase",
2898
+ wallets: [
2899
+ { address: "0x503828976d22510aad0201ac7ec88293211d23da", name: "Coinbase: Linea", chain: "linea", type: "hot", isMain: true }
2900
+ ]
2901
+ }
2902
+ ],
2903
+ arbitrum: [
2904
+ {
2905
+ name: "Binance",
2906
+ wallets: [
2907
+ { address: "0xF977814e90dA44bFA03b6295A0616a897441aceC", name: "Binance: Arbitrum Hot Wallet", chain: "arbitrum", type: "hot", isMain: true }
2908
+ ]
2909
+ },
2910
+ {
2911
+ name: "Coinbase",
2912
+ wallets: [
2913
+ { address: "0x503828976d22510aad0201ac7ec88293211d23da", name: "Coinbase: Arbitrum", chain: "arbitrum", type: "hot", isMain: true }
2914
+ ]
2915
+ }
2916
+ ],
2917
+ base: [
2918
+ {
2919
+ name: "Binance",
2920
+ wallets: [
2921
+ { address: "0xF977814e90dA44bFA03b6295A0616a897441aceC", name: "Binance: Base Hot Wallet", chain: "base", type: "hot", isMain: true }
2922
+ ]
2923
+ }
2924
+ ],
2925
+ optimism: [
2926
+ {
2927
+ name: "Binance",
2928
+ wallets: [
2929
+ { address: "0xF977814e90dA44bFA03b6295A0616a897441aceC", name: "Binance: Optimism Hot Wallet", chain: "optimism", type: "hot", isMain: true }
2930
+ ]
2931
+ },
2932
+ {
2933
+ name: "Coinbase",
2934
+ wallets: [
2935
+ { address: "0x503828976d22510aad0201ac7ec88293211d23da", name: "Coinbase: Optimism", chain: "optimism", type: "hot", isMain: true }
2936
+ ]
2937
+ }
2938
+ ],
2939
+ polygon: [
2940
+ {
2941
+ name: "Binance",
2942
+ wallets: [
2943
+ { address: "0xF977814e90dA44bFA03b6295A0616a897441aceC", name: "Binance: Polygon Hot Wallet", chain: "polygon", type: "hot", isMain: true }
2944
+ ]
2945
+ }
2946
+ ],
2947
+ bsc: [
2948
+ {
2949
+ name: "Binance",
2950
+ wallets: [
2951
+ { address: "0xF977814e90dA44bFA03b6295A0616a897441aceC", name: "Binance: BSC Hot Wallet", chain: "bsc", type: "hot", isMain: true },
2952
+ { address: "0x0E09FaBB73Bd3ade0a17ECC321fD13a19e81cE82", name: "Binance: Cake", chain: "bsc", type: "hot", isMain: false }
2953
+ ]
2954
+ }
2955
+ ]
2956
+ };
2957
+ }
2958
+ });
2959
+
2960
+ // src/services/EntityService.ts
2961
+ var EntityService_exports = {};
2962
+ __export(EntityService_exports, {
2963
+ EntityService: () => EntityService
2964
+ });
2965
+ var CACHE_TTL2, BULK_CACHE_TTL, EntityService;
2966
+ var init_EntityService = __esm({
2967
+ "src/services/EntityService.ts"() {
2968
+ init_entities();
2969
+ init_cexWallets();
2970
+ init_cache();
2971
+ CACHE_TTL2 = 300;
2972
+ BULK_CACHE_TTL = 60;
2973
+ EntityService = class _EntityService {
2974
+ /**
2975
+ * Look up a single address on a specific chain
2976
+ */
2977
+ static lookupEntity(chain, address) {
2978
+ if (!address || !chain) return null;
2979
+ const cacheKey = `entity:${chain}:${address.toLowerCase()}`;
2980
+ const cached = cache.get(cacheKey);
2981
+ if (cached) return cached;
2982
+ const entity = getEntity(chain, address);
2983
+ if (entity) {
2984
+ cache.set(cacheKey, entity, CACHE_TTL2);
2985
+ return entity;
2986
+ }
2987
+ const cexInfo = getCEXInfo(address, chain);
2988
+ if (cexInfo) {
2989
+ const cexEntity = {
2990
+ address,
2991
+ name: cexInfo.cexName,
2992
+ category: "cex",
2993
+ chain,
2994
+ confidence: 0.9,
2995
+ source: "manual",
2996
+ verified: true,
2997
+ tags: ["cex", cexInfo.type]
2998
+ };
2999
+ cache.set(cacheKey, cexEntity, CACHE_TTL2);
3000
+ return cexEntity;
3001
+ }
3002
+ cache.set(cacheKey, null, CACHE_TTL2);
3003
+ return null;
3004
+ }
3005
+ /**
3006
+ * Look up multiple addresses in batch
3007
+ */
3008
+ static bulkLookup(chain, addresses) {
3009
+ if (!addresses.length) return {};
3010
+ const cacheKey = `entity:bulk:${chain}:${addresses.length}`;
3011
+ const cached = cache.get(cacheKey);
3012
+ if (cached) return cached;
3013
+ const results = {};
3014
+ for (const addr of addresses) {
3015
+ results[addr.toLowerCase()] = _EntityService.lookupEntity(chain, addr);
3016
+ }
3017
+ cache.set(cacheKey, results, BULK_CACHE_TTL);
3018
+ return results;
3019
+ }
3020
+ /**
3021
+ * Search entities by name
3022
+ */
3023
+ static search(query, chain, category) {
3024
+ if (!query || query.length < 2) return [];
3025
+ return searchEntities(query, chain, category);
3026
+ }
3027
+ /**
3028
+ * Cross-chain entity lookup (try all chains)
3029
+ */
3030
+ static findEntityAnyChain(address) {
3031
+ if (!address) return null;
3032
+ return findEntityByAddress(address) || null;
3033
+ }
3034
+ /**
3035
+ * Auto-detect entity type from transaction patterns (for unknown addresses)
3036
+ */
3037
+ static detectEntityType(address, txCount, uniqueSenders, uniqueRecipients, totalVolumeInEth, avgTxValueInEth) {
3038
+ const cexPattern = detectCEXPattern(
3039
+ txCount,
3040
+ uniqueSenders,
3041
+ uniqueRecipients,
3042
+ avgTxValueInEth,
3043
+ totalVolumeInEth
3044
+ );
3045
+ const signals = [];
3046
+ let entityType = "wallet";
3047
+ let score = 0;
3048
+ if (cexPattern.isCEX) {
3049
+ return {
3050
+ entityType: "cex",
3051
+ score: cexPattern.score,
3052
+ signals: cexPattern.signals
3053
+ };
3054
+ }
3055
+ if (txCount > 0 && uniqueSenders === 0 && uniqueRecipients === 0) {
3056
+ entityType = "contract";
3057
+ score = 0.5;
3058
+ signals.push("no_user_transactions");
3059
+ }
3060
+ if (txCount > 50 && uniqueRecipients < 5 && totalVolumeInEth > 100) {
3061
+ score = Math.max(score, 0.6);
3062
+ entityType = "protocol";
3063
+ signals.push("high_volume_few_recipients");
3064
+ }
3065
+ if (score < 0.3) return null;
3066
+ return { entityType, score: Math.round(score * 100), signals };
3067
+ }
3068
+ };
3069
+ }
3070
+ });
3071
+
3072
+ // src/mcp/handlers.ts
3073
+ var handlers_exports = {};
3074
+ __export(handlers_exports, {
3075
+ TOOL_HANDLERS: () => TOOL_HANDLERS
3076
+ });
3077
+ function ok(text) {
3078
+ return { content: [{ type: "text", text }] };
3079
+ }
3080
+ function err(message) {
3081
+ return { content: [{ type: "text", text: message }], isError: true };
3082
+ }
3083
+ function buildApiKeyConfig() {
3084
+ const alchemyKeys = process.env.ALCHEMY_API_KEYS?.split(",") || [];
3085
+ return {
3086
+ alchemy: process.env.DEFAULT_ALCHEMY_API_KEY || alchemyKeys[0] || "",
3087
+ moralis: process.env.MORALIS_API_KEY || "",
3088
+ etherscan: process.env.ETHERSCAN_API_KEY || process.env.DEFAULT_ETHERSCAN_API_KEY || "",
3089
+ lineascan: process.env.LINEASCAN_API_KEY || process.env.DEFAULT_ETHERSCAN_API_KEY || "",
3090
+ arbiscan: process.env.ARBISCAN_API_KEY || process.env.DEFAULT_ETHERSCAN_API_KEY || "",
3091
+ basescan: process.env.BASESCAN_API_KEY || process.env.DEFAULT_ETHERSCAN_API_KEY || "",
3092
+ optimism: process.env.OPTIMISM_API_KEY || process.env.DEFAULT_ETHERSCAN_API_KEY || "",
3093
+ polygonscan: process.env.POLYGONSCAN_API_KEY || process.env.DEFAULT_ETHERSCAN_API_KEY || ""
3094
+ };
3095
+ }
3096
+ function buildSybilConfig() {
3097
+ const alchemyKeys = process.env.ALCHEMY_API_KEYS?.split(",") || [];
3098
+ const defaultKey = process.env.DEFAULT_ALCHEMY_API_KEY || alchemyKeys[0] || "";
3099
+ return {
3100
+ defaultKey,
3101
+ moralisKey: process.env.MORALIS_API_KEY || "",
3102
+ contractKeys: [defaultKey, ...alchemyKeys].filter(Boolean),
3103
+ walletKeys: [defaultKey, ...alchemyKeys].filter(Boolean)
3104
+ };
3105
+ }
3106
+ function summarizeTree(node) {
3107
+ if (!node) return null;
3108
+ return {
3109
+ address: node.address,
3110
+ label: node.label,
3111
+ totalValueInEth: node.totalValueInEth,
3112
+ txCount: node.txCount,
3113
+ suspiciousScore: node.suspiciousScore,
3114
+ suspiciousReasons: node.suspiciousReasons,
3115
+ children: node.children?.map(summarizeTree) || []
3116
+ };
3117
+ }
3118
+ var analyzeWallet, traceFunds, compareWallets, analyzeContract, detectSybilClusters, getPortfolio, getTransactions, lookupEntity, getGasPrices, getTokenInfo, TOOL_HANDLERS;
3119
+ var init_handlers = __esm({
3120
+ "src/mcp/handlers.ts"() {
3121
+ analyzeWallet = async (args, ctx) => {
3122
+ const { address, chainId, transactionLimit } = args;
3123
+ try {
3124
+ const { WalletAnalyzer } = await import("fundtracer-core");
3125
+ const analyzer = new WalletAnalyzer(buildApiKeyConfig(), (progress) => {
3126
+ console.error(`[MCP] analyze_wallet ${address}: ${progress.stage} ${progress.current}/${progress.total}`);
3127
+ });
3128
+ const result = await analyzer.analyze(address, chainId, {
3129
+ transactionLimit: transactionLimit || 500
3130
+ });
3131
+ return ok(JSON.stringify({
3132
+ address: result.wallet.address,
3133
+ chain: chainId,
3134
+ balanceEth: result.wallet.balanceInEth,
3135
+ txCount: result.wallet.txCount,
3136
+ isContract: result.wallet.isContract,
3137
+ riskScore: result.overallRiskScore,
3138
+ riskLevel: result.riskLevel,
3139
+ suspiciousIndicators: result.suspiciousIndicators.map((i) => ({
3140
+ type: i.type,
3141
+ severity: i.severity,
3142
+ description: i.description,
3143
+ score: i.score
3144
+ })),
3145
+ topFundingSources: result.summary.topFundingSources.slice(0, 5),
3146
+ topFundingDestinations: result.summary.topFundingDestinations.slice(0, 5),
3147
+ projectsInteracted: result.projectsInteracted.slice(0, 10),
3148
+ activityPeriodDays: result.summary.activityPeriodDays,
3149
+ averageTxPerDay: result.summary.averageTxPerDay
3150
+ }, null, 2));
3151
+ } catch (error) {
3152
+ return err(`Wallet analysis failed: ${error.message}`);
3153
+ }
3154
+ };
3155
+ traceFunds = async (args, ctx) => {
3156
+ const { address, chainId, maxDepth = 3, direction = "both" } = args;
3157
+ try {
3158
+ const { WalletAnalyzer } = await import("fundtracer-core");
3159
+ const analyzer = new WalletAnalyzer(buildApiKeyConfig());
3160
+ const treeConfig = { maxDepth, direction };
3161
+ if (chainId === "solana") {
3162
+ const { SolanaFundingTreeService: SolanaFundingTreeService2 } = await Promise.resolve().then(() => (init_SolanaFundingTreeService(), SolanaFundingTreeService_exports));
3163
+ const heliusKey = process.env.HELIUS_KEY_1 || process.env.DEFAULT_ALCHEMY_API_KEY || "";
3164
+ const svc = new SolanaFundingTreeService2(heliusKey.startsWith("http") ? process.env.DEFAULT_ALCHEMY_API_KEY || "" : heliusKey);
3165
+ const tree2 = await svc.buildFundingTree(address, maxDepth);
3166
+ return ok(JSON.stringify(tree2, null, 2));
3167
+ }
3168
+ const tree = await analyzer.buildFundingTree(address, chainId, { treeConfig });
3169
+ return ok(JSON.stringify({
3170
+ sources: summarizeTree(tree.fundingSources),
3171
+ destinations: summarizeTree(tree.fundingDestinations)
3172
+ }, null, 2));
3173
+ } catch (error) {
3174
+ return err(`Fund tracing failed: ${error.message}`);
3175
+ }
3176
+ };
3177
+ compareWallets = async (args, ctx) => {
3178
+ const { addresses, chainId } = args;
3179
+ const addrList = addresses.split(",").map((a) => a.trim()).filter(Boolean);
3180
+ if (addrList.length < 2) return err("At least 2 addresses required");
3181
+ try {
3182
+ const { WalletAnalyzer } = await import("fundtracer-core");
3183
+ const analyzer = new WalletAnalyzer(buildApiKeyConfig());
3184
+ const result = await analyzer.compareWallets(addrList, chainId);
3185
+ return ok(JSON.stringify({
3186
+ wallets: addrList,
3187
+ chain: chainId,
3188
+ correlationScore: result.correlationScore,
3189
+ isSybilLikely: result.isSybilLikely,
3190
+ commonFundingSources: result.commonFundingSources,
3191
+ commonDestinations: result.commonDestinations,
3192
+ sharedProjects: result.sharedProjects,
3193
+ directTransfers: result.directTransfers.length
3194
+ }, null, 2));
3195
+ } catch (error) {
3196
+ return err(`Wallet comparison failed: ${error.message}`);
3197
+ }
3198
+ };
3199
+ analyzeContract = async (args, ctx) => {
3200
+ const { contractAddress, chainId, maxInteractors = 100 } = args;
3201
+ try {
3202
+ const { WalletAnalyzer } = await import("fundtracer-core");
3203
+ const analyzer = new WalletAnalyzer(buildApiKeyConfig());
3204
+ const result = await analyzer.analyzeContract(contractAddress, chainId, {
3205
+ maxInteractors
3206
+ });
3207
+ return ok(JSON.stringify({
3208
+ contractAddress,
3209
+ chain: chainId,
3210
+ totalInteractors: result.totalInteractors,
3211
+ riskScore: result.riskScore,
3212
+ sharedFundingGroups: result.sharedFundingGroups.slice(0, 20),
3213
+ suspiciousPatterns: result.suspiciousPatterns
3214
+ }, null, 2));
3215
+ } catch (error) {
3216
+ return err(`Contract analysis failed: ${error.message}`);
3217
+ }
3218
+ };
3219
+ detectSybilClusters = async (args, ctx) => {
3220
+ const { addresses, chainId } = args;
3221
+ const addrList = addresses.split(",").map((a) => a.trim()).filter(Boolean);
3222
+ if (addrList.length < 3) return err("At least 3 addresses required for cluster detection");
3223
+ try {
3224
+ const { SybilAnalyzer } = await import("fundtracer-core");
3225
+ const alchemyKey = process.env.DEFAULT_ALCHEMY_API_KEY || "";
3226
+ const sybilConfig = buildSybilConfig();
3227
+ const analyzer = new SybilAnalyzer(chainId, sybilConfig);
3228
+ const result = await analyzer.analyzeAddresses(addrList, { minClusterSize: 2 });
3229
+ return ok(JSON.stringify({
3230
+ chain: chainId,
3231
+ totalWallets: result.totalInteractors,
3232
+ uniqueFundingSources: result.uniqueFundingSources,
3233
+ flaggedClusters: result.flaggedClusters.map((c) => ({
3234
+ fundingSource: c.fundingSource,
3235
+ fundingSourceLabel: c.fundingSourceLabel,
3236
+ walletCount: c.totalWallets,
3237
+ sybilScore: c.sybilScore,
3238
+ flags: c.flags,
3239
+ timeSpanHours: c.timeSpan.durationHours
3240
+ })),
3241
+ summary: result.summary
3242
+ }, null, 2));
3243
+ } catch (error) {
3244
+ return err(`Sybil detection failed: ${error.message}`);
3245
+ }
3246
+ };
3247
+ getPortfolio = async (args, ctx) => {
3248
+ const { address, chainId } = args;
3249
+ try {
3250
+ if (chainId === "solana") {
3251
+ const { solanaPortfolioService: solanaPortfolioService2 } = await Promise.resolve().then(() => (init_SolanaPortfolioService(), SolanaPortfolioService_exports));
3252
+ const portfolio = await solanaPortfolioService2.getPortfolio(address);
3253
+ return ok(JSON.stringify(portfolio, null, 2));
3254
+ }
3255
+ return ok(JSON.stringify({
3256
+ address,
3257
+ chainId,
3258
+ note: "For EVM chain portfolio data, use the FundTracer REST API: GET /api/portfolio?address=" + address + "&chain=" + chainId
3259
+ }, null, 2));
3260
+ } catch (error) {
3261
+ return err(`Portfolio fetch failed: ${error.message}`);
3262
+ }
3263
+ };
3264
+ getTransactions = async (args, ctx) => {
3265
+ const { address, chainId, limit = 50 } = args;
3266
+ try {
3267
+ const { WalletAnalyzer } = await import("fundtracer-core");
3268
+ const analyzer = new WalletAnalyzer(buildApiKeyConfig());
3269
+ const result = await analyzer.analyze(address, chainId, {
3270
+ transactionLimit: limit,
3271
+ skipFundingTree: true
3272
+ });
3273
+ return ok(JSON.stringify({
3274
+ address,
3275
+ chainId,
3276
+ transactions: result.transactions.slice(0, limit).map((tx) => ({
3277
+ hash: tx.hash,
3278
+ blockNumber: tx.blockNumber,
3279
+ timestamp: tx.timestamp,
3280
+ from: tx.from,
3281
+ to: tx.to,
3282
+ value: tx.valueInEth,
3283
+ status: tx.status,
3284
+ category: tx.category,
3285
+ methodName: tx.methodName
3286
+ })),
3287
+ totalCount: result.transactions.length
3288
+ }, null, 2));
3289
+ } catch (error) {
3290
+ return err(`Transaction fetch failed: ${error.message}`);
3291
+ }
3292
+ };
3293
+ lookupEntity = async (args, ctx) => {
3294
+ const { query, chainId } = args;
3295
+ try {
3296
+ const { EntityService: EntityService2 } = await Promise.resolve().then(() => (init_EntityService(), EntityService_exports));
3297
+ const chain = chainId || "ethereum";
3298
+ if (/^0x[a-fA-F0-9]{40}$/.test(query) || /^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(query)) {
3299
+ const entity = EntityService2.lookupEntity(chain, query);
3300
+ if (entity) return ok(JSON.stringify(entity, null, 2));
3301
+ return ok(JSON.stringify({ address: query, label: "Unknown address", chain }, null, 2));
3302
+ }
3303
+ const { searchEntities: searchEntities2 } = await Promise.resolve().then(() => (init_entities(), entities_exports));
3304
+ const results = searchEntities2(query);
3305
+ return ok(JSON.stringify({ query, results: results.length > 0 ? results.slice(0, 20) : "No entities found" }, null, 2));
3306
+ } catch (error) {
3307
+ return err(`Entity lookup failed: ${error.message}`);
3308
+ }
3309
+ };
3310
+ getGasPrices = async (args, ctx) => {
3311
+ try {
3312
+ const { default: axios } = await import("axios");
3313
+ const alchemyKey = process.env.DEFAULT_ALCHEMY_API_KEY;
3314
+ if (!alchemyKey) return err("Alchemy API key not configured");
3315
+ const chains = {
3316
+ ethereum: `https://eth-mainnet.g.alchemy.com/v2/${alchemyKey}`,
3317
+ base: `https://base-mainnet.g.alchemy.com/v2/${alchemyKey}`,
3318
+ arbitrum: `https://arb-mainnet.g.alchemy.com/v2/${alchemyKey}`,
3319
+ optimism: `https://opt-mainnet.g.alchemy.com/v2/${alchemyKey}`,
3320
+ polygon: `https://polygon-mainnet.g.alchemy.com/v2/${alchemyKey}`
3321
+ };
3322
+ const results = {};
3323
+ for (const [chain, url] of Object.entries(chains)) {
3324
+ try {
3325
+ const res = await axios.post(url, {
3326
+ jsonrpc: "2.0",
3327
+ method: "eth_gasPrice",
3328
+ params: [],
3329
+ id: 1
3330
+ }, { timeout: 5e3 });
3331
+ const gasWei = parseInt(res.data.result, 16);
3332
+ results[chain] = {
3333
+ gasPriceGwei: (gasWei / 1e9).toFixed(2),
3334
+ gasPriceWei: gasWei
3335
+ };
3336
+ } catch {
3337
+ results[chain] = { error: "Unavailable" };
3338
+ }
3339
+ }
3340
+ return ok(JSON.stringify(results, null, 2));
3341
+ } catch (error) {
3342
+ return err(`Gas price fetch failed: ${error.message}`);
3343
+ }
3344
+ };
3345
+ getTokenInfo = async (args, ctx) => {
3346
+ const { tokenAddress, chainId } = args;
3347
+ try {
3348
+ const { default: axios } = await import("axios");
3349
+ const coingeckoUrl = "https://api.coingecko.com/api/v3";
3350
+ const platformMap = {
3351
+ ethereum: "ethereum",
3352
+ base: "base",
3353
+ arbitrum: "arbitrum-ethereum",
3354
+ optimism: "optimistic-ethereum",
3355
+ polygon: "polygon-pos"
3356
+ };
3357
+ const platform = platformMap[chainId];
3358
+ if (platform) {
3359
+ const res = await axios.get(`${coingeckoUrl}/coins/${platform}/contract/${tokenAddress}`, {
3360
+ timeout: 1e4,
3361
+ headers: { "Accept": "application/json" }
3362
+ });
3363
+ const d = res.data;
3364
+ return ok(JSON.stringify({
3365
+ name: d.name,
3366
+ symbol: d.symbol,
3367
+ marketCapRank: d.market_cap_rank,
3368
+ currentPrice: d.market_data?.current_price?.usd || null,
3369
+ marketCap: d.market_data?.market_cap?.usd || null,
3370
+ totalVolume: d.market_data?.total_volume?.usd || null,
3371
+ priceChange24h: d.market_data?.price_change_percentage_24h || null,
3372
+ description: d.description?.en?.substring(0, 500) || ""
3373
+ }, null, 2));
3374
+ }
3375
+ const dsRes = await axios.get(`https://api.dexscreener.com/latest/dex/tokens/${tokenAddress}`, {
3376
+ timeout: 5e3
3377
+ });
3378
+ return ok(JSON.stringify(dsRes.data?.pairs?.slice(0, 5) || { note: "No data found" }, null, 2));
3379
+ } catch (error) {
3380
+ return err(`Token info fetch failed: ${error.message}`);
3381
+ }
3382
+ };
3383
+ TOOL_HANDLERS = {
3384
+ analyze_wallet: analyzeWallet,
3385
+ trace_funds: traceFunds,
3386
+ compare_wallets: compareWallets,
3387
+ analyze_contract: analyzeContract,
3388
+ detect_sybil_clusters: detectSybilClusters,
3389
+ get_portfolio: getPortfolio,
3390
+ get_transactions: getTransactions,
3391
+ lookup_entity: lookupEntity,
3392
+ get_gas_prices: getGasPrices,
3393
+ get_token_info: getTokenInfo
3394
+ };
3395
+ }
3396
+ });
3397
+
3398
+ // src/models/apiKey.ts
3399
+ var apiKey_exports = {};
3400
+ __export(apiKey_exports, {
3401
+ API_SCOPES: () => API_SCOPES,
3402
+ TIER_DEFAULT_SCOPES: () => TIER_DEFAULT_SCOPES,
3403
+ TIER_LIMITS: () => TIER_LIMITS,
3404
+ checkRateLimit: () => checkRateLimit,
3405
+ createAPIKey: () => createAPIKey,
3406
+ deactivateAPIKey: () => deactivateAPIKey,
3407
+ generateAPIKey: () => generateAPIKey,
3408
+ generateMcpKey: () => generateMcpKey,
3409
+ getAPIKeyById: () => getAPIKeyById,
3410
+ getUserAPIKeys: () => getUserAPIKeys,
3411
+ hasScope: () => hasScope,
3412
+ hashAPIKey: () => hashAPIKey,
3413
+ incrementAPIKeyUsage: () => incrementAPIKeyUsage,
3414
+ resetDailyUsageIfNeeded: () => resetDailyUsageIfNeeded,
3415
+ updateAPIKeyTier: () => updateAPIKeyTier,
3416
+ validateAPIKey: () => validateAPIKey
3417
+ });
3418
+ import { FieldValue } from "firebase-admin/firestore";
3419
+ function generateAPIKey(type = "live") {
3420
+ const prefix = type === "live" ? "ft_live_" : type === "mcp" ? "ft_mcp_" : "ft_test_";
3421
+ const randomPart = generateSecureRandom(32);
3422
+ return `${prefix}${randomPart}`;
3423
+ }
3424
+ function generateMcpKey() {
3425
+ return generateAPIKey("mcp");
3426
+ }
3427
+ function generateSecureRandom(length) {
3428
+ const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
3429
+ const array = new Uint8Array(length);
3430
+ __require("crypto").randomFillSync(array);
3431
+ return Array.from(array, (byte) => chars[byte % chars.length]).join("");
3432
+ }
3433
+ function hashAPIKey(key) {
3434
+ return __require("crypto").createHash("sha256").update(key).digest("hex");
3435
+ }
3436
+ async function createAPIKey(userId, options) {
3437
+ const db = getFirestore();
3438
+ const keyCollection = db.collection("apiKeys");
3439
+ const tier = options.tier || "free";
3440
+ const limits = TIER_LIMITS[tier];
3441
+ const existingKeys = await keyCollection.where("userId", "==", userId).where("isActive", "==", true).get();
3442
+ const currentKeyCount = existingKeys.size;
3443
+ if (limits.maxKeys !== Infinity && currentKeyCount >= limits.maxKeys) {
3444
+ throw new Error(`Maximum API keys (${limits.maxKeys}) reached for ${tier} tier. Upgrade to create more keys.`);
3445
+ }
3446
+ const keyType = options.keyType || "live";
3447
+ const rawKey = generateAPIKey(keyType);
3448
+ const keyHash = hashAPIKey(rawKey);
3449
+ const keyPrefix = rawKey.split("_").slice(0, 2).join("_");
3450
+ const scopes = options.scopes || TIER_DEFAULT_SCOPES[tier];
3451
+ const now = Date.now();
3452
+ const dailyReset = getDailyResetTimestamp();
3453
+ const apiKey = {
3454
+ id: keyCollection.doc().id,
3455
+ userId,
3456
+ keyHash,
3457
+ keyPrefix,
3458
+ keyType,
3459
+ name: options.name,
3460
+ scopes,
3461
+ tier,
3462
+ dailyUsage: 0,
3463
+ dailyUsageReset: dailyReset,
3464
+ lastUsed: now,
3465
+ createdAt: now,
3466
+ isActive: true,
3467
+ expiresAt: options.expiresAt,
3468
+ testnetOnly: options.testnetOnly || false
3469
+ };
3470
+ await keyCollection.doc(apiKey.id).set(apiKey);
3471
+ return { key: apiKey, rawKey };
3472
+ }
3473
+ async function validateAPIKey(rawKey) {
3474
+ const db = getFirestore();
3475
+ const keyCollection = db.collection("apiKeys");
3476
+ const snapshot = await keyCollection.where("isActive", "==", true).get();
3477
+ const keyHash = hashAPIKey(rawKey);
3478
+ for (const doc of snapshot.docs) {
3479
+ const keyData = doc.data();
3480
+ if (keyData.keyHash === keyHash) {
3481
+ if (keyData.expiresAt && keyData.expiresAt < Date.now()) {
3482
+ return null;
3483
+ }
3484
+ return keyData;
3485
+ }
3486
+ }
3487
+ return null;
3488
+ }
3489
+ async function getAPIKeyById(keyId) {
3490
+ const db = getFirestore();
3491
+ const doc = await db.collection("apiKeys").doc(keyId).get();
3492
+ return doc.exists ? doc.data() : null;
3493
+ }
3494
+ async function getUserAPIKeys(userId) {
3495
+ const db = getFirestore();
3496
+ const snapshot = await db.collection("apiKeys").where("userId", "==", userId).orderBy("createdAt", "desc").get();
3497
+ return snapshot.docs.map((doc) => doc.data());
3498
+ }
3499
+ async function incrementAPIKeyUsage(userId, rawKey) {
3500
+ console.log(`[INCREMENT] Starting - userId: ${userId}, rawKey: ${rawKey.substring(0, 15)}...`);
3501
+ const db = getFirestore();
3502
+ const now = Date.now();
3503
+ try {
3504
+ const topLevelRef = db.collection("apiKeys").doc(rawKey);
3505
+ await topLevelRef.update({
3506
+ lastUsed: now,
3507
+ requests: FieldValue.increment(1)
3508
+ });
3509
+ console.log(`[INCREMENT] Updated top-level apiKeys/${rawKey.substring(0, 15)}...`);
3510
+ } catch (err2) {
3511
+ console.error(`[INCREMENT] Failed to update top-level apiKeys doc:`, err2);
3512
+ }
3513
+ try {
3514
+ const subSnapshot = await db.collection("users").doc(userId).collection("apiKeys").where("key", "==", rawKey).limit(1).get();
3515
+ if (!subSnapshot.empty) {
3516
+ const subDoc = subSnapshot.docs[0];
3517
+ await subDoc.ref.update({
3518
+ lastUsed: now,
3519
+ requests: FieldValue.increment(1)
3520
+ });
3521
+ console.log(`[INCREMENT] Updated subcollection doc ${subDoc.id} for key`);
3522
+ } else {
3523
+ console.warn(`[INCREMENT] No subcollection doc found for key ${rawKey.substring(0, 15)}...`);
3524
+ }
3525
+ } catch (err2) {
3526
+ console.error(`[INCREMENT] Failed to update subcollection doc:`, err2);
3527
+ }
3528
+ try {
3529
+ const topLevelDoc = await db.collection("apiKeys").doc(rawKey).get();
3530
+ if (topLevelDoc.exists) {
3531
+ const dailyReset = getDailyResetTimestamp();
3532
+ await topLevelDoc.ref.update({
3533
+ dailyUsage: FieldValue.increment(1),
3534
+ dailyUsageReset: dailyReset
3535
+ });
3536
+ }
3537
+ } catch (err2) {
3538
+ console.error(`[INCREMENT] Failed to update daily usage:`, err2);
3539
+ }
3540
+ console.log(`[INCREMENT] Usage tracking complete for key: ${rawKey.substring(0, 15)}...`);
3541
+ }
3542
+ async function resetDailyUsageIfNeeded(key) {
3543
+ if (key.dailyUsageReset < Date.now()) {
3544
+ const db = getFirestore();
3545
+ await db.collection("apiKeys").doc(key.id).update({
3546
+ dailyUsage: 0,
3547
+ dailyUsageReset: getDailyResetTimestamp()
3548
+ });
3549
+ }
3550
+ }
3551
+ async function checkRateLimit(key) {
3552
+ const limits = TIER_LIMITS[key.tier];
3553
+ const now = Date.now();
3554
+ await resetDailyUsageIfNeeded(key);
3555
+ return {
3556
+ limit: limits.daily,
3557
+ remaining: Math.max(0, limits.daily - key.dailyUsage),
3558
+ reset: key.dailyUsageReset,
3559
+ tier: key.tier
3560
+ };
3561
+ }
3562
+ async function deactivateAPIKey(keyId, userId) {
3563
+ const db = getFirestore();
3564
+ const keyRef = db.collection("apiKeys").doc(keyId);
3565
+ const doc = await keyRef.get();
3566
+ if (!doc.exists || doc.data()?.userId !== userId) {
3567
+ return false;
3568
+ }
3569
+ await keyRef.update({ isActive: false });
3570
+ return true;
3571
+ }
3572
+ function hasScope(key, requiredScope) {
3573
+ if (key.scopes.includes(API_SCOPES.ADMIN)) {
3574
+ return true;
3575
+ }
3576
+ return key.scopes.includes(requiredScope);
3577
+ }
3578
+ function getDailyResetTimestamp() {
3579
+ const now = /* @__PURE__ */ new Date();
3580
+ const tomorrow = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() + 1));
3581
+ return tomorrow.getTime();
3582
+ }
3583
+ async function updateAPIKeyTier(keyId, updates) {
3584
+ const db = getFirestore();
3585
+ await db.collection("apiKeys").doc(keyId).update({
3586
+ ...updates.tier && { tier: updates.tier },
3587
+ ...updates.scopes && { scopes: updates.scopes },
3588
+ ...updates.expiresAt !== void 0 && { expiresAt: updates.expiresAt }
3589
+ });
3590
+ }
3591
+ var API_SCOPES, TIER_LIMITS, TIER_DEFAULT_SCOPES;
3592
+ var init_apiKey = __esm({
3593
+ "src/models/apiKey.ts"() {
3594
+ init_firebase();
3595
+ API_SCOPES = {
3596
+ READ_ADDRESS: "read:address",
3597
+ READ_TRANSACTIONS: "read:transactions",
3598
+ READ_GRAPH: "read:graph",
3599
+ WRITE_ALERTS: "write:alerts",
3600
+ WRITE_WEBHOOKS: "write:webhooks",
3601
+ ADMIN: "admin",
3602
+ MCP: "mcp"
3603
+ };
3604
+ TIER_LIMITS = {
3605
+ free: {
3606
+ daily: 1e3,
3607
+ perMinute: 100,
3608
+ burst: 200,
3609
+ maxKeys: 2,
3610
+ endpoints: ["address", "transactions", "tokens", "risk"],
3611
+ features: ["basic_address_info", "transaction_history"]
3612
+ },
3613
+ pro: {
3614
+ daily: 1e4,
3615
+ perMinute: 200,
3616
+ burst: 400,
3617
+ maxKeys: 10,
3618
+ endpoints: ["address", "transactions", "tokens", "risk", "graph", "sources", "destinations", "analyze", "entities"],
3619
+ features: ["full_graph_analysis", "async_analysis", "entity_detection"]
3620
+ },
3621
+ enterprise: {
3622
+ daily: 1e5,
3623
+ perMinute: 300,
3624
+ burst: 1e3,
3625
+ maxKeys: Infinity,
3626
+ endpoints: ["*"],
3627
+ features: ["*", "webhooks", "alerts", "websocket", "priority_support"]
3628
+ }
3629
+ };
3630
+ TIER_DEFAULT_SCOPES = {
3631
+ free: [API_SCOPES.READ_ADDRESS, API_SCOPES.READ_TRANSACTIONS],
3632
+ pro: [
3633
+ API_SCOPES.READ_ADDRESS,
3634
+ API_SCOPES.READ_TRANSACTIONS,
3635
+ API_SCOPES.READ_GRAPH,
3636
+ API_SCOPES.WRITE_ALERTS
3637
+ ],
3638
+ enterprise: [API_SCOPES.ADMIN]
3639
+ };
3640
+ }
3641
+ });
3642
+
3643
+ // src/mcp/mcpAuth.ts
3644
+ var mcpAuth_exports = {};
3645
+ __export(mcpAuth_exports, {
3646
+ mcpApiKeyAuth: () => mcpApiKeyAuth,
3647
+ validateMcpApiKey: () => validateMcpApiKey
3648
+ });
3649
+ async function validateMcpApiKey(rawKey) {
3650
+ if (!rawKey.startsWith("ft_")) throw new Error("Invalid MCP API key format");
3651
+ let firestoreResult = null;
3652
+ try {
3653
+ firestoreResult = await validateWithFirestore(rawKey);
3654
+ if (firestoreResult) return firestoreResult;
3655
+ } catch {
3656
+ }
3657
+ return validateViaHttp(rawKey);
3658
+ }
3659
+ async function validateWithFirestore(rawKey) {
3660
+ const { getFirestore: getFirestore2 } = await Promise.resolve().then(() => (init_firebase(), firebase_exports));
3661
+ const db = getFirestore2();
3662
+ if (!db) return null;
3663
+ const keyDoc = await db.collection("apiKeys").doc(rawKey).get();
3664
+ if (keyDoc.exists) {
3665
+ const data = keyDoc.data();
3666
+ if (!data) throw new Error("Invalid MCP API key");
3667
+ if (data.expiresAt && data.expiresAt < Date.now()) throw new Error("MCP API key has expired");
3668
+ if (data.active === false) throw new Error("MCP API key has been revoked");
3669
+ const scopes = data.scopes || [];
3670
+ if (data.keyType !== "mcp" && !scopes.includes("mcp")) {
3671
+ throw new Error("This API key does not have MCP access");
3672
+ }
3673
+ trackUsage(data.userId, rawKey);
3674
+ return {
3675
+ userId: data.userId,
3676
+ tier: data.tier || "free",
3677
+ apiKeyPrefix: rawKey.substring(0, 15)
3678
+ };
3679
+ }
3680
+ const { hashAPIKey: hashAPIKey2 } = await Promise.resolve().then(() => (init_apiKey(), apiKey_exports));
3681
+ const keyHash = hashAPIKey2(rawKey);
3682
+ const snapshot = await db.collection("apiKeys").where("isActive", "==", true).get();
3683
+ for (const doc of snapshot.docs) {
3684
+ const data = doc.data();
3685
+ if (data.keyHash === keyHash) {
3686
+ if (data.expiresAt && data.expiresAt < Date.now()) throw new Error("MCP API key has expired");
3687
+ if (!data.isActive) throw new Error("MCP API key has been revoked");
3688
+ const scopes = data.scopes || [];
3689
+ if (data.keyType !== "mcp" && !scopes.includes("mcp")) {
3690
+ throw new Error("This API key does not have MCP access");
3691
+ }
3692
+ trackUsage(data.userId, rawKey);
3693
+ return {
3694
+ userId: data.userId,
3695
+ tier: data.tier || "free",
3696
+ apiKeyPrefix: rawKey.substring(0, 15)
3697
+ };
3698
+ }
3699
+ }
3700
+ return null;
3701
+ }
3702
+ async function validateViaHttp(rawKey) {
3703
+ const API_URL = process.env.FUNDTRACER_API_URL || "https://api.fundtracer.xyz";
3704
+ const { default: fetch5 } = await import("node-fetch");
3705
+ const res = await fetch5(`${API_URL}/api/user/mcp-validate`, {
3706
+ method: "POST",
3707
+ headers: {
3708
+ "Content-Type": "application/json",
3709
+ "Authorization": `Bearer ${rawKey}`
3710
+ },
3711
+ body: JSON.stringify({ key: rawKey })
3712
+ });
3713
+ if (!res.ok) {
3714
+ const body = await res.text();
3715
+ throw new Error(body ? JSON.parse(body).error || body : `Validation failed (HTTP ${res.status})`);
3716
+ }
3717
+ const data = await res.json();
3718
+ return {
3719
+ userId: data.userId,
3720
+ tier: data.tier || "free",
3721
+ apiKeyPrefix: rawKey.substring(0, 15)
3722
+ };
3723
+ }
3724
+ async function trackUsage(userId, rawKey) {
3725
+ try {
3726
+ const { incrementAPIKeyUsage: incrementAPIKeyUsage2 } = await Promise.resolve().then(() => (init_apiKey(), apiKey_exports));
3727
+ await incrementAPIKeyUsage2(userId, rawKey);
3728
+ } catch {
3729
+ }
3730
+ }
3731
+ async function mcpApiKeyAuth(req, res, next) {
3732
+ const authHeader = req.headers.authorization;
3733
+ if (!authHeader || !authHeader.startsWith("Bearer ")) {
3734
+ return res.status(401).json({ error: "MCP API key required (Authorization: Bearer ft_mcp_<key>)" });
3735
+ }
3736
+ const rawKey = authHeader.slice(7).trim();
3737
+ if (!rawKey.startsWith("ft_")) {
3738
+ return res.status(401).json({ error: "Invalid MCP API key format" });
3739
+ }
3740
+ try {
3741
+ const ctx = await validateMcpApiKey(rawKey);
3742
+ req.mcpContext = ctx;
3743
+ next();
3744
+ } catch (err2) {
3745
+ return res.status(401).json({ error: err2.message });
3746
+ }
3747
+ }
3748
+ var init_mcpAuth = __esm({
3749
+ "src/mcp/mcpAuth.ts"() {
3750
+ }
3751
+ });
3752
+
3753
+ // src/mcp/stdio.ts
3754
+ import * as dotenv from "dotenv";
3755
+ import { McpServer, StdioServerTransport, fromJsonSchema } from "@modelcontextprotocol/server";
3756
+ dotenv.config();
3757
+ async function main() {
3758
+ let firebaseAvailable = false;
3759
+ try {
3760
+ const { initializeFirebase: initializeFirebase2 } = await Promise.resolve().then(() => (init_firebase(), firebase_exports));
3761
+ initializeFirebase2();
3762
+ firebaseAvailable = true;
3763
+ console.error("[MCP] Firebase initialized");
3764
+ } catch (err2) {
3765
+ console.error("[MCP] Firebase not available \u2014 key validation will fail. Set Firebase credentials in env.");
3766
+ }
3767
+ const { ALL_MCP_TOOLS: ALL_MCP_TOOLS2 } = await Promise.resolve().then(() => (init_tools(), tools_exports));
3768
+ const { TOOL_HANDLERS: TOOL_HANDLERS2 } = await Promise.resolve().then(() => (init_handlers(), handlers_exports));
3769
+ const { validateMcpApiKey: validateMcpApiKey2 } = await Promise.resolve().then(() => (init_mcpAuth(), mcpAuth_exports));
3770
+ const server = new McpServer({
3771
+ name: "FundTracer MCP",
3772
+ version: "1.0.0"
3773
+ });
3774
+ for (const toolDef of ALL_MCP_TOOLS2) {
3775
+ const handler = TOOL_HANDLERS2[toolDef.name];
3776
+ if (!handler) {
3777
+ console.error(`[MCP] No handler for tool: ${toolDef.name}`);
3778
+ continue;
3779
+ }
3780
+ server.registerTool(toolDef.name, {
3781
+ description: toolDef.description,
3782
+ inputSchema: fromJsonSchema(toolDef.inputSchema)
3783
+ }, async (args) => {
3784
+ const apiKey = process.env.FUNDTRACER_MCP_API_KEY;
3785
+ if (!apiKey) {
3786
+ return {
3787
+ content: [{ type: "text", text: "FUNDTRACER_MCP_API_KEY environment variable not set" }],
3788
+ isError: true
3789
+ };
3790
+ }
3791
+ let ctx;
3792
+ try {
3793
+ ctx = await validateMcpApiKey2(apiKey);
3794
+ } catch (err2) {
3795
+ return {
3796
+ content: [{ type: "text", text: `Authentication failed: ${err2.message}` }],
3797
+ isError: true
3798
+ };
3799
+ }
3800
+ return handler(args, ctx);
3801
+ });
3802
+ console.error(`[MCP] Registered tool: ${toolDef.name}`);
3803
+ }
3804
+ const transport = new StdioServerTransport();
3805
+ await server.connect(transport);
3806
+ console.error("[MCP] FundTracer MCP server running on stdio");
3807
+ }
3808
+ main().catch((err2) => {
3809
+ console.error("[MCP] Fatal error:", err2);
3810
+ process.exit(1);
3811
+ });
3812
+ process.on("SIGINT", async () => {
3813
+ console.error("[MCP] Shutting down...");
3814
+ const { McpServer: McpServer2 } = await import("@modelcontextprotocol/server");
3815
+ process.exit(0);
3816
+ });
3817
+ process.on("SIGTERM", async () => {
3818
+ console.error("[MCP] Shutting down...");
3819
+ process.exit(0);
3820
+ });