merge main
This commit is contained in:
commit
ad83c20215
|
@ -0,0 +1,200 @@
|
|||
import Decimal from 'decimal.js'
|
||||
import { BirdeyePriceResponse } from 'types'
|
||||
import { DAILY_SECONDS } from 'utils/constants'
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
export const NEXT_PUBLIC_BIRDEYE_API_KEY =
|
||||
process.env.NEXT_PUBLIC_BIRDEYE_API_KEY ||
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2NzM0NTE4MDF9.KTEqB1hrmZTMzk19rZNx9aesh2bIHj98Cb8sg5Ikz-Y'
|
||||
|
||||
export const API_URL = 'https://public-api.birdeye.so/'
|
||||
|
||||
export const socketUrl = `wss://public-api.birdeye.so/socket?x-api-key=${NEXT_PUBLIC_BIRDEYE_API_KEY}`
|
||||
|
||||
// Make requests to Birdeye API
|
||||
export async function makeApiRequest(path: string) {
|
||||
const response = await fetch(`${API_URL}${path}`, {
|
||||
headers: {
|
||||
'X-API-KEY': NEXT_PUBLIC_BIRDEYE_API_KEY,
|
||||
},
|
||||
})
|
||||
return response.json()
|
||||
}
|
||||
|
||||
const RESOLUTION_MAPPING: Record<string, string> = {
|
||||
'1': '1m',
|
||||
'3': '3m',
|
||||
'5': '5m',
|
||||
'15': '15m',
|
||||
'30': '30m',
|
||||
'60': '1H',
|
||||
'120': '2H',
|
||||
'240': '4H',
|
||||
'1D': '1D',
|
||||
'1W': '1W',
|
||||
}
|
||||
|
||||
export function parseResolution(resolution: string) {
|
||||
if (!resolution || !RESOLUTION_MAPPING[resolution])
|
||||
return RESOLUTION_MAPPING[0]
|
||||
|
||||
return RESOLUTION_MAPPING[resolution]
|
||||
}
|
||||
|
||||
export function getNextBarTime(lastBar: any, resolution = '1D') {
|
||||
if (!lastBar) return
|
||||
|
||||
const lastCharacter = resolution.slice(-1)
|
||||
let nextBarTime
|
||||
|
||||
switch (true) {
|
||||
case lastCharacter === 'W':
|
||||
nextBarTime = 7 * 24 * 60 * 60 * 1000 + lastBar.time
|
||||
break
|
||||
|
||||
case lastCharacter === 'D':
|
||||
nextBarTime = 1 * 24 * 60 * 60 * 1000 + lastBar.time
|
||||
break
|
||||
|
||||
default:
|
||||
nextBarTime = 1 * 60 * 1000 + lastBar.time
|
||||
break
|
||||
}
|
||||
|
||||
return nextBarTime
|
||||
}
|
||||
|
||||
export const SUBSCRIPT_NUMBER_MAP: Record<number, string> = {
|
||||
4: '₄',
|
||||
5: '₅',
|
||||
6: '₆',
|
||||
7: '₇',
|
||||
8: '₈',
|
||||
9: '₉',
|
||||
10: '₁₀',
|
||||
11: '₁₁',
|
||||
12: '₁₂',
|
||||
13: '₁₃',
|
||||
14: '₁₄',
|
||||
15: '₁₅',
|
||||
}
|
||||
|
||||
export const calcPricePrecision = (num: number | string) => {
|
||||
if (!num) return 8
|
||||
|
||||
switch (true) {
|
||||
case Math.abs(+num) < 0.00000000001:
|
||||
return 16
|
||||
|
||||
case Math.abs(+num) < 0.000000001:
|
||||
return 14
|
||||
|
||||
case Math.abs(+num) < 0.0000001:
|
||||
return 12
|
||||
|
||||
case Math.abs(+num) < 0.00001:
|
||||
return 10
|
||||
|
||||
case Math.abs(+num) < 0.05:
|
||||
return 6
|
||||
|
||||
case Math.abs(+num) < 1:
|
||||
return 4
|
||||
|
||||
case Math.abs(+num) < 20:
|
||||
return 3
|
||||
|
||||
default:
|
||||
return 2
|
||||
}
|
||||
}
|
||||
|
||||
export const formatPrice = (
|
||||
num: number,
|
||||
precision?: number,
|
||||
gr0 = true,
|
||||
): string => {
|
||||
if (!num) {
|
||||
return num.toString()
|
||||
}
|
||||
|
||||
if (!precision) {
|
||||
precision = calcPricePrecision(+num)
|
||||
}
|
||||
|
||||
let formated: string = new Decimal(num).toFixed(precision)
|
||||
|
||||
if (formated.match(/^0\.[0]+$/g)) {
|
||||
formated = formated.replace(/\.[0]+$/g, '')
|
||||
}
|
||||
|
||||
if (gr0 && formated.match(/\.0{4,15}[1-9]+/g)) {
|
||||
const match = formated.match(/\.0{4,15}/g)
|
||||
if (!match) return ''
|
||||
const matchString: string = match[0].slice(1)
|
||||
formated = formated.replace(
|
||||
/\.0{4,15}/g,
|
||||
`.0${SUBSCRIPT_NUMBER_MAP[matchString.length]}`,
|
||||
)
|
||||
}
|
||||
|
||||
return formated
|
||||
}
|
||||
|
||||
export type SwapChartDataItem = {
|
||||
time: number
|
||||
price: number
|
||||
inputTokenPrice: number
|
||||
outputTokenPrice: number
|
||||
}
|
||||
|
||||
export const fetchSwapChartPrices = async (
|
||||
inputMint: string | undefined,
|
||||
outputMint: string | undefined,
|
||||
daysToShow: string,
|
||||
) => {
|
||||
if (!inputMint || !outputMint) return []
|
||||
const interval = daysToShow === '1' ? '30m' : daysToShow === '7' ? '1H' : '4H'
|
||||
const queryEnd = Math.floor(Date.now() / 1000)
|
||||
const queryStart = queryEnd - parseInt(daysToShow) * DAILY_SECONDS
|
||||
const inputQuery = `defi/history_price?address=${inputMint}&address_type=token&type=${interval}&time_from=${queryStart}&time_to=${queryEnd}`
|
||||
const outputQuery = `defi/history_price?address=${outputMint}&address_type=token&type=${interval}&time_from=${queryStart}&time_to=${queryEnd}`
|
||||
try {
|
||||
const [inputResponse, outputResponse] = await Promise.all([
|
||||
makeApiRequest(inputQuery),
|
||||
makeApiRequest(outputQuery),
|
||||
])
|
||||
|
||||
if (
|
||||
inputResponse.success &&
|
||||
inputResponse?.data?.items?.length &&
|
||||
outputResponse.success &&
|
||||
outputResponse?.data?.items?.length
|
||||
) {
|
||||
const parsedData: SwapChartDataItem[] = []
|
||||
const inputData = inputResponse.data.items
|
||||
const outputData = outputResponse.data.items
|
||||
|
||||
for (const item of inputData) {
|
||||
const outputDataItem = outputData.find(
|
||||
(data: BirdeyePriceResponse) => data.unixTime === item.unixTime,
|
||||
)
|
||||
|
||||
const curentTimestamp = Date.now() / 1000
|
||||
|
||||
if (outputDataItem && item.unixTime <= curentTimestamp) {
|
||||
parsedData.push({
|
||||
time: Math.floor(item.unixTime * 1000),
|
||||
price: item.value / outputDataItem.value,
|
||||
inputTokenPrice: item.value,
|
||||
outputTokenPrice: outputDataItem.value,
|
||||
})
|
||||
}
|
||||
}
|
||||
return parsedData
|
||||
} else return []
|
||||
} catch (e) {
|
||||
console.log('failed to fetch swap chart data from birdeye', e)
|
||||
return []
|
||||
}
|
||||
}
|
|
@ -0,0 +1,120 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { parseResolution, getNextBarTime, socketUrl } from './helpers'
|
||||
|
||||
let subscriptionItem: any = {}
|
||||
|
||||
// Create WebSocket connection.
|
||||
const socket = new WebSocket(socketUrl, 'echo-protocol')
|
||||
|
||||
// Connection opened
|
||||
socket.addEventListener('open', (_event) => {
|
||||
console.log('[socket] Connected birdeye')
|
||||
})
|
||||
|
||||
// Listen for messages
|
||||
socket.addEventListener('message', (msg) => {
|
||||
const data = JSON.parse(msg.data)
|
||||
|
||||
if (data.type !== 'BASE_QUOTE_PRICE_DATA') return console.warn(data)
|
||||
|
||||
const currTime = data.data.unixTime * 1000
|
||||
const lastBar = subscriptionItem.lastBar
|
||||
|
||||
if (
|
||||
data.data.baseAddress !== subscriptionItem.baseAddress ||
|
||||
data.data.quoteAddress !== subscriptionItem.quoteAddress
|
||||
)
|
||||
return
|
||||
|
||||
const resolution = subscriptionItem.resolution
|
||||
const nextBarTime = getNextBarTime(lastBar, resolution)
|
||||
|
||||
let bar
|
||||
if (currTime >= nextBarTime) {
|
||||
bar = {
|
||||
time: nextBarTime,
|
||||
open: data.data.o,
|
||||
high: data.data.h,
|
||||
low: data.data.l,
|
||||
close: data.data.c,
|
||||
volume: data.data.v,
|
||||
}
|
||||
} else {
|
||||
bar = {
|
||||
...lastBar,
|
||||
high: Math.max(lastBar.high, data.data.h),
|
||||
low: Math.min(lastBar.low, data.data.l),
|
||||
close: data.data.c,
|
||||
volume: data.data.v,
|
||||
}
|
||||
}
|
||||
|
||||
subscriptionItem.lastBar = bar
|
||||
subscriptionItem.callback(bar)
|
||||
})
|
||||
|
||||
export function subscribeOnStream(
|
||||
symbolInfo: any,
|
||||
resolution: any,
|
||||
onRealtimeCallback: any,
|
||||
subscriberUID: any,
|
||||
onResetCacheNeededCallback: any,
|
||||
lastBar: any,
|
||||
) {
|
||||
subscriptionItem = {
|
||||
resolution,
|
||||
lastBar,
|
||||
callback: onRealtimeCallback,
|
||||
baseAddress: symbolInfo.base_token,
|
||||
quoteAddress: symbolInfo.quote_token,
|
||||
}
|
||||
|
||||
const msg = {
|
||||
type: 'SUBSCRIBE_BASE_QUOTE_PRICE',
|
||||
data: {
|
||||
chartType: parseResolution(resolution),
|
||||
baseAddress: symbolInfo.base_token,
|
||||
quoteAddress: symbolInfo.quote_token,
|
||||
},
|
||||
}
|
||||
|
||||
if (!isOpen(socket)) {
|
||||
console.warn('Socket Closed')
|
||||
socket.addEventListener('open', (_event) => {
|
||||
if (!msg.data.baseAddress || msg.data.quoteAddress) return
|
||||
socket.send(JSON.stringify(msg))
|
||||
})
|
||||
return
|
||||
}
|
||||
console.warn('[subscribeBars birdeye]')
|
||||
if (msg.data.baseAddress && msg.data.quoteAddress) {
|
||||
socket.send(JSON.stringify(msg))
|
||||
}
|
||||
}
|
||||
|
||||
export function unsubscribeFromStream() {
|
||||
const msg = {
|
||||
type: 'UNSUBSCRIBE_BASE_QUOTE_PRICE',
|
||||
}
|
||||
|
||||
if (!isOpen(socket)) {
|
||||
console.warn('Socket Closed')
|
||||
return
|
||||
}
|
||||
console.warn('[unsubscribeBars birdeye]')
|
||||
socket.send(JSON.stringify(msg))
|
||||
}
|
||||
|
||||
export function closeSocket() {
|
||||
if (!isOpen(socket)) {
|
||||
console.warn('Socket Closed birdeye')
|
||||
return
|
||||
}
|
||||
console.warn('[closeSocket birdeye]')
|
||||
socket.close()
|
||||
}
|
||||
|
||||
export function isOpen(ws?: WebSocket) {
|
||||
const sock = ws || socket
|
||||
return sock.readyState === sock.OPEN
|
||||
}
|
|
@ -17,6 +17,7 @@ import useBankRates from 'hooks/useBankRates'
|
|||
import usePositions from 'hooks/usePositions'
|
||||
import { AdjustmentsHorizontalIcon } from '@heroicons/react/20/solid'
|
||||
import EditLeverageModal from './modals/EditLeverageModal'
|
||||
import Tooltip from './shared/Tooltip'
|
||||
|
||||
const set = mangoStore.getState().set
|
||||
|
||||
|
@ -139,10 +140,8 @@ const PositionItem = ({
|
|||
return [liqRatio, liqPriceChangePercentage.toFixed(2)]
|
||||
}, [bank, borrowBalance, borrowBank, stakeBalance])
|
||||
|
||||
const { financialMetrics, stakeBankDepositRate } = useBankRates(
|
||||
bank.name,
|
||||
leverage,
|
||||
)
|
||||
const { financialMetrics, stakeBankDepositRate, borrowBankBorrowRate } =
|
||||
useBankRates(bank.name, leverage)
|
||||
|
||||
const APY_Daily_Compound =
|
||||
Math.pow(1 + Number(stakeBankDepositRate) / 365, 365) - 1
|
||||
|
@ -192,9 +191,83 @@ const PositionItem = ({
|
|||
</div>
|
||||
<div>
|
||||
<p className="mb-1 text-th-fgd-4">Est. APY</p>
|
||||
{bank.name !== 'USDC' ? (
|
||||
<div className="w-max">
|
||||
<Tooltip
|
||||
content={
|
||||
<>
|
||||
<div className="space-y-2 md:px-3">
|
||||
<div className="flex justify-between gap-6">
|
||||
<p className="text-th-fgd-4">
|
||||
{formatTokenSymbol(bank.name)} Yield APY
|
||||
</p>
|
||||
<span className="font-bold text-th-success">
|
||||
{financialMetrics.collectedReturnsAPY > 0.01
|
||||
? '+'
|
||||
: ''}
|
||||
<FormatNumericValue
|
||||
value={financialMetrics.collectedReturnsAPY}
|
||||
decimals={2}
|
||||
/>
|
||||
%
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between gap-6">
|
||||
<p className="text-th-fgd-4">
|
||||
{formatTokenSymbol(bank.name)} Collateral Fee APY
|
||||
</p>
|
||||
<span
|
||||
className={`font-bold ${
|
||||
financialMetrics?.collateralFeeAPY > 0.01
|
||||
? 'text-th-error'
|
||||
: 'text-th-bkg-4'
|
||||
}`}
|
||||
>
|
||||
{financialMetrics?.collateralFeeAPY > 0.01 ? '-' : ''}
|
||||
<FormatNumericValue
|
||||
value={financialMetrics?.collateralFeeAPY?.toString()}
|
||||
decimals={2}
|
||||
/>
|
||||
%
|
||||
</span>
|
||||
</div>
|
||||
{borrowBank ? (
|
||||
<>
|
||||
<div className="flex justify-between gap-6">
|
||||
<p className="text-th-fgd-4">{`${borrowBank?.name} Borrow APY`}</p>
|
||||
<span
|
||||
className={`font-bold ${
|
||||
borrowBankBorrowRate > 0.01
|
||||
? 'text-th-error'
|
||||
: 'text-th-bkg-4'
|
||||
}`}
|
||||
>
|
||||
-
|
||||
<FormatNumericValue
|
||||
value={financialMetrics.borrowsAPY}
|
||||
decimals={2}
|
||||
/>
|
||||
%
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<span className="tooltip-underline text-xl font-bold text-th-fgd-1">
|
||||
<FormatNumericValue value={Number(uiRate)} decimals={2} />%
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<span className="text-xl font-bold text-th-fgd-1">
|
||||
<FormatNumericValue value={Number(uiRate)} decimals={2} />%
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<p className="mb-1 text-th-fgd-4">Total Earned</p>
|
||||
|
|
|
@ -104,16 +104,20 @@ function UnstakeForm({ token: selectedToken }: UnstakeFormProps) {
|
|||
const stakeBankAmount =
|
||||
mangoAccount && stakeBank && mangoAccount.getTokenBalance(stakeBank)
|
||||
|
||||
const borrowAmount =
|
||||
const borrowBankAmount =
|
||||
mangoAccount && borrowBank && mangoAccount.getTokenBalance(borrowBank)
|
||||
|
||||
const leverage = useMemo(() => {
|
||||
try {
|
||||
if (stakeBankAmount && borrowAmount) {
|
||||
if (
|
||||
stakeBankAmount &&
|
||||
borrowBankAmount &&
|
||||
borrowBankAmount.toNumber() < 0
|
||||
) {
|
||||
const lev = stakeBankAmount
|
||||
.div(
|
||||
stakeBankAmount.sub(
|
||||
borrowAmount.abs().div(stakeBank.getAssetPrice()),
|
||||
borrowBankAmount.abs().div(stakeBank.getAssetPrice()),
|
||||
),
|
||||
)
|
||||
.toNumber()
|
||||
|
@ -125,7 +129,7 @@ function UnstakeForm({ token: selectedToken }: UnstakeFormProps) {
|
|||
console.log(e)
|
||||
return 1
|
||||
}
|
||||
}, [stakeBankAmount, borrowAmount, stakeBank])
|
||||
}, [stakeBankAmount, borrowBankAmount, stakeBank])
|
||||
|
||||
const tokenMax = useMemo(() => {
|
||||
if (!stakeBank || !mangoAccount) return { maxAmount: 0.0, maxDecimals: 6 }
|
||||
|
|
|
@ -35,8 +35,8 @@ const Tooltip = ({
|
|||
content={
|
||||
content ? (
|
||||
<div
|
||||
className={`${themeData.fonts.body.variable} ${themeData.fonts.display.variable} font-sans font-sans rounded-md bg-th-bkg-2 p-3 font-body text-xs leading-4 text-th-fgd-3 outline-none focus:outline-none ${className}`}
|
||||
style={{ boxShadow: '0px 0px 8px 0px rgba(0,0,0,0.25)' }}
|
||||
className={`${themeData.fonts.body.variable} ${themeData.fonts.display.variable} font-sans rounded-xl border-2 border-th-fgd-1 bg-th-bkg-1 p-4 font-body text-xs leading-4 text-th-fgd-3 outline-none focus:outline-none ${className}`}
|
||||
style={{ boxShadow: '0px 0px 0px 0px rgba(0,0,0,0)' }}
|
||||
>
|
||||
{content}
|
||||
</div>
|
||||
|
@ -59,7 +59,7 @@ const Content = ({
|
|||
} & HTMLAttributes<HTMLDivElement>) => {
|
||||
return (
|
||||
<div
|
||||
className={`inline-block cursor-help border-b border-dashed border-th-fgd-3 border-opacity-20 hover:border-th-bkg-2 ${className}`}
|
||||
className={`inline-block cursor-help border-b border-dashed border-th-fgd-3 hover:border-th-bkg-2 ${className}`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
|
|
|
@ -1,39 +1,30 @@
|
|||
import { useQuery } from '@tanstack/react-query'
|
||||
import {
|
||||
fetchAndParsePricesCsv,
|
||||
getPriceRangeFromPeriod,
|
||||
calcYield,
|
||||
DATA_SOURCE,
|
||||
PERIOD,
|
||||
} from '@glitchful-dev/sol-apy-sdk'
|
||||
import { fetchSwapChartPrices } from 'apis/birdeye/helpers'
|
||||
import { STAKEABLE_TOKENS_DATA } from 'utils/constants'
|
||||
|
||||
const fetchRates = async () => {
|
||||
try {
|
||||
const [msolPrices, jitoPrices, bsolPrices, lidoPrices] = await Promise.all([
|
||||
fetchAndParsePricesCsv(DATA_SOURCE.MARINADE_CSV),
|
||||
fetchAndParsePricesCsv(DATA_SOURCE.JITO_CSV),
|
||||
fetchAndParsePricesCsv(DATA_SOURCE.SOLBLAZE_CSV),
|
||||
fetchAndParsePricesCsv(DATA_SOURCE.LIDO_CSV),
|
||||
const [jlpPrices] = await Promise.all([
|
||||
fetchSwapChartPrices(STAKEABLE_TOKENS_DATA[0]?.mint_address, STAKEABLE_TOKENS_DATA[1]?.mint_address, '30')
|
||||
])
|
||||
|
||||
const resp = await fetch(
|
||||
`https://api.coingecko.com/api/v3/coins/jupiter-perpetuals-liquidity-provider-token/market_chart?vs_currency=usd&days=30&interval=daily`,
|
||||
)
|
||||
const jlpPricesData = await resp.json()
|
||||
const jlpPricesPrice = jlpPricesData.prices.map(
|
||||
(priceAndTime: Array<number>) => priceAndTime[1],
|
||||
)
|
||||
|
||||
// may be null if the price range cannot be calculated
|
||||
/*
|
||||
|
||||
const msolRange = getPriceRangeFromPeriod(msolPrices, PERIOD.DAYS_30)
|
||||
const jitoRange = getPriceRangeFromPeriod(jitoPrices, PERIOD.DAYS_30)
|
||||
const bsolRange = getPriceRangeFromPeriod(bsolPrices, PERIOD.DAYS_30)
|
||||
const lidoRange = getPriceRangeFromPeriod(lidoPrices, PERIOD.DAYS_30)
|
||||
|
||||
*/
|
||||
|
||||
const rateData: Record<string, number> = {}
|
||||
rateData.jlp =
|
||||
(12 * (jlpPricesPrice[jlpPricesPrice.length - 2] - jlpPricesPrice[1])) /
|
||||
jlpPricesPrice[1]
|
||||
(12 * (jlpPrices[jlpPrices.length - 2].price - jlpPrices[0].price)) /
|
||||
jlpPrices[0].price
|
||||
|
||||
|
||||
/*
|
||||
|
||||
if (msolRange) {
|
||||
rateData.msol = calcYield(msolRange)?.apy
|
||||
|
@ -47,6 +38,9 @@ const fetchRates = async () => {
|
|||
if (lidoRange) {
|
||||
rateData.stsol = calcYield(lidoRange)?.apy
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
return rateData
|
||||
} catch (e) {
|
||||
return {}
|
||||
|
|
|
@ -10,6 +10,12 @@ import { Modify } from '@blockworks-foundation/mango-v4'
|
|||
import { Event } from '@project-serum/serum/lib/queue'
|
||||
import { PublicKey } from '@solana/web3.js'
|
||||
|
||||
export interface BirdeyePriceResponse {
|
||||
address: string
|
||||
unixTime: number
|
||||
value: number
|
||||
}
|
||||
|
||||
export type EmptyObject = { [K in keyof never]?: never }
|
||||
export interface OrderbookL2 {
|
||||
bids: number[][]
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
export const BORROW_TOKEN = 'USDC'
|
||||
|
||||
export const STAKEABLE_TOKENS_DATA = [
|
||||
{ name: 'JLP', id: 1, active: true },
|
||||
{ name: 'USDC', id: 0, active: true },
|
||||
{ name: 'JLP', id: 1, active: true, mint_address: '27G8MtK7VtTcCHkpASjSDdkWWYfoqT6ggEuKidVJidD4' },
|
||||
{ name: 'USDC', id: 0, active: true, mint_address: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v' },
|
||||
]
|
||||
export const STAKEABLE_TOKENS = STAKEABLE_TOKENS_DATA.filter(
|
||||
(d) => d.active,
|
||||
|
|
Loading…
Reference in New Issue