# Directory Structure
```
├── .gitignore
├── claude_desktop_config.json
├── package.json
├── pnpm-lock.yaml
├── README.md
├── src
│ ├── actions
│ │ ├── index.ts
│ │ ├── priorityFee
│ │ │ └── getPriorityFeeEstimate.ts
│ │ ├── security
│ │ │ └── getSecurityTxt.ts
│ │ ├── transaction
│ │ │ └── getTransactionHistory.ts
│ │ └── validator
│ │ └── getValidatorInfo.ts
│ ├── index.ts
│ ├── tests
│ │ ├── test-priority-fee.ts
│ │ └── test-transaction-history.ts
│ ├── tools
│ │ ├── priorityFee.ts
│ │ ├── security.ts
│ │ ├── transaction.ts
│ │ └── validator.ts
│ └── types
│ └── action.ts
└── tsconfig.json
```
# Files
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
1 | .env
2 | node_modules
3 | build
4 |
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
1 | # Solana Agent Kit MCP Server
2 |
3 | [](https://www.npmjs.com/package/solana-mpc)
4 | [](https://opensource.org/licenses/ISC)
5 |
6 | A Model Context Protocol (MCP) server that provides onchain tools for Claude AI, allowing it to interact with the Solana blockchain through a standardized interface. This implementation is based on the Solana Agent Kit and enables AI agents to perform blockchain operations seamlessly.
7 |
8 | # DEMO VIDEO
9 | https://www.youtube.com/watch?v=VbfSzFuIzn8
10 |
11 | # Actions
12 | ### GET_VALIDATOR_INFO
13 | Retrieves detailed information about Solana validators
14 | Shows stake amounts, commission rates, and performance metrics
15 |
16 | ### GET_PRIORITY_FEE_ESTIMATE
17 | Estimates optimal transaction fees based on current network conditions
18 | Provides different fee tiers (low, medium, high) with expected confirmation times
19 |
20 | ### GET_TRANSACTION_HISTORY
21 | Fetches transaction history for any Solana wallet or token account
22 | Supports filtering by transaction types and pagination
23 |
24 | ### GET_SECURITY_TXT
25 | Extracts security contact information from Solana programs
26 | Helps users find proper channels for reporting vulnerabilities
27 | Integrated Functions
28 | Your MCP server also includes core Solana functionality for:
29 |
30 | Asset management (GET_ASSET, MINT_NFT)
31 | Token operations (DEPLOY_TOKEN, TRANSFER, TRADE)
32 | Network information (GET_TPS, BALANCE)
33 | Utility functions (REQUEST_FUNDS, REGISTER_DOMAIN)
34 | These functions together provide a comprehensive interface for interacting with the Solana blockchain through a unified MCP server.
35 |
36 |
```
--------------------------------------------------------------------------------
/src/types/action.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { z } from "zod";
2 | import { SolanaAgentKit } from "solana-agent-kit";
3 |
4 | export interface Action {
5 | name: string;
6 | similes: string[];
7 | description: string;
8 | examples: any[];
9 | schema: z.ZodObject<any>;
10 | handler: (agent: SolanaAgentKit, input: Record<string, any>) => Promise<any>;
11 | }
12 |
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "compilerOptions": {
3 | "target": "ES2022",
4 | "module": "Node16",
5 | "moduleResolution": "Node16",
6 | "outDir": "./build",
7 | "rootDir": "./src",
8 | "strict": true,
9 | "esModuleInterop": true,
10 | "skipLibCheck": true,
11 | "forceConsistentCasingInFileNames": true
12 | },
13 | "include": ["src/**/*"],
14 | "exclude": ["node_modules"]
15 | }
```
--------------------------------------------------------------------------------
/claude_desktop_config.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "mcpServers": {
3 | "agent-kit": {
4 | "command": "node",
5 | "env" : {
6 | "OPENAI_API_KEY": "optional_openai_api_key_here",
7 | "RPC_URL": "your_rpc_url_here",
8 | "SOLANA_PRIVATE_KEY": "your_private_key_here"
9 | },
10 | "args": [
11 | "/absolute/path/to/build/index.js"
12 | ]
13 | }
14 | }
15 | }
16 |
17 |
```
--------------------------------------------------------------------------------
/src/actions/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | import getValidatorInfoAction from "./validator/getValidatorInfo.js";
2 | import getPriorityFeeEstimateAction from "./priorityFee/getPriorityFeeEstimate.js";
3 | import getTransactionHistoryAction from "./transaction/getTransactionHistory.js";
4 | import getSecurityTxtAction from "./security/getSecurityTxt.js";
5 |
6 | // Export all actions
7 | export const ACTIONS = {
8 | GET_VALIDATOR_INFO_ACTION: getValidatorInfoAction,
9 | GET_PRIORITY_FEE_ESTIMATE_ACTION: getPriorityFeeEstimateAction,
10 | GET_TRANSACTION_HISTORY_ACTION: getTransactionHistoryAction,
11 | GET_SECURITY_TXT_ACTION: getSecurityTxtAction,
12 | };
13 |
14 | // Export individual actions for direct imports
15 | export { default as GET_VALIDATOR_INFO_ACTION } from "./validator/getValidatorInfo.js";
16 | export { default as GET_PRIORITY_FEE_ESTIMATE_ACTION } from "./priorityFee/getPriorityFeeEstimate.js";
17 | export { default as GET_TRANSACTION_HISTORY_ACTION } from "./transaction/getTransactionHistory.js";
18 | export { default as GET_SECURITY_TXT_ACTION } from "./security/getSecurityTxt.js";
19 |
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "name": "solana-mcp-sendai-hackathon",
3 | "version": "1.0.0",
4 | "description": "A Model Context Protocol server for interacting with the Solana blockchain, powered by the [Solana Agent Kit](https://github.com/sendaifun/solana-agent-kit)",
5 | "main": "build/index.js",
6 | "type": "module",
7 | "bin": {
8 | "solana-mcp": "./build/index.js"
9 | },
10 | "scripts": {
11 | "build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"",
12 | "start": "node build/index.js",
13 | "dev": "tsx watch src/index.ts"
14 | },
15 | "files": [
16 | "build"
17 | ],
18 | "repository": {
19 | "type": "git",
20 | "url": "https://github.com/cryptoleek/solana-mcp-sendai-hackathon.git"
21 | },
22 | "keywords": [
23 | "solana",
24 | "mcp",
25 | "solana-agent-kit",
26 | "solana-mcp"
27 | ],
28 | "author": "cryptoleek",
29 | "license": "MIT",
30 | "dependencies": {
31 | "@modelcontextprotocol/sdk": "^1.6.1",
32 | "@solana/spl-memo": "^0.2.5",
33 | "@solana/spl-token": "^0.4.13",
34 | "@solana/web3.js": "^1.98.0",
35 | "bs58": "^6.0.0",
36 | "dotenv": "^16.4.7",
37 | "helius-sdk": "^1.4.2",
38 | "solana-agent-kit": "1.4.8",
39 | "zod": "^3.24.2"
40 | },
41 | "devDependencies": {
42 | "@types/node": "^22.13.4",
43 | "ts-node": "^10.9.2",
44 | "typescript": "^5.7.3"
45 | },
46 | "packageManager": "[email protected]"
47 | }
48 |
```
--------------------------------------------------------------------------------
/src/tools/validator.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { Connection, PublicKey } from "@solana/web3.js";
2 |
3 | export async function getValidatorInfo(validatorPubkey: PublicKey, connection: Connection) {
4 | try {
5 | // Get the current epoch info
6 | const epochInfo = await connection.getEpochInfo();
7 |
8 | // Get vote accounts to find our validator
9 | const voteAccounts = await connection.getVoteAccounts();
10 |
11 | // Find the validator in either current or delinquent vote accounts
12 | const allAccounts = [...voteAccounts.current, ...voteAccounts.delinquent];
13 | const validatorAccount = allAccounts.find(
14 | account => account.votePubkey === validatorPubkey.toString()
15 | );
16 |
17 | if (!validatorAccount) {
18 | throw new Error("Validator not found");
19 | }
20 |
21 | // Get validator's identity account balance
22 | const balance = await connection.getBalance(new PublicKey(validatorAccount.nodePubkey));
23 |
24 | return {
25 | identity: validatorAccount.nodePubkey,
26 | vote: validatorAccount.votePubkey,
27 | commission: validatorAccount.commission,
28 | activatedStake: validatorAccount.activatedStake,
29 | epochVoteAccount: validatorAccount.epochVoteAccount,
30 | epochCredits: validatorAccount.epochCredits,
31 | delinquent: voteAccounts.delinquent.some(
32 | account => account.votePubkey === validatorPubkey.toString()
33 | ),
34 | lastVote: validatorAccount.lastVote,
35 | balance: balance,
36 | currentEpoch: epochInfo.epoch,
37 | };
38 | } catch (error) {
39 | throw error;
40 | }
41 | }
42 |
```
--------------------------------------------------------------------------------
/src/tests/test-transaction-history.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { Connection, PublicKey } from '@solana/web3.js';
2 | import * as dotenv from 'dotenv';
3 | import { getTransactionHistory } from '../tools/transaction.js';
4 |
5 | dotenv.config();
6 |
7 | async function main() {
8 | try {
9 | // Validate environment variables
10 | if (!process.env.RPC_URL) {
11 | throw new Error('RPC_URL environment variable is required');
12 | }
13 |
14 | // Create a connection to the Solana cluster
15 | const connection = new Connection(process.env.RPC_URL);
16 |
17 | // Print connection info for debugging
18 | console.log(`Using RPC URL: ${process.env.RPC_URL.substring(0, 30)}...`);
19 |
20 | try {
21 | const version = await connection.getVersion();
22 | console.log(`Connected to Solana ${version["solana-core"]}`);
23 | } catch (err) {
24 | console.log(`Failed to get version: ${err}`);
25 | }
26 |
27 | // Test address - using the provided address
28 | const testAddress = new PublicKey('DWdBJfMzVXJCB3TMdFzxhTeap6pMQCajamApbqXHbkQ4');
29 |
30 | console.log(`Fetching transaction history for address: ${testAddress.toString()}`);
31 |
32 | // Test with default options
33 | console.log('\n--- Test 1: Retrieve all txns ---');
34 | const defaultResults = await getTransactionHistory(testAddress, connection);
35 | console.log(`Retrieved ${defaultResults.length} transactions`);
36 | // Test with options
37 | console.log('\n--- Test 2: Options (limit: 10, type: TRANSFER) ---');
38 | const options = {
39 | limit: 10,
40 | types: ['TRANSFER']
41 | };
42 | const optionResults = await getTransactionHistory(testAddress, connection, options);
43 | console.log(`Retrieved ${optionResults.length} transactions`);
44 |
45 | console.log('--- Printing the first 10 transactions ---');
46 | for (const txn of optionResults.slice(0, 10)) {
47 | console.log(txn);
48 | }
49 |
50 | console.log('\nAll tests completed!');
51 | } catch (error) {
52 | console.error('Error running tests:', error);
53 | process.exit(1);
54 | }
55 | }
56 |
57 | main();
58 |
```
--------------------------------------------------------------------------------
/src/actions/validator/getValidatorInfo.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { Action } from "../../types/action.js";
2 | import { SolanaAgentKit } from "solana-agent-kit";
3 | import { z } from "zod";
4 | import { PublicKey } from "@solana/web3.js";
5 | import { getValidatorInfo } from "../../tools/validator.js";
6 |
7 | const getValidatorInfoAction: Action = {
8 | name: "GET_VALIDATOR_INFO",
9 | similes: [
10 | "validator status",
11 | "check validator",
12 | "validator info",
13 | "validator details",
14 | "node information",
15 | ],
16 | description:
17 | "Get detailed information about a Solana validator including stake, commission, and performance",
18 | examples: [
19 | [
20 | {
21 | input: {
22 | validatorAddress: "he1iusunGwqrNtafDtLdhsUQDFvo13z9sUa36PauBtk",
23 | },
24 | output: {
25 | status: "success",
26 | info: {
27 | identity: "HEL1USMZKAL2odpNBj2oCjffnFGaYwmbGmyewGv1e2TU",
28 | vote: "he1iusunGwqrNtafDtLdhsUQDFvo13z9sUa36PauBtk",
29 | commission: 10,
30 | activatedStake: 1520000000,
31 | delinquent: false,
32 | skipRate: 0.0234,
33 | },
34 | message: "Successfully retrieved validator information",
35 | },
36 | explanation: "Get information about a specific Solana validator",
37 | },
38 | ],
39 | ],
40 | schema: z.object({
41 | validatorAddress: z
42 | .string()
43 | .describe("The public key of the validator to get information about"),
44 | }),
45 | handler: async (agent: SolanaAgentKit, input: Record<string, any>) => {
46 | try {
47 | const validatorPubkey = new PublicKey(input.validatorAddress);
48 | const connection = agent.connection;
49 |
50 | const validatorInfo = await getValidatorInfo(validatorPubkey, connection);
51 |
52 | return {
53 | status: "success",
54 | info: validatorInfo,
55 | message: "Successfully retrieved validator information",
56 | };
57 | } catch (error: any) {
58 | return {
59 | status: "error",
60 | message: `Failed to get validator info: ${error.message}`,
61 | };
62 | }
63 | },
64 | };
65 |
66 | export default getValidatorInfoAction;
67 |
```
--------------------------------------------------------------------------------
/src/actions/security/getSecurityTxt.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { Action } from "../../types/action.js";
2 | import { SolanaAgentKit } from "solana-agent-kit";
3 | import { z } from "zod";
4 | import { PublicKey } from "@solana/web3.js";
5 | import { getSecurityTxtInfo } from "../../tools/security.js";
6 |
7 | const getSecurityTxtAction: Action = {
8 | name: "GET_SECURITY_TXT",
9 | similes: [
10 | "security.txt",
11 | "security contact",
12 | "program security",
13 | "security disclosure",
14 | "vulnerability reporting",
15 | "security information",
16 | "contact maintainers",
17 | "security policy",
18 | ],
19 | description:
20 | "Extract and display the security.txt file information for a given Solana program, making it easier to contact the program's maintainers with security concerns",
21 | examples: [
22 | [
23 | {
24 | input: {
25 | programId: "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
26 | },
27 | output: {
28 | status: "success",
29 | info: {
30 | contact: "mailto:[email protected]",
31 | expires: "2023-12-31T23:59:59.000Z",
32 | encryption: "https://solana.com/pgp-key.txt",
33 | acknowledgments: "https://solana.com/hall-of-fame",
34 | preferredLanguages: "en",
35 | },
36 | message: "Successfully retrieved security.txt information",
37 | },
38 | explanation: "Get security contact information for the Solana Token Program",
39 | },
40 | ],
41 | ],
42 | schema: z.object({
43 | programId: z
44 | .string()
45 | .describe("The program ID (public key) of the Solana program to inspect"),
46 | }),
47 | handler: async (agent: SolanaAgentKit, input: Record<string, any>) => {
48 | try {
49 | const programPubkey = new PublicKey(input.programId);
50 | const connection = agent.connection;
51 |
52 | const securityInfo = await getSecurityTxtInfo(programPubkey, connection);
53 |
54 | // If we couldn't find any security information
55 | if (Object.keys(securityInfo).length === 0) {
56 | return {
57 | status: "warning",
58 | info: null,
59 | message: "No security.txt information found for this program",
60 | };
61 | }
62 |
63 | return {
64 | status: "success",
65 | info: securityInfo,
66 | message: "Successfully retrieved security.txt information",
67 | };
68 | } catch (error: any) {
69 | return {
70 | status: "error",
71 | message: `Failed to get security.txt info: ${error.message}`,
72 | };
73 | }
74 | },
75 | };
76 |
77 | export default getSecurityTxtAction;
78 |
```
--------------------------------------------------------------------------------
/src/tests/test-priority-fee.ts:
--------------------------------------------------------------------------------
```typescript
1 | #!/usr/bin/env node
2 |
3 | /**
4 | * Simple test script to debug the priority fee estimation tool
5 | * Tests both account keys and transaction-based methods
6 | */
7 |
8 | import { Connection } from '@solana/web3.js';
9 | import {
10 | estimatePriorityFee,
11 | estimatePriorityFeeByAccountKeys,
12 | estimatePriorityFeeByTransaction,
13 | PriorityFeeEstimate
14 | } from '../tools/priorityFee.js';
15 |
16 | import * as dotenv from "dotenv";
17 |
18 | dotenv.config();
19 |
20 | // Set up Solana connection (using a public RPC endpoint)
21 | // const RPC_ENDPOINT = 'https://api.mainnet-beta.solana.com';
22 | // For Helius API, you might want to use your own endpoint with API key
23 | const RPC_ENDPOINT = process.env.RPC_URL;
24 |
25 | async function testPriorityFeeEstimate() {
26 | try {
27 | console.log('=== Testing Priority Fee Estimation ===');
28 | console.log('Using RPC endpoint:', RPC_ENDPOINT);
29 |
30 | // Create Solana connection
31 | const connection = new Connection(RPC_ENDPOINT || "");
32 |
33 | // Test 1: Default method (account keys with default Jupiter account)
34 | console.log('\n--- Test 1: Default method ---');
35 | const defaultEstimate = await estimatePriorityFee(connection);
36 | printEstimate(defaultEstimate);
37 |
38 | // Test 2: Account keys method with custom accounts
39 | console.log('\n--- Test 2: Account keys method with custom accounts ---');
40 | const customAccountsEstimate = await estimatePriorityFeeByAccountKeys(
41 | connection,
42 | [
43 | 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA', // Token program
44 | 'ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL' // Associated token program
45 | ]
46 | );
47 | printEstimate(customAccountsEstimate);
48 |
49 | // Test 3: Transaction method
50 | console.log('\n--- Test 3: Transaction method ---');
51 | const transactionEstimate = await estimatePriorityFeeByTransaction(connection);
52 | printEstimate(transactionEstimate);
53 |
54 | console.log('\n=== All tests completed ===');
55 | } catch (error: any ) {
56 | console.error('Failed to run tests:', error);
57 | if (error.cause) {
58 | console.error('Error cause:', error.cause);
59 | }
60 | }
61 | }
62 |
63 | /**
64 | * Helper function to print the fee estimate in a readable format
65 | */
66 | function printEstimate(estimate: PriorityFeeEstimate) {
67 | console.log('Priority Fee Estimate:');
68 | console.log('- Low:', estimate.low, 'microLamports');
69 | console.log('- Medium:', estimate.medium, 'microLamports');
70 | console.log('- High:', estimate.high, 'microLamports');
71 | console.log('- Suggested:', estimate.suggested, 'microLamports');
72 | console.log('- Time Estimates:', estimate.timeEstimates);
73 | console.log('- High Load Behavior:', estimate.highLoadBehavior);
74 | }
75 |
76 | // Run the test
77 | await testPriorityFeeEstimate()
78 | .catch(err => {
79 | console.error('Unhandled error:', err);
80 | process.exit(1);
81 | });
82 |
```
--------------------------------------------------------------------------------
/src/tools/transaction.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { Connection, PublicKey } from '@solana/web3.js';
2 | import { parseTransaction } from 'solana-agent-kit/dist/tools/index.js';
3 |
4 | // Interface for transaction history options
5 | interface TransactionHistoryOptions {
6 | limit?: number;
7 | before?: string;
8 | until?: string;
9 | types?: string[];
10 | minContextSlot?: number;
11 | }
12 |
13 | // Interface for transaction data
14 | interface TransactionData {
15 | signature: string;
16 | slot: number;
17 | timestamp: number;
18 | err: any;
19 | memo: string | null;
20 | blockTime: number;
21 | type: string;
22 | fee: number;
23 | status: string;
24 | }
25 |
26 | /**
27 | * Get transaction history for a Solana wallet address or token account using Helius API
28 | * @param publicKey - The public key to get transaction history for
29 | * @param connection - Solana connection object (not used with Helius but kept for compatibility)
30 | * @param options - Optional parameters for filtering transactions
31 | * @returns Array of transaction data
32 | */
33 | export async function getTransactionHistory(
34 | publicKey: PublicKey,
35 | connection: Connection,
36 | options: TransactionHistoryOptions = {}
37 | ): Promise<TransactionData[]> {
38 | try {
39 | const address = publicKey.toString();
40 |
41 | // Helius SDK doesn't have a direct method for transaction history
42 | // We'll use the fetchTransactionHistory helper function to make the API call
43 | const transactions = await fetchTransactionHistory(connection, address, options);
44 |
45 | return transactions;
46 | } catch (error) {
47 | console.error('Error fetching transaction history:', error);
48 | throw error;
49 | }
50 | }
51 |
52 | /**
53 | * Helper function to fetch transaction history using Helius API directly
54 | * @param address - The address to get transaction history for
55 | * @param options - Query parameters
56 | * @returns Array of transaction data from Helius
57 | */
58 | async function fetchTransactionHistory(
59 | connection: Connection,
60 | address: string,
61 | options: TransactionHistoryOptions = {}
62 | ): Promise<TransactionData[]> {
63 | try {
64 | // Build query parameters
65 | const queryParams: Record<string, string> = {};
66 |
67 | if (options.limit) {
68 | queryParams.limit = options.limit.toString();
69 | }
70 |
71 | if (options.before) {
72 | queryParams.before = options.before;
73 | }
74 |
75 | if (options.until) {
76 | queryParams.until = options.until;
77 | }
78 |
79 | if (options.types && options.types.length > 0) {
80 | queryParams.type = options.types.join(',');
81 | }
82 |
83 | // Convert params to URL query string
84 | const queryString = Object.entries(queryParams)
85 | .map(([key, value]) => `${key}=${encodeURIComponent(value)}`)
86 | .join('&');
87 |
88 |
89 | const rpcUrl = connection.rpcEndpoint;
90 | const apiKey = rpcUrl.split('api-key=')[1];
91 |
92 | if (!apiKey) {
93 | throw new Error('Missing Helius API key');
94 | }
95 |
96 | // Build the URL
97 | const url = `https://api.helius.xyz/v0/addresses/${address}/transactions?api-key=${apiKey}${queryString ? `&${queryString}` : ''}`;
98 |
99 | // Fetch data from Helius API
100 | const response = await fetch(url);
101 |
102 | if (!response.ok) {
103 | const errorText = await response.text();
104 | throw new Error(`Helius API error: ${response.status} ${errorText}`);
105 | }
106 |
107 | const data = await response.json();
108 |
109 | // Map the response to our expected format
110 | return data.map((tx: any) => ({
111 | signature: tx.signature,
112 | slot: tx.slot,
113 | timestamp: tx.timestamp,
114 | err: tx.err,
115 | memo: tx.memo || null,
116 | blockTime: tx.timestamp,
117 | type: tx.type || 'Unknown',
118 | fee: tx.fee || 0,
119 | status: tx.err ? 'Failed' : 'Success'
120 | }));
121 | } catch (error) {
122 | console.error('Error in fetchTransactionHistory:', error);
123 | throw error;
124 | }
125 | }
126 |
127 |
```
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | #!/usr/bin/env node
2 |
3 | import { ACTIONS as SAK_ACTIONS, SolanaAgentKit, startMcpServer } from "solana-agent-kit";
4 | import { ACTIONS as CUSTOM_ACTIONS } from "./actions/index.js";
5 | import * as dotenv from "dotenv";
6 |
7 | dotenv.config();
8 |
9 | // Validate required environment variables
10 | function validateEnvironment() {
11 | const requiredEnvVars = {
12 | 'SOLANA_PRIVATE_KEY': process.env.SOLANA_PRIVATE_KEY,
13 | 'RPC_URL': process.env.RPC_URL
14 | };
15 |
16 | const missingVars = Object.entries(requiredEnvVars)
17 | .filter(([_, value]) => !value)
18 | .map(([key]) => key);
19 |
20 | if (missingVars.length > 0) {
21 | throw new Error(`Missing required environment variables: ${missingVars.join(', ')}`);
22 | }
23 | }
24 |
25 | async function main() {
26 | try {
27 | // Validate environment before proceeding
28 | validateEnvironment();
29 |
30 | const RPC_URL = process.env.RPC_URL as string
31 | const HELIUS_API_KEY = RPC_URL.split('api-key=')[1]
32 |
33 | // Initialize the agent with error handling
34 | const agent = new SolanaAgentKit(
35 | process.env.SOLANA_PRIVATE_KEY!,
36 | process.env.RPC_URL!,
37 | {
38 | OPENAI_API_KEY: process.env.OPENAI_API_KEY || "",
39 | PERPLEXITY_API_KEY: process.env.PERPLEXITY_API_KEY || "",
40 | HELIUS_API_KEY: HELIUS_API_KEY || ""
41 | }
42 | );
43 |
44 | // Debug: Log our custom actions
45 | console.log("Registering custom actions:");
46 | console.log("GET_VALIDATOR_INFO_ACTION schema:", CUSTOM_ACTIONS.GET_VALIDATOR_INFO_ACTION.schema);
47 | console.log("GET_PRIORITY_FEE_ESTIMATE_ACTION schema:", CUSTOM_ACTIONS.GET_PRIORITY_FEE_ESTIMATE_ACTION.schema);
48 | console.log("GET_TRANSACTION_HISTORY_ACTION schema:", CUSTOM_ACTIONS.GET_TRANSACTION_HISTORY_ACTION.schema);
49 | console.log("GET_SECURITY_TXT_ACTION schema:", CUSTOM_ACTIONS.GET_SECURITY_TXT_ACTION.schema);
50 |
51 | const mcp_actions = {
52 | GET_ASSET: SAK_ACTIONS.GET_ASSET_ACTION,
53 | DEPLOY_TOKEN: SAK_ACTIONS.DEPLOY_TOKEN_ACTION,
54 | FETCH_PRICE: SAK_ACTIONS.FETCH_PRICE_ACTION,
55 | GET_WALLET_ADDRESS: SAK_ACTIONS.WALLET_ADDRESS_ACTION,
56 | BALANCE: SAK_ACTIONS.BALANCE_ACTION,
57 | TRANSFER: SAK_ACTIONS.TRANSFER_ACTION,
58 | MINT_NFT: SAK_ACTIONS.MINT_NFT_ACTION,
59 | TRADE: SAK_ACTIONS.TRADE_ACTION,
60 | REQUEST_FUNDS: SAK_ACTIONS.REQUEST_FUNDS_ACTION,
61 | REGISTER_DOMAIN: SAK_ACTIONS.RESOLVE_DOMAIN_ACTION,
62 | GET_TPS: SAK_ACTIONS.GET_TPS_ACTION,
63 | PARSE_TRANSACTION_ACTION: SAK_ACTIONS.PARSE_TRANSACTION_ACTION,
64 | // Add our custom actions
65 | GET_VALIDATOR_INFO: CUSTOM_ACTIONS.GET_VALIDATOR_INFO_ACTION,
66 | GET_PRIORITY_FEE_ESTIMATE: CUSTOM_ACTIONS.GET_PRIORITY_FEE_ESTIMATE_ACTION,
67 | GET_TRANSACTION_HISTORY: CUSTOM_ACTIONS.GET_TRANSACTION_HISTORY_ACTION,
68 | GET_SECURITY_TXT: CUSTOM_ACTIONS.GET_SECURITY_TXT_ACTION,
69 | };
70 |
71 | // Debug: Log all registered actions
72 | console.log("All registered MCP actions:", Object.keys(mcp_actions));
73 |
74 | // Start the MCP server with error handling
75 | await startMcpServer(mcp_actions, agent, {
76 | name: "solana-agent",
77 | version: "0.0.1"
78 | });
79 |
80 | // Add console logging for debugging
81 | console.log("MCP server started successfully");
82 | } catch (error) {
83 | console.error('Failed to start MCP server:', error instanceof Error ? error.message : String(error));
84 | process.exit(1);
85 | }
86 | }
87 |
88 | // Handle uncaught exceptions and rejections
89 | process.on('uncaughtException', (error) => {
90 | console.error('Uncaught Exception:', error);
91 | process.exit(1);
92 | });
93 |
94 | process.on('unhandledRejection', (reason, promise) => {
95 | console.error('Unhandled Rejection at:', promise, 'reason:', reason);
96 | process.exit(1);
97 | });
98 |
99 | main();
```
--------------------------------------------------------------------------------
/src/actions/priorityFee/getPriorityFeeEstimate.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { Action } from "../../types/action.js";
2 | import { SolanaAgentKit } from "solana-agent-kit";
3 | import { z } from "zod";
4 | import { estimatePriorityFee } from "../../tools/priorityFee.js";
5 |
6 | const getPriorityFeeEstimateAction: Action = {
7 | name: "GET_PRIORITY_FEE_ESTIMATE",
8 | similes: [
9 | "estimate priority fee",
10 | "transaction fee estimate",
11 | "optimal priority fee",
12 | "solana fee estimate",
13 | "transaction cost estimate",
14 | ],
15 | description:
16 | "Estimates optimal priority fees for Solana transactions based on recent network activity",
17 | examples: [
18 | [
19 | {
20 | input: {},
21 | output: {
22 | status: "success",
23 | estimate: {
24 | low: 1000,
25 | medium: 10000,
26 | high: 100000,
27 | suggested: 12000,
28 | timeEstimates: {
29 | low: "3-5 seconds",
30 | medium: "2-3 seconds",
31 | high: "1-2 seconds"
32 | }
33 | },
34 | message: "Successfully estimated priority fees",
35 | },
36 | explanation: "Get current priority fee estimates for Solana transactions",
37 | },
38 | ],
39 | [
40 | {
41 | input: { method: "transaction" },
42 | output: {
43 | status: "success",
44 | estimate: {
45 | low: 1000,
46 | medium: 10000,
47 | high: 100000,
48 | suggested: 12000,
49 | timeEstimates: {
50 | low: "3-5 seconds",
51 | medium: "2-3 seconds",
52 | high: "1-2 seconds"
53 | }
54 | },
55 | message: "Successfully estimated priority fees using transaction method",
56 | },
57 | explanation: "Get priority fee estimates using the transaction-based method",
58 | },
59 | ],
60 | [
61 | {
62 | input: {
63 | method: "accountKeys",
64 | accountKeys: ["TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"]
65 | },
66 | output: {
67 | status: "success",
68 | estimate: {
69 | low: 1000,
70 | medium: 10000,
71 | high: 100000,
72 | suggested: 12000,
73 | timeEstimates: {
74 | low: "3-5 seconds",
75 | medium: "2-3 seconds",
76 | high: "1-2 seconds"
77 | }
78 | },
79 | message: "Successfully estimated priority fees for specific account keys",
80 | },
81 | explanation: "Get priority fee estimates for specific account keys",
82 | },
83 | ],
84 | ],
85 | // Define schema with optional parameters
86 | schema: z.object({
87 | method: z.enum(["accountKeys", "transaction"]).optional(),
88 | accountKeys: z.array(z.string()).optional(),
89 | serializedTransaction: z.string().optional(),
90 | }),
91 | handler: async (agent: SolanaAgentKit, params: any) => {
92 | try {
93 | const connection = agent.connection;
94 |
95 | // Extract parameters
96 | const method = params.method;
97 | const accountKeys = params.accountKeys;
98 | const serializedTransaction = params.serializedTransaction;
99 |
100 | // Call the estimatePriorityFee function with the provided parameters
101 | const feeEstimate = await estimatePriorityFee(connection, {
102 | method,
103 | accountKeys,
104 | serializedTransaction
105 | });
106 |
107 | // Prepare success message based on the method used
108 | let message = "Successfully estimated priority fees";
109 | if (method === "transaction") {
110 | message = "Successfully estimated priority fees using transaction method";
111 | } else if (method === "accountKeys" && accountKeys) {
112 | message = "Successfully estimated priority fees for specific account keys";
113 | }
114 |
115 | return {
116 | status: "success",
117 | estimate: feeEstimate,
118 | message,
119 | };
120 | } catch (error: any) {
121 | return {
122 | status: "error",
123 | message: `Failed to estimate priority fees: ${error.message}`,
124 | error: error.toString(),
125 | stack: error.stack
126 | };
127 | }
128 | },
129 | };
130 |
131 | export default getPriorityFeeEstimateAction;
132 |
```
--------------------------------------------------------------------------------
/src/tools/security.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { Connection, PublicKey } from "@solana/web3.js";
2 | import * as bs58 from "bs58";
3 |
4 | // Interface for security.txt content
5 | export interface SecurityTxtInfo {
6 | contact?: string;
7 | expires?: string;
8 | encryption?: string;
9 | acknowledgments?: string;
10 | preferredLanguages?: string;
11 | canonical?: string;
12 | policy?: string;
13 | hiring?: string;
14 | // Any additional fields that might be in the security.txt
15 | [key: string]: string | undefined;
16 | }
17 |
18 | /**
19 | * Extracts and parses security.txt information from a Solana program
20 | *
21 | * @param programId - The PublicKey of the Solana program to inspect
22 | * @param connection - The Solana connection object
23 | * @returns The parsed security.txt information
24 | */
25 | export async function getSecurityTxtInfo(programId: PublicKey, connection: Connection): Promise<SecurityTxtInfo> {
26 | try {
27 | // Get the program account data
28 | const programAccount = await connection.getAccountInfo(programId);
29 |
30 | if (!programAccount) {
31 | throw new Error("Program account not found");
32 | }
33 |
34 | // Check if this is an upgradeable program
35 | const BPF_UPGRADEABLE_LOADER_ID = new PublicKey("BPFLoaderUpgradeab1e11111111111111111111111");
36 |
37 | let programData: Buffer;
38 |
39 | if (programAccount.owner.equals(BPF_UPGRADEABLE_LOADER_ID)) {
40 | // This is an upgradeable program, we need to find the program data account
41 | // The first 4 bytes indicate the account type
42 | const accountType = programAccount.data.slice(0, 4);
43 |
44 | // Check if this is a Program account (type 2)
45 | if (accountType[0] === 2) {
46 | // Extract the program data address (next 32 bytes)
47 | const programDataAddress = new PublicKey(programAccount.data.slice(4, 36));
48 |
49 | // Get the program data account
50 | const programDataAccount = await connection.getAccountInfo(programDataAddress);
51 |
52 | if (!programDataAccount) {
53 | throw new Error("Program data account not found");
54 | }
55 |
56 | // The program data starts after the header (first 8 bytes)
57 | // First byte is account type, next 7 bytes are the slot
58 | programData = programDataAccount.data.slice(8);
59 | } else {
60 | throw new Error("Not a valid upgradeable program account");
61 | }
62 | } else {
63 | // For non-upgradeable programs, use the account data directly
64 | programData = programAccount.data;
65 | }
66 |
67 | // Now search for security.txt in the program data
68 | return findAndParseSecurityTxt(programData);
69 | } catch (error) {
70 | throw error;
71 | }
72 | }
73 |
74 | /**
75 | * Finds and parses security.txt content in program data
76 | *
77 | * @param programData - The program binary data
78 | * @returns Parsed security.txt information
79 | */
80 | function findAndParseSecurityTxt(programData: Buffer): SecurityTxtInfo {
81 | try {
82 | // Convert the binary data to a string
83 | // Note: This is a simplification, as the security.txt might be encoded in various ways
84 | const dataString = new TextDecoder().decode(programData);
85 |
86 | // Look for security.txt pattern
87 | // Pattern 1: Standard security.txt format
88 | const securityTxtMatch = dataString.match(/(?:^|\n)# ?security\.txt(?:\n|$)([\s\S]*?)(?:\n\n|\n#|$)/i);
89 |
90 | if (securityTxtMatch && securityTxtMatch[1]) {
91 | return parseSecurityTxt(securityTxtMatch[1]);
92 | }
93 |
94 | // Pattern 2: Look for BEGIN/END SECURITY.TXT markers
95 | const securityTxtBlockMatch = dataString.match(/-----BEGIN SECURITY\.TXT-----([\s\S]*?)-----END SECURITY\.TXT-----/i);
96 |
97 | if (securityTxtBlockMatch && securityTxtBlockMatch[1]) {
98 | return parseSecurityTxt(securityTxtBlockMatch[1]);
99 | }
100 |
101 | // Pattern 3: Look for common security.txt fields
102 | const fieldMatch = dataString.match(/Contact: ([^\n]+)/i) ||
103 | dataString.match(/Security-Contact: ([^\n]+)/i) ||
104 | dataString.match(/mailto:([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/i);
105 |
106 | if (fieldMatch) {
107 | const result: SecurityTxtInfo = {};
108 | result.contact = fieldMatch[1].trim();
109 | return result;
110 | }
111 |
112 | // If we couldn't find any specific security.txt format, try to extract any security-related info
113 | return extractSecurityInfoFromText(dataString);
114 | } catch (error) {
115 | // If we encounter any error during parsing, return an empty object
116 | console.error("Error parsing security.txt:", error);
117 | return {};
118 | }
119 | }
120 |
121 | /**
122 | * Parses security.txt content into a structured object
123 | *
124 | * @param content - The raw security.txt content
125 | * @returns Structured security.txt information
126 | */
127 | function parseSecurityTxt(content: string): SecurityTxtInfo {
128 | const result: SecurityTxtInfo = {};
129 |
130 | // Split by lines and process each line
131 | const lines = content.split('\n');
132 |
133 | for (const line of lines) {
134 | // Skip empty lines and comments
135 | if (!line.trim() || line.trim().startsWith('#')) {
136 | continue;
137 | }
138 |
139 | // Extract field and value
140 | const match = line.match(/^([^:]+):\s*(.*)$/);
141 | if (match) {
142 | const [_, field, value] = match;
143 | const fieldName = field.trim().toLowerCase();
144 |
145 | // Convert kebab-case to camelCase
146 | const camelCaseField = fieldName.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
147 |
148 | result[camelCaseField] = value.trim();
149 | }
150 | }
151 |
152 | return result;
153 | }
154 |
155 | /**
156 | * Attempts to extract security information from program text when no explicit security.txt is found
157 | *
158 | * @param text - The program text to analyze
159 | * @returns Any security information found
160 | */
161 | function extractSecurityInfoFromText(text: string): SecurityTxtInfo {
162 | const result: SecurityTxtInfo = {};
163 |
164 | // Look for common patterns that might indicate security contact info
165 | const contactMatches = [
166 | // Email patterns
167 | ...text.match(/security@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g) || [],
168 | ...text.match(/mailto:[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g) || [],
169 | // URL patterns
170 | ...text.match(/https?:\/\/[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}\/security/g) || [],
171 | ...text.match(/https?:\/\/[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}\/responsible-disclosure/g) || [],
172 | ...text.match(/https?:\/\/[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}\/vulnerability/g) || [],
173 | ];
174 |
175 | if (contactMatches.length > 0) {
176 | result.contact = contactMatches[0];
177 | }
178 |
179 | // Look for PGP key patterns
180 | const pgpMatches = text.match(/-----BEGIN PGP PUBLIC KEY BLOCK-----([\s\S]*?)-----END PGP PUBLIC KEY BLOCK-----/g);
181 | if (pgpMatches) {
182 | result.encryption = "PGP key found in program data";
183 | }
184 |
185 | return result;
186 | }
187 |
```
--------------------------------------------------------------------------------
/src/actions/transaction/getTransactionHistory.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { Action } from "../../types/action.js";
2 | import { SolanaAgentKit } from "solana-agent-kit";
3 | import { z } from "zod";
4 | import { PublicKey } from "@solana/web3.js";
5 | import { getTransactionHistory } from "../../tools/transaction.js";
6 | import dotenv from "dotenv";
7 |
8 | // Load environment variables
9 | dotenv.config();
10 |
11 | const getTransactionHistoryAction: Action = {
12 | name: "GET_TRANSACTION_HISTORY",
13 | similes: [
14 | "transaction history",
15 | "account transactions",
16 | "transaction list",
17 | "tx history",
18 | "recent transactions",
19 | ],
20 | description:
21 | "Get transaction history for a Solana wallet address or token account using Helius API",
22 | examples: [
23 | [
24 | {
25 | input: {
26 | address: "8zFZHuSRuDpuAR7J6FzwyF3vKNx4CVW3DFHJerQhc7Zd"
27 | },
28 | output: {
29 | status: "success",
30 | transactions: [
31 | {
32 | signature: "5UJpjrKQ8641Q8kPudtvtqR2SgZMz5rSbdpuNP1qy6BxAw4aY3bRfyZQqGKEK5yQXi3yk4pVMRLqzYjnb3bawRn5",
33 | slot: 172492080,
34 | timestamp: 1678901234,
35 | err: null,
36 | memo: null,
37 | blockTime: 1678901234,
38 | type: "System Transfer",
39 | fee: 5000,
40 | status: "Success"
41 | }
42 | ],
43 | message: "Successfully retrieved transaction history"
44 | },
45 | explanation: "Get recent transaction history for a specific Solana address",
46 | },
47 | ],
48 | [
49 | {
50 | input: {
51 | address: "8zFZHuSRuDpuAR7J6FzwyF3vKNx4CVW3DFHJerQhc7Zd",
52 | limit: 5
53 | },
54 | output: {
55 | status: "success",
56 | transactions: [
57 | {
58 | signature: "5UJpjrKQ8641Q8kPudtvtqR2SgZMz5rSbdpuNP1qy6BxAw4aY3bRfyZQqGKEK5yQXi3yk4pVMRLqzYjnb3bawRn5",
59 | slot: 172492080,
60 | timestamp: 1678901234,
61 | err: null,
62 | memo: null,
63 | blockTime: 1678901234,
64 | type: "System Transfer",
65 | fee: 5000,
66 | status: "Success"
67 | }
68 | ],
69 | message: "Successfully retrieved 5 most recent transactions"
70 | },
71 | explanation: "Get the 5 most recent transactions for a Solana address",
72 | },
73 | ],
74 | [
75 | {
76 | input: {
77 | address: "8zFZHuSRuDpuAR7J6FzwyF3vKNx4CVW3DFHJerQhc7Zd",
78 | before: "5UJpjrKQ8641Q8kPudtvtqR2SgZMz5rSbdpuNP1qy6BxAw4aY3bRfyZQqGKEK5yQXi3yk4pVMRLqzYjnb3bawRn5"
79 | },
80 | output: {
81 | status: "success",
82 | transactions: [
83 | {
84 | signature: "4tSRZ8QVNfUyHuJGZQvJzuUbq3nBpZ9QFNEsrey9mEbY6iN7VDuZtFGBciogSGkAiKwbVL8YgYNJZP1XNqXhRmML",
85 | slot: 172492070,
86 | timestamp: 1678901200,
87 | err: null,
88 | memo: null,
89 | blockTime: 1678901200,
90 | type: "Token Transfer",
91 | fee: 5000,
92 | status: "Success"
93 | }
94 | ],
95 | message: "Successfully retrieved transaction history before specified signature"
96 | },
97 | explanation: "Get transaction history before a specific transaction signature",
98 | },
99 | ],
100 | [
101 | {
102 | input: {
103 | address: "8zFZHuSRuDpuAR7J6FzwyF3vKNx4CVW3DFHJerQhc7Zd",
104 | types: ["NFT_SALE", "NFT_LISTING"]
105 | },
106 | output: {
107 | status: "success",
108 | transactions: [
109 | {
110 | signature: "4tSRZ8QVNfUyHuJGZQvJzuUbq3nBpZ9QFNEsrey9mEbY6iN7VDuZtFGBciogSGkAiKwbVL8YgYNJZP1XNqXhRmML",
111 | slot: 172492070,
112 | timestamp: 1678901200,
113 | err: null,
114 | memo: null,
115 | blockTime: 1678901200,
116 | type: "NFT_SALE",
117 | fee: 5000,
118 | status: "Success"
119 | }
120 | ],
121 | message: "Successfully retrieved NFT sales and listings transactions"
122 | },
123 | explanation: "Get NFT sales and listings transactions for a Solana address",
124 | },
125 | ],
126 | ],
127 | // Define schema with optional address and optional parameters
128 | schema: z.object({
129 | address: z.string().min(32).max(44).optional(),
130 | limit: z.number().min(1).max(100).optional(),
131 | before: z.string().optional(),
132 | until: z.string().optional(),
133 | minContextSlot: z.number().optional(),
134 | types: z.array(z.string()).optional(),
135 | }),
136 | handler: async (agent: SolanaAgentKit, params: any) => {
137 | try {
138 | // Check if address is provided
139 | if (!params.address) {
140 | return {
141 | status: "input_needed",
142 | message: "Please provide a Solana wallet address to view transaction history.",
143 | error: "Missing address parameter"
144 | };
145 | }
146 |
147 | const connection = agent.connection;
148 |
149 | // Extract parameters
150 | const address = params.address;
151 | const limit = params.limit || 10;
152 | const before = params.before;
153 | const until = params.until;
154 | const minContextSlot = params.minContextSlot;
155 | const types = params.types;
156 |
157 | // Validate address
158 | let publicKey: PublicKey;
159 | try {
160 | publicKey = new PublicKey(address);
161 | } catch (error) {
162 | return {
163 | status: "error",
164 | message: "Invalid Solana address provided",
165 | error: "Invalid public key format"
166 | };
167 | }
168 |
169 | // Call the getTransactionHistory function with the provided parameters
170 | const transactions = await getTransactionHistory(publicKey, connection, {
171 | limit,
172 | before,
173 | until,
174 | minContextSlot,
175 | types
176 | });
177 |
178 | // Prepare success message based on the parameters used
179 | let message = "Successfully retrieved transaction history";
180 | if (limit) {
181 | message = `Successfully retrieved ${limit} most recent transactions`;
182 | }
183 | if (before) {
184 | message = "Successfully retrieved transaction history before specified signature";
185 | }
186 | if (until) {
187 | message = "Successfully retrieved transaction history until specified signature";
188 | }
189 | if (types && types.length > 0) {
190 | message = `Successfully retrieved ${types.join(', ')} transactions`;
191 | }
192 |
193 | return {
194 | status: "success",
195 | transactions: transactions.map(tx => ({
196 | signature: tx.signature,
197 | slot: tx.slot,
198 | timestamp: tx.timestamp,
199 | err: tx.err,
200 | memo: tx.memo,
201 | blockTime: tx.blockTime,
202 | type: tx.type,
203 | fee: tx.fee,
204 | status: tx.status
205 | })),
206 | message,
207 | };
208 | } catch (error: any) {
209 | return {
210 | status: "error",
211 | message: `Failed to retrieve transaction history: ${error.message}`,
212 | error: error.toString(),
213 | stack: error.stack
214 | };
215 | }
216 | },
217 | };
218 |
219 | export default getTransactionHistoryAction;
220 |
```
--------------------------------------------------------------------------------
/src/tools/priorityFee.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { Connection, ComputeBudgetProgram, Transaction, PublicKey, Keypair } from "@solana/web3.js";
2 |
3 | export interface PriorityFeeEstimate {
4 | low: number;
5 | medium: number;
6 | high: number;
7 | veryHigh?: number;
8 | min?: number;
9 | unsafeMax?: number;
10 | suggested: number;
11 | timeEstimates: {
12 | low: string;
13 | medium: string;
14 | high: string;
15 | };
16 | highLoadBehavior?: {
17 | low: string;
18 | medium: string;
19 | high: string;
20 | };
21 | // Raw Helius API response
22 | priorityFeeEstimate?: number;
23 | priorityFeeLevels?: {
24 | min?: number;
25 | low: number;
26 | medium: number;
27 | high: number;
28 | veryHigh?: number;
29 | unsafeMax?: number;
30 | };
31 | }
32 |
33 | /**
34 | * Estimates priority fees using Helius API via account keys method
35 | * @param connection Solana connection object
36 | * @param accountKeys Optional array of account keys to use for estimation (defaults to Jupiter account)
37 | * @param options Optional configuration for fee estimation
38 | * @returns Priority fee estimate with different fee levels
39 | */
40 | export async function estimatePriorityFeeByAccountKeys(
41 | connection: Connection,
42 | accountKeys: string[] = ["JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4"],
43 | options?: {
44 | lookbackSlots?: number;
45 | includeVote?: boolean;
46 | }
47 | ): Promise<PriorityFeeEstimate> {
48 | try {
49 | // Get RPC URL from connection
50 | const rpcUrl = connection.rpcEndpoint;
51 |
52 | // Make request to Helius API for priority fee estimate using account keys
53 | const response = await fetch(rpcUrl, {
54 | method: "POST",
55 | headers: {
56 | "Content-Type": "application/json",
57 | },
58 | body: JSON.stringify({
59 | jsonrpc: "2.0",
60 | id: 1,
61 | method: "getPriorityFeeEstimate",
62 | params: [{
63 | "accountKeys": accountKeys,
64 | "options": {
65 | "includeAllPriorityFeeLevels": true,
66 | "lookbackSlots": options?.lookbackSlots || 150,
67 | "includeVote": options?.includeVote !== undefined ? options.includeVote : true
68 | }
69 | }]
70 | }),
71 | });
72 |
73 | const data = await response.json();
74 |
75 | console.log(data)
76 |
77 | if (data.error) {
78 | throw new Error(`Helius API error: ${data.error.message}`);
79 | }
80 |
81 | // Extract fee levels from the response
82 | const result = data.result;
83 |
84 | // If using Helius API, the response will have priorityFeeLevels
85 | if (result) {
86 | const priorityFeeEstimate = result.priorityFeeEstimate || result.priorityFee;
87 | const priorityFeeLevels = result.priorityFeeLevels;
88 |
89 | if (priorityFeeLevels) {
90 | const { min, low, medium, high, veryHigh, unsafeMax } = priorityFeeLevels;
91 | // Use the recommended fee or calculate a suggested value based on medium
92 | const suggested = priorityFeeEstimate || Math.ceil(medium * 1.2);
93 |
94 | return {
95 | low: low || 0,
96 | medium: medium || 0,
97 | high: high || 0,
98 | veryHigh: veryHigh,
99 | min: min,
100 | unsafeMax: unsafeMax,
101 | suggested,
102 | timeEstimates: {
103 | low: "1-2 blocks (~0.8s)",
104 | medium: "1 block (~0.4s)",
105 | high: "Usually immediate"
106 | },
107 | highLoadBehavior: {
108 | low: "May be delayed or dropped",
109 | medium: "More consistent inclusion",
110 | high: "Very likely first-in"
111 | },
112 | // Include the raw Helius API response
113 | priorityFeeEstimate,
114 | priorityFeeLevels
115 | };
116 | }
117 | }
118 |
119 | // Fallback to default values if the API doesn't return expected format
120 | return getDefaultPriorityFees();
121 | } catch (error) {
122 | console.error("Error fetching priority fee estimate by account keys:", error);
123 | return getDefaultPriorityFees();
124 | }
125 | }
126 |
127 | /**
128 | * Estimates priority fees using Helius API via serialized transaction method
129 | * @param connection Solana connection object
130 | * @param serializedTransaction Base58 encoded serialized transaction (optional)
131 | * @param options Optional configuration for fee estimation
132 | * @returns Priority fee estimate with different fee levels
133 | */
134 | export async function estimatePriorityFeeByTransaction(
135 | connection: Connection,
136 | serializedTransaction?: string,
137 | options?: {
138 | lookbackSlots?: number;
139 | includeVote?: boolean;
140 | }
141 | ): Promise<PriorityFeeEstimate> {
142 | try {
143 | // Get RPC URL from connection
144 | const rpcUrl = connection.rpcEndpoint;
145 |
146 | // If no serialized transaction is provided, create a dummy transaction
147 | let encodedTransaction = serializedTransaction;
148 | if (!encodedTransaction) {
149 | try {
150 | // Since the transaction-based method is having issues with encoding,
151 | // let's fall back to the account keys method which is more reliable
152 | console.log("Using account keys method as fallback for transaction method");
153 | return await estimatePriorityFeeByAccountKeys(connection);
154 | } catch (txError) {
155 | console.error("Error creating dummy transaction:", txError);
156 | // If we can't create a transaction, fall back to account keys method
157 | return estimatePriorityFeeByAccountKeys(connection);
158 | }
159 | }
160 |
161 | // If we have a serialized transaction (provided externally), use it
162 | if (encodedTransaction) {
163 | // Make request to Helius API for priority fee estimate using serialized transaction
164 | const response = await fetch(rpcUrl, {
165 | method: "POST",
166 | headers: {
167 | "Content-Type": "application/json",
168 | },
169 | body: JSON.stringify({
170 | jsonrpc: "2.0",
171 | id: 1,
172 | method: "getPriorityFeeEstimate",
173 | params: [{
174 | "transaction": encodedTransaction,
175 | "options": {
176 | "includeAllPriorityFeeLevels": true,
177 | "lookbackSlots": options?.lookbackSlots || 150,
178 | "includeVote": options?.includeVote !== undefined ? options.includeVote : true
179 | }
180 | }]
181 | }),
182 | });
183 |
184 | const data = await response.json();
185 |
186 | if (data.error) {
187 | throw new Error(`Helius API error: ${data.error.message}`);
188 | }
189 |
190 | // Extract fee levels from the response
191 | const result = data.result;
192 |
193 | // If using Helius API, the response will have priorityFeeLevels
194 | if (result) {
195 | const priorityFeeEstimate = result.priorityFeeEstimate || result.priorityFee;
196 | const priorityFeeLevels = result.priorityFeeLevels;
197 |
198 | if (priorityFeeLevels) {
199 | const { min, low, medium, high, veryHigh, unsafeMax } = priorityFeeLevels;
200 | // Use the recommended fee or calculate a suggested value based on medium
201 | const suggested = priorityFeeEstimate || Math.ceil(medium * 1.2);
202 |
203 | return {
204 | low: low || 0,
205 | medium: medium || 0,
206 | high: high || 0,
207 | veryHigh: veryHigh,
208 | min: min,
209 | unsafeMax: unsafeMax,
210 | suggested,
211 | timeEstimates: {
212 | low: "3-5 seconds",
213 | medium: "2-3 seconds",
214 | high: "1-2 seconds"
215 | },
216 | highLoadBehavior: {
217 | low: "May be delayed or dropped",
218 | medium: "More consistent inclusion",
219 | high: "Very likely first-in"
220 | },
221 | // Include the raw Helius API response
222 | priorityFeeEstimate,
223 | priorityFeeLevels
224 | };
225 | }
226 | }
227 | }
228 |
229 | // Fallback to default values if the API doesn't return expected format
230 | return getDefaultPriorityFees();
231 | } catch (error) {
232 | console.error("Error fetching priority fee estimate by transaction:", error);
233 | return getDefaultPriorityFees();
234 | }
235 | }
236 |
237 | /**
238 | * Main function to estimate priority fees using the preferred method
239 | * @param connection Solana connection object
240 | * @param options Optional configuration for fee estimation
241 | * @returns Priority fee estimate with different fee levels
242 | */
243 | export async function estimatePriorityFee(
244 | connection: Connection,
245 | options?: {
246 | method?: 'accountKeys' | 'transaction';
247 | accountKeys?: string[];
248 | serializedTransaction?: string;
249 | lookbackSlots?: number;
250 | includeVote?: boolean;
251 | }
252 | ): Promise<PriorityFeeEstimate> {
253 | try {
254 | const method = options?.method || 'accountKeys';
255 |
256 | if (method === 'transaction') {
257 | // If a serialized transaction is provided, use the transaction method
258 | if (options?.serializedTransaction) {
259 | return await estimatePriorityFeeByTransaction(connection, options.serializedTransaction, {
260 | lookbackSlots: options?.lookbackSlots,
261 | includeVote: options?.includeVote
262 | });
263 | } else {
264 | // Otherwise, fall back to account keys method for reliability
265 | console.log("No serialized transaction provided, using account keys method");
266 | return await estimatePriorityFeeByAccountKeys(connection, options?.accountKeys, {
267 | lookbackSlots: options?.lookbackSlots,
268 | includeVote: options?.includeVote
269 | });
270 | }
271 | } else {
272 | return await estimatePriorityFeeByAccountKeys(connection, options?.accountKeys, {
273 | lookbackSlots: options?.lookbackSlots,
274 | includeVote: options?.includeVote
275 | });
276 | }
277 | } catch (error) {
278 | console.error("Error estimating priority fee:", error);
279 | return getDefaultPriorityFees();
280 | }
281 | }
282 |
283 | /**
284 | * Returns default priority fee values when API calls fail
285 | */
286 | function getDefaultPriorityFees(): PriorityFeeEstimate {
287 | return {
288 | low: 1000,
289 | medium: 10000,
290 | high: 100000,
291 | veryHigh: 150000,
292 | min: 0,
293 | unsafeMax: 200000,
294 | suggested: 10000,
295 | timeEstimates: {
296 | low: "1-2 blocks (~0.8s)",
297 | medium: "1 block (~0.4s)",
298 | high: "Usually immediate"
299 | },
300 | highLoadBehavior: {
301 | low: "May be delayed or dropped",
302 | medium: "More consistent inclusion",
303 | high: "Very likely first-in"
304 | },
305 | priorityFeeEstimate: 10000,
306 | priorityFeeLevels: {
307 | min: 0,
308 | low: 1000,
309 | medium: 10000,
310 | high: 100000,
311 | veryHigh: 150000,
312 | unsafeMax: 200000
313 | }
314 | };
315 | }
316 |
317 | // Helper function to create a ComputeBudgetInstruction with the priority fee
318 | export function createPriorityFeeInstruction(microLamports: number) {
319 | return ComputeBudgetProgram.setComputeUnitPrice({
320 | microLamports
321 | });
322 | }
323 |
```