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

feat: AssetList SPL token support #30345

Closed
wants to merge 44 commits into from
Closed
Show file tree
Hide file tree
Changes from 43 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
50802a2
fix: bump assets-controllers to v49
sahar-fehri Feb 11, 2025
b1463f9
Update LavaMoat policies
metamaskbot Feb 11, 2025
fb857ae
fix: dedupe
sahar-fehri Feb 11, 2025
4a453d1
Update LavaMoat policies
metamaskbot Feb 11, 2025
54c75c9
fix: merge conflicts
sahar-fehri Feb 12, 2025
db67847
fix: bump assets
sahar-fehri Feb 12, 2025
607ca16
fix: patch
sahar-fehri Feb 12, 2025
66c944f
fix: fix yarn
sahar-fehri Feb 12, 2025
98eebaa
fix: fix test
sahar-fehri Feb 12, 2025
6fd1caa
Update LavaMoat policies
metamaskbot Feb 12, 2025
70fabea
fix: fix patch
sahar-fehri Feb 12, 2025
7d4627d
Update LavaMoat policies
metamaskbot Feb 12, 2025
9d28e99
Merge branch 'main' into feat/bump-assets-controller-49.0.0
sahar-fehri Feb 12, 2025
91f130a
Merge branch 'main' into feat/bump-assets-controller-49.0.0
zone-live Feb 13, 2025
414eef1
Merge branch 'main' into feat/bump-assets-controller-49.0.0
sahar-fehri Feb 13, 2025
550a15d
fix: lint
sahar-fehri Feb 13, 2025
d27585b
Merge branch 'main' into feat/bump-assets-controller-49.0.0
sahar-fehri Feb 13, 2025
855ff7e
Merge branch 'main' into feat/bump-assets-controller-49.0.0
sahar-fehri Feb 13, 2025
209900a
[WIP] `MultichainAssetsController` init
GuillaumeRx Feb 13, 2025
51c7df5
Update multichain controllers init
GuillaumeRx Feb 13, 2025
d8aa775
fix: integrate multichainAssetsRatesController
salimtb Feb 13, 2025
48aa482
fix: fix after rebase
salimtb Feb 13, 2025
ef32bf4
fix: fix linter
salimtb Feb 13, 2025
c4175cb
feat: Integrate SPL tokens
gambinish Feb 14, 2025
ff3cd01
chore: Cleanup token-list
gambinish Feb 14, 2025
893313c
chore: Remove some hard coded values
gambinish Feb 14, 2025
5c6492e
chore: Cleanup
gambinish Feb 14, 2025
f174ac4
fix: fix PR comments
salimtb Feb 14, 2025
3276a75
Merge branch 'main' into salim/integrate-mutichain-assets-rates-contr…
salimtb Feb 14, 2025
aaabc9b
fix: fix e2e tests
salimtb Feb 14, 2025
f4ae9e5
chore: Remove hardcoded assetId
gambinish Feb 14, 2025
f05509a
fix: fix build
salimtb Feb 14, 2025
da24a10
fix: fix build
salimtb Feb 14, 2025
04ae981
fix: fix linter
salimtb Feb 14, 2025
e843399
chore: Handle native token
gambinish Feb 14, 2025
b8cc7f6
fix: Dont render network text when percent change is undefined
gambinish Feb 14, 2025
7e1a601
fix: Render token balance in primary display cell
gambinish Feb 14, 2025
6a4d6ee
fix: Replace fallback
gambinish Feb 14, 2025
61a826f
chore: Remove parsedAssetId util in favor of common metamask util
gambinish Feb 14, 2025
e99abb9
fix: Cleanup
gambinish Feb 14, 2025
eddeb92
Merge branch 'feat/spl-token-list-integration--rates' into feat/spl-t…
gambinish Feb 14, 2025
8e0e732
chore: Integrate token rates
gambinish Feb 14, 2025
a8ff195
Merge branch 'main' into feat/spl-token-rates
sahar-fehri Feb 17, 2025
21abbb2
fix: fix currentChain selector
sahar-fehri Feb 18, 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: 2 additions & 0 deletions app/scripts/controller-init/controller-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export type Controller =
| MultiChainAssetsRatesController
| MultichainBalancesController
| MultichainTransactionsController
| MultiChainAssetsRatesController
| NetworkController
| OnboardingController
| PermissionController<
Expand Down Expand Up @@ -78,6 +79,7 @@ export type ControllerFlatState = AccountsController['state'] &
MultiChainAssetsRatesController['state'] &
MultichainBalancesController['state'] &
MultichainTransactionsController['state'] &
MultiChainAssetsRatesController['state'] &
NetworkController['state'] &
OnboardingController['state'] &
PermissionController<
Expand Down
3 changes: 3 additions & 0 deletions app/scripts/controller-init/messengers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ export const CONTROLLER_MESSENGERS = {
getMessenger: getMultichainTransactionsControllerMessenger,
getInitMessenger: noop,
},
MultiChainAssetsRatesController: {
getMessenger: getMultiChainAssetsRatesControllerMessenger,
},
RateLimitController: {
getMessenger: getRateLimitControllerMessenger,
getInitMessenger: getRateLimitControllerInitMessenger,
Expand Down
2 changes: 2 additions & 0 deletions app/scripts/controller-init/messengers/multichain/index.ts
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';

Check failure on line 2 in app/scripts/controller-init/messengers/multichain/index.ts

View workflow job for this annotation

GitHub Actions / Test lint / Test lint

Multiple exports of name 'getMultiChainAssetsRatesControllerMessenger'
export { getMultichainBalancesControllerMessenger } from './multichain-balances-controller-messenger';
export { getMultichainTransactionsControllerMessenger } from './multichain-transactions-controller-messenger';
export { getMultiChainAssetsRatesControllerMessenger } from './multichain-assets-rates-controller-messenger';

Check failure on line 5 in app/scripts/controller-init/messengers/multichain/index.ts

View workflow job for this annotation

GitHub Actions / Test lint / Test lint

Multiple exports of name 'getMultiChainAssetsRatesControllerMessenger'

export type { MultichainAssetsControllerMessenger } from './multichain-assets-controller-messenger';
export type { MultiChainAssetsRatesControllerMessenger } from './multichain-assets-rates-controller-messenger';

Check failure on line 8 in app/scripts/controller-init/messengers/multichain/index.ts

View workflow job for this annotation

GitHub Actions / Test lint / Test lint

Multiple exports of name 'MultiChainAssetsRatesControllerMessenger'
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';

Check failure on line 11 in app/scripts/controller-init/messengers/multichain/index.ts

View workflow job for this annotation

GitHub Actions / Test lint / Test lint

Multiple exports of name 'MultiChainAssetsRatesControllerMessenger'
1 change: 1 addition & 0 deletions app/scripts/metamask-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -2058,6 +2058,7 @@
MultiChainAssetsRatesController: MultiChainAssetsRatesControllerInit,
MultichainBalancesController: MultichainBalancesControllerInit,
MultichainTransactionsController: MultichainTransactionsControllerInit,
MultiChainAssetsRatesController: MultiChainAssetsRatesControllerInit,

Check failure on line 2061 in app/scripts/metamask-controller.js

View workflow job for this annotation

GitHub Actions / Test lint / Test lint

Duplicate key 'MultiChainAssetsRatesController'
///: END:ONLY_INCLUDE_IF
};

Expand Down
5 changes: 5 additions & 0 deletions shared/constants/network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
RpcEndpointType,
} from '@metamask/network-controller';
import { capitalize, pick } from 'lodash';
import { MultichainNetworks } from './multichain/networks';
/**
* A type representing built-in network types, used as an identifier.
*/
Expand Down Expand Up @@ -176,6 +177,7 @@ export const CHAIN_IDS = {
INK: '0xdef1',
MODE_SEPOLIA: '0x397',
MODE: '0x868b',
SOLANA: MultichainNetworks.SOLANA,
} as const;

export const CHAINLIST_CHAIN_IDS_MAP = {
Expand Down Expand Up @@ -520,6 +522,7 @@ export const SONIC_MAINNET_IMAGE_URL = './images/sonic.svg';
export const SONEIUM_IMAGE_URL = './images/soneium.svg';
export const MODE_SEPOLIA_IMAGE_URL = './images/mode-sepolia.svg';
export const MODE_IMAGE_URL = './images/mode.svg';
export const SOLANA_IMAGE_URL = './images/solana-logo.svg';
export const UNICHAIN_IMAGE_URL = './images/unichain.svg';

export const INFURA_PROVIDER_TYPES = [
Expand Down Expand Up @@ -885,6 +888,7 @@ export const CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP = {
[CHAINLIST_CHAIN_IDS_MAP.SONEIUM_TESTNET]: SONEIUM_IMAGE_URL,
[CHAINLIST_CHAIN_IDS_MAP.MODE_SEPOLIA]: MODE_SEPOLIA_IMAGE_URL,
[CHAINLIST_CHAIN_IDS_MAP.MODE]: MODE_IMAGE_URL,
[CHAIN_IDS.SOLANA]: SOLANA_IMAGE_URL,
[CHAINLIST_CHAIN_IDS_MAP.UNICHAIN]: UNICHAIN_IMAGE_URL,
[CHAINLIST_CHAIN_IDS_MAP.UNICHAIN_SEPOLIA]: UNICHAIN_IMAGE_URL,
} as const;
Expand Down Expand Up @@ -932,6 +936,7 @@ export const CHAIN_ID_TOKEN_IMAGE_MAP = {
[CHAINLIST_CHAIN_IDS_MAP.SONIC_MAINNET]: SONIC_MAINNET_IMAGE_URL,
[CHAIN_IDS.MODE]: ETH_TOKEN_IMAGE_URL,
[CHAINLIST_CHAIN_IDS_MAP.FUNKICHAIN]: ETH_TOKEN_IMAGE_URL,
[CHAIN_IDS.SOLANA]: SOLANA_IMAGE_URL,
[CHAINLIST_CHAIN_IDS_MAP.UNICHAIN]: ETH_TOKEN_IMAGE_URL,
[CHAINLIST_CHAIN_IDS_MAP.UNICHAIN_SEPOLIA]: ETH_TOKEN_IMAGE_URL,
} as const;
Expand Down
3 changes: 2 additions & 1 deletion shared/lib/accounts/solana-wallet-snap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import { SnapId } from '@metamask/snaps-sdk';
// the Snap is being pre-installed only for Flask build (for the moment).
import SolanaWalletSnap from '@metamask/solana-wallet-snap/dist/preinstalled-snap.json';

export const SOLANA_WALLET_SNAP_ID: SnapId = SolanaWalletSnap.snapId as SnapId;
export const SOLANA_WALLET_SNAP_ID: SnapId =
'local:http://localhost:8080' as SnapId;
Comment on lines +6 to +7
Copy link
Contributor Author

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


export const SOLANA_WALLET_NAME: string =
SolanaWalletSnap.manifest.proposedName;
87 changes: 87 additions & 0 deletions ui/components/app/assets/hooks/useMultichainAssets.tsx
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;
4 changes: 2 additions & 2 deletions ui/components/app/assets/hooks/useTokenDisplayInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,9 +109,9 @@ export const useTokenDisplayInfo = ({
}
// TODO non-evm assets. this is only the native token
return {
title: token.symbol,
title: token.title,
tokenImage: token.image,
primary: '',
primary: token.primary,
secondary: token.secondary,
isStakeable: false,
tokenChainImage: token.image as string,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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.
Expand Down Expand Up @@ -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
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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,
);
13 changes: 8 additions & 5 deletions ui/components/app/assets/token-list/token-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -36,18 +38,18 @@ function TokenList({ onTokenClick }: TokenListProps) {
chainIds: chainIdsToPoll as Hex[],
});

const nonEvmNativeToken = useNativeTokenBalance();
const multichainAssets = useMultiChainAssets();
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 },
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 currentNetwork.chainId rather than this hard coded string.

Copy link
Contributor

Choose a reason for hiding this comment

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

updated here 21abbb2

filterCallback: 'inclusive',
},
]);
Expand All @@ -61,6 +63,7 @@ function TokenList({ onTokenClick }: TokenListProps) {
selectedAccount,
newTokensImported,
evmBalances,
multichainAssets,
]);

useEffect(() => {
Expand Down
1 change: 1 addition & 0 deletions ui/components/app/assets/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export type TokenWithFiatAmount = Token &
TokenDisplayValues &
TokenBalanceValues & {
isStakeable?: boolean;
title: string;
};

export type TokenFiatDisplayInfo = TokenWithFiatAmount & TokenDisplayInfo;
Expand Down
2 changes: 1 addition & 1 deletion ui/components/app/assets/util/networkTitleOverrides.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export type TranslateFunction = (arg: string) => string;
// usually in the case of native tokens for L1 networks
export const networkTitleOverrides = (
t: TranslateFunction, // translate function from useI18nContext() hook
token: TokenFiatDisplayInfo,
token: Partial<TokenFiatDisplayInfo>,
) => {
switch (token.title) {
case CURRENCY_SYMBOLS.ETH:
Expand Down
32 changes: 32 additions & 0 deletions ui/selectors/multichain-assets-rates.test.ts
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();
});
});
15 changes: 15 additions & 0 deletions ui/selectors/multichain-assets-rates.ts
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;
}
Loading