From aea44939b63dc2c91c6b790d402cfd2ba23ee810 Mon Sep 17 00:00:00 2001 From: Finn Date: Thu, 14 Mar 2024 14:10:23 +0000 Subject: [PATCH 1/5] added tooltip --- components/Positions.tsx | 98 ++++++++++++++++++++++++++++++----- components/shared/Tooltip.tsx | 4 +- 2 files changed, 88 insertions(+), 14 deletions(-) diff --git a/components/Positions.tsx b/components/Positions.tsx index 7114d7a..43ee41f 100644 --- a/components/Positions.tsx +++ b/components/Positions.tsx @@ -15,6 +15,7 @@ import { } from '@blockworks-foundation/mango-v4' import useBankRates from 'hooks/useBankRates' import usePositions from 'hooks/usePositions' +import Tooltip from './shared/Tooltip' const set = mangoStore.getState().set @@ -55,9 +56,8 @@ const Positions = ({ return ( <>
-

{`You have ${numberOfPositions} active position${ - numberOfPositions !== 1 ? 's' : '' - }`}

+

{`You have ${numberOfPositions} active position${numberOfPositions !== 1 ? 's' : '' + }`}

setShowInactivePositions(checked)} @@ -136,11 +136,12 @@ const PositionItem = ({ return [liqRatio, liqPriceChangePercentage.toFixed(2)] }, [bank, borrowBalance, borrowBank, stakeBalance]) - const { financialMetrics, stakeBankDepositRate } = useBankRates( + const { financialMetrics, stakeBankDepositRate, borrowBankBorrowRate } = useBankRates( bank.name, leverage, ) + const APY_Daily_Compound = Math.pow(1 + Number(stakeBankDepositRate) / 365, 365) - 1; const uiRate = bank.name == 'USDC' ? APY_Daily_Compound * 100 : financialMetrics.APY @@ -187,20 +188,93 @@ const PositionItem = ({

Est. APY

- - % - + {bank.name !== 'USDC' ? + +

+ Rates and Fees +

+
+
+

+ {formatTokenSymbol(bank.name)} Yield APY +

+ + {financialMetrics.collectedReturnsAPY > 0.01 + ? '+' + : ''} + + % + +
+
+

+ {formatTokenSymbol(bank.name)} Collateral Fee + APY +

+ 0.01 + ? 'text-th-error' + : 'text-th-bkg-4' + }`} + > + {financialMetrics?.collateralFeeAPY > 0.01 + ? '-' + : ''} + + % + +
+ {borrowBank ? ( + <> +
+

{`${borrowBank?.name} Borrow APY`}

+ 0.01 + ? 'text-th-error' + : 'text-th-bkg-4' + }`} + > + - + + % + +
+ + ) : null} +
+ +
}> + + % + + + : + <> + + % + + + }

Total Earned

= 0 + className={`text-xl font-bold ${!stakeBalance + ? 'text-th-fgd-4' + : pnl >= 0 ? 'text-th-success' : 'text-th-error' - }`} + }`} > {stakeBalance || pnl ? ( diff --git a/components/shared/Tooltip.tsx b/components/shared/Tooltip.tsx index 983477b..91dfadf 100644 --- a/components/shared/Tooltip.tsx +++ b/components/shared/Tooltip.tsx @@ -35,8 +35,8 @@ const Tooltip = ({ content={ content ? (
{content}
From a312eccbf90db733270224ac80172e00b27300cd Mon Sep 17 00:00:00 2001 From: Finn Date: Thu, 14 Mar 2024 15:16:36 +0000 Subject: [PATCH 2/5] fix of rate --- apis/birdeye/helpers.ts | 200 ++++++++++++++++++++++++++++++++++++++ apis/birdeye/streaming.ts | 120 +++++++++++++++++++++++ hooks/useStakeRates.ts | 38 +++----- types/index.ts | 6 ++ utils/constants.ts | 4 +- 5 files changed, 344 insertions(+), 24 deletions(-) create mode 100644 apis/birdeye/helpers.ts create mode 100644 apis/birdeye/streaming.ts diff --git a/apis/birdeye/helpers.ts b/apis/birdeye/helpers.ts new file mode 100644 index 0000000..fdc8123 --- /dev/null +++ b/apis/birdeye/helpers.ts @@ -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 = { + '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 = { + 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 [] + } +} diff --git a/apis/birdeye/streaming.ts b/apis/birdeye/streaming.ts new file mode 100644 index 0000000..f919983 --- /dev/null +++ b/apis/birdeye/streaming.ts @@ -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 +} diff --git a/hooks/useStakeRates.ts b/hooks/useStakeRates.ts index 8fff15e..b388d41 100644 --- a/hooks/useStakeRates.ts +++ b/hooks/useStakeRates.ts @@ -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) => 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 = {} 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 } @@ -47,6 +38,9 @@ const fetchRates = async () => { if (lidoRange) { rateData.stsol = calcYield(lidoRange)?.apy } + + */ + return rateData } catch (e) { return {} diff --git a/types/index.ts b/types/index.ts index 0bab8b7..4cb1921 100644 --- a/types/index.ts +++ b/types/index.ts @@ -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[][] diff --git a/utils/constants.ts b/utils/constants.ts index 4a6f211..4326795 100644 --- a/utils/constants.ts +++ b/utils/constants.ts @@ -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, From 629dbce433d7ad9b911ab9adc878a25f385d3032 Mon Sep 17 00:00:00 2001 From: Finn Date: Thu, 14 Mar 2024 15:36:12 +0000 Subject: [PATCH 3/5] shift one 4h back --- hooks/useStakeRates.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hooks/useStakeRates.ts b/hooks/useStakeRates.ts index b388d41..4998071 100644 --- a/hooks/useStakeRates.ts +++ b/hooks/useStakeRates.ts @@ -20,7 +20,7 @@ const fetchRates = async () => { const rateData: Record = {} rateData.jlp = - (12 * (jlpPrices[jlpPrices.length - 1].price - jlpPrices[0].price)) / + (12 * (jlpPrices[jlpPrices.length - 2].price - jlpPrices[0].price)) / jlpPrices[0].price From 3acd75ba7d1fe91db604041ab6c40cb5aa04fd9b Mon Sep 17 00:00:00 2001 From: saml33 Date: Fri, 15 Mar 2024 10:14:29 +1100 Subject: [PATCH 4/5] tweak styles --- components/Positions.tsx | 159 +++++++++++++++++----------------- components/shared/Tooltip.tsx | 4 +- 2 files changed, 82 insertions(+), 81 deletions(-) diff --git a/components/Positions.tsx b/components/Positions.tsx index 43ee41f..08c5b29 100644 --- a/components/Positions.tsx +++ b/components/Positions.tsx @@ -56,8 +56,9 @@ const Positions = ({ return ( <>
-

{`You have ${numberOfPositions} active position${numberOfPositions !== 1 ? 's' : '' - }`}

+

{`You have ${numberOfPositions} active position${ + numberOfPositions !== 1 ? 's' : '' + }`}

setShowInactivePositions(checked)} @@ -136,14 +137,13 @@ const PositionItem = ({ return [liqRatio, liqPriceChangePercentage.toFixed(2)] }, [bank, borrowBalance, borrowBank, stakeBalance]) - const { financialMetrics, stakeBankDepositRate, borrowBankBorrowRate } = useBankRates( - bank.name, - leverage, - ) + const { financialMetrics, stakeBankDepositRate, borrowBankBorrowRate } = + useBankRates(bank.name, leverage) - - const APY_Daily_Compound = Math.pow(1 + Number(stakeBankDepositRate) / 365, 365) - 1; - const uiRate = bank.name == 'USDC' ? APY_Daily_Compound * 100 : financialMetrics.APY + const APY_Daily_Compound = + Math.pow(1 + Number(stakeBankDepositRate) / 365, 365) - 1 + const uiRate = + bank.name == 'USDC' ? APY_Daily_Compound * 100 : financialMetrics.APY return (
@@ -188,93 +188,94 @@ const PositionItem = ({

Est. APY

- {bank.name !== 'USDC' ? - -

- Rates and Fees -

-
-
-

- {formatTokenSymbol(bank.name)} Yield APY -

- - {financialMetrics.collectedReturnsAPY > 0.01 - ? '+' - : ''} - - % - -
-
-

- {formatTokenSymbol(bank.name)} Collateral Fee - APY -

- 0.01 - ? 'text-th-error' - : 'text-th-bkg-4' - }`} - > - {financialMetrics?.collateralFeeAPY > 0.01 - ? '-' - : ''} - - % - -
- {borrowBank ? ( - <> -
-

{`${borrowBank?.name} Borrow APY`}

- 0.01 - ? 'text-th-error' - : 'text-th-bkg-4' - }`} - > - - + {bank.name !== 'USDC' ? ( +
+ +
+
+

+ {formatTokenSymbol(bank.name)} Yield APY +

+ + {financialMetrics.collectedReturnsAPY > 0.01 + ? '+' + : ''} %
- - ) : null} -
- -
}> - - % - - - : +
+

+ {formatTokenSymbol(bank.name)} Collateral Fee APY +

+ 0.01 + ? 'text-th-error' + : 'text-th-bkg-4' + }`} + > + {financialMetrics?.collateralFeeAPY > 0.01 ? '-' : ''} + + % + +
+ {borrowBank ? ( + <> +
+

{`${borrowBank?.name} Borrow APY`}

+ 0.01 + ? 'text-th-error' + : 'text-th-bkg-4' + }`} + > + - + + % + +
+ + ) : null} +
+ + } + > + + % + + +
+ ) : ( <> % - } + )}

Total Earned

= 0 + className={`text-xl font-bold ${ + !stakeBalance + ? 'text-th-fgd-4' + : pnl >= 0 ? 'text-th-success' : 'text-th-error' - }`} + }`} > {stakeBalance || pnl ? ( diff --git a/components/shared/Tooltip.tsx b/components/shared/Tooltip.tsx index 91dfadf..0f543b3 100644 --- a/components/shared/Tooltip.tsx +++ b/components/shared/Tooltip.tsx @@ -35,7 +35,7 @@ const Tooltip = ({ content={ content ? (
{content} @@ -59,7 +59,7 @@ const Content = ({ } & HTMLAttributes) => { return (
{children}
From 97fff7707c436067022a50a0def090d8ab9300ac Mon Sep 17 00:00:00 2001 From: saml33 Date: Fri, 15 Mar 2024 21:38:55 +1100 Subject: [PATCH 5/5] fix usdc unstake amount --- components/UnstakeForm.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/components/UnstakeForm.tsx b/components/UnstakeForm.tsx index db0bc09..0e836a8 100644 --- a/components/UnstakeForm.tsx +++ b/components/UnstakeForm.tsx @@ -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 }