solana-agent-kit
Version:
connect any ai agents to solana protocols
604 lines (586 loc) • 18 kB
JavaScript
// src/agent/index.ts
import { Connection } from "@solana/web3.js";
var SolanaAgentKit = class {
connection;
config;
wallet;
plugins = /* @__PURE__ */ new Map();
methods = {};
actions = [];
constructor(wallet, rpc_url, config) {
this.connection = new Connection(rpc_url);
this.wallet = wallet;
this.config = config;
}
/**
* Adds a plugin and registers its methods inside `methods`
*/
use(plugin) {
if (this.plugins.has(plugin.name)) {
return this;
}
plugin.initialize(this);
for (const [methodName, method] of Object.entries(plugin.methods)) {
if (this.methods[methodName]) {
throw new Error(`Method ${methodName} already exists in methods`);
}
this.methods[methodName] = method.bind(plugin);
}
for (const action of plugin.actions) {
this.actions.push(action);
}
this.plugins.set(plugin.name, plugin);
return this;
}
};
// src/utils/actionExecutor.ts
async function executeAction(action, agent, input) {
try {
const validatedInput = action.schema.parse(input);
const result = await action.handler(agent, validatedInput);
return {
status: "success",
...result
};
} catch (error) {
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"
};
}
}
function getActionExamples(action) {
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");
}
// src/openai/index.ts
import { tool } from "@openai/agents";
// src/openai/utils.ts
import { z, ZodObject } from "zod";
function mapZodTypeToJsonSchema(zodType) {
const isNullable = zodType instanceof z.ZodNullable;
let coreType = isNullable ? zodType._def.innerType : zodType;
while (coreType instanceof z.ZodOptional || coreType instanceof z.ZodDefault || coreType instanceof z.ZodEffects) {
coreType = coreType instanceof z.ZodEffects ? coreType._def.schema : coreType._def.innerType;
}
const typeName = coreType._def.typeName;
let jsonSchema = { type: "string" };
switch (typeName) {
case "ZodNullable":
jsonSchema.type = mapZodTypeToJsonSchema(coreType._def.innerType).type;
if (!jsonSchema.enum) {
jsonSchema.enum = [""];
} else {
jsonSchema.enum.push("");
}
break;
case "ZodString":
jsonSchema.type = "string";
break;
case "ZodNumber":
case "ZodBigInt":
jsonSchema.type = "number";
break;
case "ZodBoolean":
jsonSchema.type = "boolean";
break;
case "ZodEnum":
case "ZodNativeEnum":
jsonSchema.type = "string";
if (!(coreType._def.values instanceof Array)) {
jsonSchema.enum = Object.values(coreType._def.values).map(
(v) => v.toString()
);
break;
}
jsonSchema.enum = coreType._def.values;
break;
case "ZodLiteral":
jsonSchema.type = typeof coreType._def.value;
jsonSchema.enum = [coreType._def.value];
break;
case "ZodArray":
jsonSchema.type = "array";
jsonSchema.items = mapZodTypeToJsonSchema(coreType._def.type);
break;
case "ZodObject":
jsonSchema.type = "object";
const shape = coreType.shape;
jsonSchema.properties = {};
for (const key in shape) {
jsonSchema.properties[key] = mapZodTypeToJsonSchema(shape[key]);
}
jsonSchema.required = Object.keys(shape);
break;
default:
throw new Error(`Unsupported Zod type: ${typeName}`);
}
if (coreType.description) {
jsonSchema.description = coreType.description;
}
if (isNullable) {
if (jsonSchema.enum) {
if (!jsonSchema.enum.includes(null)) {
jsonSchema.enum.push(null);
}
} else if (typeof jsonSchema.type === "string") {
jsonSchema.type = [jsonSchema.type, "null"];
}
}
jsonSchema.additionalProperties = false;
return jsonSchema;
}
function zodToOpenAITool(schema, name, description) {
if (!(schema instanceof ZodObject)) {
throw new Error("The provided schema must be a ZodObject.");
}
const shape = schema.shape;
const properties = {};
for (const key in shape) {
properties[key] = mapZodTypeToJsonSchema(shape[key]);
}
const required = Object.keys(properties);
const parameters = {
type: "object",
properties,
required,
// This will now correctly list all properties.
additionalProperties: false
};
return {
type: "function",
function: {
name,
description,
parameters
}
};
}
// src/openai/index.ts
function createOpenAITools(solanaAgentKit, actions) {
const tools = [];
if (actions.length > 128) {
console.warn(
`Too many actions provided. Only a maximum of 128 actions allowed. You provided ${actions.length}, the last ${actions.length - 128} will be ignored.`
);
}
for (const [_index, action] of actions.slice(0, 127).entries()) {
tools.push(
tool({
name: action.name,
description: action.description,
execute: async (params) => await executeAction(action, solanaAgentKit, params),
parameters: zodToOpenAITool(
action.schema,
action.name,
action.description
).function.parameters
})
);
}
return tools;
}
// src/langchain/index.ts
import { tool as tool2 } from "@langchain/core/tools";
// src/utils/zod.ts
import { ZodObject as ZodObject2 } from "zod";
function transformToZodObject(schema) {
if (schema instanceof ZodObject2) {
return schema;
}
throw new Error(
`The provided schema is not a ZodObject: ${JSON.stringify(schema)}`
);
}
// src/langchain/index.ts
function createLangchainTools(solanaAgentKit, actions) {
if (actions.length > 128) {
console.warn(
`Too many actions provided. Only a maximum of 128 actions allowed. You provided ${actions.length}, the last ${actions.length - 128} will be ignored.`
);
}
const tools = actions.slice(0, 127).map((action) => {
const toolInstance = tool2(
async (inputs) => JSON.stringify(await action.handler(solanaAgentKit, inputs)),
{
name: action.name,
description: `
${action.description}
Similes: ${action.similes.map(
(simile) => `
${simile}
`
)}
Examples: ${action.examples.map(
(example) => `
Input: ${JSON.stringify(example[0].input)}
Output: ${JSON.stringify(example[0].output)}
Explanation: ${example[0].explanation}
`
)}`,
// convert action.schema from ZodType to ZodObject
schema: transformToZodObject(action.schema)
}
);
return toolInstance;
});
return tools;
}
// src/vercel-ai/index.ts
import { tool as tool3 } from "ai";
function createSolanaTools(solanaAgentKit, actions) {
const tools = {};
if (actions.length > 128) {
console.warn(
`Too many actions provided. Only a maximum of 128 actions allowed. You provided ${actions.length}, the last ${actions.length - 128} will be ignored.`
);
}
for (const [index, action] of actions.slice(0, 127).entries()) {
tools[index.toString()] = tool3({
id: action.name,
description: `
${action.description}
Similes: ${action.similes.map(
(simile) => `
${simile}
`
)}
Examples: ${action.examples.map(
(example) => `
Input: ${JSON.stringify(example[0].input)}
Output: ${JSON.stringify(example[0].output)}
Explanation: ${example[0].explanation}
`
)}
`.slice(0, 1023),
parameters: action.schema,
execute: async (params) => await executeAction(action, solanaAgentKit, params)
});
}
return tools;
}
// src/types/wallet.ts
import {
Transaction as Transaction2,
TransactionMessage as TransactionMessage2,
VersionedTransaction as VersionedTransaction2
} from "@solana/web3.js";
// src/utils/send_tx.ts
import {
Transaction,
TransactionMessage,
VersionedTransaction
} from "@solana/web3.js";
import { ComputeBudgetProgram } from "@solana/web3.js";
import bs58 from "bs58";
var feeTiers = {
min: 0.01,
mid: 0.5,
max: 0.95
};
async function getComputeBudgetInstructions(agent, instructions, feeTier) {
const { blockhash, lastValidBlockHeight } = await agent.connection.getLatestBlockhash();
const messageV0 = new TransactionMessage({
payerKey: agent.wallet.publicKey,
recentBlockhash: blockhash,
instructions
}).compileToV0Message();
const transaction = new VersionedTransaction(messageV0);
const simulatedTx = await agent.connection.simulateTransaction(transaction);
const estimatedComputeUnits = simulatedTx.value.unitsConsumed;
const safeComputeUnits = Math.ceil(
estimatedComputeUnits ? Math.max(estimatedComputeUnits + 1e5, estimatedComputeUnits * 1.2) : 2e5
);
const computeBudgetLimitInstruction = ComputeBudgetProgram.setComputeUnitLimit({
units: safeComputeUnits
});
let priorityFee;
if (agent.config?.HELIUS_API_KEY) {
const legacyTransaction = new Transaction();
legacyTransaction.recentBlockhash = blockhash;
legacyTransaction.lastValidBlockHeight = lastValidBlockHeight;
legacyTransaction.feePayer = agent.wallet.publicKey;
legacyTransaction.add(computeBudgetLimitInstruction, ...instructions);
const signedTransaction = await agent.wallet.signTransaction(legacyTransaction);
const response = await fetch(
`https://mainnet.helius-rpc.com/?api-key=${agent.config?.HELIUS_API_KEY}`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
jsonrpc: "2.0",
id: "1",
method: "getPriorityFeeEstimate",
params: [
{
transaction: bs58.encode(
signedTransaction.serialize()
),
options: {
priorityLevel: feeTier === "min" ? "Min" : feeTier === "mid" ? "Medium" : "High"
}
}
]
})
}
);
const data = await response.json();
if (data.error) {
throw new Error("Error fetching priority fee from Helius API");
}
priorityFee = data.result.priorityFeeEstimate;
} else {
priorityFee = await agent.connection.getRecentPrioritizationFees().then(
(fees) => fees.sort((a, b) => a.prioritizationFee - b.prioritizationFee)[Math.floor(fees.length * feeTiers[feeTier])].prioritizationFee
);
}
const computeBudgetPriorityFeeInstructions = ComputeBudgetProgram.setComputeUnitPrice({
microLamports: priorityFee
});
return {
computeBudgetLimitInstruction,
computeBudgetPriorityFeeInstructions
};
}
async function sendTx(agent, instructions, otherKeypairs, feeTier) {
const ixComputeBudget = await getComputeBudgetInstructions(
agent,
instructions,
feeTier ?? "mid"
);
const allInstructions = [
ixComputeBudget.computeBudgetLimitInstruction,
ixComputeBudget.computeBudgetPriorityFeeInstructions,
...instructions
];
const { blockhash } = await agent.connection.getLatestBlockhash();
const messageV0 = new TransactionMessage({
payerKey: agent.wallet.publicKey,
recentBlockhash: blockhash,
instructions: allInstructions
}).compileToV0Message();
const transaction = new VersionedTransaction(messageV0);
transaction.sign([...otherKeypairs ?? []]);
const signedTransaction = await agent.wallet.signTransaction(transaction);
const timeoutMs = 9e4;
const startTime = Date.now();
while (Date.now() - startTime < timeoutMs) {
const transactionStartTime = Date.now();
const signature = await agent.connection.sendTransaction(
signedTransaction,
{
maxRetries: 0,
skipPreflight: false
}
);
const statuses = await agent.connection.getSignatureStatuses([signature]);
if (statuses.value[0]) {
if (!statuses.value[0].err) {
return signature;
}
throw new Error(
`Transaction failed: ${statuses.value[0].err.toString()}`
);
}
const elapsedTime = Date.now() - transactionStartTime;
const remainingTime = Math.max(0, 1e3 - elapsedTime);
if (remainingTime > 0) {
await new Promise((resolve) => setTimeout(resolve, remainingTime));
}
}
throw new Error("Transaction timeout");
}
// src/types/wallet.ts
async function signOrSendTX(agent, instructionsOrTransaction, otherKeypairs, feeTier) {
if (Array.isArray(instructionsOrTransaction) && (instructionsOrTransaction[0] instanceof Transaction2 || instructionsOrTransaction[0] instanceof VersionedTransaction2)) {
if (agent.config.signOnly) {
return await agent.wallet.signAllTransactions(
instructionsOrTransaction
);
}
const txSigs = [];
for (const tx of instructionsOrTransaction) {
if (agent.wallet.signAndSendTransaction) {
const { signature } = await agent.wallet.signAndSendTransaction(
tx
);
txSigs.push(signature);
}
throw new Error(
"Wallet does not support signAndSendTransaction please implement it manually or use the signOnly option"
);
}
return txSigs;
}
if (instructionsOrTransaction instanceof Transaction2 || instructionsOrTransaction instanceof VersionedTransaction2) {
if (agent.config.signOnly) {
return await agent.wallet.signTransaction(instructionsOrTransaction);
}
if (!agent.wallet.signAndSendTransaction) {
throw new Error(
"Wallet does not support signAndSendTransaction please implement it manually or use the signOnly option"
);
}
return (await agent.wallet.signAndSendTransaction(instructionsOrTransaction)).signature;
}
const ixComputeBudget = await getComputeBudgetInstructions(
agent,
instructionsOrTransaction,
feeTier ?? "mid"
);
const allInstructions = [
ixComputeBudget.computeBudgetLimitInstruction,
ixComputeBudget.computeBudgetPriorityFeeInstructions,
...instructionsOrTransaction
];
const { blockhash } = await agent.connection.getLatestBlockhash();
const messageV0 = new TransactionMessage2({
payerKey: agent.wallet.publicKey,
recentBlockhash: blockhash,
instructions: allInstructions
}).compileToV0Message();
const transaction = new VersionedTransaction2(messageV0);
transaction.sign([...otherKeypairs ?? []]);
const signedTransaction = await agent.wallet.signTransaction(transaction);
if (agent.config.signOnly) {
return signedTransaction;
}
return sendTx(
agent,
instructionsOrTransaction,
otherKeypairs,
feeTier
);
}
// src/utils/keypairWallet.ts
import {
Connection as Connection2,
VersionedTransaction as VersionedTransaction3
} from "@solana/web3.js";
import nacl from "tweetnacl";
var isVersionedTransaction = (tx) => {
return "version" in tx;
};
var KeypairWallet = class {
publicKey;
payer;
rpcUrl;
/**
* Constructs a KeypairWallet with a given Keypair
* @param keypair - The Keypair to use for signing transactions
*/
constructor(keypair, rpcUrl) {
this.publicKey = keypair.publicKey;
this.payer = keypair;
this.rpcUrl = rpcUrl;
}
defaultOptions = {
preflightCommitment: "processed",
commitment: "processed"
};
async signTransaction(transaction) {
if (isVersionedTransaction(transaction)) {
transaction.sign([this.payer]);
} else {
transaction.partialSign(this.payer);
}
return transaction;
}
async signAllTransactions(txs) {
return txs.map((t) => {
if (isVersionedTransaction(t)) {
t.sign([this.payer]);
} else {
t.partialSign(this.payer);
}
return t;
});
}
async sendTransaction(transaction) {
const connection = new Connection2(this.rpcUrl);
if (transaction instanceof VersionedTransaction3) {
transaction.sign([this.payer]);
} else {
transaction.partialSign(this.payer);
}
return await connection.sendRawTransaction(transaction.serialize());
}
async signMessage(message) {
const signature = nacl.sign.detached(message, this.payer.secretKey);
return signature;
}
async signAndSendTransaction(transaction, options) {
const connection = new Connection2(this.rpcUrl);
if (transaction instanceof VersionedTransaction3) {
transaction.sign([this.payer]);
} else {
transaction.partialSign(this.payer);
}
const signature = await connection.sendRawTransaction(
transaction.serialize(),
options
);
return { signature };
}
};
// src/utils/getMintInfo.ts
import {
getMint,
TOKEN_2022_PROGRAM_ID,
TOKEN_PROGRAM_ID,
TokenInvalidAccountOwnerError
} from "@solana/spl-token";
import { PublicKey } from "@solana/web3.js";
async function getMintInfo(connection, mint, commitment) {
const publicKey = new PublicKey(mint);
try {
const mintInfo = await getMint(
connection,
publicKey,
commitment,
TOKEN_PROGRAM_ID
);
return mintInfo;
} catch (e) {
if (e instanceof TokenInvalidAccountOwnerError) {
const mintInfo = await getMint(
connection,
publicKey,
commitment,
TOKEN_2022_PROGRAM_ID
);
return mintInfo;
}
throw new Error(
`Failed to fetch mint info for token ${mint}: ${e instanceof Error ? e.message : "Unknown error"}`
);
}
}
export {
KeypairWallet,
SolanaAgentKit,
createLangchainTools,
createOpenAITools,
createSolanaTools as createVercelAITools,
executeAction,
feeTiers,
getActionExamples,
getComputeBudgetInstructions,
getMintInfo,
isVersionedTransaction,
sendTx,
signOrSendTX
};