Skip to content

Commit

Permalink
fix of rate
Browse files Browse the repository at this point in the history
  • Loading branch information
FinnCF committed Mar 14, 2024
1 parent 9eabc31 commit a312ecc
Show file tree
Hide file tree
Showing 5 changed files with 344 additions and 24 deletions.
200 changes: 200 additions & 0 deletions apis/birdeye/helpers.ts
Original file line number Diff line number Diff line change
@@ -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 []
}
}
120 changes: 120 additions & 0 deletions apis/birdeye/streaming.ts
Original file line number Diff line number Diff line change
@@ -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
}
38 changes: 16 additions & 22 deletions hooks/useStakeRates.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,31 @@
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 - 1].price - jlpPrices[0].price)) /
jlpPrices[0].price


/*
if (msolRange) {
rateData.msol = calcYield(msolRange)?.apy
}
Expand All @@ -47,6 +38,9 @@ const fetchRates = async () => {
if (lidoRange) {
rateData.stsol = calcYield(lidoRange)?.apy
}
*/

return rateData
} catch (e) {
return {}
Expand Down
6 changes: 6 additions & 0 deletions types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[][]
Expand Down
Loading

0 comments on commit a312ecc

Please sign in to comment.