@fundtracer/mcp 1.0.12 → 1.0.13
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 +1704 -193
- package/package.json +2 -2
package/fundtracer-mcp.js
CHANGED
|
@@ -133,6 +133,70 @@ var ALL_MCP_TOOLS = [
|
|
|
133
133
|
required: ["address", "chainId"]
|
|
134
134
|
}
|
|
135
135
|
},
|
|
136
|
+
{
|
|
137
|
+
name: "get_portfolio_tokens",
|
|
138
|
+
description: "Get only the token holdings for a wallet address.",
|
|
139
|
+
inputSchema: {
|
|
140
|
+
type: "object",
|
|
141
|
+
properties: {
|
|
142
|
+
address: { type: "string", description: "Wallet address" },
|
|
143
|
+
chainId: {
|
|
144
|
+
type: "string",
|
|
145
|
+
description: "Blockchain",
|
|
146
|
+
enum: ["ethereum", "solana", "base", "arbitrum", "optimism", "polygon", "linea"]
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
required: ["address", "chainId"]
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
name: "get_portfolio_nfts",
|
|
154
|
+
description: "Get only the NFT holdings for a wallet address.",
|
|
155
|
+
inputSchema: {
|
|
156
|
+
type: "object",
|
|
157
|
+
properties: {
|
|
158
|
+
address: { type: "string", description: "Wallet address" },
|
|
159
|
+
chainId: {
|
|
160
|
+
type: "string",
|
|
161
|
+
description: "Blockchain",
|
|
162
|
+
enum: ["ethereum", "solana", "base", "arbitrum", "optimism", "polygon", "linea"]
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
required: ["address", "chainId"]
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
name: "get_portfolio_activity",
|
|
170
|
+
description: "Get portfolio activity summary for a wallet address.",
|
|
171
|
+
inputSchema: {
|
|
172
|
+
type: "object",
|
|
173
|
+
properties: {
|
|
174
|
+
address: { type: "string", description: "Wallet address" },
|
|
175
|
+
chainId: {
|
|
176
|
+
type: "string",
|
|
177
|
+
description: "Blockchain",
|
|
178
|
+
enum: ["ethereum", "solana", "base", "arbitrum", "optimism", "polygon", "linea"]
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
required: ["address", "chainId"]
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
name: "get_portfolio_stablecoins",
|
|
186
|
+
description: "Get stablecoin balances for a wallet address.",
|
|
187
|
+
inputSchema: {
|
|
188
|
+
type: "object",
|
|
189
|
+
properties: {
|
|
190
|
+
address: { type: "string", description: "Wallet address" },
|
|
191
|
+
chainId: {
|
|
192
|
+
type: "string",
|
|
193
|
+
description: "Blockchain",
|
|
194
|
+
enum: ["ethereum", "solana", "base", "arbitrum", "optimism", "polygon", "linea"]
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
required: ["address", "chainId"]
|
|
198
|
+
}
|
|
199
|
+
},
|
|
136
200
|
{
|
|
137
201
|
name: "get_transactions",
|
|
138
202
|
description: "Get recent transaction history for a wallet address.",
|
|
@@ -202,210 +266,1597 @@ var ALL_MCP_TOOLS = [
|
|
|
202
266
|
},
|
|
203
267
|
required: ["tokenAddress", "chainId"]
|
|
204
268
|
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
return;
|
|
216
|
-
}
|
|
217
|
-
try {
|
|
218
|
-
const { getFirestore } = await import("../firebase.js");
|
|
219
|
-
const db = getFirestore();
|
|
220
|
-
if (!db) {
|
|
221
|
-
console.warn("[MCP-LOGGER] getFirestore() returned null");
|
|
222
|
-
return;
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
name: "resolve_name",
|
|
272
|
+
description: "Resolve an ENS, Basename, or Linea name to an address.",
|
|
273
|
+
inputSchema: {
|
|
274
|
+
type: "object",
|
|
275
|
+
properties: {
|
|
276
|
+
input: { type: "string", description: "ENS or address-like input to resolve" }
|
|
277
|
+
},
|
|
278
|
+
required: ["input"]
|
|
223
279
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
name: "get_transaction_detail",
|
|
283
|
+
description: "Fetch full transaction details, logs, gas costs, and decoded events.",
|
|
284
|
+
inputSchema: {
|
|
285
|
+
type: "object",
|
|
286
|
+
properties: {
|
|
287
|
+
chainId: { type: "string", description: "Blockchain to inspect", enum: ["ethereum", "base", "arbitrum", "optimism", "polygon", "linea", "bsc"] },
|
|
288
|
+
hash: { type: "string", description: "Transaction hash" }
|
|
289
|
+
},
|
|
290
|
+
required: ["chainId", "hash"]
|
|
230
291
|
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
const key = _mcpCtx?.apiKey || process.env.FUNDTRACER_MCP_API_KEY || "";
|
|
245
|
-
const headers = {
|
|
246
|
-
Authorization: `Bearer ${key}`,
|
|
247
|
-
"Content-Type": "application/json"
|
|
248
|
-
};
|
|
249
|
-
if (_mcpCtx?.userId) {
|
|
250
|
-
headers["X-MCP-UserId"] = _mcpCtx.userId;
|
|
251
|
-
headers["x-auth-token"] = `${key}:${_mcpCtx.userId}`;
|
|
252
|
-
}
|
|
253
|
-
return axios.create({
|
|
254
|
-
baseURL: API_BASE,
|
|
255
|
-
timeout: 6e4,
|
|
256
|
-
headers
|
|
257
|
-
});
|
|
258
|
-
}
|
|
259
|
-
var analyzeWallet = async (args, ctx) => {
|
|
260
|
-
const { address, chainId, transactionLimit } = args;
|
|
261
|
-
try {
|
|
262
|
-
const res = await api().post("/api/analyze/wallet", {
|
|
263
|
-
address,
|
|
264
|
-
chain: chainId,
|
|
265
|
-
options: { limit: transactionLimit || 500 }
|
|
266
|
-
});
|
|
267
|
-
return ok(JSON.stringify(res.data, null, 2));
|
|
268
|
-
} catch (error) {
|
|
269
|
-
const msg = error.response?.data?.error || error.message;
|
|
270
|
-
return err(`Wallet analysis failed: ${msg}`);
|
|
271
|
-
}
|
|
272
|
-
};
|
|
273
|
-
var traceFunds = async (args, ctx) => {
|
|
274
|
-
const { address, chainId, maxDepth = 3, direction = "both" } = args;
|
|
275
|
-
try {
|
|
276
|
-
const res = await api().post("/api/analyze/funding-tree", {
|
|
277
|
-
address,
|
|
278
|
-
chain: chainId,
|
|
279
|
-
maxDepth,
|
|
280
|
-
direction
|
|
281
|
-
});
|
|
282
|
-
return ok(JSON.stringify(res.data, null, 2));
|
|
283
|
-
} catch (error) {
|
|
284
|
-
const msg = error.response?.data?.error || error.message;
|
|
285
|
-
return err(`Fund tracing failed: ${msg}`);
|
|
286
|
-
}
|
|
287
|
-
};
|
|
288
|
-
var compareWallets = async (args, ctx) => {
|
|
289
|
-
const { addresses, chainId } = args;
|
|
290
|
-
const addrList = addresses.split(",").map((a) => a.trim()).filter(Boolean);
|
|
291
|
-
if (addrList.length < 2) return err("At least 2 addresses required");
|
|
292
|
-
try {
|
|
293
|
-
const res = await api().post("/api/analyze/compare", {
|
|
294
|
-
addresses: addrList,
|
|
295
|
-
chain: chainId
|
|
296
|
-
});
|
|
297
|
-
return ok(JSON.stringify(res.data, null, 2));
|
|
298
|
-
} catch (error) {
|
|
299
|
-
const msg = error.response?.data?.error || error.message;
|
|
300
|
-
return err(`Wallet comparison failed: ${msg}`);
|
|
301
|
-
}
|
|
302
|
-
};
|
|
303
|
-
var analyzeContract = async (args, ctx) => {
|
|
304
|
-
const { contractAddress, chainId, maxInteractors = 100 } = args;
|
|
305
|
-
try {
|
|
306
|
-
const res = await api().post("/api/analyze/contract", {
|
|
307
|
-
contractAddress,
|
|
308
|
-
chain: chainId,
|
|
309
|
-
maxInteractors
|
|
310
|
-
});
|
|
311
|
-
return ok(JSON.stringify(res.data, null, 2));
|
|
312
|
-
} catch (error) {
|
|
313
|
-
const msg = error.response?.data?.error || error.message;
|
|
314
|
-
return err(`Contract analysis failed: ${msg}`);
|
|
315
|
-
}
|
|
316
|
-
};
|
|
317
|
-
var detectSybilClusters = async (args, ctx) => {
|
|
318
|
-
const { addresses, chainId } = args;
|
|
319
|
-
const addrList = addresses.split(",").map((a) => a.trim()).filter(Boolean);
|
|
320
|
-
if (addrList.length < 3) return err("At least 3 addresses required for cluster detection");
|
|
321
|
-
try {
|
|
322
|
-
const res = await api().post("/api/analyze/sybil", {
|
|
323
|
-
addresses: addrList,
|
|
324
|
-
chain: chainId
|
|
325
|
-
});
|
|
326
|
-
return ok(JSON.stringify(res.data, null, 2));
|
|
327
|
-
} catch (error) {
|
|
328
|
-
const msg = error.response?.data?.error || error.message;
|
|
329
|
-
return err(`Sybil detection failed: ${msg}`);
|
|
330
|
-
}
|
|
331
|
-
};
|
|
332
|
-
var getPortfolio = async (args, ctx) => {
|
|
333
|
-
const { address, chainId } = args;
|
|
334
|
-
try {
|
|
335
|
-
const res = await api().get(`/api/portfolio/${address}`, {
|
|
336
|
-
params: { chain: chainId }
|
|
337
|
-
});
|
|
338
|
-
return ok(JSON.stringify(res.data, null, 2));
|
|
339
|
-
} catch (error) {
|
|
340
|
-
const msg = error.response?.data?.error || error.message;
|
|
341
|
-
return err(`Portfolio fetch failed: ${msg}`);
|
|
342
|
-
}
|
|
343
|
-
};
|
|
344
|
-
var getTransactions = async (args, ctx) => {
|
|
345
|
-
const { address, chainId, limit = 50 } = args;
|
|
346
|
-
try {
|
|
347
|
-
const res = await api().post("/api/history", {
|
|
348
|
-
wallet: address,
|
|
349
|
-
blockchain: chainId,
|
|
350
|
-
pageToken: null,
|
|
351
|
-
filters: {}
|
|
352
|
-
});
|
|
353
|
-
const txs = (res.data.transactions || []).slice(0, limit);
|
|
354
|
-
return ok(JSON.stringify({
|
|
355
|
-
address,
|
|
356
|
-
chainId,
|
|
357
|
-
transactions: txs,
|
|
358
|
-
totalCount: res.data.transactions?.length || 0
|
|
359
|
-
}, null, 2));
|
|
360
|
-
} catch (error) {
|
|
361
|
-
const msg = error.response?.data?.error || error.message;
|
|
362
|
-
return err(`Transaction fetch failed: ${msg}`);
|
|
363
|
-
}
|
|
364
|
-
};
|
|
365
|
-
var lookupEntity = async (args, ctx) => {
|
|
366
|
-
const { query, chainId } = args;
|
|
367
|
-
const chain = chainId || "ethereum";
|
|
368
|
-
try {
|
|
369
|
-
if (/^0x[a-fA-F0-9]{40}$/.test(query) || /^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(query)) {
|
|
370
|
-
const res2 = await api().get(`/api/entities/${query}`, {
|
|
371
|
-
params: { chain }
|
|
372
|
-
});
|
|
373
|
-
return ok(JSON.stringify(res2.data, null, 2));
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
name: "get_transaction_history",
|
|
295
|
+
description: "Fetch transaction history for a wallet with optional pagination and filters.",
|
|
296
|
+
inputSchema: {
|
|
297
|
+
type: "object",
|
|
298
|
+
properties: {
|
|
299
|
+
address: { type: "string", description: "Wallet address to inspect" },
|
|
300
|
+
chainId: { type: "string", description: "Blockchain", enum: ["ethereum", "base", "arbitrum", "optimism", "polygon", "linea", "bsc", "solana"] },
|
|
301
|
+
pageToken: { type: "string", description: "Pagination token returned by the history endpoint" },
|
|
302
|
+
filters: { type: "object", description: "Optional history filters", default: {} }
|
|
303
|
+
},
|
|
304
|
+
required: ["address", "chainId"]
|
|
374
305
|
}
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
name: "analyze_cex_flow",
|
|
309
|
+
description: "Analyze exchange inflows/outflows and connected wallets for a target wallet.",
|
|
310
|
+
inputSchema: {
|
|
311
|
+
type: "object",
|
|
312
|
+
properties: {
|
|
313
|
+
walletAddress: { type: "string", description: "Wallet address to analyze" },
|
|
314
|
+
chainId: { type: "string", description: "Blockchain", enum: ["ethereum", "base", "arbitrum", "optimism", "polygon", "linea", "bsc"] },
|
|
315
|
+
cexName: { type: "string", description: "Optional exchange name filter" },
|
|
316
|
+
depth: { type: "number", description: "Traversal depth", default: 3 }
|
|
317
|
+
},
|
|
318
|
+
required: ["walletAddress", "chainId"]
|
|
382
319
|
}
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
name: "analyze_sybil_addresses",
|
|
323
|
+
description: "Analyze a list of wallet addresses for Sybil or coordination patterns.",
|
|
324
|
+
inputSchema: {
|
|
325
|
+
type: "object",
|
|
326
|
+
properties: {
|
|
327
|
+
addresses: { type: "string", description: "Comma-separated wallet addresses" },
|
|
328
|
+
chainId: { type: "string", description: "Blockchain", enum: ["ethereum", "base", "arbitrum", "optimism", "polygon", "linea", "bsc"] },
|
|
329
|
+
txHash: { type: "string", description: "Optional transaction hash for context" }
|
|
330
|
+
},
|
|
331
|
+
required: ["addresses", "chainId"]
|
|
332
|
+
}
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
name: "search_contracts",
|
|
336
|
+
description: "Search the contract database by name or symbol.",
|
|
337
|
+
inputSchema: {
|
|
338
|
+
type: "object",
|
|
339
|
+
properties: {
|
|
340
|
+
query: { type: "string", description: "Search term" }
|
|
341
|
+
},
|
|
342
|
+
required: ["query"]
|
|
343
|
+
}
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
name: "lookup_contract",
|
|
347
|
+
description: "Look up contract metadata and labels by address.",
|
|
348
|
+
inputSchema: {
|
|
349
|
+
type: "object",
|
|
350
|
+
properties: {
|
|
351
|
+
address: { type: "string", description: "Contract address to look up" }
|
|
352
|
+
},
|
|
353
|
+
required: ["address"]
|
|
354
|
+
}
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
name: "batch_lookup_contracts",
|
|
358
|
+
description: "Look up multiple contract addresses at once.",
|
|
359
|
+
inputSchema: {
|
|
360
|
+
type: "object",
|
|
361
|
+
properties: {
|
|
362
|
+
addresses: { type: "string", description: "Comma-separated list of contract addresses" }
|
|
363
|
+
},
|
|
364
|
+
required: ["addresses"]
|
|
365
|
+
}
|
|
366
|
+
},
|
|
367
|
+
{
|
|
368
|
+
name: "get_contract_stats",
|
|
369
|
+
description: "Get contract database statistics.",
|
|
370
|
+
inputSchema: {
|
|
371
|
+
type: "object",
|
|
372
|
+
properties: {}
|
|
373
|
+
}
|
|
374
|
+
},
|
|
375
|
+
{
|
|
376
|
+
name: "refresh_contracts",
|
|
377
|
+
description: "Trigger a contract database refresh.",
|
|
378
|
+
inputSchema: {
|
|
379
|
+
type: "object",
|
|
380
|
+
properties: {}
|
|
381
|
+
}
|
|
382
|
+
},
|
|
383
|
+
{
|
|
384
|
+
name: "search_tokens",
|
|
385
|
+
description: "Search token metadata by keyword.",
|
|
386
|
+
inputSchema: {
|
|
387
|
+
type: "object",
|
|
388
|
+
properties: {
|
|
389
|
+
query: { type: "string", description: "Token search query" }
|
|
390
|
+
},
|
|
391
|
+
required: ["query"]
|
|
392
|
+
}
|
|
393
|
+
},
|
|
394
|
+
{
|
|
395
|
+
name: "get_market_stats",
|
|
396
|
+
description: "Fetch market overview statistics.",
|
|
397
|
+
inputSchema: {
|
|
398
|
+
type: "object",
|
|
399
|
+
properties: {}
|
|
400
|
+
}
|
|
401
|
+
},
|
|
402
|
+
{
|
|
403
|
+
name: "get_market_coins",
|
|
404
|
+
description: "Get top market coins with optional chain filtering.",
|
|
405
|
+
inputSchema: {
|
|
406
|
+
type: "object",
|
|
407
|
+
properties: {
|
|
408
|
+
chainId: { type: "string", description: "Optional chain filter", enum: ["ethereum", "linea", "arbitrum", "optimism", "base", "polygon", "bsc", "all"], default: "all" },
|
|
409
|
+
page: { type: "number", description: "Page number", default: 1 },
|
|
410
|
+
perPage: { type: "number", description: "Results per page", default: 100 }
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
},
|
|
414
|
+
{
|
|
415
|
+
name: "get_token_chart",
|
|
416
|
+
description: "Fetch token chart/price history from the token endpoint.",
|
|
417
|
+
inputSchema: {
|
|
418
|
+
type: "object",
|
|
419
|
+
properties: {
|
|
420
|
+
tokenAddress: { type: "string", description: "Token address" },
|
|
421
|
+
chainId: { type: "string", description: "Blockchain", enum: ["ethereum", "base", "arbitrum", "optimism", "polygon", "linea", "bsc"] },
|
|
422
|
+
coinId: { type: "string", description: "CoinGecko coin ID" },
|
|
423
|
+
days: { type: "number", description: "Number of days of history", default: 7 }
|
|
424
|
+
},
|
|
425
|
+
required: ["tokenAddress", "chainId", "coinId"]
|
|
426
|
+
}
|
|
427
|
+
},
|
|
428
|
+
{
|
|
429
|
+
name: "get_dexscreener_latest_profiles",
|
|
430
|
+
description: "Get the latest DEX Screener token profiles.",
|
|
431
|
+
inputSchema: { type: "object", properties: {} }
|
|
432
|
+
},
|
|
433
|
+
{
|
|
434
|
+
name: "get_dexscreener_top_boosts",
|
|
435
|
+
description: "Get top boosted tokens from DEX Screener.",
|
|
436
|
+
inputSchema: { type: "object", properties: {} }
|
|
437
|
+
},
|
|
438
|
+
{
|
|
439
|
+
name: "search_dexscreener_pairs",
|
|
440
|
+
description: "Search DEX Screener pairs by query.",
|
|
441
|
+
inputSchema: {
|
|
442
|
+
type: "object",
|
|
443
|
+
properties: { query: { type: "string", description: "Search term" } },
|
|
444
|
+
required: ["query"]
|
|
445
|
+
}
|
|
446
|
+
},
|
|
447
|
+
{
|
|
448
|
+
name: "get_dexscreener_token_details",
|
|
449
|
+
description: "Get detailed DEX Screener information for a token.",
|
|
450
|
+
inputSchema: {
|
|
451
|
+
type: "object",
|
|
452
|
+
properties: {
|
|
453
|
+
chainId: { type: "string", description: "Chain ID", enum: ["ethereum", "linea", "arbitrum", "optimism", "base", "polygon", "bsc"] },
|
|
454
|
+
tokenAddress: { type: "string", description: "Token address" }
|
|
455
|
+
},
|
|
456
|
+
required: ["chainId", "tokenAddress"]
|
|
457
|
+
}
|
|
458
|
+
},
|
|
459
|
+
{
|
|
460
|
+
name: "get_dexscreener_token_pairs",
|
|
461
|
+
description: "Get DEX Screener pairs for a token.",
|
|
462
|
+
inputSchema: {
|
|
463
|
+
type: "object",
|
|
464
|
+
properties: {
|
|
465
|
+
chainId: { type: "string", description: "Chain ID", enum: ["ethereum", "linea", "arbitrum", "optimism", "base", "polygon", "bsc"] },
|
|
466
|
+
tokenAddress: { type: "string", description: "Token address" }
|
|
467
|
+
},
|
|
468
|
+
required: ["chainId", "tokenAddress"]
|
|
469
|
+
}
|
|
470
|
+
},
|
|
471
|
+
{
|
|
472
|
+
name: "get_dexscreener_trending",
|
|
473
|
+
description: "Get trending DEX Screener tokens.",
|
|
474
|
+
inputSchema: { type: "object", properties: {} }
|
|
475
|
+
},
|
|
476
|
+
{
|
|
477
|
+
name: "get_scan_history",
|
|
478
|
+
description: "Get the authenticated user scan history.",
|
|
479
|
+
inputSchema: { type: "object", properties: {} }
|
|
480
|
+
},
|
|
481
|
+
{
|
|
482
|
+
name: "get_scan_history_stats",
|
|
483
|
+
description: "Get aggregate scan history stats.",
|
|
484
|
+
inputSchema: { type: "object", properties: {} }
|
|
485
|
+
},
|
|
486
|
+
{
|
|
487
|
+
name: "save_scan_history_item",
|
|
488
|
+
description: "Save a scan history item for the authenticated user.",
|
|
489
|
+
inputSchema: {
|
|
490
|
+
type: "object",
|
|
491
|
+
properties: {
|
|
492
|
+
address: { type: "string", description: "Address being saved" },
|
|
493
|
+
chain: { type: "string", description: "Chain name", default: "ethereum" },
|
|
494
|
+
label: { type: "string", description: "Optional label" },
|
|
495
|
+
type: { type: "string", description: "Item type", enum: ["wallet", "contract", "compare", "sybil"], default: "wallet" },
|
|
496
|
+
timestamp: { type: "number", description: "Unix timestamp", default: 0 },
|
|
497
|
+
riskScore: { type: "number", description: "Optional risk score" },
|
|
498
|
+
riskLevel: { type: "string", description: "Optional risk level" },
|
|
499
|
+
totalTransactions: { type: "number", description: "Optional transaction count" },
|
|
500
|
+
totalValueSentEth: { type: "number", description: "Optional sent value" },
|
|
501
|
+
totalValueReceivedEth: { type: "number", description: "Optional received value" },
|
|
502
|
+
activityPeriodDays: { type: "number", description: "Optional activity period" },
|
|
503
|
+
balanceInEth: { type: "number", description: "Optional balance" }
|
|
504
|
+
},
|
|
505
|
+
required: ["address"]
|
|
506
|
+
}
|
|
507
|
+
},
|
|
508
|
+
{
|
|
509
|
+
name: "sync_scan_history",
|
|
510
|
+
description: "Bulk sync scan history items.",
|
|
511
|
+
inputSchema: {
|
|
512
|
+
type: "object",
|
|
513
|
+
properties: {
|
|
514
|
+
items: { type: "array", description: "History items to sync" }
|
|
515
|
+
},
|
|
516
|
+
required: ["items"]
|
|
517
|
+
}
|
|
518
|
+
},
|
|
519
|
+
{
|
|
520
|
+
name: "delete_scan_history_item",
|
|
521
|
+
description: "Delete a scan history item by address.",
|
|
522
|
+
inputSchema: {
|
|
523
|
+
type: "object",
|
|
524
|
+
properties: {
|
|
525
|
+
address: { type: "string", description: "Address to delete" }
|
|
526
|
+
},
|
|
527
|
+
required: ["address"]
|
|
528
|
+
}
|
|
529
|
+
},
|
|
530
|
+
{
|
|
531
|
+
name: "clear_scan_history",
|
|
532
|
+
description: "Clear all scan history for the authenticated user.",
|
|
533
|
+
inputSchema: { type: "object", properties: {} }
|
|
534
|
+
},
|
|
535
|
+
{
|
|
536
|
+
name: "list_rooms",
|
|
537
|
+
description: "List investigation rooms the user belongs to.",
|
|
538
|
+
inputSchema: { type: "object", properties: {} }
|
|
539
|
+
},
|
|
540
|
+
{
|
|
541
|
+
name: "get_room_details",
|
|
542
|
+
description: "Get room details and members.",
|
|
543
|
+
inputSchema: {
|
|
544
|
+
type: "object",
|
|
545
|
+
properties: { roomId: { type: "string", description: "Room ID" } },
|
|
546
|
+
required: ["roomId"]
|
|
547
|
+
}
|
|
548
|
+
},
|
|
549
|
+
{
|
|
550
|
+
name: "create_room",
|
|
551
|
+
description: "Create a new investigation room.",
|
|
552
|
+
inputSchema: {
|
|
553
|
+
type: "object",
|
|
554
|
+
properties: {
|
|
555
|
+
name: { type: "string", description: "Room name" },
|
|
556
|
+
description: { type: "string", description: "Room description" },
|
|
557
|
+
seedAddress: { type: "string", description: "Seed wallet or contract address" },
|
|
558
|
+
seedChain: { type: "string", description: "Seed chain" },
|
|
559
|
+
seedSnapshot: { type: "object", description: "Optional seed snapshot" }
|
|
560
|
+
},
|
|
561
|
+
required: ["name"]
|
|
562
|
+
}
|
|
563
|
+
},
|
|
564
|
+
{
|
|
565
|
+
name: "update_room",
|
|
566
|
+
description: "Update room metadata.",
|
|
567
|
+
inputSchema: {
|
|
568
|
+
type: "object",
|
|
569
|
+
properties: {
|
|
570
|
+
roomId: { type: "string", description: "Room ID" },
|
|
571
|
+
name: { type: "string", description: "New room name" },
|
|
572
|
+
description: { type: "string", description: "New room description" }
|
|
573
|
+
},
|
|
574
|
+
required: ["roomId"]
|
|
575
|
+
}
|
|
576
|
+
},
|
|
577
|
+
{
|
|
578
|
+
name: "delete_room",
|
|
579
|
+
description: "Delete a room.",
|
|
580
|
+
inputSchema: {
|
|
581
|
+
type: "object",
|
|
582
|
+
properties: { roomId: { type: "string", description: "Room ID" } },
|
|
583
|
+
required: ["roomId"]
|
|
584
|
+
}
|
|
585
|
+
},
|
|
586
|
+
{
|
|
587
|
+
name: "list_room_messages",
|
|
588
|
+
description: "List room messages.",
|
|
589
|
+
inputSchema: {
|
|
590
|
+
type: "object",
|
|
591
|
+
properties: {
|
|
592
|
+
roomId: { type: "string", description: "Room ID" },
|
|
593
|
+
limit: { type: "number", description: "Message limit", default: 50 },
|
|
594
|
+
before: { type: "number", description: "Load messages before this timestamp" }
|
|
595
|
+
},
|
|
596
|
+
required: ["roomId"]
|
|
597
|
+
}
|
|
598
|
+
},
|
|
599
|
+
{
|
|
600
|
+
name: "send_room_message",
|
|
601
|
+
description: "Send a message to a room.",
|
|
602
|
+
inputSchema: {
|
|
603
|
+
type: "object",
|
|
604
|
+
properties: {
|
|
605
|
+
roomId: { type: "string", description: "Room ID" },
|
|
606
|
+
content: { type: "string", description: "Message content" },
|
|
607
|
+
tempId: { type: "string", description: "Optional client temp ID" }
|
|
608
|
+
},
|
|
609
|
+
required: ["roomId", "content"]
|
|
610
|
+
}
|
|
611
|
+
},
|
|
612
|
+
{
|
|
613
|
+
name: "join_room",
|
|
614
|
+
description: "Join a room, optionally with an invite code.",
|
|
615
|
+
inputSchema: {
|
|
616
|
+
type: "object",
|
|
617
|
+
properties: {
|
|
618
|
+
roomId: { type: "string", description: "Room ID" },
|
|
619
|
+
inviteCode: { type: "string", description: "Optional invite code" }
|
|
620
|
+
},
|
|
621
|
+
required: ["roomId"]
|
|
622
|
+
}
|
|
623
|
+
},
|
|
624
|
+
{
|
|
625
|
+
name: "leave_room",
|
|
626
|
+
description: "Leave a room.",
|
|
627
|
+
inputSchema: {
|
|
628
|
+
type: "object",
|
|
629
|
+
properties: { roomId: { type: "string", description: "Room ID" } },
|
|
630
|
+
required: ["roomId"]
|
|
631
|
+
}
|
|
632
|
+
},
|
|
633
|
+
{
|
|
634
|
+
name: "remove_room_member",
|
|
635
|
+
description: "Remove a member from a room.",
|
|
636
|
+
inputSchema: {
|
|
637
|
+
type: "object",
|
|
638
|
+
properties: {
|
|
639
|
+
roomId: { type: "string", description: "Room ID" },
|
|
640
|
+
uid: { type: "string", description: "User ID to remove" }
|
|
641
|
+
},
|
|
642
|
+
required: ["roomId", "uid"]
|
|
643
|
+
}
|
|
644
|
+
},
|
|
645
|
+
{
|
|
646
|
+
name: "promote_room_member",
|
|
647
|
+
description: "Change a room member role.",
|
|
648
|
+
inputSchema: {
|
|
649
|
+
type: "object",
|
|
650
|
+
properties: {
|
|
651
|
+
roomId: { type: "string", description: "Room ID" },
|
|
652
|
+
uid: { type: "string", description: "User ID to promote" },
|
|
653
|
+
role: { type: "string", description: "Target role", enum: ["admin", "member", "owner"] }
|
|
654
|
+
},
|
|
655
|
+
required: ["roomId", "uid", "role"]
|
|
656
|
+
}
|
|
657
|
+
},
|
|
658
|
+
{
|
|
659
|
+
name: "create_room_invite",
|
|
660
|
+
description: "Create or refresh a room invite code.",
|
|
661
|
+
inputSchema: {
|
|
662
|
+
type: "object",
|
|
663
|
+
properties: {
|
|
664
|
+
roomId: { type: "string", description: "Room ID" },
|
|
665
|
+
expiresInHours: { type: "number", description: "Invite expiry in hours" },
|
|
666
|
+
maxUses: { type: "number", description: "Optional max uses" }
|
|
667
|
+
},
|
|
668
|
+
required: ["roomId"]
|
|
669
|
+
}
|
|
670
|
+
},
|
|
671
|
+
{
|
|
672
|
+
name: "list_room_pins",
|
|
673
|
+
description: "List pinned evidence items in a room.",
|
|
674
|
+
inputSchema: {
|
|
675
|
+
type: "object",
|
|
676
|
+
properties: { roomId: { type: "string", description: "Room ID" } },
|
|
677
|
+
required: ["roomId"]
|
|
678
|
+
}
|
|
679
|
+
},
|
|
680
|
+
{
|
|
681
|
+
name: "pin_room_message",
|
|
682
|
+
description: "Pin a room message as evidence.",
|
|
683
|
+
inputSchema: {
|
|
684
|
+
type: "object",
|
|
685
|
+
properties: {
|
|
686
|
+
roomId: { type: "string", description: "Room ID" },
|
|
687
|
+
messageId: { type: "string", description: "Message ID" },
|
|
688
|
+
category: { type: "string", description: "Pin category", default: "evidence" },
|
|
689
|
+
note: { type: "string", description: "Optional note" }
|
|
690
|
+
},
|
|
691
|
+
required: ["roomId", "messageId"]
|
|
692
|
+
}
|
|
693
|
+
},
|
|
694
|
+
{
|
|
695
|
+
name: "unpin_room_message",
|
|
696
|
+
description: "Remove a pinned message from the evidence board.",
|
|
697
|
+
inputSchema: {
|
|
698
|
+
type: "object",
|
|
699
|
+
properties: {
|
|
700
|
+
roomId: { type: "string", description: "Room ID" },
|
|
701
|
+
messageId: { type: "string", description: "Message ID" }
|
|
702
|
+
},
|
|
703
|
+
required: ["roomId", "messageId"]
|
|
704
|
+
}
|
|
705
|
+
},
|
|
706
|
+
{
|
|
707
|
+
name: "edit_room_message",
|
|
708
|
+
description: "Edit a room message.",
|
|
709
|
+
inputSchema: {
|
|
710
|
+
type: "object",
|
|
711
|
+
properties: {
|
|
712
|
+
roomId: { type: "string", description: "Room ID" },
|
|
713
|
+
messageId: { type: "string", description: "Message ID" },
|
|
714
|
+
content: { type: "string", description: "Updated content" }
|
|
715
|
+
},
|
|
716
|
+
required: ["roomId", "messageId", "content"]
|
|
717
|
+
}
|
|
718
|
+
},
|
|
719
|
+
{
|
|
720
|
+
name: "delete_room_message",
|
|
721
|
+
description: "Delete a room message.",
|
|
722
|
+
inputSchema: {
|
|
723
|
+
type: "object",
|
|
724
|
+
properties: {
|
|
725
|
+
roomId: { type: "string", description: "Room ID" },
|
|
726
|
+
messageId: { type: "string", description: "Message ID" }
|
|
727
|
+
},
|
|
728
|
+
required: ["roomId", "messageId"]
|
|
729
|
+
}
|
|
730
|
+
},
|
|
731
|
+
{
|
|
732
|
+
name: "export_room",
|
|
733
|
+
description: "Export a room as structured evidence data.",
|
|
734
|
+
inputSchema: {
|
|
735
|
+
type: "object",
|
|
736
|
+
properties: { roomId: { type: "string", description: "Room ID" } },
|
|
737
|
+
required: ["roomId"]
|
|
738
|
+
}
|
|
739
|
+
},
|
|
740
|
+
{
|
|
741
|
+
name: "get_polymarket_markets",
|
|
742
|
+
description: "List Polymarket markets with filters.",
|
|
743
|
+
inputSchema: {
|
|
744
|
+
type: "object",
|
|
745
|
+
properties: {
|
|
746
|
+
q: { type: "string", description: "Search query" },
|
|
747
|
+
active: { type: "boolean", description: "Only active markets" },
|
|
748
|
+
closed: { type: "boolean", description: "Only closed markets" },
|
|
749
|
+
limit: { type: "number", description: "Limit", default: 20 },
|
|
750
|
+
offset: { type: "number", description: "Offset", default: 0 },
|
|
751
|
+
order: { type: "string", description: "Sort order", enum: ["volume24hr", "liquidity", "endDate", "startDate"] }
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
},
|
|
755
|
+
{
|
|
756
|
+
name: "get_polymarket_market",
|
|
757
|
+
description: "Get a Polymarket market by slug.",
|
|
758
|
+
inputSchema: {
|
|
759
|
+
type: "object",
|
|
760
|
+
properties: { slug: { type: "string", description: "Market slug" } },
|
|
761
|
+
required: ["slug"]
|
|
762
|
+
}
|
|
763
|
+
},
|
|
764
|
+
{
|
|
765
|
+
name: "get_polymarket_trending",
|
|
766
|
+
description: "Get trending Polymarket markets.",
|
|
767
|
+
inputSchema: {
|
|
768
|
+
type: "object",
|
|
769
|
+
properties: { limit: { type: "number", description: "Limit", default: 10 } }
|
|
770
|
+
}
|
|
771
|
+
},
|
|
772
|
+
{
|
|
773
|
+
name: "get_polymarket_spikes",
|
|
774
|
+
description: "Detect Polymarket volume spikes.",
|
|
775
|
+
inputSchema: {
|
|
776
|
+
type: "object",
|
|
777
|
+
properties: {
|
|
778
|
+
threshold: { type: "number", description: "Spike threshold", default: 2 },
|
|
779
|
+
minVolume: { type: "number", description: "Minimum volume", default: 1e4 }
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
},
|
|
783
|
+
{
|
|
784
|
+
name: "get_polymarket_movers",
|
|
785
|
+
description: "Get Polymarket markets with large price movements.",
|
|
786
|
+
inputSchema: {
|
|
787
|
+
type: "object",
|
|
788
|
+
properties: { minChange: { type: "number", description: "Minimum percentage change", default: 0.05 } }
|
|
789
|
+
}
|
|
790
|
+
},
|
|
791
|
+
{
|
|
792
|
+
name: "get_polymarket_events",
|
|
793
|
+
description: "Get Polymarket events.",
|
|
794
|
+
inputSchema: {
|
|
795
|
+
type: "object",
|
|
796
|
+
properties: {
|
|
797
|
+
active: { type: "boolean", description: "Only active events" },
|
|
798
|
+
limit: { type: "number", description: "Limit", default: 20 },
|
|
799
|
+
offset: { type: "number", description: "Offset", default: 0 }
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
},
|
|
803
|
+
{
|
|
804
|
+
name: "get_polymarket_leaderboard",
|
|
805
|
+
description: "Get top Polymarket traders.",
|
|
806
|
+
inputSchema: {
|
|
807
|
+
type: "object",
|
|
808
|
+
properties: { limit: { type: "number", description: "Limit", default: 20 } }
|
|
809
|
+
}
|
|
810
|
+
},
|
|
811
|
+
{
|
|
812
|
+
name: "get_polymarket_trader",
|
|
813
|
+
description: "Get a Polymarket trader profile.",
|
|
814
|
+
inputSchema: {
|
|
815
|
+
type: "object",
|
|
816
|
+
properties: { address: { type: "string", description: "Trader address" } },
|
|
817
|
+
required: ["address"]
|
|
818
|
+
}
|
|
819
|
+
},
|
|
820
|
+
{
|
|
821
|
+
name: "get_polymarket_order_book",
|
|
822
|
+
description: "Get a Polymarket order book.",
|
|
823
|
+
inputSchema: {
|
|
824
|
+
type: "object",
|
|
825
|
+
properties: { tokenId: { type: "string", description: "Order book token ID" } },
|
|
826
|
+
required: ["tokenId"]
|
|
827
|
+
}
|
|
828
|
+
},
|
|
829
|
+
{
|
|
830
|
+
name: "get_polymarket_trades",
|
|
831
|
+
description: "Get Polymarket trades for a condition.",
|
|
832
|
+
inputSchema: {
|
|
833
|
+
type: "object",
|
|
834
|
+
properties: {
|
|
835
|
+
conditionId: { type: "string", description: "Condition ID" },
|
|
836
|
+
limit: { type: "number", description: "Limit", default: 20 }
|
|
837
|
+
},
|
|
838
|
+
required: ["conditionId"]
|
|
839
|
+
}
|
|
840
|
+
},
|
|
841
|
+
{
|
|
842
|
+
name: "get_polymarket_history",
|
|
843
|
+
description: "Get Polymarket price history.",
|
|
844
|
+
inputSchema: {
|
|
845
|
+
type: "object",
|
|
846
|
+
properties: {
|
|
847
|
+
conditionId: { type: "string", description: "Condition ID" },
|
|
848
|
+
interval: { type: "string", description: "History interval", enum: ["hour", "day"] },
|
|
849
|
+
limit: { type: "number", description: "Limit", default: 50 }
|
|
850
|
+
},
|
|
851
|
+
required: ["conditionId"]
|
|
852
|
+
}
|
|
853
|
+
},
|
|
854
|
+
{
|
|
855
|
+
name: "fetch_dune_interactors",
|
|
856
|
+
description: "Fetch contract interactors from Dune Analytics.",
|
|
857
|
+
inputSchema: {
|
|
858
|
+
type: "object",
|
|
859
|
+
properties: {
|
|
860
|
+
contractAddress: { type: "string", description: "Contract address" },
|
|
861
|
+
chain: { type: "string", description: "Chain name", enum: ["ethereum", "polygon", "arbitrum", "optimism", "base", "linea"] },
|
|
862
|
+
limit: { type: "number", description: "Row limit", default: 1e3 },
|
|
863
|
+
customApiKey: { type: "string", description: "Optional Dune API key override" }
|
|
864
|
+
},
|
|
865
|
+
required: ["contractAddress", "chain"]
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
];
|
|
869
|
+
|
|
870
|
+
// src/mcp/api-handlers.ts
|
|
871
|
+
import { default as axios } from "axios";
|
|
872
|
+
|
|
873
|
+
// src/mcp/mcpLogger.ts
|
|
874
|
+
var mcpLogWarnings = 0;
|
|
875
|
+
async function logMcpRequest(entry) {
|
|
876
|
+
if (process.env.FUNDTRACER_MCP_DISABLE_LOGGING === "1") {
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
try {
|
|
880
|
+
const { getFirestore } = await import("../firebase.js");
|
|
881
|
+
const db = getFirestore();
|
|
882
|
+
if (!db) {
|
|
883
|
+
console.warn("[MCP-LOGGER] getFirestore() returned null");
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
await db.collection("mcpLogs").add(entry);
|
|
887
|
+
console.log("[MCP-LOGGER] Logged:", entry.toolName, "for user:", entry.userId, "status:", entry.status);
|
|
888
|
+
} catch (err2) {
|
|
889
|
+
mcpLogWarnings++;
|
|
890
|
+
if (mcpLogWarnings <= 3 || mcpLogWarnings % 10 === 0) {
|
|
891
|
+
console.error("[MCP-LOGGER] Failed to log MCP request:", err2?.message || err2);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
// src/utils/nameResolver.ts
|
|
897
|
+
import { JsonRpcProvider, keccak256, toUtf8Bytes } from "ethers";
|
|
898
|
+
var ETH_ADDRESS_RE = /^0x[a-fA-F0-9]{40}$/;
|
|
899
|
+
var BASENAME_RESOLVER = "0xC6d566A56A1aFf6508b41f6c90ff131615583BCD";
|
|
900
|
+
var BASE_RPC = "https://mainnet.base.org";
|
|
901
|
+
var LNS_REGISTRY = "0x50130b669B28C339991d8676FA73CF122a121267";
|
|
902
|
+
var LNS_ADDR_SELECTOR = "0x3b3b57de";
|
|
903
|
+
var cache = /* @__PURE__ */ new Map();
|
|
904
|
+
var CACHE_TTL = 5 * 60 * 1e3;
|
|
905
|
+
var MAX_CACHE_SIZE = 500;
|
|
906
|
+
function getFromCache(name) {
|
|
907
|
+
const key = name.toLowerCase();
|
|
908
|
+
const entry = cache.get(key);
|
|
909
|
+
if (!entry) return null;
|
|
910
|
+
if (Date.now() - entry.timestamp > CACHE_TTL) {
|
|
911
|
+
cache.delete(key);
|
|
912
|
+
return null;
|
|
913
|
+
}
|
|
914
|
+
cache.delete(key);
|
|
915
|
+
cache.set(key, entry);
|
|
916
|
+
return entry.address;
|
|
917
|
+
}
|
|
918
|
+
function setCache(name, address) {
|
|
919
|
+
if (cache.size >= MAX_CACHE_SIZE) {
|
|
920
|
+
const first = cache.keys().next().value;
|
|
921
|
+
if (first) cache.delete(first);
|
|
922
|
+
}
|
|
923
|
+
cache.set(name.toLowerCase(), { address, timestamp: Date.now() });
|
|
924
|
+
}
|
|
925
|
+
function isEnsName(input) {
|
|
926
|
+
if (!input || typeof input !== "string") return false;
|
|
927
|
+
if (ETH_ADDRESS_RE.test(input)) return false;
|
|
928
|
+
return /\.(eth|base\.eth|linea\.eth)$/i.test(input.trim());
|
|
929
|
+
}
|
|
930
|
+
function namehash(name) {
|
|
931
|
+
let node = "0000000000000000000000000000000000000000000000000000000000000000";
|
|
932
|
+
if (!name) return "0x" + node;
|
|
933
|
+
const labels = name.split(".");
|
|
934
|
+
for (let i = labels.length - 1; i >= 0; i--) {
|
|
935
|
+
const labelHash = keccak256(toUtf8Bytes(labels[i])).slice(2);
|
|
936
|
+
node = keccak256("0x" + node + labelHash).slice(2);
|
|
937
|
+
}
|
|
938
|
+
return "0x" + node;
|
|
939
|
+
}
|
|
940
|
+
async function resolveViaContract(rpcUrl, resolverAddress, name) {
|
|
941
|
+
try {
|
|
942
|
+
const node = namehash(name);
|
|
943
|
+
const res = await fetch(rpcUrl, {
|
|
944
|
+
method: "POST",
|
|
945
|
+
headers: { "Content-Type": "application/json" },
|
|
946
|
+
body: JSON.stringify({
|
|
947
|
+
jsonrpc: "2.0",
|
|
948
|
+
id: 1,
|
|
949
|
+
method: "eth_call",
|
|
950
|
+
params: [{
|
|
951
|
+
to: resolverAddress,
|
|
952
|
+
data: LNS_ADDR_SELECTOR + node.slice(2)
|
|
953
|
+
}, "latest"]
|
|
954
|
+
})
|
|
955
|
+
});
|
|
956
|
+
const data = await res.json();
|
|
957
|
+
const raw = data?.result;
|
|
958
|
+
if (!raw || raw === "0x" || raw === "0x" + "0".repeat(64)) return null;
|
|
959
|
+
return "0x" + raw.slice(26);
|
|
960
|
+
} catch {
|
|
961
|
+
return null;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
async function resolveEns(name) {
|
|
965
|
+
const key = process.env.DEFAULT_ALCHEMY_API_KEY || process.env.ALCHEMY_API_KEY || "";
|
|
966
|
+
if (!key) {
|
|
967
|
+
console.error("[nameResolver] No Alchemy API key configured for ENS resolution");
|
|
968
|
+
return null;
|
|
969
|
+
}
|
|
970
|
+
const provider = new JsonRpcProvider(`https://eth-mainnet.g.alchemy.com/v2/${key}`);
|
|
971
|
+
try {
|
|
972
|
+
const resolved = await Promise.race([
|
|
973
|
+
provider.resolveName(name),
|
|
974
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("ENS resolution timed out after 8s")), 8e3))
|
|
975
|
+
]);
|
|
976
|
+
return resolved || null;
|
|
977
|
+
} catch (err2) {
|
|
978
|
+
console.error(`[nameResolver] ENS resolveName("${name}") failed:`, err2.message || err2);
|
|
979
|
+
return null;
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
async function resolveBasename(name) {
|
|
983
|
+
try {
|
|
984
|
+
const ensResult = await resolveEns(name);
|
|
985
|
+
if (ensResult) return ensResult;
|
|
986
|
+
} catch {
|
|
987
|
+
}
|
|
988
|
+
return resolveViaContract(BASE_RPC, BASENAME_RESOLVER, name);
|
|
989
|
+
}
|
|
990
|
+
async function resolveLineaName(name) {
|
|
991
|
+
try {
|
|
992
|
+
const ensResult = await resolveEns(name);
|
|
993
|
+
if (ensResult) return ensResult;
|
|
994
|
+
} catch {
|
|
995
|
+
}
|
|
996
|
+
const rpcUrl = process.env.LINEA_RPC_URL || "https://rpc.linea.build";
|
|
997
|
+
return resolveViaContract(rpcUrl, LNS_REGISTRY, name);
|
|
998
|
+
}
|
|
999
|
+
async function tryResolveAddress(input) {
|
|
1000
|
+
if (!input || typeof input !== "string") {
|
|
1001
|
+
return { resolved: input || "", error: "Address is required" };
|
|
1002
|
+
}
|
|
1003
|
+
const trimmed = input.trim();
|
|
1004
|
+
if (ETH_ADDRESS_RE.test(trimmed)) {
|
|
1005
|
+
return { resolved: trimmed.toLowerCase() };
|
|
1006
|
+
}
|
|
1007
|
+
if (!isEnsName(trimmed)) {
|
|
1008
|
+
return { resolved: trimmed };
|
|
1009
|
+
}
|
|
1010
|
+
const cached = getFromCache(trimmed);
|
|
1011
|
+
if (cached) return { resolved: cached };
|
|
1012
|
+
const lower = trimmed.toLowerCase();
|
|
1013
|
+
let resolved = null;
|
|
1014
|
+
try {
|
|
1015
|
+
if (lower.endsWith(".base.eth")) {
|
|
1016
|
+
resolved = await resolveBasename(trimmed);
|
|
1017
|
+
} else if (lower.endsWith(".linea.eth")) {
|
|
1018
|
+
resolved = await resolveLineaName(trimmed);
|
|
1019
|
+
} else {
|
|
1020
|
+
resolved = await resolveEns(trimmed);
|
|
1021
|
+
}
|
|
1022
|
+
} catch {
|
|
1023
|
+
resolved = null;
|
|
1024
|
+
}
|
|
1025
|
+
if (!resolved) {
|
|
1026
|
+
const hints = {
|
|
1027
|
+
".base.eth": "Basename may not be registered on Base. Register at base.org/names.",
|
|
1028
|
+
".linea.eth": "Linea name may not be registered. Register via Linea Name Service.",
|
|
1029
|
+
".eth": "ENS name may not be registered. Check at app.ens.domains."
|
|
1030
|
+
};
|
|
1031
|
+
const hint = hints[lower.match(/\.(?:base\.eth|linea\.eth|eth)$/)?.[0] || ""] || "ENS name may not be registered or is not resolvable.";
|
|
1032
|
+
return {
|
|
1033
|
+
resolved: trimmed,
|
|
1034
|
+
error: `Could not resolve "${trimmed}": ${hint}`
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
1037
|
+
setCache(trimmed, resolved);
|
|
1038
|
+
return { resolved };
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
// src/mcp/api-handlers.ts
|
|
1042
|
+
function ok(text) {
|
|
1043
|
+
return { content: [{ type: "text", text }] };
|
|
1044
|
+
}
|
|
1045
|
+
function err(message) {
|
|
1046
|
+
return { content: [{ type: "text", text: message }], isError: true };
|
|
1047
|
+
}
|
|
1048
|
+
function extractApiError(error) {
|
|
1049
|
+
return error?.response?.data?.error || error?.response?.data?.message || error?.message || "Unknown error";
|
|
1050
|
+
}
|
|
1051
|
+
async function resolveAddressInput(input) {
|
|
1052
|
+
const { resolved, error } = await tryResolveAddress(input);
|
|
1053
|
+
if (error) return { error: err(error) };
|
|
1054
|
+
return { resolved };
|
|
1055
|
+
}
|
|
1056
|
+
async function routeGet(path, params) {
|
|
1057
|
+
return api().get(path, { params });
|
|
1058
|
+
}
|
|
1059
|
+
async function routePost(path, body, params) {
|
|
1060
|
+
return api().post(path, body, { params });
|
|
1061
|
+
}
|
|
1062
|
+
async function routePatch(path, body, params) {
|
|
1063
|
+
return api().patch(path, body, { params });
|
|
1064
|
+
}
|
|
1065
|
+
async function routeDelete(path, params) {
|
|
1066
|
+
return api().delete(path, { params });
|
|
1067
|
+
}
|
|
1068
|
+
var API_BASE = process.env.FUNDTRACER_API_URL || "https://api.fundtracer.xyz";
|
|
1069
|
+
var _mcpCtx = null;
|
|
1070
|
+
function api() {
|
|
1071
|
+
const key = _mcpCtx?.apiKey || process.env.FUNDTRACER_MCP_API_KEY || "";
|
|
1072
|
+
const headers = {
|
|
1073
|
+
Authorization: `Bearer ${key}`,
|
|
1074
|
+
"Content-Type": "application/json"
|
|
1075
|
+
};
|
|
1076
|
+
if (_mcpCtx?.userId) {
|
|
1077
|
+
headers["X-MCP-UserId"] = _mcpCtx.userId;
|
|
1078
|
+
headers["x-auth-token"] = `${key}:${_mcpCtx.userId}`;
|
|
1079
|
+
}
|
|
1080
|
+
return axios.create({
|
|
1081
|
+
baseURL: API_BASE,
|
|
1082
|
+
timeout: 6e4,
|
|
1083
|
+
headers
|
|
1084
|
+
});
|
|
1085
|
+
}
|
|
1086
|
+
var analyzeWallet = async (args, ctx) => {
|
|
1087
|
+
const { address, chainId, transactionLimit } = args;
|
|
1088
|
+
try {
|
|
1089
|
+
const res = await api().post("/api/analyze/wallet", {
|
|
1090
|
+
address,
|
|
1091
|
+
chain: chainId,
|
|
1092
|
+
options: { limit: transactionLimit || 500 }
|
|
1093
|
+
});
|
|
1094
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1095
|
+
} catch (error) {
|
|
1096
|
+
const msg = error.response?.data?.error || error.message;
|
|
1097
|
+
return err(`Wallet analysis failed: ${msg}`);
|
|
1098
|
+
}
|
|
1099
|
+
};
|
|
1100
|
+
var traceFunds = async (args, ctx) => {
|
|
1101
|
+
const { address, chainId, maxDepth = 3, direction = "both" } = args;
|
|
1102
|
+
try {
|
|
1103
|
+
const res = await api().post("/api/analyze/funding-tree", {
|
|
1104
|
+
address,
|
|
1105
|
+
chain: chainId,
|
|
1106
|
+
maxDepth,
|
|
1107
|
+
direction
|
|
1108
|
+
});
|
|
1109
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1110
|
+
} catch (error) {
|
|
1111
|
+
const msg = error.response?.data?.error || error.message;
|
|
1112
|
+
return err(`Fund tracing failed: ${msg}`);
|
|
1113
|
+
}
|
|
1114
|
+
};
|
|
1115
|
+
var compareWallets = async (args, ctx) => {
|
|
1116
|
+
const { addresses, chainId } = args;
|
|
1117
|
+
const addrList = addresses.split(",").map((a) => a.trim()).filter(Boolean);
|
|
1118
|
+
if (addrList.length < 2) return err("At least 2 addresses required");
|
|
1119
|
+
try {
|
|
1120
|
+
const res = await api().post("/api/analyze/compare", {
|
|
1121
|
+
addresses: addrList,
|
|
1122
|
+
chain: chainId
|
|
1123
|
+
});
|
|
1124
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1125
|
+
} catch (error) {
|
|
1126
|
+
const msg = error.response?.data?.error || error.message;
|
|
1127
|
+
return err(`Wallet comparison failed: ${msg}`);
|
|
1128
|
+
}
|
|
1129
|
+
};
|
|
1130
|
+
var analyzeContract = async (args, ctx) => {
|
|
1131
|
+
const { contractAddress, chainId, maxInteractors = 100 } = args;
|
|
1132
|
+
try {
|
|
1133
|
+
const res = await api().post("/api/analyze/contract", {
|
|
1134
|
+
contractAddress,
|
|
1135
|
+
chain: chainId,
|
|
1136
|
+
maxInteractors
|
|
1137
|
+
});
|
|
1138
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1139
|
+
} catch (error) {
|
|
1140
|
+
const msg = error.response?.data?.error || error.message;
|
|
1141
|
+
return err(`Contract analysis failed: ${msg}`);
|
|
1142
|
+
}
|
|
1143
|
+
};
|
|
1144
|
+
var detectSybilClusters = async (args, ctx) => {
|
|
1145
|
+
const { addresses, chainId } = args;
|
|
1146
|
+
const addrList = addresses.split(",").map((a) => a.trim()).filter(Boolean);
|
|
1147
|
+
if (addrList.length < 3) return err("At least 3 addresses required for cluster detection");
|
|
1148
|
+
try {
|
|
1149
|
+
const res = await api().post("/api/analyze/sybil", {
|
|
1150
|
+
addresses: addrList,
|
|
1151
|
+
chain: chainId
|
|
1152
|
+
});
|
|
1153
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1154
|
+
} catch (error) {
|
|
1155
|
+
const msg = error.response?.data?.error || error.message;
|
|
1156
|
+
return err(`Sybil detection failed: ${msg}`);
|
|
1157
|
+
}
|
|
1158
|
+
};
|
|
1159
|
+
var getPortfolio = async (args, ctx) => {
|
|
1160
|
+
const { address, chainId } = args;
|
|
1161
|
+
const { resolved: resolvedAddr, error: resolveErr } = await resolveAddressInput(address);
|
|
1162
|
+
if (resolveErr) return resolveErr;
|
|
1163
|
+
try {
|
|
1164
|
+
const res = await api().get(`/api/portfolio/${encodeURIComponent(resolvedAddr || address)}`, {
|
|
1165
|
+
params: { chain: chainId }
|
|
1166
|
+
});
|
|
1167
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1168
|
+
} catch (error) {
|
|
1169
|
+
const msg = extractApiError(error);
|
|
1170
|
+
return err(`Portfolio fetch failed: ${msg}`);
|
|
1171
|
+
}
|
|
1172
|
+
};
|
|
1173
|
+
var getTransactions = async (args, ctx) => {
|
|
1174
|
+
const { address, chainId, limit = 50 } = args;
|
|
1175
|
+
const { resolved: resolvedAddr, error: resolveErr } = await resolveAddressInput(address);
|
|
1176
|
+
if (resolveErr) return resolveErr;
|
|
1177
|
+
try {
|
|
1178
|
+
const res = await api().post("/api/history", {
|
|
1179
|
+
wallet: resolvedAddr || address,
|
|
1180
|
+
blockchain: chainId,
|
|
1181
|
+
pageToken: null,
|
|
1182
|
+
filters: {}
|
|
1183
|
+
});
|
|
1184
|
+
const txs = (res.data.transactions || []).slice(0, limit);
|
|
1185
|
+
return ok(JSON.stringify({
|
|
1186
|
+
address,
|
|
1187
|
+
chainId,
|
|
1188
|
+
transactions: txs,
|
|
1189
|
+
totalCount: res.data.transactions?.length || 0
|
|
1190
|
+
}, null, 2));
|
|
1191
|
+
} catch (error) {
|
|
1192
|
+
const msg = extractApiError(error);
|
|
1193
|
+
return err(`Transaction fetch failed: ${msg}`);
|
|
1194
|
+
}
|
|
1195
|
+
};
|
|
1196
|
+
var lookupEntity = async (args, ctx) => {
|
|
1197
|
+
const { query, chainId } = args;
|
|
1198
|
+
const chain = chainId || "ethereum";
|
|
1199
|
+
const { resolved: resolvedQuery } = await resolveAddressInput(query);
|
|
1200
|
+
const lookupValue = resolvedQuery || query;
|
|
1201
|
+
try {
|
|
1202
|
+
if (/^0x[a-fA-F0-9]{40}$/.test(lookupValue) || /^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(lookupValue)) {
|
|
1203
|
+
const res2 = await api().get(`/api/entities/${lookupValue}`, {
|
|
1204
|
+
params: { chain }
|
|
1205
|
+
});
|
|
1206
|
+
return ok(JSON.stringify(res2.data, null, 2));
|
|
1207
|
+
}
|
|
1208
|
+
const res = await api().get("/api/entities/search", {
|
|
1209
|
+
params: { q: lookupValue, chain }
|
|
1210
|
+
});
|
|
1211
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1212
|
+
} catch (error) {
|
|
1213
|
+
if (error.response?.status === 404) {
|
|
1214
|
+
return ok(JSON.stringify({ query: lookupValue, label: "Unknown address", chain }, null, 2));
|
|
1215
|
+
}
|
|
1216
|
+
const msg = error.response?.data?.error || error.message;
|
|
1217
|
+
return err(`Entity lookup failed: ${msg}`);
|
|
1218
|
+
}
|
|
1219
|
+
};
|
|
1220
|
+
var getGasPrices = async (args, ctx) => {
|
|
1221
|
+
const { chainId } = args;
|
|
1222
|
+
try {
|
|
390
1223
|
const res = await api().get("/api/gas", {
|
|
391
1224
|
params: chainId ? { chain: chainId } : {}
|
|
392
1225
|
});
|
|
393
1226
|
return ok(JSON.stringify(res.data, null, 2));
|
|
394
1227
|
} catch (error) {
|
|
395
|
-
const msg = error.response?.data?.error || error.message;
|
|
396
|
-
return err(`Gas price fetch failed: ${msg}`);
|
|
1228
|
+
const msg = error.response?.data?.error || error.message;
|
|
1229
|
+
return err(`Gas price fetch failed: ${msg}`);
|
|
1230
|
+
}
|
|
1231
|
+
};
|
|
1232
|
+
var getTokenInfo = async (args, ctx) => {
|
|
1233
|
+
const { tokenAddress, chainId } = args;
|
|
1234
|
+
const { resolved: resolvedAddr, error: resolveErr } = await resolveAddressInput(tokenAddress);
|
|
1235
|
+
if (resolveErr) return resolveErr;
|
|
1236
|
+
try {
|
|
1237
|
+
const res = await routeGet(`/api/tokens/${encodeURIComponent(resolvedAddr || tokenAddress)}`, {
|
|
1238
|
+
chain: chainId
|
|
1239
|
+
});
|
|
1240
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1241
|
+
} catch (error) {
|
|
1242
|
+
const msg = extractApiError(error);
|
|
1243
|
+
return err(`Token info fetch failed: ${msg}`);
|
|
1244
|
+
}
|
|
1245
|
+
};
|
|
1246
|
+
var resolveName = async (args, ctx) => {
|
|
1247
|
+
const { input } = args;
|
|
1248
|
+
const { resolved, error } = await tryResolveAddress(input);
|
|
1249
|
+
const normalizedInput = typeof input === "string" ? input.trim().toLowerCase() : "";
|
|
1250
|
+
const normalizedResolved = typeof resolved === "string" ? resolved.toLowerCase() : "";
|
|
1251
|
+
return ok(JSON.stringify({
|
|
1252
|
+
input,
|
|
1253
|
+
resolved,
|
|
1254
|
+
isAddress: /^0x[a-fA-F0-9]{40}$/.test(resolved),
|
|
1255
|
+
isResolvedName: normalizedResolved !== normalizedInput,
|
|
1256
|
+
...error ? { warning: error } : {}
|
|
1257
|
+
}, null, 2));
|
|
1258
|
+
};
|
|
1259
|
+
var getTransactionDetail = async (args, ctx) => {
|
|
1260
|
+
const { chainId, hash } = args;
|
|
1261
|
+
try {
|
|
1262
|
+
const res = await routeGet(`/api/tx/${encodeURIComponent(chainId)}/${encodeURIComponent(hash)}`);
|
|
1263
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1264
|
+
} catch (error) {
|
|
1265
|
+
return err(`Transaction detail fetch failed: ${extractApiError(error)}`);
|
|
1266
|
+
}
|
|
1267
|
+
};
|
|
1268
|
+
var getTransactionHistory = async (args, ctx) => {
|
|
1269
|
+
const { address, chainId, pageToken = null, filters = {} } = args;
|
|
1270
|
+
const { resolved: resolvedAddr, error: resolveErr } = await resolveAddressInput(address);
|
|
1271
|
+
if (resolveErr) return resolveErr;
|
|
1272
|
+
try {
|
|
1273
|
+
const res = await routePost("/api/history", {
|
|
1274
|
+
wallet: resolvedAddr || address,
|
|
1275
|
+
blockchain: chainId,
|
|
1276
|
+
pageToken,
|
|
1277
|
+
filters
|
|
1278
|
+
});
|
|
1279
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1280
|
+
} catch (error) {
|
|
1281
|
+
return err(`Transaction history fetch failed: ${extractApiError(error)}`);
|
|
1282
|
+
}
|
|
1283
|
+
};
|
|
1284
|
+
var analyzeCEXFlow = async (args, ctx) => {
|
|
1285
|
+
const { walletAddress, chainId, cexName, depth } = args;
|
|
1286
|
+
const { resolved: resolvedAddr, error: resolveErr } = await resolveAddressInput(walletAddress);
|
|
1287
|
+
if (resolveErr) return resolveErr;
|
|
1288
|
+
try {
|
|
1289
|
+
const res = await routePost("/api/analyze/cex-flow", {
|
|
1290
|
+
walletAddress: resolvedAddr || walletAddress,
|
|
1291
|
+
chain: chainId,
|
|
1292
|
+
...cexName ? { cexName } : {},
|
|
1293
|
+
...depth !== void 0 ? { depth } : {}
|
|
1294
|
+
});
|
|
1295
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1296
|
+
} catch (error) {
|
|
1297
|
+
return err(`CEX flow analysis failed: ${extractApiError(error)}`);
|
|
1298
|
+
}
|
|
1299
|
+
};
|
|
1300
|
+
var analyzeSybilAddresses = async (args, ctx) => {
|
|
1301
|
+
const { addresses, chainId, txHash } = args;
|
|
1302
|
+
const list = addresses.split(",").map((a) => a.trim()).filter(Boolean);
|
|
1303
|
+
const resolved = [];
|
|
1304
|
+
for (const a of list) {
|
|
1305
|
+
const r = await resolveAddressInput(a);
|
|
1306
|
+
if (r.error) return r.error;
|
|
1307
|
+
resolved.push(r.resolved || a);
|
|
1308
|
+
}
|
|
1309
|
+
try {
|
|
1310
|
+
const res = await routePost("/api/analyze/sybil-addresses", {
|
|
1311
|
+
addresses: resolved,
|
|
1312
|
+
chain: chainId,
|
|
1313
|
+
...txHash ? { txHash } : {}
|
|
1314
|
+
});
|
|
1315
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1316
|
+
} catch (error) {
|
|
1317
|
+
return err(`Sybil address analysis failed: ${extractApiError(error)}`);
|
|
1318
|
+
}
|
|
1319
|
+
};
|
|
1320
|
+
var searchContracts = async (args) => {
|
|
1321
|
+
const { query } = args;
|
|
1322
|
+
try {
|
|
1323
|
+
const res = await routeGet("/api/contracts/search", { q: query });
|
|
1324
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1325
|
+
} catch (error) {
|
|
1326
|
+
return err(`Contract search failed: ${extractApiError(error)}`);
|
|
1327
|
+
}
|
|
1328
|
+
};
|
|
1329
|
+
var lookupContract = async (args) => {
|
|
1330
|
+
const { address } = args;
|
|
1331
|
+
const { resolved: resolvedAddr, error: resolveErr } = await resolveAddressInput(address);
|
|
1332
|
+
if (resolveErr) return resolveErr;
|
|
1333
|
+
try {
|
|
1334
|
+
const res = await routeGet(`/api/contracts/lookup/${encodeURIComponent(resolvedAddr || address)}`);
|
|
1335
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1336
|
+
} catch (error) {
|
|
1337
|
+
return err(`Contract lookup failed: ${extractApiError(error)}`);
|
|
1338
|
+
}
|
|
1339
|
+
};
|
|
1340
|
+
var getContractStats = async () => {
|
|
1341
|
+
try {
|
|
1342
|
+
const res = await routeGet("/api/contracts/stats");
|
|
1343
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1344
|
+
} catch (error) {
|
|
1345
|
+
return err(`Contract stats fetch failed: ${extractApiError(error)}`);
|
|
1346
|
+
}
|
|
1347
|
+
};
|
|
1348
|
+
var searchTokens = async (args) => {
|
|
1349
|
+
const { query } = args;
|
|
1350
|
+
try {
|
|
1351
|
+
const res = await routeGet("/api/tokens/search", { q: query });
|
|
1352
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1353
|
+
} catch (error) {
|
|
1354
|
+
return err(`Token search failed: ${extractApiError(error)}`);
|
|
1355
|
+
}
|
|
1356
|
+
};
|
|
1357
|
+
var getMarketStats = async () => {
|
|
1358
|
+
try {
|
|
1359
|
+
const res = await routeGet("/api/market/stats");
|
|
1360
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1361
|
+
} catch (error) {
|
|
1362
|
+
return err(`Market stats fetch failed: ${extractApiError(error)}`);
|
|
1363
|
+
}
|
|
1364
|
+
};
|
|
1365
|
+
var getMarketCoins = async (args) => {
|
|
1366
|
+
const { chainId, page = 1, perPage = 100 } = args;
|
|
1367
|
+
try {
|
|
1368
|
+
const res = await routeGet("/api/market/coins", {
|
|
1369
|
+
chain: chainId || "all",
|
|
1370
|
+
page,
|
|
1371
|
+
per_page: perPage
|
|
1372
|
+
});
|
|
1373
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1374
|
+
} catch (error) {
|
|
1375
|
+
return err(`Market coins fetch failed: ${extractApiError(error)}`);
|
|
1376
|
+
}
|
|
1377
|
+
};
|
|
1378
|
+
var getTokenChart = async (args) => {
|
|
1379
|
+
const { tokenAddress, chainId, coinId, days = 7 } = args;
|
|
1380
|
+
const { resolved: resolvedAddr, error: resolveErr } = await resolveAddressInput(tokenAddress);
|
|
1381
|
+
if (resolveErr) return resolveErr;
|
|
1382
|
+
try {
|
|
1383
|
+
const res = await routeGet(`/api/tokens/${encodeURIComponent(resolvedAddr || tokenAddress)}/chart`, {
|
|
1384
|
+
chain: chainId,
|
|
1385
|
+
coinId,
|
|
1386
|
+
days
|
|
1387
|
+
});
|
|
1388
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1389
|
+
} catch (error) {
|
|
1390
|
+
return err(`Token chart fetch failed: ${extractApiError(error)}`);
|
|
397
1391
|
}
|
|
398
1392
|
};
|
|
399
|
-
var
|
|
400
|
-
|
|
1393
|
+
var getDexScreenerLatestProfiles = async () => {
|
|
1394
|
+
try {
|
|
1395
|
+
const res = await routeGet("/api/dexscreener/profiles/latest");
|
|
1396
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1397
|
+
} catch (error) {
|
|
1398
|
+
return err(`DEX Screener profiles fetch failed: ${extractApiError(error)}`);
|
|
1399
|
+
}
|
|
1400
|
+
};
|
|
1401
|
+
var getDexScreenerBoosts = async () => {
|
|
1402
|
+
try {
|
|
1403
|
+
const res = await routeGet("/api/dexscreener/boosts/top");
|
|
1404
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1405
|
+
} catch (error) {
|
|
1406
|
+
return err(`DEX Screener boosts fetch failed: ${extractApiError(error)}`);
|
|
1407
|
+
}
|
|
1408
|
+
};
|
|
1409
|
+
var searchDexScreenerPairs = async (args) => {
|
|
1410
|
+
const { query } = args;
|
|
1411
|
+
try {
|
|
1412
|
+
const res = await routeGet("/api/dexscreener/search", { q: query });
|
|
1413
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1414
|
+
} catch (error) {
|
|
1415
|
+
return err(`DEX Screener search failed: ${extractApiError(error)}`);
|
|
1416
|
+
}
|
|
1417
|
+
};
|
|
1418
|
+
var getDexScreenerTokenDetails = async (args) => {
|
|
1419
|
+
const { chainId, tokenAddress } = args;
|
|
1420
|
+
const { resolved: resolvedAddr, error: resolveErr } = await resolveAddressInput(tokenAddress);
|
|
1421
|
+
if (resolveErr) return resolveErr;
|
|
1422
|
+
try {
|
|
1423
|
+
const res = await routeGet(`/api/dexscreener/token/${encodeURIComponent(chainId)}/${encodeURIComponent(resolvedAddr || tokenAddress)}`);
|
|
1424
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1425
|
+
} catch (error) {
|
|
1426
|
+
return err(`DEX Screener token fetch failed: ${extractApiError(error)}`);
|
|
1427
|
+
}
|
|
1428
|
+
};
|
|
1429
|
+
var getDexScreenerTokenPairs = async (args) => {
|
|
1430
|
+
const { chainId, tokenAddress } = args;
|
|
1431
|
+
const { resolved: resolvedAddr, error: resolveErr } = await resolveAddressInput(tokenAddress);
|
|
1432
|
+
if (resolveErr) return resolveErr;
|
|
1433
|
+
try {
|
|
1434
|
+
const res = await routeGet(`/api/dexscreener/pairs/${encodeURIComponent(chainId)}/${encodeURIComponent(resolvedAddr || tokenAddress)}`);
|
|
1435
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1436
|
+
} catch (error) {
|
|
1437
|
+
return err(`DEX Screener pairs fetch failed: ${extractApiError(error)}`);
|
|
1438
|
+
}
|
|
1439
|
+
};
|
|
1440
|
+
var getDexScreenerTrending = async () => {
|
|
1441
|
+
try {
|
|
1442
|
+
const res = await routeGet("/api/dexscreener/trending");
|
|
1443
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1444
|
+
} catch (error) {
|
|
1445
|
+
return err(`DEX Screener trending fetch failed: ${extractApiError(error)}`);
|
|
1446
|
+
}
|
|
1447
|
+
};
|
|
1448
|
+
var getScanHistory = async () => {
|
|
1449
|
+
try {
|
|
1450
|
+
const res = await routeGet("/api/scan-history");
|
|
1451
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1452
|
+
} catch (error) {
|
|
1453
|
+
return err(`Scan history fetch failed: ${extractApiError(error)}`);
|
|
1454
|
+
}
|
|
1455
|
+
};
|
|
1456
|
+
var getScanHistoryStats = async () => {
|
|
1457
|
+
try {
|
|
1458
|
+
const res = await routeGet("/api/scan-history/stats");
|
|
1459
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1460
|
+
} catch (error) {
|
|
1461
|
+
return err(`Scan history stats fetch failed: ${extractApiError(error)}`);
|
|
1462
|
+
}
|
|
1463
|
+
};
|
|
1464
|
+
var saveScanHistoryItem = async (args) => {
|
|
1465
|
+
try {
|
|
1466
|
+
const res = await routePost("/api/scan-history", args);
|
|
1467
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1468
|
+
} catch (error) {
|
|
1469
|
+
return err(`Scan history save failed: ${extractApiError(error)}`);
|
|
1470
|
+
}
|
|
1471
|
+
};
|
|
1472
|
+
var syncScanHistory = async (args) => {
|
|
1473
|
+
const { items } = args;
|
|
1474
|
+
try {
|
|
1475
|
+
const res = await routePost("/api/scan-history/sync", { items });
|
|
1476
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1477
|
+
} catch (error) {
|
|
1478
|
+
return err(`Scan history sync failed: ${extractApiError(error)}`);
|
|
1479
|
+
}
|
|
1480
|
+
};
|
|
1481
|
+
var deleteScanHistoryItem = async (args) => {
|
|
1482
|
+
const { address } = args;
|
|
1483
|
+
const { resolved: resolvedAddr, error: resolveErr } = await resolveAddressInput(address);
|
|
1484
|
+
if (resolveErr) return resolveErr;
|
|
1485
|
+
try {
|
|
1486
|
+
const res = await routeDelete(`/api/scan-history/${encodeURIComponent(resolvedAddr || address)}`);
|
|
1487
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1488
|
+
} catch (error) {
|
|
1489
|
+
return err(`Scan history delete failed: ${extractApiError(error)}`);
|
|
1490
|
+
}
|
|
1491
|
+
};
|
|
1492
|
+
var clearScanHistory = async () => {
|
|
1493
|
+
try {
|
|
1494
|
+
const res = await routeDelete("/api/scan-history");
|
|
1495
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1496
|
+
} catch (error) {
|
|
1497
|
+
return err(`Scan history clear failed: ${extractApiError(error)}`);
|
|
1498
|
+
}
|
|
1499
|
+
};
|
|
1500
|
+
var listRooms = async () => {
|
|
1501
|
+
try {
|
|
1502
|
+
const res = await routeGet("/api/rooms");
|
|
1503
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1504
|
+
} catch (error) {
|
|
1505
|
+
return err(`Room listing failed: ${extractApiError(error)}`);
|
|
1506
|
+
}
|
|
1507
|
+
};
|
|
1508
|
+
var getRoomDetails = async (args) => {
|
|
1509
|
+
const { roomId } = args;
|
|
1510
|
+
try {
|
|
1511
|
+
const res = await routeGet(`/api/rooms/${encodeURIComponent(roomId)}`);
|
|
1512
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1513
|
+
} catch (error) {
|
|
1514
|
+
return err(`Room details fetch failed: ${extractApiError(error)}`);
|
|
1515
|
+
}
|
|
1516
|
+
};
|
|
1517
|
+
var createRoom = async (args) => {
|
|
1518
|
+
try {
|
|
1519
|
+
const res = await routePost("/api/rooms", args);
|
|
1520
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1521
|
+
} catch (error) {
|
|
1522
|
+
return err(`Room creation failed: ${extractApiError(error)}`);
|
|
1523
|
+
}
|
|
1524
|
+
};
|
|
1525
|
+
var updateRoom = async (args) => {
|
|
1526
|
+
const { roomId, ...body } = args;
|
|
1527
|
+
try {
|
|
1528
|
+
const res = await routePatch(`/api/rooms/${encodeURIComponent(roomId)}`, body);
|
|
1529
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1530
|
+
} catch (error) {
|
|
1531
|
+
return err(`Room update failed: ${extractApiError(error)}`);
|
|
1532
|
+
}
|
|
1533
|
+
};
|
|
1534
|
+
var deleteRoom = async (args) => {
|
|
1535
|
+
const { roomId } = args;
|
|
1536
|
+
try {
|
|
1537
|
+
const res = await routeDelete(`/api/rooms/${encodeURIComponent(roomId)}`);
|
|
1538
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1539
|
+
} catch (error) {
|
|
1540
|
+
return err(`Room delete failed: ${extractApiError(error)}`);
|
|
1541
|
+
}
|
|
1542
|
+
};
|
|
1543
|
+
var listRoomMessages = async (args) => {
|
|
1544
|
+
const { roomId, limit = 50, before } = args;
|
|
1545
|
+
try {
|
|
1546
|
+
const params = { limit };
|
|
1547
|
+
if (before !== void 0) params.before = before;
|
|
1548
|
+
const res = await routeGet(`/api/rooms/${encodeURIComponent(roomId)}/messages`, params);
|
|
1549
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1550
|
+
} catch (error) {
|
|
1551
|
+
return err(`Room messages fetch failed: ${extractApiError(error)}`);
|
|
1552
|
+
}
|
|
1553
|
+
};
|
|
1554
|
+
var sendRoomMessage = async (args) => {
|
|
1555
|
+
const { roomId, content, tempId } = args;
|
|
1556
|
+
try {
|
|
1557
|
+
const res = await routePost(`/api/rooms/${encodeURIComponent(roomId)}/messages`, { content, tempId });
|
|
1558
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1559
|
+
} catch (error) {
|
|
1560
|
+
return err(`Room message send failed: ${extractApiError(error)}`);
|
|
1561
|
+
}
|
|
1562
|
+
};
|
|
1563
|
+
var joinRoom = async (args) => {
|
|
1564
|
+
const { roomId, inviteCode } = args;
|
|
1565
|
+
try {
|
|
1566
|
+
const res = await routePost(`/api/rooms/${encodeURIComponent(roomId)}/join`, inviteCode ? { inviteCode } : {});
|
|
1567
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1568
|
+
} catch (error) {
|
|
1569
|
+
return err(`Room join failed: ${extractApiError(error)}`);
|
|
1570
|
+
}
|
|
1571
|
+
};
|
|
1572
|
+
var leaveRoom = async (args) => {
|
|
1573
|
+
const { roomId } = args;
|
|
1574
|
+
try {
|
|
1575
|
+
const res = await routePost(`/api/rooms/${encodeURIComponent(roomId)}/leave`, {});
|
|
1576
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1577
|
+
} catch (error) {
|
|
1578
|
+
return err(`Room leave failed: ${extractApiError(error)}`);
|
|
1579
|
+
}
|
|
1580
|
+
};
|
|
1581
|
+
var removeRoomMember = async (args) => {
|
|
1582
|
+
const { roomId, uid } = args;
|
|
1583
|
+
try {
|
|
1584
|
+
const res = await routeDelete(`/api/rooms/${encodeURIComponent(roomId)}/members/${encodeURIComponent(uid)}`);
|
|
1585
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1586
|
+
} catch (error) {
|
|
1587
|
+
return err(`Room member removal failed: ${extractApiError(error)}`);
|
|
1588
|
+
}
|
|
1589
|
+
};
|
|
1590
|
+
var promoteRoomMember = async (args) => {
|
|
1591
|
+
const { roomId, uid, role } = args;
|
|
1592
|
+
try {
|
|
1593
|
+
const res = await routePost(`/api/rooms/${encodeURIComponent(roomId)}/members/${encodeURIComponent(uid)}/role`, { role });
|
|
1594
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1595
|
+
} catch (error) {
|
|
1596
|
+
return err(`Room member promotion failed: ${extractApiError(error)}`);
|
|
1597
|
+
}
|
|
1598
|
+
};
|
|
1599
|
+
var createRoomInvite = async (args) => {
|
|
1600
|
+
const { roomId, expiresInHours, maxUses } = args;
|
|
1601
|
+
try {
|
|
1602
|
+
const body = {};
|
|
1603
|
+
if (expiresInHours !== void 0) body.expiresInHours = expiresInHours;
|
|
1604
|
+
if (maxUses !== void 0) body.maxUses = maxUses;
|
|
1605
|
+
const res = await routePost(`/api/rooms/${encodeURIComponent(roomId)}/invite`, body);
|
|
1606
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1607
|
+
} catch (error) {
|
|
1608
|
+
return err(`Room invite creation failed: ${extractApiError(error)}`);
|
|
1609
|
+
}
|
|
1610
|
+
};
|
|
1611
|
+
var listRoomPins = async (args) => {
|
|
1612
|
+
const { roomId } = args;
|
|
1613
|
+
try {
|
|
1614
|
+
const res = await routeGet(`/api/rooms/${encodeURIComponent(roomId)}/pins`);
|
|
1615
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1616
|
+
} catch (error) {
|
|
1617
|
+
return err(`Room pins fetch failed: ${extractApiError(error)}`);
|
|
1618
|
+
}
|
|
1619
|
+
};
|
|
1620
|
+
var pinRoomMessage = async (args) => {
|
|
1621
|
+
const { roomId, messageId, category, note } = args;
|
|
1622
|
+
try {
|
|
1623
|
+
const res = await routePost(`/api/rooms/${encodeURIComponent(roomId)}/messages/${encodeURIComponent(messageId)}/pin`, { category, note });
|
|
1624
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1625
|
+
} catch (error) {
|
|
1626
|
+
return err(`Room pin failed: ${extractApiError(error)}`);
|
|
1627
|
+
}
|
|
1628
|
+
};
|
|
1629
|
+
var unpinRoomMessage = async (args) => {
|
|
1630
|
+
const { roomId, messageId } = args;
|
|
1631
|
+
try {
|
|
1632
|
+
const res = await routeDelete(`/api/rooms/${encodeURIComponent(roomId)}/messages/${encodeURIComponent(messageId)}/pin`);
|
|
1633
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1634
|
+
} catch (error) {
|
|
1635
|
+
return err(`Room unpin failed: ${extractApiError(error)}`);
|
|
1636
|
+
}
|
|
1637
|
+
};
|
|
1638
|
+
var editRoomMessage = async (args) => {
|
|
1639
|
+
const { roomId, messageId, content } = args;
|
|
1640
|
+
try {
|
|
1641
|
+
const res = await routePatch(`/api/rooms/${encodeURIComponent(roomId)}/messages/${encodeURIComponent(messageId)}`, { content });
|
|
1642
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1643
|
+
} catch (error) {
|
|
1644
|
+
return err(`Room message edit failed: ${extractApiError(error)}`);
|
|
1645
|
+
}
|
|
1646
|
+
};
|
|
1647
|
+
var deleteRoomMessage = async (args) => {
|
|
1648
|
+
const { roomId, messageId } = args;
|
|
1649
|
+
try {
|
|
1650
|
+
const res = await routeDelete(`/api/rooms/${encodeURIComponent(roomId)}/messages/${encodeURIComponent(messageId)}`);
|
|
1651
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1652
|
+
} catch (error) {
|
|
1653
|
+
return err(`Room message delete failed: ${extractApiError(error)}`);
|
|
1654
|
+
}
|
|
1655
|
+
};
|
|
1656
|
+
var exportRoom = async (args) => {
|
|
1657
|
+
const { roomId } = args;
|
|
1658
|
+
try {
|
|
1659
|
+
const res = await routeGet(`/api/rooms/${encodeURIComponent(roomId)}/export`);
|
|
1660
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1661
|
+
} catch (error) {
|
|
1662
|
+
return err(`Room export failed: ${extractApiError(error)}`);
|
|
1663
|
+
}
|
|
1664
|
+
};
|
|
1665
|
+
var getPolymarketMarkets = async (args) => {
|
|
1666
|
+
const options = args;
|
|
1667
|
+
try {
|
|
1668
|
+
const res = await routeGet("/api/polymarket/markets", options);
|
|
1669
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1670
|
+
} catch (error) {
|
|
1671
|
+
return err(`Polymarket markets fetch failed: ${extractApiError(error)}`);
|
|
1672
|
+
}
|
|
1673
|
+
};
|
|
1674
|
+
var getPolymarketMarket = async (args) => {
|
|
1675
|
+
const { slug } = args;
|
|
1676
|
+
try {
|
|
1677
|
+
const res = await routeGet(`/api/polymarket/markets/${encodeURIComponent(slug)}`);
|
|
1678
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1679
|
+
} catch (error) {
|
|
1680
|
+
return err(`Polymarket market fetch failed: ${extractApiError(error)}`);
|
|
1681
|
+
}
|
|
1682
|
+
};
|
|
1683
|
+
var getPolymarketTrending = async (args) => {
|
|
1684
|
+
const { limit } = args;
|
|
1685
|
+
try {
|
|
1686
|
+
const res = await routeGet("/api/polymarket/trending", limit ? { limit } : void 0);
|
|
1687
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1688
|
+
} catch (error) {
|
|
1689
|
+
return err(`Polymarket trending fetch failed: ${extractApiError(error)}`);
|
|
1690
|
+
}
|
|
1691
|
+
};
|
|
1692
|
+
var getPolymarketSpikes = async (args) => {
|
|
1693
|
+
const { threshold, minVolume } = args;
|
|
1694
|
+
try {
|
|
1695
|
+
const params = {};
|
|
1696
|
+
if (threshold !== void 0) params.threshold = threshold;
|
|
1697
|
+
if (minVolume !== void 0) params.minVolume = minVolume;
|
|
1698
|
+
const res = await routeGet("/api/polymarket/spikes", params);
|
|
1699
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1700
|
+
} catch (error) {
|
|
1701
|
+
return err(`Polymarket spikes fetch failed: ${extractApiError(error)}`);
|
|
1702
|
+
}
|
|
1703
|
+
};
|
|
1704
|
+
var getPolymarketMovers = async (args) => {
|
|
1705
|
+
const { minChange } = args;
|
|
1706
|
+
try {
|
|
1707
|
+
const res = await routeGet("/api/polymarket/movers", minChange ? { minChange } : void 0);
|
|
1708
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1709
|
+
} catch (error) {
|
|
1710
|
+
return err(`Polymarket movers fetch failed: ${extractApiError(error)}`);
|
|
1711
|
+
}
|
|
1712
|
+
};
|
|
1713
|
+
var getPolymarketEvents = async (args) => {
|
|
1714
|
+
const options = args;
|
|
1715
|
+
try {
|
|
1716
|
+
const res = await routeGet("/api/polymarket/events", options);
|
|
1717
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1718
|
+
} catch (error) {
|
|
1719
|
+
return err(`Polymarket events fetch failed: ${extractApiError(error)}`);
|
|
1720
|
+
}
|
|
1721
|
+
};
|
|
1722
|
+
var getPolymarketLeaderboard = async (args) => {
|
|
1723
|
+
const { limit } = args;
|
|
1724
|
+
try {
|
|
1725
|
+
const res = await routeGet("/api/polymarket/leaderboard", limit ? { limit } : void 0);
|
|
1726
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1727
|
+
} catch (error) {
|
|
1728
|
+
return err(`Polymarket leaderboard fetch failed: ${extractApiError(error)}`);
|
|
1729
|
+
}
|
|
1730
|
+
};
|
|
1731
|
+
var getPolymarketTrader = async (args) => {
|
|
1732
|
+
const { address } = args;
|
|
1733
|
+
try {
|
|
1734
|
+
const res = await routeGet(`/api/polymarket/trader/${encodeURIComponent(address)}`);
|
|
1735
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1736
|
+
} catch (error) {
|
|
1737
|
+
return err(`Polymarket trader fetch failed: ${extractApiError(error)}`);
|
|
1738
|
+
}
|
|
1739
|
+
};
|
|
1740
|
+
var getPolymarketOrderBook = async (args) => {
|
|
1741
|
+
const { tokenId } = args;
|
|
1742
|
+
try {
|
|
1743
|
+
const res = await routeGet(`/api/polymarket/orderbook/${encodeURIComponent(tokenId)}`);
|
|
1744
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1745
|
+
} catch (error) {
|
|
1746
|
+
return err(`Polymarket order book fetch failed: ${extractApiError(error)}`);
|
|
1747
|
+
}
|
|
1748
|
+
};
|
|
1749
|
+
var getPolymarketTrades = async (args) => {
|
|
1750
|
+
const { conditionId, limit } = args;
|
|
1751
|
+
try {
|
|
1752
|
+
const res = await routeGet(`/api/polymarket/trades/${encodeURIComponent(conditionId)}`, limit ? { limit } : void 0);
|
|
1753
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1754
|
+
} catch (error) {
|
|
1755
|
+
return err(`Polymarket trades fetch failed: ${extractApiError(error)}`);
|
|
1756
|
+
}
|
|
1757
|
+
};
|
|
1758
|
+
var getPolymarketHistory = async (args) => {
|
|
1759
|
+
const { conditionId, interval, limit } = args;
|
|
1760
|
+
const params = {};
|
|
1761
|
+
if (interval) params.interval = interval;
|
|
1762
|
+
if (limit !== void 0) params.limit = limit;
|
|
1763
|
+
try {
|
|
1764
|
+
const res = await routeGet(`/api/polymarket/history/${encodeURIComponent(conditionId)}`, params);
|
|
1765
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1766
|
+
} catch (error) {
|
|
1767
|
+
return err(`Polymarket history fetch failed: ${extractApiError(error)}`);
|
|
1768
|
+
}
|
|
1769
|
+
};
|
|
1770
|
+
var getPortfolioTokens = async (args) => {
|
|
1771
|
+
const { address, chainId } = args;
|
|
1772
|
+
const { resolved: resolvedAddr, error: resolveErr } = await resolveAddressInput(address);
|
|
1773
|
+
if (resolveErr) return resolveErr;
|
|
401
1774
|
try {
|
|
402
|
-
const res = await api()
|
|
403
|
-
|
|
1775
|
+
const res = await routeGet(`/api/portfolio/${encodeURIComponent(resolvedAddr || address)}/tokens`, {
|
|
1776
|
+
chain: chainId
|
|
404
1777
|
});
|
|
405
1778
|
return ok(JSON.stringify(res.data, null, 2));
|
|
406
1779
|
} catch (error) {
|
|
407
|
-
|
|
408
|
-
|
|
1780
|
+
return err(`Portfolio tokens fetch failed: ${extractApiError(error)}`);
|
|
1781
|
+
}
|
|
1782
|
+
};
|
|
1783
|
+
var getPortfolioNfts = async (args) => {
|
|
1784
|
+
const { address, chainId } = args;
|
|
1785
|
+
const { resolved: resolvedAddr, error: resolveErr } = await resolveAddressInput(address);
|
|
1786
|
+
if (resolveErr) return resolveErr;
|
|
1787
|
+
try {
|
|
1788
|
+
const res = await routeGet(`/api/portfolio/${encodeURIComponent(resolvedAddr || address)}/nfts`, {
|
|
1789
|
+
chain: chainId
|
|
1790
|
+
});
|
|
1791
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1792
|
+
} catch (error) {
|
|
1793
|
+
return err(`Portfolio NFTs fetch failed: ${extractApiError(error)}`);
|
|
1794
|
+
}
|
|
1795
|
+
};
|
|
1796
|
+
var getPortfolioActivity = async (args) => {
|
|
1797
|
+
const { address, chainId } = args;
|
|
1798
|
+
const { resolved: resolvedAddr, error: resolveErr } = await resolveAddressInput(address);
|
|
1799
|
+
if (resolveErr) return resolveErr;
|
|
1800
|
+
try {
|
|
1801
|
+
const res = await routeGet(`/api/portfolio/${encodeURIComponent(resolvedAddr || address)}/activity`, {
|
|
1802
|
+
chain: chainId
|
|
1803
|
+
});
|
|
1804
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1805
|
+
} catch (error) {
|
|
1806
|
+
return err(`Portfolio activity fetch failed: ${extractApiError(error)}`);
|
|
1807
|
+
}
|
|
1808
|
+
};
|
|
1809
|
+
var getPortfolioStablecoins = async (args) => {
|
|
1810
|
+
const { address, chainId } = args;
|
|
1811
|
+
const { resolved: resolvedAddr, error: resolveErr } = await resolveAddressInput(address);
|
|
1812
|
+
if (resolveErr) return resolveErr;
|
|
1813
|
+
try {
|
|
1814
|
+
const res = await routeGet(`/api/portfolio/${encodeURIComponent(resolvedAddr || address)}/stablecoins`, {
|
|
1815
|
+
chain: chainId
|
|
1816
|
+
});
|
|
1817
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1818
|
+
} catch (error) {
|
|
1819
|
+
return err(`Portfolio stablecoins fetch failed: ${extractApiError(error)}`);
|
|
1820
|
+
}
|
|
1821
|
+
};
|
|
1822
|
+
var batchLookupContracts = async (args) => {
|
|
1823
|
+
const { addresses } = args;
|
|
1824
|
+
const list = addresses.split(",").map((a) => a.trim()).filter(Boolean);
|
|
1825
|
+
const resolved = [];
|
|
1826
|
+
for (const a of list) {
|
|
1827
|
+
const r = await resolveAddressInput(a);
|
|
1828
|
+
if (r.error) return r.error;
|
|
1829
|
+
resolved.push(r.resolved || a);
|
|
1830
|
+
}
|
|
1831
|
+
try {
|
|
1832
|
+
const res = await routePost("/api/contracts/batch", { addresses: resolved });
|
|
1833
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1834
|
+
} catch (error) {
|
|
1835
|
+
return err(`Batch contract lookup failed: ${extractApiError(error)}`);
|
|
1836
|
+
}
|
|
1837
|
+
};
|
|
1838
|
+
var refreshContracts = async () => {
|
|
1839
|
+
try {
|
|
1840
|
+
const res = await routePost("/api/contracts/refresh", {});
|
|
1841
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1842
|
+
} catch (error) {
|
|
1843
|
+
return err(`Contract refresh failed: ${extractApiError(error)}`);
|
|
1844
|
+
}
|
|
1845
|
+
};
|
|
1846
|
+
var fetchDuneInteractors = async (args) => {
|
|
1847
|
+
const { contractAddress, chain, limit, customApiKey } = args;
|
|
1848
|
+
const { resolved: resolvedAddr, error: resolveErr } = await resolveAddressInput(contractAddress);
|
|
1849
|
+
if (resolveErr) return resolveErr;
|
|
1850
|
+
try {
|
|
1851
|
+
const res = await routePost("/api/dune/fetch", {
|
|
1852
|
+
contractAddress: resolvedAddr || contractAddress,
|
|
1853
|
+
chain,
|
|
1854
|
+
...limit !== void 0 ? { limit } : {},
|
|
1855
|
+
...customApiKey ? { customApiKey } : {}
|
|
1856
|
+
});
|
|
1857
|
+
return ok(JSON.stringify(res.data, null, 2));
|
|
1858
|
+
} catch (error) {
|
|
1859
|
+
return err(`Dune interactor fetch failed: ${extractApiError(error)}`);
|
|
409
1860
|
}
|
|
410
1861
|
};
|
|
411
1862
|
function withLogging(toolName, handler) {
|
|
@@ -448,11 +1899,71 @@ var TOOL_HANDLERS = {
|
|
|
448
1899
|
compare_wallets: withLogging("compare_wallets", compareWallets),
|
|
449
1900
|
analyze_contract: withLogging("analyze_contract", analyzeContract),
|
|
450
1901
|
detect_sybil_clusters: withLogging("detect_sybil_clusters", detectSybilClusters),
|
|
1902
|
+
analyze_cex_flow: withLogging("analyze_cex_flow", analyzeCEXFlow),
|
|
1903
|
+
analyze_sybil_addresses: withLogging("analyze_sybil_addresses", analyzeSybilAddresses),
|
|
1904
|
+
resolve_name: withLogging("resolve_name", resolveName),
|
|
451
1905
|
get_portfolio: withLogging("get_portfolio", getPortfolio),
|
|
1906
|
+
get_portfolio_tokens: withLogging("get_portfolio_tokens", getPortfolioTokens),
|
|
1907
|
+
get_portfolio_nfts: withLogging("get_portfolio_nfts", getPortfolioNfts),
|
|
1908
|
+
get_portfolio_activity: withLogging("get_portfolio_activity", getPortfolioActivity),
|
|
1909
|
+
get_portfolio_stablecoins: withLogging("get_portfolio_stablecoins", getPortfolioStablecoins),
|
|
452
1910
|
get_transactions: withLogging("get_transactions", getTransactions),
|
|
1911
|
+
get_transaction_history: withLogging("get_transaction_history", getTransactionHistory),
|
|
1912
|
+
get_transaction_detail: withLogging("get_transaction_detail", getTransactionDetail),
|
|
453
1913
|
lookup_entity: withLogging("lookup_entity", lookupEntity),
|
|
1914
|
+
search_contracts: withLogging("search_contracts", searchContracts),
|
|
1915
|
+
lookup_contract: withLogging("lookup_contract", lookupContract),
|
|
1916
|
+
batch_lookup_contracts: withLogging("batch_lookup_contracts", batchLookupContracts),
|
|
1917
|
+
get_contract_stats: withLogging("get_contract_stats", getContractStats),
|
|
1918
|
+
refresh_contracts: withLogging("refresh_contracts", refreshContracts),
|
|
1919
|
+
search_tokens: withLogging("search_tokens", searchTokens),
|
|
1920
|
+
get_market_stats: withLogging("get_market_stats", getMarketStats),
|
|
1921
|
+
get_market_coins: withLogging("get_market_coins", getMarketCoins),
|
|
454
1922
|
get_gas_prices: withLogging("get_gas_prices", getGasPrices),
|
|
455
|
-
get_token_info: withLogging("get_token_info", getTokenInfo)
|
|
1923
|
+
get_token_info: withLogging("get_token_info", getTokenInfo),
|
|
1924
|
+
get_token_chart: withLogging("get_token_chart", getTokenChart),
|
|
1925
|
+
get_dexscreener_latest_profiles: withLogging("get_dexscreener_latest_profiles", getDexScreenerLatestProfiles),
|
|
1926
|
+
get_dexscreener_top_boosts: withLogging("get_dexscreener_top_boosts", getDexScreenerBoosts),
|
|
1927
|
+
search_dexscreener_pairs: withLogging("search_dexscreener_pairs", searchDexScreenerPairs),
|
|
1928
|
+
get_dexscreener_token_details: withLogging("get_dexscreener_token_details", getDexScreenerTokenDetails),
|
|
1929
|
+
get_dexscreener_token_pairs: withLogging("get_dexscreener_token_pairs", getDexScreenerTokenPairs),
|
|
1930
|
+
get_dexscreener_trending: withLogging("get_dexscreener_trending", getDexScreenerTrending),
|
|
1931
|
+
get_scan_history: withLogging("get_scan_history", getScanHistory),
|
|
1932
|
+
get_scan_history_stats: withLogging("get_scan_history_stats", getScanHistoryStats),
|
|
1933
|
+
save_scan_history_item: withLogging("save_scan_history_item", saveScanHistoryItem),
|
|
1934
|
+
sync_scan_history: withLogging("sync_scan_history", syncScanHistory),
|
|
1935
|
+
delete_scan_history_item: withLogging("delete_scan_history_item", deleteScanHistoryItem),
|
|
1936
|
+
clear_scan_history: withLogging("clear_scan_history", clearScanHistory),
|
|
1937
|
+
list_rooms: withLogging("list_rooms", listRooms),
|
|
1938
|
+
get_room_details: withLogging("get_room_details", getRoomDetails),
|
|
1939
|
+
create_room: withLogging("create_room", createRoom),
|
|
1940
|
+
update_room: withLogging("update_room", updateRoom),
|
|
1941
|
+
delete_room: withLogging("delete_room", deleteRoom),
|
|
1942
|
+
list_room_messages: withLogging("list_room_messages", listRoomMessages),
|
|
1943
|
+
send_room_message: withLogging("send_room_message", sendRoomMessage),
|
|
1944
|
+
join_room: withLogging("join_room", joinRoom),
|
|
1945
|
+
leave_room: withLogging("leave_room", leaveRoom),
|
|
1946
|
+
remove_room_member: withLogging("remove_room_member", removeRoomMember),
|
|
1947
|
+
promote_room_member: withLogging("promote_room_member", promoteRoomMember),
|
|
1948
|
+
create_room_invite: withLogging("create_room_invite", createRoomInvite),
|
|
1949
|
+
list_room_pins: withLogging("list_room_pins", listRoomPins),
|
|
1950
|
+
pin_room_message: withLogging("pin_room_message", pinRoomMessage),
|
|
1951
|
+
unpin_room_message: withLogging("unpin_room_message", unpinRoomMessage),
|
|
1952
|
+
edit_room_message: withLogging("edit_room_message", editRoomMessage),
|
|
1953
|
+
delete_room_message: withLogging("delete_room_message", deleteRoomMessage),
|
|
1954
|
+
export_room: withLogging("export_room", exportRoom),
|
|
1955
|
+
get_polymarket_markets: withLogging("get_polymarket_markets", getPolymarketMarkets),
|
|
1956
|
+
get_polymarket_market: withLogging("get_polymarket_market", getPolymarketMarket),
|
|
1957
|
+
get_polymarket_trending: withLogging("get_polymarket_trending", getPolymarketTrending),
|
|
1958
|
+
get_polymarket_spikes: withLogging("get_polymarket_spikes", getPolymarketSpikes),
|
|
1959
|
+
get_polymarket_movers: withLogging("get_polymarket_movers", getPolymarketMovers),
|
|
1960
|
+
get_polymarket_events: withLogging("get_polymarket_events", getPolymarketEvents),
|
|
1961
|
+
get_polymarket_leaderboard: withLogging("get_polymarket_leaderboard", getPolymarketLeaderboard),
|
|
1962
|
+
get_polymarket_trader: withLogging("get_polymarket_trader", getPolymarketTrader),
|
|
1963
|
+
get_polymarket_order_book: withLogging("get_polymarket_order_book", getPolymarketOrderBook),
|
|
1964
|
+
get_polymarket_trades: withLogging("get_polymarket_trades", getPolymarketTrades),
|
|
1965
|
+
get_polymarket_history: withLogging("get_polymarket_history", getPolymarketHistory),
|
|
1966
|
+
fetch_dune_interactors: withLogging("fetch_dune_interactors", fetchDuneInteractors)
|
|
456
1967
|
};
|
|
457
1968
|
|
|
458
1969
|
// src/mcp/mcpAuth.ts
|
|
@@ -513,8 +2024,8 @@ async function validateWithFirestore(rawKey) {
|
|
|
513
2024
|
}
|
|
514
2025
|
async function validateViaHttp(rawKey) {
|
|
515
2026
|
const API_URL = process.env.FUNDTRACER_API_URL || "https://api.fundtracer.xyz";
|
|
516
|
-
const { default:
|
|
517
|
-
const res = await
|
|
2027
|
+
const { default: fetch2 } = await import("node-fetch");
|
|
2028
|
+
const res = await fetch2(`${API_URL}/api/mcp/validate`, {
|
|
518
2029
|
method: "POST",
|
|
519
2030
|
headers: {
|
|
520
2031
|
"Content-Type": "application/json",
|
|
@@ -546,7 +2057,7 @@ function createFundTracerMcpServer(resolveContext, options = {}) {
|
|
|
546
2057
|
const logRegistrations = options.logRegistrations ?? true;
|
|
547
2058
|
const server = new McpServer({
|
|
548
2059
|
name: "FundTracer MCP",
|
|
549
|
-
version: "1.
|
|
2060
|
+
version: "1.1.0"
|
|
550
2061
|
});
|
|
551
2062
|
for (const toolDef of ALL_MCP_TOOLS) {
|
|
552
2063
|
const handler = TOOL_HANDLERS[toolDef.name];
|