Merge pull request #19 from blockworks-foundation/changeJLPAPI

Fixing of JLP Rate
This commit is contained in:
Adrian Brzeziński 2024-03-14 21:48:17 +01:00 committed by GitHub
commit 78d3c91152
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 344 additions and 24 deletions

200
apis/birdeye/helpers.ts Normal file
View File

@ -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
apis/birdeye/streaming.ts Normal file
View File

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

View File

@ -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 - 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 {}

View File

@ -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[][]

View File

@ -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,