mirror of
https://github.com/d0zingcat/solana-agent-kit.git
synced 2026-05-24 15:10:46 +00:00
feat: add more drift actions
This commit is contained in:
@@ -1,9 +1,16 @@
|
||||
import {
|
||||
BASE_PRECISION,
|
||||
BigNum,
|
||||
calculateDepositRate,
|
||||
calculateEstimatedEntryPriceWithL2,
|
||||
calculateInterestRate,
|
||||
calculateLongShortFundingRateAndLiveTwaps,
|
||||
convertToNumber,
|
||||
DRIFT_PROGRAM_ID,
|
||||
DriftClient,
|
||||
FastSingleTxSender,
|
||||
FUNDING_RATE_BUFFER_PRECISION,
|
||||
FUNDING_RATE_PRECISION_EXP,
|
||||
getInsuranceFundStakeAccountPublicKey,
|
||||
getLimitOrderParams,
|
||||
getMarketOrderParams,
|
||||
@@ -12,6 +19,7 @@ import {
|
||||
MainnetPerpMarkets,
|
||||
MainnetSpotMarkets,
|
||||
numberToSafeBN,
|
||||
PERCENTAGE_PRECISION,
|
||||
PositionDirection,
|
||||
PostOnlyParams,
|
||||
PRICE_PRECISION,
|
||||
@@ -26,6 +34,7 @@ import { getAssociatedTokenAddressSync } from "@solana/spl-token";
|
||||
import { PublicKey } from "@solana/web3.js";
|
||||
import { Transaction } from "@solana/web3.js";
|
||||
import { ComputeBudgetProgram } from "@solana/web3.js";
|
||||
import type { RawL2Output } from "./types";
|
||||
|
||||
export async function initClients(
|
||||
agent: SolanaAgentKit,
|
||||
@@ -748,3 +757,254 @@ export async function swapSpotToken(
|
||||
throw new Error(`Failed to swap token: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* To get funding rate as a percentage, you need to multiply by the funding rate buffer precision
|
||||
* @param rawFundingRate
|
||||
*/
|
||||
export function getFundingRateAsPercentage(rawFundingRate: anchor.BN) {
|
||||
return BigNum.from(
|
||||
rawFundingRate.mul(FUNDING_RATE_BUFFER_PRECISION),
|
||||
FUNDING_RATE_PRECISION_EXP,
|
||||
).toNum();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the funding rate for a perpetual market
|
||||
* @param agent
|
||||
* @param marketSymbol
|
||||
*/
|
||||
export async function calculatePerpMarketFundingRate(
|
||||
agent: SolanaAgentKit,
|
||||
marketSymbol: `${string}-PERP`,
|
||||
period: "year" | "hour",
|
||||
) {
|
||||
try {
|
||||
const { driftClient, cleanUp } = await initClients(agent);
|
||||
const market = driftClient.getMarketIndexAndType(
|
||||
`${marketSymbol.toUpperCase()}`,
|
||||
);
|
||||
|
||||
if (!market) {
|
||||
throw new Error(
|
||||
`This market isn't available on the Drift Protocol. Here's a list of markets that are: ${MainnetPerpMarkets.map(
|
||||
(v) => v.symbol,
|
||||
).join(", ")}`,
|
||||
);
|
||||
}
|
||||
|
||||
const marketAccount = driftClient.getPerpMarketAccount(market.marketIndex);
|
||||
|
||||
if (!marketAccount) {
|
||||
throw new Error("Market account not found");
|
||||
}
|
||||
|
||||
const [
|
||||
_marketTwapLive,
|
||||
_oracleTwapLive,
|
||||
longFundingRate,
|
||||
shortFundingRate,
|
||||
] = await calculateLongShortFundingRateAndLiveTwaps(
|
||||
marketAccount,
|
||||
driftClient.getOracleDataForPerpMarket(market.marketIndex),
|
||||
undefined,
|
||||
new anchor.BN(Date.now()),
|
||||
);
|
||||
|
||||
await cleanUp();
|
||||
|
||||
let longFundingRateNum = getFundingRateAsPercentage(longFundingRate);
|
||||
let shortFundingRateNum = getFundingRateAsPercentage(shortFundingRate);
|
||||
|
||||
if (period === "year") {
|
||||
const paymentsPerYear = 24 * 365.25;
|
||||
|
||||
longFundingRateNum *= paymentsPerYear;
|
||||
shortFundingRateNum *= paymentsPerYear;
|
||||
}
|
||||
|
||||
const longsArePaying = longFundingRateNum > 0;
|
||||
const shortsArePaying = !(shortFundingRateNum > 0);
|
||||
|
||||
const longsAreString = longsArePaying ? "pay" : "receive";
|
||||
const shortsAreString = !shortsArePaying ? "receive" : "pay";
|
||||
|
||||
const absoluteLongFundingRateNum = Math.abs(longFundingRateNum);
|
||||
const absoluteShortFundingRateNum = Math.abs(shortFundingRateNum);
|
||||
|
||||
const formattedLongRatePct = absoluteLongFundingRateNum.toFixed(
|
||||
period === "hour" ? 5 : 2,
|
||||
);
|
||||
const formattedShortRatePct = absoluteShortFundingRateNum.toFixed(
|
||||
period === "hour" ? 5 : 2,
|
||||
);
|
||||
|
||||
const paymentUnit = period === "year" ? "% APR" : "%";
|
||||
|
||||
const friendlyString = `At this rate, longs would ${longsAreString} ${formattedLongRatePct} ${paymentUnit} and shorts would ${shortsAreString} ${formattedShortRatePct} ${paymentUnit} at the end of the hour.`;
|
||||
|
||||
return {
|
||||
longRate: longsArePaying
|
||||
? -absoluteLongFundingRateNum
|
||||
: absoluteLongFundingRateNum,
|
||||
shortRate: shortsArePaying
|
||||
? -absoluteShortFundingRateNum
|
||||
: absoluteShortFundingRateNum,
|
||||
friendlyString,
|
||||
};
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
// @ts-expect-error e.message is a string
|
||||
`Something went wrong while trying to get the market's funding rate. Here's some more context: ${e.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getL2OrderBook(marketSymbol: `${string}-PERP`) {
|
||||
try {
|
||||
const serializedOrderbook: RawL2Output = await (
|
||||
await fetch(
|
||||
`https://dlob.drift.trade/l2?marketName=${marketSymbol.toUpperCase()}&includeOracle=true`,
|
||||
)
|
||||
).json();
|
||||
|
||||
return {
|
||||
asks: serializedOrderbook.asks.map((ask) => ({
|
||||
price: new anchor.BN(ask.price),
|
||||
size: new anchor.BN(ask.size),
|
||||
sources: Object.entries(ask.sources).reduce((previous, [key, val]) => {
|
||||
return {
|
||||
...(previous ?? {}),
|
||||
[key]: new anchor.BN(val),
|
||||
};
|
||||
}, {}),
|
||||
})),
|
||||
bids: serializedOrderbook.bids.map((bid) => ({
|
||||
price: new anchor.BN(bid.price),
|
||||
size: new anchor.BN(bid.size),
|
||||
sources: Object.entries(bid.sources).reduce((previous, [key, val]) => {
|
||||
return {
|
||||
...(previous ?? {}),
|
||||
[key]: new anchor.BN(val),
|
||||
};
|
||||
}, {}),
|
||||
})),
|
||||
oracleData: {
|
||||
price: serializedOrderbook.oracleData.price
|
||||
? new anchor.BN(serializedOrderbook.oracleData.price)
|
||||
: undefined,
|
||||
slot: serializedOrderbook.oracleData.slot
|
||||
? new anchor.BN(serializedOrderbook.oracleData.slot)
|
||||
: undefined,
|
||||
confidence: serializedOrderbook.oracleData.confidence
|
||||
? new anchor.BN(serializedOrderbook.oracleData.confidence)
|
||||
: undefined,
|
||||
hasSufficientNumberOfDataPoints:
|
||||
serializedOrderbook.oracleData.hasSufficientNumberOfDataPoints,
|
||||
twap: serializedOrderbook.oracleData.twap
|
||||
? new anchor.BN(serializedOrderbook.oracleData.twap)
|
||||
: undefined,
|
||||
twapConfidence: serializedOrderbook.oracleData.twapConfidence
|
||||
? new anchor.BN(serializedOrderbook.oracleData.twapConfidence)
|
||||
: undefined,
|
||||
maxPrice: serializedOrderbook.oracleData.maxPrice
|
||||
? new anchor.BN(serializedOrderbook.oracleData.maxPrice)
|
||||
: undefined,
|
||||
},
|
||||
slot: serializedOrderbook.slot,
|
||||
};
|
||||
} catch (e) {
|
||||
throw new Error();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the estimated entry quote of a perp trade
|
||||
* @param agent
|
||||
* @param marketSymbol
|
||||
* @param amount
|
||||
* @param type
|
||||
*/
|
||||
export async function getEntryQuoteOfPerpTrade(
|
||||
marketSymbol: `${string}-PERP`,
|
||||
amount: number,
|
||||
type: "long" | "short",
|
||||
) {
|
||||
try {
|
||||
const l2OrderBookData = await getL2OrderBook(marketSymbol);
|
||||
const estimatedEntryPriceData = calculateEstimatedEntryPriceWithL2(
|
||||
"quote",
|
||||
numberToSafeBN(amount, BASE_PRECISION),
|
||||
type === "long" ? PositionDirection.LONG : PositionDirection.SHORT,
|
||||
BASE_PRECISION,
|
||||
// @ts-expect-error - false type conflict
|
||||
l2OrderBookData,
|
||||
);
|
||||
|
||||
return {
|
||||
entryPrice: convertToNumber(
|
||||
estimatedEntryPriceData.entryPrice,
|
||||
QUOTE_PRECISION,
|
||||
),
|
||||
priceImpact: convertToNumber(
|
||||
estimatedEntryPriceData.priceImpact,
|
||||
QUOTE_PRECISION,
|
||||
),
|
||||
bestPrice: convertToNumber(
|
||||
estimatedEntryPriceData.bestPrice,
|
||||
QUOTE_PRECISION,
|
||||
),
|
||||
worstPrice: convertToNumber(
|
||||
estimatedEntryPriceData.worstPrice,
|
||||
QUOTE_PRECISION,
|
||||
),
|
||||
};
|
||||
} catch (e) {
|
||||
// @ts-expect-error - error message is a string
|
||||
throw new Error(`Failed to get entry quote: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the APY for lending and borrowing a specific token on drift protocol
|
||||
* @param agent
|
||||
* @param symbol
|
||||
*/
|
||||
export async function getLendingAndBorrowAPY(
|
||||
agent: SolanaAgentKit,
|
||||
symbol: string,
|
||||
) {
|
||||
try {
|
||||
const { driftClient, cleanUp } = await initClients(agent);
|
||||
const token = MainnetSpotMarkets.find(
|
||||
(v) => v.symbol === symbol.toUpperCase(),
|
||||
);
|
||||
|
||||
if (!token) {
|
||||
throw new Error(
|
||||
`Token with symbol ${symbol} not found. Here's a list of available spot markets: ${MainnetSpotMarkets.map(
|
||||
(v) => v.symbol,
|
||||
).join(", ")}`,
|
||||
);
|
||||
}
|
||||
|
||||
const marketAccount = driftClient.getSpotMarketAccount(token.marketIndex);
|
||||
|
||||
if (!marketAccount) {
|
||||
throw new Error("Market account not found");
|
||||
}
|
||||
|
||||
const lendAPY = calculateDepositRate(marketAccount);
|
||||
const borrowAPY = calculateInterestRate(marketAccount);
|
||||
|
||||
await cleanUp();
|
||||
|
||||
return {
|
||||
lendingAPY: convertToNumber(lendAPY, PERCENTAGE_PRECISION) * 100, // convert to percentage
|
||||
borrowAPY: convertToNumber(borrowAPY, PERCENTAGE_PRECISION) * 100, // convert to percentage
|
||||
};
|
||||
} catch (e) {
|
||||
// @ts-expect-error - error message is a string
|
||||
throw new Error(`Failed to get APYs: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
33
src/tools/drift/types.ts
Normal file
33
src/tools/drift/types.ts
Normal file
@@ -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;
|
||||
};
|
||||
Reference in New Issue
Block a user