feat: add more drift actions

This commit is contained in:
michaelessiet
2025-01-17 17:12:34 +01:00
parent 79fe5b0cb4
commit 0c840d9bcb
12 changed files with 625 additions and 1 deletions

View File

@@ -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
View 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;
};