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.
Files changed (67) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/LICENSE +1 -1
  3. package/README.md +97 -21
  4. package/dist/http.d.ts +1 -0
  5. package/dist/http.js +2 -0
  6. package/dist/index.js +93 -2
  7. package/dist/scripts/validate-catalog.d.ts +13 -0
  8. package/dist/scripts/validate-catalog.js +76 -0
  9. package/dist/tools/accounts.js +114 -204
  10. package/dist/tools/assets.js +109 -123
  11. package/dist/tools/auth.d.ts +2 -0
  12. package/dist/tools/auth.js +459 -0
  13. package/dist/tools/balance.js +28 -32
  14. package/dist/tools/blocks.js +68 -87
  15. package/dist/tools/config.js +18 -79
  16. package/dist/tools/das-extras.js +56 -41
  17. package/dist/tools/docs.js +12 -54
  18. package/dist/tools/enhanced-websockets.js +104 -74
  19. package/dist/tools/fees.js +42 -61
  20. package/dist/tools/guides.js +126 -515
  21. package/dist/tools/index.js +50 -2
  22. package/dist/tools/laserstream.js +107 -53
  23. package/dist/tools/network.js +47 -69
  24. package/dist/tools/plans.d.ts +21 -0
  25. package/dist/tools/plans.js +105 -246
  26. package/dist/tools/product-catalog.d.ts +10 -0
  27. package/dist/tools/product-catalog.js +123 -0
  28. package/dist/tools/recommend.d.ts +4 -0
  29. package/dist/tools/recommend.js +233 -0
  30. package/dist/tools/shared.js +8 -3
  31. package/dist/tools/solana-knowledge.d.ts +2 -0
  32. package/dist/tools/solana-knowledge.js +544 -0
  33. package/dist/tools/tokens.js +17 -18
  34. package/dist/tools/transactions.js +232 -302
  35. package/dist/tools/transfers.d.ts +2 -0
  36. package/dist/tools/transfers.js +270 -0
  37. package/dist/tools/wallet.js +175 -177
  38. package/dist/tools/webhooks.js +80 -82
  39. package/dist/types/transaction-types.d.ts +1 -1
  40. package/dist/types/transaction-types.js +2 -1
  41. package/dist/utils/config.d.ts +27 -0
  42. package/dist/utils/config.js +76 -0
  43. package/dist/utils/docs.d.ts +24 -0
  44. package/dist/utils/docs.js +72 -0
  45. package/dist/utils/errors.d.ts +32 -0
  46. package/dist/utils/errors.js +157 -0
  47. package/dist/utils/feedback.d.ts +16 -0
  48. package/dist/utils/feedback.js +87 -0
  49. package/dist/utils/formatters.d.ts +0 -1
  50. package/dist/utils/formatters.js +0 -3
  51. package/dist/utils/helius.d.ts +15 -5
  52. package/dist/utils/helius.js +52 -45
  53. package/dist/version.d.ts +1 -0
  54. package/dist/version.js +1 -0
  55. package/package.json +17 -7
  56. package/system-prompts/helius/claude.system.md +170 -0
  57. package/system-prompts/helius/full.md +2868 -0
  58. package/system-prompts/helius/openai.developer.md +170 -0
  59. package/system-prompts/helius-dflow/claude.system.md +290 -0
  60. package/system-prompts/helius-dflow/full.md +3647 -0
  61. package/system-prompts/helius-dflow/openai.developer.md +290 -0
  62. package/system-prompts/helius-phantom/claude.system.md +348 -0
  63. package/system-prompts/helius-phantom/full.md +5472 -0
  64. package/system-prompts/helius-phantom/openai.developer.md +348 -0
  65. package/system-prompts/svm/claude.system.md +174 -0
  66. package/system-prompts/svm/full.md +699 -0
  67. package/system-prompts/svm/openai.developer.md +174 -0
@@ -1,7 +1,8 @@
1
1
  import { z } from 'zod';
2
- import { getHeliusClient, hasApiKey, getRpcUrl } from '../utils/helius.js';
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 JSON-RPC) — supports native sortOrder asc/desc
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
- const response = await fetch(url, {
47
- method: 'POST',
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
- const assets = await helius.getAssetBatch({ ids: [...mints] });
83
- for (const asset of assets) {
84
- if (asset?.id)
85
- metadata.set(asset.id, asset);
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 transaction type (SWAP, TRANSFER, NFT_SALE, etc.), source program (Jupiter, Raydium, Magic Eden, etc.), SOL and token transfers with token names and proper decimal formatting, fees (in both SOL and lamports), timestamp, program IDs involved, and a plain-English description. Use showRaw=true to see all program IDs, instruction accounts, instruction data bytes, inner instructions, and auto-decoded ComputeBudget instructions (CU limit, priority fee, heap frame).', {
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
- content: [{
180
- type: 'text',
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
- programIds.forEach(pid => {
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
- outputLines.push('', '**SOL Transfers:**');
274
- tx.nativeTransfers.forEach((t) => {
275
- const from = t.fromUserAccount || 'unknown';
276
- const to = t.toUserAccount || 'unknown';
277
- outputLines.push(`- ${from} → ${to}: ${formatSol(t.amount)} (${t.amount.toLocaleString()} lamports)`);
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, 10);
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', 'Get transaction history for a Solana wallet. Supports three modes: "parsed" (default) returns human-readable decoded data with types, descriptions, actions, and fees. "signatures" returns a lightweight list of transaction signatures with slot/time/status. "raw" returns full raw data with advanced Helius filters (time/slot ranges, status, token accounts). All modes support sortOrder="asc" for finding wallet funding sources — no pagination needed. By default only successful transactions are shown; set status="any" or status="failed" to include failed ones.', {
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.enum(['parsed', 'signatures', 'raw']).optional().default('parsed').describe('"parsed" = decoded human-readable history (default), "signatures" = lightweight signature list, "raw" = full data with advanced Helius filters'),
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.enum(['asc', 'desc']).optional().default('desc').describe('"desc" = newest first (default), "asc" = oldest first (great for finding funding sources)'),
369
- before: z.string().optional().describe('[signatures mode, desc only] Cursor: start searching backwards from this signature'),
370
- until: z.string().optional().describe('[signatures mode, desc only] Cursor: search until this signature'),
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.enum(['signatures', 'full']).optional().default('signatures').describe('[raw mode] "signatures" for basic info (up to 1000), "full" for complete transaction data (up to 100)'),
373
- status: z.enum(['succeeded', 'failed', 'any']).optional().default('succeeded').describe('Filter by transaction status. Defaults to "succeeded" — set to "failed" or "any" to include failed transactions.'),
374
- tokenAccounts: z.enum(['none', 'balanceChanged', 'all']).optional().describe('"none" = only direct transactions, "balanceChanged" = include token transfers, "all" = all token account activity'),
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
- const response = await fetch(url, {
416
- method: 'POST',
417
- headers: { 'Content-Type': 'application/json' },
418
- body: JSON.stringify({
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 ? '[FAILED]' : '[OK]';
446
- const time = sig.blockTime ? new Date(sig.blockTime * 1000).toISOString() : 'N/A';
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
- const data = await fetchTransactionsForAddress(address, {
471
- transactionDetails: 'signatures',
472
- sortOrder,
473
- limit,
474
- ...(paginationToken && { paginationToken }),
475
- ...(Object.keys(filters).length > 0 && { filters })
476
- });
477
- if (data.error) {
478
- return {
479
- content: [{
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 ? '[FAILED]' : '[OK]';
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()} | Index: ${item.transactionIndex} | Time: ${time}`);
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
- else {
557
- const items = result.data;
558
- items.forEach((item) => {
559
- const sig = item.transaction.signatures[0];
560
- const txStatus = item.meta.err ? '[FAILED]' : '[OK]';
561
- const time = item.blockTime ? new Date(item.blockTime * 1000).toISOString() : 'N/A';
562
- const fee = formatSol(item.meta.fee);
563
- lines.push(`${txStatus} ${sig}`);
564
- lines.push(` Slot: ${item.slot.toLocaleString()} | Index: ${item.transactionIndex} | Time: ${time}`);
565
- lines.push(` Fee: ${fee}`);
566
- // Show balance changes
567
- const balanceChanges = [];
568
- item.meta.preBalances.forEach((pre, idx) => {
569
- const post = item.meta.postBalances[idx];
570
- const change = post - pre;
571
- if (change !== 0) {
572
- const changeStr = change > 0 ? `+${formatSol(change)}` : formatSol(change);
573
- balanceChanges.push(changeStr);
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
- if (balanceChanges.length > 0) {
577
- lines.push(` Balance changes: ${balanceChanges.slice(0, 3).join(', ')}${balanceChanges.length > 3 ? '...' : ''}`);
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
- if (result.paginationToken) {
582
- lines.push('', `**Next Page Token:** \`${result.paginationToken}\``);
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
- // Step 1: Use getTransactionsForAddress to get sorted signatures in one call
593
- const sigData = await fetchTransactionsForAddress(address, {
594
- transactionDetails: 'signatures',
595
- sortOrder,
596
- limit: Math.min(limit, 100),
597
- ...(paginationToken && { paginationToken }),
598
- ...(Object.keys(filters).length > 0 && { filters })
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 (enriched)
628
- allTransactions.push(...enriched);
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
- // Compact token transfer summary (for non-swap transactions)
662
- if (!tx.events?.swap && tx.tokenTransfers && tx.tokenTransfers.length > 0) {
663
- const transferParts = tx.tokenTransfers.slice(0, 3).map((t) => {
664
- const asset = tokenMetadata.get(t.mint);
665
- const symbol = asset?.token_info?.symbol || asset?.content?.metadata?.symbol || asset?.content?.metadata?.name || formatAddress(t.mint);
666
- const decimals = asset?.token_info?.decimals ?? t.decimals ?? 0;
667
- const amount = t.tokenAmount.toLocaleString(undefined, { maximumFractionDigits: Math.min(decimals || 4, 4) });
668
- return `${amount} ${symbol}`;
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
- const moreCount = tx.tokenTransfers.length > 3 ? ` +${tx.tokenTransfers.length - 3} more` : '';
671
- lines.push(` Transfers: ${transferParts.join(', ')}${moreCount}`);
563
+ if (enriched)
564
+ allTransactions.push(...enriched);
672
565
  }
673
- if (tx.fee) {
674
- lines.push(` Fee: ${formatSol(tx.fee)}`);
566
+ if (allTransactions.length === 0) {
567
+ return mcpText(`**Transaction History for ${formatAddress(address)}**\n\nNo transactions could be enriched.`);
675
568
  }
676
- lines.push('');
677
- });
678
- if (sigResult.paginationToken) {
679
- lines.push(`**Next Page Token:** \`${sigResult.paginationToken}\``);
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
  }