Skip to content

MetaMask: update chain data and add NFT tokens #2598

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

Merged
merged 2 commits into from
Mar 5, 2025
Merged
Show file tree
Hide file tree
Changes from all 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
17 changes: 8 additions & 9 deletions configs/envs/.env.eth_sepolia
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,15 @@ NEXT_PUBLIC_AD_ADBUTLER_CONFIG_DESKTOP={ "id": "632019", "width": "728", "height
NEXT_PUBLIC_AD_ADBUTLER_CONFIG_MOBILE={ "id": "632018", "width": "320", "height": "100" }
NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com
NEXT_PUBLIC_API_BASE_PATH=/
NEXT_PUBLIC_API_HOST=eth-sepolia.k8s-dev.blockscout.com
NEXT_PUBLIC_API_HOST=eth-sepolia.blockscout.com
NEXT_PUBLIC_API_SPEC_URL=https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml
NEXT_PUBLIC_CONTRACT_CODE_IDES=[{'title':'Remix IDE','url':'https://remix.ethereum.org/?address={hash}&blockscout={domain}','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}]
NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info-test.k8s-dev.blockscout.com
NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.com
NEXT_PUBLIC_DATA_AVAILABILITY_ENABLED=true
NEXT_PUBLIC_DEFI_DROPDOWN_ITEMS=[{'text':'Swap','icon':'swap','dappId':'cow-swap'},{'text':'Payment link','icon':'payment_link','dappId':'peanut-protocol'}]
NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/eth-sepolia.json
NEXT_PUBLIC_FOOTER_LINKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/footer-links/sepolia.json
NEXT_PUBLIC_GAME_BADGE_CLAIM_LINK=https://badges.blockscout.com/mint/sherblockHolmesBadge
NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0xbf69c7abc4fee283b59a9633dadfdaedde5c5ee0fba3e80a08b5b8a3acbd4363
NEXT_PUBLIC_HAS_BEACON_CHAIN=true
NEXT_PUBLIC_HAS_USER_OPS=true
Expand All @@ -30,12 +31,11 @@ NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG={'background':['rgba(51, 53, 67, 1)'],'t
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
NEXT_PUBLIC_IS_TESTNET=true
NEXT_PUBLIC_LOGOUT_URL=https://blockscout-goerli.us.auth0.com/v2/logout
NEXT_PUBLIC_MAINTENANCE_ALERT_MESSAGE=<p>Participated in our recent Blockscout activities? <a href="https://badges.blockscout.com?utm_source=instance&utm_medium=sepolia" target="_blank">Check your eligibility</a> and claim your NFT Scout badges. More exciting things are coming soon!</p>
NEXT_PUBLIC_MAINTENANCE_ALERT_MESSAGE=<p>Joined recent campaigns? Mint your Merit Badge <a href="https://badges.blockscout.com?utm_source=instance&utm_medium=sepolia">here</a></p>
NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL=https://gist.githubusercontent.com/maikReal/974c47f86a3158c1a86b092ae2f044b3/raw/abcc7e02150cd85d4974503a0357162c0a2c35a9/merits-banner.html
NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL=https://swap.blockscout.com?utm_source=blockscout&utm_medium=eth-sepolia
NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-categories/default.json
NEXT_PUBLIC_MARKETPLACE_ENABLED=true
NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_API_KEY=patbqG4V2CI998jAq.9810c58c9de973ba2650621c94559088cbdfa1a914498e385621ed035d33c0d0
NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_BASE_ID=appGkvtmKI7fXE4Vs
NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-security-reports/default.json
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/appiy5yijZpMMSKjT/shr6uMGPKjj1DK7NL
Expand All @@ -62,11 +62,10 @@ NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation
NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED=true
NEXT_PUBLIC_OG_IMAGE_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/og-images/sepolia-testnet.png
NEXT_PUBLIC_OTHER_LINKS=[{'url':'https://sepolia.drpc.org?ref=559183','text':'Public RPC'}]
NEXT_PUBLIC_REWARDS_SERVICE_API_HOST=https://points.k8s-dev.blockscout.com
NEXT_PUBLIC_REWARDS_SERVICE_API_HOST=https://merits.blockscout.com
NEXT_PUBLIC_SAFE_TX_SERVICE_URL=https://safe-transaction-sepolia.safe.global
NEXT_PUBLIC_STATS_API_HOST=https://stats-sepolia.k8s.blockscout.com
NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=noves
NEXT_PUBLIC_STATS_API_HOST=https://stats-sepolia.k8s-prod-2.blockscout.com
NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout
NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
NEXT_PUBLIC_XSTAR_SCORE_URL=https://docs.xname.app/the-solution-adaptive-proof-of-humanity-on-blockchain/xhs-scoring-algorithm?utm_source=blockscout&utm_medium=address
NEXT_PUBLIC_DEX_POOLS_ENABLED=true
NEXT_PUBLIC_XSTAR_SCORE_URL=https://docs.xname.app/the-solution-adaptive-proof-of-humanity-on-blockchain/xhs-scoring-algorithm?utm_source=blockscout&utm_medium=address
40 changes: 40 additions & 0 deletions lib/web3/useAddChain.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import React from 'react';
import type { AddEthereumChainParameter } from 'viem';

import config from 'configs/app';

import useProvider from './useProvider';
import { getHexadecimalChainId } from './utils';

function getParams(): AddEthereumChainParameter {
if (!config.chain.id) {
throw new Error('Missing required chain config');
}

return {
chainId: getHexadecimalChainId(Number(config.chain.id)),
chainName: config.chain.name ?? '',
nativeCurrency: {
name: config.chain.currency.name ?? '',
symbol: config.chain.currency.symbol ?? '',
decimals: config.chain.currency.decimals ?? 18,
},
rpcUrls: config.chain.rpcUrls,
blockExplorerUrls: [ config.app.baseUrl ],
};
}

export default function useAddChain() {
const { wallet, provider } = useProvider();

return React.useCallback(() => {
if (!wallet || !provider) {
throw new Error('Wallet or provider not found');
}

return provider.request({
method: 'wallet_addEthereumChain',
params: [ getParams() ],
});
}, [ wallet, provider ]);
}
54 changes: 0 additions & 54 deletions lib/web3/useAddOrSwitchChain.tsx

This file was deleted.

29 changes: 29 additions & 0 deletions lib/web3/useSwitchChain.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import React from 'react';

import config from 'configs/app';

import useProvider from './useProvider';
import { getHexadecimalChainId } from './utils';

function getParams(): { chainId: string } {
if (!config.chain.id) {
throw new Error('Missing required chain config');
}

return { chainId: getHexadecimalChainId(Number(config.chain.id)) };
}

export default function useSwitchChain() {
const { wallet, provider } = useProvider();

return React.useCallback(() => {
if (!wallet || !provider) {
throw new Error('Wallet or provider not found');
}

return provider.request({
method: 'wallet_switchEthereumChain',
params: [ getParams() ],
});
}, [ wallet, provider ]);
}
35 changes: 35 additions & 0 deletions lib/web3/useSwitchOrAddChain.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { get } from 'es-toolkit/compat';
import React from 'react';

import getErrorObj from 'lib/errors/getErrorObj';

import useAddChain from './useAddChain';
import useProvider from './useProvider';
import useSwitchChain from './useSwitchChain';

export default function useSwitchOrAddChain() {
const { wallet, provider } = useProvider();
const addChain = useAddChain();
const switchChain = useSwitchChain();

return React.useCallback(async() => {
if (!wallet || !provider) {
return;
}

try {
return switchChain();
} catch (error) {
const errorObj = getErrorObj(error);
const code = get(errorObj, 'code');
const originalErrorCode = get(errorObj, 'data.originalError.code');

// This error code indicates that the chain has not been added to Wallet.
if (code === 4902 || originalErrorCode === 4902) {
return addChain();
}

throw error;
}
}, [ addChain, provider, wallet, switchChain ]);
}
3 changes: 3 additions & 0 deletions lib/web3/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function getHexadecimalChainId(chainId: number) {
return '0x' + Number(chainId).toString(16);
}
11 changes: 7 additions & 4 deletions ui/shared/NetworkAddToWallet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import React from 'react';
import config from 'configs/app';
import useToast from 'lib/hooks/useToast';
import * as mixpanel from 'lib/mixpanel/index';
import useAddOrSwitchChain from 'lib/web3/useAddOrSwitchChain';
import useAddChain from 'lib/web3/useAddChain';
import useProvider from 'lib/web3/useProvider';
import useSwitchChain from 'lib/web3/useSwitchChain';
import { WALLETS_INFO } from 'lib/web3/wallets';
import IconSvg from 'ui/shared/IconSvg';

Expand All @@ -14,15 +15,17 @@ const feature = config.features.web3Wallet;
const NetworkAddToWallet = () => {
const toast = useToast();
const { provider, wallet } = useProvider();
const addOrSwitchChain = useAddOrSwitchChain();
const addChain = useAddChain();
const switchChain = useSwitchChain();

const handleClick = React.useCallback(async() => {
if (!wallet || !provider) {
return;
}

try {
await addOrSwitchChain();
await addChain();
await switchChain();

toast({
position: 'top-right',
Expand All @@ -48,7 +51,7 @@ const NetworkAddToWallet = () => {
isClosable: true,
});
}
}, [ addOrSwitchChain, provider, toast, wallet ]);
}, [ addChain, provider, toast, wallet, switchChain ]);

if (!provider || !wallet || !config.chain.rpcUrls.length || !feature.isEnabled) {
return null;
Expand Down
72 changes: 57 additions & 15 deletions ui/shared/address/AddressAddToWallet.tsx
Original file line number Diff line number Diff line change
@@ -1,52 +1,85 @@
import { Box, chakra, IconButton, Tooltip } from '@chakra-ui/react';
import React from 'react';
import type { WatchAssetParams } from 'viem';

import type { TokenInfo } from 'types/api/token';

import config from 'configs/app';
import useIsMobile from 'lib/hooks/useIsMobile';
import useToast from 'lib/hooks/useToast';
import * as mixpanel from 'lib/mixpanel/index';
import useAddOrSwitchChain from 'lib/web3/useAddOrSwitchChain';
import useProvider from 'lib/web3/useProvider';
import useSwitchOrAddChain from 'lib/web3/useSwitchOrAddChain';
import { WALLETS_INFO } from 'lib/web3/wallets';
import Skeleton from 'ui/shared/chakra/Skeleton';
import IconSvg from 'ui/shared/IconSvg';

const feature = config.features.web3Wallet;

function getRequestParams(token: TokenInfo, tokenId?: string): WatchAssetParams | undefined {
switch (token.type) {
case 'ERC-20':
return {
type: 'ERC20',
options: {
address: token.address,
symbol: token.symbol || '',
decimals: Number(token.decimals) || 18,
image: token.icon_url || '',
},
};
case 'ERC-721':
case 'ERC-1155': {
if (!tokenId) {
return;
}

return {
type: token.type === 'ERC-721' ? 'ERC721' : 'ERC1155',
options: {
address: token.address,
tokenId: tokenId,
},
} as never; // There is no official EIP, and therefore no typings for these token types.
}
default:
return;
}
}

interface Props {
className?: string;
token: TokenInfo;
tokenId?: string;
isLoading?: boolean;
variant?: 'icon' | 'button';
iconSize?: number;
}

const AddressAddToWallet = ({ className, token, isLoading, variant = 'icon', iconSize = 6 }: Props) => {
const AddressAddToWallet = ({ className, token, tokenId, isLoading, variant = 'icon', iconSize = 6 }: Props) => {
const toast = useToast();
const { provider, wallet } = useProvider();
const addOrSwitchChain = useAddOrSwitchChain();
const switchOrAddChain = useSwitchOrAddChain();
const isMobile = useIsMobile();

const handleClick = React.useCallback(async() => {
if (!wallet) {
return;
}

try {
const params = getRequestParams(token, tokenId);

if (!params) {
throw new Error('Unsupported token type');
}

// switch to the correct network otherwise the token will be added to the wrong one
await addOrSwitchChain();
await switchOrAddChain();

const wasAdded = await provider?.request?.({
method: 'wallet_watchAsset',
params: {
type: 'ERC20', // Initially only supports ERC20, but eventually more!
options: {
address: token.address,
symbol: token.symbol || '',
decimals: Number(token.decimals) || 18,
image: token.icon_url || '',
},
},
params,
});

if (wasAdded) {
Expand Down Expand Up @@ -75,7 +108,7 @@ const AddressAddToWallet = ({ className, token, isLoading, variant = 'icon', ico
isClosable: true,
});
}
}, [ toast, token, provider, wallet, addOrSwitchChain ]);
}, [ wallet, token, tokenId, switchOrAddChain, provider, toast ]);

if (!provider || !wallet) {
return null;
Expand All @@ -85,7 +118,16 @@ const AddressAddToWallet = ({ className, token, isLoading, variant = 'icon', ico
return <Skeleton className={ className } boxSize={ iconSize } borderRadius="base"/>;
}

if (!feature.isEnabled) {
const canBeAdded = (
// MetaMask can add NFTs now, but this is still experimental feature, and doesn't work on mobile devices
// https://docs.metamask.io/wallet/how-to/display/tokens/#display-nfts
wallet === 'metamask' &&
[ 'ERC-721', 'ERC-1155' ].includes(token.type) &&
tokenId &&
!isMobile
) || token.type === 'ERC-20';

if (!feature.isEnabled || !canBeAdded) {
return null;
}

Expand Down
2 changes: 1 addition & 1 deletion ui/tokenInstance/TokenInstancePageTitle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ const TokenInstancePageTitle = ({ isLoading, token, instance, hash }: Props) =>
maxW="700px"
/>
) }
{ !isLoading && <AddressAddToWallet token={ token } variant="button"/> }
{ !isLoading && <AddressAddToWallet token={ token } tokenId={ instance?.id } variant="button"/> }
<AddressQrCode address={ address } isLoading={ isLoading }/>
<AccountActionsMenu isLoading={ isLoading } showUpdateMetadataItem/>
{ appLink }
Expand Down
Loading