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/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/drift/requestUnstakeFromDriftInsuranceFund.ts b/src/actions/drift/requestUnstakeFromDriftInsuranceFund.ts new file mode 100644 index 0000000..ca3ef0d --- /dev/null +++ b/src/actions/drift/requestUnstakeFromDriftInsuranceFund.ts @@ -0,0 +1,61 @@ +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", + "request to unstake an 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..1ca07e2 --- /dev/null +++ b/src/actions/drift/swapSpotToken.ts @@ -0,0 +1,78 @@ +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 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: [ + [ + { + 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 e.g 50 === 50 SOL") + .optional(), + toAmount: z + .number() + .positive() + .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%") + .default(0.5), + }), + 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..2b11056 --- /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 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", + "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 1dc90d6..5fca366 100644 --- a/src/actions/index.ts +++ b/src/actions/index.ts @@ -61,6 +61,14 @@ 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"; +import perpMarktetFundingRateAction from "./drift/perpMarketFundingRate"; +import entryQuoteOfPerpTradeAction from "./drift/entryQuoteOfPerpTrade"; +import lendAndBorrowAPYAction from "./drift/getLendAndBorrowAPY"; import getVoltrPositionValuesAction from "./voltr/getPositionValues"; import depositVoltrStrategyAction from "./voltr/depositStrategy"; import withdrawVoltrStrategyAction from "./voltr/withdrawStrategy"; @@ -130,6 +138,15 @@ 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, + DRIFT_PERP_MARKET_FUNDING_RATE_ACTION: perpMarktetFundingRateAction, + DRIFT_GET_ENTRY_QUOTE_OF_PERP_TRADE_ACTION: entryQuoteOfPerpTradeAction, + DRIFT_GET_LEND_AND_BORROW_APY_ACTION: lendAndBorrowAPYAction, GET_VOLTR_POSITION_VALUES_ACTION: getVoltrPositionValuesAction, DEPOSIT_VOLTR_STRATEGY_ACTION: depositVoltrStrategyAction, WITHDRAW_VOLTR_STRATEGY_ACTION: withdrawVoltrStrategyAction, diff --git a/src/agent/index.ts b/src/agent/index.ts index a3c3478..8d82a9b 100644 --- a/src/agent/index.ts +++ b/src/agent/index.ts @@ -100,6 +100,15 @@ import { withdrawFromDriftVault, updateVaultDelegate, get_token_balance, + getAvailableDriftSpotMarkets, + getAvailableDriftPerpMarkets, + stakeToDriftInsuranceFund, + requestUnstakeFromDriftInsuranceFund, + unstakeFromDriftInsuranceFund, + swapSpotToken, + calculatePerpMarketFundingRate, + getEntryQuoteOfPerpTrade, + getLendingAndBorrowAPY, voltrGetPositionValues, voltrDepositStrategy, voltrWithdrawStrategy, @@ -759,6 +768,7 @@ export class SolanaAgentKit { async createDriftUserAccount(depositAmount: number, depositSymbol: string) { return await createDriftUserAccount(this, depositAmount, depositSymbol); } + async createDriftVault(params: { name: string; marketName: `${string}-${string}`; @@ -772,6 +782,7 @@ export class SolanaAgentKit { }) { return await createVault(this, params); } + async depositIntoDriftVault(amount: number, vault: string) { return await depositIntoVault(this, amount, vault); } @@ -854,6 +865,66 @@ export class SolanaAgentKit { 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, + }); + } + 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); + async voltrDepositStrategy( depositAmount: BN, vault: PublicKey, 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/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/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/langchain/index.ts b/src/langchain/index.ts index 6848b5e..788a834 100644 --- a/src/langchain/index.ts +++ b/src/langchain/index.ts @@ -28,7 +28,7 @@ export * from "./helius"; export * from "./drift"; export * from "./voltr"; -import { SolanaAgentKit } from "../agent"; +import type { SolanaAgentKit } from "../agent"; import { SolanaBalanceTool, SolanaBalanceOtherTool, @@ -117,6 +117,13 @@ import { SolanaUpdateDriftVaultTool, SolanaWithdrawFromDriftAccountTool, SolanaWithdrawFromDriftVaultTool, + SolanaDriftLendAndBorrowAPYTool, + SolanaDriftEntryQuoteOfPerpTradeTool, + SolanaDriftPerpMarketFundingRateTool, + SolanaDriftSpotTokenSwapTool, + SolanaRequestUnstakeFromDriftInsuranceFundTool, + SolanaStakeToDriftInsuranceFundTool, + SolanaUnstakeFromDriftInsuranceFundTool, SolanaVoltrGetPositionValues, SolanaVoltrDepositStrategy, SolanaVoltrWithdrawStrategy, @@ -216,6 +223,13 @@ 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), new SolanaVoltrGetPositionValues(solanaKit), new SolanaVoltrDepositStrategy(solanaKit), new SolanaVoltrWithdrawStrategy(solanaKit), diff --git a/src/tools/drift/drift.ts b/src/tools/drift/drift.ts index 97105a4..6c6ec1b 100644 --- a/src/tools/drift/drift.ts +++ b/src/tools/drift/drift.ts @@ -1,14 +1,25 @@ 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, getUserAccountPublicKeySync, + JupiterClient, + MainnetPerpMarkets, MainnetSpotMarkets, numberToSafeBN, + PERCENTAGE_PRECISION, PositionDirection, PostOnlyParams, PRICE_PRECISION, @@ -23,6 +34,8 @@ 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"; +import { MINIMUM_COMPUTE_PRICE_FOR_COMPLEX_ACTIONS } from "../../constants"; export async function initClients( agent: SolanaAgentKit, @@ -56,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, @@ -115,7 +128,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 +187,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) { @@ -193,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; @@ -237,7 +257,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); @@ -254,7 +278,7 @@ export async function withdrawFromDriftUserAccount( const tx = new Transaction().add(...withdrawInstruction).add( ComputeBudgetProgram.setComputeUnitPrice({ - microLamports: 0.000001 * 1000000 * 1000000, + microLamports: MINIMUM_COMPUTE_PRICE_FOR_COMPLEX_ACTIONS, }), ); tx.recentBlockhash = latestBlockhash.blockhash; @@ -313,7 +337,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( @@ -357,7 +385,7 @@ export async function driftPerpTrade( marketIndex: market.marketIndex, }), { - computeUnitsPrice: 0.000001 * 1000000 * 1000000, + computeUnitsPrice: MINIMUM_COMPUTE_PRICE_FOR_COMPLEX_ACTIONS, }, ); } @@ -388,9 +416,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, @@ -433,10 +463,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, @@ -446,8 +475,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, @@ -458,3 +485,527 @@ 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 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, + collateralAccountPublicKey: getAssociatedTokenAddressSync( + token.mint, + agent.wallet.publicKey, + ), + initializeStakeAccount: shouldCreateAccount, + txParams: { + computeUnitsPrice: MINIMUM_COMPUTE_PRICE_FOR_COMPLEX_ACTIONS, + }, + }); + + 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: MINIMUM_COMPUTE_PRICE_FOR_COMPLEX_ACTIONS }, + ); + + 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: MINIMUM_COMPUTE_PRICE_FOR_COMPLEX_ACTIONS, + }, + ); + + 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 + * @param params.slippage slippage tolerance in percentage + */ +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 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, + v6: { + quote: res, + }, + slippageBps: (params.slippage ?? 0.5) * 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 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, + v6: { + quote: res, + }, + slippageBps: (params.slippage ?? 0.5) * 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}`); + } +} + +/** + * 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/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", ); } 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; +}; 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), 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