feat: Enhance Solana tools with action-based architecture

- Introduced action system for Solana tools, allowing for better modularity and maintainability.
- Updated SolanaBalanceTool, SolanaTransferTool, SolanaDeployTokenTool, SolanaDeployCollectionTool, SolanaMintNFTTool, SolanaTradeTool, and SolanaRequestFundsTool to utilize action handlers.
- Added new action exports in index.ts for better organization and accessibility.
This commit is contained in:
Fahri Bilici
2024-12-26 21:54:55 +01:00
parent 8d299244fc
commit 79cada2cbd
13 changed files with 825 additions and 165 deletions

59
src/actions/balance.ts Normal file
View File

@@ -0,0 +1,59 @@
import { PublicKey } from "@solana/web3.js";
import { Action } from "../types/action";
import { SolanaAgentKit } from "../agent";
import { z } from "zod";
const balanceAction: Action = {
name: "solana_balance",
similes: [
"check balance",
"get wallet balance",
"view balance",
"show balance",
"check token balance"
],
description: `Get the balance of a Solana wallet or token account.
If you want to get the balance of your wallet, you don't need to provide the tokenAddress.
If no tokenAddress is provided, the balance will be in SOL.`,
examples: [
[
{
input: {},
output: {
status: "success",
balance: "100",
token: "SOL"
},
explanation: "Get SOL balance of the wallet"
}
],
[
{
input: {
tokenAddress: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
},
output: {
status: "success",
balance: "1000",
token: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
},
explanation: "Get USDC token balance"
}
]
],
schema: z.object({
tokenAddress: z.string().optional()
}),
handler: async (agent: SolanaAgentKit, input: Record<string, any>) => {
const tokenAddress = input.tokenAddress ? new PublicKey(input.tokenAddress) : undefined;
const balance = await agent.getBalance(tokenAddress);
return {
status: "success",
balance: balance,
token: input.tokenAddress || "SOL"
};
}
};
export default balanceAction;

View File

@@ -0,0 +1,78 @@
import { PublicKey } from "@solana/web3.js";
import { Action } from "../types/action";
import { SolanaAgentKit } from "../agent";
import { z } from "zod";
interface CollectionOptions {
name: string;
uri: string;
royaltyBasisPoints?: number;
}
const deployCollectionAction: Action = {
name: "solana_deploy_collection",
similes: [
"create collection",
"launch collection",
"deploy nft collection",
"create nft collection",
"mint collection"
],
description: `Deploy a new NFT collection on Solana blockchain.`,
examples: [
[
{
input: {
name: "My Collection",
uri: "https://example.com/collection.json",
royaltyBasisPoints: 500
},
output: {
status: "success",
message: "Collection deployed successfully",
collectionAddress: "7nE9GvcwsqzYxmJLSrYmSB1V1YoJWVK1KWzAcWAzjXkN",
name: "My Collection"
},
explanation: "Deploy an NFT collection with 5% royalty"
}
],
[
{
input: {
name: "Basic Collection",
uri: "https://example.com/basic.json"
},
output: {
status: "success",
message: "Collection deployed successfully",
collectionAddress: "8nE9GvcwsqzYxmJLSrYmSB1V1YoJWVK1KWzAcWAzjXkM",
name: "Basic Collection"
},
explanation: "Deploy a basic NFT collection without royalties"
}
]
],
schema: z.object({
name: z.string().min(1, "Name is required"),
uri: z.string().url("URI must be a valid URL"),
royaltyBasisPoints: z.number().min(0).max(10000).optional()
}),
handler: async (agent: SolanaAgentKit, input: Record<string, any>) => {
const options: CollectionOptions = {
name: input.name,
uri: input.uri,
royaltyBasisPoints: input.royaltyBasisPoints
};
const result = await agent.deployCollection(options);
return {
status: "success",
message: "Collection deployed successfully",
collectionAddress: result.collectionAddress.toString(),
name: input.name
};
}
};
export default deployCollectionAction;

View File

@@ -0,0 +1,74 @@
import { PublicKey } from "@solana/web3.js";
import { Action, ActionExample } from "../types/action";
import { SolanaAgentKit } from "../agent";
import { z } from "zod";
const deployTokenAction: Action = {
name: "deploy_token",
similes: [
"create token",
"launch token",
"deploy new token",
"create new token",
"mint token",
],
description: "Deploy a new SPL token on the Solana blockchain with specified parameters",
examples: [
[
{
input: {
name: "My Token",
uri: "https://example.com/token.json",
symbol: "MTK",
decimals: 9,
initialSupply: 1000000
},
output: {
mint: "7nE9GvcwsqzYxmJLSrYmSB1V1YoJWVK1KWzAcWAzjXkN",
status: "success",
message: "Token deployed successfully"
},
explanation: "Deploy a token with initial supply and metadata"
}
],
[
{
input: {
name: "Basic Token",
uri: "https://example.com/basic.json",
symbol: "BASIC"
},
output: {
mint: "8nE9GvcwsqzYxmJLSrYmSB1V1YoJWVK1KWzAcWAzjXkM",
status: "success",
message: "Token deployed successfully"
},
explanation: "Deploy a basic token with minimal parameters"
}
]
],
schema: z.object({
name: z.string().min(1, "Name is required"),
uri: z.string().url("URI must be a valid URL"),
symbol: z.string().min(1, "Symbol is required"),
decimals: z.number().optional(),
initialSupply: z.number().optional()
}),
handler: async (agent: SolanaAgentKit, input: Record<string, any>) => {
const result = await agent.deployToken(
input.name,
input.uri,
input.symbol,
input.decimals,
input.initialSupply
);
return {
mint: result.mint.toString(),
status: "success",
message: "Token deployed successfully"
};
}
}
export default deployTokenAction;

20
src/actions/index.ts Normal file
View File

@@ -0,0 +1,20 @@
import deployTokenAction from "./deployToken";
import balanceAction from "./balance";
import transferAction from "./transfer";
import deployCollectionAction from "./deployCollection";
import mintNFTAction from "./mintNFT";
import tradeAction from "./trade";
import requestFundsAction from "./requestFunds";
export const actions = [
deployTokenAction,
balanceAction,
transferAction,
deployCollectionAction,
mintNFTAction,
tradeAction,
requestFundsAction,
// Add more actions here as they are implemented
];
export type { Action, ActionExample, Handler } from "../types/action";

88
src/actions/mintNFT.ts Normal file
View File

@@ -0,0 +1,88 @@
import { PublicKey } from "@solana/web3.js";
import { Action } from "../types/action";
import { SolanaAgentKit } from "../agent";
import { z } from "zod";
const mintNFTAction: Action = {
name: "solana_mint_nft",
similes: [
"mint nft",
"create nft",
"mint token",
"create token",
"add nft to collection"
],
description: `Mint a new NFT in a collection on Solana blockchain.`,
examples: [
[
{
input: {
collectionMint: "J1S9H3QjnRtBbbuD4HjPV6RpRhwuk4zKbxsnCHuTgh9w",
name: "My NFT",
uri: "https://example.com/nft.json"
},
output: {
status: "success",
message: "NFT minted successfully",
mintAddress: "7nE9GvcwsqzYxmJLSrYmSB1V1YoJWVK1KWzAcWAzjXkN",
metadata: {
name: "My NFT",
uri: "https://example.com/nft.json"
},
recipient: "7nE9GvcwsqzYxmJLSrYmSB1V1YoJWVK1KWzAcWAzjXkN"
},
explanation: "Mint an NFT to the default wallet"
}
],
[
{
input: {
collectionMint: "J1S9H3QjnRtBbbuD4HjPV6RpRhwuk4zKbxsnCHuTgh9w",
name: "Gift NFT",
uri: "https://example.com/gift.json",
recipient: "9aUn5swQzUTRanaaTwmszxiv89cvFwUCjEBv1vZCoT1u"
},
output: {
status: "success",
message: "NFT minted successfully",
mintAddress: "8nE9GvcwsqzYxmJLSrYmSB1V1YoJWVK1KWzAcWAzjXkM",
metadata: {
name: "Gift NFT",
uri: "https://example.com/gift.json"
},
recipient: "9aUn5swQzUTRanaaTwmszxiv89cvFwUCjEBv1vZCoT1u"
},
explanation: "Mint an NFT to a specific recipient"
}
]
],
schema: z.object({
collectionMint: z.string().min(32, "Invalid collection mint address"),
name: z.string().min(1, "Name is required"),
uri: z.string().url("URI must be a valid URL"),
recipient: z.string().min(32, "Invalid recipient address").optional()
}),
handler: async (agent: SolanaAgentKit, input: Record<string, any>) => {
const result = await agent.mintNFT(
new PublicKey(input.collectionMint),
{
name: input.name,
uri: input.uri,
},
input.recipient ? new PublicKey(input.recipient) : agent.wallet_address
);
return {
status: "success",
message: "NFT minted successfully",
mintAddress: result.mint.toString(),
metadata: {
name: input.name,
uri: input.uri
},
recipient: input.recipient || result.mint.toString()
};
}
};
export default mintNFTAction;

View File

@@ -0,0 +1,40 @@
import { Action } from "../types/action";
import { SolanaAgentKit } from "../agent";
import { z } from "zod";
const requestFundsAction: Action = {
name: "solana_request_funds",
similes: [
"request sol",
"get test sol",
"use faucet",
"request test tokens",
"get devnet sol"
],
description: "Request SOL from Solana faucet (devnet/testnet only)",
examples: [
[
{
input: {},
output: {
status: "success",
message: "Successfully requested faucet funds",
network: "devnet.solana.com"
},
explanation: "Request SOL from the devnet faucet"
}
]
],
schema: z.object({}), // No input parameters required
handler: async (agent: SolanaAgentKit, _input: Record<string, any>) => {
await agent.requestFaucetFunds();
return {
status: "success",
message: "Successfully requested faucet funds",
network: agent.connection.rpcEndpoint.split("/")[2]
};
}
};
export default requestFundsAction;

81
src/actions/trade.ts Normal file
View File

@@ -0,0 +1,81 @@
import { PublicKey } from "@solana/web3.js";
import { Action } from "../types/action";
import { SolanaAgentKit } from "../agent";
import { z } from "zod";
const tradeAction: Action = {
name: "solana_trade",
similes: [
"swap tokens",
"exchange tokens",
"trade tokens",
"convert tokens",
"swap sol"
],
description: `This tool can be used to swap tokens to another token (It uses Jupiter Exchange).`,
examples: [
[
{
input: {
outputMint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
inputAmount: 1
},
output: {
status: "success",
message: "Trade executed successfully",
transaction: "5UfgJ5vVZxUxefDGqzqkVLHzHxVTyYH9StYyHKgvHYmXJgqJKxEqy9k4Rz9LpXrHF9kUZB7",
inputAmount: 1,
inputToken: "SOL",
outputToken: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
},
explanation: "Swap 1 SOL for USDC"
}
],
[
{
input: {
outputMint: "So11111111111111111111111111111111111111112",
inputAmount: 100,
inputMint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
slippageBps: 100
},
output: {
status: "success",
message: "Trade executed successfully",
transaction: "4VfgJ5vVZxUxefDGqzqkVLHzHxVTyYH9StYyHKgvHYmXJgqJKxEqy9k4Rz9LpXrHF9kUZB7",
inputAmount: 100,
inputToken: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
outputToken: "So11111111111111111111111111111111111111112"
},
explanation: "Swap 100 USDC for SOL with 1% slippage"
}
]
],
schema: z.object({
outputMint: z.string().min(32, "Invalid output mint address"),
inputAmount: z.number().positive("Input amount must be positive"),
inputMint: z.string().min(32, "Invalid input mint address").optional(),
slippageBps: z.number().min(0).max(10000).optional()
}),
handler: async (agent: SolanaAgentKit, input: Record<string, any>) => {
const tx = await agent.trade(
new PublicKey(input.outputMint),
input.inputAmount,
input.inputMint
? new PublicKey(input.inputMint)
: new PublicKey("So11111111111111111111111111111111111111112"),
input.slippageBps
);
return {
status: "success",
message: "Trade executed successfully",
transaction: tx,
inputAmount: input.inputAmount,
inputToken: input.inputMint || "SOL",
outputToken: input.outputMint
};
}
};
export default tradeAction;

75
src/actions/transfer.ts Normal file
View File

@@ -0,0 +1,75 @@
import { PublicKey } from "@solana/web3.js";
import { Action } from "../types/action";
import { SolanaAgentKit } from "../agent";
import { z } from "zod";
const transferAction: Action = {
name: "solana_transfer",
similes: [
"send tokens",
"transfer funds",
"send money",
"send sol",
"transfer tokens"
],
description: `Transfer tokens or SOL to another address (also called as wallet address).`,
examples: [
[
{
input: {
to: "8x2dR8Mpzuz2YqyZyZjUbYWKSWesBo5jMx2Q9Y86udVk",
amount: 1
},
output: {
status: "success",
message: "Transfer completed successfully",
amount: 1,
recipient: "8x2dR8Mpzuz2YqyZyZjUbYWKSWesBo5jMx2Q9Y86udVk",
token: "SOL",
transaction: "5UfgJ5vVZxUxefDGqzqkVLHzHxVTyYH9StYyHKgvHYmXJgqJKxEqy9k4Rz9LpXrHF9kUZB7"
},
explanation: "Transfer 1 SOL to the recipient address"
}
],
[
{
input: {
to: "8x2dR8Mpzuz2YqyZyZjUbYWKSWesBo5jMx2Q9Y86udVk",
amount: 100,
mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
},
output: {
status: "success",
message: "Transfer completed successfully",
amount: 100,
recipient: "8x2dR8Mpzuz2YqyZyZjUbYWKSWesBo5jMx2Q9Y86udVk",
token: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
transaction: "4VfgJ5vVZxUxefDGqzqkVLHzHxVTyYH9StYyHKgvHYmXJgqJKxEqy9k4Rz9LpXrHF9kUZB7"
},
explanation: "Transfer 100 USDC tokens to the recipient address"
}
]
],
schema: z.object({
to: z.string().min(32, "Invalid Solana address"),
amount: z.number().positive("Amount must be positive"),
mint: z.string().optional()
}),
handler: async (agent: SolanaAgentKit, input: Record<string, any>) => {
const recipient = new PublicKey(input.to);
const mintAddress = input.mint ? new PublicKey(input.mint) : undefined;
const tx = await agent.transfer(recipient, input.amount, mintAddress);
return {
status: "success",
message: "Transfer completed successfully",
amount: input.amount,
recipient: input.to,
token: input.mint || "SOL",
transaction: tx
};
}
};
export default transferAction;

View File

@@ -5,3 +5,8 @@ export { SolanaAgentKit, createSolanaTools };
// Optional: Export types that users might need
export * from "./types";
// Export action system
export * from "./actions";
export * from "./types/action";
export * from "./utils/actionExecutor";

View File

@@ -1,6 +1,6 @@
import { PublicKey } from "@solana/web3.js";
import Decimal from "decimal.js";
import { Tool } from "langchain/tools";
import { Tool } from "@langchain/core/tools";
import {
GibworkCreateTaskReponse,
PythFetchPriceResponse,
@@ -10,282 +10,203 @@ import { create_image } from "../tools/create_image";
import { BN } from "@coral-xyz/anchor";
import { FEE_TIERS } from "../tools";
import { toJSON } from "../utils/toJSON";
import { wrapLangChainTool } from "../utils/langchainWrapper";
import deployTokenAction from "../actions/deployToken";
import balanceAction from "../actions/balance";
import transferAction from "../actions/transfer";
import deployCollectionAction from "../actions/deployCollection";
import mintNFTAction from "../actions/mintNFT";
import tradeAction from "../actions/trade";
import requestFundsAction from "../actions/requestFunds";
export class SolanaBalanceTool extends Tool {
name = "solana_balance";
description = `Get the balance of a Solana wallet or token account.
If you want to get the balance of your wallet, you don't need to provide the tokenAddress.
If no tokenAddress is provided, the balance will be in SOL.
Inputs:
tokenAddress: string, eg "So11111111111111111111111111111111111111112" (optional)`;
private action = balanceAction;
name = this.action.name;
description = this.action.description;
constructor(private solanaKit: SolanaAgentKit) {
super();
}
protected async _call(input: string): Promise<string> {
async _call(input: string): Promise<string> {
try {
const tokenAddress = input ? new PublicKey(input) : undefined;
const balance = await this.solanaKit.getBalance(tokenAddress);
return JSON.stringify({
status: "success",
balance: balance,
token: input || "SOL",
});
// Parse input as JSON if provided, otherwise use empty object
const parsedInput = input ? JSON.parse(input) : {};
// Validate and execute using the action
const result = await this.action.handler(this.solanaKit, parsedInput);
return JSON.stringify(result);
} catch (error: any) {
return JSON.stringify({
status: "error",
message: error.message,
code: error.code || "UNKNOWN_ERROR",
code: error.code || "UNKNOWN_ERROR"
});
}
}
}
export class SolanaTransferTool extends Tool {
name = "solana_transfer";
description = `Transfer tokens or SOL to another address ( also called as wallet address ).
Inputs ( input is a JSON string ):
to: string, eg "8x2dR8Mpzuz2YqyZyZjUbYWKSWesBo5jMx2Q9Y86udVk" (required)
amount: number, eg 1 (required)
mint?: string, eg "So11111111111111111111111111111111111111112" or "SENDdRQtYMWaQrBroBrJ2Q53fgVuq95CV9UPGEvpCxa" (optional)`;
private action = transferAction;
name = this.action.name;
description = this.action.description;
constructor(private solanaKit: SolanaAgentKit) {
super();
}
protected async _call(input: string): Promise<string> {
async _call(input: string): Promise<string> {
try {
// Parse input as JSON
const parsedInput = JSON.parse(input);
const recipient = new PublicKey(parsedInput.to);
const mintAddress = parsedInput.mint
? new PublicKey(parsedInput.mint)
: undefined;
const tx = await this.solanaKit.transfer(
recipient,
parsedInput.amount,
mintAddress,
);
return JSON.stringify({
status: "success",
message: "Transfer completed successfully",
amount: parsedInput.amount,
recipient: parsedInput.to,
token: parsedInput.mint || "SOL",
transaction: tx,
});
// Validate and execute using the action
const result = await this.action.handler(this.solanaKit, parsedInput);
return JSON.stringify(result);
} catch (error: any) {
return JSON.stringify({
status: "error",
message: error.message,
code: error.code || "UNKNOWN_ERROR",
code: error.code || "UNKNOWN_ERROR"
});
}
}
}
export class SolanaDeployTokenTool extends Tool {
name = "solana_deploy_token";
description = `Deploy a new token on Solana blockchain.
Inputs (input is a JSON string):
name: string, eg "My Token" (required)
uri: string, eg "https://example.com/token.json" (required)
symbol: string, eg "MTK" (required)
decimals?: number, eg 9 (optional, defaults to 9)
initialSupply?: number, eg 1000000 (optional)`;
private action = deployTokenAction;
name = this.action.name;
description = this.action.description;
constructor(private solanaKit: SolanaAgentKit) {
super();
}
protected async _call(input: string): Promise<string> {
async _call(input: string): Promise<string> {
try {
// Parse input as JSON
const parsedInput = JSON.parse(input);
const result = await this.solanaKit.deployToken(
parsedInput.name,
parsedInput.uri,
parsedInput.symbol,
parsedInput.decimals,
parsedInput.initialSupply,
);
return JSON.stringify({
status: "success",
message: "Token deployed successfully",
mintAddress: result.mint.toString(),
decimals: parsedInput.decimals || 9,
});
// Validate and execute using the action
const result = await this.action.handler(this.solanaKit, parsedInput);
return JSON.stringify(result);
} catch (error: any) {
return JSON.stringify({
status: "error",
message: error.message,
code: error.code || "UNKNOWN_ERROR",
code: error.code || "UNKNOWN_ERROR"
});
}
}
}
export class SolanaDeployCollectionTool extends Tool {
name = "solana_deploy_collection";
description = `Deploy a new NFT collection on Solana blockchain.
Inputs (input is a JSON string):
name: string, eg "My Collection" (required)
uri: string, eg "https://example.com/collection.json" (required)
royaltyBasisPoints?: number, eg 500 for 5% (optional)`;
private action = deployCollectionAction;
name = this.action.name;
description = this.action.description;
constructor(private solanaKit: SolanaAgentKit) {
super();
}
protected async _call(input: string): Promise<string> {
async _call(input: string): Promise<string> {
try {
// Parse input as JSON
const parsedInput = JSON.parse(input);
const result = await this.solanaKit.deployCollection(parsedInput);
return JSON.stringify({
status: "success",
message: "Collection deployed successfully",
collectionAddress: result.collectionAddress.toString(),
name: parsedInput.name,
});
// Validate and execute using the action
const result = await this.action.handler(this.solanaKit, parsedInput);
return JSON.stringify(result);
} catch (error: any) {
return JSON.stringify({
status: "error",
message: error.message,
code: error.code || "UNKNOWN_ERROR",
code: error.code || "UNKNOWN_ERROR"
});
}
}
}
export class SolanaMintNFTTool extends Tool {
name = "solana_mint_nft";
description = `Mint a new NFT in a collection on Solana blockchain.
Inputs (input is a JSON string):
collectionMint: string, eg "J1S9H3QjnRtBbbuD4HjPV6RpRhwuk4zKbxsnCHuTgh9w" (required) - The address of the collection to mint into
name: string, eg "My NFT" (required)
uri: string, eg "https://example.com/nft.json" (required)
recipient?: string, eg "9aUn5swQzUTRanaaTwmszxiv89cvFwUCjEBv1vZCoT1u" (optional) - The wallet to receive the NFT, defaults to agent's wallet which is ${this.solanaKit.wallet_address.toString()}`;
private action = mintNFTAction;
name = this.action.name;
description = this.action.description;
constructor(private solanaKit: SolanaAgentKit) {
super();
}
protected async _call(input: string): Promise<string> {
async _call(input: string): Promise<string> {
try {
// Parse input as JSON
const parsedInput = JSON.parse(input);
const result = await this.solanaKit.mintNFT(
new PublicKey(parsedInput.collectionMint),
{
name: parsedInput.name,
uri: parsedInput.uri,
},
parsedInput.recipient
? new PublicKey(parsedInput.recipient)
: this.solanaKit.wallet_address,
);
return JSON.stringify({
status: "success",
message: "NFT minted successfully",
mintAddress: result.mint.toString(),
metadata: {
name: parsedInput.name,
symbol: parsedInput.symbol,
uri: parsedInput.uri,
},
recipient: parsedInput.recipient || result.mint.toString(),
});
// Validate and execute using the action
const result = await this.action.handler(this.solanaKit, parsedInput);
return JSON.stringify(result);
} catch (error: any) {
return JSON.stringify({
status: "error",
message: error.message,
code: error.code || "UNKNOWN_ERROR",
code: error.code || "UNKNOWN_ERROR"
});
}
}
}
export class SolanaTradeTool extends Tool {
name = "solana_trade";
description = `This tool can be used to swap tokens to another token ( It uses Jupiter Exchange ).
Inputs ( input is a JSON string ):
outputMint: string, eg "So11111111111111111111111111111111111111112" or "SENDdRQtYMWaQrBroBrJ2Q53fgVuq95CV9UPGEvpCxa" (required)
inputAmount: number, eg 1 or 0.01 (required)
inputMint?: string, eg "So11111111111111111111111111111111111111112" (optional)
slippageBps?: number, eg 100 (optional)`;
private action = tradeAction;
name = this.action.name;
description = this.action.description;
constructor(private solanaKit: SolanaAgentKit) {
super();
}
protected async _call(input: string): Promise<string> {
async _call(input: string): Promise<string> {
try {
// Parse input as JSON
const parsedInput = JSON.parse(input);
const tx = await this.solanaKit.trade(
new PublicKey(parsedInput.outputMint),
parsedInput.inputAmount,
parsedInput.inputMint
? new PublicKey(parsedInput.inputMint)
: new PublicKey("So11111111111111111111111111111111111111112"),
parsedInput.slippageBps,
);
return JSON.stringify({
status: "success",
message: "Trade executed successfully",
transaction: tx,
inputAmount: parsedInput.inputAmount,
inputToken: parsedInput.inputMint || "SOL",
outputToken: parsedInput.outputMint,
});
// Validate and execute using the action
const result = await this.action.handler(this.solanaKit, parsedInput);
return JSON.stringify(result);
} catch (error: any) {
return JSON.stringify({
status: "error",
message: error.message,
code: error.code || "UNKNOWN_ERROR",
code: error.code || "UNKNOWN_ERROR"
});
}
}
}
export class SolanaRequestFundsTool extends Tool {
name = "solana_request_funds";
description = "Request SOL from Solana faucet (devnet/testnet only)";
private action = requestFundsAction;
name = this.action.name;
description = this.action.description;
constructor(private solanaKit: SolanaAgentKit) {
super();
}
protected async _call(_input: string): Promise<string> {
async _call(_input: string): Promise<string> {
try {
await this.solanaKit.requestFaucetFunds();
return JSON.stringify({
status: "success",
message: "Successfully requested faucet funds",
network: this.solanaKit.connection.rpcEndpoint.split("/")[2],
});
// No input needed for this action
const result = await this.action.handler(this.solanaKit, {});
return JSON.stringify(result);
} catch (error: any) {
return JSON.stringify({
status: "error",
message: error.message,
code: error.code || "UNKNOWN_ERROR",
code: error.code || "UNKNOWN_ERROR"
});
}
}
@@ -1230,7 +1151,7 @@ export class SolanaCreateGibworkTask extends Tool {
}
export function createSolanaTools(solanaKit: SolanaAgentKit) {
return [
const tools = [
new SolanaBalanceTool(solanaKit),
new SolanaTransferTool(solanaKit),
new SolanaDeployTokenTool(solanaKit),
@@ -1264,4 +1185,7 @@ export function createSolanaTools(solanaKit: SolanaAgentKit) {
new SolanaResolveAllDomainsTool(solanaKit),
new SolanaCreateGibworkTask(solanaKit),
];
// Convert LangChain tools to our Action interface
return tools.map(tool => wrapLangChainTool(tool, solanaKit));
}

53
src/types/action.ts Normal file
View File

@@ -0,0 +1,53 @@
import { SolanaAgentKit } from "../agent";
import { z } from "zod";
/**
* Example of an action with input and output
*/
export interface ActionExample {
input: Record<string, any>;
output: Record<string, any>;
explanation: string;
}
/**
* Handler function type for executing the action
*/
export type Handler = (agent: SolanaAgentKit, input: Record<string, any>) => Promise<Record<string, any>>;
/**
* Main Action interface inspired by ELIZA
* This interface makes it easier to implement actions across different frameworks
*/
export interface Action {
/**
* Unique name of the action
*/
name: string;
/**
* Alternative names/phrases that can trigger this action
*/
similes: string[];
/**
* Detailed description of what the action does
*/
description: string;
/**
* Array of example inputs and outputs for the action
* Each inner array represents a group of related examples
*/
examples: ActionExample[][];
/**
* Zod schema for input validation
*/
schema: z.ZodType<any>;
/**
* Function that executes the action
*/
handler: Handler;
}

View File

@@ -0,0 +1,67 @@
import { Action } from "../types/action";
import { SolanaAgentKit } from "../agent";
import { actions } from "../actions";
/**
* Find an action by its name or one of its similes
*/
export function findAction(query: string): Action | undefined {
const normalizedQuery = query.toLowerCase().trim();
return actions.find(action =>
action.name.toLowerCase() === normalizedQuery ||
action.similes.some(simile => simile.toLowerCase() === normalizedQuery)
);
}
/**
* Execute an action with the given input
*/
export async function executeAction(
action: Action,
agent: SolanaAgentKit,
input: Record<string, any>
): Promise<Record<string, any>> {
try {
// Validate input using Zod schema
const validatedInput = action.schema.parse(input);
// Execute the action with validated input
const result = await action.handler(agent, validatedInput);
return {
status: "success",
...result
};
} catch (error: any) {
// Handle Zod validation errors specially
if (error.errors) {
return {
status: "error",
message: "Validation error",
details: error.errors,
code: "VALIDATION_ERROR"
};
}
return {
status: "error",
message: error.message,
code: error.code || "EXECUTION_ERROR"
};
}
}
/**
* Get examples for an action
*/
export function getActionExamples(action: Action): string {
return action.examples
.flat()
.map(example => {
return `Input: ${JSON.stringify(example.input, null, 2)}
Output: ${JSON.stringify(example.output, null, 2)}
Explanation: ${example.explanation}
---`;
})
.join("\n");
}

View File

@@ -0,0 +1,96 @@
import { Tool } from "langchain/tools";
import { Action } from "../types/action";
import { SolanaAgentKit } from "../agent";
import { z } from "zod";
/**
* Convert a LangChain tool to our Action interface
*/
export function wrapLangChainTool(tool: Tool, agent: SolanaAgentKit): Action {
// Parse the description to extract input parameters
const inputParams = parseToolDescription(tool.description);
return {
name: tool.name,
similes: [], // LangChain tools don't have similes
description: tool.description,
examples: [], // LangChain tools don't have examples
schema: createZodSchema(inputParams),
handler: async (agent: SolanaAgentKit, input: Record<string, any>) => {
const result = await tool.call(JSON.stringify(input));
try {
return JSON.parse(result);
} catch {
return { result };
}
}
};
}
/**
* Parse tool description to extract input parameters
*/
function parseToolDescription(description: string): Array<{name: string, type: string, required: boolean}> {
const lines = description.split('\n');
const params: Array<{name: string, type: string, required: boolean}> = [];
let inInputsSection = false;
for (const line of lines) {
const trimmed = line.trim();
if (trimmed === 'Inputs:' || trimmed === 'Inputs (input is a JSON string):') {
inInputsSection = true;
continue;
}
if (inInputsSection && trimmed) {
// Match patterns like: name: string, eg "value" (required)
const match = trimmed.match(/(\w+):\s*([\w\[\]]+)(?:,\s*eg[:\s]+"[^"]+")?(?:\s*\((required|optional)\))?/);
if (match) {
params.push({
name: match[1],
type: match[2],
required: match[3] === 'required'
});
}
}
}
return params;
}
/**
* Create a Zod schema from parsed parameters
*/
function createZodSchema(params: Array<{name: string, type: string, required: boolean}>): z.ZodType<any> {
const schemaObj: Record<string, z.ZodType<any>> = {};
for (const param of params) {
let schema: z.ZodType<any>;
switch (param.type.toLowerCase()) {
case 'string':
schema = z.string();
break;
case 'number':
schema = z.number();
break;
case 'boolean':
schema = z.boolean();
break;
case 'string[]':
schema = z.array(z.string());
break;
default:
schema = z.any();
}
if (!param.required) {
schema = schema.optional();
}
schemaObj[param.name] = schema;
}
return z.object(schemaObj);
}