From 484a64de851755d0cdbc642c19a16f87ed67ef3a Mon Sep 17 00:00:00 2001 From: michaelessiet Date: Wed, 15 Jan 2025 14:52:13 +0100 Subject: [PATCH 1/6] fix: drift user account info fetching --- src/tools/drift/drift.ts | 4 +- test/agent_sdks/vercel_ai.ts | 272 +++++++++++++++++------------------ 2 files changed, 139 insertions(+), 137 deletions(-) diff --git a/src/tools/drift/drift.ts b/src/tools/drift/drift.ts index 97105a4..1a8b4a9 100644 --- a/src/tools/drift/drift.ts +++ b/src/tools/drift/drift.ts @@ -254,7 +254,7 @@ export async function withdrawFromDriftUserAccount( const tx = new Transaction().add(...withdrawInstruction).add( ComputeBudgetProgram.setComputeUnitPrice({ - microLamports: 0.000001 * 1000000 * 1000000, + microLamports: 0.000003 * 1000000 * 1000000, }), ); tx.recentBlockhash = latestBlockhash.blockhash; @@ -388,9 +388,11 @@ export async function doesUserHaveDriftAccount(agent: SolanaAgentKit) { agent.wallet.publicKey, ), }); + await user.subscribe(); user.getActivePerpPositions(); const userAccountExists = await user.exists(); await cleanUp(); + await user.unsubscribe(); return { hasAccount: userAccountExists, account: user.userAccountPublicKey, diff --git a/test/agent_sdks/vercel_ai.ts b/test/agent_sdks/vercel_ai.ts index 77fda22..8fd19c1 100644 --- a/test/agent_sdks/vercel_ai.ts +++ b/test/agent_sdks/vercel_ai.ts @@ -8,191 +8,191 @@ import { createOpenAI } from "@ai-sdk/openai"; dotenv.config(); function validateEnvironment(): void { - const missingVars: string[] = []; - const requiredVars = ["OPENAI_API_KEY", "RPC_URL", "SOLANA_PRIVATE_KEY"]; + const missingVars: string[] = []; + const requiredVars = ["OPENAI_API_KEY", "RPC_URL", "SOLANA_PRIVATE_KEY"]; - requiredVars.forEach((varName) => { - if (!process.env[varName]) { - missingVars.push(varName); - } - }); + requiredVars.forEach((varName) => { + if (!process.env[varName]) { + missingVars.push(varName); + } + }); - if (missingVars.length > 0) { - console.error("Error: Required environment variables are not set"); - missingVars.forEach((varName) => { - console.error(`${varName}=your_${varName.toLowerCase()}_here`); - }); - process.exit(1); - } + if (missingVars.length > 0) { + console.error("Error: Required environment variables are not set"); + missingVars.forEach((varName) => { + console.error(`${varName}=your_${varName.toLowerCase()}_here`); + }); + process.exit(1); + } } validateEnvironment(); async function runAutonomousMode(interval = 10) { - console.log("Starting autonomous mode..."); - const openai = createOpenAI({ - apiKey: process.env.OPENAI_API_KEY as string, - }); + console.log("Starting autonomous mode..."); + const openai = createOpenAI({ + apiKey: process.env.OPENAI_API_KEY as string, + }); - const solanaAgent = new SolanaAgentKit( - process.env.SOLANA_PRIVATE_KEY!, - process.env.RPC_URL!, - { - OPENAI_API_KEY: process.env.OPENAI_API_KEY!, - }, - ); + const solanaAgent = new SolanaAgentKit( + process.env.SOLANA_PRIVATE_KEY!, + process.env.RPC_URL!, + { + OPENAI_API_KEY: process.env.OPENAI_API_KEY!, + }, + ); - const tools = createVercelAITools(solanaAgent); + const tools = createVercelAITools(solanaAgent); - while (true) { - try { - const thought = - "Be creative and do something interesting on the blockchain. " + - "Choose an action or set of actions and execute it that highlights your abilities."; + while (true) { + try { + const thought = + "Be creative and do something interesting on the blockchain. " + + "Choose an action or set of actions and execute it that highlights your abilities."; - const response = streamText({ - prompt: thought, - tools, - model: openai("gpt-4o-mini"), - temperature: 0.7, - system: `You are a helpful agent that can interact onchain using the Solana Agent Kit. You are + const response = streamText({ + prompt: thought, + tools, + model: openai("gpt-4o-mini"), + temperature: 0.7, + system: `You are a helpful agent that can interact onchain using the Solana Agent Kit. You are empowered to interact onchain using your tools. If you ever need funds, you can request them from the faucet. If not, you can provide your wallet details and request funds from the user. If there is a 5XX (internal) HTTP error code, ask the user to try again later. If someone asks you to do something you can't do with your currently available tools, you must say so, and encourage them to implement it themselves using the Solana Agent Kit, recommend they go to https://www.solanaagentkit.xyz for more information. Be concise and helpful with your responses. Refrain from restating your tools' descriptions unless it is explicitly requested.`, - maxSteps: 10, - }); + maxSteps: 10, + }); - for await (const textPart of response.textStream) { - process.stdout.write(textPart); - } - console.log(); + for await (const textPart of response.textStream) { + process.stdout.write(textPart); + } + console.log(); - await new Promise((resolve) => setTimeout(resolve, interval * 1000)); - } catch (error) { - if (error instanceof Error) { - console.error("Error:", error.message); - } - process.exit(1); - } - } + await new Promise((resolve) => setTimeout(resolve, interval * 1000)); + } catch (error) { + if (error instanceof Error) { + console.error("Error:", error.message); + } + process.exit(1); + } + } } async function runChatMode() { - console.log("Starting chat mode... Type 'exit' to end."); - const openai = createOpenAI({ - apiKey: process.env.OPENAI_API_KEY as string, - }); + console.log("Starting chat mode... Type 'exit' to end."); + const openai = createOpenAI({ + apiKey: process.env.OPENAI_API_KEY as string, + }); - const solanaAgent = new SolanaAgentKit( - process.env.SOLANA_PRIVATE_KEY!, - process.env.RPC_URL!, - { - OPENAI_API_KEY: process.env.OPENAI_API_KEY!, - }, - ); + const solanaAgent = new SolanaAgentKit( + process.env.SOLANA_PRIVATE_KEY!, + process.env.RPC_URL!, + { + OPENAI_API_KEY: process.env.OPENAI_API_KEY!, + }, + ); - const tools = createVercelAITools(solanaAgent); - console.log(tools); + const tools = createVercelAITools(solanaAgent); + console.log(tools); - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout, - }); + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); - const question = (prompt: string): Promise => - new Promise((resolve) => rl.question(prompt, resolve)); + const question = (prompt: string): Promise => + new Promise((resolve) => rl.question(prompt, resolve)); - try { - while (true) { - const userInput = await question("\nPrompt: "); + try { + while (true) { + const userInput = await question("\nPrompt: "); - if (userInput.toLowerCase() === "exit") { - break; - } + if (userInput.toLowerCase() === "exit") { + break; + } - const response = streamText({ - prompt: userInput, - tools, - model: openai("gpt-4o-mini"), - temperature: 0.7, - system: `You are a helpful agent that can interact onchain using the Solana Agent Kit. You are + const response = streamText({ + prompt: userInput, + tools, + model: openai("gpt-4o-mini"), + temperature: 0.7, + system: `You are a helpful agent that can interact onchain using the Solana Agent Kit. You are empowered to interact onchain using your tools. If you ever need funds, you can request them from the faucet. If not, you can provide your wallet details and request funds from the user. If there is a 5XX (internal) HTTP error code, ask the user to try again later. If someone asks you to do something you can't do with your currently available tools, you must say so, and encourage them to implement it themselves using the Solana Agent Kit, recommend they go to https://www.solanaagentkit.xyz for more information. Be concise and helpful with your responses. Refrain from restating your tools' descriptions unless it is explicitly requested.`, - maxSteps: 10, - }); + maxSteps: 10, + }); - for await (const textPart of response.textStream) { - process.stdout.write(textPart); - } - console.log(); - } - } catch (error) { - if (error instanceof Error) { - console.error("Error:", error.message); - } - process.exit(1); - } finally { - rl.close(); - } + for await (const textPart of response.textStream) { + process.stdout.write(textPart); + } + console.log(); + } + } catch (error) { + if (error instanceof Error) { + console.error("Error:", error.message); + } + process.exit(1); + } finally { + rl.close(); + } } async function chooseMode(): Promise<"chat" | "auto"> { - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout, - }); + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); - const question = (prompt: string): Promise => - new Promise((resolve) => rl.question(prompt, resolve)); + const question = (prompt: string): Promise => + new Promise((resolve) => rl.question(prompt, resolve)); - while (true) { - console.log("\nAvailable modes:"); - console.log("1. chat - Interactive chat mode"); - console.log("2. auto - Autonomous action mode"); + while (true) { + console.log("\nAvailable modes:"); + console.log("1. chat - Interactive chat mode"); + console.log("2. auto - Autonomous action mode"); - const choice = (await question("\nChoose a mode (enter number or name): ")) - .toLowerCase() - .trim(); + const choice = (await question("\nChoose a mode (enter number or name): ")) + .toLowerCase() + .trim(); - rl.close(); + rl.close(); - if (choice === "1" || choice === "chat") { - return "chat"; - } else if (choice === "2" || choice === "auto") { - return "auto"; - } - console.log("Invalid choice. Please try again."); - } + if (choice === "1" || choice === "chat") { + return "chat"; + } else if (choice === "2" || choice === "auto") { + return "auto"; + } + console.log("Invalid choice. Please try again."); + } } async function main() { - try { - console.log("Starting Agent..."); - const mode = await chooseMode(); + try { + console.log("Starting Agent..."); + const mode = await chooseMode(); - if (mode === "chat") { - await runChatMode(); - } else { - await runAutonomousMode(); - } - } catch (error) { - if (error instanceof Error) { - console.error("Error:", error.message); - } - process.exit(1); - } + if (mode === "chat") { + await runChatMode(); + } else { + await runAutonomousMode(); + } + } catch (error) { + if (error instanceof Error) { + console.error("Error:", error.message); + } + process.exit(1); + } } if (require.main === module) { - main().catch((error) => { - console.error("Fatal error:", error); - process.exit(1); - }); + main().catch((error) => { + console.error("Fatal error:", error); + process.exit(1); + }); } From 97e96730896551e64f69b16543816c219d3c17fe Mon Sep 17 00:00:00 2001 From: michaelessiet Date: Thu, 16 Jan 2025 19:39:47 +0100 Subject: [PATCH 2/6] feat: add more drift actions --- src/actions/drift/availableMarkets.ts | 55 ++++ src/actions/drift/createDriftUserAccount.ts | 7 +- src/actions/drift/createVault.ts | 9 +- src/actions/drift/depositIntoVault.ts | 4 +- .../drift/depositToDriftUserAccount.ts | 2 +- .../requestUnstakeFromDriftInsuranceFund.ts | 62 ++++ .../drift/requestWithdrawalFromVault.ts | 4 +- .../drift/stakeToDriftInsuranceFund.ts | 59 ++++ src/actions/drift/swapSpotToken.ts | 76 +++++ src/actions/drift/tradeDelegatedDriftVault.ts | 13 +- src/actions/drift/tradePerpAccount.ts | 21 +- .../drift/unstakeFromDriftInsuranceFund.ts | 51 ++++ src/actions/drift/updateDriftVaultDelegate.ts | 4 +- src/actions/drift/updateVault.ts | 31 +- src/actions/drift/withdrawFromDriftAccount.ts | 2 +- src/actions/drift/withdrawFromVault.ts | 2 +- src/actions/index.ts | 11 + src/agent/index.ts | 50 ++++ .../request_unstake_from_insurance_fund.ts | 37 +++ .../drift/stake_to_insurance_fund.ts | 37 +++ src/langchain/drift/swap_spot_token.ts | 37 +++ .../drift/unstake_from_insurance_fund.ts | 32 +++ src/tools/drift/drift.ts | 270 +++++++++++++++++- src/tools/drift/drift_vault.ts | 30 +- 24 files changed, 863 insertions(+), 43 deletions(-) create mode 100644 src/actions/drift/availableMarkets.ts create mode 100644 src/actions/drift/requestUnstakeFromDriftInsuranceFund.ts create mode 100644 src/actions/drift/stakeToDriftInsuranceFund.ts create mode 100644 src/actions/drift/swapSpotToken.ts create mode 100644 src/actions/drift/unstakeFromDriftInsuranceFund.ts create mode 100644 src/langchain/drift/request_unstake_from_insurance_fund.ts create mode 100644 src/langchain/drift/stake_to_insurance_fund.ts create mode 100644 src/langchain/drift/swap_spot_token.ts create mode 100644 src/langchain/drift/unstake_from_insurance_fund.ts diff --git a/src/actions/drift/availableMarkets.ts b/src/actions/drift/availableMarkets.ts new file mode 100644 index 0000000..85bd174 --- /dev/null +++ b/src/actions/drift/availableMarkets.ts @@ -0,0 +1,55 @@ +import { MainnetSpotMarkets } from "@drift-labs/sdk"; +import type { Action } from "../../types"; +import { z } from "zod"; +import { + getAvailableDriftPerpMarkets, + getAvailableDriftSpotMarkets, +} from "../../tools"; + +const availableDriftMarketsAction: Action = { + name: "AVAILABLE_DRIFT_MARKETS", + description: "Get a list of available drift markets", + similes: [ + "get drift markets", + "drift markets", + "available drift markets", + "get available drift perp markets", + "get available spot markets on drift", + ], + examples: [ + [ + { + input: { + marketType: "spot", + }, + output: { + status: "success", + message: `The list of available spot markets are ${MainnetSpotMarkets.map((v) => v.symbol).join(", ")}`, + data: MainnetSpotMarkets, + }, + explanation: "Get the list of available spot markets/tokens on drift", + }, + ], + ], + schema: z.object({ + marketType: z + .enum(["spot", "perp"]) + .describe("Type of market to get") + .optional(), + }), + handler: async (agent, input) => { + switch (input.marketType) { + case "perp": + return getAvailableDriftPerpMarkets(); + case "spot": + return getAvailableDriftSpotMarkets(); + default: + return { + spot: getAvailableDriftSpotMarkets(), + perp: getAvailableDriftPerpMarkets(), + }; + } + }, +}; + +export default availableDriftMarketsAction; diff --git a/src/actions/drift/createDriftUserAccount.ts b/src/actions/drift/createDriftUserAccount.ts index 32c62ef..43638c3 100644 --- a/src/actions/drift/createDriftUserAccount.ts +++ b/src/actions/drift/createDriftUserAccount.ts @@ -27,7 +27,12 @@ const createDriftUserAccountAction: Action = { ], ], schema: z.object({ - amount: z.number().positive().describe("Amount of the token to deposit"), + amount: z + .number() + .positive() + .describe( + "Amount of the token to deposit. In normal token amounts e.g 50 SOL, 100 USDC, etc", + ), symbol: z.string().describe("Symbol of the token to deposit"), }), handler: async (agent, input) => { diff --git a/src/actions/drift/createVault.ts b/src/actions/drift/createVault.ts index 26ac59b..e09e07d 100644 --- a/src/actions/drift/createVault.ts +++ b/src/actions/drift/createVault.ts @@ -53,9 +53,14 @@ const createDriftVaultAction: Action = { .int() .min(100, "Max tokens must be at least 100") .describe( - "The maximum amount of tokens the vault will be accomodating. For example some vaults have a cap at 10 million USDC", + "The maximum amount of tokens the vault will be accomodating. For example some vaults have a cap at 10 million USDC. This amount should be normal token amounts e.g 50 SOL, 100 USDC, etc", + ), + minDepositAmount: z + .number() + .positive() + .describe( + "Minimum deposit amount in normal token values e.g 50 SOL, 100 USDC, etc", ), - minDepositAmount: z.number().positive().describe("Minimum deposit amount"), managementFee: z .number() .positive() diff --git a/src/actions/drift/depositIntoVault.ts b/src/actions/drift/depositIntoVault.ts index eddb9f0..ca14c53 100644 --- a/src/actions/drift/depositIntoVault.ts +++ b/src/actions/drift/depositIntoVault.ts @@ -28,7 +28,9 @@ const depositIntoDriftVaultAction: Action = { amount: z .number() .positive() - .describe("The amount in tokens you'd like to deposit into the vault"), + .describe( + "The amount in tokens you'd like to deposit into the vault in normal token amounts e.g 50 SOL, 100 USDC, etc", + ), }), handler: async (agent, input) => { try { diff --git a/src/actions/drift/depositToDriftUserAccount.ts b/src/actions/drift/depositToDriftUserAccount.ts index af152c3..3e0dc7b 100644 --- a/src/actions/drift/depositToDriftUserAccount.ts +++ b/src/actions/drift/depositToDriftUserAccount.ts @@ -34,7 +34,7 @@ const depositToDriftUserAccountAction: Action = { .number() .positive() .describe( - "The amount in tokens you'd like to deposit into your drift user account", + "The amount in tokens you'd like to deposit into your drift user account in normal token amounts e.g 50 SOL, 100 USDC, etc", ), symbol: z .string() diff --git a/src/actions/drift/requestUnstakeFromDriftInsuranceFund.ts b/src/actions/drift/requestUnstakeFromDriftInsuranceFund.ts new file mode 100644 index 0000000..fc1b645 --- /dev/null +++ b/src/actions/drift/requestUnstakeFromDriftInsuranceFund.ts @@ -0,0 +1,62 @@ +import { z } from "zod"; +import type { Action } from "../../types"; +import { requestUnstakeFromDriftInsuranceFund } from "../../tools"; + +const requestUnstakeFromDriftInsuranceFundAction: Action = { + name: "REQUEST_UNSTAKE_FROM_DRIFT_INSURANCE_FUND_ACTION", + description: + "Request to unstake a certain amount of a token from the Drift Insurance Fund", + similes: [ + "request an unstake from the drift insurance fund", + "unstake an amount from the drift insurance fund", + "ask to unstake a certain amount from the drift insurance fund", + ], + examples: [ + [ + { + input: { + amount: 100, + symbol: "SOL", + }, + output: { + status: "success", + message: "Requested to unstake 100 SOL from the Drift Insurance Fund", + signature: "4FdasklhiIHyOI", + }, + explanation: "Request to unstake 100 SOL from the Drift Insurance Fund", + }, + ], + ], + schema: z.object({ + amount: z + .number() + .positive() + .describe("Amount to unstake in normal units e.g 50 === 50 SOL"), + symbol: z.string().describe("Symbol of the token to unstake"), + }), + handler: async (agent, input) => { + try { + const tx = await requestUnstakeFromDriftInsuranceFund( + agent, + input.amount, + input.symbol, + ); + + return { + status: "success", + message: `Requested to unstake ${input.amount} ${input.symbol} from the Drift Insurance Fund`, + data: { + signature: tx, + }, + }; + } catch (e) { + return { + status: "error", + // @ts-expect-error error is not a string + message: e.message, + }; + } + }, +}; + +export default requestUnstakeFromDriftInsuranceFundAction; diff --git a/src/actions/drift/requestWithdrawalFromVault.ts b/src/actions/drift/requestWithdrawalFromVault.ts index df6939c..9dd1641 100644 --- a/src/actions/drift/requestWithdrawalFromVault.ts +++ b/src/actions/drift/requestWithdrawalFromVault.ts @@ -29,7 +29,9 @@ const requestWithdrawalFromVaultAction: Action = { amount: z .number() .positive() - .describe("Amount of shares you would like to withdraw from the vault"), + .describe( + "Amount of shares you would like to withdraw from the vault in normal token amounts e.g 50 SOL, 100 USDC, etc", + ), }), handler: async (agent: SolanaAgentKit, input) => { try { diff --git a/src/actions/drift/stakeToDriftInsuranceFund.ts b/src/actions/drift/stakeToDriftInsuranceFund.ts new file mode 100644 index 0000000..39cf179 --- /dev/null +++ b/src/actions/drift/stakeToDriftInsuranceFund.ts @@ -0,0 +1,59 @@ +import { z } from "zod"; +import type { Action } from "../../types"; +import { stakeToDriftInsuranceFund } from "../../tools"; + +const stakeToDriftInsuranceFundAction: Action = { + name: "STAKE_TO_DRIFT_INSURANCE_FUND_ACTION", + description: "Stake a token to Drift Insurance Fund", + similes: ["Stake a token to Drift Insurance Fund"], + examples: [ + [ + { + input: { + amount: 100, + symbol: "SOL", + }, + output: { + status: "success", + message: "Staked 100 SOL to the Drift Insurance Fund", + data: { + signature: "signature", + }, + }, + explanation: "Stake 100 SOL to the Drift Insurance Fund", + }, + ], + ], + schema: z.object({ + amount: z + .number() + .positive() + .describe("Amount to stake in normal units e.g 50 === 50 SOL"), + symbol: z.string().describe("Symbol of the token stake"), + }), + handler: async (agent, input) => { + try { + const tx = await stakeToDriftInsuranceFund( + agent, + input.amount, + input.symbol, + ); + + return { + status: "sucess", + message: `Staked ${input.amount} ${input.symbol} to the Drift Insurance Fund`, + data: { + signature: tx, + }, + }; + } catch (error) { + return { + status: "error", + // @ts-expect-error error is not a string + message: error.message, + }; + } + }, +}; + +export default stakeToDriftInsuranceFundAction; diff --git a/src/actions/drift/swapSpotToken.ts b/src/actions/drift/swapSpotToken.ts new file mode 100644 index 0000000..a076cb8 --- /dev/null +++ b/src/actions/drift/swapSpotToken.ts @@ -0,0 +1,76 @@ +import { z } from "zod"; +import type { Action } from "../../types"; +import { swapSpotToken } from "../../tools"; + +const driftSpotTokenSwapAction: Action = { + name: "DRIFT_SPOT_TOKEN_SWAP_ACTION", + description: "Swap a spot token for another spot token on Drift", + similes: [ + "swap a token for another token on drift", + "exchange a token for another token on drift", + "trade a token for another token on drift", + ], + examples: [ + [ + { + input: { + fromSymbol: "SOL", + toSymbol: "USDC", + fromAmount: 100, + }, + output: { + status: "success", + message: "Swapped 100 SOL for USDC on Drift", + signature: "4FdasklhiIHyOI", + }, + explanation: "Swap 100 SOL for USDC on Drift", + }, + ], + ], + schema: z.object({ + fromSymbol: z.string().describe("Symbol of the token to swap from"), + toSymbol: z.string().describe("Symbol of the token to swap to"), + fromAmount: z + .number() + .positive() + .describe("Amount to swap from in normal units e.g 50 === 50 SOL") + .optional(), + toAmount: z + .number() + .positive() + .describe("Amount to swap to in normal units e.g 5000 === 5000 USDC") + .optional(), + slippage: z + .number() + .positive() + .describe("Slippage tolerance in percentage e.g 0.5 === 0.5%") + .optional(), + }), + handler: async (agent, input) => { + try { + const tx = await swapSpotToken(agent, { + fromSymbol: input.fromSymbol, + toSymbol: input.toSymbol, + fromAmount: input.fromAmount, + toAmount: input.toAmount, + slippage: input.slippage, + }); + + return { + status: "success", + message: `Swapped ${input.fromAmount} ${input.fromSymbol} for ${input.toAmount} ${input.toSymbol} on Drift`, + data: { + signature: tx, + }, + }; + } catch (e) { + return { + status: "error", + // @ts-expect-error error is not a string + message: e.message, + }; + } + }, +}; + +export default driftSpotTokenSwapAction; diff --git a/src/actions/drift/tradeDelegatedDriftVault.ts b/src/actions/drift/tradeDelegatedDriftVault.ts index c85d8b3..5d28a84 100644 --- a/src/actions/drift/tradeDelegatedDriftVault.ts +++ b/src/actions/drift/tradeDelegatedDriftVault.ts @@ -64,11 +64,20 @@ const tradeDelegatedDriftVaultAction: Action = { ], schema: z.object({ vaultAddress: z.string().describe("Address of the Drift vault to trade in"), - amount: z.number().positive().describe("Amount to trade"), + amount: z + .number() + .positive() + .describe( + "Amount to trade in normal token amounts e.g 50 SOL, 100 USDC, etc", + ), symbol: z.string().describe("Symbol of the token to trade"), action: z.enum(["long", "short"]).describe("Trade action - long or short"), type: z.enum(["market", "limit"]).describe("Trade type - market or limit"), - price: z.number().positive().optional().describe("Price for limit order"), + price: z + .number() + .positive() + .optional() + .describe("USD price for limit order"), }), handler: async (agent: SolanaAgentKit, input) => { try { diff --git a/src/actions/drift/tradePerpAccount.ts b/src/actions/drift/tradePerpAccount.ts index ae1db7e..15a53b4 100644 --- a/src/actions/drift/tradePerpAccount.ts +++ b/src/actions/drift/tradePerpAccount.ts @@ -46,14 +46,27 @@ export const tradeDriftPerpAccountAction: Action = { ], ], schema: z.object({ - amount: z.number().positive(), + amount: z + .number() + .positive() + .describe( + "The amount of the token to trade in normal token amounts e.g 50 SOL, 100 USDC", + ), symbol: z .string() .toUpperCase() .describe("Symbol of the token to open a position on "), - action: z.enum(["long", "short"]), - type: z.enum(["market", "limit"]), - price: z.number().positive().optional(), + action: z + .enum(["long", "short"]) + .describe( + "The action you would like to carry out whether it be a long or a short", + ), + type: z + .enum(["market", "limit"]) + .describe( + "The type of trade you would like to open, market or limit order", + ), + price: z.number().positive().optional().describe("USD price of the token"), }), handler: async (agent, input) => { try { diff --git a/src/actions/drift/unstakeFromDriftInsuranceFund.ts b/src/actions/drift/unstakeFromDriftInsuranceFund.ts new file mode 100644 index 0000000..4d9516e --- /dev/null +++ b/src/actions/drift/unstakeFromDriftInsuranceFund.ts @@ -0,0 +1,51 @@ +import { z } from "zod"; +import type { Action } from "../../types"; +import { unstakeFromDriftInsuranceFund } from "../../tools"; + +const unstakeFromDriftInsuranceFundAction: Action = { + name: "UNSTAKE_FROM_DRIFT_INSURANCE_FUND_ACTION", + description: + "Unstake requested unstake amount from the Drift Insurance fund once the cool period has elapsed", + similes: [ + "unstake from the drift insurance fund", + "withdraw from the drift insurance fund", + "take out funds from the drift insurance fund", + ], + examples: [ + [ + { + input: { + symbol: "SOL", + }, + output: { + status: "success", + message: "Unstaked your SOL from the Drift Insurance Fund", + signature: "4FdasklhiIHyOI", + }, + explanation: "Unstake SOL from the Drift Insurance Fund", + }, + ], + ], + schema: z.object({ + symbol: z.string().describe("Symbol of the token to unstake"), + }), + handler: async (agent, input) => { + try { + const tx = await unstakeFromDriftInsuranceFund(agent, input.symbol); + + return { + status: "success", + message: `Unstaked your ${input.symbol} from the Drift Insurance Fund`, + signature: tx, + }; + } catch (e) { + return { + status: "error", + // @ts-expect-error error is not a string + message: e.message, + }; + } + }, +}; + +export default unstakeFromDriftInsuranceFundAction; diff --git a/src/actions/drift/updateDriftVaultDelegate.ts b/src/actions/drift/updateDriftVaultDelegate.ts index defc7a6..d203e0d 100644 --- a/src/actions/drift/updateDriftVaultDelegate.ts +++ b/src/actions/drift/updateDriftVaultDelegate.ts @@ -24,8 +24,8 @@ const updateDriftVaultDelegateAction: Action = { ], ], schema: z.object({ - vaultAddress: z.string(), - newDelegate: z.string(), + vaultAddress: z.string().describe("vault's address"), + newDelegate: z.string().describe("new address to delegate the vault to"), }), handler: async (agent, input) => { try { diff --git a/src/actions/drift/updateVault.ts b/src/actions/drift/updateVault.ts index 4d0f66e..2d0abed 100644 --- a/src/actions/drift/updateVault.ts +++ b/src/actions/drift/updateVault.ts @@ -41,16 +41,35 @@ const updateDriftVaultAction: Action = { redeemPeriod: z .number() .int() - .min(1, "Redeem period must be at least 1") + .min(1, "Redeem period must be at least 1 day") .optional(), maxTokens: z .number() .int() - .min(100, "Max tokens must be at least 100") - .optional(), - minDepositAmount: z.number().positive().optional(), - managementFee: z.number().positive().max(20).optional(), - profitShare: z.number().positive().max(90).optional(), + .min(100, "Max tokens must be at least be 100 units") + .optional() + .describe( + "The maximum number of tokens the vault is willing to accept and manage", + ), + minDepositAmount: z + .number() + .positive() + .optional() + .describe( + "The minimum amount that is allowed to be deposited into the vault in normal token amounts e.g 10 USDC", + ), + managementFee: z + .number() + .positive() + .max(20) + .optional() + .describe("The percentage fee the vault takes for asset management"), + profitShare: z + .number() + .positive() + .max(90) + .optional() + .describe("Profit share in percentage e.g 2 === 2%"), handleRate: z.number().optional(), permissioned: z .boolean() diff --git a/src/actions/drift/withdrawFromDriftAccount.ts b/src/actions/drift/withdrawFromDriftAccount.ts index 00d72b6..693a294 100644 --- a/src/actions/drift/withdrawFromDriftAccount.ts +++ b/src/actions/drift/withdrawFromDriftAccount.ts @@ -36,7 +36,7 @@ const withdrawFromDriftAccountAction: Action = { .number() .positive() .describe( - "The amount in tokens you'd like to withdraw from your drift account", + "The amount in tokens you'd like to withdraw from your drift account in normal token amounts, e.g 50 SOL, 100 USDC, etc", ), symbol: z .string() diff --git a/src/actions/drift/withdrawFromVault.ts b/src/actions/drift/withdrawFromVault.ts index b6007f2..4b4318b 100644 --- a/src/actions/drift/withdrawFromVault.ts +++ b/src/actions/drift/withdrawFromVault.ts @@ -25,7 +25,7 @@ const withdrawFromVaultAction: Action = { ], ], schema: z.object({ - vaultAddress: z.string(), + vaultAddress: z.string().describe("Vault's address"), }), handler: async (agent: SolanaAgentKit, input) => { try { diff --git a/src/actions/index.ts b/src/actions/index.ts index 29cf233..5b4324e 100644 --- a/src/actions/index.ts +++ b/src/actions/index.ts @@ -59,6 +59,11 @@ import withdrawFromDriftAccountAction from "./drift/withdrawFromDriftAccount"; import driftUserAccountInfoAction from "./drift/driftUserAccountInfo"; import deriveDriftVaultAddressAction from "./drift/deriveVaultAddress"; import updateDriftVaultDelegateAction from "./drift/updateDriftVaultDelegate"; +import availableDriftMarketsAction from "./drift/availableMarkets"; +import stakeToDriftInsuranceFundAction from "./drift/stakeToDriftInsuranceFund"; +import requestUnstakeFromDriftInsuranceFundAction from "./drift/requestUnstakeFromDriftInsuranceFund"; +import unstakeFromDriftInsuranceFundAction from "./drift/unstakeFromDriftInsuranceFund"; +import driftSpotTokenSwapAction from "./drift/swapSpotToken"; export const ACTIONS = { WALLET_ADDRESS_ACTION: getWalletAddressAction, @@ -123,6 +128,12 @@ export const ACTIONS = { DRIFT_USER_ACCOUNT_INFO_ACTION: driftUserAccountInfoAction, DERIVE_DRIFT_VAULT_ADDRESS_ACTION: deriveDriftVaultAddressAction, UPDATE_DRIFT_VAULT_DELEGATE_ACTION: updateDriftVaultDelegateAction, + AVAILABLE_DRIFT_MARKETS_ACTION: availableDriftMarketsAction, + STAKE_TO_DRIFT_INSURANCE_FUND_ACTION: stakeToDriftInsuranceFundAction, + REQUEST_UNSTAKE_FROM_DRIFT_INSURANCE_FUND_ACTION: + requestUnstakeFromDriftInsuranceFundAction, + UNSTAKE_FROM_DRIFT_INSURANCE_FUND_ACTION: unstakeFromDriftInsuranceFundAction, + DRIFT_SPOT_TOKEN_SWAP_ACTION: driftSpotTokenSwapAction, }; export type { Action, ActionExample, Handler } from "../types/action"; diff --git a/src/agent/index.ts b/src/agent/index.ts index d087931..0f6c834 100644 --- a/src/agent/index.ts +++ b/src/agent/index.ts @@ -98,6 +98,12 @@ import { withdrawFromDriftVault, updateVaultDelegate, get_token_balance, + getAvailableDriftSpotMarkets, + getAvailableDriftPerpMarkets, + stakeToDriftInsuranceFund, + requestUnstakeFromDriftInsuranceFund, + unstakeFromDriftInsuranceFund, + swapSpotToken, } from "../tools"; import { Config, @@ -821,4 +827,48 @@ export class SolanaAgentKit { async updateDriftVaultDelegate(vaultAddress: string, delegate: string) { return await updateVaultDelegate(this, vaultAddress, delegate); } + getAvailableDriftMarkets(type?: "spot" | "perp") { + switch (type) { + case "spot": + return getAvailableDriftSpotMarkets(); + case "perp": + return getAvailableDriftPerpMarkets(); + default: + return { + spot: getAvailableDriftSpotMarkets(), + perp: getAvailableDriftPerpMarkets(), + }; + } + } + async stakeToDriftInsuranceFund(amount: number, symbol: string) { + return await stakeToDriftInsuranceFund(this, amount, symbol); + } + async requestUnstakeFromDriftInsuranceFund(amount: number, symbol: string) { + return await requestUnstakeFromDriftInsuranceFund(this, amount, symbol); + } + async unstakeFromDriftInsuranceFund(symbol: string) { + return await unstakeFromDriftInsuranceFund(this, symbol); + } + async driftSpotTokenSwap( + params: { + fromSymbol: string; + toSymbol: string; + slippage?: number; + } & ( + | { + toAmount: number; + } + | { fromAmount: number } + ), + ) { + return await swapSpotToken(this, { + fromSymbol: params.fromSymbol, + toSymbol: params.toSymbol, + // @ts-expect-error - fromAmount and toAmount are mutually exclusive + fromAmount: params.fromAmount, + // @ts-expect-error - fromAmount and toAmount are mutually exclusive + toAmount: params.toAmount, + slippage: params.slippage, + }); + } } diff --git a/src/langchain/drift/request_unstake_from_insurance_fund.ts b/src/langchain/drift/request_unstake_from_insurance_fund.ts new file mode 100644 index 0000000..edeb630 --- /dev/null +++ b/src/langchain/drift/request_unstake_from_insurance_fund.ts @@ -0,0 +1,37 @@ +import { Tool } from "langchain/tools"; +import type { SolanaAgentKit } from "../../agent"; + +export class SolanaRequestUnstakeFromDriftInsuranceFundTool extends Tool { + name = "request_unstake_from_drift_insurance_fund"; + description = `Request to unstake tokens from Drift Insurance Fund. + + Inputs (JSON string): + - amount: number, amount to unstake (required) + - symbol: string, token symbol (required)`; + + constructor(private solanaKit: SolanaAgentKit) { + super(); + } + + protected async _call(input: string): Promise { + try { + const parsedInput = JSON.parse(input); + const tx = await this.solanaKit.requestUnstakeFromDriftInsuranceFund( + parsedInput.amount, + parsedInput.symbol, + ); + + return JSON.stringify({ + status: "success", + message: `Requested unstake of ${parsedInput.amount} ${parsedInput.symbol} from the Drift Insurance Fund`, + signature: tx, + }); + } catch (error: any) { + return JSON.stringify({ + status: "error", + message: error.message, + code: error.code || "REQUEST_UNSTAKE_FROM_DRIFT_INSURANCE_FUND_ERROR", + }); + } + } +} diff --git a/src/langchain/drift/stake_to_insurance_fund.ts b/src/langchain/drift/stake_to_insurance_fund.ts new file mode 100644 index 0000000..09cc363 --- /dev/null +++ b/src/langchain/drift/stake_to_insurance_fund.ts @@ -0,0 +1,37 @@ +import { Tool } from "langchain/tools"; +import type { SolanaAgentKit } from "../../agent"; + +export class SolanaStakeToDriftInsuranceFundTool extends Tool { + name = "stake_to_drift_insurance_fund"; + description = `Stake a token to Drift Insurance Fund. + + Inputs (JSON string): + - amount: number, amount to stake (required) + - symbol: string, token symbol (required)`; + + constructor(private solanaKit: SolanaAgentKit) { + super(); + } + + protected async _call(input: string): Promise { + try { + const parsedInput = JSON.parse(input); + const tx = await this.solanaKit.stakeToDriftInsuranceFund( + parsedInput.amount, + parsedInput.symbol, + ); + + return JSON.stringify({ + status: "success", + message: `Staked ${parsedInput.amount} ${parsedInput.symbol} to the Drift Insurance Fund`, + signature: tx, + }); + } catch (error: any) { + return JSON.stringify({ + status: "error", + message: error.message, + code: error.code || "STAKE_TO_DRIFT_INSURANCE_FUND_ERROR", + }); + } + } +} diff --git a/src/langchain/drift/swap_spot_token.ts b/src/langchain/drift/swap_spot_token.ts new file mode 100644 index 0000000..f5f7314 --- /dev/null +++ b/src/langchain/drift/swap_spot_token.ts @@ -0,0 +1,37 @@ +import { Tool } from "langchain/tools"; +import type { SolanaAgentKit } from "../../agent"; + +export class SolanaDriftSpotTokenSwapTool extends Tool { + name = "drift_spot_token_swap"; + description = `Swap spot tokens on Drift protocol. + + Inputs (JSON string): + - fromSymbol: string, symbol of token to swap from (required) + - toSymbol: string, symbol of token to swap to (required) + - fromAmount: number, amount to swap from (optional) required if toAmount is not provided + - toAmount: number, amount to swap to (optional) required if fromAmount is not provided + - slippage: number, slippage tolerance in percentage (optional)`; + + constructor(private solanaKit: SolanaAgentKit) { + super(); + } + + protected async _call(input: string): Promise { + try { + const parsedInput = JSON.parse(input); + const tx = await this.solanaKit.driftSpotTokenSwap(parsedInput); + + return JSON.stringify({ + status: "success", + message: `Swapped ${parsedInput.fromAmount} ${parsedInput.fromSymbol} for ${parsedInput.toSymbol}`, + signature: tx, + }); + } catch (error: any) { + return JSON.stringify({ + status: "error", + message: error.message, + code: error.code || "DRIFT_SPOT_TOKEN_SWAP_ERROR", + }); + } + } +} diff --git a/src/langchain/drift/unstake_from_insurance_fund.ts b/src/langchain/drift/unstake_from_insurance_fund.ts new file mode 100644 index 0000000..b610aae --- /dev/null +++ b/src/langchain/drift/unstake_from_insurance_fund.ts @@ -0,0 +1,32 @@ +import { Tool } from "langchain/tools"; +import type { SolanaAgentKit } from "../../agent"; + +export class SolanaUnstakeFromDriftInsuranceFundTool extends Tool { + name = "unstake_from_drift_insurance_fund"; + description = `Unstake tokens from Drift Insurance Fund after request period has elapsed. + + Inputs (JSON string): + - symbol: string, token symbol (required)`; + + constructor(private solanaKit: SolanaAgentKit) { + super(); + } + + protected async _call(input: string): Promise { + try { + const tx = await this.solanaKit.unstakeFromDriftInsuranceFund(input); + + return JSON.stringify({ + status: "success", + message: `Unstaked ${input} from the Drift Insurance Fund`, + signature: tx, + }); + } catch (error: any) { + return JSON.stringify({ + status: "error", + message: error.message, + code: error.code || "UNSTAKE_FROM_DRIFT_INSURANCE_FUND_ERROR", + }); + } + } +} diff --git a/src/tools/drift/drift.ts b/src/tools/drift/drift.ts index 1a8b4a9..57999f5 100644 --- a/src/tools/drift/drift.ts +++ b/src/tools/drift/drift.ts @@ -7,6 +7,8 @@ import { getLimitOrderParams, getMarketOrderParams, getUserAccountPublicKeySync, + JupiterClient, + MainnetPerpMarkets, MainnetSpotMarkets, numberToSafeBN, PositionDirection, @@ -115,7 +117,10 @@ export async function createDriftUserAccount( ); if (!token) { - throw new Error(`Token with symbol ${symbol} not found`); + throw new Error(`Token with symbol ${symbol} not found. Here's a list of available spot markets: ${MainnetSpotMarkets.map( + (v) => v.symbol, + ).join(", ")} + `); } if (!userAccountExists) { @@ -171,7 +176,11 @@ export async function depositToDriftUserAccount( ); if (!token) { - throw new Error(`Token with symbol ${symbol} not found`); + throw new Error( + `Token with symbol ${symbol} not found. Here's a list of available spot markets: ${MainnetSpotMarkets.map( + (v) => v.symbol, + ).join(", ")}`, + ); } if (!userAccountExists) { @@ -237,7 +246,11 @@ export async function withdrawFromDriftUserAccount( ); if (!token) { - throw new Error(`Token with symbol ${symbol} not found`); + throw new Error( + `Token with symbol ${symbol} not found. Here's a list of available spot markets: ${MainnetSpotMarkets.map( + (v) => v.symbol, + ).join(", ")}`, + ); } const withdrawAmount = numberToSafeBN(amount, token.precision); @@ -313,7 +326,11 @@ export async function driftPerpTrade( ); if (!market) { - throw new Error(`Token with symbol ${params.symbol} not found`); + throw new Error( + `Token with symbol ${params.symbol} not found. Here's a list of available perp markets: ${MainnetPerpMarkets.map( + (v) => v.symbol, + ).join(", ")}`, + ); } const baseAssetPrice = driftClient.getOracleDataForPerpMarket( @@ -435,10 +452,9 @@ export async function driftUserAccountInfo(agent: SolanaAgentKit) { })); const spotPositions = account.spotPositions.map((pos) => ({ ...pos, - scaledBalance: convertToNumber(pos.scaledBalance, BASE_PRECISION), - cumulativeDeposits: convertToNumber( - pos.cumulativeDeposits, - BASE_PRECISION, + availableBalance: convertToNumber( + pos.scaledBalance, + MainnetSpotMarkets[pos.marketIndex].precision, ), symbol: MainnetSpotMarkets.find((v) => v.marketIndex === pos.marketIndex) ?.symbol, @@ -448,8 +464,6 @@ export async function driftUserAccountInfo(agent: SolanaAgentKit) { ...account, name: account.name, authority: account.authority, - totalDeposits: `$${convertToNumber(account.totalDeposits, QUOTE_PRECISION)}`, - totalWithdraws: `$${convertToNumber(account.totalWithdraws, QUOTE_PRECISION)}`, settledPerpPnl: `$${convertToNumber(account.settledPerpPnl, QUOTE_PRECISION)}`, lastActiveSlot: account.lastActiveSlot.toNumber(), perpPositions, @@ -460,3 +474,239 @@ export async function driftUserAccountInfo(agent: SolanaAgentKit) { throw new Error(`Failed to check user account: ${e.message}`); } } + +/** + * Get available spot markets on drift protocol + */ +export function getAvailableDriftSpotMarkets() { + return MainnetSpotMarkets; +} + +/** + * Get available perp markets on drift protocol + */ +export function getAvailableDriftPerpMarkets() { + return MainnetPerpMarkets; +} + +/** + * Stake a token to the drift insurance fund + * @param agent + * @param amount + * @param symbol + */ +export async function stakeToDriftInsuranceFund( + agent: SolanaAgentKit, + amount: number, + symbol: string, +) { + try { + const { cleanUp, driftClient } = await initClients(agent); + const token = MainnetSpotMarkets.find( + (v) => v.symbol === symbol.toUpperCase(), + ); + + if (!token) { + throw new Error( + `Token with symbol ${symbol} not found. Here's a list of available spot markets: ${MainnetSpotMarkets.map( + (v) => v.symbol, + ).join(", ")}`, + ); + } + + const signature = await driftClient.addInsuranceFundStake({ + amount: numberToSafeBN(amount, token.precision), + marketIndex: token.marketIndex, + collateralAccountPublicKey: getAssociatedTokenAddressSync( + token.mint, + agent.wallet.publicKey, + ), + txParams: { + computeUnitsPrice: 0.000002 * 1000000 * 1000000, + }, + }); + + await cleanUp(); + return signature; + } catch (e) { + // @ts-expect-error - error message is a string + throw new Error(`Failed to get APYs: ${e.message}`); + } +} + +/** + * Request an unstake from the drift insurance fund + * @param agent + * @param amount + * @param symbol + */ +export async function requestUnstakeFromDriftInsuranceFund( + agent: SolanaAgentKit, + amount: number, + symbol: string, +) { + try { + const { driftClient, cleanUp } = await initClients(agent); + const token = MainnetSpotMarkets.find( + (v) => v.symbol === symbol.toUpperCase(), + ); + + if (!token) { + throw new Error( + `Token with symbol ${symbol} not found. Here's a list of available spot markets: ${MainnetSpotMarkets.map( + (v) => v.symbol, + ).join(", ")}`, + ); + } + + const signature = await driftClient.requestRemoveInsuranceFundStake( + token.marketIndex, + numberToSafeBN(amount, token.precision), + { computeUnitsPrice: 0.000002 * 1000000 * 1000000 }, + ); + + await cleanUp(); + return signature; + } catch (e) { + // @ts-expect-error error message is a string + throw new Error(`Failed to unstake from insurance fund: ${e.message}`); + } +} + +/** + * Unstake requested funds from the drift insurance fund once cool down period is elapsed + * @param agent + * @param symbol + */ +export async function unstakeFromDriftInsuranceFund( + agent: SolanaAgentKit, + symbol: string, +) { + try { + const { driftClient, cleanUp } = await initClients(agent); + const token = MainnetSpotMarkets.find( + (v) => v.symbol === symbol.toUpperCase(), + ); + + if (!token) { + throw new Error( + `Token with symbol ${symbol} not found. Here's a list of available spot markets: ${MainnetSpotMarkets.map( + (v) => v.symbol, + ).join(", ")}`, + ); + } + + const signature = await driftClient.removeInsuranceFundStake( + token.marketIndex, + getAssociatedTokenAddressSync(token.mint, agent.wallet.publicKey), + { + computeUnitsPrice: 0.000002 * 1000000 * 1000000, + }, + ); + + await cleanUp(); + return signature; + } catch (e) { + // @ts-expect-error error message is a string + throw new Error(`Failed to unstake from insurance fund: ${e.message}`); + } +} + +/** + * Swap a spot token for another on drift + * @param agent + * @param params + * @param params.fromSymbol symbol of the token to deposit + * @param params.toSymbol symbol of the token to receive + * @param params.fromAmount amount of the token to deposit + * @param params.toAmount amount of the token to receive + */ +export async function swapSpotToken( + agent: SolanaAgentKit, + params: { + fromSymbol: string; + toSymbol: string; + slippage?: number | undefined; + } & ( + | { + fromAmount: number; + } + | { + toAmount: number; + } + ), +) { + try { + const { driftClient, cleanUp } = await initClients(agent); + const fromToken = MainnetSpotMarkets.find( + (v) => v.symbol === params.fromSymbol.toUpperCase(), + ); + const toToken = MainnetSpotMarkets.find( + (v) => v.symbol === params.toSymbol.toUpperCase(), + ); + + if (!fromToken) { + throw new Error( + `Token with symbol ${params.fromSymbol} not found. Here's a list of available spot markets: ${MainnetSpotMarkets.map( + (v) => v.symbol, + ).join(", ")}`, + ); + } + + if (!toToken) { + throw new Error( + `Token with symbol ${params.toSymbol} not found. Here's a list of available spot markets: ${MainnetSpotMarkets.map( + (v) => v.symbol, + ).join(", ")}`, + ); + } + + let txSig: string; + + // @ts-expect-error - false undefined type conflict + if (params.fromAmount) { + const jupiterClient = new JupiterClient({ connection: agent.connection }); + // @ts-expect-error - false undefined type conflict + const fromAmount = numberToSafeBN(params.fromAmount, fromToken.precision); + const signature = await driftClient.swap({ + amount: fromAmount, + inMarketIndex: fromToken.marketIndex, + outMarketIndex: toToken.marketIndex, + jupiterClient: jupiterClient, + slippageBps: params.slippage ?? 100, + swapMode: "ExactIn", + }); + + txSig = signature; + } + + // @ts-expect-error - false undefined type conflict + if (params.toAmount) { + const jupiterClient = new JupiterClient({ connection: agent.connection }); + // @ts-expect-error - false undefined type conflict + const toAmount = numberToSafeBN(params.toAmount, toToken.precision); + const signature = await driftClient.swap({ + amount: toAmount, + inMarketIndex: toToken.marketIndex, + outMarketIndex: fromToken.marketIndex, + jupiterClient: jupiterClient, + slippageBps: params.slippage ?? 100, + swapMode: "ExactOut", + }); + + txSig = signature; + } + + await cleanUp(); + + // @ts-expect-error - false use before assignment + if (txSig) { + return txSig; + } + + throw new Error("Either fromAmount or toAmount must be provided"); + } catch (e) { + // @ts-expect-error error message is a string + throw new Error(`Failed to swap token: ${e.message}`); + } +} diff --git a/src/tools/drift/drift_vault.ts b/src/tools/drift/drift_vault.ts index feb23f1..c8d1214 100644 --- a/src/tools/drift/drift_vault.ts +++ b/src/tools/drift/drift_vault.ts @@ -37,14 +37,18 @@ export function getMarketIndexAndType(name: `${string}-${string}`) { if (type === "PERP") { const token = MainnetPerpMarkets.find((v) => v.baseAssetSymbol === symbol); if (!token) { - throw new Error("Drift doesn't have that market"); + throw new Error( + `Drift doesn't have that market. Here's a list of available perp markets: ${MainnetPerpMarkets.map((v) => v.baseAssetSymbol).join(", ")}`, + ); } return { marketIndex: token.marketIndex, marketType: MarketType.PERP }; } const token = MainnetSpotMarkets.find((v) => v.symbol === symbol); if (!token) { - throw new Error("Drift doesn't have that market"); + throw new Error( + `Drift doesn't have that market. Here's a list of available spot markets: ${MainnetSpotMarkets.map((v) => v.symbol).join(", ")}`, + ); } return { marketIndex: token.marketIndex, marketType: MarketType.SPOT }; } @@ -134,22 +138,22 @@ export async function createVault( const { vaultClient, driftClient, cleanUp } = await initClients(agent); const marketIndexAndType = getMarketIndexAndType(params.marketName); - if (!marketIndexAndType) { - throw new Error("Invalid market name"); - } - const spotMarket = driftClient.getSpotMarketAccount( marketIndexAndType.marketIndex, ); if (!spotMarket) { - throw new Error("Market not found"); + throw new Error( + `Market not found. Here's a list of available spot markets: ${MainnetSpotMarkets.map((v) => `${v.symbol}-SPOT`).join(", ")}`, + ); } const spotPrecision = TEN.pow(new BN(spotMarket.decimals)); if (marketIndexAndType.marketType === MarketType.PERP) { - throw new Error("Only SPOT market names are supported"); + throw new Error( + `Only SPOT market names are supported. Such as ${MainnetSpotMarkets.map((v) => `${v.symbol}-SPOT`).join(", ")}`, + ); } const tx = await vaultClient.initializeVault({ @@ -239,7 +243,9 @@ export async function updateVault( ); if (!spotMarket) { - throw new Error("Market not found"); + throw new Error( + "Market not found. This vault's market is no longer supported", + ); } const spotPrecision = TEN.pow(new BN(spotMarket.decimals)); @@ -370,7 +376,9 @@ export async function depositIntoVault( ); if (!spotMarket) { - throw new Error("Market not found"); + throw new Error( + "Market not found. This vaults market is no longer supported", + ); } const spotPrecision = TEN.pow(new BN(spotMarket.decimals)); @@ -544,7 +552,7 @@ export async function tradeDriftVault( if (!isOwned) { throw new Error( - "This vault is owned by someone else, so you can't trade with it", + "This vault is owned/delegated to someone else, you can't trade with it", ); } From 79fe5b0cb4eff7ae7039662164c8eccceae13b41 Mon Sep 17 00:00:00 2001 From: michaelessiet Date: Thu, 16 Jan 2025 21:33:07 +0100 Subject: [PATCH 3/6] fix: bugs noticed during testing --- .../requestUnstakeFromDriftInsuranceFund.ts | 3 +- src/actions/drift/swapSpotToken.ts | 10 +++-- .../drift/unstakeFromDriftInsuranceFund.ts | 2 +- src/tools/drift/drift.ts | 42 ++++++++++++++++++- src/vercel-ai/index.ts | 10 ++++- 5 files changed, 57 insertions(+), 10 deletions(-) diff --git a/src/actions/drift/requestUnstakeFromDriftInsuranceFund.ts b/src/actions/drift/requestUnstakeFromDriftInsuranceFund.ts index fc1b645..ca3ef0d 100644 --- a/src/actions/drift/requestUnstakeFromDriftInsuranceFund.ts +++ b/src/actions/drift/requestUnstakeFromDriftInsuranceFund.ts @@ -8,8 +8,7 @@ const requestUnstakeFromDriftInsuranceFundAction: Action = { "Request to unstake a certain amount of a token from the Drift Insurance Fund", similes: [ "request an unstake from the drift insurance fund", - "unstake an amount from the drift insurance fund", - "ask to unstake a certain amount from the drift insurance fund", + "request to unstake an amount from the drift insurance fund", ], examples: [ [ diff --git a/src/actions/drift/swapSpotToken.ts b/src/actions/drift/swapSpotToken.ts index a076cb8..1ca07e2 100644 --- a/src/actions/drift/swapSpotToken.ts +++ b/src/actions/drift/swapSpotToken.ts @@ -4,11 +4,13 @@ import { swapSpotToken } from "../../tools"; const driftSpotTokenSwapAction: Action = { name: "DRIFT_SPOT_TOKEN_SWAP_ACTION", - description: "Swap a spot token for another spot token on Drift", + description: "Swap a token for another token on Drift", similes: [ "swap a token for another token on drift", "exchange a token for another token on drift", "trade a token for another token on drift", + "swap usdc to 5 sol on drift (in this case 5 sol is the toAmount)", + "swap 5 usdt to DRIFT on drift (in this case 5 usdt is the fromAmount)", ], examples: [ [ @@ -33,18 +35,18 @@ const driftSpotTokenSwapAction: Action = { fromAmount: z .number() .positive() - .describe("Amount to swap from in normal units e.g 50 === 50 SOL") + .describe("Amount to swap from e.g 50 === 50 SOL") .optional(), toAmount: z .number() .positive() - .describe("Amount to swap to in normal units e.g 5000 === 5000 USDC") + .describe("Amount to swap to e.g 5000 === 5000 USDC") .optional(), slippage: z .number() .positive() .describe("Slippage tolerance in percentage e.g 0.5 === 0.5%") - .optional(), + .default(0.5), }), handler: async (agent, input) => { try { diff --git a/src/actions/drift/unstakeFromDriftInsuranceFund.ts b/src/actions/drift/unstakeFromDriftInsuranceFund.ts index 4d9516e..2b11056 100644 --- a/src/actions/drift/unstakeFromDriftInsuranceFund.ts +++ b/src/actions/drift/unstakeFromDriftInsuranceFund.ts @@ -5,7 +5,7 @@ import { unstakeFromDriftInsuranceFund } from "../../tools"; const unstakeFromDriftInsuranceFundAction: Action = { name: "UNSTAKE_FROM_DRIFT_INSURANCE_FUND_ACTION", description: - "Unstake requested unstake amount from the Drift Insurance fund once the cool period has elapsed", + "Unstake requested unstake token from the Drift Insurance fund once the cool period has elapsed", similes: [ "unstake from the drift insurance fund", "withdraw from the drift insurance fund", diff --git a/src/tools/drift/drift.ts b/src/tools/drift/drift.ts index 57999f5..eb3f27c 100644 --- a/src/tools/drift/drift.ts +++ b/src/tools/drift/drift.ts @@ -4,6 +4,7 @@ import { DRIFT_PROGRAM_ID, DriftClient, FastSingleTxSender, + getInsuranceFundStakeAccountPublicKey, getLimitOrderParams, getMarketOrderParams, getUserAccountPublicKeySync, @@ -514,6 +515,25 @@ export async function stakeToDriftInsuranceFund( ); } + const deriveInsuranceFundStakeAccount = + getInsuranceFundStakeAccountPublicKey( + driftClient.program.programId, + agent.wallet.publicKey, + token.marketIndex, + ); + let shouldCreateAccount = false; + + try { + await driftClient.connection.getAccountInfo( + deriveInsuranceFundStakeAccount, + ); + } catch (e) { + // @ts-expect-error - error message is a string + if (e.message.includes("Account not found")) { + shouldCreateAccount = true; + } + } + const signature = await driftClient.addInsuranceFundStake({ amount: numberToSafeBN(amount, token.precision), marketIndex: token.marketIndex, @@ -521,6 +541,7 @@ export async function stakeToDriftInsuranceFund( token.mint, agent.wallet.publicKey, ), + initializeStakeAccount: shouldCreateAccount, txParams: { computeUnitsPrice: 0.000002 * 1000000 * 1000000, }, @@ -620,6 +641,7 @@ export async function unstakeFromDriftInsuranceFund( * @param params.toSymbol symbol of the token to receive * @param params.fromAmount amount of the token to deposit * @param params.toAmount amount of the token to receive + * @param params.slippage slippage tolerance in percentage */ export async function swapSpotToken( agent: SolanaAgentKit, @@ -668,12 +690,20 @@ export async function swapSpotToken( const jupiterClient = new JupiterClient({ connection: agent.connection }); // @ts-expect-error - false undefined type conflict const fromAmount = numberToSafeBN(params.fromAmount, fromToken.precision); + const res = await ( + await fetch( + `https://quote-api.jup.ag/v6/quote?inputMint=${fromToken.mint}&outputMint=${toToken.mint}&amount=${fromAmount.toNumber()}&slippageBps=${(params.slippage ?? 0.5) * 100}&swapMode=ExactIn`, + ) + ).json(); const signature = await driftClient.swap({ amount: fromAmount, inMarketIndex: fromToken.marketIndex, outMarketIndex: toToken.marketIndex, jupiterClient: jupiterClient, - slippageBps: params.slippage ?? 100, + v6: { + quote: res, + }, + slippageBps: (params.slippage ?? 0.5) * 100, swapMode: "ExactIn", }); @@ -685,12 +715,20 @@ export async function swapSpotToken( const jupiterClient = new JupiterClient({ connection: agent.connection }); // @ts-expect-error - false undefined type conflict const toAmount = numberToSafeBN(params.toAmount, toToken.precision); + const res = await ( + await fetch( + `https://quote-api.jup.ag/v6/quote?inputMint=${fromToken.mint}&outputMint=${toToken.mint}&amount=${toAmount.toNumber()}&slippageBps=${(params.slippage ?? 0.5) * 100}&swapMode=ExactOut`, + ) + ).json(); const signature = await driftClient.swap({ amount: toAmount, inMarketIndex: toToken.marketIndex, outMarketIndex: fromToken.marketIndex, jupiterClient: jupiterClient, - slippageBps: params.slippage ?? 100, + v6: { + quote: res, + }, + slippageBps: (params.slippage ?? 0.5) * 100, swapMode: "ExactOut", }); diff --git a/src/vercel-ai/index.ts b/src/vercel-ai/index.ts index ba35643..8d1219a 100644 --- a/src/vercel-ai/index.ts +++ b/src/vercel-ai/index.ts @@ -14,7 +14,15 @@ export function createSolanaTools( tools[key] = tool({ // @ts-expect-error Value matches type however TS still shows error id: action.name, - description: action.description, + description: ` + ${action.description} + + Similes: ${action.similes.map( + (simile) => ` + ${simile} + `, + )} + `.slice(0, 1023), parameters: action.schema, execute: async (params) => await executeAction(action, solanaAgentKit, params), From 0c840d9bcbe2f0d26a9beafc6782db636bfc9a66 Mon Sep 17 00:00:00 2001 From: michaelessiet Date: Fri, 17 Jan 2025 17:12:34 +0100 Subject: [PATCH 4/6] feat: add more drift actions --- src/actions/drift/entryQuoteOfPerpTrade.ts | 65 +++++ src/actions/drift/getLendAndBorrowAPY.ts | 52 ++++ src/actions/drift/perpMarketFundingRate.ts | 61 ++++ src/actions/index.ts | 6 + src/agent/index.ts | 19 ++ .../drift/entry_quote_of_perp_trade.ts | 39 +++ src/langchain/drift/index.ts | 7 + src/langchain/drift/lend_and_borrow_apy.ts | 32 +++ .../drift/perp_market_funding_rate.ts | 36 +++ src/langchain/index.ts | 16 +- src/tools/drift/drift.ts | 260 ++++++++++++++++++ src/tools/drift/types.ts | 33 +++ 12 files changed, 625 insertions(+), 1 deletion(-) create mode 100644 src/actions/drift/entryQuoteOfPerpTrade.ts create mode 100644 src/actions/drift/getLendAndBorrowAPY.ts create mode 100644 src/actions/drift/perpMarketFundingRate.ts create mode 100644 src/langchain/drift/entry_quote_of_perp_trade.ts create mode 100644 src/langchain/drift/lend_and_borrow_apy.ts create mode 100644 src/langchain/drift/perp_market_funding_rate.ts create mode 100644 src/tools/drift/types.ts diff --git a/src/actions/drift/entryQuoteOfPerpTrade.ts b/src/actions/drift/entryQuoteOfPerpTrade.ts new file mode 100644 index 0000000..a524968 --- /dev/null +++ b/src/actions/drift/entryQuoteOfPerpTrade.ts @@ -0,0 +1,65 @@ +import { z } from "zod"; +import type { Action } from "../../types"; +import { getEntryQuoteOfPerpTrade } from "../../tools"; + +const entryQuoteOfPerpTradeAction: Action = { + name: "DRIFT_GET_ENTRY_QUOTE_OF_PERP_TRADE_ACTION", + description: "Get the entry quote of a perpetual trade on Drift", + similes: [ + "get the entry quote of a perpetual trade on drift", + "get the entry quote of a perp trade on drift", + "get the entry quote of the BTC-PERP trade on drift", + "get the entry quote of the SOL-PERP trade on drift", + "get the entry quote of a 1000 USDC long on the SOL-PERP market", + "get the entry quote of a 1000 USDC short on the SOL-PERP market", + "quote for a $1000 long on the BTC-PERP market", + ], + examples: [ + [ + { + input: { + marketSymbol: "BTC-PERP", + type: "long", + amount: 1000, + }, + output: { + status: "success", + data: { + entryPrice: 100000, + priceImpact: 0.0001, + bestPrice: 100001, + worstPrice: 99999, + baseFilled: 1000, + quoteFilled: 1000, + }, + }, + explanation: + "Get the entry quote of a $1000 long on the BTC-PERP market", + }, + ], + ], + schema: z.object({ + marketSymbol: z.string().describe("Symbol of the perpetual market"), + type: z.enum(["long", "short"]).describe("Type of trade"), + amount: z.number().positive().describe("Amount to trade"), + }), + handler: async (agent, input) => { + try { + const data = await getEntryQuoteOfPerpTrade( + input.marketSymbol, + input.amount, + input.type, + ); + + return data; + } catch (e) { + return { + status: "error", + // @ts-expect-error error is not a string + message: e.message, + }; + } + }, +}; + +export default entryQuoteOfPerpTradeAction; diff --git a/src/actions/drift/getLendAndBorrowAPY.ts b/src/actions/drift/getLendAndBorrowAPY.ts new file mode 100644 index 0000000..239b66b --- /dev/null +++ b/src/actions/drift/getLendAndBorrowAPY.ts @@ -0,0 +1,52 @@ +import { z } from "zod"; +import type { Action } from "../../types"; +import { getLendingAndBorrowAPY } from "../../tools"; + +const lendAndBorrowAPYAction: Action = { + name: "DRIFT_GET_LEND_AND_BORROW_APY_ACTION", + description: "Get the lending and borrowing APY (in %) of a token on Drift", + similes: [ + "get the lending and borrowing APY of a token on drift", + "get the lending and borrowing APY of a token on drift", + "get the lending and borrowing APY of the USDC token on drift", + "get the lending and borrowing APY of the SOL token on drift", + ], + examples: [ + [ + { + input: { + symbol: "USDC", + }, + output: { + status: "success", + data: { + lendingAPY: 10, + borrowingAPY: 12.1, + }, + }, + explanation: "Get the lending and borrowing APY of the USDC token", + }, + ], + ], + schema: z.object({ + symbol: z.string().describe("Symbol of the token"), + }), + handler: async (agent, input) => { + try { + const data = await getLendingAndBorrowAPY(agent, input.symbol); + + return { + status: "success", + data, + }; + } catch (e) { + return { + status: "error", + // @ts-expect-error error is not a string + message: e.message, + }; + } + }, +}; + +export default lendAndBorrowAPYAction; diff --git a/src/actions/drift/perpMarketFundingRate.ts b/src/actions/drift/perpMarketFundingRate.ts new file mode 100644 index 0000000..e15d477 --- /dev/null +++ b/src/actions/drift/perpMarketFundingRate.ts @@ -0,0 +1,61 @@ +import { z } from "zod"; +import type { Action } from "../../types"; +import { calculatePerpMarketFundingRate } from "../../tools"; + +const perpMarktetFundingRateAction: Action = { + name: "DRIFT_PERP_MARKET_FUNDING_RATE_ACTION", + description: "Get the funding rate of a perpetual market on Drift", + similes: [ + "get the yearly funding rate of a perpetual market on drift", + "get the funding rate of a perp market on drift", + "get the hourly funding rate of a perpetual market on drift", + "get the funding rate of the BTC-PERP market on drift", + "get the funding rate of the SOL-PERP market on drift", + ], + examples: [ + [ + { + input: { + marketSymbol: "BTC-PERP", + }, + output: { + status: "success", + data: { + longRate: 0.0001, + shortRate: 0.0002, + }, + }, + explanation: "Get the funding rate of the BTC-PERP market", + }, + ], + ], + schema: z.object({ + marketSymbol: z + .string() + .toUpperCase() + .describe("Symbol of the perpetual market"), + period: z.enum(["year", "hour"]).default("hour").optional(), + }), + handler: async (agent, input) => { + try { + const data = await calculatePerpMarketFundingRate( + agent, + input.marketSymbol, + input.period, + ); + + return { + status: "success", + data, + }; + } catch (e) { + return { + status: "error", + // @ts-expect-error error is not a string + message: e.message, + }; + } + }, +}; + +export default perpMarktetFundingRateAction; diff --git a/src/actions/index.ts b/src/actions/index.ts index 5b4324e..03a5b83 100644 --- a/src/actions/index.ts +++ b/src/actions/index.ts @@ -64,6 +64,9 @@ import stakeToDriftInsuranceFundAction from "./drift/stakeToDriftInsuranceFund"; import requestUnstakeFromDriftInsuranceFundAction from "./drift/requestUnstakeFromDriftInsuranceFund"; import unstakeFromDriftInsuranceFundAction from "./drift/unstakeFromDriftInsuranceFund"; import driftSpotTokenSwapAction from "./drift/swapSpotToken"; +import perpMarktetFundingRateAction from "./drift/perpMarketFundingRate"; +import entryQuoteOfPerpTradeAction from "./drift/entryQuoteOfPerpTrade"; +import lendAndBorrowAPYAction from "./drift/getLendAndBorrowAPY"; export const ACTIONS = { WALLET_ADDRESS_ACTION: getWalletAddressAction, @@ -134,6 +137,9 @@ export const ACTIONS = { requestUnstakeFromDriftInsuranceFundAction, UNSTAKE_FROM_DRIFT_INSURANCE_FUND_ACTION: unstakeFromDriftInsuranceFundAction, DRIFT_SPOT_TOKEN_SWAP_ACTION: driftSpotTokenSwapAction, + DRIFT_PERP_MARKET_FUNDING_RATE_ACTION: perpMarktetFundingRateAction, + DRIFT_GET_ENTRY_QUOTE_OF_PERP_TRADE_ACTION: entryQuoteOfPerpTradeAction, + DRIFT_GET_LEND_AND_BORROW_APY_ACTION: lendAndBorrowAPYAction, }; export type { Action, ActionExample, Handler } from "../types/action"; diff --git a/src/agent/index.ts b/src/agent/index.ts index 0f6c834..52add58 100644 --- a/src/agent/index.ts +++ b/src/agent/index.ts @@ -104,6 +104,9 @@ import { requestUnstakeFromDriftInsuranceFund, unstakeFromDriftInsuranceFund, swapSpotToken, + calculatePerpMarketFundingRate, + getEntryQuoteOfPerpTrade, + getLendingAndBorrowAPY, } from "../tools"; import { Config, @@ -871,4 +874,20 @@ export class SolanaAgentKit { slippage: params.slippage, }); } + async getPerpMarketFundingRate( + symbol: `${string}-PERP`, + period: "year" | "hour" = "year", + ) { + return calculatePerpMarketFundingRate(this, symbol, period); + } + async getEntryQuoteOfPerpTrade( + amount: number, + symbol: `${string}-PERP`, + action: "short" | "long", + ) { + return getEntryQuoteOfPerpTrade(symbol, amount, action); + } + async getLendAndBorrowAPY(symbol: string) { + return getLendingAndBorrowAPY(this, symbol); + } } diff --git a/src/langchain/drift/entry_quote_of_perp_trade.ts b/src/langchain/drift/entry_quote_of_perp_trade.ts new file mode 100644 index 0000000..30e04fe --- /dev/null +++ b/src/langchain/drift/entry_quote_of_perp_trade.ts @@ -0,0 +1,39 @@ +import { Tool } from "langchain/tools"; +import type { SolanaAgentKit } from "../../agent"; + +export class SolanaDriftEntryQuoteOfPerpTradeTool extends Tool { + name = "drift_entry_quote_of_perp_trade"; + description = `Get an entry quote for a perpetual trade on Drift protocol. + + Inputs (JSON string): + - amount: number, amount to trade (required) + - symbol: string, market symbol (required) + - action: "long" | "short", trade direction (required)`; + + constructor(private solanaKit: SolanaAgentKit) { + super(); + } + + protected async _call(input: string): Promise { + try { + const parsedInput = JSON.parse(input); + const quote = await this.solanaKit.getEntryQuoteOfPerpTrade( + parsedInput.amount, + parsedInput.symbol, + parsedInput.action, + ); + + return JSON.stringify({ + status: "success", + message: `Entry quote retrieved for ${parsedInput.action} ${parsedInput.amount} ${parsedInput.symbol}`, + data: quote, + }); + } catch (error: any) { + return JSON.stringify({ + status: "error", + message: error.message, + code: error.code || "ENTRY_QUOTE_OF_PERP_TRADE_ERROR", + }); + } + } +} diff --git a/src/langchain/drift/index.ts b/src/langchain/drift/index.ts index 3c9a4c8..34627d7 100644 --- a/src/langchain/drift/index.ts +++ b/src/langchain/drift/index.ts @@ -13,3 +13,10 @@ export * from "./update_vault"; export * from "./vault_info"; export * from "./withdraw_from_account"; export * from "./withdraw_from_vault"; +export * from "./perp_market_funding_rate"; +export * from "./entry_quote_of_perp_trade"; +export * from "./lend_and_borrow_apy"; +export * from "./stake_to_insurance_fund"; +export * from "./swap_spot_token"; +export * from "./unstake_from_insurance_fund"; +export * from "./request_unstake_from_insurance_fund"; diff --git a/src/langchain/drift/lend_and_borrow_apy.ts b/src/langchain/drift/lend_and_borrow_apy.ts new file mode 100644 index 0000000..25eacad --- /dev/null +++ b/src/langchain/drift/lend_and_borrow_apy.ts @@ -0,0 +1,32 @@ +import { Tool } from "langchain/tools"; +import type { SolanaAgentKit } from "../../agent"; + +export class SolanaDriftLendAndBorrowAPYTool extends Tool { + name = "drift_lend_and_borrow_apy"; + description = `Get lending and borrowing APY for a token on Drift protocol. + + Inputs (JSON string): + - symbol: string, token symbol (required)`; + + constructor(private solanaKit: SolanaAgentKit) { + super(); + } + + protected async _call(input: string): Promise { + try { + const apyInfo = await this.solanaKit.getLendAndBorrowAPY(input); + + return JSON.stringify({ + status: "success", + message: `APY information retrieved for ${input}`, + data: apyInfo, + }); + } catch (error: any) { + return JSON.stringify({ + status: "error", + message: error.message, + code: error.code || "LEND_AND_BORROW_APY_ERROR", + }); + } + } +} diff --git a/src/langchain/drift/perp_market_funding_rate.ts b/src/langchain/drift/perp_market_funding_rate.ts new file mode 100644 index 0000000..6e17810 --- /dev/null +++ b/src/langchain/drift/perp_market_funding_rate.ts @@ -0,0 +1,36 @@ +import { Tool } from "langchain/tools"; +import type { SolanaAgentKit } from "../../agent"; + +export class SolanaDriftPerpMarketFundingRateTool extends Tool { + name = "drift_perp_market_funding_rate"; + description = `Get the funding rate for a perpetual market on Drift protocol. + + Inputs (JSON string): + - symbol: string, market symbol (required) + - period: year or hour (default: hour)`; + + constructor(private solanaKit: SolanaAgentKit) { + super(); + } + + protected async _call(input: string): Promise { + try { + const parsedInput = JSON.parse(input); + const fundingRate = await this.solanaKit.getPerpMarketFundingRate( + parsedInput.symbol, + parsedInput.period, + ); + + return JSON.stringify({ + status: "success", + message: `Funding rate retrieved for ${parsedInput.symbol}`, + data: fundingRate, + }); + } catch (error: any) { + return JSON.stringify({ + status: "error", + message: error.message, + }); + } + } +} diff --git a/src/langchain/index.ts b/src/langchain/index.ts index 809c340..7fe40e3 100644 --- a/src/langchain/index.ts +++ b/src/langchain/index.ts @@ -27,7 +27,7 @@ export * from "./squads"; export * from "./helius"; export * from "./drift"; -import { SolanaAgentKit } from "../agent"; +import type { SolanaAgentKit } from "../agent"; import { SolanaBalanceTool, SolanaBalanceOtherTool, @@ -114,6 +114,13 @@ import { SolanaUpdateDriftVaultTool, SolanaWithdrawFromDriftAccountTool, SolanaWithdrawFromDriftVaultTool, + SolanaDriftLendAndBorrowAPYTool, + SolanaDriftEntryQuoteOfPerpTradeTool, + SolanaDriftPerpMarketFundingRateTool, + SolanaDriftSpotTokenSwapTool, + SolanaRequestUnstakeFromDriftInsuranceFundTool, + SolanaStakeToDriftInsuranceFundTool, + SolanaUnstakeFromDriftInsuranceFundTool, } from "./index"; export function createSolanaTools(solanaKit: SolanaAgentKit) { @@ -208,5 +215,12 @@ export function createSolanaTools(solanaKit: SolanaAgentKit) { new SolanaDriftVaultInfoTool(solanaKit), new SolanaWithdrawFromDriftAccountTool(solanaKit), new SolanaWithdrawFromDriftVaultTool(solanaKit), + new SolanaDriftSpotTokenSwapTool(solanaKit), + new SolanaStakeToDriftInsuranceFundTool(solanaKit), + new SolanaRequestUnstakeFromDriftInsuranceFundTool(solanaKit), + new SolanaUnstakeFromDriftInsuranceFundTool(solanaKit), + new SolanaDriftLendAndBorrowAPYTool(solanaKit), + new SolanaDriftEntryQuoteOfPerpTradeTool(solanaKit), + new SolanaDriftPerpMarketFundingRateTool(solanaKit), ]; } diff --git a/src/tools/drift/drift.ts b/src/tools/drift/drift.ts index eb3f27c..9f8f88d 100644 --- a/src/tools/drift/drift.ts +++ b/src/tools/drift/drift.ts @@ -1,9 +1,16 @@ import { BASE_PRECISION, + BigNum, + calculateDepositRate, + calculateEstimatedEntryPriceWithL2, + calculateInterestRate, + calculateLongShortFundingRateAndLiveTwaps, convertToNumber, DRIFT_PROGRAM_ID, DriftClient, FastSingleTxSender, + FUNDING_RATE_BUFFER_PRECISION, + FUNDING_RATE_PRECISION_EXP, getInsuranceFundStakeAccountPublicKey, getLimitOrderParams, getMarketOrderParams, @@ -12,6 +19,7 @@ import { MainnetPerpMarkets, MainnetSpotMarkets, numberToSafeBN, + PERCENTAGE_PRECISION, PositionDirection, PostOnlyParams, PRICE_PRECISION, @@ -26,6 +34,7 @@ import { getAssociatedTokenAddressSync } from "@solana/spl-token"; import { PublicKey } from "@solana/web3.js"; import { Transaction } from "@solana/web3.js"; import { ComputeBudgetProgram } from "@solana/web3.js"; +import type { RawL2Output } from "./types"; export async function initClients( agent: SolanaAgentKit, @@ -748,3 +757,254 @@ export async function swapSpotToken( throw new Error(`Failed to swap token: ${e.message}`); } } + +/** + * To get funding rate as a percentage, you need to multiply by the funding rate buffer precision + * @param rawFundingRate + */ +export function getFundingRateAsPercentage(rawFundingRate: anchor.BN) { + return BigNum.from( + rawFundingRate.mul(FUNDING_RATE_BUFFER_PRECISION), + FUNDING_RATE_PRECISION_EXP, + ).toNum(); +} + +/** + * Calculate the funding rate for a perpetual market + * @param agent + * @param marketSymbol + */ +export async function calculatePerpMarketFundingRate( + agent: SolanaAgentKit, + marketSymbol: `${string}-PERP`, + period: "year" | "hour", +) { + try { + const { driftClient, cleanUp } = await initClients(agent); + const market = driftClient.getMarketIndexAndType( + `${marketSymbol.toUpperCase()}`, + ); + + if (!market) { + throw new Error( + `This market isn't available on the Drift Protocol. Here's a list of markets that are: ${MainnetPerpMarkets.map( + (v) => v.symbol, + ).join(", ")}`, + ); + } + + const marketAccount = driftClient.getPerpMarketAccount(market.marketIndex); + + if (!marketAccount) { + throw new Error("Market account not found"); + } + + const [ + _marketTwapLive, + _oracleTwapLive, + longFundingRate, + shortFundingRate, + ] = await calculateLongShortFundingRateAndLiveTwaps( + marketAccount, + driftClient.getOracleDataForPerpMarket(market.marketIndex), + undefined, + new anchor.BN(Date.now()), + ); + + await cleanUp(); + + let longFundingRateNum = getFundingRateAsPercentage(longFundingRate); + let shortFundingRateNum = getFundingRateAsPercentage(shortFundingRate); + + if (period === "year") { + const paymentsPerYear = 24 * 365.25; + + longFundingRateNum *= paymentsPerYear; + shortFundingRateNum *= paymentsPerYear; + } + + const longsArePaying = longFundingRateNum > 0; + const shortsArePaying = !(shortFundingRateNum > 0); + + const longsAreString = longsArePaying ? "pay" : "receive"; + const shortsAreString = !shortsArePaying ? "receive" : "pay"; + + const absoluteLongFundingRateNum = Math.abs(longFundingRateNum); + const absoluteShortFundingRateNum = Math.abs(shortFundingRateNum); + + const formattedLongRatePct = absoluteLongFundingRateNum.toFixed( + period === "hour" ? 5 : 2, + ); + const formattedShortRatePct = absoluteShortFundingRateNum.toFixed( + period === "hour" ? 5 : 2, + ); + + const paymentUnit = period === "year" ? "% APR" : "%"; + + const friendlyString = `At this rate, longs would ${longsAreString} ${formattedLongRatePct} ${paymentUnit} and shorts would ${shortsAreString} ${formattedShortRatePct} ${paymentUnit} at the end of the hour.`; + + return { + longRate: longsArePaying + ? -absoluteLongFundingRateNum + : absoluteLongFundingRateNum, + shortRate: shortsArePaying + ? -absoluteShortFundingRateNum + : absoluteShortFundingRateNum, + friendlyString, + }; + } catch (e) { + throw new Error( + // @ts-expect-error e.message is a string + `Something went wrong while trying to get the market's funding rate. Here's some more context: ${e.message}`, + ); + } +} + +export async function getL2OrderBook(marketSymbol: `${string}-PERP`) { + try { + const serializedOrderbook: RawL2Output = await ( + await fetch( + `https://dlob.drift.trade/l2?marketName=${marketSymbol.toUpperCase()}&includeOracle=true`, + ) + ).json(); + + return { + asks: serializedOrderbook.asks.map((ask) => ({ + price: new anchor.BN(ask.price), + size: new anchor.BN(ask.size), + sources: Object.entries(ask.sources).reduce((previous, [key, val]) => { + return { + ...(previous ?? {}), + [key]: new anchor.BN(val), + }; + }, {}), + })), + bids: serializedOrderbook.bids.map((bid) => ({ + price: new anchor.BN(bid.price), + size: new anchor.BN(bid.size), + sources: Object.entries(bid.sources).reduce((previous, [key, val]) => { + return { + ...(previous ?? {}), + [key]: new anchor.BN(val), + }; + }, {}), + })), + oracleData: { + price: serializedOrderbook.oracleData.price + ? new anchor.BN(serializedOrderbook.oracleData.price) + : undefined, + slot: serializedOrderbook.oracleData.slot + ? new anchor.BN(serializedOrderbook.oracleData.slot) + : undefined, + confidence: serializedOrderbook.oracleData.confidence + ? new anchor.BN(serializedOrderbook.oracleData.confidence) + : undefined, + hasSufficientNumberOfDataPoints: + serializedOrderbook.oracleData.hasSufficientNumberOfDataPoints, + twap: serializedOrderbook.oracleData.twap + ? new anchor.BN(serializedOrderbook.oracleData.twap) + : undefined, + twapConfidence: serializedOrderbook.oracleData.twapConfidence + ? new anchor.BN(serializedOrderbook.oracleData.twapConfidence) + : undefined, + maxPrice: serializedOrderbook.oracleData.maxPrice + ? new anchor.BN(serializedOrderbook.oracleData.maxPrice) + : undefined, + }, + slot: serializedOrderbook.slot, + }; + } catch (e) { + throw new Error(); + } +} + +/** + * Get the estimated entry quote of a perp trade + * @param agent + * @param marketSymbol + * @param amount + * @param type + */ +export async function getEntryQuoteOfPerpTrade( + marketSymbol: `${string}-PERP`, + amount: number, + type: "long" | "short", +) { + try { + const l2OrderBookData = await getL2OrderBook(marketSymbol); + const estimatedEntryPriceData = calculateEstimatedEntryPriceWithL2( + "quote", + numberToSafeBN(amount, BASE_PRECISION), + type === "long" ? PositionDirection.LONG : PositionDirection.SHORT, + BASE_PRECISION, + // @ts-expect-error - false type conflict + l2OrderBookData, + ); + + return { + entryPrice: convertToNumber( + estimatedEntryPriceData.entryPrice, + QUOTE_PRECISION, + ), + priceImpact: convertToNumber( + estimatedEntryPriceData.priceImpact, + QUOTE_PRECISION, + ), + bestPrice: convertToNumber( + estimatedEntryPriceData.bestPrice, + QUOTE_PRECISION, + ), + worstPrice: convertToNumber( + estimatedEntryPriceData.worstPrice, + QUOTE_PRECISION, + ), + }; + } catch (e) { + // @ts-expect-error - error message is a string + throw new Error(`Failed to get entry quote: ${e.message}`); + } +} + +/** + * Get the APY for lending and borrowing a specific token on drift protocol + * @param agent + * @param symbol + */ +export async function getLendingAndBorrowAPY( + agent: SolanaAgentKit, + symbol: string, +) { + try { + const { driftClient, cleanUp } = await initClients(agent); + const token = MainnetSpotMarkets.find( + (v) => v.symbol === symbol.toUpperCase(), + ); + + if (!token) { + throw new Error( + `Token with symbol ${symbol} not found. Here's a list of available spot markets: ${MainnetSpotMarkets.map( + (v) => v.symbol, + ).join(", ")}`, + ); + } + + const marketAccount = driftClient.getSpotMarketAccount(token.marketIndex); + + if (!marketAccount) { + throw new Error("Market account not found"); + } + + const lendAPY = calculateDepositRate(marketAccount); + const borrowAPY = calculateInterestRate(marketAccount); + + await cleanUp(); + + return { + lendingAPY: convertToNumber(lendAPY, PERCENTAGE_PRECISION) * 100, // convert to percentage + borrowAPY: convertToNumber(borrowAPY, PERCENTAGE_PRECISION) * 100, // convert to percentage + }; + } catch (e) { + // @ts-expect-error - error message is a string + throw new Error(`Failed to get APYs: ${e.message}`); + } +} diff --git a/src/tools/drift/types.ts b/src/tools/drift/types.ts new file mode 100644 index 0000000..4784109 --- /dev/null +++ b/src/tools/drift/types.ts @@ -0,0 +1,33 @@ +import type { L2OrderBook, MarketType, OraclePriceData } from "@drift-labs/sdk"; + +export type L2WithOracle = L2OrderBook & { oracleData: OraclePriceData }; + +export type RawL2Output = { + marketIndex: number; + marketType: MarketType; + marketName: string; + asks: { + price: string; + size: string; + sources: { + [key: string]: string; + }; + }[]; + bids: { + price: string; + size: string; + sources: { + [key: string]: string; + }; + }[]; + oracleData: { + price: string; + slot: string; + confidence: string; + hasSufficientNumberOfDataPoints: boolean; + twap?: string; + twapConfidence?: string; + maxPrice?: string; + }; + slot?: number; +}; From 67fa8217a7ab5cd780f67d1d7692157c40440d75 Mon Sep 17 00:00:00 2001 From: michaelessiet Date: Fri, 17 Jan 2025 17:15:05 +0100 Subject: [PATCH 5/6] fix: revert all changes in vercel ai test file --- test/agent_sdks/vercel_ai.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/agent_sdks/vercel_ai.ts b/test/agent_sdks/vercel_ai.ts index bf1585e..130528b 100644 --- a/test/agent_sdks/vercel_ai.ts +++ b/test/agent_sdks/vercel_ai.ts @@ -194,4 +194,4 @@ if (require.main === module) { console.error("Fatal error:", error); process.exit(1); }); -} +} \ No newline at end of file From e4fa501c52456885087d448e530c31f3d6131ecf Mon Sep 17 00:00:00 2001 From: michaelessiet Date: Fri, 17 Jan 2025 17:23:15 +0100 Subject: [PATCH 6/6] chore: add minimum compute price to constants --- src/constants/index.ts | 6 ++++++ src/tools/drift/drift.ts | 15 ++++++++------- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/constants/index.ts b/src/constants/index.ts index 69965bf..5e261b4 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -33,3 +33,9 @@ export const DEFAULT_OPTIONS = { export const JUP_API = "https://quote-api.jup.ag/v6"; export const JUP_REFERRAL_ADDRESS = "REFER4ZgmyYx9c6He5XfaTMiGfdLwRnkV4RPp9t9iF3"; + +/** + * Minimum compute price required to carry out complex transactions on the Drift protocol + */ +export const MINIMUM_COMPUTE_PRICE_FOR_COMPLEX_ACTIONS = + 0.000003 * 1000000 * 1000000; diff --git a/src/tools/drift/drift.ts b/src/tools/drift/drift.ts index 9f8f88d..6c6ec1b 100644 --- a/src/tools/drift/drift.ts +++ b/src/tools/drift/drift.ts @@ -35,6 +35,7 @@ import { PublicKey } from "@solana/web3.js"; import { Transaction } from "@solana/web3.js"; import { ComputeBudgetProgram } from "@solana/web3.js"; import type { RawL2Output } from "./types"; +import { MINIMUM_COMPUTE_PRICE_FOR_COMPLEX_ACTIONS } from "../../constants"; export async function initClients( agent: SolanaAgentKit, @@ -68,7 +69,7 @@ export async function initClients( activeSubAccountId: params?.activeSubAccountId, subAccountIds: params?.subAccountIds, txParams: { - computeUnitsPrice: 0.000001 * 1000000 * 1000000, + computeUnitsPrice: MINIMUM_COMPUTE_PRICE_FOR_COMPLEX_ACTIONS, }, txSender: new FastSingleTxSender({ connection: agent.connection, @@ -212,7 +213,7 @@ export async function depositToDriftUserAccount( const tx = new Transaction().add(...depInstruction).add( ComputeBudgetProgram.setComputeUnitPrice({ - microLamports: 0.000001 * 1000000 * 1000000, + microLamports: MINIMUM_COMPUTE_PRICE_FOR_COMPLEX_ACTIONS, }), ); tx.recentBlockhash = latestBlockhash.blockhash; @@ -277,7 +278,7 @@ export async function withdrawFromDriftUserAccount( const tx = new Transaction().add(...withdrawInstruction).add( ComputeBudgetProgram.setComputeUnitPrice({ - microLamports: 0.000003 * 1000000 * 1000000, + microLamports: MINIMUM_COMPUTE_PRICE_FOR_COMPLEX_ACTIONS, }), ); tx.recentBlockhash = latestBlockhash.blockhash; @@ -384,7 +385,7 @@ export async function driftPerpTrade( marketIndex: market.marketIndex, }), { - computeUnitsPrice: 0.000001 * 1000000 * 1000000, + computeUnitsPrice: MINIMUM_COMPUTE_PRICE_FOR_COMPLEX_ACTIONS, }, ); } @@ -552,7 +553,7 @@ export async function stakeToDriftInsuranceFund( ), initializeStakeAccount: shouldCreateAccount, txParams: { - computeUnitsPrice: 0.000002 * 1000000 * 1000000, + computeUnitsPrice: MINIMUM_COMPUTE_PRICE_FOR_COMPLEX_ACTIONS, }, }); @@ -592,7 +593,7 @@ export async function requestUnstakeFromDriftInsuranceFund( const signature = await driftClient.requestRemoveInsuranceFundStake( token.marketIndex, numberToSafeBN(amount, token.precision), - { computeUnitsPrice: 0.000002 * 1000000 * 1000000 }, + { computeUnitsPrice: MINIMUM_COMPUTE_PRICE_FOR_COMPLEX_ACTIONS }, ); await cleanUp(); @@ -630,7 +631,7 @@ export async function unstakeFromDriftInsuranceFund( token.marketIndex, getAssociatedTokenAddressSync(token.mint, agent.wallet.publicKey), { - computeUnitsPrice: 0.000002 * 1000000 * 1000000, + computeUnitsPrice: MINIMUM_COMPUTE_PRICE_FOR_COMPLEX_ACTIONS, }, );