diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c862186..6b4d5a2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -14,6 +14,9 @@ jobs: with: version: 9.4.0 + - name: Install node-gyp prerequisites + run: sudo apt update && sudo apt install -y build-essential python3 pkg-config libudev-dev libusb-1.0-0-dev + - uses: actions/setup-node@v4 with: node-version: "23" @@ -21,7 +24,7 @@ jobs: - name: Install dependencies run: pnpm install -r --no-frozen-lockfile - + - name: Run lint and fix run: pnpm run lint:fix diff --git a/src/actions/index.ts b/src/actions/index.ts index d4b73c9..84774cb 100644 --- a/src/actions/index.ts +++ b/src/actions/index.ts @@ -1,3 +1,4 @@ +import tokenBalancesAction from "./tokenBalances"; import deployTokenAction from "./metaplex/deployToken"; import balanceAction from "./solana/balance"; import transferAction from "./solana/transfer"; @@ -64,6 +65,7 @@ import withdrawVoltrStrategyAction from "./voltr/withdrawStrategy"; export const ACTIONS = { WALLET_ADDRESS_ACTION: getWalletAddressAction, + TOKEN_BALANCES_ACTION: tokenBalancesAction, DEPLOY_TOKEN_ACTION: deployTokenAction, BALANCE_ACTION: balanceAction, TRANSFER_ACTION: transferAction, diff --git a/src/actions/solana/balance.ts b/src/actions/solana/balance.ts index 1009033..b343fef 100644 --- a/src/actions/solana/balance.ts +++ b/src/actions/solana/balance.ts @@ -1,6 +1,6 @@ import { PublicKey } from "@solana/web3.js"; -import { Action } from "../../types/action"; -import { SolanaAgentKit } from "../../agent"; +import type { Action } from "../../types/action"; +import type { SolanaAgentKit } from "../../agent"; import { z } from "zod"; import { get_balance } from "../../tools"; diff --git a/src/actions/tokenBalances.ts b/src/actions/tokenBalances.ts new file mode 100644 index 0000000..f21ccbf --- /dev/null +++ b/src/actions/tokenBalances.ts @@ -0,0 +1,80 @@ +import { PublicKey } from "@solana/web3.js"; +import type { Action } from "../types/action"; +import type { SolanaAgentKit } from "../agent"; +import { z } from "zod"; +import { get_token_balance } from "../tools"; + +const tokenBalancesAction: Action = { + name: "TOKEN_BALANCE_ACTION", + similes: [ + "check token balances", + "get wallet token balances", + "view token balances", + "show token balances", + "check token balance", + ], + description: `Get the token balances of a Solana wallet. + If you want to get the balance of your wallet, you don't need to provide the wallet address.`, + examples: [ + [ + { + input: {}, + output: { + status: "success", + balance: { + sol: 100, + tokens: [ + { + tokenAddress: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + name: "USD Coin", + symbol: "USDC", + balance: 100, + decimals: 9, + }, + ], + }, + }, + explanation: "Get token balances of the wallet", + }, + ], + [ + { + input: { + walletAddress: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + }, + output: { + status: "success", + balance: { + sol: 100, + tokens: [ + { + tokenAddress: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + name: "USD Coin", + symbol: "USDC", + balance: 100, + decimals: 9, + }, + ], + }, + }, + explanation: "Get address token balance", + }, + ], + ], + schema: z.object({ + walletAddress: z.string().optional(), + }), + handler: async (agent: SolanaAgentKit, input) => { + const balance = await get_token_balance( + agent, + input.tokenAddress && new PublicKey(input.tokenAddress), + ); + + return { + status: "success", + balance: balance, + }; + }, +}; + +export default tokenBalancesAction; diff --git a/src/agent/index.ts b/src/agent/index.ts index f278d19..fca9d13 100644 --- a/src/agent/index.ts +++ b/src/agent/index.ts @@ -97,6 +97,7 @@ import { withdrawFromDriftUserAccount, withdrawFromDriftVault, updateVaultDelegate, + get_token_balance, voltrGetPositionValues, voltrDepositStrategy, voltrWithdrawStrategy, @@ -192,6 +193,19 @@ export class SolanaAgentKit { return get_balance(this, token_address); } + async getTokenBalances(wallet_address?: PublicKey): Promise<{ + sol: number; + tokens: Array<{ + tokenAddress: string; + name: string; + symbol: string; + balance: number; + decimals: number; + }>; + }> { + return get_token_balance(this, wallet_address); + } + async getBalanceOther( walletAddress: PublicKey, tokenAddress?: PublicKey, diff --git a/src/langchain/drift/create_user_account.ts b/src/langchain/drift/create_user_account.ts new file mode 100644 index 0000000..36f8ecf --- /dev/null +++ b/src/langchain/drift/create_user_account.ts @@ -0,0 +1,38 @@ +import { Tool } from "langchain/tools"; +import { SolanaAgentKit } from "../../agent"; + +export class SolanaCreateDriftUserAccountTool extends Tool { + name = "create_drift_user_account"; + description = `Create a new user account with a deposit on Drift protocol. + + Inputs (JSON string): + - amount: number, amount of the token to deposit (required) + - symbol: string, symbol of the token to deposit (required)`; + + constructor(private solanaKit: SolanaAgentKit) { + super(); + } + + protected async _call(input: string): Promise { + try { + const parsedInput = JSON.parse(input); + const res = await this.solanaKit.createDriftUserAccount( + parsedInput.amount, + parsedInput.symbol, + ); + + return JSON.stringify({ + status: "success", + message: `User account created with ${parsedInput.amount} ${parsedInput.symbol} successfully deposited`, + account: res.account, + signature: res.txSignature, + }); + } catch (error: any) { + return JSON.stringify({ + status: "error", + message: error.message, + code: error.code || "CREATE_DRIFT_USER_ACCOUNT_ERROR", + }); + } + } +} diff --git a/src/langchain/drift/create_vault.ts b/src/langchain/drift/create_vault.ts new file mode 100644 index 0000000..5ff62f2 --- /dev/null +++ b/src/langchain/drift/create_vault.ts @@ -0,0 +1,42 @@ +import { Tool } from "langchain/tools"; +import { SolanaAgentKit } from "../../agent"; + +export class SolanaCreateDriftVaultTool extends Tool { + name = "create_drift_vault"; + description = `Create a new drift vault delegating the agents address as the owner. + + Inputs (JSON string): + - name: string, unique vault name (min 5 chars) + - marketName: string, market name in TOKEN-SPOT format + - redeemPeriod: number, days to wait before funds can be redeemed (min 1) + - maxTokens: number, maximum tokens vault can accommodate (min 100) + - minDepositAmount: number, minimum deposit amount + - managementFee: number, fee percentage for managing funds (max 20) + - profitShare: number, profit sharing percentage (max 90, default 5) + - hurdleRate: number, optional hurdle rate + - permissioned: boolean, whether vault has whitelist`; + + constructor(private solanaKit: SolanaAgentKit) { + super(); + } + + protected async _call(input: string): Promise { + try { + const parsedInput = JSON.parse(input); + const tx = await this.solanaKit.createDriftVault(parsedInput); + + return JSON.stringify({ + status: "success", + message: "Drift vault created successfully", + vaultName: parsedInput.name, + signature: tx, + }); + } catch (error: any) { + return JSON.stringify({ + status: "error", + message: error.message, + code: error.code || "CREATE_DRIFT_VAULT_ERROR", + }); + } + } +} diff --git a/src/langchain/drift/deposit_into_vault.ts b/src/langchain/drift/deposit_into_vault.ts new file mode 100644 index 0000000..689fba1 --- /dev/null +++ b/src/langchain/drift/deposit_into_vault.ts @@ -0,0 +1,37 @@ +import { Tool } from "langchain/tools"; +import { SolanaAgentKit } from "../../agent"; + +export class SolanaDepositIntoDriftVaultTool extends Tool { + name = "deposit_into_drift_vault"; + description = `Deposit funds into an existing drift vault. + + Inputs (JSON string): + - vaultAddress: string, address of the vault (required) + - amount: number, amount to deposit (required)`; + + constructor(private solanaKit: SolanaAgentKit) { + super(); + } + + protected async _call(input: string): Promise { + try { + const parsedInput = JSON.parse(input); + const tx = await this.solanaKit.depositIntoDriftVault( + parsedInput.amount, + parsedInput.vaultAddress, + ); + + return JSON.stringify({ + status: "success", + message: "Funds deposited successfully", + signature: tx, + }); + } catch (error: any) { + return JSON.stringify({ + status: "error", + message: error.message, + code: error.code || "DEPOSIT_INTO_VAULT_ERROR", + }); + } + } +} diff --git a/src/langchain/drift/deposit_to_user_account.ts b/src/langchain/drift/deposit_to_user_account.ts new file mode 100644 index 0000000..a9ecb43 --- /dev/null +++ b/src/langchain/drift/deposit_to_user_account.ts @@ -0,0 +1,39 @@ +import { Tool } from "langchain/tools"; +import { SolanaAgentKit } from "../../agent"; + +export class SolanaDepositToDriftUserAccountTool extends Tool { + name = "deposit_to_drift_user_account"; + description = `Deposit funds into your drift user account. + + Inputs (JSON string): + - amount: number, amount to deposit (required) + - symbol: string, token symbol (required) + - repay: boolean, whether to repay borrowed funds (optional, default: false)`; + + constructor(private solanaKit: SolanaAgentKit) { + super(); + } + + protected async _call(input: string): Promise { + try { + const parsedInput = JSON.parse(input); + const tx = await this.solanaKit.depositToDriftUserAccount( + parsedInput.amount, + parsedInput.symbol, + parsedInput.repay, + ); + + return JSON.stringify({ + status: "success", + message: "Funds deposited successfully", + signature: tx, + }); + } catch (error: any) { + return JSON.stringify({ + status: "error", + message: error.message, + code: error.code || "DEPOSIT_TO_DRIFT_ACCOUNT_ERROR", + }); + } + } +} diff --git a/src/langchain/drift/derive_vault_address.ts b/src/langchain/drift/derive_vault_address.ts new file mode 100644 index 0000000..bbd3b8a --- /dev/null +++ b/src/langchain/drift/derive_vault_address.ts @@ -0,0 +1,32 @@ +import { Tool } from "langchain/tools"; +import { SolanaAgentKit } from "../../agent"; + +export class SolanaDeriveVaultAddressTool extends Tool { + name = "derive_drift_vault_address"; + description = `Derive a drift vault address from the vault's name. + + Inputs (JSON string): + - name: string, name of the vault to derive the address of (required)`; + + constructor(private solanaKit: SolanaAgentKit) { + super(); + } + + protected async _call(input: string): Promise { + try { + const address = await this.solanaKit.deriveDriftVaultAddress(input); + + return JSON.stringify({ + status: "success", + message: "Vault address derived successfully", + address, + }); + } catch (error: any) { + return JSON.stringify({ + status: "error", + message: error.message, + code: error.code || "DERIVE_VAULT_ADDRESS_ERROR", + }); + } + } +} diff --git a/src/langchain/drift/does_user_have_drift_account.ts b/src/langchain/drift/does_user_have_drift_account.ts new file mode 100644 index 0000000..7673b57 --- /dev/null +++ b/src/langchain/drift/does_user_have_drift_account.ts @@ -0,0 +1,38 @@ +import { Tool } from "langchain/tools"; +import { SolanaAgentKit } from "../../agent"; + +export class SolanaCheckDriftAccountTool extends Tool { + name = "does_user_have_drift_account"; + description = `Check if a user has a Drift account. + + Inputs: No inputs required - checks the current user's account`; + + constructor(private solanaKit: SolanaAgentKit) { + super(); + } + + protected async _call(_input: string): Promise { + try { + const res = await this.solanaKit.doesUserHaveDriftAccount(); + + if (!res.hasAccount) { + return JSON.stringify({ + status: "error", + message: "You do not have a Drift account", + }); + } + + return JSON.stringify({ + status: "success", + message: "Nice! You have a Drift account", + account: res.account, + }); + } catch (error: any) { + return JSON.stringify({ + status: "error", + message: error.message, + code: error.code || "CHECK_DRIFT_ACCOUNT_ERROR", + }); + } + } +} diff --git a/src/langchain/drift/drift_user_account_info.ts b/src/langchain/drift/drift_user_account_info.ts new file mode 100644 index 0000000..191577a --- /dev/null +++ b/src/langchain/drift/drift_user_account_info.ts @@ -0,0 +1,29 @@ +import { Tool } from "langchain/tools"; +import { SolanaAgentKit } from "../../agent"; + +export class SolanaDriftUserAccountInfoTool extends Tool { + name = "drift_user_account_info"; + description = `Get information about your drift account. + + Inputs: No inputs required - retrieves current user's account info`; + + constructor(private solanaKit: SolanaAgentKit) { + super(); + } + + protected async _call(_input: string): Promise { + try { + const accountInfo = await this.solanaKit.driftUserAccountInfo(); + return JSON.stringify({ + status: "success", + data: accountInfo, + }); + } catch (error: any) { + return JSON.stringify({ + status: "error", + message: error.message, + code: error.code || "DRIFT_ACCOUNT_INFO_ERROR", + }); + } + } +} diff --git a/src/langchain/drift/index.ts b/src/langchain/drift/index.ts new file mode 100644 index 0000000..3c9a4c8 --- /dev/null +++ b/src/langchain/drift/index.ts @@ -0,0 +1,15 @@ +export * from "./create_user_account"; +export * from "./create_vault"; +export * from "./deposit_into_vault"; +export * from "./deposit_to_user_account"; +export * from "./derive_vault_address"; +export * from "./does_user_have_drift_account"; +export * from "./drift_user_account_info"; +export * from "./request_withdrawal"; +export * from "./trade_delegated_vault"; +export * from "./trade_perp_account"; +export * from "./update_drift_vault_delegate"; +export * from "./update_vault"; +export * from "./vault_info"; +export * from "./withdraw_from_account"; +export * from "./withdraw_from_vault"; diff --git a/src/langchain/drift/request_withdrawal.ts b/src/langchain/drift/request_withdrawal.ts new file mode 100644 index 0000000..507a31e --- /dev/null +++ b/src/langchain/drift/request_withdrawal.ts @@ -0,0 +1,37 @@ +import { Tool } from "langchain/tools"; +import { SolanaAgentKit } from "../../agent"; + +export class SolanaRequestDriftWithdrawalTool extends Tool { + name = "request_withdrawal_from_drift_vault"; + description = `Request a withdrawal from an existing drift vault. + + Inputs (JSON string): + - vaultAddress: string, vault address (required) + - amount: number, amount of shares to withdraw (required)`; + + constructor(private solanaKit: SolanaAgentKit) { + super(); + } + + protected async _call(input: string): Promise { + try { + const parsedInput = JSON.parse(input); + const tx = await this.solanaKit.requestWithdrawalFromDriftVault( + parsedInput.amount, + parsedInput.vaultAddress, + ); + + return JSON.stringify({ + status: "success", + message: "Withdrawal request successful", + signature: tx, + }); + } catch (error: any) { + return JSON.stringify({ + status: "error", + message: error.message, + code: error.code || "REQUEST_DRIFT_WITHDRAWAL_ERROR", + }); + } + } +} diff --git a/src/langchain/drift/trade_delegated_vault.ts b/src/langchain/drift/trade_delegated_vault.ts new file mode 100644 index 0000000..131efd8 --- /dev/null +++ b/src/langchain/drift/trade_delegated_vault.ts @@ -0,0 +1,49 @@ +import { Tool } from "langchain/tools"; +import { SolanaAgentKit } from "../../agent"; + +export class SolanaTradeDelegatedDriftVaultTool extends Tool { + name = "trade_delegated_drift_vault"; + description = `Carry out trades in a Drift vault. + + Inputs (JSON string): + - vaultAddress: string, address of the Drift vault + - amount: number, amount to trade + - symbol: string, symbol of the token to trade + - action: "long" | "short", trade direction + - type: "market" | "limit", order type + - price: number, optional limit price`; + + constructor(private solanaKit: SolanaAgentKit) { + super(); + } + + protected async _call(input: string): Promise { + try { + const parsedInput = JSON.parse(input); + const tx = await this.solanaKit.tradeUsingDelegatedDriftVault( + parsedInput.vaultAddress, + parsedInput.amount, + parsedInput.symbol, + parsedInput.action, + parsedInput.type, + parsedInput.price, + ); + + return JSON.stringify({ + status: "success", + message: + parsedInput.type === "limit" + ? "Order placed successfully" + : "Trade successful", + transactionId: tx, + ...parsedInput, + }); + } catch (error: any) { + return JSON.stringify({ + status: "error", + message: error.message, + code: error.code || "TRADE_DRIFT_VAULT_ERROR", + }); + } + } +} diff --git a/src/langchain/drift/trade_perp_account.ts b/src/langchain/drift/trade_perp_account.ts new file mode 100644 index 0000000..29f4ac1 --- /dev/null +++ b/src/langchain/drift/trade_perp_account.ts @@ -0,0 +1,42 @@ +import { Tool } from "langchain/tools"; +import { SolanaAgentKit } from "../../agent"; + +export class SolanaTradeDriftPerpAccountTool extends Tool { + name = "trade_drift_perp_account"; + description = `Trade a perpetual account on Drift protocol. + + Inputs (JSON string): + - amount: number, amount to trade (required) + - symbol: string, token symbol (required) + - action: "long" | "short", trade direction (required) + - type: "market" | "limit", order type (required) + - price: number, required for limit orders`; + + constructor(private solanaKit: SolanaAgentKit) { + super(); + } + + protected async _call(input: string): Promise { + try { + const parsedInput = JSON.parse(input); + const signature = await this.solanaKit.tradeUsingDriftPerpAccount( + parsedInput.amount, + parsedInput.symbol, + parsedInput.action, + parsedInput.type, + parsedInput.price, + ); + + return JSON.stringify({ + status: "success", + signature, + }); + } catch (error: any) { + return JSON.stringify({ + status: "error", + message: error.message, + code: error.code || "TRADE_PERP_ACCOUNT_ERROR", + }); + } + } +} diff --git a/src/langchain/drift/update_drift_vault_delegate.ts b/src/langchain/drift/update_drift_vault_delegate.ts new file mode 100644 index 0000000..8608593 --- /dev/null +++ b/src/langchain/drift/update_drift_vault_delegate.ts @@ -0,0 +1,37 @@ +import { Tool } from "langchain/tools"; +import { SolanaAgentKit } from "../../agent"; + +export class SolanaUpdateDriftVaultDelegateTool extends Tool { + name = "update_drift_vault_delegate"; + description = `Update the delegate of a drift vault. + + Inputs (JSON string): + - vaultAddress: string, address of the vault (required) + - newDelegate: string, address of the new delegate (required)`; + + constructor(private solanaKit: SolanaAgentKit) { + super(); + } + + protected async _call(input: string): Promise { + try { + const parsedInput = JSON.parse(input); + const tx = await this.solanaKit.updateDriftVaultDelegate( + parsedInput.vaultAddress, + parsedInput.newDelegate, + ); + + return JSON.stringify({ + status: "success", + message: "Vault delegate updated successfully", + signature: tx, + }); + } catch (error: any) { + return JSON.stringify({ + status: "error", + message: error.message, + code: error.code || "UPDATE_DRIFT_VAULT_DELEGATE_ERROR", + }); + } + } +} diff --git a/src/langchain/drift/update_vault.ts b/src/langchain/drift/update_vault.ts new file mode 100644 index 0000000..608d963 --- /dev/null +++ b/src/langchain/drift/update_vault.ts @@ -0,0 +1,52 @@ +import { Tool } from "langchain/tools"; +import { SolanaAgentKit } from "../../agent"; + +export class SolanaUpdateDriftVaultTool extends Tool { + name = "update_drift_vault"; + description = `Update an existing drift vault with new settings. + + Inputs (JSON string): + - vaultAddress: string, vault address (required) + - redeemPeriod: number, days until redemption (optional) + - maxTokens: number, maximum tokens allowed (optional) + - minDepositAmount: number, minimum deposit amount (optional) + - managementFee: number, management fee percentage (optional) + - profitShare: number, profit sharing percentage (optional) + - hurdleRate: number, hurdle rate (optional) + - permissioned: boolean, whitelist requirement (optional)`; + + constructor(private solanaKit: SolanaAgentKit) { + super(); + } + + protected async _call(input: string): Promise { + try { + const parsedInput = JSON.parse(input); + const tx = await this.solanaKit.updateDriftVault( + parsedInput.vaultAddress, + // @ts-expect-error - type mismatch + { + hurdleRate: parsedInput.hurdleRate, + maxTokens: parsedInput.maxTokens, + minDepositAmount: parsedInput.minDepositAmount, + profitShare: parsedInput.profitShare, + managementFee: parsedInput.managementFee, + permissioned: parsedInput.permissioned, + redeemPeriod: parsedInput.redeemPeriod, + }, + ); + + return JSON.stringify({ + status: "success", + message: "Drift vault parameters updated successfully", + signature: tx, + }); + } catch (error: any) { + return JSON.stringify({ + status: "error", + message: error.message, + code: error.code || "UPDATE_DRIFT_VAULT_ERROR", + }); + } + } +} diff --git a/src/langchain/drift/vault_info.ts b/src/langchain/drift/vault_info.ts new file mode 100644 index 0000000..fc250e9 --- /dev/null +++ b/src/langchain/drift/vault_info.ts @@ -0,0 +1,32 @@ +import { Tool } from "langchain/tools"; +import { SolanaAgentKit } from "../../agent"; + +export class SolanaDriftVaultInfoTool extends Tool { + name = "drift_vault_info"; + description = `Get information about a drift vault. + + Inputs (JSON string): + - vaultNameOrAddress: string, name or address of the vault (required)`; + + constructor(private solanaKit: SolanaAgentKit) { + super(); + } + + protected async _call(input: string): Promise { + try { + const vaultInfo = await this.solanaKit.getDriftVaultInfo(input); + + return JSON.stringify({ + status: "success", + message: "Vault info retrieved successfully", + data: vaultInfo, + }); + } catch (error: any) { + return JSON.stringify({ + status: "error", + message: error.message, + code: error.code || "DRIFT_VAULT_INFO_ERROR", + }); + } + } +} diff --git a/src/langchain/drift/withdraw_from_account.ts b/src/langchain/drift/withdraw_from_account.ts new file mode 100644 index 0000000..ce76375 --- /dev/null +++ b/src/langchain/drift/withdraw_from_account.ts @@ -0,0 +1,39 @@ +import { Tool } from "langchain/tools"; +import { SolanaAgentKit } from "../../agent"; + +export class SolanaWithdrawFromDriftAccountTool extends Tool { + name = "withdraw_from_drift_account"; + description = `Withdraw or borrow funds from your drift account. + + Inputs (JSON string): + - amount: number, amount to withdraw (required) + - symbol: string, token symbol (required) + - isBorrow: boolean, whether to borrow funds instead of withdrawing (optional, default: false)`; + + constructor(private solanaKit: SolanaAgentKit) { + super(); + } + + protected async _call(input: string): Promise { + try { + const parsedInput = JSON.parse(input); + const tx = await this.solanaKit.withdrawFromDriftAccount( + parsedInput.amount, + parsedInput.symbol, + parsedInput.isBorrow, + ); + + return JSON.stringify({ + status: "success", + message: "Funds withdrawn successfully", + signature: tx, + }); + } catch (error: any) { + return JSON.stringify({ + status: "error", + message: error.message, + code: error.code || "WITHDRAW_FROM_DRIFT_ACCOUNT_ERROR", + }); + } + } +} diff --git a/src/langchain/drift/withdraw_from_vault.ts b/src/langchain/drift/withdraw_from_vault.ts new file mode 100644 index 0000000..1bace25 --- /dev/null +++ b/src/langchain/drift/withdraw_from_vault.ts @@ -0,0 +1,32 @@ +import { Tool } from "langchain/tools"; +import { SolanaAgentKit } from "../../agent"; + +export class SolanaWithdrawFromDriftVaultTool extends Tool { + name = "withdraw_from_drift_vault"; + description = `Withdraw funds from a vault given the redemption time has elapsed. + + Inputs (JSON string): + - vaultAddress: string, vault address (required)`; + + constructor(private solanaKit: SolanaAgentKit) { + super(); + } + + protected async _call(input: string): Promise { + try { + const tx = await this.solanaKit.withdrawFromDriftVault(input); + + return JSON.stringify({ + status: "success", + message: "Withdrawal successful", + signature: tx, + }); + } catch (error: any) { + return JSON.stringify({ + status: "error", + message: error.message, + code: error.code || "WITHDRAW_FROM_DRIFT_VAULT_ERROR", + }); + } + } +} diff --git a/src/langchain/index.ts b/src/langchain/index.ts index ca18fc6..d77dc20 100644 --- a/src/langchain/index.ts +++ b/src/langchain/index.ts @@ -25,6 +25,7 @@ export * from "./sns"; export * from "./lightprotocol"; export * from "./squads"; export * from "./helius"; +export * from "./drift"; export * from "./voltr"; import { SolanaAgentKit } from "../agent"; @@ -99,6 +100,21 @@ import { SolanaDeleteHeliusWebhookTool, SolanaParseTransactionHeliusTool, SolanaGetAllAssetsByOwner, + SolanaCheckDriftAccountTool, + SolanaCreateDriftUserAccountTool, + SolanaCreateDriftVaultTool, + SolanaDepositIntoDriftVaultTool, + SolanaDepositToDriftUserAccountTool, + SolanaDeriveVaultAddressTool, + SolanaDriftUserAccountInfoTool, + SolanaDriftVaultInfoTool, + SolanaRequestDriftWithdrawalTool, + SolanaTradeDelegatedDriftVaultTool, + SolanaTradeDriftPerpAccountTool, + SolanaUpdateDriftVaultDelegateTool, + SolanaUpdateDriftVaultTool, + SolanaWithdrawFromDriftAccountTool, + SolanaWithdrawFromDriftVaultTool, SolanaVoltrGetPositionValues, SolanaVoltrDepositStrategy, SolanaVoltrWithdrawStrategy, @@ -181,6 +197,21 @@ export function createSolanaTools(solanaKit: SolanaAgentKit) { new SolanaHeliusWebhookTool(solanaKit), new SolanaGetHeliusWebhookTool(solanaKit), new SolanaDeleteHeliusWebhookTool(solanaKit), + new SolanaCreateDriftUserAccountTool(solanaKit), + new SolanaCreateDriftVaultTool(solanaKit), + new SolanaDepositIntoDriftVaultTool(solanaKit), + new SolanaDepositToDriftUserAccountTool(solanaKit), + new SolanaDeriveVaultAddressTool(solanaKit), + new SolanaCheckDriftAccountTool(solanaKit), + new SolanaDriftUserAccountInfoTool(solanaKit), + new SolanaRequestDriftWithdrawalTool(solanaKit), + new SolanaTradeDelegatedDriftVaultTool(solanaKit), + new SolanaTradeDriftPerpAccountTool(solanaKit), + new SolanaUpdateDriftVaultDelegateTool(solanaKit), + new SolanaUpdateDriftVaultTool(solanaKit), + new SolanaDriftVaultInfoTool(solanaKit), + new SolanaWithdrawFromDriftAccountTool(solanaKit), + new SolanaWithdrawFromDriftVaultTool(solanaKit), new SolanaVoltrGetPositionValues(solanaKit), new SolanaVoltrDepositStrategy(solanaKit), new SolanaVoltrWithdrawStrategy(solanaKit), diff --git a/src/tools/solana/get_token_balances.ts b/src/tools/solana/get_token_balances.ts new file mode 100644 index 0000000..47272db --- /dev/null +++ b/src/tools/solana/get_token_balances.ts @@ -0,0 +1,59 @@ +import { LAMPORTS_PER_SOL, type PublicKey } from "@solana/web3.js"; +import type { SolanaAgentKit } from "../../index"; +import { TOKEN_PROGRAM_ID } from "@solana/spl-token"; +import { getTokenMetadata } from "../../utils/tokenMetadata"; + +/** + * Get the token balances of a Solana wallet + * @param agent - SolanaAgentKit instance + * @param token_address - Optional SPL token mint address. If not provided, returns SOL balance + * @returns Promise resolving to the balance as an object containing sol balance and token balances with their respective mints, symbols, names and decimals + */ +export async function get_token_balance( + agent: SolanaAgentKit, + walletAddress?: PublicKey, +): Promise<{ + sol: number; + tokens: Array<{ + tokenAddress: string; + name: string; + symbol: string; + balance: number; + decimals: number; + }>; +}> { + const [lamportsBalance, tokenAccountData] = await Promise.all([ + agent.connection.getBalance(walletAddress ?? agent.wallet_address), + agent.connection.getParsedTokenAccountsByOwner( + walletAddress ?? agent.wallet_address, + { + programId: TOKEN_PROGRAM_ID, + }, + ), + ]); + + const removedZeroBalance = tokenAccountData.value.filter( + (v) => v.account.data.parsed.info.tokenAmount.uiAmount !== 0, + ); + + const tokenBalances = await Promise.all( + removedZeroBalance.map(async (v) => { + const mint = v.account.data.parsed.info.mint; + const mintInfo = await getTokenMetadata(agent.connection, mint); + return { + tokenAddress: mint, + name: mintInfo.name ?? "", + symbol: mintInfo.symbol ?? "", + balance: v.account.data.parsed.info.tokenAmount.uiAmount as number, + decimals: v.account.data.parsed.info.tokenAmount.decimals as number, + }; + }), + ); + + const solBalance = lamportsBalance / LAMPORTS_PER_SOL; + + return { + sol: solBalance, + tokens: tokenBalances, + }; +} diff --git a/src/tools/solana/index.ts b/src/tools/solana/index.ts index f9681a4..a7b6de9 100644 --- a/src/tools/solana/index.ts +++ b/src/tools/solana/index.ts @@ -4,3 +4,4 @@ export * from "./close_empty_token_accounts"; export * from "./transfer"; export * from "./get_balance"; export * from "./get_balance_other"; +export * from "./get_token_balances"; diff --git a/src/utils/tokenMetadata.ts b/src/utils/tokenMetadata.ts new file mode 100644 index 0000000..514a208 --- /dev/null +++ b/src/utils/tokenMetadata.ts @@ -0,0 +1,83 @@ +import { Connection, PublicKey } from "@solana/web3.js"; + +export async function getTokenMetadata( + connection: Connection, + tokenMint: string, +) { + const METADATA_PROGRAM_ID = new PublicKey( + "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s", + ); + + const [metadataPDA] = PublicKey.findProgramAddressSync( + [ + Buffer.from("metadata"), + METADATA_PROGRAM_ID.toBuffer(), + new PublicKey(tokenMint).toBuffer(), + ], + METADATA_PROGRAM_ID, + ); + + const metadata = await connection.getAccountInfo(metadataPDA); + if (!metadata?.data) { + throw new Error("Metadata not found"); + } + + let offset = 1 + 32 + 32; // key + update auth + mint + const data = metadata.data; + const decoder = new TextDecoder(); + + // Read variable length strings + const readString = () => { + let nameLength = data[offset]; + + while (nameLength === 0) { + offset++; + nameLength = data[offset]; + if (offset >= data.length) { + return null; + } + } + + offset++; + const name = decoder + .decode(data.slice(offset, offset + nameLength)) + // @eslint-disable-next-line no-control-regex + .replace(new RegExp(String.fromCharCode(0), "g"), ""); + offset += nameLength; + return name; + }; + + const name = readString(); + const symbol = readString(); + const uri = readString(); + + // Read remaining data + const sellerFeeBasisPoints = data.readUInt16LE(offset); + offset += 2; + + let creators: + | { address: PublicKey; verified: boolean; share: number }[] + | null = null; + if (data[offset] === 1) { + offset++; + const numCreators = data[offset]; + offset++; + creators = [...Array(numCreators)].map(() => { + const creator = { + address: new PublicKey(data.slice(offset, offset + 32)), + verified: data[offset + 32] === 1, + share: data[offset + 33], + }; + offset += 34; + return creator; + }); + } + + return { + name, + symbol, + uri, + sellerFeeBasisPoints, + creators, + }; +} diff --git a/test/agent_sdks/vercel_ai.ts b/test/agent_sdks/vercel_ai.ts index 77fda22..bf1585e 100644 --- a/test/agent_sdks/vercel_ai.ts +++ b/test/agent_sdks/vercel_ai.ts @@ -95,7 +95,6 @@ async function runChatMode() { ); const tools = createVercelAITools(solanaAgent); - console.log(tools); const rl = readline.createInterface({ input: process.stdin,