@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.
- package/fundtracer-mcp.js +3820 -0
- 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
|
+
});
|