From 5cb0dbda38daee7bb8b6380c0d1924e4f310137f Mon Sep 17 00:00:00 2001 From: Jared Vu Date: Thu, 19 Dec 2024 15:17:50 -0800 Subject: [PATCH] feat: Migrate Market Details page (#1381) --- src/abacus-ts/calculators/assets.ts | 26 ++++++++ src/abacus-ts/calculators/markets.ts | 49 ++++++++++++++- src/abacus-ts/calculators/subaccount.ts | 36 +---------- src/abacus-ts/selectors/assets.ts | 20 ++++++ src/abacus-ts/selectors/markets.ts | 6 ++ src/abacus-ts/summaryTypes.ts | 4 ++ src/hooks/useMetadataService.ts | 6 +- src/lib/assetUtils.ts | 5 ++ .../MarketDetails/CurrentMarketDetails.tsx | 61 +++++++++---------- 9 files changed, 142 insertions(+), 71 deletions(-) create mode 100644 src/abacus-ts/calculators/assets.ts create mode 100644 src/abacus-ts/selectors/assets.ts diff --git a/src/abacus-ts/calculators/assets.ts b/src/abacus-ts/calculators/assets.ts new file mode 100644 index 000000000..d0ecad118 --- /dev/null +++ b/src/abacus-ts/calculators/assets.ts @@ -0,0 +1,26 @@ +import { mapValues } from 'lodash'; +import { weakMapMemoize } from 'reselect'; + +import { MetadataServiceAssetInfo, MetadataServiceInfoResponse } from '@/constants/assetMetadata'; + +export const parseAssetInfo = weakMapMemoize( + (assetInfo: MetadataServiceAssetInfo, assetId: string) => ({ + assetId, + name: assetInfo.name, + logo: assetInfo.logo, + urls: { + website: assetInfo.urls.website, + technicalDoc: assetInfo.urls.technical_doc, + cmc: assetInfo.urls.cmc, + }, + }) +); + +export const transformAssetsInfo = (assetsInfo: MetadataServiceInfoResponse | undefined) => { + if (assetsInfo == null) { + return assetsInfo; + } + + // Add id field to each assetInfo + return mapValues(assetsInfo, (assetInfo, key) => parseAssetInfo(assetInfo, key)); +}; diff --git a/src/abacus-ts/calculators/markets.ts b/src/abacus-ts/calculators/markets.ts index 3e1c997c0..525fc0865 100644 --- a/src/abacus-ts/calculators/markets.ts +++ b/src/abacus-ts/calculators/markets.ts @@ -1,10 +1,16 @@ import { IndexerPerpetualMarketResponseObject } from '@/types/indexer/indexerApiGen'; +import BigNumber from 'bignumber.js'; import { mapValues } from 'lodash'; import { weakMapMemoize } from 'reselect'; import { TOKEN_DECIMALS, USD_DECIMALS } from '@/constants/numbers'; -import { MaybeBigNumber } from '@/lib/numbers'; +import { + getAssetFromMarketId, + getDisplayableAssetFromTicker, + getDisplayableTickerFromMarket, +} from '@/lib/assetUtils'; +import { MaybeBigNumber, MustBigNumber } from '@/lib/numbers'; import { MarketsData } from '../rawTypes'; import { MarketInfo, MarketsInfo } from '../summaryTypes'; @@ -16,9 +22,50 @@ export function calculateAllMarkets(markets: MarketsData | undefined): MarketsIn return mapValues(markets, calculateMarket); } +export function getMarketEffectiveInitialMarginForMarket( + market: IndexerPerpetualMarketResponseObject +) { + const initialMarginFraction = MaybeBigNumber(market.initialMarginFraction); + const openInterest = MaybeBigNumber(market.openInterest); + const openInterestLowerCap = MaybeBigNumber(market.openInterestLowerCap); + const openInterestUpperCap = MaybeBigNumber(market.openInterestUpperCap); + const oraclePrice = MaybeBigNumber(market.oraclePrice); + + if (initialMarginFraction == null) return null; + if ( + oraclePrice == null || + openInterest == null || + openInterestLowerCap == null || + openInterestUpperCap == null + ) { + return initialMarginFraction; + } + + // if these are equal we can throw an error from dividing by zero + if (openInterestUpperCap.eq(openInterestLowerCap)) { + return initialMarginFraction; + } + + const openNotional = openInterest.times(oraclePrice); + const scalingFactor = openNotional + .minus(openInterestLowerCap) + .div(openInterestUpperCap.minus(openInterestLowerCap)); + const imfIncrease = scalingFactor.times(MustBigNumber(1).minus(initialMarginFraction)); + + const effectiveIMF = BigNumber.minimum( + initialMarginFraction.plus(BigNumber.maximum(imfIncrease, 0.0)), + 1.0 + ); + return effectiveIMF; +} + const calculateMarket = weakMapMemoize( (market: IndexerPerpetualMarketResponseObject): MarketInfo => ({ ...market, + assetId: getAssetFromMarketId(market.ticker), + displayableAsset: getDisplayableAssetFromTicker(market.ticker), + effectiveInitialMarginFraction: getMarketEffectiveInitialMarginForMarket(market), + displayableTicker: getDisplayableTickerFromMarket(market.ticker), stepSizeDecimals: MaybeBigNumber(market.stepSize)?.decimalPlaces() ?? TOKEN_DECIMALS, tickSizeDecimals: MaybeBigNumber(market.tickSize)?.decimalPlaces() ?? USD_DECIMALS, }) diff --git a/src/abacus-ts/calculators/subaccount.ts b/src/abacus-ts/calculators/subaccount.ts index a54c66ec3..bf7415016 100644 --- a/src/abacus-ts/calculators/subaccount.ts +++ b/src/abacus-ts/calculators/subaccount.ts @@ -25,6 +25,7 @@ import { SubaccountSummaryCore, SubaccountSummaryDerived, } from '../summaryTypes'; +import { getMarketEffectiveInitialMarginForMarket } from './markets'; export function calculateParentSubaccountPositions( parent: Omit, @@ -287,38 +288,3 @@ function calculatePositionDerivedExtra( updatedUnrealizedPnlPercent, }; } - -function getMarketEffectiveInitialMarginForMarket(config: IndexerPerpetualMarketResponseObject) { - const initialMarginFraction = MaybeBigNumber(config.initialMarginFraction); - const openInterest = MaybeBigNumber(config.openInterest); - const openInterestLowerCap = MaybeBigNumber(config.openInterestLowerCap); - const openInterestUpperCap = MaybeBigNumber(config.openInterestUpperCap); - const oraclePrice = MaybeBigNumber(config.oraclePrice); - - if (initialMarginFraction == null) return null; - if ( - oraclePrice == null || - openInterest == null || - openInterestLowerCap == null || - openInterestUpperCap == null - ) { - return initialMarginFraction; - } - - // if these are equal we can throw an error from dividing by zero - if (openInterestUpperCap.eq(openInterestLowerCap)) { - return initialMarginFraction; - } - - const openNotional = openInterest.times(oraclePrice); - const scalingFactor = openNotional - .minus(openInterestLowerCap) - .div(openInterestUpperCap.minus(openInterestLowerCap)); - const imfIncrease = scalingFactor.times(MustBigNumber(1).minus(initialMarginFraction)); - - const effectiveIMF = BigNumber.minimum( - initialMarginFraction.plus(BigNumber.maximum(imfIncrease, 0.0)), - 1.0 - ); - return effectiveIMF; -} diff --git a/src/abacus-ts/selectors/assets.ts b/src/abacus-ts/selectors/assets.ts new file mode 100644 index 000000000..98170180f --- /dev/null +++ b/src/abacus-ts/selectors/assets.ts @@ -0,0 +1,20 @@ +import { createSelector } from 'reselect'; + +import { transformAssetsInfo } from '../calculators/assets'; +import { selectRawAssetsData } from './base'; +import { selectCurrentMarketInfo } from './markets'; + +export const selectAllAssetsInfo = createSelector([selectRawAssetsData], (assets) => + transformAssetsInfo(assets) +); + +export const selectCurrentMarketAssetInfo = createSelector( + [selectCurrentMarketInfo, selectAllAssetsInfo], + (currentMarketInfo, assets) => { + if (currentMarketInfo == null || assets == null) { + return undefined; + } + + return assets[currentMarketInfo.assetId]; + } +); diff --git a/src/abacus-ts/selectors/markets.ts b/src/abacus-ts/selectors/markets.ts index e8baba619..1b31eaa79 100644 --- a/src/abacus-ts/selectors/markets.ts +++ b/src/abacus-ts/selectors/markets.ts @@ -1,4 +1,5 @@ import { createAppSelector } from '@/state/appTypes'; +import { getCurrentMarketId } from '@/state/perpetualsSelectors'; import { calculateAllMarkets } from '../calculators/markets'; import { selectRawMarketsData } from './base'; @@ -6,3 +7,8 @@ import { selectRawMarketsData } from './base'; export const selectAllMarketsInfo = createAppSelector([selectRawMarketsData], (markets) => calculateAllMarkets(markets) ); + +export const selectCurrentMarketInfo = createAppSelector( + [selectAllMarketsInfo, getCurrentMarketId], + (markets, currentMarketId) => (currentMarketId ? markets?.[currentMarketId] : undefined) +); diff --git a/src/abacus-ts/summaryTypes.ts b/src/abacus-ts/summaryTypes.ts index e4da33b1e..b0f044378 100644 --- a/src/abacus-ts/summaryTypes.ts +++ b/src/abacus-ts/summaryTypes.ts @@ -21,6 +21,10 @@ type ConvertStringToBigNumber> = { }; export type MarketInfo = IndexerPerpetualMarketResponseObject & { + assetId: string; + displayableAsset: string; + displayableTicker: string; + effectiveInitialMarginFraction: BigNumber | null; stepSizeDecimals: number; tickSizeDecimals: number; }; diff --git a/src/hooks/useMetadataService.ts b/src/hooks/useMetadataService.ts index 38c0369bf..355c2ac01 100644 --- a/src/hooks/useMetadataService.ts +++ b/src/hooks/useMetadataService.ts @@ -85,8 +85,8 @@ export const useMetadataService = () => { export const useMetadataServiceAssetFromId = (marketId?: string) => { const metadataServiceData = useMetadataService(); - const launchableAsset = useMemo(() => { - if (!metadataServiceData.data || !marketId) { + const asset = useMemo(() => { + if (!Object.keys(metadataServiceData.data).length || !marketId) { return null; } @@ -94,7 +94,7 @@ export const useMetadataServiceAssetFromId = (marketId?: string) => { return metadataServiceData.data[assetId]; }, [metadataServiceData.data, marketId]); - return launchableAsset; + return asset; }; export const useMetadataServiceCandles = ( diff --git a/src/lib/assetUtils.ts b/src/lib/assetUtils.ts index cb58d2108..ac8acb310 100644 --- a/src/lib/assetUtils.ts +++ b/src/lib/assetUtils.ts @@ -102,3 +102,8 @@ export const getDefaultChainIDFromNetworkType = (networkType: NetworkType): stri if (networkType === 'cosmos') return 'noble-1'; return undefined; }; + +export const getAssetDescriptionStringKeys = (assetId: string) => ({ + primary: `APP.__ASSETS.${assetId}.PRIMARY`, + secondary: `APP.__ASSETS.${assetId}.SECONDARY`, +}); diff --git a/src/views/MarketDetails/CurrentMarketDetails.tsx b/src/views/MarketDetails/CurrentMarketDetails.tsx index ddfa4f6f8..0e47e0987 100644 --- a/src/views/MarketDetails/CurrentMarketDetails.tsx +++ b/src/views/MarketDetails/CurrentMarketDetails.tsx @@ -1,7 +1,9 @@ +import { selectCurrentMarketAssetInfo } from '@/abacus-ts/selectors/assets'; +import { selectCurrentMarketInfo } from '@/abacus-ts/selectors/markets'; +import { IndexerPerpetualMarketType } from '@/types/indexer/indexerApiGen'; import BigNumber from 'bignumber.js'; import { shallowEqual } from 'react-redux'; -import { PerpetualMarketType } from '@/constants/abacus'; import { STRING_KEYS } from '@/constants/localization'; import { useStringGetter } from '@/hooks/useStringGetter'; @@ -11,56 +13,51 @@ import { DiffOutput } from '@/components/DiffOutput'; import { Output, OutputType } from '@/components/Output'; import { useAppSelector } from '@/state/appTypes'; -import { getCurrentMarketAssetData } from '@/state/assetsSelectors'; -import { getCurrentMarketData } from '@/state/perpetualsSelectors'; -import { getDisplayableAssetFromBaseAsset } from '@/lib/assetUtils'; +import { getAssetDescriptionStringKeys } from '@/lib/assetUtils'; import { BIG_NUMBERS } from '@/lib/numbers'; +import { orEmptyObj } from '@/lib/typeUtils'; import { MarketDetails } from './MarketDetails'; export const CurrentMarketDetails = () => { const stringGetter = useStringGetter(); - const { configs, displayId } = useAppSelector(getCurrentMarketData, shallowEqual) ?? {}; - const { id, name, resources } = useAppSelector(getCurrentMarketAssetData, shallowEqual) ?? {}; - - if (!configs) return null; + const currentMarketData = useAppSelector(selectCurrentMarketInfo, shallowEqual); + const asset = useAppSelector(selectCurrentMarketAssetInfo); const { - tickSize, - stepSize, - initialMarginFraction, + displayableAsset, + displayableTicker, effectiveInitialMarginFraction, + initialMarginFraction, maintenanceMarginFraction, - minOrderSize, - perpetualMarketType, - stepSizeDecimals, + marketType, + tickSize, tickSizeDecimals, - } = configs; + stepSize, + stepSizeDecimals, + } = orEmptyObj(currentMarketData); - const { - coinMarketCapsLink, - primaryDescriptionKey, - secondaryDescriptionKey, - websiteLink, - whitepaperLink, - } = resources ?? {}; + const { assetId, logo, name, urls } = orEmptyObj(asset); + const { cmc, website, technicalDoc } = orEmptyObj(urls); + const { primary, secondary } = getAssetDescriptionStringKeys(assetId ?? ''); const preferEIMF = Boolean( - effectiveInitialMarginFraction && initialMarginFraction !== effectiveInitialMarginFraction + effectiveInitialMarginFraction && + initialMarginFraction !== effectiveInitialMarginFraction.toString() ); const items = [ { key: 'ticker', label: stringGetter({ key: STRING_KEYS.TICKER }), - value: displayId, + value: displayableTicker, }, { key: 'market-type', label: stringGetter({ key: STRING_KEYS.TYPE }), value: - perpetualMarketType === PerpetualMarketType.CROSS + marketType === IndexerPerpetualMarketType.CROSS ? stringGetter({ key: STRING_KEYS.CROSS }) : stringGetter({ key: STRING_KEYS.ISOLATED }), }, @@ -86,7 +83,7 @@ export const CurrentMarketDetails = () => { useGrouping value={stepSize} type={OutputType.Asset} - tag={getDisplayableAssetFromBaseAsset(id)} + tag={displayableAsset} fractionDigits={stepSizeDecimals} /> ), @@ -97,9 +94,9 @@ export const CurrentMarketDetails = () => { value: ( ), @@ -149,11 +146,11 @@ export const CurrentMarketDetails = () => { return ( ); };