diff --git a/background/redux-slices/assets.ts b/background/redux-slices/assets.ts index 5feaf15872..de2b11a46a 100644 --- a/background/redux-slices/assets.ts +++ b/background/redux-slices/assets.ts @@ -11,18 +11,24 @@ import { } from "../assets" import { ERC20_INTERFACE } from "../lib/erc20" import logger from "../lib/logger" -import { EVMNetwork, sameNetwork } from "../networks" +import { EVMNetwork, NetworkBaseAsset, sameNetwork } from "../networks" import { NormalizedEVMAddress } from "../types" import { removeAssetReferences, updateAssetReferences } from "./accounts" import { createBackgroundAsyncThunk } from "./utils" -import { isBaseAssetForNetwork, isSameAsset } from "./utils/asset-utils" +import { + FullAssetID, + getFullAssetID, + isBaseAssetForNetwork, + isBaselineTrustedAsset, + isNetworkBaseAsset, +} from "./utils/asset-utils" import { getProvider } from "./utils/contract-utils" export type SingleAssetState = AnyAsset -export type AssetsState = SingleAssetState[] +export type AssetsState = { [assetID: FullAssetID]: SingleAssetState } -export const initialState: AssetsState = [] +export const initialState: AssetsState = {} const assetsSlice = createSlice({ name: "assets", @@ -30,61 +36,52 @@ const assetsSlice = createSlice({ reducers: { assetsLoaded: ( immerState, - { payload: newAssets }: { payload: AnyAsset[] } + { + payload: newAssets, + }: { payload: (NetworkBaseAsset | SmartContractFungibleAsset)[] } ) => { - const mappedAssets: { [sym: string]: SingleAssetState[] } = {} - // bin existing known assets - immerState.forEach((asset) => { - if (mappedAssets[asset.symbol] === undefined) { - mappedAssets[asset.symbol] = [] - } - // if an asset is already in state, assume unique checks have been done - // no need to check network, contract address, etc - mappedAssets[asset.symbol].push(asset) - }) - // merge in new assets newAssets.forEach((newAsset) => { - if (mappedAssets[newAsset.symbol] === undefined) { - mappedAssets[newAsset.symbol] = [ - { - ...newAsset, - }, - ] - } else { - const duplicateIndexes = mappedAssets[newAsset.symbol].reduce< - number[] - >((acc, existingAsset, id) => { - if (isSameAsset(newAsset, existingAsset)) { - acc.push(id) - } - return acc - }, []) + const newAssetId = getFullAssetID(newAsset) + + const existing = immerState[newAssetId] + if (existing) { + // Update all properties except metadata for network base assets + if (isNetworkBaseAsset(existing)) { + const { metadata: _, ...rest } = newAsset + Object.assign(existing, rest) + } - // if there aren't duplicates, add the asset - if (duplicateIndexes.length === 0) { - mappedAssets[newAsset.symbol].push({ - ...newAsset, - }) - } else { - // TODO if there are duplicates... when should we replace assets? - duplicateIndexes.forEach((id) => { - // Update only the metadata for the duplicate - mappedAssets[newAsset.symbol][id] = { - ...mappedAssets[newAsset.symbol][id], - metadata: newAsset.metadata, + if (isSmartContractFungibleAsset(existing) && newAsset.metadata) { + existing.metadata ??= {} + + // Update verified status, token lists or discovery txs for custom assets + if (!isBaselineTrustedAsset(existing)) { + existing.metadata.verified = (( + newAsset + ))?.metadata?.verified + + if (newAsset.metadata?.tokenLists?.length) { + existing.metadata.tokenLists = newAsset.metadata.tokenLists + } + + if ("discoveryTxHash" in newAsset.metadata) { + existing.metadata.discoveryTxHash = + newAsset.metadata.discoveryTxHash } - }) + } } + } else { + immerState[newAssetId] = newAsset } }) - - return Object.values(mappedAssets).flat() }, removeAsset: ( immerState, - { payload: removedAsset }: { payload: AnyAsset } + { + payload: removedAsset, + }: { payload: NetworkBaseAsset | SmartContractFungibleAsset } ) => { - return immerState.filter((asset) => !isSameAsset(asset, removedAsset)) + delete immerState[getFullAssetID(removedAsset)] }, }, }) diff --git a/background/redux-slices/migrations/to-34.ts b/background/redux-slices/migrations/to-34.ts index fc989f4865..1a17af8007 100644 --- a/background/redux-slices/migrations/to-34.ts +++ b/background/redux-slices/migrations/to-34.ts @@ -21,7 +21,7 @@ type OldState = { } type NewState = { - assets: unknown[] + assets: Record account: { accountsData: { evm: { @@ -62,6 +62,6 @@ export default (prevState: Record): NewState => { return { ...typedPrevState, - assets: [], + assets: {}, } } diff --git a/background/redux-slices/selectors/0xSwapSelectors.ts b/background/redux-slices/selectors/0xSwapSelectors.ts index 44c4f79f53..59df803aba 100644 --- a/background/redux-slices/selectors/0xSwapSelectors.ts +++ b/background/redux-slices/selectors/0xSwapSelectors.ts @@ -19,7 +19,7 @@ export const selectSwapBuyAssets = createSelector( (state: RootState) => state.assets, selectCurrentNetwork, (assets, currentNetwork) => { - return assets.filter((asset): asset is SwappableAsset => { + return Object.values(assets).filter((asset): asset is SwappableAsset => { // Only list assets for the current network. const assetIsOnCurrentNetwork = isBaseAssetForNetwork(asset, currentNetwork) || diff --git a/background/redux-slices/selectors/uiSelectors.ts b/background/redux-slices/selectors/uiSelectors.ts index 1ee380a985..8b663d09a4 100644 --- a/background/redux-slices/selectors/uiSelectors.ts +++ b/background/redux-slices/selectors/uiSelectors.ts @@ -50,11 +50,3 @@ export const selectMainCurrencySign = createSelector( () => null, () => hardcodedMainCurrencySign ) - -export const selectMainCurrency = createSelector( - (state: RootState) => state.ui, - (state: RootState) => state.assets, - (state: RootState) => selectMainCurrencySymbol(state), - (_, assets, mainCurrencySymbol) => - assets.find((asset) => asset.symbol === mainCurrencySymbol) -) diff --git a/background/redux-slices/tests/assets.unit.test.ts b/background/redux-slices/tests/assets.unit.test.ts index d5637c8b92..1ad682c43a 100644 --- a/background/redux-slices/tests/assets.unit.test.ts +++ b/background/redux-slices/tests/assets.unit.test.ts @@ -29,16 +29,16 @@ const pricesState: PricesState = { describe("Reducers", () => { describe("assetsLoaded", () => { test("updates cached asset metadata", () => { - const state = reducer([], assetsLoaded([asset])) + const state = reducer({}, assetsLoaded([asset])) - expect(state[0].metadata?.verified).not.toBeDefined() + expect(state[getFullAssetID(asset)].metadata?.verified).not.toBeDefined() const newState = reducer( state, assetsLoaded([{ ...asset, metadata: { verified: true } }]) ) - expect(newState[0].metadata?.verified).toBeTruthy() + expect(newState[getFullAssetID(asset)].metadata?.verified).toBeTruthy() }) }) }) diff --git a/background/redux-slices/utils/0x-swap-utils.ts b/background/redux-slices/utils/0x-swap-utils.ts index 16fe2231a8..f12c9ba091 100644 --- a/background/redux-slices/utils/0x-swap-utils.ts +++ b/background/redux-slices/utils/0x-swap-utils.ts @@ -48,7 +48,7 @@ export async function getAssetPricePoint( network: EVMNetwork ): Promise { // FIXME: review - const assetPricesNetworks = assets + const assetPricesNetworks = Object.values(assets) .filter( (assetItem) => "contractAddress" in assetItem && diff --git a/background/services/indexing/index.ts b/background/services/indexing/index.ts index e2a12c0212..dbfccc02bf 100644 --- a/background/services/indexing/index.ts +++ b/background/services/indexing/index.ts @@ -1,9 +1,8 @@ import logger from "../../lib/logger" import { HexString } from "../../types" -import { EVMNetwork, sameNetwork } from "../../networks" +import { EVMNetwork, NetworkBaseAsset, sameNetwork } from "../../networks" import { AccountBalance, AddressOnNetwork } from "../../accounts" import { - AnyAsset, AnyAssetMetadata, FungibleAsset, isSmartContractFungibleAsset, @@ -61,6 +60,8 @@ const FAST_TOKEN_REFRESH_BLOCK_RANGE = 10 // before balance-checking them. const ACCELERATED_TOKEN_REFRESH_TIMEOUT = 300 +type IndexedAsset = SmartContractFungibleAsset | NetworkBaseAsset + interface Events extends ServiceLifecycleEvents { accountsWithBalances: { /** @@ -75,7 +76,7 @@ interface Events extends ServiceLifecycleEvents { addressOnNetwork: AddressOnNetwork } prices: PricePoint[] - assets: AnyAsset[] + assets: IndexedAsset[] refreshAsset: SmartContractFungibleAsset removeAssetData: SmartContractFungibleAsset } @@ -133,7 +134,7 @@ export default class IndexingService extends BaseService { private lastPriceAlarmTime = 0 - private cachedAssets: Record = + private cachedAssets: Record = Object.fromEntries( Object.keys(NETWORK_BY_CHAIN_ID).map((network) => [network, []]) ) @@ -272,7 +273,7 @@ export default class IndexingService extends BaseService { * @returns An array of assets, including network base assets, token list * assets and custom assets. */ - getCachedAssets(network: EVMNetwork): AnyAsset[] { + getCachedAssets(network: EVMNetwork): IndexedAsset[] { return this.cachedAssets[network.chainID] ?? [] } @@ -288,7 +289,7 @@ export default class IndexingService extends BaseService { await this.preferenceService.getTokenListPreferences() const tokenLists = await this.db.getLatestTokenLists(tokenListPrefs.urls) - this.cachedAssets[network.chainID] = mergeAssets( + this.cachedAssets[network.chainID] = mergeAssets( [network.baseAsset], customAssets, networkAssetsFromLists(network, tokenLists) diff --git a/background/tests/earn/assets.mock.ts b/background/tests/earn/assets.mock.ts index a322c10721..ac652c81b9 100644 --- a/background/tests/earn/assets.mock.ts +++ b/background/tests/earn/assets.mock.ts @@ -3,22 +3,25 @@ import { ETHEREUM } from "../../constants" import { PricesState } from "../../redux-slices/prices" import { getFullAssetID } from "../../redux-slices/utils/asset-utils" -export const assets: SmartContractFungibleAsset[] = [ - { - name: "Wrapped Ether", - symbol: "WETH", - decimals: 18, - homeNetwork: ETHEREUM, - contractAddress: "0x0", - }, - { - name: "Uniswap", - symbol: "UNI", - decimals: 18, - homeNetwork: ETHEREUM, - contractAddress: "0x0", - }, -] +export const assets: Record = + Object.fromEntries( + [ + { + name: "Wrapped Ether", + symbol: "WETH", + decimals: 18, + homeNetwork: ETHEREUM, + contractAddress: "0x0", + }, + { + name: "Uniswap", + symbol: "UNI", + decimals: 18, + homeNetwork: ETHEREUM, + contractAddress: "0x0", + }, + ].map((asset) => [getFullAssetID(asset), asset]) + ) export const prices: PricesState = { [getFullAssetID(assets[0])]: {