UNPKG

@drift-labs/sdk-browser

Version:
526 lines (486 loc) 13.2 kB
import { AddressLookupTableAccount, LAMPORTS_PER_SOL, PublicKey, TransactionInstruction, } from '@solana/web3.js'; import { JupiterClient, QuoteResponse } from '../jupiter/jupiterClient'; import { DriftClient } from '../driftClient'; import { getMarinadeFinanceProgram, getMarinadeMSolPrice } from '../marinade'; import { BN } from '@coral-xyz/anchor'; import { User } from '../user'; import { DepositRecord, isVariant } from '../types'; import { LAMPORTS_PRECISION, ZERO } from '../constants/numericConstants'; import fetch from 'node-fetch'; import { checkSameDate } from './utils'; export type BSOL_STATS_API_RESPONSE = { success: boolean; stats?: { conversion: { bsol_to_sol: number; sol_to_bsol: number; }; apy: { base: number; blze: number; total: number; lending: number; liquidity: number; }; }; }; export type BSOL_EMISSIONS_API_RESPONSE = { success: boolean; emissions?: { lend: number; }; }; export async function fetchBSolMetrics() { return await fetch('https://stake.solblaze.org/api/v1/stats'); } export async function fetchBSolDriftEmissions() { return await fetch('https://stake.solblaze.org/api/v1/drift_emissions'); } export async function findBestSuperStakeIxs({ marketIndex, amount, jupiterClient, driftClient, userAccountPublicKey, price, forceMarinade, onlyDirectRoutes, jupiterQuote, }: { marketIndex: number; amount: BN; jupiterClient: JupiterClient; driftClient: DriftClient; price?: number; userAccountPublicKey?: PublicKey; forceMarinade?: boolean; onlyDirectRoutes?: boolean; jupiterQuote?: QuoteResponse; }): Promise<{ ixs: TransactionInstruction[]; lookupTables: AddressLookupTableAccount[]; method: 'jupiter' | 'marinade'; price?: number; }> { if (marketIndex === 2) { return findBestMSolSuperStakeIxs({ amount, jupiterClient, driftClient, userAccountPublicKey, price, forceMarinade, onlyDirectRoutes, jupiterQuote, }); } else if (marketIndex === 6) { return findBestJitoSolSuperStakeIxs({ amount, jupiterClient, driftClient, userAccountPublicKey, onlyDirectRoutes, jupiterQuote, }); } else if (marketIndex === 8) { return findBestLstSuperStakeIxs({ amount, lstMint: driftClient.getSpotMarketAccount(8).mint, lstMarketIndex: 8, jupiterClient, driftClient, userAccountPublicKey, onlyDirectRoutes, jupiterQuote, }); } else { throw new Error(`Unsupported superstake market index: ${marketIndex}`); } } export async function findBestMSolSuperStakeIxs({ amount, jupiterClient, driftClient, userAccountPublicKey, price, forceMarinade, onlyDirectRoutes, jupiterQuote, }: { amount: BN; jupiterClient: JupiterClient; driftClient: DriftClient; price?: number; userAccountPublicKey?: PublicKey; forceMarinade?: boolean; onlyDirectRoutes?: boolean; jupiterQuote?: QuoteResponse; }): Promise<{ ixs: TransactionInstruction[]; lookupTables: AddressLookupTableAccount[]; method: 'jupiter' | 'marinade'; price: number; }> { if (!price) { const marinadeProgram = getMarinadeFinanceProgram(driftClient.provider); price = await getMarinadeMSolPrice(marinadeProgram); } const solSpotMarketAccount = driftClient.getSpotMarketAccount(1); const mSolSpotMarketAccount = driftClient.getSpotMarketAccount(2); let jupiterPrice: number; let quote = jupiterQuote; if (!jupiterQuote) { try { const fetchedQuote = await jupiterClient.getQuote({ inputMint: solSpotMarketAccount.mint, outputMint: mSolSpotMarketAccount.mint, amount, slippageBps: 1000, onlyDirectRoutes, }); jupiterPrice = +quote.outAmount / +quote.inAmount; quote = fetchedQuote; } catch (e) { console.error('Error getting jupiter price', e); } } if (!jupiterPrice || price <= jupiterPrice || forceMarinade) { const ixs = await driftClient.getStakeForMSOLIx({ amount, userAccountPublicKey, }); return { method: 'marinade', ixs, lookupTables: [], price: price, }; } else { const { ixs, lookupTables } = await driftClient.getJupiterSwapIxV6({ inMarketIndex: 1, outMarketIndex: 2, jupiterClient, amount, userAccountPublicKey, onlyDirectRoutes, quote, }); return { method: 'jupiter', ixs, lookupTables, price: jupiterPrice, }; } } export async function findBestJitoSolSuperStakeIxs({ amount, jupiterClient, driftClient, userAccountPublicKey, onlyDirectRoutes, jupiterQuote, }: { amount: BN; jupiterClient: JupiterClient; driftClient: DriftClient; userAccountPublicKey?: PublicKey; onlyDirectRoutes?: boolean; jupiterQuote?: QuoteResponse; }): Promise<{ ixs: TransactionInstruction[]; lookupTables: AddressLookupTableAccount[]; method: 'jupiter' | 'marinade'; price?: number; }> { return await findBestLstSuperStakeIxs({ amount, jupiterClient, driftClient, userAccountPublicKey, onlyDirectRoutes, lstMint: driftClient.getSpotMarketAccount(6).mint, lstMarketIndex: 6, jupiterQuote, }); } /** * Finds best swap instructions for a generic lstMint * * Without doing any extra steps like checking if you can get a better rate by staking directly with that LST platform */ export async function findBestLstSuperStakeIxs({ amount, jupiterClient, driftClient, userAccountPublicKey, onlyDirectRoutes, lstMarketIndex, jupiterQuote, }: { amount: BN; lstMint: PublicKey; lstMarketIndex: number; jupiterClient: JupiterClient; driftClient: DriftClient; userAccountPublicKey?: PublicKey; onlyDirectRoutes?: boolean; jupiterQuote?: QuoteResponse; }): Promise<{ ixs: TransactionInstruction[]; lookupTables: AddressLookupTableAccount[]; method: 'jupiter' | 'marinade'; }> { const { ixs, lookupTables } = await driftClient.getJupiterSwapIxV6({ inMarketIndex: 1, outMarketIndex: lstMarketIndex, jupiterClient, amount, userAccountPublicKey, onlyDirectRoutes, quote: jupiterQuote, }); return { method: 'jupiter', ixs, lookupTables, // price: jupiterPrice, }; } export type JITO_SOL_METRICS_ENDPOINT_RESPONSE = { tvl: { // TVL in SOL, BN data: number; date: string; }[]; supply: { // jitoSOL supply data: number; date: string; }[]; apy: { data: number; date: string; }[]; }; /** * Removes hours, minutes, seconds from a date, and returns the ISO string value (with milliseconds trimmed from the output (required by Jito API)) * @param inDate * @returns */ const getNormalizedDateString = (inDate: Date) => { const date = new Date(inDate.getTime()); date.setUTCHours(0, 0, 0, 0); return date.toISOString().slice(0, 19) + 'Z'; }; const get30DAgo = () => { const date = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000); return date; }; export async function fetchJitoSolMetrics() { const res = await fetch( 'https://kobe.mainnet.jito.network/api/v1/stake_pool_stats', { headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ bucket_type: 'Daily', range_filter: { start: getNormalizedDateString(get30DAgo()), end: getNormalizedDateString(new Date()), }, sort_by: { order: 'Asc', field: 'BlockTime', }, }), method: 'POST', } ); const data: JITO_SOL_METRICS_ENDPOINT_RESPONSE = await res.json(); return data; } export type MSOL_METRICS_ENDPOINT_RESPONSE = { total_active_balance: number; available_reserve_balance: number; emergency_cooling_down: number; tvl_sol: number; msol_directed_stake_sol: number; msol_directed_stake_msol: number; mnde_total_supply: number; mnde_circulating_supply: number; validators_count: number; stake_accounts: number; staking_sol_cap: number; m_sol_price: number; avg_staking_apy: number; msol_price_apy_14d: number; msol_price_apy_30d: number; msol_price_apy_90d: number; msol_price_apy_365d: number; reserve_pda: number; treasury_m_sol_amount: number; m_sol_mint_supply: number; m_sol_supply_state: number; liq_pool_sol: number; liq_pool_m_sol: number; liq_pool_value: number; liq_pool_token_supply: number; liq_pool_token_price: number; liq_pool_target: number; liq_pool_min_fee: number; liq_pool_max_fee: number; liq_pool_current_fee: number; liq_pool_treasury_cut: number; liq_pool_cap: number; total_cooling_down: number; last_stake_delta_epoch: number; circulating_ticket_count: number; circulating_ticket_balance: number; reward_fee_bp: number; lido_staking: number; lido_st_sol_price: number; lido_stsol_price_apy_14d: number; lido_stsol_price_apy_30d: number; lido_stsol_price_apy_90d: number; lido_stsol_price_apy_365d: number; stake_delta: number; bot_balance: number; treasury_farm_claim_mnde_balance: number; last_3_epochs_avg_duration_hs: number; mnde_votes_validators: number; }; export const fetchMSolMetrics = async () => { const res = await fetch('https://api2.marinade.finance/metrics_json'); const data: MSOL_METRICS_ENDPOINT_RESPONSE = await res.json(); return data; }; const getJitoSolHistoricalPriceMap = async (timestamps: number[]) => { try { const data = await fetchJitoSolMetrics(); const jitoSolHistoricalPriceMap = new Map<number, number>(); const jitoSolHistoricalPriceInSol = []; for (let i = 0; i < data.supply.length; i++) { const priceInSol = data.tvl[i].data / 10 ** 9 / data.supply[i].data; jitoSolHistoricalPriceInSol.push({ price: priceInSol, ts: data.tvl[i].date, }); } for (const timestamp of timestamps) { const date = new Date(timestamp * 1000); const dateString = date.toISOString(); const price = jitoSolHistoricalPriceInSol.find((p) => checkSameDate(p.ts, dateString) ); if (price) { jitoSolHistoricalPriceMap.set(timestamp, price.price); } } return jitoSolHistoricalPriceMap; } catch (err) { console.error(err); return undefined; } }; export async function calculateSolEarned({ marketIndex, user, depositRecords, }: { marketIndex: number; user: User; depositRecords: DepositRecord[]; }): Promise<BN> { const now = Date.now() / 1000; const timestamps: number[] = [ now, ...depositRecords .filter((r) => r.marketIndex === marketIndex) .map((r) => r.ts.toNumber()), ]; let lstRatios = new Map<number, number>(); const getMsolPrice = async (timestamp) => { const date = new Date(timestamp * 1000); // Convert Unix timestamp to milliseconds const swaggerApiDateTime = date.toISOString(); // Format date as swagger API date-time const url = `https://api.marinade.finance/msol/price_sol?time=${swaggerApiDateTime}`; const response = await fetch(url); if (response.status === 200) { const data = await response.json(); lstRatios.set(timestamp, data); } }; const getBSolPrice = async (timestamps: number[]) => { // Currently there's only one bSOL price, no timestamped data // So just use the same price for every timestamp for now const response = await fetchBSolMetrics(); if (response.status === 200) { const data = (await response.json()) as BSOL_STATS_API_RESPONSE; const bSolRatio = data?.stats?.conversion?.bsol_to_sol; if (bSolRatio) { timestamps.forEach((timestamp) => lstRatios.set(timestamp, bSolRatio)); } } }; // This block kind of assumes the record are all from the same market // Otherwise the following code that checks the record.marketIndex would break if (marketIndex === 2) { await Promise.all(timestamps.map(getMsolPrice)); } else if (marketIndex === 6) { lstRatios = await getJitoSolHistoricalPriceMap(timestamps); } else if (marketIndex === 8) { await getBSolPrice(timestamps); } let solEarned = ZERO; for (const record of depositRecords) { if (record.marketIndex === 1) { if (isVariant(record.direction, 'deposit')) { solEarned = solEarned.sub(record.amount); } else { solEarned = solEarned.add(record.amount); } } else if ( record.marketIndex === 2 || record.marketIndex === 6 || record.marketIndex === 8 ) { const lstRatio = lstRatios.get(record.ts.toNumber()); const lstRatioBN = new BN(lstRatio * LAMPORTS_PER_SOL); const solAmount = record.amount.mul(lstRatioBN).div(LAMPORTS_PRECISION); if (isVariant(record.direction, 'deposit')) { solEarned = solEarned.sub(solAmount); } else { solEarned = solEarned.add(solAmount); } } } const currentLstTokenAmount = await user.getTokenAmount(marketIndex); const currentLstRatio = lstRatios.get(now); const currentLstRatioBN = new BN(currentLstRatio * LAMPORTS_PER_SOL); solEarned = solEarned.add( currentLstTokenAmount.mul(currentLstRatioBN).div(LAMPORTS_PRECISION) ); const currentSOLTokenAmount = await user.getTokenAmount(1); solEarned = solEarned.add(currentSOLTokenAmount); return solEarned; } // calculate estimated liquidation price (in LST/SOL) based on target amounts export function calculateEstimatedSuperStakeLiquidationPrice( lstDepositAmount: number, lstMaintenanceAssetWeight: number, solBorrowAmount: number, solMaintenanceLiabilityWeight: number, lstPriceRatio: number ): number { const liquidationDivergence = (solMaintenanceLiabilityWeight * solBorrowAmount) / (lstMaintenanceAssetWeight * lstDepositAmount * lstPriceRatio); const liquidationPrice = lstPriceRatio * liquidationDivergence; return liquidationPrice; }