Skip to content

feat: extend TokenHero support for all ERC20 tokens - transfer transactions #15259

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

Draft
wants to merge 21 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
1815336
feat: extend useTokenValues to support all ERC20 tokens
digiwand May 8, 2025
2bb1eba
style: useTokenValues abstract logic
digiwand May 8, 2025
6c070b8
feat: create useTokenDetails hook
digiwand May 8, 2025
050a953
fix: fix flickering value by waiting for async decimals
digiwand May 8, 2025
d0017d1
refactor: change NetworkAssetLogo style prop to optional
digiwand May 8, 2025
77eeced
fix: useTokenValues fiat conversion should read per tokenAddress
digiwand May 8, 2025
f37868e
style: alphabetize useTokenValues methods
digiwand May 8, 2025
3908f52
style: clean useTokenValues
digiwand May 8, 2025
f8b097a
style: format useTokenValues
digiwand May 9, 2025
eb5b4c0
feat: useTokenValues support native tokens
digiwand May 9, 2025
64b50da
feat: extend TokenHero support to all ERC20 tokens
digiwand May 9, 2025
faf6499
style: rn displayTokenAmountIsRounded -> isRoundedTokenAmount
digiwand May 9, 2025
ac55007
style: TokenHero rm AvatarTokenNetworkWithBadge style prop
digiwand May 9, 2025
1921f63
style: rn networkAndTokenContainer -> containerAvatarTokenNetworkWith…
digiwand May 9, 2025
ee8a016
feat: add TokenHero badge conditional
digiwand May 9, 2025
c5f7f62
style: clean
digiwand May 9, 2025
aac4e06
refactor: useTokenValues use safeToChecksumAddress
digiwand May 10, 2025
b62fc93
fix: export fetchTokenFiatRates from useBalanceChanges
digiwand May 10, 2025
7471309
refactor: mv fetchTokenFiatRates util method
digiwand May 10, 2025
bbf4ec2
feat: include native networkImage
digiwand May 10, 2025
4ae75a5
refactor: create AvatarTokenWithNetworkBadge
digiwand May 10, 2025
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
2 changes: 1 addition & 1 deletion app/components/UI/NetworkAssetLogo/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import TokenIcon from '../Swaps/components/TokenIcon';
interface NetworkAssetLogoProps {
chainId: string;
ticker: string;
style: object;
style?: object;
big: boolean;
biggest: boolean;
testID: string;
Expand Down
49 changes: 1 addition & 48 deletions app/components/UI/SimulationDetails/useBalanceChanges.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,6 @@ import {
SimulationTokenBalanceChange,
SimulationTokenStandard,
} from '@metamask/transaction-controller';
import {
ContractExchangeRates,
fetchTokenContractExchangeRates,
CodefiTokenPricesServiceV2,
} from '@metamask/assets-controllers';

import {
BalanceChange,
Expand All @@ -21,6 +16,7 @@ import {
NativeAssetIdentifier,
} from './types';
import { getTokenDetails } from '../../../util/address';
import { fetchTokenFiatRates } from '../../../util/tokens';
import {
selectConversionRateByChainId,
selectCurrentCurrency,
Expand Down Expand Up @@ -85,49 +81,6 @@ async function fetchAllErc20Decimals(
);
}

/**
* Retrieves token prices
*
* @param {string} nativeCurrency - native currency to fetch prices for.
* @param {Hex[]} tokenAddresses - set of contract addresses
* @param {Hex} chainId - current chainId
* @returns The prices for the requested tokens.
*/
const fetchTokenExchangeRates = async (
nativeCurrency: string,
tokenAddresses: Hex[],
chainId: Hex,
) => {
try {
return await fetchTokenContractExchangeRates({
tokenPricesService: new CodefiTokenPricesServiceV2(),
nativeCurrency,
tokenAddresses,
chainId,
});
} catch (err) {
return {};
}
};

async function fetchTokenFiatRates(
fiatCurrency: string,
erc20TokenAddresses: Hex[],
chainId: Hex,
): Promise<ContractExchangeRates> {
const tokenRates = await fetchTokenExchangeRates(
fiatCurrency,
erc20TokenAddresses,
chainId,
);

return Object.fromEntries(
Object.entries(tokenRates).map(([address, rate]) => [
address.toLowerCase(),
rate,
]),
);
}

// Compiles the balance change for the native asset
function getNativeBalanceChange(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { StyleSheet } from 'react-native';
import { Theme } from '../../../../../../../../util/theme/models';

export const styleSheet = (params: {
theme: Theme;
}) => {
const { theme } = params;
return StyleSheet.create({
avatarNetwork: {
backgroundColor: theme.colors.background.default,
borderRadius: 99,
},
});
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { TransactionMeta } from '@metamask/transaction-controller';

import { AvatarSize } from '../../../../../../../../component-library/components/Avatars/Avatar/Avatar.types';
import AvatarNetwork from '../../../../../../../../component-library/components/Avatars/Avatar/variants/AvatarNetwork/AvatarNetwork';
import AvatarToken from '../../../../../../../../component-library/components/Avatars/Avatar/variants/AvatarToken/AvatarToken';
import Badge, {
BadgeVariant,
} from '../../../../../../../../component-library/components/Badges/Badge';
import BadgeWrapper, {
BadgePosition,
} from '../../../../../../../../component-library/components/Badges/BadgeWrapper';
import { useStyles } from '../../../../../../../../component-library/hooks';
import { NameType } from '../../../../../../../UI/Name/Name.types';
import { useDisplayName } from '../../../../../../../hooks/DisplayName/useDisplayName';
import { RootState } from '../../../../../../../../reducers';
import { selectNetworkConfigurationByChainId } from '../../../../../../../../selectors/networkController';
import { getNetworkImageSource } from '../../../../../../../../util/networks';
import useNetworkInfo from '../../../../../hooks/useNetworkInfo';
import { useTransactionMetadataRequest } from '../../../../../hooks/transactions/useTransactionMetadataRequest';
import { isNativeToken } from '../../../../../utils/token';
import { styleSheet } from './avatar-token-with-network-badge.styles';

const AvatarTokenNetwork = () => {
const { styles } = useStyles(styleSheet, {});

const transactionMeta =
useTransactionMetadataRequest() ?? ({} as TransactionMeta);
const { chainId } = transactionMeta;

const { nativeCurrency } = useSelector((state: RootState) =>
selectNetworkConfigurationByChainId(state, chainId),
);
const isNative = isNativeToken(transactionMeta);
const networkImage = getNetworkImageSource({ chainId });

const { image, name: symbol } = useDisplayName({
preferContractSymbol: true,
type: NameType.EthereumAddress,
value: transactionMeta?.txParams?.to ?? '',
variation: chainId ?? '',
});

return isNative ? (
<AvatarNetwork
name={nativeCurrency ?? ''}
imageSource={networkImage}
size={AvatarSize.Xl}
style={styles.avatarNetwork}
/>
) : (
<AvatarToken
imageSource={image ? { uri: image } : undefined}
name={symbol ?? ''}
size={AvatarSize.Xl}
/>
);
};

export const AvatarTokenWithNetworkBadge = () => {
const transactionMeta = useTransactionMetadataRequest() ?? ({} as TransactionMeta);
const { networkName, networkImage } = useNetworkInfo(
transactionMeta?.chainId,
);
const isNative = isNativeToken(transactionMeta);

return (
<BadgeWrapper
badgePosition={BadgePosition.BottomRight}
badgeElement={!isNative && networkImage ? (
<Badge
imageSource={networkImage}
variant={BadgeVariant.Network}
name={networkName}
/>
) : null}
>
<AvatarTokenNetwork />
</BadgeWrapper>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { AvatarTokenWithNetworkBadge } from './avatar-token-with-network-badge';
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,15 @@ const styleSheet = (params: {
textAlign: 'center',
color: theme.colors.text.alternative,
},
networkAndTokenContainer: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
},
networkLogo: {
width: 48,
height: 48,
},
container: {
paddingBottom: 16,
paddingTop: isFlatConfirmation ? 16 : 0,
},
containerAvatarTokenNetworkWithBadge: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
},
});
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,50 +1,33 @@
import React, { useState } from 'react';
import { StyleSheet, TouchableOpacity, View } from 'react-native';

import { strings } from '../../../../../../../../locales/i18n';
import Badge, {
BadgeVariant,
} from '../../../../../../../component-library/components/Badges/Badge';
import BadgeWrapper, {
BadgePosition,
} from '../../../../../../../component-library/components/Badges/BadgeWrapper';
import Text, {
TextVariant,
} from '../../../../../../../component-library/components/Texts/Text';
import { useStyles } from '../../../../../../../component-library/hooks';
import images from '../../../../../../../images/image-icons';
import TokenIcon from '../../../../../../UI/Swaps/components/TokenIcon';
import { useTokenDetails } from '../../../../hooks/useTokenDetails';
import { useTokenValues } from '../../../../hooks/useTokenValues';
import { useFlatConfirmation } from '../../../../hooks/ui/useFlatConfirmation';
import { TooltipModal } from '../../../UI/Tooltip/Tooltip';
import styleSheet from './token-hero.styles';
import { AvatarTokenWithNetworkBadge } from './avatar-token-with-network-badge';

const NetworkAndTokenImage = ({
tokenSymbol,
styles,
}: {
tokenSymbol: string;
styles: StyleSheet.NamedStyles<Record<string, unknown>>;
}) => (
<View style={styles.networkAndTokenContainer}>
<BadgeWrapper
badgePosition={BadgePosition.BottomRight}
badgeElement={
<Badge imageSource={images.ETHEREUM} variant={BadgeVariant.Network} />
}
>
<TokenIcon big symbol={tokenSymbol} />
</BadgeWrapper>
</View>
);
// todo:
// - add conditional logic to fiat value. e.g. should hide if testnet
// - fix inconsistent fiat value. can be off by pennies
// - tokenlist sometimes only has 0x0000000000000000000000000000000000000000
// - style: confirm if we'd like to add the symbol in the modal precise token amount text
// - style: confirm fallback avatar - non-bold + background color

const AssetAmount = ({
tokenAmountDisplayValue,
tokenSymbol,
styles,
setIsModalVisible,
}: {
tokenAmountDisplayValue: string;
tokenSymbol: string;
tokenAmountDisplayValue?: string;
tokenSymbol?: string;
styles: StyleSheet.NamedStyles<Record<string, unknown>>;
setIsModalVisible: ((isModalVisible: boolean) => void) | null;
}) => (
Expand All @@ -67,44 +50,44 @@ const AssetFiatConversion = ({
fiatDisplayValue,
styles,
}: {
fiatDisplayValue: string;
fiatDisplayValue?: string;
styles: StyleSheet.NamedStyles<Record<string, unknown>>;
}) => (
<Text style={styles.assetFiatConversionText} variant={TextVariant.BodyMD}>
{fiatDisplayValue}
</Text>
);
}) => fiatDisplayValue ? (
<Text style={styles.assetFiatConversionText} variant={TextVariant.BodyMD}>
{fiatDisplayValue}
</Text>
) : null;

const TokenHero = ({ amountWei }: { amountWei?: string }) => {
const { isFlatConfirmation } = useFlatConfirmation();
const { styles } = useStyles(styleSheet, {
isFlatConfirmation,
});
const { tokenAmountValue, tokenAmountDisplayValue, fiatDisplayValue } =
useTokenValues({ amountWei });
const [isModalVisible, setIsModalVisible] = useState(false);

const displayTokenAmountIsRounded =
tokenAmountValue !== tokenAmountDisplayValue;
const tokenDetails = useTokenDetails();
const { symbol } = tokenDetails;
const { tokenAmountValue, tokenAmountDisplayValue, fiatDisplayValue } =
useTokenValues({ amountWei });

const tokenSymbol = 'ETH';
const isRoundedTokenAmount = tokenAmountValue !== tokenAmountDisplayValue;

return (
<View style={styles.container}>
<NetworkAndTokenImage tokenSymbol={tokenSymbol} styles={styles} />
<View style={styles.containerAvatarTokenNetworkWithBadge}>
<AvatarTokenWithNetworkBadge />
</View>
<AssetAmount
tokenAmountDisplayValue={tokenAmountDisplayValue}
tokenSymbol={tokenSymbol}
tokenSymbol={symbol}
styles={styles}
setIsModalVisible={
displayTokenAmountIsRounded ? setIsModalVisible : null
}
setIsModalVisible={isRoundedTokenAmount ? setIsModalVisible : null}
/>
<AssetFiatConversion
fiatDisplayValue={fiatDisplayValue}
styles={styles}
/>
{displayTokenAmountIsRounded && (
{isRoundedTokenAmount && (
<TooltipModal
open={isModalVisible}
setOpen={setIsModalVisible}
Expand Down
2 changes: 2 additions & 0 deletions app/components/Views/confirmations/constants/tokens.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export const NATIVE_TOKEN_ADDRESS = '0x0000000000000000000000000000000000000000';

export const TOKEN_ADDRESS = {
DAI: '0x6b175474e89094c44da98b954eedeac495271d0f',
};
Loading
Loading