helius-mcp 0.5.3 → 1.2.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/CHANGELOG.md +52 -0
- package/LICENSE +1 -1
- package/README.md +97 -21
- package/dist/http.d.ts +1 -0
- package/dist/http.js +2 -0
- package/dist/index.js +93 -2
- package/dist/scripts/validate-catalog.d.ts +13 -0
- package/dist/scripts/validate-catalog.js +76 -0
- package/dist/tools/accounts.js +114 -204
- package/dist/tools/assets.js +109 -123
- package/dist/tools/auth.d.ts +2 -0
- package/dist/tools/auth.js +459 -0
- package/dist/tools/balance.js +28 -32
- package/dist/tools/blocks.js +68 -87
- package/dist/tools/config.js +18 -79
- package/dist/tools/das-extras.js +56 -41
- package/dist/tools/docs.js +12 -54
- package/dist/tools/enhanced-websockets.js +104 -74
- package/dist/tools/fees.js +42 -61
- package/dist/tools/guides.js +126 -515
- package/dist/tools/index.js +50 -2
- package/dist/tools/laserstream.js +107 -53
- package/dist/tools/network.js +47 -69
- package/dist/tools/plans.d.ts +21 -0
- package/dist/tools/plans.js +105 -246
- package/dist/tools/product-catalog.d.ts +10 -0
- package/dist/tools/product-catalog.js +123 -0
- package/dist/tools/recommend.d.ts +4 -0
- package/dist/tools/recommend.js +233 -0
- package/dist/tools/shared.js +8 -3
- package/dist/tools/solana-knowledge.d.ts +2 -0
- package/dist/tools/solana-knowledge.js +544 -0
- package/dist/tools/tokens.js +17 -18
- package/dist/tools/transactions.js +232 -302
- package/dist/tools/transfers.d.ts +2 -0
- package/dist/tools/transfers.js +270 -0
- package/dist/tools/wallet.js +175 -177
- package/dist/tools/webhooks.js +80 -82
- package/dist/types/transaction-types.d.ts +1 -1
- package/dist/types/transaction-types.js +2 -1
- package/dist/utils/config.d.ts +27 -0
- package/dist/utils/config.js +76 -0
- package/dist/utils/docs.d.ts +24 -0
- package/dist/utils/docs.js +72 -0
- package/dist/utils/errors.d.ts +32 -0
- package/dist/utils/errors.js +157 -0
- package/dist/utils/feedback.d.ts +16 -0
- package/dist/utils/feedback.js +87 -0
- package/dist/utils/formatters.d.ts +0 -1
- package/dist/utils/formatters.js +0 -3
- package/dist/utils/helius.d.ts +15 -5
- package/dist/utils/helius.js +52 -45
- package/dist/version.d.ts +1 -0
- package/dist/version.js +1 -0
- package/package.json +17 -7
- package/system-prompts/helius/claude.system.md +170 -0
- package/system-prompts/helius/full.md +2868 -0
- package/system-prompts/helius/openai.developer.md +170 -0
- package/system-prompts/helius-dflow/claude.system.md +290 -0
- package/system-prompts/helius-dflow/full.md +3647 -0
- package/system-prompts/helius-dflow/openai.developer.md +290 -0
- package/system-prompts/helius-phantom/claude.system.md +348 -0
- package/system-prompts/helius-phantom/full.md +5472 -0
- package/system-prompts/helius-phantom/openai.developer.md +348 -0
- package/system-prompts/svm/claude.system.md +174 -0
- package/system-prompts/svm/full.md +699 -0
- package/system-prompts/svm/openai.developer.md +174 -0
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { getHeliusClient, hasApiKey
|
|
2
|
+
import { getHeliusClient, hasApiKey } from '../utils/helius.js';
|
|
3
3
|
import { formatSol, formatAddress, formatTimestamp } from '../utils/formatters.js';
|
|
4
4
|
import { noApiKeyResponse } from './shared.js';
|
|
5
|
+
import { mcpText, validateEnum, handleToolError, http400Error } from '../utils/errors.js';
|
|
5
6
|
import bs58 from 'bs58';
|
|
6
7
|
// ─── Helpers ───
|
|
7
8
|
const COMPUTE_BUDGET_PROGRAM = 'ComputeBudget111111111111111111111111111111';
|
|
@@ -30,9 +31,8 @@ function decodeComputeBudget(dataBase58) {
|
|
|
30
31
|
return null;
|
|
31
32
|
}
|
|
32
33
|
}
|
|
33
|
-
// Shared helper: call getTransactionsForAddress (Helius
|
|
34
|
-
async function fetchTransactionsForAddress(address, params) {
|
|
35
|
-
const url = getRpcUrl();
|
|
34
|
+
// Shared helper: call getTransactionsForAddress via SDK (Helius custom RPC method)
|
|
35
|
+
async function fetchTransactionsForAddress(helius, address, params) {
|
|
36
36
|
const reqParams = {
|
|
37
37
|
transactionDetails: params.transactionDetails,
|
|
38
38
|
sortOrder: params.sortOrder,
|
|
@@ -43,17 +43,8 @@ async function fetchTransactionsForAddress(address, params) {
|
|
|
43
43
|
reqParams.paginationToken = params.paginationToken;
|
|
44
44
|
if (params.filters && Object.keys(params.filters).length > 0)
|
|
45
45
|
reqParams.filters = params.filters;
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
headers: { 'Content-Type': 'application/json' },
|
|
49
|
-
body: JSON.stringify({
|
|
50
|
-
jsonrpc: '2.0',
|
|
51
|
-
id: 'get-transactions-for-address',
|
|
52
|
-
method: 'getTransactionsForAddress',
|
|
53
|
-
params: [address, reqParams]
|
|
54
|
-
})
|
|
55
|
-
});
|
|
56
|
-
return await response.json();
|
|
46
|
+
// SDK returns { data, paginationToken } directly
|
|
47
|
+
return await helius.getTransactionsForAddress([address, reqParams]);
|
|
57
48
|
}
|
|
58
49
|
const BASE58_REGEX = /^[1-9A-HJ-NP-Za-km-z]{86,88}$/;
|
|
59
50
|
function validateSignatures(sigs) {
|
|
@@ -79,10 +70,21 @@ async function fetchTokenMetadata(helius, mints) {
|
|
|
79
70
|
const metadata = new Map();
|
|
80
71
|
if (mints.size === 0)
|
|
81
72
|
return metadata;
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
73
|
+
// Filter out empty/invalid mint strings before calling the API
|
|
74
|
+
const validMints = [...mints].filter(m => m && m.trim().length > 0);
|
|
75
|
+
if (validMints.length === 0)
|
|
76
|
+
return metadata;
|
|
77
|
+
try {
|
|
78
|
+
const assets = await helius.getAssetBatch({ ids: validMints });
|
|
79
|
+
for (const asset of assets) {
|
|
80
|
+
if (asset?.id)
|
|
81
|
+
metadata.set(asset.id, asset);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// If batch fails (e.g. invalid pubkeys in mint list), return empty metadata
|
|
86
|
+
// rather than crashing the entire tool call
|
|
87
|
+
return metadata;
|
|
86
88
|
}
|
|
87
89
|
// Enrich tokens missing symbol/name (batch response can have incomplete metadata)
|
|
88
90
|
const unknownMints = [...metadata.entries()]
|
|
@@ -149,8 +151,8 @@ function formatSwapSummary(swap, tokenMetadata) {
|
|
|
149
151
|
}
|
|
150
152
|
export function registerTransactionTools(server) {
|
|
151
153
|
// Parse Transactions (Enhanced API)
|
|
152
|
-
server.tool('parseTransactions', 'Parse one or more Solana transactions into human-readable format. Returns
|
|
153
|
-
signatures: z.array(z.string()).describe('Array of transaction signatures (base58 encoded). Can be 1 or more.'),
|
|
154
|
+
server.tool('parseTransactions', 'BEST FOR: decoding specific transaction(s) by signature. Parse one or more Solana transactions into human-readable format. Returns type, source, transfers, fees, timestamp, and description. Use showRaw=true for full instruction data. Credit cost: 100 credits/call.', {
|
|
155
|
+
signatures: z.array(z.string()).describe('Array of transaction signatures (base58 encoded, 86-88 characters). Can be 1 or more.'),
|
|
154
156
|
showRaw: z.boolean().optional().default(false).describe('Include raw instruction data: program IDs, accounts, inner instructions. Useful for debugging or tracing fund flows.')
|
|
155
157
|
}, async ({ signatures, showRaw }) => {
|
|
156
158
|
if (!hasApiKey())
|
|
@@ -161,12 +163,7 @@ export function registerTransactionTools(server) {
|
|
|
161
163
|
if (validSigs.length === 0) {
|
|
162
164
|
const lines = ['**Invalid Signature Format**', '', 'None of the provided signatures are valid base58-encoded transaction signatures (expected 86-88 characters).', ''];
|
|
163
165
|
invalidSigs.forEach(sig => lines.push(`- \`${sig}\``));
|
|
164
|
-
return
|
|
165
|
-
content: [{
|
|
166
|
-
type: 'text',
|
|
167
|
-
text: lines.join('\n')
|
|
168
|
-
}]
|
|
169
|
-
};
|
|
166
|
+
return mcpText(lines.join('\n'));
|
|
170
167
|
}
|
|
171
168
|
let transactions;
|
|
172
169
|
try {
|
|
@@ -175,12 +172,9 @@ export function registerTransactionTools(server) {
|
|
|
175
172
|
});
|
|
176
173
|
}
|
|
177
174
|
catch (err) {
|
|
178
|
-
return
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
text: `**Error fetching transactions:** ${err instanceof Error ? err.message : String(err)}`
|
|
182
|
-
}]
|
|
183
|
-
};
|
|
175
|
+
return handleToolError(err, 'Error fetching transactions', [
|
|
176
|
+
http400Error('Parse Transactions Error'),
|
|
177
|
+
]);
|
|
184
178
|
}
|
|
185
179
|
// Identify valid signatures that weren't returned (not found on-chain)
|
|
186
180
|
const returnedSigs = new Set((transactions || []).map(tx => tx.signature));
|
|
@@ -196,12 +190,7 @@ export function registerTransactionTools(server) {
|
|
|
196
190
|
lines.push('**Not found on Solana mainnet** (may be pending, very old, or nonexistent):');
|
|
197
191
|
notFoundSigs.forEach(sig => lines.push(`- \`${sig}\``));
|
|
198
192
|
}
|
|
199
|
-
return
|
|
200
|
-
content: [{
|
|
201
|
-
type: 'text',
|
|
202
|
-
text: lines.join('\n')
|
|
203
|
-
}]
|
|
204
|
-
};
|
|
193
|
+
return mcpText(lines.join('\n'));
|
|
205
194
|
}
|
|
206
195
|
const tokenMetadata = await fetchTokenMetadata(helius, collectMints(transactions));
|
|
207
196
|
const outputLines = [];
|
|
@@ -212,7 +201,7 @@ export function registerTransactionTools(server) {
|
|
|
212
201
|
const feeDisplay = tx.fee
|
|
213
202
|
? `${formatSol(tx.fee)} (${tx.fee.toLocaleString()} lamports)`
|
|
214
203
|
: 'N/A';
|
|
215
|
-
outputLines.push(`**Transaction: ${tx.signature}**`, '', `**Type:** ${tx.type || 'UNKNOWN'}`, `**Source:** ${tx.source || 'N/A'}`, `**Fee:** ${feeDisplay}`, `**Fee Payer:** ${tx.feePayer || 'N/A'}`, `**Timestamp:** ${tx.timestamp ? formatTimestamp(tx.timestamp) : 'N/A'}`, `**Status:** ${tx.transactionError ? 'Failed' : 'Success'}`);
|
|
204
|
+
outputLines.push(`**Transaction: ${tx.signature}**`, '', `**Type:** ${tx.type || 'UNKNOWN'}`, `**Source:** ${tx.source || 'N/A'}`, `**Fee:** ${feeDisplay}`, `**Fee Payer:** ${tx.feePayer || 'N/A'}`, `**Timestamp:** ${tx.timestamp ? formatTimestamp(tx.timestamp) : 'N/A'}`, `**Status:** ${tx.transactionError ? '❌ Failed' : '✅ Success'}`);
|
|
216
205
|
// Show error details for failed transactions
|
|
217
206
|
if (tx.transactionError) {
|
|
218
207
|
let errDetail;
|
|
@@ -232,7 +221,7 @@ export function registerTransactionTools(server) {
|
|
|
232
221
|
if (tx.description) {
|
|
233
222
|
outputLines.push('', `**Description:** ${tx.description}`);
|
|
234
223
|
}
|
|
235
|
-
// Extract unique program IDs from instructions
|
|
224
|
+
// Extract unique program IDs from instructions (only show when ≤5 to reduce noise)
|
|
236
225
|
if (tx.instructions && tx.instructions.length > 0) {
|
|
237
226
|
const programIds = new Set();
|
|
238
227
|
const collectProgramIds = (instructions) => {
|
|
@@ -244,9 +233,14 @@ export function registerTransactionTools(server) {
|
|
|
244
233
|
};
|
|
245
234
|
collectProgramIds(tx.instructions);
|
|
246
235
|
outputLines.push('', '**Programs Involved:**');
|
|
247
|
-
|
|
236
|
+
const pidArray = Array.from(programIds);
|
|
237
|
+
const shown = pidArray.slice(0, 5);
|
|
238
|
+
shown.forEach(pid => {
|
|
248
239
|
outputLines.push(`- ${pid}`);
|
|
249
240
|
});
|
|
241
|
+
if (programIds.size > 5) {
|
|
242
|
+
outputLines.push(`_(Showing 5 of ${programIds.size} programs)_`);
|
|
243
|
+
}
|
|
250
244
|
}
|
|
251
245
|
// Swap summary from events.swap
|
|
252
246
|
if (tx.events?.swap) {
|
|
@@ -269,13 +263,16 @@ export function registerTransactionTools(server) {
|
|
|
269
263
|
outputLines.push(`- ${from} → ${to}: ${formattedAmount} ${tokenDisplay}`);
|
|
270
264
|
});
|
|
271
265
|
}
|
|
266
|
+
// Skip SOL transfers when there's only 1 and a description already covers it
|
|
272
267
|
if (tx.nativeTransfers && tx.nativeTransfers.length > 0) {
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
268
|
+
if (!(tx.nativeTransfers.length === 1 && tx.description)) {
|
|
269
|
+
outputLines.push('', '**SOL Transfers:**');
|
|
270
|
+
tx.nativeTransfers.forEach((t) => {
|
|
271
|
+
const from = t.fromUserAccount || 'unknown';
|
|
272
|
+
const to = t.toUserAccount || 'unknown';
|
|
273
|
+
outputLines.push(`- ${from} → ${to}: ${formatSol(t.amount)} (${t.amount.toLocaleString()} lamports)`);
|
|
274
|
+
});
|
|
275
|
+
}
|
|
279
276
|
}
|
|
280
277
|
// Show raw instruction data if requested
|
|
281
278
|
if (showRaw && tx.instructions) {
|
|
@@ -328,7 +325,7 @@ export function registerTransactionTools(server) {
|
|
|
328
325
|
const significantChanges = tx.accountData
|
|
329
326
|
.filter(a => a.nativeBalanceChange !== 0)
|
|
330
327
|
.sort((a, b) => Math.abs(b.nativeBalanceChange) - Math.abs(a.nativeBalanceChange))
|
|
331
|
-
.slice(0,
|
|
328
|
+
.slice(0, 5);
|
|
332
329
|
if (significantChanges.length > 0) {
|
|
333
330
|
outputLines.push('', '**Account Balance Changes:**');
|
|
334
331
|
significantChanges.forEach(a => {
|
|
@@ -353,25 +350,20 @@ export function registerTransactionTools(server) {
|
|
|
353
350
|
}
|
|
354
351
|
}
|
|
355
352
|
const fullText = outputLines.join('\n');
|
|
356
|
-
return
|
|
357
|
-
content: [{
|
|
358
|
-
type: 'text',
|
|
359
|
-
text: truncateResponse(fullText)
|
|
360
|
-
}]
|
|
361
|
-
};
|
|
353
|
+
return mcpText(truncateResponse(fullText));
|
|
362
354
|
});
|
|
363
355
|
// Get Transaction History (unified: parsed, signatures, or raw mode)
|
|
364
|
-
server.tool('getTransactionHistory', '
|
|
356
|
+
server.tool('getTransactionHistory', 'BEST FOR: general-purpose transaction history. PREFER getWalletTransfers for sends/receives, getWalletHistory for balance deltas. Get transaction history for a wallet. Modes: "parsed" (~110 credits), "signatures" (~10 credits), "raw" (~10 credits). Supports sortOrder, status filters, and time/slot ranges.', {
|
|
365
357
|
address: z.string().describe('Solana wallet address (base58 encoded)'),
|
|
366
|
-
mode: z.
|
|
358
|
+
mode: z.string().optional().default('parsed').describe('"parsed" = decoded human-readable history (default), "signatures" = lightweight signature list, "raw" = full data with advanced Helius filters'),
|
|
367
359
|
limit: z.number().optional().default(10).describe('Number of results (1-1000 for signatures, 1-100 for full/parsed)'),
|
|
368
|
-
sortOrder: z.
|
|
369
|
-
before: z.string().optional().describe('[signatures mode, desc only] Cursor: start searching backwards from
|
|
370
|
-
until: z.string().optional().describe('[signatures mode, desc only] Cursor: search until
|
|
360
|
+
sortOrder: z.string().optional().default('desc').describe('"desc" = newest first (default), "asc" = oldest first (great for finding funding sources)'),
|
|
361
|
+
before: z.string().optional().describe('[signatures mode, desc only] Cursor: transaction signature (base58 encoded, 86-88 characters) to start searching backwards from'),
|
|
362
|
+
until: z.string().optional().describe('[signatures mode, desc only] Cursor: transaction signature (base58 encoded, 86-88 characters) to search until'),
|
|
371
363
|
paginationToken: z.string().optional().describe('Pagination token from previous response for fetching next page'),
|
|
372
|
-
transactionDetails: z.
|
|
373
|
-
status: z.
|
|
374
|
-
tokenAccounts: z.
|
|
364
|
+
transactionDetails: z.string().optional().default('signatures').describe('[raw mode] "signatures" for basic info (up to 1000), "full" for complete transaction data (up to 100)'),
|
|
365
|
+
status: z.string().optional().default('succeeded').describe('Filter by transaction status. Defaults to "succeeded" — set to "failed" or "any" to include failed transactions.'),
|
|
366
|
+
tokenAccounts: z.string().optional().describe('"none" = only direct transactions, "balanceChanged" = include token transfers, "all" = all token account activity'),
|
|
375
367
|
blockTimeGte: z.number().optional().describe('Filter: block time >= this Unix timestamp'),
|
|
376
368
|
blockTimeLte: z.number().optional().describe('Filter: block time <= this Unix timestamp'),
|
|
377
369
|
slotGte: z.number().optional().describe('Filter: slot >= this value'),
|
|
@@ -379,6 +371,25 @@ export function registerTransactionTools(server) {
|
|
|
379
371
|
}, async ({ address, mode, limit, sortOrder, before, until, paginationToken, transactionDetails, status, tokenAccounts, blockTimeGte, blockTimeLte, slotGte, slotLte }) => {
|
|
380
372
|
if (!hasApiKey())
|
|
381
373
|
return noApiKeyResponse();
|
|
374
|
+
const helius = getHeliusClient();
|
|
375
|
+
let err;
|
|
376
|
+
err = validateEnum(mode, ['parsed', 'signatures', 'raw'], 'Transaction History Error', 'mode');
|
|
377
|
+
if (err)
|
|
378
|
+
return err;
|
|
379
|
+
err = validateEnum(sortOrder, ['asc', 'desc'], 'Transaction History Error', 'sortOrder');
|
|
380
|
+
if (err)
|
|
381
|
+
return err;
|
|
382
|
+
err = validateEnum(transactionDetails, ['signatures', 'full'], 'Transaction History Error', 'transactionDetails');
|
|
383
|
+
if (err)
|
|
384
|
+
return err;
|
|
385
|
+
err = validateEnum(status, ['succeeded', 'failed', 'any'], 'Transaction History Error', 'status');
|
|
386
|
+
if (err)
|
|
387
|
+
return err;
|
|
388
|
+
if (tokenAccounts) {
|
|
389
|
+
err = validateEnum(tokenAccounts, ['none', 'balanceChanged', 'all'], 'Transaction History Error', 'tokenAccounts');
|
|
390
|
+
if (err)
|
|
391
|
+
return err;
|
|
392
|
+
}
|
|
382
393
|
const filters = {};
|
|
383
394
|
if (status)
|
|
384
395
|
filters.status = status;
|
|
@@ -406,284 +417,203 @@ export function registerTransactionTools(server) {
|
|
|
406
417
|
// Fast path: getSignaturesForAddress — cheap RPC call for latest sigs (desc only, no filters)
|
|
407
418
|
if (useQuickPath) {
|
|
408
419
|
try {
|
|
409
|
-
const url = getRpcUrl();
|
|
410
420
|
const rpcParams = { limit };
|
|
411
421
|
if (before)
|
|
412
422
|
rpcParams.before = before;
|
|
413
423
|
if (until)
|
|
414
424
|
rpcParams.until = until;
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
jsonrpc: '2.0',
|
|
420
|
-
id: 'get-signatures-for-address',
|
|
421
|
-
method: 'getSignaturesForAddress',
|
|
422
|
-
params: [address, rpcParams]
|
|
423
|
-
})
|
|
424
|
-
});
|
|
425
|
-
const json = await response.json();
|
|
426
|
-
if (json.error) {
|
|
427
|
-
return {
|
|
428
|
-
content: [{
|
|
429
|
-
type: 'text',
|
|
430
|
-
text: `Error fetching signatures: ${json.error.message}`
|
|
431
|
-
}]
|
|
432
|
-
};
|
|
433
|
-
}
|
|
434
|
-
const sigs = json.result || [];
|
|
435
|
-
if (sigs.length === 0) {
|
|
436
|
-
return {
|
|
437
|
-
content: [{
|
|
438
|
-
type: 'text',
|
|
439
|
-
text: `**Signatures for ${formatAddress(address)}**\n\nNo signatures found.`
|
|
440
|
-
}]
|
|
441
|
-
};
|
|
425
|
+
// Kit returns bigint for slot/blockTime fields
|
|
426
|
+
const sigs = await helius.getSignaturesForAddress(address, rpcParams);
|
|
427
|
+
if (!sigs || sigs.length === 0) {
|
|
428
|
+
return mcpText(`**Signatures for ${formatAddress(address)}**\n\nNo signatures found.`);
|
|
442
429
|
}
|
|
443
430
|
const lines = [`**Signatures for ${formatAddress(address)}** (${sigs.length} results, newest first${statusNote})`, ''];
|
|
444
431
|
sigs.forEach((sig) => {
|
|
445
|
-
const sigStatus = sig.err ? '
|
|
446
|
-
const
|
|
432
|
+
const sigStatus = sig.err ? '❌' : '✅';
|
|
433
|
+
const blockTime = sig.blockTime != null ? Number(sig.blockTime) : null;
|
|
434
|
+
const time = blockTime ? new Date(blockTime * 1000).toISOString() : 'N/A';
|
|
447
435
|
lines.push(`${sigStatus} ${sig.signature}`);
|
|
448
|
-
lines.push(` Slot: ${sig.slot.toLocaleString()} | Time: ${time}`);
|
|
436
|
+
lines.push(` Slot: ${Number(sig.slot).toLocaleString()} | Time: ${time}`);
|
|
449
437
|
if (sig.memo) {
|
|
450
438
|
lines.push(` Memo: ${sig.memo}`);
|
|
451
439
|
}
|
|
452
440
|
});
|
|
453
|
-
return
|
|
454
|
-
content: [{
|
|
455
|
-
type: 'text',
|
|
456
|
-
text: lines.join('\n')
|
|
457
|
-
}]
|
|
458
|
-
};
|
|
441
|
+
return mcpText(lines.join('\n'));
|
|
459
442
|
}
|
|
460
443
|
catch (err) {
|
|
461
|
-
return
|
|
462
|
-
content: [{
|
|
463
|
-
type: 'text',
|
|
464
|
-
text: `Error fetching signatures: ${err instanceof Error ? err.message : String(err)}`
|
|
465
|
-
}]
|
|
466
|
-
};
|
|
444
|
+
return handleToolError(err, 'Error fetching signatures');
|
|
467
445
|
}
|
|
468
446
|
}
|
|
469
447
|
// Full path: getTransactionsForAddress — needed for asc sort, filters, or paginationToken
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
type: 'text',
|
|
481
|
-
text: `**Error**\n\n${data.error.message}`
|
|
482
|
-
}]
|
|
483
|
-
};
|
|
484
|
-
}
|
|
485
|
-
const result = data.result;
|
|
486
|
-
if (!result || !result.data || result.data.length === 0) {
|
|
487
|
-
return {
|
|
488
|
-
content: [{
|
|
489
|
-
type: 'text',
|
|
490
|
-
text: `**Signatures for ${formatAddress(address)}**\n\nNo signatures found.`
|
|
491
|
-
}]
|
|
492
|
-
};
|
|
493
|
-
}
|
|
494
|
-
const items = result.data;
|
|
495
|
-
const orderLabel = sortOrder === 'asc' ? 'oldest first' : 'newest first';
|
|
496
|
-
const lines = [`**Signatures for ${formatAddress(address)}** (${items.length} results, ${orderLabel}${statusNote})`, ''];
|
|
497
|
-
items.forEach((item) => {
|
|
498
|
-
const txStatus = item.err ? '[FAILED]' : '[OK]';
|
|
499
|
-
const time = item.blockTime ? new Date(item.blockTime * 1000).toISOString() : 'N/A';
|
|
500
|
-
lines.push(`${txStatus} ${item.signature}`);
|
|
501
|
-
lines.push(` Slot: ${item.slot.toLocaleString()} | Time: ${time}`);
|
|
502
|
-
if (item.memo) {
|
|
503
|
-
lines.push(` Memo: ${item.memo}`);
|
|
448
|
+
try {
|
|
449
|
+
const result = await fetchTransactionsForAddress(helius, address, {
|
|
450
|
+
transactionDetails: 'signatures',
|
|
451
|
+
sortOrder,
|
|
452
|
+
limit,
|
|
453
|
+
...(paginationToken && { paginationToken }),
|
|
454
|
+
...(Object.keys(filters).length > 0 && { filters })
|
|
455
|
+
});
|
|
456
|
+
if (!result || !result.data || result.data.length === 0) {
|
|
457
|
+
return mcpText(`**Signatures for ${formatAddress(address)}**\n\nNo signatures found.`);
|
|
504
458
|
}
|
|
505
|
-
});
|
|
506
|
-
if (result.paginationToken) {
|
|
507
|
-
lines.push('', `**Next Page Token:** \`${result.paginationToken}\``);
|
|
508
|
-
}
|
|
509
|
-
return {
|
|
510
|
-
content: [{
|
|
511
|
-
type: 'text',
|
|
512
|
-
text: lines.join('\n')
|
|
513
|
-
}]
|
|
514
|
-
};
|
|
515
|
-
}
|
|
516
|
-
// ─── RAW MODE ───
|
|
517
|
-
if (mode === 'raw') {
|
|
518
|
-
const data = await fetchTransactionsForAddress(address, {
|
|
519
|
-
transactionDetails: transactionDetails,
|
|
520
|
-
sortOrder,
|
|
521
|
-
limit,
|
|
522
|
-
...(paginationToken && { paginationToken }),
|
|
523
|
-
...(Object.keys(filters).length > 0 && { filters })
|
|
524
|
-
});
|
|
525
|
-
if (data.error) {
|
|
526
|
-
return {
|
|
527
|
-
content: [{
|
|
528
|
-
type: 'text',
|
|
529
|
-
text: `**Error**\n\n${data.error.message}`
|
|
530
|
-
}]
|
|
531
|
-
};
|
|
532
|
-
}
|
|
533
|
-
const result = data.result;
|
|
534
|
-
if (!result || !result.data || result.data.length === 0) {
|
|
535
|
-
return {
|
|
536
|
-
content: [{
|
|
537
|
-
type: 'text',
|
|
538
|
-
text: `**Transactions for ${formatAddress(address)}**\n\nNo transactions found.`
|
|
539
|
-
}]
|
|
540
|
-
};
|
|
541
|
-
}
|
|
542
|
-
const orderLabel = sortOrder === 'asc' ? 'oldest first' : 'newest first';
|
|
543
|
-
const lines = [`**Transactions for ${formatAddress(address)}** (${result.data.length} results, ${orderLabel}${statusNote})`, ''];
|
|
544
|
-
if (transactionDetails === 'signatures') {
|
|
545
459
|
const items = result.data;
|
|
460
|
+
const orderLabel = sortOrder === 'asc' ? 'oldest first' : 'newest first';
|
|
461
|
+
const lines = [`**Signatures for ${formatAddress(address)}** (${items.length} results, ${orderLabel}${statusNote})`, ''];
|
|
546
462
|
items.forEach((item) => {
|
|
547
|
-
const txStatus = item.err ? '
|
|
463
|
+
const txStatus = item.err ? '❌' : '✅';
|
|
548
464
|
const time = item.blockTime ? new Date(item.blockTime * 1000).toISOString() : 'N/A';
|
|
549
465
|
lines.push(`${txStatus} ${item.signature}`);
|
|
550
|
-
lines.push(` Slot: ${item.slot.toLocaleString()} |
|
|
466
|
+
lines.push(` Slot: ${item.slot.toLocaleString()} | Time: ${time}`);
|
|
551
467
|
if (item.memo) {
|
|
552
468
|
lines.push(` Memo: ${item.memo}`);
|
|
553
469
|
}
|
|
554
470
|
});
|
|
471
|
+
if (result.paginationToken) {
|
|
472
|
+
lines.push('', `**Next Page Token:** \`${result.paginationToken}\``);
|
|
473
|
+
}
|
|
474
|
+
return mcpText(lines.join('\n'));
|
|
555
475
|
}
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
476
|
+
catch (err) {
|
|
477
|
+
return handleToolError(err, 'Error fetching signatures');
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
// ─── RAW MODE ───
|
|
481
|
+
if (mode === 'raw') {
|
|
482
|
+
try {
|
|
483
|
+
const result = await fetchTransactionsForAddress(helius, address, {
|
|
484
|
+
transactionDetails: transactionDetails,
|
|
485
|
+
sortOrder,
|
|
486
|
+
limit,
|
|
487
|
+
...(paginationToken && { paginationToken }),
|
|
488
|
+
...(Object.keys(filters).length > 0 && { filters })
|
|
489
|
+
});
|
|
490
|
+
if (!result || !result.data || result.data.length === 0) {
|
|
491
|
+
return mcpText(`**Transactions for ${formatAddress(address)}**\n\nNo transactions found.`);
|
|
492
|
+
}
|
|
493
|
+
const orderLabel = sortOrder === 'asc' ? 'oldest first' : 'newest first';
|
|
494
|
+
const lines = [`**Transactions for ${formatAddress(address)}** (${result.data.length} results, ${orderLabel}${statusNote})`, ''];
|
|
495
|
+
if (transactionDetails === 'signatures') {
|
|
496
|
+
const items = result.data;
|
|
497
|
+
items.forEach((item) => {
|
|
498
|
+
const txStatus = item.err ? '❌' : '✅';
|
|
499
|
+
const time = item.blockTime ? new Date(item.blockTime * 1000).toISOString() : 'N/A';
|
|
500
|
+
lines.push(`${txStatus} ${item.signature}`);
|
|
501
|
+
lines.push(` Slot: ${item.slot.toLocaleString()} | Index: ${item.transactionIndex} | Time: ${time}`);
|
|
502
|
+
if (item.memo) {
|
|
503
|
+
lines.push(` Memo: ${item.memo}`);
|
|
574
504
|
}
|
|
575
505
|
});
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
506
|
+
}
|
|
507
|
+
else {
|
|
508
|
+
const items = result.data;
|
|
509
|
+
items.forEach((item) => {
|
|
510
|
+
const sig = item.transaction.signatures[0];
|
|
511
|
+
const txStatus = item.meta.err ? '❌' : '✅';
|
|
512
|
+
const time = item.blockTime ? new Date(item.blockTime * 1000).toISOString() : 'N/A';
|
|
513
|
+
const fee = formatSol(item.meta.fee);
|
|
514
|
+
lines.push(`${txStatus} ${sig}`);
|
|
515
|
+
lines.push(` Slot: ${item.slot.toLocaleString()} | Index: ${item.transactionIndex} | Time: ${time}`);
|
|
516
|
+
lines.push(` Fee: ${fee}`);
|
|
517
|
+
// Show balance changes
|
|
518
|
+
const balanceChanges = [];
|
|
519
|
+
item.meta.preBalances.forEach((pre, idx) => {
|
|
520
|
+
const post = item.meta.postBalances[idx];
|
|
521
|
+
const change = post - pre;
|
|
522
|
+
if (change !== 0) {
|
|
523
|
+
const changeStr = change > 0 ? `+${formatSol(change)}` : formatSol(change);
|
|
524
|
+
balanceChanges.push(changeStr);
|
|
525
|
+
}
|
|
526
|
+
});
|
|
527
|
+
if (balanceChanges.length > 0) {
|
|
528
|
+
lines.push(` Balance changes: ${balanceChanges.slice(0, 3).join(', ')}${balanceChanges.length > 3 ? '...' : ''}`);
|
|
529
|
+
}
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
if (result.paginationToken) {
|
|
533
|
+
lines.push('', `**Next Page Token:** \`${result.paginationToken}\``);
|
|
534
|
+
}
|
|
535
|
+
return mcpText(lines.join('\n'));
|
|
580
536
|
}
|
|
581
|
-
|
|
582
|
-
|
|
537
|
+
catch (err) {
|
|
538
|
+
return handleToolError(err, 'Error fetching transactions');
|
|
583
539
|
}
|
|
584
|
-
return {
|
|
585
|
-
content: [{
|
|
586
|
-
type: 'text',
|
|
587
|
-
text: lines.join('\n')
|
|
588
|
-
}]
|
|
589
|
-
};
|
|
590
540
|
}
|
|
591
541
|
// ─── PARSED MODE (default) ───
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
if (sigData.error) {
|
|
601
|
-
return {
|
|
602
|
-
content: [{
|
|
603
|
-
type: 'text',
|
|
604
|
-
text: `**Error**\n\n${sigData.error.message}`
|
|
605
|
-
}]
|
|
606
|
-
};
|
|
607
|
-
}
|
|
608
|
-
const sigResult = sigData.result;
|
|
609
|
-
if (!sigResult || !sigResult.data || sigResult.data.length === 0) {
|
|
610
|
-
return {
|
|
611
|
-
content: [{
|
|
612
|
-
type: 'text',
|
|
613
|
-
text: `**Transaction History for ${formatAddress(address)}**\n\nNo transactions found.`
|
|
614
|
-
}]
|
|
615
|
-
};
|
|
616
|
-
}
|
|
617
|
-
const sortedSigs = sigResult.data.map(item => item.signature);
|
|
618
|
-
// Step 2: Send sorted signatures to Enhanced API for rich data
|
|
619
|
-
const helius = getHeliusClient();
|
|
620
|
-
// Batch in chunks of 100 (Enhanced API limit)
|
|
621
|
-
const allTransactions = [];
|
|
622
|
-
for (let i = 0; i < sortedSigs.length; i += 100) {
|
|
623
|
-
const batch = sortedSigs.slice(i, i + 100);
|
|
624
|
-
const enriched = await helius.enhanced.getTransactions({
|
|
625
|
-
transactions: batch
|
|
542
|
+
try {
|
|
543
|
+
// Step 1: Use getTransactionsForAddress to get sorted signatures in one call
|
|
544
|
+
const sigResult = await fetchTransactionsForAddress(helius, address, {
|
|
545
|
+
transactionDetails: 'signatures',
|
|
546
|
+
sortOrder,
|
|
547
|
+
limit: Math.min(limit, 100),
|
|
548
|
+
...(paginationToken && { paginationToken }),
|
|
549
|
+
...(Object.keys(filters).length > 0 && { filters })
|
|
626
550
|
});
|
|
627
|
-
if (
|
|
628
|
-
|
|
629
|
-
}
|
|
630
|
-
if (allTransactions.length === 0) {
|
|
631
|
-
return {
|
|
632
|
-
content: [{
|
|
633
|
-
type: 'text',
|
|
634
|
-
text: `**Transaction History for ${formatAddress(address)}**\n\nNo transactions could be enriched.`
|
|
635
|
-
}]
|
|
636
|
-
};
|
|
637
|
-
}
|
|
638
|
-
// Re-sort to match original order (Enhanced API may return in different order)
|
|
639
|
-
const sigOrder = new Map(sortedSigs.map((sig, idx) => [sig, idx]));
|
|
640
|
-
allTransactions.sort((a, b) => (sigOrder.get(a.signature) ?? 999) - (sigOrder.get(b.signature) ?? 999));
|
|
641
|
-
const tokenMetadata = await fetchTokenMetadata(helius, collectMints(allTransactions));
|
|
642
|
-
const orderLabel = sortOrder === 'asc' ? 'oldest first' : 'newest first';
|
|
643
|
-
const lines = [`**Transaction History for ${formatAddress(address)}** (${allTransactions.length} transactions, ${orderLabel}${statusNote})`, ''];
|
|
644
|
-
allTransactions.forEach((tx) => {
|
|
645
|
-
const time = tx.timestamp ? formatTimestamp(tx.timestamp) : 'N/A';
|
|
646
|
-
const txStatus = tx.transactionError ? '[FAILED]' : '[OK]';
|
|
647
|
-
const type = tx.type || 'UNKNOWN';
|
|
648
|
-
const source = tx.source || '';
|
|
649
|
-
lines.push(`${txStatus} **${type}**${source ? ` (${source})` : ''} - ${time}`);
|
|
650
|
-
lines.push(` Sig: \`${tx.signature}\``);
|
|
651
|
-
if (tx.description) {
|
|
652
|
-
lines.push(` ${tx.description}`);
|
|
653
|
-
}
|
|
654
|
-
// Swap summary
|
|
655
|
-
if (tx.events?.swap) {
|
|
656
|
-
const swapSummary = formatSwapSummary(tx.events.swap, tokenMetadata);
|
|
657
|
-
if (swapSummary) {
|
|
658
|
-
lines.push(` Swap: ${swapSummary}`);
|
|
659
|
-
}
|
|
551
|
+
if (!sigResult || !sigResult.data || sigResult.data.length === 0) {
|
|
552
|
+
return mcpText(`**Transaction History for ${formatAddress(address)}**\n\nNo transactions found.`);
|
|
660
553
|
}
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
554
|
+
const sortedSigs = sigResult.data.map(item => item.signature);
|
|
555
|
+
// Step 2: Send sorted signatures to Enhanced API for rich data
|
|
556
|
+
// Batch in chunks of 100 (Enhanced API limit)
|
|
557
|
+
const allTransactions = [];
|
|
558
|
+
for (let i = 0; i < sortedSigs.length; i += 100) {
|
|
559
|
+
const batch = sortedSigs.slice(i, i + 100);
|
|
560
|
+
const enriched = await helius.enhanced.getTransactions({
|
|
561
|
+
transactions: batch
|
|
669
562
|
});
|
|
670
|
-
|
|
671
|
-
|
|
563
|
+
if (enriched)
|
|
564
|
+
allTransactions.push(...enriched);
|
|
672
565
|
}
|
|
673
|
-
if (
|
|
674
|
-
|
|
566
|
+
if (allTransactions.length === 0) {
|
|
567
|
+
return mcpText(`**Transaction History for ${formatAddress(address)}**\n\nNo transactions could be enriched.`);
|
|
675
568
|
}
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
569
|
+
// Re-sort to match original order (Enhanced API may return in different order)
|
|
570
|
+
const sigOrder = new Map(sortedSigs.map((sig, idx) => [sig, idx]));
|
|
571
|
+
allTransactions.sort((a, b) => (sigOrder.get(a.signature) ?? 999) - (sigOrder.get(b.signature) ?? 999));
|
|
572
|
+
const tokenMetadata = await fetchTokenMetadata(helius, collectMints(allTransactions));
|
|
573
|
+
const orderLabel = sortOrder === 'asc' ? 'oldest first' : 'newest first';
|
|
574
|
+
const lines = [`**Transaction History for ${formatAddress(address)}** (${allTransactions.length} transactions, ${orderLabel}${statusNote})`, ''];
|
|
575
|
+
allTransactions.forEach((tx) => {
|
|
576
|
+
const time = tx.timestamp ? formatTimestamp(tx.timestamp) : 'N/A';
|
|
577
|
+
const txStatus = tx.transactionError ? '❌' : '✅';
|
|
578
|
+
const type = tx.type || 'UNKNOWN';
|
|
579
|
+
const source = tx.source || '';
|
|
580
|
+
lines.push(`${txStatus} **${type}**${source ? ` (${source})` : ''} - ${time}`);
|
|
581
|
+
lines.push(` Sig: \`${tx.signature}\``);
|
|
582
|
+
if (tx.description) {
|
|
583
|
+
lines.push(` ${tx.description}`);
|
|
584
|
+
}
|
|
585
|
+
// Swap summary
|
|
586
|
+
if (tx.events?.swap) {
|
|
587
|
+
const swapSummary = formatSwapSummary(tx.events.swap, tokenMetadata);
|
|
588
|
+
if (swapSummary) {
|
|
589
|
+
lines.push(` Swap: ${swapSummary}`);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
// Compact token transfer summary (for non-swap transactions)
|
|
593
|
+
if (!tx.events?.swap && tx.tokenTransfers && tx.tokenTransfers.length > 0) {
|
|
594
|
+
const transferParts = tx.tokenTransfers.slice(0, 3).map((t) => {
|
|
595
|
+
const asset = tokenMetadata.get(t.mint);
|
|
596
|
+
const symbol = asset?.token_info?.symbol || asset?.content?.metadata?.symbol || asset?.content?.metadata?.name || formatAddress(t.mint);
|
|
597
|
+
const decimals = asset?.token_info?.decimals ?? t.decimals ?? 0;
|
|
598
|
+
const amount = t.tokenAmount.toLocaleString(undefined, { maximumFractionDigits: Math.min(decimals || 4, 4) });
|
|
599
|
+
return `${amount} ${symbol}`;
|
|
600
|
+
});
|
|
601
|
+
const moreCount = tx.tokenTransfers.length > 3 ? ` +${tx.tokenTransfers.length - 3} more` : '';
|
|
602
|
+
lines.push(` Transfers: ${transferParts.join(', ')}${moreCount}`);
|
|
603
|
+
}
|
|
604
|
+
if (tx.fee) {
|
|
605
|
+
lines.push(` Fee: ${formatSol(tx.fee)}`);
|
|
606
|
+
}
|
|
607
|
+
lines.push('');
|
|
608
|
+
});
|
|
609
|
+
if (sigResult.paginationToken) {
|
|
610
|
+
lines.push(`**Next Page Token:** \`${sigResult.paginationToken}\``);
|
|
611
|
+
}
|
|
612
|
+
const fullText = lines.join('\n');
|
|
613
|
+
return mcpText(truncateResponse(fullText));
|
|
614
|
+
}
|
|
615
|
+
catch (err) {
|
|
616
|
+
return handleToolError(err, 'Error fetching transaction history');
|
|
680
617
|
}
|
|
681
|
-
const fullText = lines.join('\n');
|
|
682
|
-
return {
|
|
683
|
-
content: [{
|
|
684
|
-
type: 'text',
|
|
685
|
-
text: truncateResponse(fullText)
|
|
686
|
-
}]
|
|
687
|
-
};
|
|
688
618
|
});
|
|
689
619
|
}
|