From 79cada2cbd65d87ef0a6d97531dad3648a9dab43 Mon Sep 17 00:00:00 2001 From: Fahri Bilici <28020526+FahriBilici@users.noreply.github.com> Date: Thu, 26 Dec 2024 21:54:55 +0100 Subject: [PATCH] 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. --- src/actions/balance.ts | 59 ++++++++ src/actions/deployCollection.ts | 78 ++++++++++ src/actions/deployToken.ts | 74 ++++++++++ src/actions/index.ts | 20 +++ src/actions/mintNFT.ts | 88 +++++++++++ src/actions/requestFunds.ts | 40 +++++ src/actions/trade.ts | 81 ++++++++++ src/actions/transfer.ts | 75 ++++++++++ src/index.ts | 5 + src/langchain/index.ts | 254 +++++++++++--------------------- src/types/action.ts | 53 +++++++ src/utils/actionExecutor.ts | 67 +++++++++ src/utils/langchainWrapper.ts | 96 ++++++++++++ 13 files changed, 825 insertions(+), 165 deletions(-) create mode 100644 src/actions/balance.ts create mode 100644 src/actions/deployCollection.ts create mode 100644 src/actions/deployToken.ts create mode 100644 src/actions/index.ts create mode 100644 src/actions/mintNFT.ts create mode 100644 src/actions/requestFunds.ts create mode 100644 src/actions/trade.ts create mode 100644 src/actions/transfer.ts create mode 100644 src/types/action.ts create mode 100644 src/utils/actionExecutor.ts create mode 100644 src/utils/langchainWrapper.ts diff --git a/src/actions/balance.ts b/src/actions/balance.ts new file mode 100644 index 0000000..2a4d34d --- /dev/null +++ b/src/actions/balance.ts @@ -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) => { + 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; \ No newline at end of file diff --git a/src/actions/deployCollection.ts b/src/actions/deployCollection.ts new file mode 100644 index 0000000..0127b5c --- /dev/null +++ b/src/actions/deployCollection.ts @@ -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) => { + 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; \ No newline at end of file diff --git a/src/actions/deployToken.ts b/src/actions/deployToken.ts new file mode 100644 index 0000000..046c120 --- /dev/null +++ b/src/actions/deployToken.ts @@ -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) => { + 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; \ No newline at end of file diff --git a/src/actions/index.ts b/src/actions/index.ts new file mode 100644 index 0000000..bf72aca --- /dev/null +++ b/src/actions/index.ts @@ -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"; \ No newline at end of file diff --git a/src/actions/mintNFT.ts b/src/actions/mintNFT.ts new file mode 100644 index 0000000..4e9e6ae --- /dev/null +++ b/src/actions/mintNFT.ts @@ -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) => { + 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; \ No newline at end of file diff --git a/src/actions/requestFunds.ts b/src/actions/requestFunds.ts new file mode 100644 index 0000000..03159a7 --- /dev/null +++ b/src/actions/requestFunds.ts @@ -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) => { + await agent.requestFaucetFunds(); + + return { + status: "success", + message: "Successfully requested faucet funds", + network: agent.connection.rpcEndpoint.split("/")[2] + }; + } +}; + +export default requestFundsAction; \ No newline at end of file diff --git a/src/actions/trade.ts b/src/actions/trade.ts new file mode 100644 index 0000000..8a536b3 --- /dev/null +++ b/src/actions/trade.ts @@ -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) => { + 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; \ No newline at end of file diff --git a/src/actions/transfer.ts b/src/actions/transfer.ts new file mode 100644 index 0000000..426a071 --- /dev/null +++ b/src/actions/transfer.ts @@ -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) => { + 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; \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 1b4116f..4ae9056 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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"; diff --git a/src/langchain/index.ts b/src/langchain/index.ts index f28b000..68cd157 100644 --- a/src/langchain/index.ts +++ b/src/langchain/index.ts @@ -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 { + async _call(input: string): Promise { 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 { + async _call(input: string): Promise { 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 { + async _call(input: string): Promise { 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 { + async _call(input: string): Promise { 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 { + async _call(input: string): Promise { 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 { + async _call(input: string): Promise { 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 { + async _call(_input: string): Promise { 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)); } diff --git a/src/types/action.ts b/src/types/action.ts new file mode 100644 index 0000000..d715177 --- /dev/null +++ b/src/types/action.ts @@ -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; + output: Record; + explanation: string; +} + +/** + * Handler function type for executing the action + */ +export type Handler = (agent: SolanaAgentKit, input: Record) => Promise>; + +/** + * 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; + + /** + * Function that executes the action + */ + handler: Handler; +} \ No newline at end of file diff --git a/src/utils/actionExecutor.ts b/src/utils/actionExecutor.ts new file mode 100644 index 0000000..0bb8f4b --- /dev/null +++ b/src/utils/actionExecutor.ts @@ -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 +): Promise> { + 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"); +} \ No newline at end of file diff --git a/src/utils/langchainWrapper.ts b/src/utils/langchainWrapper.ts new file mode 100644 index 0000000..c7bf500 --- /dev/null +++ b/src/utils/langchainWrapper.ts @@ -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) => { + 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 { + const schemaObj: Record> = {}; + + for (const param of params) { + let schema: z.ZodType; + + 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); +} \ No newline at end of file