Skip to content

Commit c786bb6

Browse files
tom2drumcarlomigueldy
authored andcommitted
MetaMask: update chain data and add NFT tokens (blockscout#2598)
* Tweak `Add {Network}` button to enable network details update in MetaMask Fixes blockscout#2591 * add NFTs tokens to MM wallet
1 parent 93cb017 commit c786bb6

File tree

9 files changed

+180
-83
lines changed

9 files changed

+180
-83
lines changed

configs/envs/.env.eth_sepolia

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,15 @@ NEXT_PUBLIC_AD_ADBUTLER_CONFIG_DESKTOP={ "id": "632019", "width": "728", "height
1414
NEXT_PUBLIC_AD_ADBUTLER_CONFIG_MOBILE={ "id": "632018", "width": "320", "height": "100" }
1515
NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com
1616
NEXT_PUBLIC_API_BASE_PATH=/
17-
NEXT_PUBLIC_API_HOST=eth-sepolia.k8s-dev.blockscout.com
17+
NEXT_PUBLIC_API_HOST=eth-sepolia.blockscout.com
1818
NEXT_PUBLIC_API_SPEC_URL=https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml
1919
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'}]
20-
NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info-test.k8s-dev.blockscout.com
20+
NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.com
2121
NEXT_PUBLIC_DATA_AVAILABILITY_ENABLED=true
2222
NEXT_PUBLIC_DEFI_DROPDOWN_ITEMS=[{'text':'Swap','icon':'swap','dappId':'cow-swap'},{'text':'Payment link','icon':'payment_link','dappId':'peanut-protocol'}]
2323
NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/eth-sepolia.json
2424
NEXT_PUBLIC_FOOTER_LINKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/footer-links/sepolia.json
25+
NEXT_PUBLIC_GAME_BADGE_CLAIM_LINK=https://badges.blockscout.com/mint/sherblockHolmesBadge
2526
NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0xbf69c7abc4fee283b59a9633dadfdaedde5c5ee0fba3e80a08b5b8a3acbd4363
2627
NEXT_PUBLIC_HAS_BEACON_CHAIN=true
2728
NEXT_PUBLIC_HAS_USER_OPS=true
@@ -30,12 +31,11 @@ NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG={'background':['rgba(51, 53, 67, 1)'],'t
3031
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
3132
NEXT_PUBLIC_IS_TESTNET=true
3233
NEXT_PUBLIC_LOGOUT_URL=https://blockscout-goerli.us.auth0.com/v2/logout
33-
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>
34+
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>
3435
NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL=https://gist.githubusercontent.com/maikReal/974c47f86a3158c1a86b092ae2f044b3/raw/abcc7e02150cd85d4974503a0357162c0a2c35a9/merits-banner.html
3536
NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL=https://swap.blockscout.com?utm_source=blockscout&utm_medium=eth-sepolia
3637
NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-categories/default.json
3738
NEXT_PUBLIC_MARKETPLACE_ENABLED=true
38-
NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_API_KEY=patbqG4V2CI998jAq.9810c58c9de973ba2650621c94559088cbdfa1a914498e385621ed035d33c0d0
3939
NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_BASE_ID=appGkvtmKI7fXE4Vs
4040
NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-security-reports/default.json
4141
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/appiy5yijZpMMSKjT/shr6uMGPKjj1DK7NL
@@ -62,11 +62,10 @@ NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation
6262
NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED=true
6363
NEXT_PUBLIC_OG_IMAGE_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/og-images/sepolia-testnet.png
6464
NEXT_PUBLIC_OTHER_LINKS=[{'url':'https://sepolia.drpc.org?ref=559183','text':'Public RPC'}]
65-
NEXT_PUBLIC_REWARDS_SERVICE_API_HOST=https://points.k8s-dev.blockscout.com
65+
NEXT_PUBLIC_REWARDS_SERVICE_API_HOST=https://merits.blockscout.com
6666
NEXT_PUBLIC_SAFE_TX_SERVICE_URL=https://safe-transaction-sepolia.safe.global
67-
NEXT_PUBLIC_STATS_API_HOST=https://stats-sepolia.k8s.blockscout.com
68-
NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=noves
67+
NEXT_PUBLIC_STATS_API_HOST=https://stats-sepolia.k8s-prod-2.blockscout.com
68+
NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout
6969
NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true
7070
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
71-
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
72-
NEXT_PUBLIC_DEX_POOLS_ENABLED=true
71+
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

lib/web3/useAddChain.tsx

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import React from 'react';
2+
import type { AddEthereumChainParameter } from 'viem';
3+
4+
import config from 'configs/app';
5+
6+
import useProvider from './useProvider';
7+
import { getHexadecimalChainId } from './utils';
8+
9+
function getParams(): AddEthereumChainParameter {
10+
if (!config.chain.id) {
11+
throw new Error('Missing required chain config');
12+
}
13+
14+
return {
15+
chainId: getHexadecimalChainId(Number(config.chain.id)),
16+
chainName: config.chain.name ?? '',
17+
nativeCurrency: {
18+
name: config.chain.currency.name ?? '',
19+
symbol: config.chain.currency.symbol ?? '',
20+
decimals: config.chain.currency.decimals ?? 18,
21+
},
22+
rpcUrls: config.chain.rpcUrls,
23+
blockExplorerUrls: [ config.app.baseUrl ],
24+
};
25+
}
26+
27+
export default function useAddChain() {
28+
const { wallet, provider } = useProvider();
29+
30+
return React.useCallback(() => {
31+
if (!wallet || !provider) {
32+
throw new Error('Wallet or provider not found');
33+
}
34+
35+
return provider.request({
36+
method: 'wallet_addEthereumChain',
37+
params: [ getParams() ],
38+
});
39+
}, [ wallet, provider ]);
40+
}

lib/web3/useAddOrSwitchChain.tsx

Lines changed: 0 additions & 54 deletions
This file was deleted.

lib/web3/useSwitchChain.tsx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import React from 'react';
2+
3+
import config from 'configs/app';
4+
5+
import useProvider from './useProvider';
6+
import { getHexadecimalChainId } from './utils';
7+
8+
function getParams(): { chainId: string } {
9+
if (!config.chain.id) {
10+
throw new Error('Missing required chain config');
11+
}
12+
13+
return { chainId: getHexadecimalChainId(Number(config.chain.id)) };
14+
}
15+
16+
export default function useSwitchChain() {
17+
const { wallet, provider } = useProvider();
18+
19+
return React.useCallback(() => {
20+
if (!wallet || !provider) {
21+
throw new Error('Wallet or provider not found');
22+
}
23+
24+
return provider.request({
25+
method: 'wallet_switchEthereumChain',
26+
params: [ getParams() ],
27+
});
28+
}, [ wallet, provider ]);
29+
}

lib/web3/useSwitchOrAddChain.tsx

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { get } from 'es-toolkit/compat';
2+
import React from 'react';
3+
4+
import getErrorObj from 'lib/errors/getErrorObj';
5+
6+
import useAddChain from './useAddChain';
7+
import useProvider from './useProvider';
8+
import useSwitchChain from './useSwitchChain';
9+
10+
export default function useSwitchOrAddChain() {
11+
const { wallet, provider } = useProvider();
12+
const addChain = useAddChain();
13+
const switchChain = useSwitchChain();
14+
15+
return React.useCallback(async() => {
16+
if (!wallet || !provider) {
17+
return;
18+
}
19+
20+
try {
21+
return switchChain();
22+
} catch (error) {
23+
const errorObj = getErrorObj(error);
24+
const code = get(errorObj, 'code');
25+
const originalErrorCode = get(errorObj, 'data.originalError.code');
26+
27+
// This error code indicates that the chain has not been added to Wallet.
28+
if (code === 4902 || originalErrorCode === 4902) {
29+
return addChain();
30+
}
31+
32+
throw error;
33+
}
34+
}, [ addChain, provider, wallet, switchChain ]);
35+
}

lib/web3/utils.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export function getHexadecimalChainId(chainId: number) {
2+
return '0x' + Number(chainId).toString(16);
3+
}

ui/shared/NetworkAddToWallet.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ import React from 'react';
44
import config from 'configs/app';
55
import useToast from 'lib/hooks/useToast';
66
import * as mixpanel from 'lib/mixpanel/index';
7-
import useAddOrSwitchChain from 'lib/web3/useAddOrSwitchChain';
7+
import useAddChain from 'lib/web3/useAddChain';
88
import useProvider from 'lib/web3/useProvider';
9+
import useSwitchChain from 'lib/web3/useSwitchChain';
910
import { WALLETS_INFO } from 'lib/web3/wallets';
1011
import IconSvg from 'ui/shared/IconSvg';
1112

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

1921
const handleClick = React.useCallback(async() => {
2022
if (!wallet || !provider) {
2123
return;
2224
}
2325

2426
try {
25-
await addOrSwitchChain();
27+
await addChain();
28+
await switchChain();
2629

2730
toast({
2831
position: 'top-right',
@@ -48,7 +51,7 @@ const NetworkAddToWallet = () => {
4851
isClosable: true,
4952
});
5053
}
51-
}, [ addOrSwitchChain, provider, toast, wallet ]);
54+
}, [ addChain, provider, toast, wallet, switchChain ]);
5255

5356
if (!provider || !wallet || !config.chain.rpcUrls.length || !feature.isEnabled) {
5457
return null;

ui/shared/address/AddressAddToWallet.tsx

Lines changed: 57 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,85 @@
11
import { Box, chakra, IconButton, Tooltip } from '@chakra-ui/react';
22
import React from 'react';
3+
import type { WatchAssetParams } from 'viem';
34

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

67
import config from 'configs/app';
8+
import useIsMobile from 'lib/hooks/useIsMobile';
79
import useToast from 'lib/hooks/useToast';
810
import * as mixpanel from 'lib/mixpanel/index';
9-
import useAddOrSwitchChain from 'lib/web3/useAddOrSwitchChain';
1011
import useProvider from 'lib/web3/useProvider';
12+
import useSwitchOrAddChain from 'lib/web3/useSwitchOrAddChain';
1113
import { WALLETS_INFO } from 'lib/web3/wallets';
1214
import Skeleton from 'ui/shared/chakra/Skeleton';
1315
import IconSvg from 'ui/shared/IconSvg';
1416

1517
const feature = config.features.web3Wallet;
1618

19+
function getRequestParams(token: TokenInfo, tokenId?: string): WatchAssetParams | undefined {
20+
switch (token.type) {
21+
case 'ERC-20':
22+
return {
23+
type: 'ERC20',
24+
options: {
25+
address: token.address,
26+
symbol: token.symbol || '',
27+
decimals: Number(token.decimals) || 18,
28+
image: token.icon_url || '',
29+
},
30+
};
31+
case 'ERC-721':
32+
case 'ERC-1155': {
33+
if (!tokenId) {
34+
return;
35+
}
36+
37+
return {
38+
type: token.type === 'ERC-721' ? 'ERC721' : 'ERC1155',
39+
options: {
40+
address: token.address,
41+
tokenId: tokenId,
42+
},
43+
} as never; // There is no official EIP, and therefore no typings for these token types.
44+
}
45+
default:
46+
return;
47+
}
48+
}
49+
1750
interface Props {
1851
className?: string;
1952
token: TokenInfo;
53+
tokenId?: string;
2054
isLoading?: boolean;
2155
variant?: 'icon' | 'button';
2256
iconSize?: number;
2357
}
2458

25-
const AddressAddToWallet = ({ className, token, isLoading, variant = 'icon', iconSize = 6 }: Props) => {
59+
const AddressAddToWallet = ({ className, token, tokenId, isLoading, variant = 'icon', iconSize = 6 }: Props) => {
2660
const toast = useToast();
2761
const { provider, wallet } = useProvider();
28-
const addOrSwitchChain = useAddOrSwitchChain();
62+
const switchOrAddChain = useSwitchOrAddChain();
63+
const isMobile = useIsMobile();
2964

3065
const handleClick = React.useCallback(async() => {
3166
if (!wallet) {
3267
return;
3368
}
3469

3570
try {
71+
const params = getRequestParams(token, tokenId);
72+
73+
if (!params) {
74+
throw new Error('Unsupported token type');
75+
}
76+
3677
// switch to the correct network otherwise the token will be added to the wrong one
37-
await addOrSwitchChain();
78+
await switchOrAddChain();
3879

3980
const wasAdded = await provider?.request?.({
4081
method: 'wallet_watchAsset',
41-
params: {
42-
type: 'ERC20', // Initially only supports ERC20, but eventually more!
43-
options: {
44-
address: token.address,
45-
symbol: token.symbol || '',
46-
decimals: Number(token.decimals) || 18,
47-
image: token.icon_url || '',
48-
},
49-
},
82+
params,
5083
});
5184

5285
if (wasAdded) {
@@ -75,7 +108,7 @@ const AddressAddToWallet = ({ className, token, isLoading, variant = 'icon', ico
75108
isClosable: true,
76109
});
77110
}
78-
}, [ toast, token, provider, wallet, addOrSwitchChain ]);
111+
}, [ wallet, token, tokenId, switchOrAddChain, provider, toast ]);
79112

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

88-
if (!feature.isEnabled) {
121+
const canBeAdded = (
122+
// MetaMask can add NFTs now, but this is still experimental feature, and doesn't work on mobile devices
123+
// https://docs.metamask.io/wallet/how-to/display/tokens/#display-nfts
124+
wallet === 'metamask' &&
125+
[ 'ERC-721', 'ERC-1155' ].includes(token.type) &&
126+
tokenId &&
127+
!isMobile
128+
) || token.type === 'ERC-20';
129+
130+
if (!feature.isEnabled || !canBeAdded) {
89131
return null;
90132
}
91133

ui/tokenInstance/TokenInstancePageTitle.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ const TokenInstancePageTitle = ({ isLoading, token, instance, hash }: Props) =>
103103
maxW="700px"
104104
/>
105105
) }
106-
{ !isLoading && <AddressAddToWallet token={ token } variant="button"/> }
106+
{ !isLoading && <AddressAddToWallet token={ token } tokenId={ instance?.id } variant="button"/> }
107107
<AddressQrCode address={ address } isLoading={ isLoading }/>
108108
<AccountActionsMenu isLoading={ isLoading } showUpdateMetadataItem/>
109109
{ appLink }

0 commit comments

Comments
 (0)