-
Notifications
You must be signed in to change notification settings - Fork 5.1k
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
feat: AssetList
SPL token support
#30345
Changes from 43 commits
50802a2
b1463f9
fb857ae
4a453d1
54c75c9
db67847
607ca16
66c944f
98eebaa
6fd1caa
70fabea
7d4627d
9d28e99
91f130a
414eef1
550a15d
d27585b
855ff7e
209900a
51c7df5
d8aa775
48aa482
ef32bf4
c4175cb
ff3cd01
893313c
5c6492e
f174ac4
3276a75
aaabc9b
f4ae9e5
f05509a
da24a10
04ae981
e843399
b8cc7f6
7e1a601
6a4d6ee
61a826f
e99abb9
eddeb92
8e0e732
a8ff195
21abbb2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,11 @@ | ||
export { getMultichainAssetsControllerMessenger } from './multichain-assets-controller-messenger'; | ||
export { getMultiChainAssetsRatesControllerMessenger } from './multichain-assets-rates-controller-messenger'; | ||
export { getMultichainBalancesControllerMessenger } from './multichain-balances-controller-messenger'; | ||
export { getMultichainTransactionsControllerMessenger } from './multichain-transactions-controller-messenger'; | ||
export { getMultiChainAssetsRatesControllerMessenger } from './multichain-assets-rates-controller-messenger'; | ||
|
||
export type { MultichainAssetsControllerMessenger } from './multichain-assets-controller-messenger'; | ||
export type { MultiChainAssetsRatesControllerMessenger } from './multichain-assets-rates-controller-messenger'; | ||
export type { MultichainBalancesControllerMessenger } from './multichain-balances-controller-messenger'; | ||
export type { MultichainTransactionsControllerMessenger } from './multichain-transactions-controller-messenger'; | ||
export type { MultiChainAssetsRatesControllerMessenger } from './multichain-assets-rates-controller-messenger'; | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
import { useSelector } from 'react-redux'; | ||
import { Hex } from '@metamask/utils'; | ||
import { getMultichainBalances } from '../../../../selectors/multichain'; | ||
import { | ||
getAccountAssets, | ||
getAssetsMetadata, | ||
} from '../../../../selectors/assets'; | ||
import { CHAIN_ID_TOKEN_IMAGE_MAP } from '../../../../../shared/constants/network'; | ||
import { getSelectedInternalAccount } from '../../../../selectors'; | ||
import { | ||
TranslateFunction, | ||
networkTitleOverrides, | ||
} from '../util/networkTitleOverrides'; | ||
import { useI18nContext } from '../../../../hooks/useI18nContext'; | ||
import { getAssetsRates } from '../../../../selectors/multichain-assets-rates'; | ||
import { formatWithThreshold } from '../util/formatWithThreshold'; | ||
import { getIntlLocale } from '../../../../ducks/locale/locale'; | ||
import { getCurrentCurrency } from '../../../../ducks/metamask/metamask'; | ||
|
||
const useMultiChainAssets = () => { | ||
const t = useI18nContext(); | ||
const locale = useSelector(getIntlLocale); | ||
const currentCurrency = useSelector(getCurrentCurrency); | ||
const account = useSelector(getSelectedInternalAccount); | ||
const multichainBalances = useSelector(getMultichainBalances); | ||
const accountAssets = useSelector(getAccountAssets); | ||
const assetsMetadata = useSelector(getAssetsMetadata); | ||
const assetRates = useSelector(getAssetsRates); | ||
|
||
const assetIds = accountAssets[account.id] || []; | ||
const balances = multichainBalances[account.id]; | ||
|
||
return assetIds.map((assetId) => { | ||
const [chainId, assetDetails] = assetId.split('/'); | ||
const isToken = assetDetails.split(':')[0] === 'token'; | ||
|
||
const balance = balances[assetId] || { amount: '0', unit: '' }; | ||
const rate = assetRates[assetId]?.rate || '0'; | ||
const fiatBalance = parseFloat(rate) * parseFloat(balance.amount); | ||
|
||
const fiatAmount = formatWithThreshold(fiatBalance, 0.01, locale, { | ||
style: 'currency', | ||
currency: currentCurrency.toUpperCase(), | ||
}); | ||
|
||
const metadata = assetsMetadata[assetId] || { | ||
name: balance.unit, | ||
symbol: balance.unit || '', | ||
fungible: true, | ||
units: [{ name: assetId, symbol: balance.unit || '', decimals: 0 }], | ||
}; | ||
|
||
let tokenImage = ''; | ||
|
||
if (isToken) { | ||
tokenImage = metadata.iconUrl || ''; | ||
} else { | ||
tokenImage = | ||
CHAIN_ID_TOKEN_IMAGE_MAP[ | ||
chainId as keyof typeof CHAIN_ID_TOKEN_IMAGE_MAP | ||
] || ''; | ||
} | ||
|
||
const decimals = metadata.units[0]?.decimals || 0; | ||
|
||
return { | ||
title: isToken | ||
? metadata.name | ||
: networkTitleOverrides(t as TranslateFunction, { | ||
title: balance.unit, | ||
}), | ||
address: assetId as Hex, | ||
symbol: metadata.symbol, | ||
image: tokenImage, | ||
decimals, | ||
chainId, | ||
isNative: false, | ||
primary: balance.amount, | ||
secondary: fiatAmount, // secondary balance (usually in fiat) | ||
string: '', | ||
tokenFiatAmount: fiatBalance, // for now we are keeping this is to satisfy sort, this should be fiat amount | ||
isStakeable: false, | ||
}; | ||
}); | ||
}; | ||
|
||
export default useMultiChainAssets; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,20 +2,11 @@ import React from 'react'; | |
import { useSelector } from 'react-redux'; | ||
import { getNativeTokenAddress } from '@metamask/assets-controllers'; | ||
import { Hex } from '@metamask/utils'; | ||
import { | ||
TextColor, | ||
TextVariant, | ||
} from '../../../../../helpers/constants/design-system'; | ||
import { Text } from '../../../../component-library'; | ||
import { Box } from '../../../../component-library'; | ||
import { getMarketData } from '../../../../../selectors'; | ||
import { getMultichainIsEvm } from '../../../../../selectors/multichain'; | ||
import { useI18nContext } from '../../../../../hooks/useI18nContext'; | ||
import { TokenFiatDisplayInfo } from '../../types'; | ||
import { PercentageChange } from '../../../../multichain/token-list-item/price/percentage-change'; | ||
import { | ||
TranslateFunction, | ||
networkTitleOverrides, | ||
} from '../../util/networkTitleOverrides'; | ||
|
||
type TokenCellPercentChangeProps = { | ||
token: TokenFiatDisplayInfo; | ||
|
@@ -24,7 +15,6 @@ type TokenCellPercentChangeProps = { | |
export const TokenCellPercentChange = React.memo( | ||
({ token }: TokenCellPercentChangeProps) => { | ||
const isEvm = useSelector(getMultichainIsEvm); | ||
const t = useI18nContext(); | ||
const multiChainMarketData = useSelector(getMarketData); | ||
|
||
// We do not want to display any percentage with non-EVM since we don't have the data for this yet. | ||
|
@@ -52,17 +42,9 @@ export const TokenCellPercentChange = React.memo( | |
); | ||
} | ||
|
||
// fallback value (is this valid?) | ||
return ( | ||
<Text | ||
variant={TextVariant.bodySmMedium} | ||
color={TextColor.textAlternative} | ||
data-testid="multichain-token-list-item-token-name" | ||
ellipsis | ||
> | ||
{networkTitleOverrides(t as TranslateFunction, token)} | ||
</Text> | ||
); | ||
Comment on lines
-55
to
-65
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think that we should show the network name in the case that percent change doesn't exist. Seems like an irrelevant fallback, and I'm not sure why it existed in the first place. |
||
// we don't support non-evm price changes yet. | ||
// annoyingly, we need an empty component here for flexbox to align everything nicely | ||
return <Box></Box>; | ||
}, | ||
(prevProps, nextProps) => prevProps.token.address === nextProps.token.address, | ||
); |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,11 +12,13 @@ import { | |
} from '../../../../selectors'; | ||
import { endTrace, TraceName } from '../../../../../shared/lib/trace'; | ||
import { useTokenBalances as pollAndUpdateEvmBalances } from '../../../../hooks/useTokenBalances'; | ||
import { useNativeTokenBalance, useNetworkFilter } from '../hooks'; | ||
import { useNetworkFilter } from '../hooks'; | ||
import { TokenWithFiatAmount } from '../types'; | ||
import { getMultichainIsEvm } from '../../../../selectors/multichain'; | ||
import { filterAssets } from '../util/filter'; | ||
import { sortAssets } from '../util/sort'; | ||
import useMultiChainAssets from '../hooks/useMultichainAssets'; | ||
import { getMultichainIsEvm } from '../../../../selectors/multichain'; | ||
import { MultichainNetworks } from '../../../../../shared/constants/multichain/networks'; | ||
|
||
type TokenListProps = { | ||
onTokenClick: (chainId: string, address: string) => void; | ||
|
@@ -36,18 +38,18 @@ function TokenList({ onTokenClick }: TokenListProps) { | |
chainIds: chainIdsToPoll as Hex[], | ||
}); | ||
|
||
sahar-fehri marked this conversation as resolved.
Show resolved
Hide resolved
|
||
const nonEvmNativeToken = useNativeTokenBalance(); | ||
const multichainAssets = useMultiChainAssets(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this hook can be a selector. Will update. |
||
|
||
// network filter to determine which tokens to show in list | ||
// on EVM we want to filter based on network filter controls, on non-evm we only want tokens from that chain identifier | ||
const { networkFilter } = useNetworkFilter(); | ||
|
||
const sortedFilteredTokens = useMemo(() => { | ||
const balances = isEvm ? evmBalances : [nonEvmNativeToken]; | ||
const balances = isEvm ? evmBalances : multichainAssets; | ||
const filteredAssets: TokenWithFiatAmount[] = filterAssets(balances, [ | ||
{ | ||
key: 'chainId', | ||
opts: isEvm ? networkFilter : { [nonEvmNativeToken.chainId]: true }, | ||
opts: isEvm ? networkFilter : { [MultichainNetworks.SOLANA]: true }, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Have the controller updates been integrated on extension? Once they are, we can rely on the result of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. updated here 21abbb2 |
||
filterCallback: 'inclusive', | ||
}, | ||
]); | ||
|
@@ -61,6 +63,7 @@ function TokenList({ onTokenClick }: TokenListProps) { | |
selectedAccount, | ||
newTokensImported, | ||
evmBalances, | ||
multichainAssets, | ||
]); | ||
|
||
useEffect(() => { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
import { getAssetsRates, AssetsState } from './multichain-assets-rates'; | ||
|
||
// Mock state for testing | ||
const mockState = { | ||
metamask: { | ||
conversionRates: { | ||
'token-1': { rate: 1.5, currency: 'USD' }, | ||
'token-2': { rate: 0.8, currency: 'EUR' }, | ||
}, | ||
}, | ||
}; | ||
describe('getAssetsRates', () => { | ||
it('should return the assetsRates from the state', () => { | ||
const result = getAssetsRates(mockState); | ||
expect(result).toEqual(mockState.metamask.conversionRates); | ||
}); | ||
|
||
it('should return an empty object if assetsRates is empty', () => { | ||
const emptyState: AssetsState = { | ||
metamask: { | ||
conversionRates: {}, | ||
}, | ||
}; | ||
const result = getAssetsRates(emptyState); | ||
expect(result).toEqual({}); | ||
}); | ||
|
||
it('should return undefined if state does not have metamask property', () => { | ||
const invalidState = {} as AssetsState; | ||
expect(() => getAssetsRates(invalidState)).toThrow(); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import { MultichainAssetsRatesControllerState } from '@metamask/assets-controllers'; | ||
|
||
export type AssetsState = { | ||
metamask: MultichainAssetsRatesControllerState; | ||
}; | ||
|
||
/** | ||
* Gets non-EVM accounts assets rates. | ||
* | ||
* @param state - Redux state object. | ||
* @returns An object containing non-EVM assets per accounts. | ||
*/ | ||
export function getAssetsRates(state: AssetsState) { | ||
return state.metamask.conversionRates; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TODO: Remove and make this into an .env var