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

fix: import all detected tokens automatically #29357

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 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
12 changes: 12 additions & 0 deletions ui/components/app/assets/asset-list/asset-list.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,18 @@ jest.mock('../../../../store/actions', () => {
})),
tokenBalancesStartPolling: jest.fn().mockResolvedValue('pollingToken'),
tokenBalancesStopPollingByPollingToken: jest.fn(),
addImportedTokens: jest.fn(),
};
});

// Mock the dispatch function
const mockDispatch = jest.fn();

jest.mock('react-redux', () => {
const actual = jest.requireActual('react-redux');
return {
...actual,
useDispatch: () => mockDispatch,
};
});

Expand Down
137 changes: 108 additions & 29 deletions ui/components/app/assets/asset-list/asset-list.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import React, { useContext, useState } from 'react';
import { useSelector } from 'react-redux';
import React, { useContext, useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Token } from '@metamask/assets-controllers';
import { NetworkConfiguration } from '@metamask/network-controller';
import TokenList from '../token-list';
import { PRIMARY } from '../../../../helpers/constants/common';
import { useUserPreferencedCurrency } from '../../../../hooks/useUserPreferencedCurrency';
import {
getAllDetectedTokensForSelectedAddress,
getDetectedTokensInCurrentNetwork,
getIstokenDetectionInactiveOnNonMainnetSupportedNetwork,
getIsTokenNetworkFilterEqualCurrentNetwork,
getSelectedAccount,
getSelectedAddress,
getUseTokenDetection,
} from '../../../../selectors';
import {
getMultichainIsEvm,
Expand All @@ -23,9 +26,10 @@ import { MetaMetricsContext } from '../../../../contexts/metametrics';
import {
MetaMetricsEventCategory,
MetaMetricsEventName,
MetaMetricsTokenEventSource,
} from '../../../../../shared/constants/metametrics';
import DetectedToken from '../../detected-token/detected-token';
import { DetectedTokensBanner, ReceiveModal } from '../../../multichain';
import { ReceiveModal } from '../../../multichain';
import { useI18nContext } from '../../../../hooks/useI18nContext';
import { FundingMethodModal } from '../../../multichain/funding-method-modal/funding-method-modal';
///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask)
Expand All @@ -35,6 +39,16 @@ import {
} from '../../../multichain/ramps-card/ramps-card';
import { getIsNativeTokenBuyable } from '../../../../ducks/ramps';
///: END:ONLY_INCLUDE_IF
import {
getCurrentChainId,
getNetworkConfigurationsByChainId,
getSelectedNetworkClientId,
} from '../../../../../shared/modules/selectors/networks';
import { addImportedTokens } from '../../../../store/actions';
import {
AssetType,
TokenStandard,
} from '../../../../../shared/constants/transaction';
import AssetListControlBar from './asset-list-control-bar';
import NativeToken from './native-token';

Expand All @@ -54,6 +68,7 @@ export type AssetListProps = {
};

const AssetList = ({ onClickAsset, showTokensLinks }: AssetListProps) => {
const dispatch = useDispatch();
const [showDetectedTokens, setShowDetectedTokens] = useState(false);
const selectedAccount = useSelector(getSelectedAccount);
const t = useI18nContext();
Expand All @@ -74,14 +89,19 @@ const AssetList = ({ onClickAsset, showTokensLinks }: AssetListProps) => {
});

const detectedTokens = useSelector(getDetectedTokensInCurrentNetwork) || [];
const isTokenDetectionInactiveOnNonMainnetSupportedNetwork = useSelector(
getIstokenDetectionInactiveOnNonMainnetSupportedNetwork,
);

const isTokenNetworkFilterEqualCurrentNetwork = useSelector(
getIsTokenNetworkFilterEqualCurrentNetwork,
);

const allNetworks: Record<`0x${string}`, NetworkConfiguration> = useSelector(
getNetworkConfigurationsByChainId,
);
const networkClientId = useSelector(getSelectedNetworkClientId);
const selectedAddress = useSelector(getSelectedAddress);
const useTokenDetection = useSelector(getUseTokenDetection);
const currentChainId = useSelector(getCurrentChainId);

const [showFundingMethodModal, setShowFundingMethodModal] = useState(false);
const [showReceiveModal, setShowReceiveModal] = useState(false);

Expand All @@ -104,32 +124,91 @@ const AssetList = ({ onClickAsset, showTokensLinks }: AssetListProps) => {
// for EVM assets
const shouldShowTokensLinks = showTokensLinks ?? isEvm;

const detectedTokensMultichain = useSelector(
getAllDetectedTokensForSelectedAddress,
const detectedTokensMultichain: {
[key: `0x${string}`]: Token[];
} = useSelector(getAllDetectedTokensForSelectedAddress);

const multichainDetectedTokensLength = Object.keys(
detectedTokensMultichain || {},
).reduce(
(sum, key) => sum + detectedTokensMultichain[key as `0x${string}`].length,
0,
);

const totalTokens =
process.env.PORTFOLIO_VIEW &&
!isTokenNetworkFilterEqualCurrentNetwork &&
detectedTokensMultichain
? (Object.values(detectedTokensMultichain).reduce(
// @ts-expect-error TS18046: 'tokenArray' is of type 'unknown'
(count, tokenArray) => count + tokenArray.length,
0,
) as number)
: detectedTokens.length;
// Add detected tokens to sate
useEffect(() => {
const importAllDetectedTokens = async () => {
// If autodetect tokens toggle is OFF, return
sahar-fehri marked this conversation as resolved.
Show resolved Hide resolved
if (!useTokenDetection) {
return;
}
// TODO add event for MetaMetricsEventName.TokenAdded

if (
process.env.PORTFOLIO_VIEW &&
!isTokenNetworkFilterEqualCurrentNetwork
) {
const importPromises = Object.entries(detectedTokensMultichain).map(
async ([networkId, tokens]) => {
const chainConfig = allNetworks[networkId as `0x${string}`];
const { defaultRpcEndpointIndex } = chainConfig;
const { networkClientId: networkInstanceId } =
chainConfig.rpcEndpoints[defaultRpcEndpointIndex];

await dispatch(
addImportedTokens(tokens as Token[], networkInstanceId),
);
tokens.forEach((importedToken) => {
trackEvent({
event: MetaMetricsEventName.TokenAdded,
category: MetaMetricsEventCategory.Wallet,
sensitiveProperties: {
token_symbol: importedToken.symbol,
token_contract_address: importedToken.address,
token_decimal_precision: importedToken.decimals,
source: MetaMetricsTokenEventSource.Detected,
token_standard: TokenStandard.ERC20,
asset_type: AssetType.token,
token_added_type: 'detected',
chain_id: chainConfig.chainId,
},
});
});
},
);

await Promise.all(importPromises);
} else {
await dispatch(addImportedTokens(detectedTokens, networkClientId));
detectedTokens.forEach((importedToken: Token) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should have a check for whether there are any detected tokens. Otherwise it's doing an unnecessary call to the controller + state update on each load.

Screenshot 2024-12-20 at 11 10 56 AM

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe just changing the else to else if (detectedTokens?.length > 0) {

trackEvent({
event: MetaMetricsEventName.TokenAdded,
category: MetaMetricsEventCategory.Wallet,
sensitiveProperties: {
token_symbol: importedToken.symbol,
token_contract_address: importedToken.address,
token_decimal_precision: importedToken.decimals,
source: MetaMetricsTokenEventSource.Detected,
token_standard: TokenStandard.ERC20,
asset_type: AssetType.token,
token_added_type: 'detected',
chain_id: currentChainId,
},
});
});
}
};
importAllDetectedTokens();
}, [
isTokenNetworkFilterEqualCurrentNetwork,
selectedAddress,
networkClientId,
detectedTokens.length,
multichainDetectedTokensLength,
]);

return (
<>
{totalTokens &&
totalTokens > 0 &&
!isTokenDetectionInactiveOnNonMainnetSupportedNetwork ? (
<DetectedTokensBanner
className=""
actionButtonOnClick={() => setShowDetectedTokens(true)}
margin={4}
marginBottom={1}
/>
) : null}
<AssetListControlBar showTokensLinks={shouldShowTokensLinks} />
<TokenList
// nativeToken is still needed to avoid breaking flask build's support for bitcoin
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ jest.mock('../../../store/actions', () => ({
setTokenNetworkFilter: jest.fn(),
updateSlides: jest.fn(),
removeSlide: jest.fn(),
addImportedTokens: jest.fn(),
}));

// Mock the dispatch function
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ jest.mock('../../../store/actions', () => ({
setTokenNetworkFilter: jest.fn(),
updateSlides: jest.fn(),
removeSlide: jest.fn(),
addImportedTokens: jest.fn(),
}));

// Mock the dispatch function
Expand Down
Loading