# 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:
--------------------------------------------------------------------------------
```
.env
node_modules
build
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
# Solana Agent Kit MCP Server
[](https://www.npmjs.com/package/solana-mpc)
[](https://opensource.org/licenses/ISC)
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.
# DEMO VIDEO
https://www.youtube.com/watch?v=VbfSzFuIzn8
# Actions
### GET_VALIDATOR_INFO
Retrieves detailed information about Solana validators
Shows stake amounts, commission rates, and performance metrics
### GET_PRIORITY_FEE_ESTIMATE
Estimates optimal transaction fees based on current network conditions
Provides different fee tiers (low, medium, high) with expected confirmation times
### GET_TRANSACTION_HISTORY
Fetches transaction history for any Solana wallet or token account
Supports filtering by transaction types and pagination
### GET_SECURITY_TXT
Extracts security contact information from Solana programs
Helps users find proper channels for reporting vulnerabilities
Integrated Functions
Your MCP server also includes core Solana functionality for:
Asset management (GET_ASSET, MINT_NFT)
Token operations (DEPLOY_TOKEN, TRANSFER, TRADE)
Network information (GET_TPS, BALANCE)
Utility functions (REQUEST_FUNDS, REGISTER_DOMAIN)
These functions together provide a comprehensive interface for interacting with the Solana blockchain through a unified MCP server.
```
--------------------------------------------------------------------------------
/src/types/action.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from "zod";
import { SolanaAgentKit } from "solana-agent-kit";
export interface Action {
name: string;
similes: string[];
description: string;
examples: any[];
schema: z.ZodObject<any>;
handler: (agent: SolanaAgentKit, input: Record<string, any>) => Promise<any>;
}
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./build",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
```
--------------------------------------------------------------------------------
/claude_desktop_config.json:
--------------------------------------------------------------------------------
```json
{
"mcpServers": {
"agent-kit": {
"command": "node",
"env" : {
"OPENAI_API_KEY": "optional_openai_api_key_here",
"RPC_URL": "your_rpc_url_here",
"SOLANA_PRIVATE_KEY": "your_private_key_here"
},
"args": [
"/absolute/path/to/build/index.js"
]
}
}
}
```
--------------------------------------------------------------------------------
/src/actions/index.ts:
--------------------------------------------------------------------------------
```typescript
import getValidatorInfoAction from "./validator/getValidatorInfo.js";
import getPriorityFeeEstimateAction from "./priorityFee/getPriorityFeeEstimate.js";
import getTransactionHistoryAction from "./transaction/getTransactionHistory.js";
import getSecurityTxtAction from "./security/getSecurityTxt.js";
// Export all actions
export const ACTIONS = {
GET_VALIDATOR_INFO_ACTION: getValidatorInfoAction,
GET_PRIORITY_FEE_ESTIMATE_ACTION: getPriorityFeeEstimateAction,
GET_TRANSACTION_HISTORY_ACTION: getTransactionHistoryAction,
GET_SECURITY_TXT_ACTION: getSecurityTxtAction,
};
// Export individual actions for direct imports
export { default as GET_VALIDATOR_INFO_ACTION } from "./validator/getValidatorInfo.js";
export { default as GET_PRIORITY_FEE_ESTIMATE_ACTION } from "./priorityFee/getPriorityFeeEstimate.js";
export { default as GET_TRANSACTION_HISTORY_ACTION } from "./transaction/getTransactionHistory.js";
export { default as GET_SECURITY_TXT_ACTION } from "./security/getSecurityTxt.js";
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
{
"name": "solana-mcp-sendai-hackathon",
"version": "1.0.0",
"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)",
"main": "build/index.js",
"type": "module",
"bin": {
"solana-mcp": "./build/index.js"
},
"scripts": {
"build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"",
"start": "node build/index.js",
"dev": "tsx watch src/index.ts"
},
"files": [
"build"
],
"repository": {
"type": "git",
"url": "https://github.com/cryptoleek/solana-mcp-sendai-hackathon.git"
},
"keywords": [
"solana",
"mcp",
"solana-agent-kit",
"solana-mcp"
],
"author": "cryptoleek",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.6.1",
"@solana/spl-memo": "^0.2.5",
"@solana/spl-token": "^0.4.13",
"@solana/web3.js": "^1.98.0",
"bs58": "^6.0.0",
"dotenv": "^16.4.7",
"helius-sdk": "^1.4.2",
"solana-agent-kit": "1.4.8",
"zod": "^3.24.2"
},
"devDependencies": {
"@types/node": "^22.13.4",
"ts-node": "^10.9.2",
"typescript": "^5.7.3"
},
"packageManager": "[email protected]"
}
```
--------------------------------------------------------------------------------
/src/tools/validator.ts:
--------------------------------------------------------------------------------
```typescript
import { Connection, PublicKey } from "@solana/web3.js";
export async function getValidatorInfo(validatorPubkey: PublicKey, connection: Connection) {
try {
// Get the current epoch info
const epochInfo = await connection.getEpochInfo();
// Get vote accounts to find our validator
const voteAccounts = await connection.getVoteAccounts();
// Find the validator in either current or delinquent vote accounts
const allAccounts = [...voteAccounts.current, ...voteAccounts.delinquent];
const validatorAccount = allAccounts.find(
account => account.votePubkey === validatorPubkey.toString()
);
if (!validatorAccount) {
throw new Error("Validator not found");
}
// Get validator's identity account balance
const balance = await connection.getBalance(new PublicKey(validatorAccount.nodePubkey));
return {
identity: validatorAccount.nodePubkey,
vote: validatorAccount.votePubkey,
commission: validatorAccount.commission,
activatedStake: validatorAccount.activatedStake,
epochVoteAccount: validatorAccount.epochVoteAccount,
epochCredits: validatorAccount.epochCredits,
delinquent: voteAccounts.delinquent.some(
account => account.votePubkey === validatorPubkey.toString()
),
lastVote: validatorAccount.lastVote,
balance: balance,
currentEpoch: epochInfo.epoch,
};
} catch (error) {
throw error;
}
}
```
--------------------------------------------------------------------------------
/src/tests/test-transaction-history.ts:
--------------------------------------------------------------------------------
```typescript
import { Connection, PublicKey } from '@solana/web3.js';
import * as dotenv from 'dotenv';
import { getTransactionHistory } from '../tools/transaction.js';
dotenv.config();
async function main() {
try {
// Validate environment variables
if (!process.env.RPC_URL) {
throw new Error('RPC_URL environment variable is required');
}
// Create a connection to the Solana cluster
const connection = new Connection(process.env.RPC_URL);
// Print connection info for debugging
console.log(`Using RPC URL: ${process.env.RPC_URL.substring(0, 30)}...`);
try {
const version = await connection.getVersion();
console.log(`Connected to Solana ${version["solana-core"]}`);
} catch (err) {
console.log(`Failed to get version: ${err}`);
}
// Test address - using the provided address
const testAddress = new PublicKey('DWdBJfMzVXJCB3TMdFzxhTeap6pMQCajamApbqXHbkQ4');
console.log(`Fetching transaction history for address: ${testAddress.toString()}`);
// Test with default options
console.log('\n--- Test 1: Retrieve all txns ---');
const defaultResults = await getTransactionHistory(testAddress, connection);
console.log(`Retrieved ${defaultResults.length} transactions`);
// Test with options
console.log('\n--- Test 2: Options (limit: 10, type: TRANSFER) ---');
const options = {
limit: 10,
types: ['TRANSFER']
};
const optionResults = await getTransactionHistory(testAddress, connection, options);
console.log(`Retrieved ${optionResults.length} transactions`);
console.log('--- Printing the first 10 transactions ---');
for (const txn of optionResults.slice(0, 10)) {
console.log(txn);
}
console.log('\nAll tests completed!');
} catch (error) {
console.error('Error running tests:', error);
process.exit(1);
}
}
main();
```
--------------------------------------------------------------------------------
/src/actions/validator/getValidatorInfo.ts:
--------------------------------------------------------------------------------
```typescript
import { Action } from "../../types/action.js";
import { SolanaAgentKit } from "solana-agent-kit";
import { z } from "zod";
import { PublicKey } from "@solana/web3.js";
import { getValidatorInfo } from "../../tools/validator.js";
const getValidatorInfoAction: Action = {
name: "GET_VALIDATOR_INFO",
similes: [
"validator status",
"check validator",
"validator info",
"validator details",
"node information",
],
description:
"Get detailed information about a Solana validator including stake, commission, and performance",
examples: [
[
{
input: {
validatorAddress: "he1iusunGwqrNtafDtLdhsUQDFvo13z9sUa36PauBtk",
},
output: {
status: "success",
info: {
identity: "HEL1USMZKAL2odpNBj2oCjffnFGaYwmbGmyewGv1e2TU",
vote: "he1iusunGwqrNtafDtLdhsUQDFvo13z9sUa36PauBtk",
commission: 10,
activatedStake: 1520000000,
delinquent: false,
skipRate: 0.0234,
},
message: "Successfully retrieved validator information",
},
explanation: "Get information about a specific Solana validator",
},
],
],
schema: z.object({
validatorAddress: z
.string()
.describe("The public key of the validator to get information about"),
}),
handler: async (agent: SolanaAgentKit, input: Record<string, any>) => {
try {
const validatorPubkey = new PublicKey(input.validatorAddress);
const connection = agent.connection;
const validatorInfo = await getValidatorInfo(validatorPubkey, connection);
return {
status: "success",
info: validatorInfo,
message: "Successfully retrieved validator information",
};
} catch (error: any) {
return {
status: "error",
message: `Failed to get validator info: ${error.message}`,
};
}
},
};
export default getValidatorInfoAction;
```
--------------------------------------------------------------------------------
/src/actions/security/getSecurityTxt.ts:
--------------------------------------------------------------------------------
```typescript
import { Action } from "../../types/action.js";
import { SolanaAgentKit } from "solana-agent-kit";
import { z } from "zod";
import { PublicKey } from "@solana/web3.js";
import { getSecurityTxtInfo } from "../../tools/security.js";
const getSecurityTxtAction: Action = {
name: "GET_SECURITY_TXT",
similes: [
"security.txt",
"security contact",
"program security",
"security disclosure",
"vulnerability reporting",
"security information",
"contact maintainers",
"security policy",
],
description:
"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",
examples: [
[
{
input: {
programId: "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
},
output: {
status: "success",
info: {
contact: "mailto:[email protected]",
expires: "2023-12-31T23:59:59.000Z",
encryption: "https://solana.com/pgp-key.txt",
acknowledgments: "https://solana.com/hall-of-fame",
preferredLanguages: "en",
},
message: "Successfully retrieved security.txt information",
},
explanation: "Get security contact information for the Solana Token Program",
},
],
],
schema: z.object({
programId: z
.string()
.describe("The program ID (public key) of the Solana program to inspect"),
}),
handler: async (agent: SolanaAgentKit, input: Record<string, any>) => {
try {
const programPubkey = new PublicKey(input.programId);
const connection = agent.connection;
const securityInfo = await getSecurityTxtInfo(programPubkey, connection);
// If we couldn't find any security information
if (Object.keys(securityInfo).length === 0) {
return {
status: "warning",
info: null,
message: "No security.txt information found for this program",
};
}
return {
status: "success",
info: securityInfo,
message: "Successfully retrieved security.txt information",
};
} catch (error: any) {
return {
status: "error",
message: `Failed to get security.txt info: ${error.message}`,
};
}
},
};
export default getSecurityTxtAction;
```
--------------------------------------------------------------------------------
/src/tests/test-priority-fee.ts:
--------------------------------------------------------------------------------
```typescript
#!/usr/bin/env node
/**
* Simple test script to debug the priority fee estimation tool
* Tests both account keys and transaction-based methods
*/
import { Connection } from '@solana/web3.js';
import {
estimatePriorityFee,
estimatePriorityFeeByAccountKeys,
estimatePriorityFeeByTransaction,
PriorityFeeEstimate
} from '../tools/priorityFee.js';
import * as dotenv from "dotenv";
dotenv.config();
// Set up Solana connection (using a public RPC endpoint)
// const RPC_ENDPOINT = 'https://api.mainnet-beta.solana.com';
// For Helius API, you might want to use your own endpoint with API key
const RPC_ENDPOINT = process.env.RPC_URL;
async function testPriorityFeeEstimate() {
try {
console.log('=== Testing Priority Fee Estimation ===');
console.log('Using RPC endpoint:', RPC_ENDPOINT);
// Create Solana connection
const connection = new Connection(RPC_ENDPOINT || "");
// Test 1: Default method (account keys with default Jupiter account)
console.log('\n--- Test 1: Default method ---');
const defaultEstimate = await estimatePriorityFee(connection);
printEstimate(defaultEstimate);
// Test 2: Account keys method with custom accounts
console.log('\n--- Test 2: Account keys method with custom accounts ---');
const customAccountsEstimate = await estimatePriorityFeeByAccountKeys(
connection,
[
'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA', // Token program
'ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL' // Associated token program
]
);
printEstimate(customAccountsEstimate);
// Test 3: Transaction method
console.log('\n--- Test 3: Transaction method ---');
const transactionEstimate = await estimatePriorityFeeByTransaction(connection);
printEstimate(transactionEstimate);
console.log('\n=== All tests completed ===');
} catch (error: any ) {
console.error('Failed to run tests:', error);
if (error.cause) {
console.error('Error cause:', error.cause);
}
}
}
/**
* Helper function to print the fee estimate in a readable format
*/
function printEstimate(estimate: PriorityFeeEstimate) {
console.log('Priority Fee Estimate:');
console.log('- Low:', estimate.low, 'microLamports');
console.log('- Medium:', estimate.medium, 'microLamports');
console.log('- High:', estimate.high, 'microLamports');
console.log('- Suggested:', estimate.suggested, 'microLamports');
console.log('- Time Estimates:', estimate.timeEstimates);
console.log('- High Load Behavior:', estimate.highLoadBehavior);
}
// Run the test
await testPriorityFeeEstimate()
.catch(err => {
console.error('Unhandled error:', err);
process.exit(1);
});
```
--------------------------------------------------------------------------------
/src/tools/transaction.ts:
--------------------------------------------------------------------------------
```typescript
import { Connection, PublicKey } from '@solana/web3.js';
import { parseTransaction } from 'solana-agent-kit/dist/tools/index.js';
// Interface for transaction history options
interface TransactionHistoryOptions {
limit?: number;
before?: string;
until?: string;
types?: string[];
minContextSlot?: number;
}
// Interface for transaction data
interface TransactionData {
signature: string;
slot: number;
timestamp: number;
err: any;
memo: string | null;
blockTime: number;
type: string;
fee: number;
status: string;
}
/**
* Get transaction history for a Solana wallet address or token account using Helius API
* @param publicKey - The public key to get transaction history for
* @param connection - Solana connection object (not used with Helius but kept for compatibility)
* @param options - Optional parameters for filtering transactions
* @returns Array of transaction data
*/
export async function getTransactionHistory(
publicKey: PublicKey,
connection: Connection,
options: TransactionHistoryOptions = {}
): Promise<TransactionData[]> {
try {
const address = publicKey.toString();
// Helius SDK doesn't have a direct method for transaction history
// We'll use the fetchTransactionHistory helper function to make the API call
const transactions = await fetchTransactionHistory(connection, address, options);
return transactions;
} catch (error) {
console.error('Error fetching transaction history:', error);
throw error;
}
}
/**
* Helper function to fetch transaction history using Helius API directly
* @param address - The address to get transaction history for
* @param options - Query parameters
* @returns Array of transaction data from Helius
*/
async function fetchTransactionHistory(
connection: Connection,
address: string,
options: TransactionHistoryOptions = {}
): Promise<TransactionData[]> {
try {
// Build query parameters
const queryParams: Record<string, string> = {};
if (options.limit) {
queryParams.limit = options.limit.toString();
}
if (options.before) {
queryParams.before = options.before;
}
if (options.until) {
queryParams.until = options.until;
}
if (options.types && options.types.length > 0) {
queryParams.type = options.types.join(',');
}
// Convert params to URL query string
const queryString = Object.entries(queryParams)
.map(([key, value]) => `${key}=${encodeURIComponent(value)}`)
.join('&');
const rpcUrl = connection.rpcEndpoint;
const apiKey = rpcUrl.split('api-key=')[1];
if (!apiKey) {
throw new Error('Missing Helius API key');
}
// Build the URL
const url = `https://api.helius.xyz/v0/addresses/${address}/transactions?api-key=${apiKey}${queryString ? `&${queryString}` : ''}`;
// Fetch data from Helius API
const response = await fetch(url);
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Helius API error: ${response.status} ${errorText}`);
}
const data = await response.json();
// Map the response to our expected format
return data.map((tx: any) => ({
signature: tx.signature,
slot: tx.slot,
timestamp: tx.timestamp,
err: tx.err,
memo: tx.memo || null,
blockTime: tx.timestamp,
type: tx.type || 'Unknown',
fee: tx.fee || 0,
status: tx.err ? 'Failed' : 'Success'
}));
} catch (error) {
console.error('Error in fetchTransactionHistory:', error);
throw error;
}
}
```
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
```typescript
#!/usr/bin/env node
import { ACTIONS as SAK_ACTIONS, SolanaAgentKit, startMcpServer } from "solana-agent-kit";
import { ACTIONS as CUSTOM_ACTIONS } from "./actions/index.js";
import * as dotenv from "dotenv";
dotenv.config();
// Validate required environment variables
function validateEnvironment() {
const requiredEnvVars = {
'SOLANA_PRIVATE_KEY': process.env.SOLANA_PRIVATE_KEY,
'RPC_URL': process.env.RPC_URL
};
const missingVars = Object.entries(requiredEnvVars)
.filter(([_, value]) => !value)
.map(([key]) => key);
if (missingVars.length > 0) {
throw new Error(`Missing required environment variables: ${missingVars.join(', ')}`);
}
}
async function main() {
try {
// Validate environment before proceeding
validateEnvironment();
const RPC_URL = process.env.RPC_URL as string
const HELIUS_API_KEY = RPC_URL.split('api-key=')[1]
// Initialize the agent with error handling
const agent = new SolanaAgentKit(
process.env.SOLANA_PRIVATE_KEY!,
process.env.RPC_URL!,
{
OPENAI_API_KEY: process.env.OPENAI_API_KEY || "",
PERPLEXITY_API_KEY: process.env.PERPLEXITY_API_KEY || "",
HELIUS_API_KEY: HELIUS_API_KEY || ""
}
);
// Debug: Log our custom actions
console.log("Registering custom actions:");
console.log("GET_VALIDATOR_INFO_ACTION schema:", CUSTOM_ACTIONS.GET_VALIDATOR_INFO_ACTION.schema);
console.log("GET_PRIORITY_FEE_ESTIMATE_ACTION schema:", CUSTOM_ACTIONS.GET_PRIORITY_FEE_ESTIMATE_ACTION.schema);
console.log("GET_TRANSACTION_HISTORY_ACTION schema:", CUSTOM_ACTIONS.GET_TRANSACTION_HISTORY_ACTION.schema);
console.log("GET_SECURITY_TXT_ACTION schema:", CUSTOM_ACTIONS.GET_SECURITY_TXT_ACTION.schema);
const mcp_actions = {
GET_ASSET: SAK_ACTIONS.GET_ASSET_ACTION,
DEPLOY_TOKEN: SAK_ACTIONS.DEPLOY_TOKEN_ACTION,
FETCH_PRICE: SAK_ACTIONS.FETCH_PRICE_ACTION,
GET_WALLET_ADDRESS: SAK_ACTIONS.WALLET_ADDRESS_ACTION,
BALANCE: SAK_ACTIONS.BALANCE_ACTION,
TRANSFER: SAK_ACTIONS.TRANSFER_ACTION,
MINT_NFT: SAK_ACTIONS.MINT_NFT_ACTION,
TRADE: SAK_ACTIONS.TRADE_ACTION,
REQUEST_FUNDS: SAK_ACTIONS.REQUEST_FUNDS_ACTION,
REGISTER_DOMAIN: SAK_ACTIONS.RESOLVE_DOMAIN_ACTION,
GET_TPS: SAK_ACTIONS.GET_TPS_ACTION,
PARSE_TRANSACTION_ACTION: SAK_ACTIONS.PARSE_TRANSACTION_ACTION,
// Add our custom actions
GET_VALIDATOR_INFO: CUSTOM_ACTIONS.GET_VALIDATOR_INFO_ACTION,
GET_PRIORITY_FEE_ESTIMATE: CUSTOM_ACTIONS.GET_PRIORITY_FEE_ESTIMATE_ACTION,
GET_TRANSACTION_HISTORY: CUSTOM_ACTIONS.GET_TRANSACTION_HISTORY_ACTION,
GET_SECURITY_TXT: CUSTOM_ACTIONS.GET_SECURITY_TXT_ACTION,
};
// Debug: Log all registered actions
console.log("All registered MCP actions:", Object.keys(mcp_actions));
// Start the MCP server with error handling
await startMcpServer(mcp_actions, agent, {
name: "solana-agent",
version: "0.0.1"
});
// Add console logging for debugging
console.log("MCP server started successfully");
} catch (error) {
console.error('Failed to start MCP server:', error instanceof Error ? error.message : String(error));
process.exit(1);
}
}
// Handle uncaught exceptions and rejections
process.on('uncaughtException', (error) => {
console.error('Uncaught Exception:', error);
process.exit(1);
});
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
process.exit(1);
});
main();
```
--------------------------------------------------------------------------------
/src/actions/priorityFee/getPriorityFeeEstimate.ts:
--------------------------------------------------------------------------------
```typescript
import { Action } from "../../types/action.js";
import { SolanaAgentKit } from "solana-agent-kit";
import { z } from "zod";
import { estimatePriorityFee } from "../../tools/priorityFee.js";
const getPriorityFeeEstimateAction: Action = {
name: "GET_PRIORITY_FEE_ESTIMATE",
similes: [
"estimate priority fee",
"transaction fee estimate",
"optimal priority fee",
"solana fee estimate",
"transaction cost estimate",
],
description:
"Estimates optimal priority fees for Solana transactions based on recent network activity",
examples: [
[
{
input: {},
output: {
status: "success",
estimate: {
low: 1000,
medium: 10000,
high: 100000,
suggested: 12000,
timeEstimates: {
low: "3-5 seconds",
medium: "2-3 seconds",
high: "1-2 seconds"
}
},
message: "Successfully estimated priority fees",
},
explanation: "Get current priority fee estimates for Solana transactions",
},
],
[
{
input: { method: "transaction" },
output: {
status: "success",
estimate: {
low: 1000,
medium: 10000,
high: 100000,
suggested: 12000,
timeEstimates: {
low: "3-5 seconds",
medium: "2-3 seconds",
high: "1-2 seconds"
}
},
message: "Successfully estimated priority fees using transaction method",
},
explanation: "Get priority fee estimates using the transaction-based method",
},
],
[
{
input: {
method: "accountKeys",
accountKeys: ["TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"]
},
output: {
status: "success",
estimate: {
low: 1000,
medium: 10000,
high: 100000,
suggested: 12000,
timeEstimates: {
low: "3-5 seconds",
medium: "2-3 seconds",
high: "1-2 seconds"
}
},
message: "Successfully estimated priority fees for specific account keys",
},
explanation: "Get priority fee estimates for specific account keys",
},
],
],
// Define schema with optional parameters
schema: z.object({
method: z.enum(["accountKeys", "transaction"]).optional(),
accountKeys: z.array(z.string()).optional(),
serializedTransaction: z.string().optional(),
}),
handler: async (agent: SolanaAgentKit, params: any) => {
try {
const connection = agent.connection;
// Extract parameters
const method = params.method;
const accountKeys = params.accountKeys;
const serializedTransaction = params.serializedTransaction;
// Call the estimatePriorityFee function with the provided parameters
const feeEstimate = await estimatePriorityFee(connection, {
method,
accountKeys,
serializedTransaction
});
// Prepare success message based on the method used
let message = "Successfully estimated priority fees";
if (method === "transaction") {
message = "Successfully estimated priority fees using transaction method";
} else if (method === "accountKeys" && accountKeys) {
message = "Successfully estimated priority fees for specific account keys";
}
return {
status: "success",
estimate: feeEstimate,
message,
};
} catch (error: any) {
return {
status: "error",
message: `Failed to estimate priority fees: ${error.message}`,
error: error.toString(),
stack: error.stack
};
}
},
};
export default getPriorityFeeEstimateAction;
```
--------------------------------------------------------------------------------
/src/tools/security.ts:
--------------------------------------------------------------------------------
```typescript
import { Connection, PublicKey } from "@solana/web3.js";
import * as bs58 from "bs58";
// Interface for security.txt content
export interface SecurityTxtInfo {
contact?: string;
expires?: string;
encryption?: string;
acknowledgments?: string;
preferredLanguages?: string;
canonical?: string;
policy?: string;
hiring?: string;
// Any additional fields that might be in the security.txt
[key: string]: string | undefined;
}
/**
* Extracts and parses security.txt information from a Solana program
*
* @param programId - The PublicKey of the Solana program to inspect
* @param connection - The Solana connection object
* @returns The parsed security.txt information
*/
export async function getSecurityTxtInfo(programId: PublicKey, connection: Connection): Promise<SecurityTxtInfo> {
try {
// Get the program account data
const programAccount = await connection.getAccountInfo(programId);
if (!programAccount) {
throw new Error("Program account not found");
}
// Check if this is an upgradeable program
const BPF_UPGRADEABLE_LOADER_ID = new PublicKey("BPFLoaderUpgradeab1e11111111111111111111111");
let programData: Buffer;
if (programAccount.owner.equals(BPF_UPGRADEABLE_LOADER_ID)) {
// This is an upgradeable program, we need to find the program data account
// The first 4 bytes indicate the account type
const accountType = programAccount.data.slice(0, 4);
// Check if this is a Program account (type 2)
if (accountType[0] === 2) {
// Extract the program data address (next 32 bytes)
const programDataAddress = new PublicKey(programAccount.data.slice(4, 36));
// Get the program data account
const programDataAccount = await connection.getAccountInfo(programDataAddress);
if (!programDataAccount) {
throw new Error("Program data account not found");
}
// The program data starts after the header (first 8 bytes)
// First byte is account type, next 7 bytes are the slot
programData = programDataAccount.data.slice(8);
} else {
throw new Error("Not a valid upgradeable program account");
}
} else {
// For non-upgradeable programs, use the account data directly
programData = programAccount.data;
}
// Now search for security.txt in the program data
return findAndParseSecurityTxt(programData);
} catch (error) {
throw error;
}
}
/**
* Finds and parses security.txt content in program data
*
* @param programData - The program binary data
* @returns Parsed security.txt information
*/
function findAndParseSecurityTxt(programData: Buffer): SecurityTxtInfo {
try {
// Convert the binary data to a string
// Note: This is a simplification, as the security.txt might be encoded in various ways
const dataString = new TextDecoder().decode(programData);
// Look for security.txt pattern
// Pattern 1: Standard security.txt format
const securityTxtMatch = dataString.match(/(?:^|\n)# ?security\.txt(?:\n|$)([\s\S]*?)(?:\n\n|\n#|$)/i);
if (securityTxtMatch && securityTxtMatch[1]) {
return parseSecurityTxt(securityTxtMatch[1]);
}
// Pattern 2: Look for BEGIN/END SECURITY.TXT markers
const securityTxtBlockMatch = dataString.match(/-----BEGIN SECURITY\.TXT-----([\s\S]*?)-----END SECURITY\.TXT-----/i);
if (securityTxtBlockMatch && securityTxtBlockMatch[1]) {
return parseSecurityTxt(securityTxtBlockMatch[1]);
}
// Pattern 3: Look for common security.txt fields
const fieldMatch = dataString.match(/Contact: ([^\n]+)/i) ||
dataString.match(/Security-Contact: ([^\n]+)/i) ||
dataString.match(/mailto:([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/i);
if (fieldMatch) {
const result: SecurityTxtInfo = {};
result.contact = fieldMatch[1].trim();
return result;
}
// If we couldn't find any specific security.txt format, try to extract any security-related info
return extractSecurityInfoFromText(dataString);
} catch (error) {
// If we encounter any error during parsing, return an empty object
console.error("Error parsing security.txt:", error);
return {};
}
}
/**
* Parses security.txt content into a structured object
*
* @param content - The raw security.txt content
* @returns Structured security.txt information
*/
function parseSecurityTxt(content: string): SecurityTxtInfo {
const result: SecurityTxtInfo = {};
// Split by lines and process each line
const lines = content.split('\n');
for (const line of lines) {
// Skip empty lines and comments
if (!line.trim() || line.trim().startsWith('#')) {
continue;
}
// Extract field and value
const match = line.match(/^([^:]+):\s*(.*)$/);
if (match) {
const [_, field, value] = match;
const fieldName = field.trim().toLowerCase();
// Convert kebab-case to camelCase
const camelCaseField = fieldName.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
result[camelCaseField] = value.trim();
}
}
return result;
}
/**
* Attempts to extract security information from program text when no explicit security.txt is found
*
* @param text - The program text to analyze
* @returns Any security information found
*/
function extractSecurityInfoFromText(text: string): SecurityTxtInfo {
const result: SecurityTxtInfo = {};
// Look for common patterns that might indicate security contact info
const contactMatches = [
// Email patterns
...text.match(/security@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g) || [],
...text.match(/mailto:[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g) || [],
// URL patterns
...text.match(/https?:\/\/[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}\/security/g) || [],
...text.match(/https?:\/\/[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}\/responsible-disclosure/g) || [],
...text.match(/https?:\/\/[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}\/vulnerability/g) || [],
];
if (contactMatches.length > 0) {
result.contact = contactMatches[0];
}
// Look for PGP key patterns
const pgpMatches = text.match(/-----BEGIN PGP PUBLIC KEY BLOCK-----([\s\S]*?)-----END PGP PUBLIC KEY BLOCK-----/g);
if (pgpMatches) {
result.encryption = "PGP key found in program data";
}
return result;
}
```
--------------------------------------------------------------------------------
/src/actions/transaction/getTransactionHistory.ts:
--------------------------------------------------------------------------------
```typescript
import { Action } from "../../types/action.js";
import { SolanaAgentKit } from "solana-agent-kit";
import { z } from "zod";
import { PublicKey } from "@solana/web3.js";
import { getTransactionHistory } from "../../tools/transaction.js";
import dotenv from "dotenv";
// Load environment variables
dotenv.config();
const getTransactionHistoryAction: Action = {
name: "GET_TRANSACTION_HISTORY",
similes: [
"transaction history",
"account transactions",
"transaction list",
"tx history",
"recent transactions",
],
description:
"Get transaction history for a Solana wallet address or token account using Helius API",
examples: [
[
{
input: {
address: "8zFZHuSRuDpuAR7J6FzwyF3vKNx4CVW3DFHJerQhc7Zd"
},
output: {
status: "success",
transactions: [
{
signature: "5UJpjrKQ8641Q8kPudtvtqR2SgZMz5rSbdpuNP1qy6BxAw4aY3bRfyZQqGKEK5yQXi3yk4pVMRLqzYjnb3bawRn5",
slot: 172492080,
timestamp: 1678901234,
err: null,
memo: null,
blockTime: 1678901234,
type: "System Transfer",
fee: 5000,
status: "Success"
}
],
message: "Successfully retrieved transaction history"
},
explanation: "Get recent transaction history for a specific Solana address",
},
],
[
{
input: {
address: "8zFZHuSRuDpuAR7J6FzwyF3vKNx4CVW3DFHJerQhc7Zd",
limit: 5
},
output: {
status: "success",
transactions: [
{
signature: "5UJpjrKQ8641Q8kPudtvtqR2SgZMz5rSbdpuNP1qy6BxAw4aY3bRfyZQqGKEK5yQXi3yk4pVMRLqzYjnb3bawRn5",
slot: 172492080,
timestamp: 1678901234,
err: null,
memo: null,
blockTime: 1678901234,
type: "System Transfer",
fee: 5000,
status: "Success"
}
],
message: "Successfully retrieved 5 most recent transactions"
},
explanation: "Get the 5 most recent transactions for a Solana address",
},
],
[
{
input: {
address: "8zFZHuSRuDpuAR7J6FzwyF3vKNx4CVW3DFHJerQhc7Zd",
before: "5UJpjrKQ8641Q8kPudtvtqR2SgZMz5rSbdpuNP1qy6BxAw4aY3bRfyZQqGKEK5yQXi3yk4pVMRLqzYjnb3bawRn5"
},
output: {
status: "success",
transactions: [
{
signature: "4tSRZ8QVNfUyHuJGZQvJzuUbq3nBpZ9QFNEsrey9mEbY6iN7VDuZtFGBciogSGkAiKwbVL8YgYNJZP1XNqXhRmML",
slot: 172492070,
timestamp: 1678901200,
err: null,
memo: null,
blockTime: 1678901200,
type: "Token Transfer",
fee: 5000,
status: "Success"
}
],
message: "Successfully retrieved transaction history before specified signature"
},
explanation: "Get transaction history before a specific transaction signature",
},
],
[
{
input: {
address: "8zFZHuSRuDpuAR7J6FzwyF3vKNx4CVW3DFHJerQhc7Zd",
types: ["NFT_SALE", "NFT_LISTING"]
},
output: {
status: "success",
transactions: [
{
signature: "4tSRZ8QVNfUyHuJGZQvJzuUbq3nBpZ9QFNEsrey9mEbY6iN7VDuZtFGBciogSGkAiKwbVL8YgYNJZP1XNqXhRmML",
slot: 172492070,
timestamp: 1678901200,
err: null,
memo: null,
blockTime: 1678901200,
type: "NFT_SALE",
fee: 5000,
status: "Success"
}
],
message: "Successfully retrieved NFT sales and listings transactions"
},
explanation: "Get NFT sales and listings transactions for a Solana address",
},
],
],
// Define schema with optional address and optional parameters
schema: z.object({
address: z.string().min(32).max(44).optional(),
limit: z.number().min(1).max(100).optional(),
before: z.string().optional(),
until: z.string().optional(),
minContextSlot: z.number().optional(),
types: z.array(z.string()).optional(),
}),
handler: async (agent: SolanaAgentKit, params: any) => {
try {
// Check if address is provided
if (!params.address) {
return {
status: "input_needed",
message: "Please provide a Solana wallet address to view transaction history.",
error: "Missing address parameter"
};
}
const connection = agent.connection;
// Extract parameters
const address = params.address;
const limit = params.limit || 10;
const before = params.before;
const until = params.until;
const minContextSlot = params.minContextSlot;
const types = params.types;
// Validate address
let publicKey: PublicKey;
try {
publicKey = new PublicKey(address);
} catch (error) {
return {
status: "error",
message: "Invalid Solana address provided",
error: "Invalid public key format"
};
}
// Call the getTransactionHistory function with the provided parameters
const transactions = await getTransactionHistory(publicKey, connection, {
limit,
before,
until,
minContextSlot,
types
});
// Prepare success message based on the parameters used
let message = "Successfully retrieved transaction history";
if (limit) {
message = `Successfully retrieved ${limit} most recent transactions`;
}
if (before) {
message = "Successfully retrieved transaction history before specified signature";
}
if (until) {
message = "Successfully retrieved transaction history until specified signature";
}
if (types && types.length > 0) {
message = `Successfully retrieved ${types.join(', ')} transactions`;
}
return {
status: "success",
transactions: transactions.map(tx => ({
signature: tx.signature,
slot: tx.slot,
timestamp: tx.timestamp,
err: tx.err,
memo: tx.memo,
blockTime: tx.blockTime,
type: tx.type,
fee: tx.fee,
status: tx.status
})),
message,
};
} catch (error: any) {
return {
status: "error",
message: `Failed to retrieve transaction history: ${error.message}`,
error: error.toString(),
stack: error.stack
};
}
},
};
export default getTransactionHistoryAction;
```
--------------------------------------------------------------------------------
/src/tools/priorityFee.ts:
--------------------------------------------------------------------------------
```typescript
import { Connection, ComputeBudgetProgram, Transaction, PublicKey, Keypair } from "@solana/web3.js";
export interface PriorityFeeEstimate {
low: number;
medium: number;
high: number;
veryHigh?: number;
min?: number;
unsafeMax?: number;
suggested: number;
timeEstimates: {
low: string;
medium: string;
high: string;
};
highLoadBehavior?: {
low: string;
medium: string;
high: string;
};
// Raw Helius API response
priorityFeeEstimate?: number;
priorityFeeLevels?: {
min?: number;
low: number;
medium: number;
high: number;
veryHigh?: number;
unsafeMax?: number;
};
}
/**
* Estimates priority fees using Helius API via account keys method
* @param connection Solana connection object
* @param accountKeys Optional array of account keys to use for estimation (defaults to Jupiter account)
* @param options Optional configuration for fee estimation
* @returns Priority fee estimate with different fee levels
*/
export async function estimatePriorityFeeByAccountKeys(
connection: Connection,
accountKeys: string[] = ["JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4"],
options?: {
lookbackSlots?: number;
includeVote?: boolean;
}
): Promise<PriorityFeeEstimate> {
try {
// Get RPC URL from connection
const rpcUrl = connection.rpcEndpoint;
// Make request to Helius API for priority fee estimate using account keys
const response = await fetch(rpcUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
jsonrpc: "2.0",
id: 1,
method: "getPriorityFeeEstimate",
params: [{
"accountKeys": accountKeys,
"options": {
"includeAllPriorityFeeLevels": true,
"lookbackSlots": options?.lookbackSlots || 150,
"includeVote": options?.includeVote !== undefined ? options.includeVote : true
}
}]
}),
});
const data = await response.json();
console.log(data)
if (data.error) {
throw new Error(`Helius API error: ${data.error.message}`);
}
// Extract fee levels from the response
const result = data.result;
// If using Helius API, the response will have priorityFeeLevels
if (result) {
const priorityFeeEstimate = result.priorityFeeEstimate || result.priorityFee;
const priorityFeeLevels = result.priorityFeeLevels;
if (priorityFeeLevels) {
const { min, low, medium, high, veryHigh, unsafeMax } = priorityFeeLevels;
// Use the recommended fee or calculate a suggested value based on medium
const suggested = priorityFeeEstimate || Math.ceil(medium * 1.2);
return {
low: low || 0,
medium: medium || 0,
high: high || 0,
veryHigh: veryHigh,
min: min,
unsafeMax: unsafeMax,
suggested,
timeEstimates: {
low: "1-2 blocks (~0.8s)",
medium: "1 block (~0.4s)",
high: "Usually immediate"
},
highLoadBehavior: {
low: "May be delayed or dropped",
medium: "More consistent inclusion",
high: "Very likely first-in"
},
// Include the raw Helius API response
priorityFeeEstimate,
priorityFeeLevels
};
}
}
// Fallback to default values if the API doesn't return expected format
return getDefaultPriorityFees();
} catch (error) {
console.error("Error fetching priority fee estimate by account keys:", error);
return getDefaultPriorityFees();
}
}
/**
* Estimates priority fees using Helius API via serialized transaction method
* @param connection Solana connection object
* @param serializedTransaction Base58 encoded serialized transaction (optional)
* @param options Optional configuration for fee estimation
* @returns Priority fee estimate with different fee levels
*/
export async function estimatePriorityFeeByTransaction(
connection: Connection,
serializedTransaction?: string,
options?: {
lookbackSlots?: number;
includeVote?: boolean;
}
): Promise<PriorityFeeEstimate> {
try {
// Get RPC URL from connection
const rpcUrl = connection.rpcEndpoint;
// If no serialized transaction is provided, create a dummy transaction
let encodedTransaction = serializedTransaction;
if (!encodedTransaction) {
try {
// Since the transaction-based method is having issues with encoding,
// let's fall back to the account keys method which is more reliable
console.log("Using account keys method as fallback for transaction method");
return await estimatePriorityFeeByAccountKeys(connection);
} catch (txError) {
console.error("Error creating dummy transaction:", txError);
// If we can't create a transaction, fall back to account keys method
return estimatePriorityFeeByAccountKeys(connection);
}
}
// If we have a serialized transaction (provided externally), use it
if (encodedTransaction) {
// Make request to Helius API for priority fee estimate using serialized transaction
const response = await fetch(rpcUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
jsonrpc: "2.0",
id: 1,
method: "getPriorityFeeEstimate",
params: [{
"transaction": encodedTransaction,
"options": {
"includeAllPriorityFeeLevels": true,
"lookbackSlots": options?.lookbackSlots || 150,
"includeVote": options?.includeVote !== undefined ? options.includeVote : true
}
}]
}),
});
const data = await response.json();
if (data.error) {
throw new Error(`Helius API error: ${data.error.message}`);
}
// Extract fee levels from the response
const result = data.result;
// If using Helius API, the response will have priorityFeeLevels
if (result) {
const priorityFeeEstimate = result.priorityFeeEstimate || result.priorityFee;
const priorityFeeLevels = result.priorityFeeLevels;
if (priorityFeeLevels) {
const { min, low, medium, high, veryHigh, unsafeMax } = priorityFeeLevels;
// Use the recommended fee or calculate a suggested value based on medium
const suggested = priorityFeeEstimate || Math.ceil(medium * 1.2);
return {
low: low || 0,
medium: medium || 0,
high: high || 0,
veryHigh: veryHigh,
min: min,
unsafeMax: unsafeMax,
suggested,
timeEstimates: {
low: "3-5 seconds",
medium: "2-3 seconds",
high: "1-2 seconds"
},
highLoadBehavior: {
low: "May be delayed or dropped",
medium: "More consistent inclusion",
high: "Very likely first-in"
},
// Include the raw Helius API response
priorityFeeEstimate,
priorityFeeLevels
};
}
}
}
// Fallback to default values if the API doesn't return expected format
return getDefaultPriorityFees();
} catch (error) {
console.error("Error fetching priority fee estimate by transaction:", error);
return getDefaultPriorityFees();
}
}
/**
* Main function to estimate priority fees using the preferred method
* @param connection Solana connection object
* @param options Optional configuration for fee estimation
* @returns Priority fee estimate with different fee levels
*/
export async function estimatePriorityFee(
connection: Connection,
options?: {
method?: 'accountKeys' | 'transaction';
accountKeys?: string[];
serializedTransaction?: string;
lookbackSlots?: number;
includeVote?: boolean;
}
): Promise<PriorityFeeEstimate> {
try {
const method = options?.method || 'accountKeys';
if (method === 'transaction') {
// If a serialized transaction is provided, use the transaction method
if (options?.serializedTransaction) {
return await estimatePriorityFeeByTransaction(connection, options.serializedTransaction, {
lookbackSlots: options?.lookbackSlots,
includeVote: options?.includeVote
});
} else {
// Otherwise, fall back to account keys method for reliability
console.log("No serialized transaction provided, using account keys method");
return await estimatePriorityFeeByAccountKeys(connection, options?.accountKeys, {
lookbackSlots: options?.lookbackSlots,
includeVote: options?.includeVote
});
}
} else {
return await estimatePriorityFeeByAccountKeys(connection, options?.accountKeys, {
lookbackSlots: options?.lookbackSlots,
includeVote: options?.includeVote
});
}
} catch (error) {
console.error("Error estimating priority fee:", error);
return getDefaultPriorityFees();
}
}
/**
* Returns default priority fee values when API calls fail
*/
function getDefaultPriorityFees(): PriorityFeeEstimate {
return {
low: 1000,
medium: 10000,
high: 100000,
veryHigh: 150000,
min: 0,
unsafeMax: 200000,
suggested: 10000,
timeEstimates: {
low: "1-2 blocks (~0.8s)",
medium: "1 block (~0.4s)",
high: "Usually immediate"
},
highLoadBehavior: {
low: "May be delayed or dropped",
medium: "More consistent inclusion",
high: "Very likely first-in"
},
priorityFeeEstimate: 10000,
priorityFeeLevels: {
min: 0,
low: 1000,
medium: 10000,
high: 100000,
veryHigh: 150000,
unsafeMax: 200000
}
};
}
// Helper function to create a ComputeBudgetInstruction with the priority fee
export function createPriorityFeeInstruction(microLamports: number) {
return ComputeBudgetProgram.setComputeUnitPrice({
microLamports
});
}
```