From f84610109ae4f02e2361d8f986b9c428b81adbc7 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Mon, 2 Sep 2024 17:57:35 +0200 Subject: [PATCH 01/10] [skip ci] wip: augment runtime LP tokens metadata from Portals --- .../ProviderDetails/OpportunityRow.tsx | 5 +- src/lib/market-service/portals/types.ts | 5 + src/state/apis/zapper/zapperApi.ts | 24 ++- .../slices/portfolioSlice/portfolioSlice.ts | 139 +++++++++++++++++- 4 files changed, 166 insertions(+), 7 deletions(-) diff --git a/src/components/EarnDashboard/components/ProviderDetails/OpportunityRow.tsx b/src/components/EarnDashboard/components/ProviderDetails/OpportunityRow.tsx index 896acadaaa3..af4526fc8b7 100644 --- a/src/components/EarnDashboard/components/ProviderDetails/OpportunityRow.tsx +++ b/src/components/EarnDashboard/components/ProviderDetails/OpportunityRow.tsx @@ -86,6 +86,9 @@ export const OpportunityRow: React.FC< const handleClick = useCallback( (action: DefiAction) => { + if (asset?.isPool) { + return history.push(`/trade/${assetId}`) + } if (opportunity.isReadOnly) { const url = getMetadataForProvider(opportunity.provider)?.url url && window.open(url, '_blank') @@ -93,7 +96,7 @@ export const OpportunityRow: React.FC< } onClick(opportunity, action) }, - [onClick, opportunity], + [asset?.isPool, assetId, history, onClick, opportunity], ) const handleClaimClick = useCallback(() => handleClick(DefiAction.Claim), [handleClick]) diff --git a/src/lib/market-service/portals/types.ts b/src/lib/market-service/portals/types.ts index 240c385eecf..8914de3ebda 100644 --- a/src/lib/market-service/portals/types.ts +++ b/src/lib/market-service/portals/types.ts @@ -29,6 +29,11 @@ export type PlatformsById = Record export type GetPlatformsResponse = Platform[] +export type GetBalancesResponse = { + // Not strictly true, this has additional fields, but we're only interested in the token info part + balances: TokenInfo[] +} + export type GetTokensResponse = { totalItems: number pageItems: number diff --git a/src/state/apis/zapper/zapperApi.ts b/src/state/apis/zapper/zapperApi.ts index 512c5518edf..c53525c29dc 100644 --- a/src/state/apis/zapper/zapperApi.ts +++ b/src/state/apis/zapper/zapperApi.ts @@ -1,6 +1,13 @@ import { createApi } from '@reduxjs/toolkit/dist/query/react' import type { AccountId, AssetId } from '@shapeshiftoss/caip' -import { ethChainId, fromAssetId, toAccountId, toAssetId } from '@shapeshiftoss/caip' +import { + ASSET_NAMESPACE, + bscChainId, + ethChainId, + fromAssetId, + toAccountId, + toAssetId, +} from '@shapeshiftoss/caip' import { evmChainIds } from '@shapeshiftoss/chain-adapters' import { makeAsset } from '@shapeshiftoss/utils' import type { AxiosRequestConfig } from 'axios' @@ -461,10 +468,21 @@ export const zapper = createApi({ const defiType = DefiType.Staking const topLevelAsset = (() => { - const maybeLpAsset = asset.tokens.find( + const chainId = zapperNetworkToChainId(asset.network) + if (!chainId) throw new Error('chainIs is required') + + const maybeTopLevelLpAssetId = toAssetId({ + chainId, + assetNamespace: + chainId === bscChainId ? ASSET_NAMESPACE.bep20 : ASSET_NAMESPACE.erc20, + assetReference: asset.address, + }) + const maybeTopLevelLpAsset = assets[maybeTopLevelLpAssetId] + if (maybeTopLevelLpAsset?.isPool) return asset + const maybeUnderlyingLpAsset = asset.tokens.find( token => token.metaType === 'supplied' || token.metaType === 'borrowed', ) - if (maybeLpAsset) return maybeLpAsset + if (maybeUnderlyingLpAsset) return maybeUnderlyingLpAsset return asset })() diff --git a/src/state/slices/portfolioSlice/portfolioSlice.ts b/src/state/slices/portfolioSlice/portfolioSlice.ts index 4450f4eccf7..ad76f3f3014 100644 --- a/src/state/slices/portfolioSlice/portfolioSlice.ts +++ b/src/state/slices/portfolioSlice/portfolioSlice.ts @@ -1,15 +1,25 @@ import { createSlice, prepareAutoBatched } from '@reduxjs/toolkit' import { createApi } from '@reduxjs/toolkit/query/react' -import type { AccountId, ChainId } from '@shapeshiftoss/caip' -import { fromAccountId, isNft } from '@shapeshiftoss/caip' +import type { AccountId, AssetId, ChainId } from '@shapeshiftoss/caip' +import { ASSET_NAMESPACE, bscChainId, fromAccountId, isNft, toAssetId } from '@shapeshiftoss/caip' import { type Account, type EvmChainId, evmChainIds } from '@shapeshiftoss/chain-adapters' import type { AccountMetadataById } from '@shapeshiftoss/types' +import type { MinimalAsset } from '@shapeshiftoss/utils' import { makeAsset } from '@shapeshiftoss/utils' +import axios from 'axios' +import { getConfig } from 'config' import cloneDeep from 'lodash/cloneDeep' import merge from 'lodash/merge' import uniq from 'lodash/uniq' import { PURGE } from 'redux-persist' import { getChainAdapterManager } from 'context/PluginProvider/chainAdapterSingleton' +import { CHAIN_ID_TO_PORTALS_NETWORK } from 'lib/market-service/portals/constants' +import type { + GetBalancesResponse, + GetPlatformsResponse, + PlatformsById, + TokenInfo, +} from 'lib/market-service/portals/types' import { getMixPanel } from 'lib/mixpanel/mixPanelSingleton' import { MixPanelEvent } from 'lib/mixpanel/types' import { BASE_RTK_CREATE_API_CONFIG } from 'state/apis/const' @@ -179,6 +189,73 @@ type GetAccountArgs = { upsertOnFetch?: boolean } +const fetchPortalsPlatforms = async (): Promise => { + const url = `${getConfig().REACT_APP_PORTALS_BASE_URL}/v2/platforms` + + try { + const { data: platforms } = await axios.get(url, { + headers: { + Authorization: `Bearer ${getConfig().REACT_APP_PORTALS_API_KEY}`, + }, + }) + + const byId = platforms.reduce((acc, platform) => { + acc[platform.platform] = platform + return acc + }, {}) + + return byId + } catch (error) { + if (axios.isAxiosError(error)) { + console.error(`Failed to fetch Portals platforms: ${error.message}`) + } + console.error(`Failed to fetch Portals platforms: ${error}`) + + return {} + } +} + +// TODO(gomes): temp, better home +export const fetchPortalsAccount = async ( + chainId: ChainId, + owner: string, +): Promise> => { + const url = `${getConfig().REACT_APP_PORTALS_BASE_URL}/v2/account` + + const network = CHAIN_ID_TO_PORTALS_NETWORK[chainId] + + if (!network) throw new Error(`Unsupported chainId: ${chainId}`) + + try { + const { data } = await axios.get(url, { + params: { + networks: [network], + owner, + }, + headers: { + Authorization: `Bearer ${getConfig().REACT_APP_PORTALS_API_KEY}`, + }, + }) + + return data.balances.reduce>((acc, token) => { + const assetId = toAssetId({ + chainId, + assetNamespace: chainId === bscChainId ? ASSET_NAMESPACE.bep20 : ASSET_NAMESPACE.erc20, + assetReference: token.address, + }) + acc[assetId] = token + return acc + }, {}) + } catch (error) { + if (axios.isAxiosError(error)) { + console.error(`Failed to fetch Portals account: ${error.message}`) + } else { + console.error(error) + } + return {} + } +} + export const portfolioApi = createApi({ ...BASE_RTK_CREATE_API_CONFIG, reducerPath: 'portfolioApi', @@ -196,6 +273,9 @@ export const portfolioApi = createApi({ const portfolioAccounts = { [pubkey]: await adapter.getAccount(pubkey) } const nftCollectionsById = selectNftCollections(state) + const maybePortalsAccounts = await fetchPortalsAccount(chainId, pubkey) + const maybePortalsPlatforms = await fetchPortalsPlatforms() + const data = ((): Portfolio => { // add placeholder non spam assets for evm chains if (evmChainIds.includes(chainId as EvmChainId)) { @@ -208,7 +288,60 @@ export const portfolioApi = createApi({ return isSpammyTokenText(text) }) if (state.assets.byId[token.assetId] || isSpam) return prev - prev.byId[token.assetId] = makeAsset(state.assets.byId, { ...token }) + const minimalAsset: MinimalAsset = token + const maybePortalsAsset = maybePortalsAccounts[token.assetId] + if (maybePortalsAsset) { + const isPool = Boolean( + maybePortalsAsset.platform && maybePortalsAsset.tokens?.length, + ) + const platform = maybePortalsPlatforms[maybePortalsAsset.platform] + + const name = (() => { + // For single assets, just use the token name + if (!isPool) return maybePortalsAsset.name + // For pools, create a name in the format of " Pool" + // e.g "UniswapV2 ETH/FOX Pool" + const assetSymbols = + maybePortalsAsset.tokens?.map(underlyingToken => { + const assetId = toAssetId({ + chainId, + assetNamespace: + chainId === bscChainId + ? ASSET_NAMESPACE.bep20 + : ASSET_NAMESPACE.erc20, + assetReference: underlyingToken, + }) + const underlyingAsset = state.assets.byId[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 + // We could potentially use related assets for this and use primary implementation, though we'd have to remove BTC from there as WBTC and BTC are very + // much different assets on diff networks, i.e can't deposit BTC instead of WBTC automagically like you would with ETH instead of WETH + switch (underlyingAsset.symbol) { + case 'WETH': + return 'ETH' + case 'WBNB': + return 'BNB' + case 'WMATIC': + return 'MATIC' + case 'WAVAX': + return 'AVAX' + default: + return underlyingAsset.symbol + } + }) ?? [] + + // Our best effort to contruct sane name using the native asset -> asset naming hack failed, but thankfully, upstream name is very close e.g + // for "UniswapV2 LP TRUST/WETH", we just have to append "Pool" to that and we're gucci + if (assetSymbols.some(symbol => !symbol)) return `${token.name} Pool` + return `${platform.name} ${assetSymbols.join('/')} Pool` + })() + + // TODO(gomes): same name format as in assets gen + minimalAsset.name = name + minimalAsset.isPool = isPool + } + prev.byId[token.assetId] = makeAsset(state.assets.byId, minimalAsset) prev.ids.push(token.assetId) return prev }, From a2f6a6b380ad0b18fa9414abace4d269399d1816 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Mon, 2 Sep 2024 21:07:54 +0200 Subject: [PATCH 02/10] [skip ci] feat: common createThrottle --- .../utils/src/createThrottle.ts | 0 packages/utils/src/index.ts | 1 + src/lib/market-service/portals/portals.ts | 2 +- src/lib/market-service/zerion/zerion.ts | 2 +- .../slices/portfolioSlice/portfolioSlice.ts | 57 +++++++++++++++++-- 5 files changed, 56 insertions(+), 6 deletions(-) rename src/lib/market-service/utils.ts => packages/utils/src/createThrottle.ts (100%) diff --git a/src/lib/market-service/utils.ts b/packages/utils/src/createThrottle.ts similarity index 100% rename from src/lib/market-service/utils.ts rename to packages/utils/src/createThrottle.ts diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 8f20df69e14..08dc654bf5e 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -12,6 +12,7 @@ export * from './baseUnits/baseUnits' export * from './promises' export * from './treasury' export * from './timeout' +export * from './createThrottle' export const isSome = (option: T | null | undefined): option is T => !isUndefined(option) && !isNull(option) diff --git a/src/lib/market-service/portals/portals.ts b/src/lib/market-service/portals/portals.ts index 57d938c0135..3bff0762fff 100644 --- a/src/lib/market-service/portals/portals.ts +++ b/src/lib/market-service/portals/portals.ts @@ -10,6 +10,7 @@ import type { PriceHistoryArgs, } from '@shapeshiftoss/types' import { HistoryTimeframe } from '@shapeshiftoss/types' +import { createThrottle } from '@shapeshiftoss/utils' import Axios from 'axios' import { setupCache } from 'axios-cache-interceptor' import { getConfig } from 'config' @@ -22,7 +23,6 @@ import { assertUnreachable, getTimeFrameBounds, isToken } from 'lib/utils' import generatedAssetData from '../../asset-service/service/generatedAssetData.json' import type { MarketService } from '../api' import { DEFAULT_CACHE_TTL_MS } from '../config' -import { createThrottle } from '../utils' import { isValidDate } from '../utils/isValidDate' import { CHAIN_ID_TO_PORTALS_NETWORK } from './constants' import type { GetTokensResponse, HistoryResponse } from './types' diff --git a/src/lib/market-service/zerion/zerion.ts b/src/lib/market-service/zerion/zerion.ts index 82ff1ce3fd8..e35198bdc1b 100644 --- a/src/lib/market-service/zerion/zerion.ts +++ b/src/lib/market-service/zerion/zerion.ts @@ -16,6 +16,7 @@ import { type PriceHistoryArgs, ZERION_CHAINS_MAP, } from '@shapeshiftoss/types' +import { createThrottle } from '@shapeshiftoss/utils' import Axios from 'axios' import { setupCache } from 'axios-cache-interceptor' import { getConfig } from 'config' @@ -25,7 +26,6 @@ import { assertUnreachable, isToken } from 'lib/utils' import type { MarketService } from '../api' import { DEFAULT_CACHE_TTL_MS } from '../config' -import { createThrottle } from '../utils' import type { ListFungiblesResponse, ZerionChartResponse, diff --git a/src/state/slices/portfolioSlice/portfolioSlice.ts b/src/state/slices/portfolioSlice/portfolioSlice.ts index ad76f3f3014..56b83247192 100644 --- a/src/state/slices/portfolioSlice/portfolioSlice.ts +++ b/src/state/slices/portfolioSlice/portfolioSlice.ts @@ -256,6 +256,12 @@ export const fetchPortalsAccount = async ( } } +const maybeTokenImage = (image: string | undefined) => { + if (!image) return + if (image === 'missing_large.png') return + return image +} + export const portfolioApi = createApi({ ...BASE_RTK_CREATE_API_CONFIG, reducerPath: 'portfolioApi', @@ -288,7 +294,7 @@ export const portfolioApi = createApi({ return isSpammyTokenText(text) }) if (state.assets.byId[token.assetId] || isSpam) return prev - const minimalAsset: MinimalAsset = token + let minimalAsset: MinimalAsset = token const maybePortalsAsset = maybePortalsAccounts[token.assetId] if (maybePortalsAsset) { const isPool = Boolean( @@ -337,9 +343,52 @@ export const portfolioApi = createApi({ return `${platform.name} ${assetSymbols.join('/')} Pool` })() - // TODO(gomes): same name format as in assets gen - minimalAsset.name = name - minimalAsset.isPool = isPool + const images = maybePortalsAsset.images ?? [] + const [, ...underlyingAssetsImages] = images + const iconOrIcons = (() => { + // There are no underlying tokens' images, return asset icon if it exists + if (!underlyingAssetsImages?.length) + return { icon: state.assets.byId[token.assetId]?.icon } + + if (underlyingAssetsImages.length === 1) { + return { + icon: maybeTokenImage( + maybePortalsAsset.image || underlyingAssetsImages[0], + ), + } + } + // This is a multiple assets pool, populate icons array + if (underlyingAssetsImages.length > 1) + return { + icons: underlyingAssetsImages.map((underlyingAssetsImage, i) => { + // No token at that index, but this isn't reliable as we've found out, it may be missing in tokens but present in images + // However, this has to be an early return and we can't use our own flavour of that asset... because we have no idea which asset it is. + if (!maybePortalsAsset.tokens[i]) + return maybeTokenImage(underlyingAssetsImage) + + const underlyingAssetId = toAssetId({ + chainId, + assetNamespace: + chainId === bscChainId + ? ASSET_NAMESPACE.bep20 + : ASSET_NAMESPACE.erc20, + assetReference: maybePortalsAsset.tokens[i], + }) + const underlyingAsset = state.assets.byId[underlyingAssetId] + // Prioritise our own flavour of icons for that asset if available, else use upstream if present + return underlyingAsset?.icon || maybeTokenImage(underlyingAssetsImage) + }), + icon: undefined, + } + })() + + // @ts-ignore this is the best we can do, some icons *may* be missing + minimalAsset = { + ...minimalAsset, + name, + isPool, + ...iconOrIcons, + } } prev.byId[token.assetId] = makeAsset(state.assets.byId, minimalAsset) prev.ids.push(token.assetId) From 031ad1d7498a44ffb8119ea245b6662653abad75 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Mon, 2 Sep 2024 22:36:29 +0200 Subject: [PATCH 03/10] feat: cleanup --- .../slices/portfolioSlice/portfolioSlice.ts | 79 +----------------- .../{utils.ts => utils/index.ts} | 4 +- .../slices/portfolioSlice/utils/portals.ts | 82 +++++++++++++++++++ 3 files changed, 86 insertions(+), 79 deletions(-) rename src/state/slices/portfolioSlice/{utils.ts => utils/index.ts} (99%) create mode 100644 src/state/slices/portfolioSlice/utils/portals.ts diff --git a/src/state/slices/portfolioSlice/portfolioSlice.ts b/src/state/slices/portfolioSlice/portfolioSlice.ts index 56b83247192..c9cc4fb1eb4 100644 --- a/src/state/slices/portfolioSlice/portfolioSlice.ts +++ b/src/state/slices/portfolioSlice/portfolioSlice.ts @@ -1,25 +1,16 @@ import { createSlice, prepareAutoBatched } from '@reduxjs/toolkit' import { createApi } from '@reduxjs/toolkit/query/react' -import type { AccountId, AssetId, ChainId } from '@shapeshiftoss/caip' +import type { AccountId, ChainId } from '@shapeshiftoss/caip' import { ASSET_NAMESPACE, bscChainId, fromAccountId, isNft, toAssetId } from '@shapeshiftoss/caip' import { type Account, type EvmChainId, evmChainIds } from '@shapeshiftoss/chain-adapters' import type { AccountMetadataById } from '@shapeshiftoss/types' import type { MinimalAsset } from '@shapeshiftoss/utils' import { makeAsset } from '@shapeshiftoss/utils' -import axios from 'axios' -import { getConfig } from 'config' import cloneDeep from 'lodash/cloneDeep' import merge from 'lodash/merge' import uniq from 'lodash/uniq' import { PURGE } from 'redux-persist' import { getChainAdapterManager } from 'context/PluginProvider/chainAdapterSingleton' -import { CHAIN_ID_TO_PORTALS_NETWORK } from 'lib/market-service/portals/constants' -import type { - GetBalancesResponse, - GetPlatformsResponse, - PlatformsById, - TokenInfo, -} from 'lib/market-service/portals/types' import { getMixPanel } from 'lib/mixpanel/mixPanelSingleton' import { MixPanelEvent } from 'lib/mixpanel/types' import { BASE_RTK_CREATE_API_CONFIG } from 'state/apis/const' @@ -32,6 +23,7 @@ import { assets as assetSlice } from '../assetsSlice/assetsSlice' import type { Portfolio, WalletId } from './portfolioSliceCommon' import { initialState } from './portfolioSliceCommon' import { accountToPortfolio, haveSameElements } from './utils' +import { fetchPortalsAccount, fetchPortalsPlatforms } from './utils/portals' type WalletMetaPayload = { walletId: WalletId @@ -189,73 +181,6 @@ type GetAccountArgs = { upsertOnFetch?: boolean } -const fetchPortalsPlatforms = async (): Promise => { - const url = `${getConfig().REACT_APP_PORTALS_BASE_URL}/v2/platforms` - - try { - const { data: platforms } = await axios.get(url, { - headers: { - Authorization: `Bearer ${getConfig().REACT_APP_PORTALS_API_KEY}`, - }, - }) - - const byId = platforms.reduce((acc, platform) => { - acc[platform.platform] = platform - return acc - }, {}) - - return byId - } catch (error) { - if (axios.isAxiosError(error)) { - console.error(`Failed to fetch Portals platforms: ${error.message}`) - } - console.error(`Failed to fetch Portals platforms: ${error}`) - - return {} - } -} - -// TODO(gomes): temp, better home -export const fetchPortalsAccount = async ( - chainId: ChainId, - owner: string, -): Promise> => { - const url = `${getConfig().REACT_APP_PORTALS_BASE_URL}/v2/account` - - const network = CHAIN_ID_TO_PORTALS_NETWORK[chainId] - - if (!network) throw new Error(`Unsupported chainId: ${chainId}`) - - try { - const { data } = await axios.get(url, { - params: { - networks: [network], - owner, - }, - headers: { - Authorization: `Bearer ${getConfig().REACT_APP_PORTALS_API_KEY}`, - }, - }) - - return data.balances.reduce>((acc, token) => { - const assetId = toAssetId({ - chainId, - assetNamespace: chainId === bscChainId ? ASSET_NAMESPACE.bep20 : ASSET_NAMESPACE.erc20, - assetReference: token.address, - }) - acc[assetId] = token - return acc - }, {}) - } catch (error) { - if (axios.isAxiosError(error)) { - console.error(`Failed to fetch Portals account: ${error.message}`) - } else { - console.error(error) - } - return {} - } -} - const maybeTokenImage = (image: string | undefined) => { if (!image) return if (image === 'missing_large.png') return diff --git a/src/state/slices/portfolioSlice/utils.ts b/src/state/slices/portfolioSlice/utils/index.ts similarity index 99% rename from src/state/slices/portfolioSlice/utils.ts rename to src/state/slices/portfolioSlice/utils/index.ts index 2c34aabf365..1c2c6d64226 100644 --- a/src/state/slices/portfolioSlice/utils.ts +++ b/src/state/slices/portfolioSlice/utils/index.ts @@ -53,8 +53,8 @@ import type { Portfolio, PortfolioAccountBalancesById, PortfolioAccounts as PortfolioSliceAccounts, -} from './portfolioSliceCommon' -import { initialState } from './portfolioSliceCommon' +} from '../portfolioSliceCommon' +import { initialState } from '../portfolioSliceCommon' // note - this isn't a selector, just a pure utility function export const accountIdToLabel = (accountId: AccountId): string => { diff --git a/src/state/slices/portfolioSlice/utils/portals.ts b/src/state/slices/portfolioSlice/utils/portals.ts new file mode 100644 index 00000000000..8c5c1730dd9 --- /dev/null +++ b/src/state/slices/portfolioSlice/utils/portals.ts @@ -0,0 +1,82 @@ +import { + ASSET_NAMESPACE, + type AssetId, + bscChainId, + type ChainId, + toAssetId, +} from '@shapeshiftoss/caip' +import axios from 'axios' +import { getConfig } from 'config' +import { CHAIN_ID_TO_PORTALS_NETWORK } from 'lib/market-service/portals/constants' +import type { + GetBalancesResponse, + GetPlatformsResponse, + PlatformsById, + TokenInfo, +} from 'lib/market-service/portals/types' + +export const fetchPortalsPlatforms = async (): Promise => { + const url = `${getConfig().REACT_APP_PORTALS_BASE_URL}/v2/platforms` + + try { + const { data: platforms } = await axios.get(url, { + headers: { + Authorization: `Bearer ${getConfig().REACT_APP_PORTALS_API_KEY}`, + }, + }) + + const byId = platforms.reduce((acc, platform) => { + acc[platform.platform] = platform + return acc + }, {}) + + return byId + } catch (error) { + if (axios.isAxiosError(error)) { + console.error(`Failed to fetch Portals platforms: ${error.message}`) + } + console.error(`Failed to fetch Portals platforms: ${error}`) + + return {} + } +} + +export const fetchPortalsAccount = async ( + chainId: ChainId, + owner: string, +): Promise> => { + const url = `${getConfig().REACT_APP_PORTALS_BASE_URL}/v2/account` + + const network = CHAIN_ID_TO_PORTALS_NETWORK[chainId] + + if (!network) throw new Error(`Unsupported chainId: ${chainId}`) + + try { + const { data } = await axios.get(url, { + params: { + networks: [network], + owner, + }, + headers: { + Authorization: `Bearer ${getConfig().REACT_APP_PORTALS_API_KEY}`, + }, + }) + + return data.balances.reduce>((acc, token) => { + const assetId = toAssetId({ + chainId, + assetNamespace: chainId === bscChainId ? ASSET_NAMESPACE.bep20 : ASSET_NAMESPACE.erc20, + assetReference: token.address, + }) + acc[assetId] = token + return acc + }, {}) + } catch (error) { + if (axios.isAxiosError(error)) { + console.error(`Failed to fetch Portals account: ${error.message}`) + } else { + console.error(error) + } + return {} + } +} From eab564cc560710a54fd8e7058ee32ad7cb80b76f Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Tue, 3 Sep 2024 11:48:24 +0200 Subject: [PATCH 04/10] feat: revert fluff --- .../ProviderDetails/OpportunityRow.tsx | 5 +--- src/state/apis/zapper/zapperApi.ts | 24 +++---------------- 2 files changed, 4 insertions(+), 25 deletions(-) diff --git a/src/components/EarnDashboard/components/ProviderDetails/OpportunityRow.tsx b/src/components/EarnDashboard/components/ProviderDetails/OpportunityRow.tsx index af4526fc8b7..896acadaaa3 100644 --- a/src/components/EarnDashboard/components/ProviderDetails/OpportunityRow.tsx +++ b/src/components/EarnDashboard/components/ProviderDetails/OpportunityRow.tsx @@ -86,9 +86,6 @@ export const OpportunityRow: React.FC< const handleClick = useCallback( (action: DefiAction) => { - if (asset?.isPool) { - return history.push(`/trade/${assetId}`) - } if (opportunity.isReadOnly) { const url = getMetadataForProvider(opportunity.provider)?.url url && window.open(url, '_blank') @@ -96,7 +93,7 @@ export const OpportunityRow: React.FC< } onClick(opportunity, action) }, - [asset?.isPool, assetId, history, onClick, opportunity], + [onClick, opportunity], ) const handleClaimClick = useCallback(() => handleClick(DefiAction.Claim), [handleClick]) diff --git a/src/state/apis/zapper/zapperApi.ts b/src/state/apis/zapper/zapperApi.ts index c53525c29dc..512c5518edf 100644 --- a/src/state/apis/zapper/zapperApi.ts +++ b/src/state/apis/zapper/zapperApi.ts @@ -1,13 +1,6 @@ import { createApi } from '@reduxjs/toolkit/dist/query/react' import type { AccountId, AssetId } from '@shapeshiftoss/caip' -import { - ASSET_NAMESPACE, - bscChainId, - ethChainId, - fromAssetId, - toAccountId, - toAssetId, -} from '@shapeshiftoss/caip' +import { ethChainId, fromAssetId, toAccountId, toAssetId } from '@shapeshiftoss/caip' import { evmChainIds } from '@shapeshiftoss/chain-adapters' import { makeAsset } from '@shapeshiftoss/utils' import type { AxiosRequestConfig } from 'axios' @@ -468,21 +461,10 @@ export const zapper = createApi({ const defiType = DefiType.Staking const topLevelAsset = (() => { - const chainId = zapperNetworkToChainId(asset.network) - if (!chainId) throw new Error('chainIs is required') - - const maybeTopLevelLpAssetId = toAssetId({ - chainId, - assetNamespace: - chainId === bscChainId ? ASSET_NAMESPACE.bep20 : ASSET_NAMESPACE.erc20, - assetReference: asset.address, - }) - const maybeTopLevelLpAsset = assets[maybeTopLevelLpAssetId] - if (maybeTopLevelLpAsset?.isPool) return asset - const maybeUnderlyingLpAsset = asset.tokens.find( + const maybeLpAsset = asset.tokens.find( token => token.metaType === 'supplied' || token.metaType === 'borrowed', ) - if (maybeUnderlyingLpAsset) return maybeUnderlyingLpAsset + if (maybeLpAsset) return maybeLpAsset return asset })() From 146cb14650bbec583a5dd0b0c340cd82dd8330fd Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Tue, 3 Sep 2024 11:48:49 +0200 Subject: [PATCH 05/10] feat: migration --- src/state/migrations/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/state/migrations/index.ts b/src/state/migrations/index.ts index b2cb3ad22e3..d93d747282c 100644 --- a/src/state/migrations/index.ts +++ b/src/state/migrations/index.ts @@ -107,4 +107,5 @@ export const migrations = { 96: clearTxHistory, 97: clearAssets, 98: clearAssets, + 99: clearAssets, } From 76a31049edd51422c961c8f43e2f969bca56e238 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Tue, 3 Sep 2024 11:58:01 +0200 Subject: [PATCH 06/10] feat: more cleanup --- src/state/slices/portfolioSlice/portfolioSlice.ts | 8 +------- src/state/slices/portfolioSlice/utils/portals.ts | 6 ++++++ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/state/slices/portfolioSlice/portfolioSlice.ts b/src/state/slices/portfolioSlice/portfolioSlice.ts index c9cc4fb1eb4..76f85f255c1 100644 --- a/src/state/slices/portfolioSlice/portfolioSlice.ts +++ b/src/state/slices/portfolioSlice/portfolioSlice.ts @@ -23,7 +23,7 @@ import { assets as assetSlice } from '../assetsSlice/assetsSlice' import type { Portfolio, WalletId } from './portfolioSliceCommon' import { initialState } from './portfolioSliceCommon' import { accountToPortfolio, haveSameElements } from './utils' -import { fetchPortalsAccount, fetchPortalsPlatforms } from './utils/portals' +import { fetchPortalsAccount, fetchPortalsPlatforms, maybeTokenImage } from './utils/portals' type WalletMetaPayload = { walletId: WalletId @@ -181,12 +181,6 @@ type GetAccountArgs = { upsertOnFetch?: boolean } -const maybeTokenImage = (image: string | undefined) => { - if (!image) return - if (image === 'missing_large.png') return - return image -} - export const portfolioApi = createApi({ ...BASE_RTK_CREATE_API_CONFIG, reducerPath: 'portfolioApi', diff --git a/src/state/slices/portfolioSlice/utils/portals.ts b/src/state/slices/portfolioSlice/utils/portals.ts index 8c5c1730dd9..a8dba006299 100644 --- a/src/state/slices/portfolioSlice/utils/portals.ts +++ b/src/state/slices/portfolioSlice/utils/portals.ts @@ -80,3 +80,9 @@ export const fetchPortalsAccount = async ( return {} } } + +export const maybeTokenImage = (image: string | undefined) => { + if (!image) return + if (image === 'missing_large.png') return + return image +} From 7ebbe3f576d2b656c21b7374852dcb32f52223cc Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Tue, 10 Sep 2024 22:02:41 +0200 Subject: [PATCH 07/10] feat: move portals utils to src/utils --- .../portfolioSlice/utils/portals.ts => lib/portals/utils.ts} | 0 src/state/slices/portfolioSlice/portfolioSlice.ts | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename src/{state/slices/portfolioSlice/utils/portals.ts => lib/portals/utils.ts} (100%) diff --git a/src/state/slices/portfolioSlice/utils/portals.ts b/src/lib/portals/utils.ts similarity index 100% rename from src/state/slices/portfolioSlice/utils/portals.ts rename to src/lib/portals/utils.ts diff --git a/src/state/slices/portfolioSlice/portfolioSlice.ts b/src/state/slices/portfolioSlice/portfolioSlice.ts index f77eddab72e..b0de688a3ef 100644 --- a/src/state/slices/portfolioSlice/portfolioSlice.ts +++ b/src/state/slices/portfolioSlice/portfolioSlice.ts @@ -14,6 +14,7 @@ import { PURGE } from 'redux-persist' import { getChainAdapterManager } from 'context/PluginProvider/chainAdapterSingleton' import { getMixPanel } from 'lib/mixpanel/mixPanelSingleton' import { MixPanelEvent } from 'lib/mixpanel/types' +import { fetchPortalsAccount, fetchPortalsPlatforms, maybeTokenImage } from 'lib/portals/utils' import { BASE_RTK_CREATE_API_CONFIG } from 'state/apis/const' import { isSpammyNftText, isSpammyTokenText } from 'state/apis/nft/constants' import { selectNftCollections } from 'state/apis/nft/selectors' @@ -24,7 +25,6 @@ import { assets as assetSlice } from '../assetsSlice/assetsSlice' import type { Portfolio, WalletId } from './portfolioSliceCommon' import { initialState } from './portfolioSliceCommon' import { accountToPortfolio, haveSameElements } from './utils' -import { fetchPortalsAccount, fetchPortalsPlatforms, maybeTokenImage } from './utils/portals' type WalletMetaPayload = { walletId: WalletId From a179d895f06ba0df6c21db012871689f40ce7b6b Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Tue, 10 Sep 2024 22:04:22 +0200 Subject: [PATCH 08/10] feat: lsp mang --- src/lib/portals/utils.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/lib/portals/utils.ts b/src/lib/portals/utils.ts index a8dba006299..f7c8cbaadd8 100644 --- a/src/lib/portals/utils.ts +++ b/src/lib/portals/utils.ts @@ -1,10 +1,5 @@ -import { - ASSET_NAMESPACE, - type AssetId, - bscChainId, - type ChainId, - toAssetId, -} from '@shapeshiftoss/caip' +import type { AssetId, ChainId } from '@shapeshiftoss/caip' +import { ASSET_NAMESPACE, bscChainId, toAssetId } from '@shapeshiftoss/caip' import axios from 'axios' import { getConfig } from 'config' import { CHAIN_ID_TO_PORTALS_NETWORK } from 'lib/market-service/portals/constants' From dbc66bde92fee982fa9ec5e4dd41a93716f4b4f2 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Tue, 10 Sep 2024 22:05:31 +0200 Subject: [PATCH 09/10] feat: colocate utils/index.ts and utils/index.test.ts --- .../portfolioSlice/{utils.test.ts => utils/index.test.ts} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/state/slices/portfolioSlice/{utils.test.ts => utils/index.test.ts} (98%) diff --git a/src/state/slices/portfolioSlice/utils.test.ts b/src/state/slices/portfolioSlice/utils/index.test.ts similarity index 98% rename from src/state/slices/portfolioSlice/utils.test.ts rename to src/state/slices/portfolioSlice/utils/index.test.ts index b77f6d90e8d..1f654ddb9ac 100644 --- a/src/state/slices/portfolioSlice/utils.test.ts +++ b/src/state/slices/portfolioSlice/utils/index.test.ts @@ -13,7 +13,7 @@ import { describe, expect, it, vi } from 'vitest' import { trimWithEndEllipsis } from 'lib/utils' import { accountIdToFeeAssetId } from 'lib/utils/accounts' -import { accountIdToLabel, findAccountsByAssetId } from './utils' +import { accountIdToLabel, findAccountsByAssetId } from '.' vi.mock('context/PluginProvider/chainAdapterSingleton', () => ({ getChainAdapterManager: () => mockChainAdapters, From 9256068b8b4085d35f665a25618799278a1a3b98 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Tue, 10 Sep 2024 22:07:27 +0200 Subject: [PATCH 10/10] feat: move within IIFE --- src/state/slices/portfolioSlice/portfolioSlice.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/state/slices/portfolioSlice/portfolioSlice.ts b/src/state/slices/portfolioSlice/portfolioSlice.ts index b0de688a3ef..512b667413a 100644 --- a/src/state/slices/portfolioSlice/portfolioSlice.ts +++ b/src/state/slices/portfolioSlice/portfolioSlice.ts @@ -199,12 +199,11 @@ export const portfolioApi = createApi({ const portfolioAccounts = { [pubkey]: await adapter.getAccount(pubkey) } const nftCollectionsById = selectNftCollections(state) - const maybePortalsAccounts = await fetchPortalsAccount(chainId, pubkey) - const maybePortalsPlatforms = await fetchPortalsPlatforms() - - const data = ((): Portfolio => { + const data = await (async (): Promise => { // add placeholder non spam assets for evm chains if (evmChainIds.includes(chainId as EvmChainId)) { + const maybePortalsAccounts = await fetchPortalsAccount(chainId, pubkey) + const maybePortalsPlatforms = await fetchPortalsPlatforms() const account = portfolioAccounts[pubkey] as Account const assets = (account.chainSpecific.tokens ?? []).reduce(