diff --git a/components/MangoProvider.tsx b/components/MangoProvider.tsx index 8afd42c2..674a9762 100644 --- a/components/MangoProvider.tsx +++ b/components/MangoProvider.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect } from 'react' +import { useEffect } from 'react' import mangoStore from '@store/mangoStore' import { Keypair, PublicKey } from '@solana/web3.js' import { useRouter } from 'next/router' @@ -15,29 +15,36 @@ const HydrateStore = () => { const { mangoAccountPk, mangoAccountAddress } = useMangoAccount() const connection = mangoStore((s) => s.connection) - const fetchData = useCallback(async () => { - await actions.fetchGroup() - }, []) - useEffect(() => { if (marketName && typeof marketName === 'string') { set((s) => { s.selectedMarket.name = marketName }) } - fetchData() + actions.fetchGroup() }, [marketName]) useInterval(() => { - fetchData() + actions.fetchGroup() }, 15000) + // refetches open orders every 30 seconds + // only the selected market's open orders are updated via websocket useInterval(() => { if (mangoAccountAddress) { actions.fetchOpenOrders() } }, 30000) + // refetch trade history and activity feed when switching accounts + useEffect(() => { + const actions = mangoStore.getState().actions + if (mangoAccountAddress) { + actions.fetchTradeHistory() + actions.fetchActivityFeed(mangoAccountAddress) + } + }, [mangoAccountAddress]) + // The websocket library solana/web3.js uses closes its websocket connection when the subscription list // is empty after opening its first time, preventing subsequent subscriptions from receiving responses. // This is a hack to prevent the list from every getting empty diff --git a/components/account/AccountChart.tsx b/components/account/AccountChart.tsx index 6c20bb7f..cd43b70d 100644 --- a/components/account/AccountChart.tsx +++ b/components/account/AccountChart.tsx @@ -1,8 +1,8 @@ import { useTranslation } from 'next-i18next' import { useMemo, useState } from 'react' -import { PerformanceDataItem } from '@store/mangoStore' import dynamic from 'next/dynamic' import { formatYAxis } from 'utils/formatting' +import { PerformanceDataItem } from 'types' const DetailedAreaChart = dynamic( () => import('@components/shared/DetailedAreaChart'), { ssr: false } diff --git a/components/account/AccountPage.tsx b/components/account/AccountPage.tsx index f1a5103b..0e8dace2 100644 --- a/components/account/AccountPage.tsx +++ b/components/account/AccountPage.tsx @@ -5,7 +5,7 @@ import { import { useTranslation } from 'next-i18next' import { useEffect, useMemo, useState } from 'react' import AccountActions from './AccountActions' -import mangoStore, { PerformanceDataItem } from '@store/mangoStore' +import mangoStore from '@store/mangoStore' import { formatCurrencyValue } from '../../utils/numbers' import FlipNumbers from 'react-flip-numbers' import dynamic from 'next/dynamic' @@ -43,6 +43,7 @@ import useMangoGroup from 'hooks/useMangoGroup' import PnlHistoryModal from '@components/modals/PnlHistoryModal' import FormatNumericValue from '@components/shared/FormatNumericValue' import HealthBar from './HealthBar' +import { PerformanceDataItem } from 'types' const AccountPage = () => { const { t } = useTranslation(['common', 'account']) diff --git a/components/account/ActivityFeed.tsx b/components/account/ActivityFeed.tsx index e83575aa..0a9d865f 100644 --- a/components/account/ActivityFeed.tsx +++ b/components/account/ActivityFeed.tsx @@ -2,7 +2,7 @@ import { EXPLORERS } from '@components/settings/PreferredExplorerSettings' import { IconButton } from '@components/shared/Button' import FormatNumericValue from '@components/shared/FormatNumericValue' import { ArrowLeftIcon } from '@heroicons/react/20/solid' -import mangoStore, { LiquidationFeedItem } from '@store/mangoStore' +import mangoStore from '@store/mangoStore' import dayjs from 'dayjs' import useLocalStorageState from 'hooks/useLocalStorageState' import { useTranslation } from 'next-i18next' @@ -10,6 +10,7 @@ import Image from 'next/legacy/image' import { useState } from 'react' import { PREFERRED_EXPLORER_KEY } from 'utils/constants' import ActivityFeedTable from './ActivityFeedTable' +import { LiquidationActivity } from 'types' const ActivityFeed = () => { const activityFeed = mangoStore((s) => s.activityFeed.feed) @@ -38,7 +39,7 @@ const ActivityDetails = ({ activity, setShowActivityDetail, }: { - activity: LiquidationFeedItem + activity: LiquidationActivity setShowActivityDetail: (x: any) => void }) => { const { t } = useTranslation(['common', 'activity', 'settings']) diff --git a/components/account/ActivityFeedTable.tsx b/components/account/ActivityFeedTable.tsx index de831ad0..628254e7 100644 --- a/components/account/ActivityFeedTable.tsx +++ b/components/account/ActivityFeedTable.tsx @@ -12,7 +12,7 @@ import { NoSymbolIcon, } from '@heroicons/react/20/solid' import { useWallet } from '@solana/wallet-adapter-react' -import mangoStore, { LiquidationFeedItem } from '@store/mangoStore' +import mangoStore from '@store/mangoStore' import dayjs from 'dayjs' import useLocalStorageState from 'hooks/useLocalStorageState' import useMangoAccount from 'hooks/useMangoAccount' @@ -20,6 +20,7 @@ import { useViewport } from 'hooks/useViewport' import { useTranslation } from 'next-i18next' import Image from 'next/legacy/image' import { Fragment, useCallback, useState } from 'react' +import { ActivityFeed, isLiquidationFeedItem, LiquidationActivity } from 'types' import { PAGINATION_PAGE_LENGTH, PREFERRED_EXPLORER_KEY } from 'utils/constants' import { formatNumericValue } from 'utils/numbers' import { breakpoints } from 'utils/theme' @@ -158,8 +159,8 @@ const ActivityFeedTable = ({ activityFeed, handleShowActivityDetails, }: { - activityFeed: any - handleShowActivityDetails: (x: LiquidationFeedItem) => void + activityFeed: ActivityFeed[] + handleShowActivityDetails: (x: LiquidationActivity) => void }) => { const { t } = useTranslation(['common', 'activity']) const { mangoAccountAddress } = useMangoAccount() @@ -211,7 +212,7 @@ const ActivityFeedTable = ({ - {activityFeed.map((activity: any, index: number) => { + {activityFeed.map((activity, index: number) => { const { activity_type, block_datetime } = activity const { signature } = activity.activity_details const isLiquidation = @@ -227,7 +228,7 @@ const ActivityFeedTable = ({ isLiquidation ? 'cursor-pointer' : '' }`} onClick={ - isLiquidation + isLiquidationFeedItem(activity) ? () => handleShowActivityDetails(activity) : undefined } diff --git a/components/modals/PnlHistoryModal.tsx b/components/modals/PnlHistoryModal.tsx index d7d420fa..d0027b03 100644 --- a/components/modals/PnlHistoryModal.tsx +++ b/components/modals/PnlHistoryModal.tsx @@ -1,6 +1,6 @@ import { ModalProps } from '../../types/modal' import Modal from '../shared/Modal' -import mangoStore, { PerformanceDataItem } from '@store/mangoStore' +import mangoStore from '@store/mangoStore' import { useTranslation } from 'next-i18next' import { useEffect, useMemo } from 'react' import useMangoAccount from 'hooks/useMangoAccount' @@ -8,6 +8,7 @@ import dayjs from 'dayjs' import Change from '@components/shared/Change' import SheenLoader from '@components/shared/SheenLoader' import { NoSymbolIcon } from '@heroicons/react/20/solid' +import { PerformanceDataItem } from 'types' interface PnlChange { time: string diff --git a/components/stats/MangoPerpStatsCharts.tsx b/components/stats/MangoPerpStatsCharts.tsx index 779b73e8..79fae1ce 100644 --- a/components/stats/MangoPerpStatsCharts.tsx +++ b/components/stats/MangoPerpStatsCharts.tsx @@ -1,7 +1,8 @@ import { useTranslation } from 'next-i18next' import { useMemo, useState } from 'react' import dynamic from 'next/dynamic' -import mangoStore, { PerpStatsItem } from '@store/mangoStore' +import mangoStore from '@store/mangoStore' +import { PerpStatsItem } from 'types' const DetailedAreaChart = dynamic( () => import('@components/shared/DetailedAreaChart'), { ssr: false } diff --git a/components/stats/PerpMarketsTable.tsx b/components/stats/PerpMarketsTable.tsx index f67565b6..3f5543d3 100644 --- a/components/stats/PerpMarketsTable.tsx +++ b/components/stats/PerpMarketsTable.tsx @@ -2,7 +2,7 @@ import { PerpMarket } from '@blockworks-foundation/mango-v4' import { useTranslation } from 'next-i18next' import { useTheme } from 'next-themes' import { useViewport } from '../../hooks/useViewport' -import mangoStore, { PerpStatsItem } from '@store/mangoStore' +import mangoStore from '@store/mangoStore' import { COLORS } from '../../styles/colors' import { breakpoints } from '../../utils/theme' import ContentBox from '../shared/ContentBox' @@ -16,6 +16,7 @@ import { ChevronRightIcon } from '@heroicons/react/20/solid' import FormatNumericValue from '@components/shared/FormatNumericValue' import { getDecimalCount } from 'utils/numbers' import Tooltip from '@components/shared/Tooltip' +import { PerpStatsItem } from 'types' const SimpleAreaChart = dynamic( () => import('@components/shared/SimpleAreaChart'), { ssr: false } diff --git a/components/stats/TotalDepositBorrowCharts.tsx b/components/stats/TotalDepositBorrowCharts.tsx index cd392c5a..a558671e 100644 --- a/components/stats/TotalDepositBorrowCharts.tsx +++ b/components/stats/TotalDepositBorrowCharts.tsx @@ -1,10 +1,11 @@ -import mangoStore, { TokenStatsItem } from '@store/mangoStore' +import mangoStore from '@store/mangoStore' import { useTranslation } from 'next-i18next' import dynamic from 'next/dynamic' import { useMemo, useState } from 'react' import dayjs from 'dayjs' import { formatYAxis } from 'utils/formatting' import useBanksWithBalances from 'hooks/useBanksWithBalances' +import { TokenStatsItem } from 'types' const DetailedAreaChart = dynamic( () => import('@components/shared/DetailedAreaChart'), { ssr: false } diff --git a/components/token/ChartTabs.tsx b/components/token/ChartTabs.tsx index df7c0d2a..cdd01330 100644 --- a/components/token/ChartTabs.tsx +++ b/components/token/ChartTabs.tsx @@ -1,9 +1,10 @@ import TabButtons from '@components/shared/TabButtons' -import mangoStore, { TokenStatsItem } from '@store/mangoStore' +import mangoStore from '@store/mangoStore' import useMangoGroup from 'hooks/useMangoGroup' import { useTranslation } from 'next-i18next' import dynamic from 'next/dynamic' import { useEffect, useMemo, useState } from 'react' +import { TokenStatsItem } from 'types' import { formatYAxis } from 'utils/formatting' const DetailedAreaChart = dynamic( () => import('@components/shared/DetailedAreaChart'), diff --git a/components/trade/PerpSideBadge.tsx b/components/trade/PerpSideBadge.tsx index 1aef7288..fce1c8c5 100644 --- a/components/trade/PerpSideBadge.tsx +++ b/components/trade/PerpSideBadge.tsx @@ -4,7 +4,7 @@ const PerpSideBadge = ({ basePosition }: { basePosition: number }) => { return ( <> {basePosition !== 0 ? ( - 0 ? 'long' : 'short'} /> + 0 ? 'buy' : 'sell'} /> ) : ( '--' )} diff --git a/store/mangoStore.ts b/store/mangoStore.ts index af76f7ea..5c56ed37 100644 --- a/store/mangoStore.ts +++ b/store/mangoStore.ts @@ -40,11 +40,24 @@ import { RPC_PROVIDER_KEY, } from '../utils/constants' import { + AccountPerformanceData, + ActivityFeed, + EmptyObject, OrderbookL2, + PerformanceDataItem, + PerpStatsItem, PerpTradeHistory, SerumEvent, SpotBalances, SpotTradeHistory, + SwapHistoryItem, + TotalInterestDataItem, + TradeForm, + TradeHistoryApiResponseType, + TokenStatsItem, + NFT, + TourSettings, + ProfileDetails, } from 'types' import spotBalancesUpdater from './spotBalancesUpdater' import { PerpMarket } from '@blockworks-foundation/mango-v4/' @@ -100,147 +113,6 @@ const initMangoClient = ( } let mangoGroupRetryAttempt = 0 - -export interface TotalInterestDataItem { - borrow_interest: number - deposit_interest: number - borrow_interest_usd: number - deposit_interest_usd: number - symbol: string -} - -export interface PerformanceDataItem { - account_equity: number - borrow_interest_cumulative_usd: number - deposit_interest_cumulative_usd: number - pnl: number - spot_value: number - time: string - transfer_balance: number -} - -export interface DepositWithdrawFeedItem { - activity_details: { - block_datetime: string - mango_account: string - quantity: number - signature: string - symbol: string - usd_equivalent: number - wallet_pk: string - } - activity_type: string - block_datetime: string - symbol: string -} - -export interface LiquidationFeedItem { - activity_details: { - asset_amount: number - asset_price: number - asset_symbol: string - block_datetime: string - liab_amount: number - liab_price: number - liab_symbol: string - mango_account: string - mango_group: string - side: string - signature: string - } - activity_type: string - block_datetime: string - symbol: string -} - -export interface SwapHistoryItem { - block_datetime: string - mango_account: string - signature: string - swap_in_amount: number - swap_in_loan: number - swap_in_loan_origination_fee: number - swap_in_price_usd: number - swap_in_symbol: string - swap_out_amount: number - loan: number - loan_origination_fee: number - swap_out_price_usd: number - swap_out_symbol: string -} - -interface NFT { - address: string - image: string -} - -export interface PerpStatsItem { - date_hour: string - fees_accrued: number - funding_rate_hourly: number - instantaneous_funding_rate: number - mango_group: string - market_index: number - open_interest: number - perp_market: string - price: number - stable_price: number -} - -interface ProfileDetails { - profile_image_url?: string - profile_name: string - trader_category: string - wallet_pk: string -} - -interface TourSettings { - account_tour_seen: boolean - swap_tour_seen: boolean - trade_tour_seen: boolean - wallet_pk: string -} - -export interface TokenStatsItem { - borrow_apr: number - borrow_rate: number - collected_fees: number - date_hour: string - deposit_apr: number - deposit_rate: number - mango_group: string - price: number - symbol: string - token_index: number - total_borrows: number - total_deposits: number -} - -// const defaultUserSettings = { -// account_tour_seen: false, -// default_language: 'English', -// default_market: 'SOL-Perp', -// orderbook_animation: false, -// rpc_endpoint: 'Triton (RPC Pool)', -// rpc_node_url: null, -// spot_margin: false, -// swap_tour_seen: false, -// theme: 'Mango', -// trade_tour_seen: false, -// wallet_pk: '', -// } - -interface TradeForm { - side: 'buy' | 'sell' - price: string | undefined - baseSize: string - quoteSize: string - tradeType: 'Market' | 'Limit' - postOnly: boolean - ioc: boolean - reduceOnly: boolean -} - export const DEFAULT_TRADE_FORM: TradeForm = { side: 'buy', price: undefined, @@ -254,7 +126,7 @@ export const DEFAULT_TRADE_FORM: TradeForm = { export type MangoStore = { activityFeed: { - feed: Array + feed: Array loading: boolean queryParams: string } @@ -554,20 +426,24 @@ const mangoStore = create()( .subtract(range, 'day') .format('YYYY-MM-DD')}` ) - const parsedResponse = await response.json() - const entries: any = Object.entries(parsedResponse).sort((a, b) => - b[0].localeCompare(a[0]) - ) + const parsedResponse: + | null + | EmptyObject + | AccountPerformanceData[] = await response.json() - const stats = entries - .map(([key, value]: Array<{ key: string; value: number }>) => { - return { ...value, time: key } + if (parsedResponse?.length) { + const entries = Object.entries(parsedResponse).sort((a, b) => + b[0].localeCompare(a[0]) + ) + + const stats = entries.map(([key, value]) => { + return { ...value, time: key } as PerformanceDataItem }) - .filter((x: string) => x) - set((state) => { - state.mangoAccount.performance.data = stats.reverse() - }) + set((state) => { + state.mangoAccount.performance.data = stats.reverse() + }) + } } catch (e) { console.error('Failed to load account performance data', e) } finally { @@ -583,9 +459,6 @@ const mangoStore = create()( ) => { const set = get().set const loadedFeed = mangoStore.getState().activityFeed.feed - const connectedMangoAccountPk = mangoStore - .getState() - .mangoAccount.current?.publicKey.toString() try { const response = await fetch( @@ -593,36 +466,34 @@ const mangoStore = create()( params ? params : '' }` ) - const parsedResponse = await response.json() - const entries: any = Object.entries(parsedResponse).sort((a, b) => - b[0].localeCompare(a[0]) - ) + const parsedResponse: + | null + | EmptyObject + | Array = await response.json() - const latestFeed = entries - .map(([key, value]: Array<{ key: string; value: number }>) => { - return { ...value, symbol: key } - }) - .filter((x: string) => x) - .sort( - ( - a: DepositWithdrawFeedItem | LiquidationFeedItem, - b: DepositWithdrawFeedItem | LiquidationFeedItem - ) => - dayjs(b.block_datetime).unix() - - dayjs(a.block_datetime).unix() + if (parsedResponse?.length) { + const entries = Object.entries(parsedResponse).sort((a, b) => + b[0].localeCompare(a[0]) ) - // only add to current feed if data request is offset and the mango account hasn't changed - const combinedFeed = - offset !== 0 && - connectedMangoAccountPk === - loadedFeed[0]?.activity_details?.mango_account - ? loadedFeed.concat(latestFeed) - : latestFeed + const latestFeed = entries + .map(([key, value]) => { + return { ...value, symbol: key } + }) + .sort( + (a, b) => + dayjs(b.block_datetime).unix() - + dayjs(a.block_datetime).unix() + ) - set((state) => { - state.activityFeed.feed = combinedFeed - }) + // only add to current feed if data request is offset and the mango account hasn't changed + const combinedFeed = + offset !== 0 ? loadedFeed.concat(latestFeed) : latestFeed + + set((state) => { + state.activityFeed.feed = combinedFeed + }) + } } catch (e) { console.error('Failed to fetch account activity feed', e) } finally { @@ -1015,8 +886,8 @@ const mangoStore = create()( set((s) => { s.client = client }) - } catch (e: any) { - if (e.name.includes('WalletLoadError')) { + } catch (e) { + if (e instanceof Error && e.name.includes('WalletLoadError')) { notify({ title: `${wallet.adapter.name} Error`, type: 'error', @@ -1118,17 +989,16 @@ const mangoStore = create()( const response = await fetch( `${MANGO_DATA_API_URL}/stats/trade-history?mango-account=${mangoAccountPk}&limit=${PAGINATION_PAGE_LENGTH}&offset=${offset}` ) - const jsonResponse = await response.json() + const jsonResponse: + | null + | EmptyObject + | TradeHistoryApiResponseType = await response.json() if (jsonResponse?.length) { - const newHistory = jsonResponse.map( - (h: any) => h.activity_details - ) + const newHistory = jsonResponse.map((h) => h.activity_details) const history = offset !== 0 ? loadedHistory.concat(newHistory) : newHistory set((s) => { - s.mangoAccount.tradeHistory.data = history?.sort( - (x: any) => x.block_datetime - ) + s.mangoAccount.tradeHistory.data = history }) } else { set((s) => { diff --git a/store/perpPositionsUpdater.ts b/store/perpPositionsUpdater.ts index 61e0f5ed..35a765b4 100644 --- a/store/perpPositionsUpdater.ts +++ b/store/perpPositionsUpdater.ts @@ -1,10 +1,4 @@ -import { - Group, - MangoAccount, - PerpMarket, - PerpPosition, - toUiI80F48, -} from '@blockworks-foundation/mango-v4' +import { PerpPosition } from '@blockworks-foundation/mango-v4' import mangoStore from './mangoStore' const perpPositionsUpdater = (_newState: any, _prevState: any) => { diff --git a/types/index.ts b/types/index.ts index 7f216d29..23641962 100644 --- a/types/index.ts +++ b/types/index.ts @@ -1,22 +1,8 @@ import { PerpMarket, Serum3Market } from '@blockworks-foundation/mango-v4' import { Modify } from '@blockworks-foundation/mango-v4' -import { BN } from '@project-serum/anchor' import { Event } from '@project-serum/serum/lib/queue' -export interface ChartTradeType { - market: string - size: number - quantity: number | any - price: number | any - orderId: string - time: number - side: string - takerSide: any - feeCost: number - marketAddress: string - timestamp: BN -} - +export type EmptyObject = { [K in keyof T]?: never } export interface OrderbookL2 { bids: number[][] asks: number[][] @@ -79,3 +65,165 @@ export type SerumEvent = Modify< > export type GenericMarket = Serum3Market | PerpMarket + +export type TradeHistoryApiResponseType = Array<{ + trade_type: string + block_datetime: string + activity_details: PerpTradeHistory | SpotTradeHistory +}> + +export type AccountPerformanceData = { + [date: string]: { + account_equity: number + pnl: number + spot_value: number + transfer_balance: number + deposit_interest_cumulative_usd: number + borrow_interest_cumulative_usd: number + spot_volume_usd: number + } +} + +export interface TotalInterestDataItem { + borrow_interest: number + deposit_interest: number + borrow_interest_usd: number + deposit_interest_usd: number + symbol: string +} + +export interface PerformanceDataItem { + account_equity: number + borrow_interest_cumulative_usd: number + deposit_interest_cumulative_usd: number + pnl: number + spot_value: number + time: string + transfer_balance: number +} + +export interface DepositWithdrawFeedItem { + block_datetime: string + mango_account: string + quantity: number + signature: string + symbol: string + usd_equivalent: number + wallet_pk: string +} + +export interface LiquidationFeedItem { + asset_amount: number + asset_price: number + asset_symbol: string + block_datetime: string + liab_amount: number + liab_price: number + liab_symbol: string + mango_account: string + mango_group: string + side: string + signature: string +} + +export interface LiquidationActivity { + activity_details: LiquidationFeedItem + block_datetime: string + activity_type: string + symbol: string +} + +export function isLiquidationFeedItem( + item: ActivityFeed +): item is LiquidationActivity { + if (item.activity_type.includes('liquidate')) { + return true + } + return false +} + +export interface SwapHistoryItem { + block_datetime: string + mango_account: string + signature: string + swap_in_amount: number + swap_in_loan: number + swap_in_loan_origination_fee: number + swap_in_price_usd: number + swap_in_symbol: string + swap_out_amount: number + loan: number + loan_origination_fee: number + swap_out_price_usd: number + swap_out_symbol: string +} + +export interface NFT { + address: string + image: string +} + +export interface PerpStatsItem { + date_hour: string + fees_accrued: number + funding_rate_hourly: number + instantaneous_funding_rate: number + mango_group: string + market_index: number + open_interest: number + perp_market: string + price: number + stable_price: number +} + +export type ActivityFeed = { + activity_type: string + block_datetime: string + symbol: string + activity_details: + | DepositWithdrawFeedItem + | LiquidationFeedItem + | SwapHistoryItem + | PerpTradeHistory + | SpotTradeHistory +} + +export interface ProfileDetails { + profile_image_url?: string + profile_name: string + trader_category: string + wallet_pk: string +} + +export interface TourSettings { + account_tour_seen: boolean + swap_tour_seen: boolean + trade_tour_seen: boolean + wallet_pk: string +} + +export interface TokenStatsItem { + borrow_apr: number + borrow_rate: number + collected_fees: number + date_hour: string + deposit_apr: number + deposit_rate: number + mango_group: string + price: number + symbol: string + token_index: number + total_borrows: number + total_deposits: number +} + +export interface TradeForm { + side: 'buy' | 'sell' + price: string | undefined + baseSize: string + quoteSize: string + tradeType: 'Market' | 'Limit' + postOnly: boolean + ioc: boolean + reduceOnly: boolean +}