Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

perf: market data benchmarking outcomes #8399

Merged
merged 5 commits into from
Jan 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 0 additions & 7 deletions src/context/AppProvider/AppContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import { useRouteAssetId } from 'hooks/useRouteAssetId/useRouteAssetId'
import { useWallet } from 'hooks/useWallet/useWallet'
import { walletSupportsChain } from 'hooks/useWalletSupportsChain/useWalletSupportsChain'
import { snapshotApi } from 'state/apis/snapshot/snapshot'
import { useGetAssetsQuery } from 'state/slices/assetsSlice/assetsSlice'
import {
marketApi,
marketData,
Expand Down Expand Up @@ -89,12 +88,6 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
// track anonymous portfolio
useMixpanelPortfolioTracking()

// immediately load all assets, before the wallet is even connected,
// so the app is functional and ready
// if we already have assets in store, we don't need to refetch the base assets, as these won't change
// if they do, it means we regenerated generatedAssetData.json, and can run a migration to trigger a refetch of base assets
useGetAssetsQuery(undefined, { skip: Boolean(assetIds.length) })

// load top 1000 assets market data
// this is needed to sort assets by market cap
// and covers most assets users will have
Expand Down
4 changes: 2 additions & 2 deletions src/lib/asset-service/service/AssetService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export class AssetService {
}

get assets(): Asset[] {
return Object.values(this.assetsById)
return Object.values(this.assetsById) as Asset[]
}

getRelatedAssetIds(assetId: AssetId): AssetId[] {
Expand All @@ -38,7 +38,7 @@ export class AssetService {
const polyglot = new Polyglot({
phrases: localeDescriptions,
allowMissing: true,
onMissingKey: key => descriptions.en[key], // fallback to English overriden description, which should always be added as a base translation
onMissingKey: key => descriptions.en[key], // fallback to English overridden description, which should always be added as a base translation
})
const overriddenDescription = polyglot.t(assetId)

Expand Down
14 changes: 6 additions & 8 deletions src/lib/portals/utils.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import type { AssetId, ChainId } from '@shapeshiftoss/caip'
import { ASSET_NAMESPACE, bscChainId, toAssetId } from '@shapeshiftoss/caip'
import type { Asset, AssetsByIdPartial } from '@shapeshiftoss/types'
import type { Asset } from '@shapeshiftoss/types'
import { createThrottle, isSome } from '@shapeshiftoss/utils'
import axios from 'axios'
import { getConfig } from 'config'
import qs from 'qs'
import { getAddress, isAddressEqual, zeroAddress } from 'viem'
import { queryClient } from 'context/QueryClientProvider/queryClient'
import { localAssetData } from 'lib/asset-service'

import generatedAssetData from '../../lib/asset-service/service/generatedAssetData.json'
import { CHAIN_ID_TO_PORTALS_NETWORK } from './constants'
import type {
GetBalancesResponse,
Expand Down Expand Up @@ -124,8 +124,6 @@ export const fetchPortalsTokens = async ({
}
}

const assets = generatedAssetData as unknown as AssetsByIdPartial

export const portalTokenToAsset = ({
token,
portalsPlatforms,
Expand All @@ -142,7 +140,7 @@ export const portalTokenToAsset = ({
assetNamespace: chainId === bscChainId ? ASSET_NAMESPACE.bep20 : ASSET_NAMESPACE.erc20,
assetReference: token.address,
})
const asset = assets[assetId]
const asset = localAssetData[assetId]

const explorerData = {
explorer: nativeAsset.explorer,
Expand Down Expand Up @@ -175,7 +173,7 @@ export const portalTokenToAsset = ({
assetNamespace: chainId === bscChainId ? ASSET_NAMESPACE.bep20 : ASSET_NAMESPACE.erc20,
assetReference: token.tokens[i],
})
const underlyingAsset = assets[underlyingAssetId]
const underlyingAsset = localAssetData[underlyingAssetId]
// Prioritise our own flavour of icons for that asset if available, else use upstream if present
return underlyingAsset?.icon || maybeTokenImage(underlyingAssetsImage)
}),
Expand All @@ -199,7 +197,7 @@ export const portalTokenToAsset = ({
assetNamespace: chainId === bscChainId ? ASSET_NAMESPACE.bep20 : ASSET_NAMESPACE.erc20,
assetReference: underlyingToken,
})
const underlyingAsset = assets[assetId]
const underlyingAsset = localAssetData[assetId]
if (!underlyingAsset) return undefined

// This doesn't generalize, but this'll do, this is only a visual hack to display native asset instead of wrapped
Expand Down Expand Up @@ -229,7 +227,7 @@ export const portalTokenToAsset = ({

return {
...explorerData,
color: assets[assetId]?.color ?? '#FFFFFF',
color: localAssetData[assetId]?.color ?? '#FFFFFF',
// This looks weird but we need this - l.165 check above nulls the type safety of this object, so we cast it back
...(iconOrIcons as { icon: string } | { icons: string[]; icon: undefined }),
name,
Expand Down
101 changes: 39 additions & 62 deletions src/state/slices/assetsSlice/assetsSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,34 +13,44 @@ import {
solanaChainId,
} from '@shapeshiftoss/caip'
import type { Asset, AssetsByIdPartial, PartialRecord } from '@shapeshiftoss/types'
import cloneDeep from 'lodash/cloneDeep'
import { getConfig } from 'config'
import { AssetService } from 'lib/asset-service'
import { BASE_RTK_CREATE_API_CONFIG } from 'state/apis/const'
import type { ReduxState } from 'state/reducer'
import { selectFeatureFlags } from 'state/slices/preferencesSlice/selectors'

let service: AssetService | undefined = undefined

// do not export this, views get data from selectors
// or directly from the store outside react components
const getAssetService = () => {
if (!service) {
service = new AssetService()
}

return service
}
const service = new AssetService()

export type AssetsState = {
byId: AssetsByIdPartial
ids: AssetId[]
relatedAssetIndex: PartialRecord<AssetId, AssetId[]>
}

// This looks weird because it is - we want to hydrate the initial state synchronously with as
// little overhead as possible.
const config = getConfig()
const byId = Object.entries(service.assetsById).reduce<AssetsByIdPartial>(
(prev, [assetId, asset]) => {
if (!config.REACT_APP_FEATURE_OPTIMISM && asset.chainId === optimismChainId) return prev
if (!config.REACT_APP_FEATURE_BNBSMARTCHAIN && asset.chainId === bscChainId) return prev
if (!config.REACT_APP_FEATURE_POLYGON && asset.chainId === polygonChainId) return prev
if (!config.REACT_APP_FEATURE_GNOSIS && asset.chainId === gnosisChainId) return prev
if (!config.REACT_APP_FEATURE_ARBITRUM && asset.chainId === arbitrumChainId) return prev
if (!config.REACT_APP_FEATURE_ARBITRUM_NOVA && asset.chainId === arbitrumNovaChainId)
return prev
if (!config.REACT_APP_FEATURE_BASE && asset.chainId === baseChainId) return prev
if (!config.REACT_APP_FEATURE_SOLANA && asset.chainId === solanaChainId) return prev
prev[assetId] = asset
return prev
},
{},
)

export const initialState: AssetsState = {
byId: {},
ids: [],
relatedAssetIndex: {},
byId,
ids: Object.keys(byId), // TODO: Use pre-sorted array to maintain pre-sorting of assets
relatedAssetIndex: service.relatedAssetIndex,
}

export const defaultAsset: Asset = {
Expand All @@ -66,15 +76,12 @@ export const assets = createSlice({
clear: () => initialState,
upsertAssets: (state, action: PayloadAction<UpsertAssetsPayload>) => {
state.byId = Object.assign({}, state.byId, action.payload.byId) // upsert
state.ids = Array.from(new Set(state.ids.concat(action.payload.ids)))
state.ids = Array.from(new Set(state.ids.concat(action.payload.ids))) // TODO: Preserve sorting here
},
upsertAsset: (state, action: PayloadAction<Asset>) => {
const { assetId } = action.payload
state.byId[assetId] = Object.assign({}, state.byId[assetId], action.payload)
state.ids = Array.from(new Set(state.ids.concat(assetId)))
},
setRelatedAssetIndex: (state, action: PayloadAction<PartialRecord<AssetId, AssetId[]>>) => {
state.relatedAssetIndex = action.payload
state.ids = Array.from(new Set(state.ids.concat(assetId))) // TODO: Preserve sorting here
},
},
})
Expand All @@ -83,58 +90,28 @@ export const assetApi = createApi({
...BASE_RTK_CREATE_API_CONFIG,
reducerPath: 'assetApi',
endpoints: build => ({
getAssets: build.query<UpsertAssetsPayload, void>({
// all assets
queryFn: (_, { getState, dispatch }) => {
const flags = selectFeatureFlags(getState() as ReduxState)
const service = getAssetService()

dispatch(assets.actions.setRelatedAssetIndex(service.relatedAssetIndex))

const assetsById = Object.entries(service?.assetsById ?? {}).reduce<AssetsByIdPartial>(
(prev, [assetId, asset]) => {
if (!flags.Optimism && asset.chainId === optimismChainId) return prev
if (!flags.BnbSmartChain && asset.chainId === bscChainId) return prev
if (!flags.Polygon && asset.chainId === polygonChainId) return prev
if (!flags.Gnosis && asset.chainId === gnosisChainId) return prev
if (!flags.Arbitrum && asset.chainId === arbitrumChainId) return prev
if (!flags.ArbitrumNova && asset.chainId === arbitrumNovaChainId) return prev
if (!flags.Base && asset.chainId === baseChainId) return prev
if (!flags.Solana && asset.chainId === solanaChainId) return prev
prev[assetId] = asset
return prev
},
{},
)
const data = {
byId: assetsById,
ids: Object.keys(assetsById) ?? [],
}

if (data) dispatch(assets.actions.upsertAssets(data))
return { data }
},
}),
getAssetDescription: build.query<
UpsertAssetsPayload,
string,
{ assetId: AssetId | undefined; selectedLocale: string }
>({
queryFn: async ({ assetId, selectedLocale }, { getState, dispatch }) => {
if (!assetId) {
throw new Error('assetId not provided')
}
const service = getAssetService()

// limitation of redux tookit https://redux-toolkit.js.org/rtk-query/api/createApi#queryfn
const { byId: byIdOriginal, ids } = (getState() as any).assets as AssetsState
const byId = cloneDeep(byIdOriginal)
const { byId: byIdOriginal } = (getState() as any).assets as AssetsState
const originalAsset = byIdOriginal[assetId]

try {
const { description, isTrusted } = await service.description(assetId, selectedLocale)
const originalAsset = byId[assetId]
byId[assetId] = originalAsset && Object.assign(originalAsset, { description, isTrusted })
const data = { byId, ids }
const byId = {
[assetId]: originalAsset && Object.assign(originalAsset, { description, isTrusted }),
}

dispatch(assets.actions.upsertAssets({ byId, ids: [assetId] }))

if (data) dispatch(assets.actions.upsertAssets(data))
return { data }
return { data: description }
} catch (e) {
const data = `getAssetDescription: error fetching description for ${assetId}`
const status = 400
Expand All @@ -146,4 +123,4 @@ export const assetApi = createApi({
}),
})

export const { useGetAssetsQuery, useGetAssetDescriptionQuery } = assetApi
export const { useGetAssetDescriptionQuery } = assetApi
8 changes: 6 additions & 2 deletions src/state/slices/marketDataSlice/marketDataSlice.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,10 @@ export const marketData = createSlice({
state.isMarketDataLoaded = true
},
setCryptoMarketData: {
reducer: (state, { payload }: { payload: MarketDataById<AssetId> }) => {
reducer: function setCryptoMarketData(
state,
{ payload }: { payload: MarketDataById<AssetId> },
) {
state.crypto.byId = Object.assign(state.crypto.byId, payload) // upsert
state.crypto.ids = Object.keys(state.crypto.byId).sort((assetIdA, assetIdB) => {
const marketDataA = state.crypto.byId[assetIdA]
Expand Down Expand Up @@ -156,7 +159,8 @@ export const marketApi = createApi({
endpoints: build => ({
findAll: build.query<MarketCapResult, void>({
// top 1000 assets
queryFn: async (_, { dispatch }) => {
// named function for profiling+debugging purposes
queryFn: async function findAll(_, { dispatch }) {
try {
const data = await getMarketServiceManager().findAll({ count: 1000 })
dispatch(marketData.actions.setCryptoMarketData(data))
Expand Down