Skip to content

Commit

Permalink
refactor: asset-list and token-list (#29886)
Browse files Browse the repository at this point in the history
## **Description**

This is the first of a series of PRs that aim to address two things:

1. Simplify/fix some technical debt accrued during `PortfolioView`
sprints
2. Prepare AssetList for chain agnostic tokens (removal of EVM specific
conventions)

This PR aims to simplify `asset-list` and `token-list` components. The
next PR will simplify `token-cell` and `token-list-item`

- Refactor `consolidateTokenBalances` into a redux selector
`getTokenBalancesEvm`. Eventually (soon) this will be wrapped in a more
generic selector, that will understand how to select/calculate balances
for tokens on different chains.

- Add a `app/assets/hooks` directory to help clean up and extract
business logic away from the body of `asset-list` and `token-list`
components and improve readability

- Adds two utility files: `calculateTokenBalance` and
`calculateTokenFiatAmount` (these are called from the selector file)

- Adds a `app/assets/types.ts` file to consolidate all the types related
to assets.

[![Open in GitHub
Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/29886?quickstart=1)

## **Related issues**

## **Manual testing steps**

This PR should not break anything. It also should not introduce
anything.

## **Screenshots/Recordings**

## **Pre-merge author checklist**

- [x] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask
Extension Coding
Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md).
- [x] I've completed the PR template to the best of my ability
- [x] I’ve included tests if applicable
- [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [x] I’ve applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.

## **Pre-merge reviewer checklist**

- [ ] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [ ] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.
  • Loading branch information
gambinish authored Feb 7, 2025
1 parent 36b58cd commit 43efd3f
Show file tree
Hide file tree
Showing 21 changed files with 532 additions and 455 deletions.
8 changes: 0 additions & 8 deletions development/circular-deps.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,6 @@
"ui/components/app/assets/asset-list/native-token/native-token.tsx",
"ui/components/app/assets/asset-list/native-token/use-native-token-balance.ts"
],
[
"ui/components/app/assets/token-list/token-list.tsx",
"ui/components/app/assets/util/calculateTokenBalance.ts"
],
[
"ui/components/app/assets/token-list/token-list.tsx",
"ui/components/app/assets/util/calculateTokenFiatAmount.ts"
],
[
"ui/components/app/name/name-details/name-details.tsx",
"ui/components/app/name/name.tsx"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import React, { useState } from 'react';
import { useSelector } from 'react-redux';
import { ReceiveModal } from '../../../../multichain';
import { FundingMethodModal } from '../../../../multichain/funding-method-modal/funding-method-modal';
import { useI18nContext } from '../../../../../hooks/useI18nContext';
import { getSelectedAccount } from '../../../../../selectors';
///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask)
import {
getMultichainIsBitcoin,
getMultichainSelectedAccountCachedBalanceIsZero,
} from '../../../../../selectors/multichain';
import { getIsNativeTokenBuyable } from '../../../../../ducks/ramps';
import { RampsCard } from '../../../../multichain/ramps-card';
import { RAMPS_CARD_VARIANT_TYPES } from '../../../../multichain/ramps-card/ramps-card';
///: END:ONLY_INCLUDE_IF

const AssetListFundingModals = () => {
const t = useI18nContext();
const selectedAccount = useSelector(getSelectedAccount);

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

const onClickReceive = () => {
setShowFundingMethodModal(false);
setShowReceiveModal(true);
};

///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask)
const balanceIsZero = useSelector(
getMultichainSelectedAccountCachedBalanceIsZero,
);
const isBuyableChain = useSelector(getIsNativeTokenBuyable);
const shouldShowBuy = isBuyableChain && balanceIsZero;
const isBtc = useSelector(getMultichainIsBitcoin);
///: END:ONLY_INCLUDE_IF

return (
<>
{
///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask)
shouldShowBuy ? (
<RampsCard
variant={
isBtc
? RAMPS_CARD_VARIANT_TYPES.BTC
: RAMPS_CARD_VARIANT_TYPES.TOKEN
}
handleOnClick={
isBtc ? undefined : () => setShowFundingMethodModal(true)
}
/>
) : null
///: END:ONLY_INCLUDE_IF
}
{showReceiveModal && selectedAccount?.address && (
<ReceiveModal
address={selectedAccount.address}
onClose={() => setShowReceiveModal(false)}
/>
)}
{showFundingMethodModal && (
<FundingMethodModal
isOpen={showFundingMethodModal}
onClose={() => setShowFundingMethodModal(false)}
title={t('fundingMethod')}
onClickReceive={onClickReceive}
/>
)}
</>
);
};

export default AssetListFundingModals;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './asset-list-funding-modals';
212 changes: 10 additions & 202 deletions ui/components/app/assets/asset-list/asset-list.tsx
Original file line number Diff line number Diff line change
@@ -1,56 +1,18 @@
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 React, { useContext } from 'react';
import { useSelector } from 'react-redux';
import TokenList from '../token-list';
import { PRIMARY } from '../../../../helpers/constants/common';
import { useUserPreferencedCurrency } from '../../../../hooks/useUserPreferencedCurrency';
import {
getAllDetectedTokensForSelectedAddress,
getDetectedTokensInCurrentNetwork,
getIsTokenNetworkFilterEqualCurrentNetwork,
getSelectedAccount,
getSelectedAddress,
getUseTokenDetection,
} from '../../../../selectors';
import {
getMultichainIsEvm,
getMultichainSelectedAccountCachedBalance,
///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask)
getMultichainIsBitcoin,
getMultichainSelectedAccountCachedBalanceIsZero,
///: END:ONLY_INCLUDE_IF
} from '../../../../selectors/multichain';
import { useCurrencyDisplay } from '../../../../hooks/useCurrencyDisplay';
import { getMultichainIsEvm } from '../../../../selectors/multichain';
import { MetaMetricsContext } from '../../../../contexts/metametrics';
import {
MetaMetricsEventCategory,
MetaMetricsEventName,
MetaMetricsTokenEventSource,
} from '../../../../../shared/constants/metametrics';
import DetectedToken from '../../detected-token/detected-token';
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)
import {
RAMPS_CARD_VARIANT_TYPES,
RampsCard,
} 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 useAssetListTokenDetection from '../hooks/useAssetListTokenDetection';
import usePrimaryCurrencyProperties from '../hooks/usePrimaryCurrencyProperties';
import AssetListControlBar from './asset-list-control-bar';
import NativeToken from './native-token';
import AssetListFundingModals from './asset-list-funding-modals';

export type TokenWithBalance = {
address: string;
Expand All @@ -68,142 +30,17 @@ export type AssetListProps = {
};

const AssetList = ({ onClickAsset, showTokensLinks }: AssetListProps) => {
const dispatch = useDispatch();
const [showDetectedTokens, setShowDetectedTokens] = useState(false);
const selectedAccount = useSelector(getSelectedAccount);
const t = useI18nContext();
const { showDetectedTokens, setShowDetectedTokens } =
useAssetListTokenDetection();
const trackEvent = useContext(MetaMetricsContext);
const balance = useSelector(getMultichainSelectedAccountCachedBalance);

const {
currency: primaryCurrency,
numberOfDecimals: primaryNumberOfDecimals,
} = useUserPreferencedCurrency(PRIMARY, {
ethNumberOfDecimals: 4,
shouldCheckShowNativeToken: true,
});

const [, primaryCurrencyProperties] = useCurrencyDisplay(balance, {
numberOfDecimals: primaryNumberOfDecimals,
currency: primaryCurrency,
});

const detectedTokens = useSelector(getDetectedTokensInCurrentNetwork) || [];

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);

const onClickReceive = () => {
setShowFundingMethodModal(false);
setShowReceiveModal(true);
};

///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask)
const balanceIsZero = useSelector(
getMultichainSelectedAccountCachedBalanceIsZero,
);
const isBuyableChain = useSelector(getIsNativeTokenBuyable);
const shouldShowBuy = isBuyableChain && balanceIsZero;
const isBtc = useSelector(getMultichainIsBitcoin);
///: END:ONLY_INCLUDE_IF
const { primaryCurrencyProperties } = usePrimaryCurrencyProperties();

const isEvm = useSelector(getMultichainIsEvm);
// NOTE: Since we can parametrize it now, we keep the original behavior
// for EVM assets
const shouldShowTokensLinks = showTokensLinks ?? isEvm;

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

const multichainDetectedTokensLength = Object.values(
detectedTokensMultichain || {},
).reduce((acc, tokens) => acc + tokens.length, 0);

// Add detected tokens to sate
useEffect(() => {
const importAllDetectedTokens = async () => {
// If autodetect tokens toggle is OFF, return
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 if (detectedTokens.length > 0) {
await dispatch(addImportedTokens(detectedTokens, networkClientId));
detectedTokens.forEach((importedToken: Token) => {
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 (
<>
<AssetListControlBar showTokensLinks={shouldShowTokensLinks} />
Expand All @@ -223,39 +60,10 @@ const AssetList = ({ onClickAsset, showTokensLinks }: AssetListProps) => {
});
}}
/>
{
///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask)
shouldShowBuy ? (
<RampsCard
variant={
isBtc
? RAMPS_CARD_VARIANT_TYPES.BTC
: RAMPS_CARD_VARIANT_TYPES.TOKEN
}
handleOnClick={
isBtc ? undefined : () => setShowFundingMethodModal(true)
}
/>
) : null
///: END:ONLY_INCLUDE_IF
}
{showDetectedTokens && (
<DetectedToken setShowDetectedTokens={setShowDetectedTokens} />
)}
{showReceiveModal && selectedAccount?.address && (
<ReceiveModal
address={selectedAccount.address}
onClose={() => setShowReceiveModal(false)}
/>
)}
{showFundingMethodModal && (
<FundingMethodModal
isOpen={showFundingMethodModal}
onClose={() => setShowFundingMethodModal(false)}
title={t('fundingMethod')}
onClickReceive={onClickReceive}
/>
)}
<AssetListFundingModals />
</>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
BackgroundColor,
TextColor,
} from '../../../../../helpers/constants/design-system';

import { getMultichainIsEvm } from '../../../../../selectors/multichain';

type AssetListControlBarProps = {
Expand Down
10 changes: 0 additions & 10 deletions ui/components/app/assets/auto-detect-token/index.scss

This file was deleted.

Loading

0 comments on commit 43efd3f

Please sign in to comment.