Commit 613b9ec6 authored by tom goriunov's avatar tom goriunov Committed by GitHub

MetaMask: update chain data and add NFT tokens (#2598)

* Tweak `Add {Network}` button to enable network details update in MetaMask

Fixes #2591

* add NFTs tokens to MM wallet
parent ef9bbebd
......@@ -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
......@@ -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
......@@ -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
\ No newline at end of file
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 ]);
}
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 ]);
}
import { get } from 'es-toolkit/compat';
import React from 'react';
import config from 'configs/app';
import getErrorObj from 'lib/errors/getErrorObj';
import useAddChain from './useAddChain';
import useProvider from './useProvider';
import useSwitchChain from './useSwitchChain';
export default function useAddOrSwitchChain() {
export default function useSwitchOrAddChain() {
const { wallet, provider } = useProvider();
const addChain = useAddChain();
const switchChain = useSwitchChain();
return React.useCallback(async() => {
if (!wallet || !provider) {
return;
}
const hexadecimalChainId = '0x' + Number(config.chain.id).toString(16);
try {
return await provider.request({
method: 'wallet_switchEthereumChain',
params: [ { chainId: hexadecimalChainId } ],
});
return switchChain();
} catch (error) {
const errorObj = getErrorObj(error);
const code = errorObj && 'code' in errorObj ? errorObj.code : undefined;
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) {
const params = [ {
chainId: hexadecimalChainId,
chainName: config.chain.name,
nativeCurrency: {
name: config.chain.currency.name,
symbol: config.chain.currency.symbol,
decimals: config.chain.currency.decimals,
},
rpcUrls: config.chain.rpcUrls,
blockExplorerUrls: [ config.app.baseUrl ],
} ] as never;
// in wagmi types for wallet_addEthereumChain method is not provided
return await provider.request({
method: 'wallet_addEthereumChain',
params: params,
});
return addChain();
}
throw error;
}
}, [ provider, wallet ]);
}, [ addChain, provider, wallet, switchChain ]);
}
export function getHexadecimalChainId(chainId: number) {
return '0x' + Number(chainId).toString(16);
}
......@@ -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';
......@@ -14,7 +15,8 @@ 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) {
......@@ -22,7 +24,8 @@ const NetworkAddToWallet = () => {
}
try {
await addOrSwitchChain();
await addChain();
await switchChain();
toast({
position: 'top-right',
......@@ -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;
......
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) {
......@@ -33,20 +68,18 @@ const AddressAddToWallet = ({ className, token, isLoading, variant = 'icon', ico
}
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) {
......@@ -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;
......@@ -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;
}
......
......@@ -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 }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment