Commit daa4dc19 authored by tom goriunov's avatar tom goriunov Committed by GitHub

TokenPocket wallet integration (#1088)

* base implementation

* switch network before adding token

* make envs-validator happy

* add deployment values

* fix env type

* update icon
parent 673c6dee
import type { Feature } from './types';
import type { WalletType } from 'types/client/wallets';
import { getEnvValue } from '../utils';
import { getEnvValue, parseEnvJson } from '../utils';
const wallets = ((): Array<WalletType> | undefined => {
const envValue = getEnvValue(process.env.NEXT_PUBLIC_WEB3_WALLETS);
if (envValue === 'none') {
return;
}
const defaultWallet = ((): WalletType => {
const envValue = getEnvValue(process.env.NEXT_PUBLIC_WEB3_DEFAULT_WALLET) as WalletType;
const SUPPORTED_WALLETS: Array<WalletType> = [
'metamask',
'coinbase',
'token_pocket',
];
return envValue && SUPPORTED_WALLETS.includes(envValue) ? envValue : 'metamask';
const wallets = parseEnvJson<Array<WalletType>>(envValue)?.filter((type) => SUPPORTED_WALLETS.includes(type));
if (!wallets || wallets.length === 0) {
return [ 'metamask' ];
}
return wallets;
})();
const title = 'Web3 wallet integration (add token or network to the wallet)';
const config: Feature<{ defaultWallet: Exclude<WalletType, 'none'>; addToken: { isDisabled: boolean }}> = (() => {
if (defaultWallet !== 'none') {
const config: Feature<{ wallets: Array<WalletType>; addToken: { isDisabled: boolean }}> = (() => {
if (wallets && wallets.length > 0) {
return Object.freeze({
title,
isEnabled: true,
defaultWallet,
wallets,
addToken: {
isDisabled: getEnvValue(process.env.NEXT_PUBLIC_WEB3_DISABLE_ADD_TOKEN_TO_WALLET) === 'true',
},
......
......@@ -46,3 +46,4 @@ NEXT_PUBLIC_STATS_API_HOST=https://stats-goerli.k8s-dev.blockscout.com
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.com
NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com
NEXT_PUBLIC_WEB3_WALLETS=['token_pocket','metamask']
......@@ -37,7 +37,7 @@ NEXT_PUBLIC_APP_INSTANCE=local
NEXT_PUBLIC_APP_ENV=development
NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0x4a0ed8ddf751a7cb5297f827699117b0f6d21a0b2907594d300dc9fed75c7e62
NEXT_PUBLIC_API_SPEC_URL=https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml
NEXT_PUBLIC_WEB3_DEFAULT_WALLET=coinbase
NEXT_PUBLIC_WEB3_WALLETS=['coinbase']
NEXT_PUBLIC_WEB3_DISABLE_ADD_TOKEN_TO_WALLET=true
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
NEXT_PUBLIC_AUTH_URL=http://localhost:3000
......
......@@ -187,7 +187,7 @@ frontend:
NEXT_PUBLIC_NETWORK_EXPLORERS: ''
NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND: "linear-gradient(136.9deg,rgb(107 94 236) 1.5%,rgb(0 82 255) 56.84%,rgb(82 62 231) 98.54%)"
NEXT_PUBLIC_NETWORK_RPC_URL: https://goerli.optimism.io
NEXT_PUBLIC_WEB3_DEFAULT_WALLET: coinbase
NEXT_PUBLIC_WEB3_WALLETS: "['coinbase']"
NEXT_PUBLIC_WEB3_DISABLE_ADD_TOKEN_TO_WALLET: true
NEXT_PUBLIC_HOMEPAGE_CHARTS: "['daily_txs']"
NEXT_PUBLIC_VISUALIZE_API_HOST: https://visualizer-test.k8s-dev.blockscout.com
......
......@@ -158,6 +158,7 @@ frontend:
NEXT_PUBLIC_API_SPEC_URL: https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml
NEXT_PUBLIC_MARKETPLACE_CONFIG_URL: https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/marketplace/eth-goerli.json
NEXT_PUBLIC_GRAPHIQL_TRANSACTION: 0xf7d4972356e6ae44ae948d0cf19ef2beaf0e574c180997e969a2837da15e349d
NEXT_PUBLIC_WEB3_WALLETS: "['token_pocket','coinbase','metamask']"
envFromSecret:
NEXT_PUBLIC_SENTRY_DSN: ref+vault://deployment-values/blockscout/dev/front-main?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_SENTRY_DSN
SENTRY_CSP_REPORT_URI: ref+vault://deployment-values/blockscout/dev/front-main?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/SENTRY_CSP_REPORT_URI
......
......@@ -109,8 +109,8 @@ frontend:
_default: "linear-gradient(136.9deg,rgb(107 94 236) 1.5%,rgb(0 82 255) 56.84%,rgb(82 62 231) 98.54%)"
NEXT_PUBLIC_NETWORK_RPC_URL:
_default: https://goerli.optimism.io
NEXT_PUBLIC_WEB3_DEFAULT_WALLET:
_default: coinbase
NEXT_PUBLIC_WEB3_WALLETS:
_default: "['coinbase']"
NEXT_PUBLIC_WEB3_DISABLE_ADD_TOKEN_TO_WALLET:
_default: true
NEXT_PUBLIC_HOMEPAGE_CHARTS:
......
......@@ -123,3 +123,5 @@ frontend:
_default: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY
NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID:
_default: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID
NEXT_PUBLIC_WEB3_WALLETS:
_default: "['token_pocket','coinbase','metamask']"
......@@ -333,11 +333,11 @@ For each application, you need to specify the `MarketplaceCategoryId` to which i
### Web3 wallet integration (add token or network to the wallet)
This feature is **enabled by default** with the `metamask` wallet type. To switch it off pass `NEXT_PUBLIC_WEB3_DEFAULT_WALLET=none`.
This feature is **enabled by default** with the `['metamask']` value. To switch it off pass `NEXT_PUBLIC_WEB3_WALLETS=none`.
| Variable | Type| Description | Compulsoriness | Default value | Example value |
| --- | --- | --- | --- | --- | --- |
| NEXT_PUBLIC_WEB3_DEFAULT_WALLET | `metamask` \| `coinbase` \| `none` | Type of Web3 wallet which will be used by default to add tokens or chains to | - | `metamask` | `coinbase` |
| NEXT_PUBLIC_WEB3_WALLETS | `Array<'metamask' \| 'coinbase' \| 'token_pocket'>` | Array of Web3 wallets which will be used to add tokens or chain to. The first wallet which is enabled in user's browser will be shown. | - | `[ 'metamask' ]` | `[ 'coinbase' ]` |
| NEXT_PUBLIC_WEB3_DISABLE_ADD_TOKEN_TO_WALLET | `boolean`| Set to `true` to hide icon "Add to your wallet" next to token addresses | - | - | `true` |
&nbsp;
......
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="token-pocket_svg__a" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="2" y="6" width="20" height="13">
<path d="M22 6H2v12.904h20V6Z" fill="#fff"/>
</mask>
<g mask="url(#token-pocket_svg__a)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.84 6H2.611A.61.61 0 0 0 2 6.611v2.482a.61.61 0 0 0 .611.611h2.327v8.589c0 .338.274.611.612.611h2.604a.61.61 0 0 0 .611-.611V9.704h1.064c1.022 0 1.853-.83 1.853-1.852A1.838 1.838 0 0 0 9.84 6Zm5.877 10.112.003.001v-4.992a1.156 1.156 0 0 1 2.309 0 1.15 1.15 0 0 1-1.154 1.155v3.967a5.125 5.125 0 0 0 5.121-5.121A5.119 5.119 0 0 0 16.875 6a5.123 5.123 0 0 0-5.121 5.121v7.154a.61.61 0 0 0 .611.612h2.74a.61.61 0 0 0 .612-.612v-2.163Z" fill="#2980FE"/>
<path d="M15.72 11.122v4.991a5.169 5.169 0 0 0 1.04.13h.036v-3.97c-.6-.04-1.075-.54-1.075-1.151Z" fill="url(#token-pocket_svg__b)"/>
<path d="M16.875 16.243v-3.967c-.029 0-.054 0-.083-.004v3.97h.083Z" fill="#2980FE"/>
</g>
<defs>
<linearGradient id="token-pocket_svg__b" x1="16.877" y1="13.683" x2="15.721" y2="13.683" gradientUnits="userSpaceOnUse">
<stop stop-color="#2980FE"/>
<stop offset=".967" stop-color="#6CA8FF"/>
<stop offset="1" stop-color="#2980FE"/>
</linearGradient>
</defs>
</svg>
import type { WalletType } from 'types/client/wallets';
export enum EventTypes {
PAGE_VIEW = 'Page view',
SEARCH_QUERY = 'Search query',
ADD_TO_WALLET = 'Add to wallet',
}
/* eslint-disable @typescript-eslint/indent */
export type EventPayload<Type extends EventTypes> =
Type extends EventTypes.PAGE_VIEW ?
{
'Page type': string;
'Tab': string;
'Page'?: string;
} :
Type extends EventTypes.SEARCH_QUERY ? {
'Search query': string;
'Source page type': string;
'Result URL': string;
} :
undefined;
Type extends EventTypes.PAGE_VIEW ?
{
'Page type': string;
'Tab': string;
'Page'?: string;
} :
Type extends EventTypes.SEARCH_QUERY ? {
'Search query': string;
'Source page type': string;
'Result URL': string;
} :
Type extends EventTypes.ADD_TO_WALLET ? (
{
'Wallet': WalletType;
'Target': 'network';
} | {
'Wallet': WalletType;
'Target': 'token';
'Token': string;
}
) :
undefined;
/* eslint-enable @typescript-eslint/indent */
import React from 'react';
import config from 'configs/app';
import getErrorObj from 'lib/errors/getErrorObj';
import useProvider from './useProvider';
export default function useAddOrSwitchChain() {
const { wallet, provider } = useProvider();
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 } ],
});
} catch (error) {
const errorObj = getErrorObj(error);
const code = errorObj && 'code' in errorObj ? errorObj.code : undefined;
// This error code indicates that the chain has not been added to Wallet.
if (code === 4902) {
const params = {
method: 'wallet_addEthereumChain',
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.rpcUrl ],
blockExplorerUrls: [ config.app.baseUrl ],
} ],
// in wagmi types for wallet_addEthereumChain method is not provided
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any;
return await provider.request({
method: 'wallet_addEthereumChain',
params,
});
}
throw error;
}
}, [ provider, wallet ]);
}
......@@ -3,12 +3,15 @@ import type { WindowProvider } from 'wagmi';
import 'wagmi/window';
import type { WalletType } from 'types/client/wallets';
import config from 'configs/app';
const feature = config.features.web3Wallet;
export default function useProvider() {
const [ provider, setProvider ] = React.useState<WindowProvider>();
const [ wallet, setWallet ] = React.useState<WalletType>();
React.useEffect(() => {
if (!('ethereum' in window && window.ethereum) || !feature.isEnabled) {
......@@ -19,16 +22,22 @@ export default function useProvider() {
// if user has only one wallet, the provider is injected in the window.ethereum directly
const providers = Array.isArray(window.ethereum.providers) ? window.ethereum.providers : [ window.ethereum ];
providers.forEach(async(provider) => {
if (feature.defaultWallet === 'coinbase' && provider.isCoinbaseWallet) {
return setProvider(provider);
}
if (feature.defaultWallet === 'metamask' && provider.isMetaMask) {
return setProvider(provider);
for (const wallet of feature.wallets) {
const provider = providers.find((provider) => {
return (
(wallet === 'coinbase' && provider.isCoinbaseWallet) ||
(wallet === 'metamask' && provider.isMetaMask) ||
(wallet === 'token_pocket' && provider.isTokenPocket)
);
});
if (provider) {
setProvider(provider);
setWallet(wallet);
break;
}
});
}
}, []);
return provider;
return { provider, wallet };
}
......@@ -2,6 +2,7 @@ import type { WalletType, WalletInfo } from 'types/client/wallets';
import coinbaseIcon from 'icons/wallets/coinbase.svg';
import metamaskIcon from 'icons/wallets/metamask.svg';
import tokenPocketIcon from 'icons/wallets/token-pocket.svg';
export const WALLETS_INFO: Record<Exclude<WalletType, 'none'>, WalletInfo> = {
metamask: {
......@@ -12,4 +13,8 @@ export const WALLETS_INFO: Record<Exclude<WalletType, 'none'>, WalletInfo> = {
name: 'Coinbase Wallet',
icon: coinbaseIcon,
},
token_pocket: {
name: 'TokenPocket',
icon: tokenPocketIcon,
},
};
export type WalletType = 'metamask' | 'coinbase' | 'none';
export type WalletType = 'metamask' | 'coinbase' | 'token_pocket';
export interface WalletInfo {
name: string;
......
......@@ -22,7 +22,7 @@ export type NextPublicEnvs = {
NEXT_PUBLIC_FOOTER_LINKS?: string;
NEXT_PUBLIC_API_SPEC_URL?: string;
NEXT_PUBLIC_GRAPHIQL_TRANSACTION?: string;
NEXT_PUBLIC_WEB3_DEFAULT_WALLET?: 'metamask' | 'coinbase';
NEXT_PUBLIC_WEB3_WALLETS?: string;
NEXT_PUBLIC_WEB3_DISABLE_ADD_TOKEN_TO_WALLET?: 'true' | 'false';
NEXT_PUBLIC_HIDE_INDEXING_ALERT?: 'true' | 'false';
......
......@@ -3,6 +3,8 @@ 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 useProvider from 'lib/web3/useProvider';
import { WALLETS_INFO } from 'lib/web3/wallets';
......@@ -14,28 +16,17 @@ interface Props {
const NetworkAddToWallet = ({ className }: Props) => {
const toast = useToast();
const provider = useProvider();
const { provider, wallet } = useProvider();
const addOrSwitchChain = useAddOrSwitchChain();
const handleClick = React.useCallback(async() => {
if (!wallet || !provider) {
return;
}
try {
const hexadecimalChainId = '0x' + Number(config.chain.id).toString(16);
const params = {
method: 'wallet_addEthereumChain',
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.rpcUrl ],
blockExplorerUrls: [ config.app.baseUrl ],
} ],
// in wagmi types for wallet_addEthereumChain method is not provided
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any;
await provider?.request?.(params);
await addOrSwitchChain();
toast({
position: 'top-right',
title: 'Success',
......@@ -44,6 +35,12 @@ const NetworkAddToWallet = ({ className }: Props) => {
variant: 'subtle',
isClosable: true,
});
mixpanel.logEvent(mixpanel.EventTypes.ADD_TO_WALLET, {
Target: 'network',
Wallet: wallet,
});
} catch (error) {
toast({
position: 'top-right',
......@@ -54,15 +51,15 @@ const NetworkAddToWallet = ({ className }: Props) => {
isClosable: true,
});
}
}, [ provider, toast ]);
}, [ addOrSwitchChain, provider, toast, wallet ]);
if (!provider || !config.chain.rpcUrl || !feature.isEnabled) {
if (!provider || !wallet || !config.chain.rpcUrl || !feature.isEnabled) {
return null;
}
return (
<Button variant="outline" size="sm" onClick={ handleClick } className={ className }>
<Icon as={ WALLETS_INFO[feature.defaultWallet].icon } boxSize={ 5 } mr={ 2 }/>
<Icon as={ WALLETS_INFO[wallet].icon } boxSize={ 5 } mr={ 2 }/>
Add { config.chain.name }
</Button>
);
......
......@@ -5,6 +5,8 @@ import type { TokenInfo } from 'types/api/token';
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 useProvider from 'lib/web3/useProvider';
import { WALLETS_INFO } from 'lib/web3/wallets';
......@@ -18,10 +20,18 @@ interface Props {
const AddressAddToWallet = ({ className, token, isLoading }: Props) => {
const toast = useToast();
const provider = useProvider();
const { provider, wallet } = useProvider();
const addOrSwitchChain = useAddOrSwitchChain();
const handleClick = React.useCallback(async() => {
if (!wallet) {
return;
}
try {
// switch to the correct network otherwise the token will be added to the wrong one
await addOrSwitchChain();
const wasAdded = await provider?.request?.({
method: 'wallet_watchAsset',
params: {
......@@ -30,8 +40,7 @@ const AddressAddToWallet = ({ className, token, isLoading }: Props) => {
address: token.address,
symbol: token.symbol || '',
decimals: Number(token.decimals) || 18,
// TODO: add token image when we have it in API
// image: ''
image: token.icon_url || '',
},
},
});
......@@ -45,6 +54,12 @@ const AddressAddToWallet = ({ className, token, isLoading }: Props) => {
variant: 'subtle',
isClosable: true,
});
mixpanel.logEvent(mixpanel.EventTypes.ADD_TO_WALLET, {
Target: 'token',
Wallet: wallet,
Token: token.symbol || '',
});
}
} catch (error) {
toast({
......@@ -56,9 +71,9 @@ const AddressAddToWallet = ({ className, token, isLoading }: Props) => {
isClosable: true,
});
}
}, [ toast, token, provider ]);
}, [ toast, token, provider, wallet, addOrSwitchChain ]);
if (!provider) {
if (!provider || !wallet) {
return null;
}
......@@ -71,9 +86,9 @@ const AddressAddToWallet = ({ className, token, isLoading }: Props) => {
}
return (
<Tooltip label={ `Add token to ${ WALLETS_INFO[feature.defaultWallet].name }` }>
<Tooltip label={ `Add token to ${ WALLETS_INFO[wallet].name }` }>
<Box className={ className } display="inline-flex" cursor="pointer" onClick={ handleClick }>
<Icon as={ WALLETS_INFO[feature.defaultWallet].icon } boxSize={ 6 }/>
<Icon as={ WALLETS_INFO[wallet].icon } boxSize={ 6 }/>
</Box>
</Tooltip>
);
......
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