mirror of
https://github.com/d0zingcat/solana-agent-kit.git
synced 2026-05-30 23:26:49 +00:00
merge: main. Also removed eslint config from discord bot starter it was conflicting with global eslint config
This commit is contained in:
446
src/tools/drift/drift.ts
Normal file
446
src/tools/drift/drift.ts
Normal file
@@ -0,0 +1,446 @@
|
||||
import {
|
||||
BASE_PRECISION,
|
||||
convertToNumber,
|
||||
DRIFT_PROGRAM_ID,
|
||||
DriftClient,
|
||||
FastSingleTxSender,
|
||||
getLimitOrderParams,
|
||||
getMarketOrderParams,
|
||||
getUserAccountPublicKeySync,
|
||||
MainnetSpotMarkets,
|
||||
numberToSafeBN,
|
||||
PositionDirection,
|
||||
PostOnlyParams,
|
||||
PRICE_PRECISION,
|
||||
QUOTE_PRECISION,
|
||||
User,
|
||||
type IWallet,
|
||||
} from "@drift-labs/sdk";
|
||||
import type { SolanaAgentKit } from "../../agent";
|
||||
import * as anchor from "@coral-xyz/anchor";
|
||||
import { IDL, VAULT_PROGRAM_ID, VaultClient } from "@drift-labs/vaults-sdk";
|
||||
import { getAssociatedTokenAddressSync } from "@solana/spl-token";
|
||||
import { PublicKey } from "@solana/web3.js";
|
||||
import { Transaction } from "@solana/web3.js";
|
||||
import { ComputeBudgetProgram } from "@solana/web3.js";
|
||||
|
||||
export async function initClients(agent: SolanaAgentKit) {
|
||||
const wallet: IWallet = {
|
||||
publicKey: agent.wallet.publicKey,
|
||||
payer: agent.wallet,
|
||||
signAllTransactions: async (txs) => {
|
||||
for (const tx of txs) {
|
||||
tx.sign(agent.wallet);
|
||||
}
|
||||
return txs;
|
||||
},
|
||||
signTransaction: async (tx) => {
|
||||
tx.sign(agent.wallet);
|
||||
return tx;
|
||||
},
|
||||
};
|
||||
|
||||
const driftClient = new DriftClient({
|
||||
connection: agent.connection,
|
||||
wallet,
|
||||
env: "mainnet-beta",
|
||||
txSender: new FastSingleTxSender({
|
||||
connection: agent.connection,
|
||||
wallet,
|
||||
timeout: 30000,
|
||||
blockhashRefreshInterval: 1000,
|
||||
opts: {
|
||||
commitment: agent.connection.commitment ?? "confirmed",
|
||||
skipPreflight: false,
|
||||
preflightCommitment: agent.connection.commitment ?? "confirmed",
|
||||
},
|
||||
}),
|
||||
});
|
||||
const vaultProgram = new anchor.Program(
|
||||
IDL,
|
||||
VAULT_PROGRAM_ID,
|
||||
driftClient.provider,
|
||||
);
|
||||
const vaultClient = new VaultClient({
|
||||
driftClient,
|
||||
// @ts-expect-error - type mismatch due to different dep versions
|
||||
program: vaultProgram,
|
||||
cliMode: false,
|
||||
});
|
||||
await driftClient.subscribe();
|
||||
|
||||
async function cleanUp() {
|
||||
await driftClient.unsubscribe();
|
||||
}
|
||||
|
||||
return { driftClient, vaultClient, cleanUp };
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a drift user account provided an amount
|
||||
* @param amount amount of the token to deposit
|
||||
* @param symbol symbol of the token to deposit
|
||||
*/
|
||||
export async function createDriftUserAccount(
|
||||
agent: SolanaAgentKit,
|
||||
amount: number,
|
||||
symbol: string,
|
||||
) {
|
||||
try {
|
||||
const { driftClient, cleanUp } = await initClients(agent);
|
||||
const user = new User({
|
||||
driftClient,
|
||||
userAccountPublicKey: getUserAccountPublicKeySync(
|
||||
new PublicKey(DRIFT_PROGRAM_ID),
|
||||
agent.wallet.publicKey,
|
||||
),
|
||||
});
|
||||
const userAccountExists = await user.exists();
|
||||
const token = MainnetSpotMarkets.find(
|
||||
(v) => v.symbol === symbol.toUpperCase(),
|
||||
);
|
||||
|
||||
if (!token) {
|
||||
throw new Error(`Token with symbol ${symbol} not found`);
|
||||
}
|
||||
|
||||
if (!userAccountExists) {
|
||||
const depositAmount = numberToSafeBN(amount, token.precision);
|
||||
const [txSignature, account] =
|
||||
await driftClient.initializeUserAccountAndDepositCollateral(
|
||||
depositAmount,
|
||||
getAssociatedTokenAddressSync(token.mint, agent.wallet.publicKey),
|
||||
);
|
||||
|
||||
await cleanUp();
|
||||
return { txSignature, account };
|
||||
}
|
||||
|
||||
await cleanUp();
|
||||
return {
|
||||
message: "User account already exists",
|
||||
account: user.userAccountPublicKey,
|
||||
};
|
||||
} catch (e) {
|
||||
// @ts-expect-error - error message is a string
|
||||
throw new Error(`Failed to create user account: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deposit to your drift user account
|
||||
* @param agent
|
||||
* @param amount
|
||||
* @param symbol
|
||||
* @param isRepay
|
||||
* @returns
|
||||
*/
|
||||
export async function depositToDriftUserAccount(
|
||||
agent: SolanaAgentKit,
|
||||
amount: number,
|
||||
symbol: string,
|
||||
isRepay = false,
|
||||
) {
|
||||
try {
|
||||
const { driftClient, cleanUp } = await initClients(agent);
|
||||
const publicKey = agent.wallet.publicKey;
|
||||
const user = new User({
|
||||
driftClient,
|
||||
userAccountPublicKey: getUserAccountPublicKeySync(
|
||||
new PublicKey(DRIFT_PROGRAM_ID),
|
||||
publicKey,
|
||||
),
|
||||
});
|
||||
const userAccountExists = await user.exists();
|
||||
const token = MainnetSpotMarkets.find(
|
||||
(v) => v.symbol === symbol.toUpperCase(),
|
||||
);
|
||||
|
||||
if (!token) {
|
||||
throw new Error(`Token with symbol ${symbol} not found`);
|
||||
}
|
||||
|
||||
if (!userAccountExists) {
|
||||
throw new Error("You need to create a Drift user account first.");
|
||||
}
|
||||
|
||||
const depositAmount = numberToSafeBN(amount, token.precision);
|
||||
|
||||
const [depInstruction, latestBlockhash] = await Promise.all([
|
||||
driftClient.getDepositTxnIx(
|
||||
depositAmount,
|
||||
token.marketIndex,
|
||||
getAssociatedTokenAddressSync(token.mint, publicKey),
|
||||
undefined,
|
||||
isRepay,
|
||||
),
|
||||
driftClient.connection.getLatestBlockhash(),
|
||||
]);
|
||||
|
||||
const tx = new Transaction().add(...depInstruction).add(
|
||||
ComputeBudgetProgram.setComputeUnitPrice({
|
||||
microLamports: 0.000001 * 1000000 * 1000000,
|
||||
}),
|
||||
);
|
||||
tx.recentBlockhash = latestBlockhash.blockhash;
|
||||
tx.sign(agent.wallet);
|
||||
const txSignature = await driftClient.txSender.sendRawTransaction(
|
||||
tx.serialize(),
|
||||
{ ...driftClient.opts },
|
||||
);
|
||||
|
||||
await cleanUp();
|
||||
return txSignature;
|
||||
} catch (e) {
|
||||
// @ts-expect-error - error message is a string
|
||||
throw new Error(`Failed to deposit to user account: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function withdrawFromDriftUserAccount(
|
||||
agent: SolanaAgentKit,
|
||||
amount: number,
|
||||
symbol: string,
|
||||
isBorrow = false,
|
||||
) {
|
||||
try {
|
||||
const { driftClient, cleanUp } = await initClients(agent);
|
||||
const user = new User({
|
||||
driftClient,
|
||||
userAccountPublicKey: getUserAccountPublicKeySync(
|
||||
new PublicKey(DRIFT_PROGRAM_ID),
|
||||
agent.wallet.publicKey,
|
||||
),
|
||||
});
|
||||
const userAccountExists = await user.exists();
|
||||
|
||||
if (!userAccountExists) {
|
||||
throw new Error("You need to create a Drift user account first.");
|
||||
}
|
||||
|
||||
const token = MainnetSpotMarkets.find(
|
||||
(v) => v.symbol === symbol.toUpperCase(),
|
||||
);
|
||||
|
||||
if (!token) {
|
||||
throw new Error(`Token with symbol ${symbol} not found`);
|
||||
}
|
||||
|
||||
const withdrawAmount = numberToSafeBN(amount, token.precision);
|
||||
|
||||
const [withdrawInstruction, latestBlockhash] = await Promise.all([
|
||||
driftClient.getWithdrawalIxs(
|
||||
withdrawAmount,
|
||||
token.marketIndex,
|
||||
getAssociatedTokenAddressSync(token.mint, agent.wallet.publicKey),
|
||||
!isBorrow,
|
||||
),
|
||||
driftClient.connection.getLatestBlockhash(),
|
||||
]);
|
||||
|
||||
const tx = new Transaction().add(...withdrawInstruction).add(
|
||||
ComputeBudgetProgram.setComputeUnitPrice({
|
||||
microLamports: 0.000001 * 1000000 * 1000000,
|
||||
}),
|
||||
);
|
||||
tx.recentBlockhash = latestBlockhash.blockhash;
|
||||
tx.sign(agent.wallet);
|
||||
|
||||
const txSignature = await driftClient.txSender.sendRawTransaction(
|
||||
tx.serialize(),
|
||||
{ ...driftClient.opts },
|
||||
);
|
||||
|
||||
await cleanUp();
|
||||
return txSignature;
|
||||
} catch (e) {
|
||||
// @ts-expect-error - error message is a string
|
||||
throw new Error(`Failed to withdraw from user account: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a perpetual trade on drift
|
||||
* @param agent
|
||||
* @param params.amount
|
||||
* @param params.symbol
|
||||
* @param params.action
|
||||
* @param params.type
|
||||
* @param params.price this should only be supplied if type is limit
|
||||
* @param params.reduceOnly
|
||||
*/
|
||||
export async function driftPerpTrade(
|
||||
agent: SolanaAgentKit,
|
||||
params: {
|
||||
amount: number;
|
||||
symbol: string;
|
||||
action: "long" | "short";
|
||||
type: "market" | "limit";
|
||||
price?: number;
|
||||
},
|
||||
) {
|
||||
try {
|
||||
const { driftClient, cleanUp } = await initClients(agent);
|
||||
const user = new User({
|
||||
driftClient,
|
||||
userAccountPublicKey: getUserAccountPublicKeySync(
|
||||
new PublicKey(DRIFT_PROGRAM_ID),
|
||||
agent.wallet.publicKey,
|
||||
),
|
||||
});
|
||||
const userAccountExists = await user.exists();
|
||||
|
||||
if (!userAccountExists) {
|
||||
throw new Error("You need to create a Drift user account first.");
|
||||
}
|
||||
|
||||
const market = driftClient.getMarketIndexAndType(
|
||||
`${params.symbol.toUpperCase()}-PERP`,
|
||||
);
|
||||
|
||||
if (!market) {
|
||||
throw new Error(`Token with symbol ${params.symbol} not found`);
|
||||
}
|
||||
|
||||
const baseAssetPrice = driftClient.getOracleDataForPerpMarket(
|
||||
market.marketIndex,
|
||||
);
|
||||
const convertedAmount =
|
||||
params.amount / convertToNumber(baseAssetPrice.price, PRICE_PRECISION);
|
||||
|
||||
let signature: anchor.web3.TransactionSignature;
|
||||
|
||||
if (params.type === "limit") {
|
||||
if (!params.price) {
|
||||
throw new Error("Price is required for limit orders");
|
||||
}
|
||||
|
||||
signature = await driftClient.placePerpOrder(
|
||||
getLimitOrderParams({
|
||||
baseAssetAmount: numberToSafeBN(convertedAmount, BASE_PRECISION),
|
||||
reduceOnly: false,
|
||||
direction:
|
||||
params.action === "long"
|
||||
? PositionDirection.LONG
|
||||
: PositionDirection.SHORT,
|
||||
marketIndex: market.marketIndex,
|
||||
price: numberToSafeBN(params.price, PRICE_PRECISION),
|
||||
postOnly: PostOnlyParams.SLIDE,
|
||||
}),
|
||||
{
|
||||
computeUnitsPrice: 0.000001 * 1000000 * 1000000,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
signature = await driftClient.placePerpOrder(
|
||||
getMarketOrderParams({
|
||||
baseAssetAmount: numberToSafeBN(convertedAmount, BASE_PRECISION),
|
||||
reduceOnly: false,
|
||||
direction:
|
||||
params.action === "long"
|
||||
? PositionDirection.LONG
|
||||
: PositionDirection.SHORT,
|
||||
marketIndex: market.marketIndex,
|
||||
}),
|
||||
{
|
||||
computeUnitsPrice: 0.000001 * 1000000 * 1000000,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (!signature) {
|
||||
throw new Error("Failed to place order. Please make sure ");
|
||||
}
|
||||
|
||||
await cleanUp();
|
||||
return signature;
|
||||
} catch (e) {
|
||||
// @ts-expect-error - error message is a string
|
||||
throw new Error(`Failed to place order: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a user has a drift account
|
||||
* @param agent
|
||||
*/
|
||||
export async function doesUserHaveDriftAccount(agent: SolanaAgentKit) {
|
||||
try {
|
||||
const { driftClient, cleanUp } = await initClients(agent);
|
||||
const user = new User({
|
||||
driftClient,
|
||||
userAccountPublicKey: getUserAccountPublicKeySync(
|
||||
new PublicKey(DRIFT_PROGRAM_ID),
|
||||
agent.wallet.publicKey,
|
||||
),
|
||||
});
|
||||
user.getActivePerpPositions();
|
||||
const userAccountExists = await user.exists();
|
||||
await cleanUp();
|
||||
return {
|
||||
hasAccount: userAccountExists,
|
||||
account: user.userAccountPublicKey,
|
||||
};
|
||||
} catch (e) {
|
||||
// @ts-expect-error - error message is a string
|
||||
throw new Error(`Failed to check user account: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get account info for a drift User
|
||||
* @param agent
|
||||
* @returns
|
||||
*/
|
||||
export async function driftUserAccountInfo(agent: SolanaAgentKit) {
|
||||
try {
|
||||
const { driftClient, cleanUp } = await initClients(agent);
|
||||
const user = new User({
|
||||
driftClient,
|
||||
userAccountPublicKey: getUserAccountPublicKeySync(
|
||||
new PublicKey(DRIFT_PROGRAM_ID),
|
||||
agent.wallet.publicKey,
|
||||
),
|
||||
});
|
||||
const userAccountExists = await user.exists();
|
||||
|
||||
if (!userAccountExists) {
|
||||
throw new Error("User account does not exist");
|
||||
}
|
||||
await user.subscribe();
|
||||
const account = user.getUserAccount();
|
||||
await user.unsubscribe();
|
||||
|
||||
await cleanUp();
|
||||
const perpPositions = account.perpPositions.map((pos) => ({
|
||||
...pos,
|
||||
baseAssetAmount: convertToNumber(pos.baseAssetAmount, BASE_PRECISION),
|
||||
settledPnl: convertToNumber(pos.settledPnl, QUOTE_PRECISION),
|
||||
}));
|
||||
const spotPositions = account.spotPositions.map((pos) => ({
|
||||
...pos,
|
||||
scaledBalance: convertToNumber(pos.scaledBalance, BASE_PRECISION),
|
||||
cumulativeDeposits: convertToNumber(
|
||||
pos.cumulativeDeposits,
|
||||
BASE_PRECISION,
|
||||
),
|
||||
symbol: MainnetSpotMarkets.find((v) => v.marketIndex === pos.marketIndex)
|
||||
?.symbol,
|
||||
}));
|
||||
|
||||
return {
|
||||
...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,
|
||||
spotPositions,
|
||||
};
|
||||
} catch (e) {
|
||||
// @ts-expect-error - error message is a string
|
||||
throw new Error(`Failed to check user account: ${e.message}`);
|
||||
}
|
||||
}
|
||||
541
src/tools/drift/drift_vault.ts
Normal file
541
src/tools/drift/drift_vault.ts
Normal file
@@ -0,0 +1,541 @@
|
||||
import {
|
||||
BASE_PRECISION,
|
||||
convertToNumber,
|
||||
getLimitOrderParams,
|
||||
getMarketOrderParams,
|
||||
getOrderParams,
|
||||
MainnetPerpMarkets,
|
||||
MainnetSpotMarkets,
|
||||
MarketType,
|
||||
numberToSafeBN,
|
||||
PERCENTAGE_PRECISION,
|
||||
PositionDirection,
|
||||
PostOnlyParams,
|
||||
PRICE_PRECISION,
|
||||
QUOTE_PRECISION,
|
||||
TEN,
|
||||
} from "@drift-labs/sdk";
|
||||
import {
|
||||
WithdrawUnit,
|
||||
encodeName,
|
||||
getVaultDepositorAddressSync,
|
||||
} from "@drift-labs/vaults-sdk";
|
||||
import {
|
||||
ComputeBudgetProgram,
|
||||
PublicKey,
|
||||
type TransactionInstruction,
|
||||
} from "@solana/web3.js";
|
||||
import type { SolanaAgentKit } from "../../agent";
|
||||
import { BN } from "bn.js";
|
||||
import { initClients } from "./drift";
|
||||
|
||||
export function getMarketIndexAndType(name: `${string}-${string}`) {
|
||||
const [symbol, type] = name.toUpperCase().split("-");
|
||||
|
||||
if (type === "PERP") {
|
||||
const token = MainnetPerpMarkets.find((v) => v.symbol === symbol);
|
||||
if (!token) {
|
||||
throw new Error("Drift doesn't have that market");
|
||||
}
|
||||
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");
|
||||
}
|
||||
return { marketIndex: token.marketIndex, marketType: MarketType.SPOT };
|
||||
}
|
||||
|
||||
async function getOrCreateVaultDepositor(agent: SolanaAgentKit, vault: string) {
|
||||
const { vaultClient, cleanUp } = await initClients(agent);
|
||||
const vaultPublicKey = new PublicKey(vault);
|
||||
const vaultDepositor = getVaultDepositorAddressSync(
|
||||
vaultClient.program.programId,
|
||||
vaultPublicKey,
|
||||
agent.wallet.publicKey,
|
||||
);
|
||||
|
||||
try {
|
||||
await vaultClient.getVaultDepositor(vaultDepositor);
|
||||
await cleanUp();
|
||||
return vaultDepositor;
|
||||
} catch (e) {
|
||||
// @ts-expect-error - error message is a string
|
||||
if (e.message === "Account not found") {
|
||||
await vaultClient.initializeVaultDepositor(vaultDepositor);
|
||||
}
|
||||
await cleanUp();
|
||||
return vaultDepositor;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Create a vault
|
||||
@param agent SolanaAgentKit instance
|
||||
@param params Vault creation parameters
|
||||
@param params.name Name of the vault (must be unique)
|
||||
@param params.marketName Market name of the vault (e.g. "USDC-SPOT")
|
||||
@param params.redeemPeriod Redeem period in seconds
|
||||
@param params.maxTokens Maximum amount that can be deposited into the vault (in tokens)
|
||||
@param params.minDepositAmount Minimum amount that can be deposited into the vault (in tokens)
|
||||
@param params.managementFee Management fee percentage (e.g 2 == 2%)
|
||||
@param params.profitShare Profit share percentage (e.g 20 == 20%)
|
||||
@param params.hurdleRate Hurdle rate percentage
|
||||
@param params.permissioned Whether the vault uses a whitelist
|
||||
@returns Promise<anchor.Web3.TransactionSignature> - The transaction signature of the vault creation
|
||||
*/
|
||||
export async function createVault(
|
||||
agent: SolanaAgentKit,
|
||||
params: {
|
||||
name: string;
|
||||
marketName: `${string}-${string}`;
|
||||
redeemPeriod: number;
|
||||
maxTokens: number;
|
||||
minDepositAmount: number;
|
||||
managementFee: number;
|
||||
profitShare: number;
|
||||
hurdleRate?: number;
|
||||
permissioned?: boolean;
|
||||
},
|
||||
) {
|
||||
try {
|
||||
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");
|
||||
}
|
||||
|
||||
const spotPrecision = TEN.pow(new BN(spotMarket.decimals));
|
||||
|
||||
if (marketIndexAndType.marketType === MarketType.PERP) {
|
||||
throw new Error("Only SPOT market names are supported");
|
||||
}
|
||||
|
||||
const tx = await vaultClient.initializeVault({
|
||||
name: encodeName(params.name),
|
||||
spotMarketIndex: marketIndexAndType.marketIndex,
|
||||
hurdleRate: new BN(params.hurdleRate ?? 0)
|
||||
.mul(PERCENTAGE_PRECISION)
|
||||
.div(new BN(100))
|
||||
.toNumber(),
|
||||
profitShare: new BN(params.profitShare)
|
||||
.mul(PERCENTAGE_PRECISION)
|
||||
.div(new BN(100))
|
||||
.toNumber(),
|
||||
minDepositAmount: numberToSafeBN(params.minDepositAmount, spotPrecision),
|
||||
redeemPeriod: new BN(params.redeemPeriod * 86400),
|
||||
maxTokens: numberToSafeBN(params.maxTokens, spotPrecision),
|
||||
managementFee: new BN(params.managementFee)
|
||||
.mul(PERCENTAGE_PRECISION)
|
||||
.div(new BN(100)),
|
||||
permissioned: params.permissioned ?? false,
|
||||
});
|
||||
|
||||
await cleanUp();
|
||||
|
||||
return tx;
|
||||
} catch (e) {
|
||||
// @ts-expect-error - error message is a string
|
||||
throw new Error(`Failed to create Drift vault: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Update the vault's info
|
||||
@param agent SolanaAgentKit instance
|
||||
@param vault Vault address
|
||||
@param params Vault update parameters
|
||||
@param params.redeemPeriod Redeem period in seconds
|
||||
@param params.maxTokens Maximum amount that can be deposited into the vault (in tokens)
|
||||
@param params.minDepositAmount Minimum amount that can be deposited into the vault (in tokens)
|
||||
@param params.managementFee Management fee percentage (e.g 2 == 2%)
|
||||
@param params.profitShare Profit share percentage (e.g 20 == 20%)
|
||||
@param params.hurdleRate Hurdle rate percentage
|
||||
@param params.permissioned Whether the vault uses a whitelist
|
||||
@returns Promise<anchor.Web3.TransactionSignature> - The transaction signature of the vault update
|
||||
*/
|
||||
export async function updateVault(
|
||||
agent: SolanaAgentKit,
|
||||
vault: string,
|
||||
params: {
|
||||
redeemPeriod?: number;
|
||||
maxTokens?: number;
|
||||
minDepositAmount?: number;
|
||||
managementFee?: number;
|
||||
profitShare?: number;
|
||||
hurdleRate?: number;
|
||||
permissioned?: boolean;
|
||||
},
|
||||
) {
|
||||
try {
|
||||
const { vaultClient, cleanUp, driftClient } = await initClients(agent);
|
||||
const vaultPublicKey = new PublicKey(vault);
|
||||
const vaultDetails = await vaultClient.getVault(vaultPublicKey);
|
||||
|
||||
const spotMarket = driftClient.getSpotMarketAccount(
|
||||
vaultDetails.spotMarketIndex,
|
||||
);
|
||||
|
||||
if (!spotMarket) {
|
||||
throw new Error("Market not found");
|
||||
}
|
||||
|
||||
const spotPrecision = TEN.pow(new BN(spotMarket.decimals));
|
||||
|
||||
const tx = await vaultClient.managerUpdateVault(vaultPublicKey, {
|
||||
redeemPeriod: new BN(
|
||||
params.redeemPeriod
|
||||
? params.redeemPeriod * 86400
|
||||
: vaultDetails.redeemPeriod,
|
||||
),
|
||||
maxTokens: params.maxTokens
|
||||
? numberToSafeBN(params.maxTokens, spotPrecision)
|
||||
: vaultDetails.maxTokens,
|
||||
minDepositAmount: params.minDepositAmount
|
||||
? numberToSafeBN(params.minDepositAmount, spotPrecision)
|
||||
: vaultDetails.minDepositAmount,
|
||||
managementFee: params.managementFee
|
||||
? new BN(params.managementFee)
|
||||
.mul(PERCENTAGE_PRECISION)
|
||||
.div(new BN(100))
|
||||
: vaultDetails.managementFee,
|
||||
profitShare: params.profitShare
|
||||
? new BN(params.profitShare)
|
||||
.mul(PERCENTAGE_PRECISION)
|
||||
.div(new BN(100))
|
||||
.toNumber()
|
||||
: vaultDetails.profitShare,
|
||||
hurdleRate: params.hurdleRate
|
||||
? new BN(params.hurdleRate)
|
||||
.mul(PERCENTAGE_PRECISION)
|
||||
.div(new BN(100))
|
||||
.toNumber()
|
||||
: vaultDetails.hurdleRate,
|
||||
permissioned: params.permissioned ?? vaultDetails.permissioned,
|
||||
});
|
||||
|
||||
await cleanUp();
|
||||
|
||||
return tx;
|
||||
} catch (e) {
|
||||
// @ts-expect-error - error message is a string
|
||||
throw new Error(`Failed to update Drift vault: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getVaultInfo(
|
||||
agent: SolanaAgentKit,
|
||||
vaultAddress: string,
|
||||
) {
|
||||
try {
|
||||
const { vaultClient, cleanUp } = await initClients(agent);
|
||||
const vaultPublicKey = new PublicKey(vaultAddress);
|
||||
const vaultDetails = await vaultClient.getVault(vaultPublicKey);
|
||||
|
||||
await cleanUp();
|
||||
|
||||
return vaultDetails;
|
||||
} catch (e) {
|
||||
// @ts-expect-error - error message is a string
|
||||
throw new Error(`Failed to get vault info: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Deposit tokens into a vault
|
||||
@param agent SolanaAgentKit instance
|
||||
@param amount Amount to deposit into the vault (in tokens)
|
||||
@param vault Vault address
|
||||
@returns Promise<anchor.Web3.TransactionSignature> - The transaction signature of the deposit
|
||||
*/
|
||||
export async function depositIntoVault(
|
||||
agent: SolanaAgentKit,
|
||||
amount: number,
|
||||
vault: string,
|
||||
) {
|
||||
const { vaultClient, driftClient, cleanUp } = await initClients(agent);
|
||||
|
||||
try {
|
||||
const vaultPublicKey = new PublicKey(vault);
|
||||
const [isOwned, vaultDetails] = await Promise.all([
|
||||
getIsOwned(agent, vault),
|
||||
vaultClient.getVault(vaultPublicKey),
|
||||
]);
|
||||
const spotMarket = driftClient.getSpotMarketAccount(
|
||||
vaultDetails.spotMarketIndex,
|
||||
);
|
||||
|
||||
if (!spotMarket) {
|
||||
throw new Error("Market not found");
|
||||
}
|
||||
|
||||
const spotPrecision = TEN.pow(new BN(spotMarket.decimals));
|
||||
const amountBN = numberToSafeBN(amount, spotPrecision);
|
||||
|
||||
if (isOwned) {
|
||||
return await vaultClient.managerDeposit(vaultPublicKey, amountBN);
|
||||
}
|
||||
|
||||
const vaultDepositor = await getOrCreateVaultDepositor(agent, vault);
|
||||
const tx = await vaultClient.deposit(vaultDepositor, amountBN);
|
||||
|
||||
await cleanUp();
|
||||
|
||||
return tx;
|
||||
} catch (e) {
|
||||
// @ts-expect-error - error message is a string
|
||||
throw new Error(`Failed to deposit into Drift vault: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Request a withdrawal from a vault. If successful redemption period starts and the user can redeem the tokens after the period ends
|
||||
@param agent SolanaAgentKit instance
|
||||
@param amount Amount to withdraw from the vault (in shares)
|
||||
@param vault Vault address
|
||||
*/
|
||||
export async function requestWithdrawalFromVault(
|
||||
agent: SolanaAgentKit,
|
||||
amount: number,
|
||||
vault: string,
|
||||
) {
|
||||
try {
|
||||
const { vaultClient, cleanUp } = await initClients(agent);
|
||||
const vaultPublicKey = new PublicKey(vault);
|
||||
const isOwned = await getIsOwned(agent, vault);
|
||||
|
||||
if (isOwned) {
|
||||
return await vaultClient.managerRequestWithdraw(
|
||||
vaultPublicKey,
|
||||
new BN(amount.toFixed(0)),
|
||||
WithdrawUnit.SHARES,
|
||||
);
|
||||
}
|
||||
|
||||
const vaultDepositor = await getOrCreateVaultDepositor(agent, vault);
|
||||
|
||||
const tx = await vaultClient.requestWithdraw(
|
||||
vaultDepositor,
|
||||
new BN(amount.toFixed(0)),
|
||||
WithdrawUnit.SHARES,
|
||||
);
|
||||
|
||||
await cleanUp();
|
||||
|
||||
return tx;
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
// @ts-expect-error - error message is a string
|
||||
`Failed to request withdrawal from Drift vault: ${e.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Withdraw tokens once the redemption period has elapsed.
|
||||
@param agent SolanaAgentKit instance
|
||||
@param vault Vault address
|
||||
@returns Promise<anchor.Web3.TransactionSignature> - The transaction signature of the redemption
|
||||
*/
|
||||
export async function withdrawFromDriftVault(
|
||||
agent: SolanaAgentKit,
|
||||
vault: string,
|
||||
) {
|
||||
try {
|
||||
const { vaultClient, cleanUp } = await initClients(agent);
|
||||
const vaultPublicKey = new PublicKey(vault);
|
||||
const isOwned = await getIsOwned(agent, vault);
|
||||
|
||||
if (isOwned) {
|
||||
return await vaultClient.managerWithdraw(vaultPublicKey);
|
||||
}
|
||||
|
||||
const vaultDepositor = await getOrCreateVaultDepositor(agent, vault);
|
||||
|
||||
const tx = await vaultClient.withdraw(vaultDepositor);
|
||||
|
||||
await cleanUp();
|
||||
|
||||
return tx;
|
||||
} catch (e) {
|
||||
// @ts-expect-error - error message is a string
|
||||
throw new Error(`Failed to redeem tokens from Drift vault: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Get if vault is owned by the user
|
||||
@param agent SolanaAgentKit instance
|
||||
@param vault Vault address
|
||||
@returns Promise<boolean> - Whether the vault is owned by the user
|
||||
*/
|
||||
async function getIsOwned(agent: SolanaAgentKit, vault: string) {
|
||||
try {
|
||||
const { vaultClient, cleanUp } = await initClients(agent);
|
||||
const vaultPublicKey = new PublicKey(vault);
|
||||
const vaultDetails = await vaultClient.getVault(vaultPublicKey);
|
||||
const isOwned = vaultDetails.delegate.equals(agent.wallet.publicKey);
|
||||
|
||||
await cleanUp();
|
||||
|
||||
return isOwned;
|
||||
} catch (e) {
|
||||
// @ts-expect-error - error message is a string
|
||||
throw new Error(`Failed to check if vault is owned: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Carry out a trade with a delegated vault
|
||||
@param agent SolanaAgentKit instance
|
||||
@param amount Amount to trade (in tokens)
|
||||
@param symbol Symbol of the token to trade
|
||||
@param action Action to take (e.g. "buy" or "sell")
|
||||
@param type Type of trade (e.g. "market" or "limit")
|
||||
@param vault Vault address
|
||||
*/
|
||||
export async function tradeDriftVault(
|
||||
agent: SolanaAgentKit,
|
||||
vault: string,
|
||||
amount: number,
|
||||
symbol: string,
|
||||
action: "buy" | "sell",
|
||||
type: "market" | "limit",
|
||||
price?: number,
|
||||
) {
|
||||
try {
|
||||
const { driftClient, vaultClient, cleanUp } = await initClients(agent);
|
||||
const [isOwned, vaultDetails, driftLookupTableAccount] = await Promise.all([
|
||||
getIsOwned(agent, vault),
|
||||
vaultClient.getVault(new PublicKey(vault)),
|
||||
driftClient.fetchMarketLookupTableAccount(),
|
||||
]);
|
||||
|
||||
if (!isOwned) {
|
||||
throw new Error(
|
||||
"This vault is owned by someone else, so you can't trade with it",
|
||||
);
|
||||
}
|
||||
|
||||
driftClient.authority = new PublicKey(vault);
|
||||
driftClient.activeSubAccountId = 0;
|
||||
vaultClient.driftClient = driftClient;
|
||||
|
||||
const usdcSpotMarket = driftClient.getSpotMarketAccount(0);
|
||||
if (!usdcSpotMarket) {
|
||||
throw new Error("USDC-SPOT market not found");
|
||||
}
|
||||
|
||||
const usdcPrecision = TEN.pow(new BN(usdcSpotMarket.decimals));
|
||||
const vaultWithdrawalsRequested = convertToNumber(
|
||||
vaultDetails.totalWithdrawRequested,
|
||||
usdcPrecision,
|
||||
);
|
||||
// this is actually the authority provided
|
||||
const user = driftClient.getUser();
|
||||
const currentVaultBalance =
|
||||
convertToNumber(user.getNetSpotMarketValue(), QUOTE_PRECISION) +
|
||||
convertToNumber(user.getUnrealizedPNL(true), QUOTE_PRECISION);
|
||||
const availableBalanceInUSD =
|
||||
currentVaultBalance - vaultWithdrawalsRequested;
|
||||
|
||||
if (amount > availableBalanceInUSD) {
|
||||
throw new Error(
|
||||
"Insufficient balance: You don't have enough balance to make this trade",
|
||||
);
|
||||
}
|
||||
|
||||
const perpMarketIndexAndType = getMarketIndexAndType(
|
||||
`${symbol.toUpperCase()}-PERP`,
|
||||
);
|
||||
const perpMarketAccount = driftClient.getPerpMarketAccount(
|
||||
perpMarketIndexAndType.marketIndex,
|
||||
);
|
||||
|
||||
if (!perpMarketIndexAndType || !perpMarketAccount) {
|
||||
throw new Error(
|
||||
"Invalid symbol: Drift doesn't have a market for this token",
|
||||
);
|
||||
}
|
||||
|
||||
const perpOracle = driftClient.getOracleDataForPerpMarket(
|
||||
perpMarketAccount.marketIndex,
|
||||
);
|
||||
const oraclePriceNumber = convertToNumber(
|
||||
perpOracle.price,
|
||||
PRICE_PRECISION,
|
||||
);
|
||||
const baseAmount = amount / oraclePriceNumber;
|
||||
const instructions: TransactionInstruction[] = [];
|
||||
|
||||
instructions.push(
|
||||
ComputeBudgetProgram.setComputeUnitLimit({ units: 1400000 }),
|
||||
);
|
||||
|
||||
if (type === "limit" || price) {
|
||||
if (!price) {
|
||||
throw new Error("Price is required for limit orders");
|
||||
}
|
||||
|
||||
const instruction = await driftClient.getPlaceOrdersIx([
|
||||
getOrderParams(
|
||||
getLimitOrderParams({
|
||||
price: numberToSafeBN(price, PRICE_PRECISION),
|
||||
marketType: MarketType.PERP,
|
||||
baseAssetAmount: numberToSafeBN(baseAmount, BASE_PRECISION),
|
||||
direction:
|
||||
action === "buy"
|
||||
? PositionDirection.LONG
|
||||
: PositionDirection.SHORT,
|
||||
marketIndex: perpMarketAccount.marketIndex,
|
||||
postOnly: PostOnlyParams.SLIDE,
|
||||
}),
|
||||
),
|
||||
]);
|
||||
|
||||
instructions.push(instruction);
|
||||
} else {
|
||||
// defaults to market order if type is not limit and price is not provided
|
||||
const instruction = await driftClient.getPlaceOrdersIx([
|
||||
getOrderParams(
|
||||
getMarketOrderParams({
|
||||
marketType: MarketType.PERP,
|
||||
baseAssetAmount: numberToSafeBN(baseAmount, BASE_PRECISION),
|
||||
direction:
|
||||
action === "buy"
|
||||
? PositionDirection.LONG
|
||||
: PositionDirection.SHORT,
|
||||
marketIndex: perpMarketAccount.marketIndex,
|
||||
}),
|
||||
),
|
||||
]);
|
||||
instructions.push(instruction);
|
||||
}
|
||||
|
||||
const tx = await driftClient.txSender.sendVersionedTransaction(
|
||||
await driftClient.txSender.getVersionedTransaction(
|
||||
instructions,
|
||||
[driftLookupTableAccount],
|
||||
[],
|
||||
driftClient.opts,
|
||||
),
|
||||
);
|
||||
|
||||
await cleanUp();
|
||||
|
||||
return tx;
|
||||
} catch (e) {
|
||||
// @ts-expect-error - error message is a string
|
||||
throw new Error(`Failed to trade with Drift vault: ${e.message}`);
|
||||
}
|
||||
}
|
||||
2
src/tools/drift/index.ts
Normal file
2
src/tools/drift/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./drift";
|
||||
export * from "./drift_vault";
|
||||
Reference in New Issue
Block a user