Commit 6e98beeb authored by tom goriunov's avatar tom goriunov Committed by GitHub

Merge pull request #739 from blockscout/account/verified-addresses

address verification in my account
parents 4908184b 033aadcd
...@@ -50,6 +50,7 @@ NEXT_PUBLIC_API_PROTOCOL=__PLACEHOLDER_FOR_NEXT_PUBLIC_API_PROTOCOL__ ...@@ -50,6 +50,7 @@ NEXT_PUBLIC_API_PROTOCOL=__PLACEHOLDER_FOR_NEXT_PUBLIC_API_PROTOCOL__
NEXT_PUBLIC_API_PORT=__PLACEHOLDER_FOR_NEXT_PUBLIC_API_PORT__ NEXT_PUBLIC_API_PORT=__PLACEHOLDER_FOR_NEXT_PUBLIC_API_PORT__
NEXT_PUBLIC_STATS_API_HOST=__PLACEHOLDER_FOR_NEXT_PUBLIC_STATS_API_HOST__ NEXT_PUBLIC_STATS_API_HOST=__PLACEHOLDER_FOR_NEXT_PUBLIC_STATS_API_HOST__
NEXT_PUBLIC_VISUALIZE_API_HOST=__PLACEHOLDER_FOR_NEXT_PUBLIC_VISUALIZE_API_HOST__ NEXT_PUBLIC_VISUALIZE_API_HOST=__PLACEHOLDER_FOR_NEXT_PUBLIC_VISUALIZE_API_HOST__
NEXT_PUBLIC_CONTRACT_INFO_API_HOST=__PLACEHOLDER_FOR_NEXT_PUBLIC_CONTRACT_INFO_API_HOST__
NEXT_PUBLIC_API_SPEC_URL=__PLACEHOLDER_FOR_NEXT_PUBLIC_API_SPEC_URL__ NEXT_PUBLIC_API_SPEC_URL=__PLACEHOLDER_FOR_NEXT_PUBLIC_API_SPEC_URL__
# external services config # external services config
......
{
"typescript.tsdk": "node_modules/typescript/lib"
}
\ No newline at end of file
...@@ -99,9 +99,9 @@ The app instance could be customized by passing following variables to NodeJS en ...@@ -99,9 +99,9 @@ The app instance could be customized by passing following variables to NodeJS en
| --- | --- | --- | --- | | --- | --- | --- | --- |
| NEXT_PUBLIC_API_HOST | `string` *(optional)* | By default the API endpoint base URL will be set as `https://blockscout.com`. If it is not the case, pass the API host in this variable | `my-host.com` | | NEXT_PUBLIC_API_HOST | `string` *(optional)* | By default the API endpoint base URL will be set as `https://blockscout.com`. If it is not the case, pass the API host in this variable | `my-host.com` |
| NEXT_PUBLIC_API_BASE_PATH | `string` *(optional)* | Base path for API endpoint url | `/poa/core` | | NEXT_PUBLIC_API_BASE_PATH | `string` *(optional)* | Base path for API endpoint url | `/poa/core` |
| NEXT_PUBLIC_STATS_API_HOST | `string` *(optional)* | Pass the Stats API host in this variable | `https://my-host.com` | | NEXT_PUBLIC_STATS_API_HOST | `string` | Pass the Stats API host in this variable | `https://my-host.com` |
| NEXT_PUBLIC_VISUALIZE_API_HOST | `string` *(optional)* | Pass the Visualize API host in this variable | `https://my-host.com` | | NEXT_PUBLIC_VISUALIZE_API_HOST | `string` | Pass the Visualize API host in this variable | `https://my-host.com` |
| NEXT_PUBLIC_CONTRACT_INFO_API_HOST | `string` *(optional)* | Pass the Contract Info API host in this variable if token info submission feature should be available in the app | `https://my-host.com` |
### Featured network configuration properties ### Featured network configuration properties
......
...@@ -26,7 +26,8 @@ const baseUrl = [ ...@@ -26,7 +26,8 @@ const baseUrl = [
appPort && ':' + appPort, appPort && ':' + appPort,
].filter(Boolean).join(''); ].filter(Boolean).join('');
const authUrl = getEnvValue(process.env.NEXT_PUBLIC_AUTH_URL) || baseUrl; const authUrl = getEnvValue(process.env.NEXT_PUBLIC_AUTH_URL) || baseUrl;
const apiHost = getEnvValue(process.env.NEXT_PUBLIC_API_HOST); // const apiHost = getEnvValue(process.env.NEXT_PUBLIC_API_HOST);
const apiHost = 'eth-goerli.blockscout.com';
const apiSchema = getEnvValue(process.env.NEXT_PUBLIC_API_PROTOCOL) || 'https'; const apiSchema = getEnvValue(process.env.NEXT_PUBLIC_API_PROTOCOL) || 'https';
const apiPort = getEnvValue(process.env.NEXT_PUBLIC_API_PORT); const apiPort = getEnvValue(process.env.NEXT_PUBLIC_API_PORT);
const apiEndpoint = apiHost ? [ const apiEndpoint = apiHost ? [
...@@ -118,6 +119,10 @@ const config = Object.freeze({ ...@@ -118,6 +119,10 @@ const config = Object.freeze({
endpoint: getEnvValue(process.env.NEXT_PUBLIC_VISUALIZE_API_HOST), endpoint: getEnvValue(process.env.NEXT_PUBLIC_VISUALIZE_API_HOST),
basePath: '', basePath: '',
}, },
contractInfoApi: {
endpoint: getEnvValue(process.env.NEXT_PUBLIC_CONTRACT_INFO_API_HOST),
basePath: '',
},
homepage: { homepage: {
charts: parseEnvJson<Array<ChainIndicatorId>>(getEnvValue(process.env.NEXT_PUBLIC_HOMEPAGE_CHARTS)) || [], charts: parseEnvJson<Array<ChainIndicatorId>>(getEnvValue(process.env.NEXT_PUBLIC_HOMEPAGE_CHARTS)) || [],
plateGradient: getEnvValue(process.env.NEXT_PUBLIC_HOMEPAGE_PLATE_GRADIENT) || plateGradient: getEnvValue(process.env.NEXT_PUBLIC_HOMEPAGE_PLATE_GRADIENT) ||
......
...@@ -16,3 +16,4 @@ NEXT_PUBLIC_API_SPEC_URL=https://raw.githubusercontent.com/blockscout/blockscout ...@@ -16,3 +16,4 @@ NEXT_PUBLIC_API_SPEC_URL=https://raw.githubusercontent.com/blockscout/blockscout
NEXT_PUBLIC_API_HOST=blockscout.com NEXT_PUBLIC_API_HOST=blockscout.com
NEXT_PUBLIC_STATS_API_HOST=https://stats-test.aws-k8s.blockscout.com NEXT_PUBLIC_STATS_API_HOST=https://stats-test.aws-k8s.blockscout.com
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.aws-k8s.blockscout.com NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.aws-k8s.blockscout.com
NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info-test.aws-k8s.blockscout.com
...@@ -19,3 +19,4 @@ NEXT_PUBLIC_IS_TESTNET=true ...@@ -19,3 +19,4 @@ NEXT_PUBLIC_IS_TESTNET=true
# api config # api config
NEXT_PUBLIC_API_HOST=blockscout.com NEXT_PUBLIC_API_HOST=blockscout.com
NEXT_PUBLIC_STATS_API_HOST=https://stats-test.aws-k8s.blockscout.com NEXT_PUBLIC_STATS_API_HOST=https://stats-test.aws-k8s.blockscout.com
NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info-test.aws-k8s.blockscout.com
...@@ -397,6 +397,8 @@ frontend: ...@@ -397,6 +397,8 @@ frontend:
_default: "true" _default: "true"
NEXT_PUBLIC_VISUALIZE_API_HOST: NEXT_PUBLIC_VISUALIZE_API_HOST:
_default: https://visualizer-optimism-goerli.test.aws-k8s.blockscout.com _default: https://visualizer-optimism-goerli.test.aws-k8s.blockscout.com
NEXT_PUBLIC_CONTRACT_INFO_API_HOST:
_default: https://contracts-info-test.aws-k8s.blockscout.com
NEXT_PUBLIC_IS_L2_NETWORK: NEXT_PUBLIC_IS_L2_NETWORK:
_default: "true" _default: "true"
NEXT_PUBLIC_L1_BASE_URL: NEXT_PUBLIC_L1_BASE_URL:
......
...@@ -352,9 +352,11 @@ frontend: ...@@ -352,9 +352,11 @@ frontend:
NEXT_PUBLIC_API_HOST: NEXT_PUBLIC_API_HOST:
_default: blockscout.com/eth/goerli _default: blockscout.com/eth/goerli
NEXT_PUBLIC_STATS_API_HOST: NEXT_PUBLIC_STATS_API_HOST:
_default: https://stats-test.aws-k8s.blockscout.com/ _default: https://stats-test.aws-k8s.blockscout.com
NEXT_PUBLIC_VISUALIZE_API_HOST: NEXT_PUBLIC_VISUALIZE_API_HOST:
_default: https://visualizer.aws-k8s.blockscout.com _default: https://visualizer.aws-k8s.blockscout.com
NEXT_PUBLIC_CONTRACT_INFO_API_HOST:
_default: https://contracts-info-test.aws-k8s.blockscout.com
NEXT_PUBLIC_APP_HOST: NEXT_PUBLIC_APP_HOST:
_default: blockscout.com/eth/goerli _default: blockscout.com/eth/goerli
NEXT_PUBLIC_LOGOUT_URL: NEXT_PUBLIC_LOGOUT_URL:
......
...@@ -123,6 +123,8 @@ frontend: ...@@ -123,6 +123,8 @@ frontend:
_default: "true" _default: "true"
NEXT_PUBLIC_VISUALIZE_API_HOST: NEXT_PUBLIC_VISUALIZE_API_HOST:
_default: https://visualizer-optimism-goerli.test.aws-k8s.blockscout.com _default: https://visualizer-optimism-goerli.test.aws-k8s.blockscout.com
NEXT_PUBLIC_CONTRACT_INFO_API_HOST:
_default: https://contracts-info-test.aws-k8s.blockscout.com
NEXT_PUBLIC_IS_L2_NETWORK: NEXT_PUBLIC_IS_L2_NETWORK:
_default: "true" _default: "true"
NEXT_PUBLIC_L1_BASE_URL: NEXT_PUBLIC_L1_BASE_URL:
......
...@@ -95,6 +95,8 @@ frontend: ...@@ -95,6 +95,8 @@ frontend:
_default: https://stats-test.aws-k8s.blockscout.com/ _default: https://stats-test.aws-k8s.blockscout.com/
NEXT_PUBLIC_VISUALIZE_API_HOST: NEXT_PUBLIC_VISUALIZE_API_HOST:
_default: https://visualizer.aws-k8s.blockscout.com _default: https://visualizer.aws-k8s.blockscout.com
NEXT_PUBLIC_CONTRACT_INFO_API_HOST:
_default: https://contracts-info-test.aws-k8s.blockscout.com
NEXT_PUBLIC_AUTH_URL: NEXT_PUBLIC_AUTH_URL:
_default: https://blockscout-main.test.aws-k8s.blockscout.com _default: https://blockscout-main.test.aws-k8s.blockscout.com
NEXT_PUBLIC_API_BASE_PATH: NEXT_PUBLIC_API_BASE_PATH:
......
import appConfig from 'configs/app/config'; // import appConfig from 'configs/app/config';
// FIXME // FIXME
// I was not able to figure out how to send CORS with credentials from localhost // I was not able to figure out how to send CORS with credentials from localhost
// unsuccessfully tried different ways, even custom local dev domain // unsuccessfully tried different ways, even custom local dev domain
// so for local development we have to use next.js api as proxy server // so for local development we have to use next.js api as proxy server
export default function isNeedProxy() { export default function isNeedProxy() {
return appConfig.host === 'localhost' && appConfig.host !== appConfig.api.host; return true;
// return appConfig.host === 'localhost' && appConfig.host !== appConfig.api.host;
} }
import type { UserInfo, CustomAbis, PublicTags, AddressTags, TransactionTags, ApiKeys, WatchlistAddress } from 'types/api/account'; import type { UserInfo, CustomAbis, PublicTags, AddressTags, TransactionTags, ApiKeys, WatchlistAddress, VerifiedAddressResponse } from 'types/api/account';
import type { import type {
Address, Address,
AddressCounters, AddressCounters,
...@@ -86,6 +86,21 @@ export const RESOURCES = { ...@@ -86,6 +86,21 @@ export const RESOURCES = {
pathParams: [ 'id' as const ], pathParams: [ 'id' as const ],
}, },
// ACCOUNT: ADDRESS VERIFICATION & TOKEN INFO
address_verification: {
path: '/api/v1/chains/:chainId/verified-addresses:type',
pathParams: [ 'chainId' as const, 'type' as const ],
endpoint: appConfig.contractInfoApi.endpoint,
basePath: appConfig.contractInfoApi.basePath,
},
verified_addresses: {
path: '/api/v1/chains/:chainId/verified-addresses',
pathParams: [ 'chainId' as const ],
endpoint: appConfig.contractInfoApi.endpoint,
basePath: appConfig.contractInfoApi.basePath,
},
// STATS // STATS
stats_counters: { stats_counters: {
path: '/api/v1/counters', path: '/api/v1/counters',
...@@ -484,6 +499,7 @@ Q extends 'private_tags_address' ? AddressTags : ...@@ -484,6 +499,7 @@ Q extends 'private_tags_address' ? AddressTags :
Q extends 'private_tags_tx' ? TransactionTags : Q extends 'private_tags_tx' ? TransactionTags :
Q extends 'api_keys' ? ApiKeys : Q extends 'api_keys' ? ApiKeys :
Q extends 'watchlist' ? Array<WatchlistAddress> : Q extends 'watchlist' ? Array<WatchlistAddress> :
Q extends 'verified_addresses' ? VerifiedAddressResponse :
Q extends 'homepage_stats' ? HomeStats : Q extends 'homepage_stats' ? HomeStats :
Q extends 'homepage_chart_txs' ? ChartTransactionResponse : Q extends 'homepage_chart_txs' ? ChartTransactionResponse :
Q extends 'homepage_chart_market' ? ChartMarketResponse : Q extends 'homepage_chart_market' ? ChartMarketResponse :
......
...@@ -223,7 +223,14 @@ export default function useNavItems(): ReturnType { ...@@ -223,7 +223,14 @@ export default function useNavItems(): ReturnType {
isActive: pathname === '/account/custom_abi', isActive: pathname === '/account/custom_abi',
isNewUi: true, isNewUi: true,
}, },
]; appConfig.contractInfoApi.endpoint && {
text: 'Verified addrs',
nextRoute: { pathname: '/account/verified_addresses' as const },
icon: verifiedIcon,
isActive: pathname === '/account/verified_addresses',
isNewUi: true,
},
].filter(Boolean);
const profileItem = { const profileItem = {
text: 'My profile', text: 'My profile',
......
export default function shortenString(string: string | null) {
if (!string) {
return '';
}
if (string.length <= 7) {
return string;
}
return string.slice(0, 4) + '...' + string.slice(-4);
}
export const SIGNATURE_REGEXP = /^0x[a-fA-F\d]{130}$/;
import type { NextPage } from 'next';
import Head from 'next/head';
import React from 'react';
import getNetworkTitle from 'lib/networks/getNetworkTitle';
import VerifiedAddresses from 'ui/pages/VerifiedAddresses';
const VerifiedAddressesPage: NextPage = () => {
const title = getNetworkTitle();
return (
<>
<Head><title>{ title }</title></Head>
<VerifiedAddresses/>
</>
);
};
export default VerifiedAddressesPage;
export { getServerSideProps } from 'lib/next/getServerSideProps';
import type { test } from '@playwright/experimental-ct-react'; import type { test } from '@playwright/experimental-ct-react';
import type { PlaywrightWorkerArgs } from '@playwright/test';
interface Env { interface Env {
name: string; name: string;
...@@ -6,9 +7,17 @@ interface Env { ...@@ -6,9 +7,17 @@ interface Env {
} }
// keep in mind that all passed variables here should be present in env config files (.env.pw or .env.poa) // keep in mind that all passed variables here should be present in env config files (.env.pw or .env.poa)
export default function createContextWithEnvs(envs: Array<Env>): Parameters<typeof test.extend>[0]['context'] { export default function contextWithEnvsFixture(envs: Array<Env>): Parameters<typeof test.extend>[0]['context'] {
return async({ browser }, use) => { return async({ browser }, use) => {
const context = await browser.newContext({ const context = await createContextWithEnvs(browser, envs);
await use(context);
await context.close();
};
}
export function createContextWithEnvs(browser: PlaywrightWorkerArgs['browser'], envs: Array<Env>) {
return browser.newContext({
storageState: { storageState: {
origins: [ origins: [
{ origin: 'http://localhost:3100', localStorage: envs }, { origin: 'http://localhost:3100', localStorage: envs },
...@@ -16,8 +25,4 @@ export default function createContextWithEnvs(envs: Array<Env>): Parameters<type ...@@ -16,8 +25,4 @@ export default function createContextWithEnvs(envs: Array<Env>): Parameters<type
cookies: [], cookies: [],
}, },
}); });
await use(context);
await context.close();
};
} }
...@@ -3,7 +3,6 @@ const zIndices = { ...@@ -3,7 +3,6 @@ const zIndices = {
auto: 'auto', auto: 'auto',
base: 0, base: 0,
docked: 10, docked: 10,
tooltip: 900,
dropdown: 1000, dropdown: 1000,
sticky: 1100, sticky: 1100,
sticky1: 1101, sticky1: 1101,
...@@ -12,6 +11,7 @@ const zIndices = { ...@@ -12,6 +11,7 @@ const zIndices = {
overlay: 1300, overlay: 1300,
modal: 1400, modal: 1400,
popover: 1500, popover: 1500,
tooltip: 1550, // otherwise tooltips will not be visible in modals
skipLink: 1600, skipLink: 1600,
toast: 1700, toast: 1700,
}; };
......
...@@ -160,3 +160,14 @@ export type PublicTagErrors = { ...@@ -160,3 +160,14 @@ export type PublicTagErrors = {
full_name: Array<string>; full_name: Array<string>;
tags: Array<string>; tags: Array<string>;
} }
export interface VerifiedAddress {
userId: string;
chainId: string;
contractAddress: string;
verifiedDate: string;
}
export interface VerifiedAddressResponse {
verifiedAddresses: Array<VerifiedAddress>;
}
...@@ -10,6 +10,7 @@ declare module "nextjs-routes" { ...@@ -10,6 +10,7 @@ declare module "nextjs-routes" {
| StaticRoute<"/account/custom_abi"> | StaticRoute<"/account/custom_abi">
| StaticRoute<"/account/public_tags_request"> | StaticRoute<"/account/public_tags_request">
| StaticRoute<"/account/tag_address"> | StaticRoute<"/account/tag_address">
| StaticRoute<"/account/verified_addresses">
| StaticRoute<"/account/watchlist"> | StaticRoute<"/account/watchlist">
| StaticRoute<"/accounts"> | StaticRoute<"/accounts">
| DynamicRoute<"/address/[hash]/contract_verification", { "hash": string }> | DynamicRoute<"/address/[hash]/contract_verification", { "hash": string }>
......
import { useColorModeValue, useToken } from '@chakra-ui/react';
import {
EthereumClient,
modalConnectors,
walletConnectProvider,
} from '@web3modal/ethereum';
import { Web3Modal } from '@web3modal/react';
import React from 'react'; import React from 'react';
import type { Chain } from 'wagmi';
import { configureChains, createClient, WagmiConfig } from 'wagmi';
import type { RoutedSubTab } from 'ui/shared/RoutedTabs/types'; import type { RoutedSubTab } from 'ui/shared/RoutedTabs/types';
import appConfig from 'configs/app/config';
import { ContractContextProvider } from 'ui/address/contract/context'; import { ContractContextProvider } from 'ui/address/contract/context';
import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs'; import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
import Web3ModalProvider from 'ui/shared/Web3ModalProvider';
interface Props { interface Props {
tabs: Array<RoutedSubTab>; tabs: Array<RoutedSubTab>;
addressHash?: string; addressHash?: string;
} }
const { wagmiClient, ethereumClient } = (() => {
try {
const currentChain: Chain = {
id: Number(appConfig.network.id),
name: appConfig.network.name || '',
network: appConfig.network.name || '',
nativeCurrency: {
decimals: appConfig.network.currency.decimals,
name: appConfig.network.currency.name || '',
symbol: appConfig.network.currency.symbol || '',
},
rpcUrls: {
'default': {
http: [ appConfig.network.rpcUrl || '' ],
},
},
blockExplorers: {
'default': {
name: 'Blockscout',
url: appConfig.baseUrl,
},
},
};
const chains = [ currentChain ];
const { provider } = configureChains(chains, [
walletConnectProvider({ projectId: appConfig.walletConnect.projectId || '' }),
]);
const wagmiClient = createClient({
autoConnect: true,
connectors: modalConnectors({ appName: 'web3Modal', chains }),
provider,
});
const ethereumClient = new EthereumClient(wagmiClient, chains);
return { wagmiClient, ethereumClient };
} catch (error) {
return { wagmiClient: undefined, ethereumClient: undefined };
}
})();
const TAB_LIST_PROPS = { const TAB_LIST_PROPS = {
columnGap: 3, columnGap: 3,
}; };
const AddressContract = ({ addressHash, tabs }: Props) => { const AddressContract = ({ addressHash, tabs }: Props) => {
const modalZIndex = useToken<string>('zIndices', 'modal'); const fallback = React.useCallback(() => {
const web3ModalTheme = useColorModeValue('light', 'dark'); const noProviderTabs = tabs.filter(({ id }) => id === 'contact_code');
const noProviderTabs = React.useMemo(() => tabs.filter(({ id }) => id === 'contact_code'), [ tabs ]);
if (!wagmiClient || !ethereumClient) {
return <RoutedTabs tabs={ noProviderTabs } variant="outline" colorScheme="gray" size="sm" tabListProps={ TAB_LIST_PROPS }/>; return <RoutedTabs tabs={ noProviderTabs } variant="outline" colorScheme="gray" size="sm" tabListProps={ TAB_LIST_PROPS }/>;
} }, [ tabs ]);
return ( return (
<WagmiConfig client={ wagmiClient }> <Web3ModalProvider fallback={ fallback }>
<ContractContextProvider addressHash={ addressHash }> <ContractContextProvider addressHash={ addressHash }>
<RoutedTabs tabs={ tabs } variant="outline" colorScheme="gray" size="sm" tabListProps={ TAB_LIST_PROPS }/> <RoutedTabs tabs={ tabs } variant="outline" colorScheme="gray" size="sm" tabListProps={ TAB_LIST_PROPS }/>
</ContractContextProvider> </ContractContextProvider>
<Web3Modal </Web3ModalProvider>
projectId={ appConfig.walletConnect.projectId }
ethereumClient={ ethereumClient }
themeZIndex={ Number(modalZIndex) }
themeMode={ web3ModalTheme }
themeBackground="themeColor"
/>
</WagmiConfig>
); );
}; };
......
...@@ -221,7 +221,7 @@ const ContractCode = ({ addressHash, noSocket }: Props) => { ...@@ -221,7 +221,7 @@ const ContractCode = ({ addressHash, noSocket }: Props) => {
<RawDataSnippet <RawDataSnippet
data={ data.creation_bytecode } data={ data.creation_bytecode }
title="Contract creation code" title="Contract creation code"
rightSlot={ data.is_verified ? null : verificationButton } rightSlot={ data.is_verified || data.is_self_destructed ? null : verificationButton }
beforeSlot={ data.is_self_destructed ? ( beforeSlot={ data.is_self_destructed ? (
<Alert status="info" whiteSpace="pre-wrap" mb={ 3 }> <Alert status="info" whiteSpace="pre-wrap" mb={ 3 }>
Contracts that self destruct in their constructors have no contract code published and cannot be verified. Contracts that self destruct in their constructors have no contract code published and cannot be verified.
......
import { Icon, Modal, ModalBody, ModalCloseButton, ModalContent, ModalHeader, ModalOverlay, Link } from '@chakra-ui/react';
import { useQueryClient } from '@tanstack/react-query';
import React from 'react';
import type { AddressVerificationFormFirstStepFields, AddressCheckStatusSuccess } from './types';
import type { VerifiedAddress, VerifiedAddressResponse } from 'types/api/account';
import appConfig from 'configs/app/config';
import eastArrowIcon from 'icons/arrows/east.svg';
import { getResourceKey } from 'lib/api/useApiQuery';
import Web3ModalProvider from 'ui/shared/Web3ModalProvider';
import AddressVerificationStepAddress from './steps/AddressVerificationStepAddress';
import AddressVerificationStepSignature from './steps/AddressVerificationStepSignature';
import AddressVerificationStepSuccess from './steps/AddressVerificationStepSuccess';
interface Props {
isOpen: boolean;
onClose: () => void;
}
const AddressVerificationModal = ({ isOpen, onClose }: Props) => {
const [ stepIndex, setStepIndex ] = React.useState(0);
const [ data, setData ] = React.useState<AddressVerificationFormFirstStepFields & AddressCheckStatusSuccess>({ address: '', signingMessage: '' });
const queryClient = useQueryClient();
const handleGoToSecondStep = React.useCallback((firstStepResult: typeof data) => {
setData(firstStepResult);
setStepIndex((prev) => prev + 1);
}, []);
const handleGoToThirdStep = React.useCallback((newItem: VerifiedAddress) => {
queryClient.setQueryData(
getResourceKey('verified_addresses', { pathParams: { chainId: appConfig.network.id } }),
(prevData: VerifiedAddressResponse | undefined) => {
if (!prevData) {
return { verifiedAddresses: [ newItem ] };
}
return {
verifiedAddresses: [ newItem, ...prevData.verifiedAddresses ],
};
});
setStepIndex((prev) => prev + 1);
}, [ queryClient ]);
const handleGoToPrevStep = React.useCallback(() => {
setStepIndex((prev) => prev - 1);
}, []);
const handleClose = React.useCallback(() => {
onClose();
setStepIndex(0);
}, [ onClose ]);
const steps = [
{ title: 'Verify new address ownership', content: <AddressVerificationStepAddress onContinue={ handleGoToSecondStep }/> },
{ title: 'Copy and sign message', content: <AddressVerificationStepSignature { ...data } onContinue={ handleGoToThirdStep }/> },
{ title: 'Congrats! Address is verified.', content: <AddressVerificationStepSuccess onShowListClick={ handleClose } onAddTokenClick={ handleClose }/> },
];
const step = steps[stepIndex];
return (
<Modal isOpen={ isOpen } onClose={ handleClose } size={{ base: 'full', lg: 'md' }}>
<ModalOverlay/>
<ModalContent>
<ModalHeader fontWeight="500" textStyle="h3" mb={ 6 }>
{ stepIndex !== 0 && (
<Link mr={ 3 } onClick={ handleGoToPrevStep }>
<Icon as={ eastArrowIcon } boxSize={ 6 } transform="rotate(180deg)" verticalAlign="middle"/>
</Link>
) }
<span>{ step.title }</span>
</ModalHeader>
<ModalCloseButton/>
<ModalBody mb={ 0 }>
<Web3ModalProvider>
{ step.content }
</Web3ModalProvider>
</ModalBody>
</ModalContent>
</Modal>
);
};
export default React.memo(AddressVerificationModal);
import { FormControl, Input, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
import type { Control, ControllerRenderProps, FormState } from 'react-hook-form';
import { Controller } from 'react-hook-form';
import type { AddressVerificationFormFirstStepFields, RootFields } from '../types';
import { ADDRESS_REGEXP, ADDRESS_LENGTH } from 'lib/validations/address';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
type Fields = RootFields & AddressVerificationFormFirstStepFields;
interface Props {
formState: FormState<Fields>;
control: Control<Fields>;
}
const AddressVerificationFieldAddress = ({ formState, control }: Props) => {
const backgroundColor = useColorModeValue('white', 'gray.900');
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<Fields, 'address'>}) => {
const error = 'address' in formState.errors ? formState.errors.address : undefined;
return (
<FormControl variant="floating" id={ field.name } isRequired size="md" backgroundColor={ backgroundColor } mt={ 8 }>
<Input
{ ...field }
required
isInvalid={ Boolean(error) }
maxLength={ ADDRESS_LENGTH }
isDisabled={ formState.isSubmitting }
autoComplete="off"
/>
<InputPlaceholder text="Smart contract address (0x...)" error={ error }/>
</FormControl>
);
}, [ formState.errors, formState.isSubmitting, backgroundColor ]);
return (
<Controller
name="address"
control={ control }
render={ renderControl }
rules={{ required: true, pattern: ADDRESS_REGEXP }}
/>
);
};
export default React.memo(AddressVerificationFieldAddress);
import { FormControl, Textarea, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
import type { Control, ControllerRenderProps, FormState } from 'react-hook-form';
import { Controller } from 'react-hook-form';
import type { AddressVerificationFormSecondStepFields, RootFields } from '../types';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
type Fields = RootFields & AddressVerificationFormSecondStepFields;
interface Props {
formState: FormState<Fields>;
control: Control<Fields>;
}
const AddressVerificationFieldMessage = ({ formState, control }: Props) => {
const backgroundColor = useColorModeValue('white', 'gray.900');
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<Fields, 'message'>}) => {
const error = 'message' in formState.errors ? formState.errors.message : undefined;
return (
<FormControl variant="floating" id={ field.name } isRequired size="md" backgroundColor={ backgroundColor }>
<Textarea
{ ...field }
required
isInvalid={ Boolean(error) }
isDisabled
autoComplete="off"
maxH={{ base: '140px', lg: '80px' }}
/>
<InputPlaceholder text="Message to sign" error={ error } isInModal/>
</FormControl>
);
}, [ formState.errors, backgroundColor ]);
return (
<Controller
defaultValue="some value"
name="message"
control={ control }
render={ renderControl }
rules={{ required: true }}
/>
);
};
export default React.memo(AddressVerificationFieldMessage);
import { FormControl, Input, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
import type { Control, ControllerRenderProps, FormState } from 'react-hook-form';
import { Controller } from 'react-hook-form';
import type { AddressVerificationFormSecondStepFields, RootFields } from '../types';
import { SIGNATURE_REGEXP } from 'lib/validations/signature';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
type Fields = RootFields & AddressVerificationFormSecondStepFields;
interface Props {
formState: FormState<Fields>;
control: Control<Fields>;
}
const AddressVerificationFieldSignature = ({ formState, control }: Props) => {
const backgroundColor = useColorModeValue('white', 'gray.900');
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<Fields, 'signature'>}) => {
const error = 'signature' in formState.errors ? formState.errors.signature : undefined;
return (
<FormControl variant="floating" id={ field.name } isRequired size="md" backgroundColor={ backgroundColor }>
<Input
{ ...field }
required
isInvalid={ Boolean(error) }
isDisabled={ formState.isSubmitting }
autoComplete="off"
/>
<InputPlaceholder text="Signature hash" error={ error }/>
</FormControl>
);
}, [ formState.errors, formState.isSubmitting, backgroundColor ]);
return (
<Controller
name="signature"
control={ control }
render={ renderControl }
rules={{ required: true, pattern: SIGNATURE_REGEXP }}
/>
);
};
export default React.memo(AddressVerificationFieldSignature);
import { Alert, Box, Button, Flex, Link } from '@chakra-ui/react';
import { route } from 'nextjs-routes';
import React from 'react';
import type { SubmitHandler } from 'react-hook-form';
import { useForm } from 'react-hook-form';
import type {
AddressVerificationResponseError,
AddressCheckResponseSuccess,
AddressCheckStatusSuccess,
AddressVerificationFormFirstStepFields,
RootFields,
} from '../types';
import appConfig from 'configs/app/config';
import type { ResourceError } from 'lib/api/resources';
import useApiFetch from 'lib/api/useApiFetch';
import LinkInternal from 'ui/shared/LinkInternal';
import AddressVerificationFieldAddress from '../fields/AddressVerificationFieldAddress';
type Fields = RootFields & AddressVerificationFormFirstStepFields;
interface Props {
onContinue: (data: AddressVerificationFormFirstStepFields & AddressCheckStatusSuccess) => void;
}
const AddressVerificationStepAddress = ({ onContinue }: Props) => {
const formApi = useForm<Fields>({
mode: 'onBlur',
});
const { handleSubmit, formState, control, setError, clearErrors, watch } = formApi;
const apiFetch = useApiFetch();
const address = watch('address');
React.useEffect(() => {
clearErrors('root');
}, [ address, clearErrors ]);
const onFormSubmit: SubmitHandler<Fields> = React.useCallback(async(data) => {
try {
const body = {
contractAddress: data.address,
};
const response = await apiFetch<'address_verification', AddressCheckResponseSuccess, AddressVerificationResponseError>('address_verification', {
fetchParams: { method: 'POST', body },
pathParams: { chainId: appConfig.network.id, type: ':prepare' },
});
if (response.status !== 'SUCCESS') {
const type = typeof response.status === 'number' ? 'UNKNOWN_ERROR' : response.status;
const message = ('payload' in response ? response.payload?.message : undefined) || 'Oops! Something went wrong';
return setError('root', { type, message });
}
onContinue({ ...response.result, address: data.address });
} catch (_error) {
const error = _error as ResourceError<AddressVerificationResponseError>;
setError('root', { type: 'manual', message: error.payload?.message || 'Oops! Something went wrong' });
}
}, [ apiFetch, onContinue, setError ]);
const onSubmit = handleSubmit(onFormSubmit);
const rootError = (() => {
switch (formState.errors.root?.type) {
case 'INVALID_ADDRESS_ERROR': {
return <span>Specified address either does not exist or is EOA.</span>;
}
case 'IS_OWNER_ERROR': {
return <span>Ownership of this contract address ownership is already verified by this account.</span>;
}
case 'OWNERSHIP_VERIFIED_ERROR': {
return <span>Ownership of this contract address is already verified by another account.</span>;
}
case 'SOURCE_CODE_NOT_VERIFIED_ERROR': {
const href = route({ pathname: '/address/[hash]/contract_verification', query: { hash: address } });
return (
<Box>
<span>The contract source code you entered is not yet verified. Please follow these steps to </span>
<LinkInternal href={ href }>verify the contract</LinkInternal>
<span>.</span>
</Box>
);
}
case undefined: {
return null;
}
default: {
return formState.errors.root?.message;
}
}
})();
return (
<form noValidate onSubmit={ onSubmit }>
<Box>Let’s check your address...</Box>
{ rootError && <Alert status="warning" mt={ 3 }>{ rootError }</Alert> }
<AddressVerificationFieldAddress formState={ formState } control={ control }/>
<Flex alignItems="center" mt={ 8 } columnGap={ 5 }>
<Button size="lg" type="submit" isDisabled={ formState.isSubmitting }>
Continue
</Button>
<Box>
<span>Contact </span>
<Link>support@blockscout.com</Link>
</Box>
</Flex>
</form>
);
};
export default React.memo(AddressVerificationStepAddress);
import { Alert, Box, Button, chakra, Flex, Link, Radio, RadioGroup } from '@chakra-ui/react';
import { useWeb3Modal } from '@web3modal/react';
import React from 'react';
import type { SubmitHandler } from 'react-hook-form';
import { useForm } from 'react-hook-form';
import { useSignMessage, useAccount } from 'wagmi';
import type {
AddressVerificationFormSecondStepFields,
AddressCheckStatusSuccess,
AddressVerificationFormFirstStepFields,
RootFields,
AddressVerificationResponseError,
AddressValidationResponseSuccess,
} from '../types';
import type { VerifiedAddress } from 'types/api/account';
import appConfig from 'configs/app/config';
import useApiFetch from 'lib/api/useApiFetch';
import shortenString from 'lib/shortenString';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import AddressVerificationFieldMessage from '../fields/AddressVerificationFieldMessage';
import AddressVerificationFieldSignature from '../fields/AddressVerificationFieldSignature';
type Fields = RootFields & AddressVerificationFormSecondStepFields;
interface Props extends AddressVerificationFormFirstStepFields, AddressCheckStatusSuccess{
onContinue: (newItem: VerifiedAddress) => void;
}
const AddressVerificationStepSignature = ({ address, signingMessage, contractCreator, contractOwner, onContinue }: Props) => {
const [ signMethod, setSignMethod ] = React.useState<'wallet' | 'manually'>('wallet');
const { open: openWeb3Modal } = useWeb3Modal();
const { isConnected } = useAccount();
const formApi = useForm<Fields>({
mode: 'onBlur',
defaultValues: {
message: signingMessage,
},
});
const { handleSubmit, formState, control, setValue, getValues, setError, clearErrors, watch } = formApi;
const apiFetch = useApiFetch();
const signature = watch('signature');
React.useEffect(() => {
clearErrors('root');
}, [ clearErrors, signature ]);
const onFormSubmit: SubmitHandler<Fields> = React.useCallback(async(data) => {
try {
const body = {
contractAddress: address,
message: data.message,
signature: data.signature,
};
const response = await apiFetch<'address_verification', AddressValidationResponseSuccess, AddressVerificationResponseError>('address_verification', {
fetchParams: { method: 'POST', body },
pathParams: { chainId: appConfig.network.id, type: ':verify' },
});
if (response.status !== 'SUCCESS') {
const type = typeof response.status === 'number' ? 'UNKNOWN_STATUS' : response.status;
return setError('root', { type, message: response.status === 'INVALID_SIGNER_ERROR' ? response.invalidSigner.signer : undefined });
}
onContinue(response.result.verifiedAddress);
} catch (error) {
setError('root', { type: 'UNKNOWN_STATUS' });
}
}, [ address, apiFetch, onContinue, setError ]);
const onSubmit = handleSubmit(onFormSubmit);
const { signMessage, isLoading: isSigning } = useSignMessage({
onSuccess: (data) => {
setValue('signature', data);
onSubmit();
},
onError: (error) => {
return setError('root', { type: 'SIGNING_FAIL', message: (error as Error)?.message || 'Oops! Something went wrong' });
},
});
const handleSignMethodChange = React.useCallback((value: typeof signMethod) => {
setSignMethod(value);
clearErrors('root');
}, [ clearErrors ]);
const handleOpenWeb3Modal = React.useCallback(() => {
openWeb3Modal();
}, [ openWeb3Modal ]);
const handleWeb3SignClick = React.useCallback(() => {
if (!isConnected) {
return setError('root', { type: 'manual', message: 'Please connect to your Web3 wallet first' });
}
const message = getValues('message');
signMessage({ message });
}, [ getValues, signMessage, isConnected, setError ]);
const handleManualSignClick = React.useCallback(() => {
onSubmit();
}, [ onSubmit ]);
const button = (() => {
if (signMethod === 'manually') {
return (
<Button
size="lg"
onClick={ handleManualSignClick }
isLoading={ formState.isSubmitting }
loadingText="Verifying"
>
Verify
</Button>
);
}
return (
<Button
size="lg"
onClick={ isConnected ? handleWeb3SignClick : handleOpenWeb3Modal }
isLoading={ formState.isSubmitting || isSigning }
loadingText={ isSigning ? 'Signing' : 'Verifying' }
>
{ isConnected ? 'Sign and verify' : 'Connect wallet' }
</Button>
);
})();
const contactUsLink = <Link>contact us</Link>;
const rootError = (() => {
switch (formState.errors.root?.type) {
case 'INVALID_SIGNATURE_ERROR': {
return <span>The signature could not be processed.</span>;
}
case 'VALIDITY_EXPIRED_ERROR': {
return <span>This verification message has expired. Add the contract address to restart the process.</span>;
}
case 'SIGNING_FAIL': {
return <span>{ formState.errors.root.message }</span>;
}
case 'INVALID_SIGNER_ERROR': {
const signer = shortenString(formState.errors.root.message || '');
const expectedSigners = [ contractCreator, contractOwner ].filter(Boolean).map(shortenString).join(', ');
return (
<Box>
<span>This address </span>
<span>{ signer }</span>
<span> is not a creator/owner of the requested contract and cannot claim ownership. Only </span>
<span>{ expectedSigners }2</span>
<span> can verify ownership of this contract.</span>
</Box>
);
}
case 'UNKNOWN_STATUS': {
return (
<Box>
<span>We are not able to process the verify account ownership for this contract address. Kindly </span>
{ contactUsLink }
<span> for further assistance.</span>
</Box>
);
}
case undefined: {
return null;
}
}
})();
return (
<form noValidate onSubmit={ onSubmit }>
{ rootError && <Alert status="warning" mb={ 6 }>{ rootError }</Alert> }
<Box mb={ 8 }>
<span>Please select the address to sign and copy the message and sign it using the Blockscout message provider of your choice. </span>
<Link href="https://docs.blockscout.com/for-users/my-account/verified-addresses/copy-and-sign-message" target="_blank">
Additional instructions
</Link>
<span>. If you do not see your address here but are sure that you are the owner of the contract, kindly </span>
{ contactUsLink }
<span> for further assistance.</span>
</Box>
{ (contractOwner || contractCreator) && (
<Flex flexDir="column" rowGap={ 4 } mb={ 4 }>
{ contractCreator && (
<Box>
<chakra.span fontWeight={ 600 }>Contract creator: </chakra.span>
<chakra.span>{ contractCreator }</chakra.span>
</Box>
) }
{ contractOwner && (
<Box>
<chakra.span fontWeight={ 600 }>Contract owner: </chakra.span>
<chakra.span>{ contractOwner }</chakra.span>
</Box>
) }
</Flex>
) }
<Flex rowGap={ 5 } flexDir="column">
<div>
<CopyToClipboard text={ signingMessage } ml="auto" display="block"/>
<AddressVerificationFieldMessage formState={ formState } control={ control }/>
</div>
<RadioGroup onChange={ handleSignMethodChange } value={ signMethod } display="flex" flexDir="column" rowGap={ 4 }>
<Radio value="wallet">Sign via Web3 wallet</Radio>
<Radio value="manually">Sign manually</Radio>
</RadioGroup>
{ signMethod === 'manually' && <AddressVerificationFieldSignature formState={ formState } control={ control }/> }
</Flex>
<Flex alignItems="center" mt={ 8 } columnGap={ 5 }>
{ button }
</Flex>
</form>
);
};
export default React.memo(AddressVerificationStepSignature);
import { Alert, Box, Button, chakra, Flex } from '@chakra-ui/react';
import React from 'react';
interface Props {
onShowListClick: () => void;
onAddTokenClick: () => void;
}
const AddressVerificationStepSuccess = ({ onAddTokenClick, onShowListClick }: Props) => {
return (
<Box>
<Alert status="success" flexWrap="wrap" whiteSpace="pre-wrap" wordBreak="break-word" mb={ 3 } display="inline-block">
<span>The address ownership for </span>
<chakra.span fontWeight={ 700 }>0xaba7161a7fb69c88e16ed9f455ce62b791ee4d03</chakra.span>
<span> is verified.</span>
</Alert>
<p>You may now submit the “Add token information” request</p>
<Flex alignItems="center" mt={ 8 } columnGap={ 5 } flexWrap="wrap" rowGap={ 5 }>
<Button size="lg" variant="outline" onClick={ onShowListClick }>
View my verified addresses
</Button>
<Button size="lg" onClick={ onAddTokenClick }>
Add token information
</Button>
</Flex>
</Box>
);
};
export default React.memo(AddressVerificationStepSuccess);
export interface AddressVerificationFormFirstStepFields {
address: string;
}
export interface AddressVerificationFormSecondStepFields {
signature: string;
message: string;
}
export interface RootFields {
root: string;
}
export interface AddressCheckStatusSuccess {
contractCreator?: string;
contractOwner?: string;
signingMessage: string;
}
export type AddressCheckResponseSuccess = {
status: 'SUCCESS';
result: AddressCheckStatusSuccess;
} |
{ status: 'IS_OWNER_ERROR' } |
{ status: 'OWNERSHIP_VERIFIED_ERROR' } |
{ status: 'SOURCE_CODE_NOT_VERIFIED_ERROR' } |
{ status: 'INVALID_ADDRESS_ERROR' };
export interface AddressVerificationResponseError {
code: number;
message: string;
}
export type AddressValidationResponseSuccess = {
status: 'SUCCESS';
result: {
verifiedAddress: {
chainId: string;
contractAddress: string;
userId: string;
verifiedDate: string;
};
};
} |
{
status: 'INVALID_SIGNER_ERROR';
invalidSigner: {
signer: string;
validAddresses: Array<string>;
};
} |
{ status: 'VALIDITY_EXPIRED_ERROR' } |
{ status: 'INVALID_SIGNATURE_ERROR' } |
{ status: 'UNKNOWN_STATUS' }
...@@ -38,8 +38,6 @@ const AddressPageContent = () => { ...@@ -38,8 +38,6 @@ const AddressPageContent = () => {
const appProps = useAppContext(); const appProps = useAppContext();
const hasGoBackLink = appProps.referrer && appProps.referrer.includes('/accounts');
const tabsScrollRef = React.useRef<HTMLDivElement>(null); const tabsScrollRef = React.useRef<HTMLDivElement>(null);
const hash = getQueryParamString(router.query.hash); const hash = getQueryParamString(router.query.hash);
...@@ -98,6 +96,19 @@ const AddressPageContent = () => { ...@@ -98,6 +96,19 @@ const AddressPageContent = () => {
const content = addressQuery.isError ? null : <RoutedTabs tabs={ tabs } tabListProps={{ mt: 8 }}/>; const content = addressQuery.isError ? null : <RoutedTabs tabs={ tabs } tabListProps={{ mt: 8 }}/>;
const backLink = React.useMemo(() => {
const hasGoBackLink = appProps.referrer && appProps.referrer.includes('/accounts');
if (!hasGoBackLink) {
return;
}
return {
label: 'Back to top accounts list',
url: appProps.referrer,
};
}, [ appProps.referrer ]);
return ( return (
<Page> <Page>
{ addressQuery.isLoading ? <Skeleton h={{ base: 12, lg: 6 }} mb={ 6 } w="100%" maxW="680px"/> : <TextAd mb={ 6 }/> } { addressQuery.isLoading ? <Skeleton h={{ base: 12, lg: 6 }} mb={ 6 } w="100%" maxW="680px"/> : <TextAd mb={ 6 }/> }
...@@ -107,8 +118,7 @@ const AddressPageContent = () => { ...@@ -107,8 +118,7 @@ const AddressPageContent = () => {
<PageTitle <PageTitle
text={ `${ addressQuery.data?.is_contract ? 'Contract' : 'Address' } details` } text={ `${ addressQuery.data?.is_contract ? 'Contract' : 'Address' } details` }
additionalsRight={ tagsNode } additionalsRight={ tagsNode }
backLinkUrl={ hasGoBackLink ? appProps.referrer : undefined } backLink={ backLink }
backLinkLabel="Back to top accounts list"
/> />
) } ) }
<AddressDetails addressQuery={ addressQuery } scrollRef={ tabsScrollRef }/> <AddressDetails addressQuery={ addressQuery } scrollRef={ tabsScrollRef }/>
......
...@@ -57,8 +57,19 @@ const BlockPageContent = () => { ...@@ -57,8 +57,19 @@ const BlockPageContent = () => {
const hasPagination = !isMobile && tab === 'txs' && blockTxsQuery.isPaginationVisible; const hasPagination = !isMobile && tab === 'txs' && blockTxsQuery.isPaginationVisible;
const backLink = React.useMemo(() => {
const hasGoBackLink = appProps.referrer && appProps.referrer.includes('/blocks'); const hasGoBackLink = appProps.referrer && appProps.referrer.includes('/blocks');
if (!hasGoBackLink) {
return;
}
return {
label: 'Back to blocks list',
url: appProps.referrer,
};
}, [ appProps.referrer ]);
return ( return (
<> <>
{ blockQuery.isLoading ? <Skeleton h={{ base: 12, lg: 6 }} mb={ 6 } w="100%" maxW="680px"/> : <TextAd mb={ 6 }/> } { blockQuery.isLoading ? <Skeleton h={{ base: 12, lg: 6 }} mb={ 6 } w="100%" maxW="680px"/> : <TextAd mb={ 6 }/> }
...@@ -67,8 +78,7 @@ const BlockPageContent = () => { ...@@ -67,8 +78,7 @@ const BlockPageContent = () => {
) : ( ) : (
<PageTitle <PageTitle
text={ `Block #${ blockQuery.data?.height }` } text={ `Block #${ blockQuery.data?.height }` }
backLinkUrl={ hasGoBackLink ? appProps.referrer : undefined } backLink={ backLink }
backLinkLabel="Back to blocks list"
/> />
) } ) }
<RoutedTabs <RoutedTabs
......
...@@ -20,7 +20,6 @@ import PageTitle from 'ui/shared/Page/PageTitle'; ...@@ -20,7 +20,6 @@ import PageTitle from 'ui/shared/Page/PageTitle';
const ContractVerification = () => { const ContractVerification = () => {
const appProps = useAppContext(); const appProps = useAppContext();
const hasGoBackLink = appProps.referrer && appProps.referrer.includes('/address');
const router = useRouter(); const router = useRouter();
const hash = getQueryParamString(router.query.hash); const hash = getQueryParamString(router.query.hash);
...@@ -83,12 +82,24 @@ const ContractVerification = () => { ...@@ -83,12 +82,24 @@ const ContractVerification = () => {
); );
})(); })();
const backLink = React.useMemo(() => {
const hasGoBackLink = appProps.referrer && appProps.referrer.includes('/address');
if (!hasGoBackLink) {
return;
}
return {
label: 'Back to contract',
url: appProps.referrer,
};
}, [ appProps.referrer ]);
return ( return (
<Page> <Page>
<PageTitle <PageTitle
text="New smart contract verification" text="New smart contract verification"
backLinkUrl={ hasGoBackLink ? appProps.referrer : undefined } backLink={ backLink }
backLinkLabel="Back to contract"
/> />
{ hash && ( { hash && (
<Address mb={ 12 }> <Address mb={ 12 }>
......
...@@ -50,7 +50,6 @@ const CsvExport = () => { ...@@ -50,7 +50,6 @@ const CsvExport = () => {
const addressHash = router.query.address?.toString() || ''; const addressHash = router.query.address?.toString() || '';
const exportType = router.query.type?.toString() || ''; const exportType = router.query.type?.toString() || '';
const hasGoBackLink = appProps.referrer && appProps.referrer.includes('/address');
const addressQuery = useApiQuery('address', { const addressQuery = useApiQuery('address', {
pathParams: { hash: addressHash }, pathParams: { hash: addressHash },
...@@ -59,6 +58,19 @@ const CsvExport = () => { ...@@ -59,6 +58,19 @@ const CsvExport = () => {
}, },
}); });
const backLink = React.useMemo(() => {
const hasGoBackLink = appProps.referrer && appProps.referrer.includes('/address');
if (!hasGoBackLink) {
return;
}
return {
label: 'Back to address',
url: appProps.referrer,
};
}, [ appProps.referrer ]);
if (!isCorrectExportType(exportType) || !addressHash || addressQuery.error?.status === 400) { if (!isCorrectExportType(exportType) || !addressHash || addressQuery.error?.status === 400) {
throw Error('Not found', { cause: { status: 404 } }); throw Error('Not found', { cause: { status: 404 } });
} }
...@@ -79,8 +91,7 @@ const CsvExport = () => { ...@@ -79,8 +91,7 @@ const CsvExport = () => {
<Page> <Page>
<PageTitle <PageTitle
text="Export data to CSV file" text="Export data to CSV file"
backLinkUrl={ hasGoBackLink ? appProps.referrer : undefined } backLink={ backLink }
backLinkLabel="Back to address"
/> />
<Flex mb={ 10 } whiteSpace="pre-wrap" flexWrap="wrap"> <Flex mb={ 10 } whiteSpace="pre-wrap" flexWrap="wrap">
<span>Export { EXPORT_TYPES[exportType].text } for address </span> <span>Export { EXPORT_TYPES[exportType].text } for address </span>
......
...@@ -17,14 +17,25 @@ const Sol2Uml = () => { ...@@ -17,14 +17,25 @@ const Sol2Uml = () => {
const appProps = useAppContext(); const appProps = useAppContext();
const addressHash = router.query.address?.toString() || ''; const addressHash = router.query.address?.toString() || '';
const backLink = React.useMemo(() => {
const hasGoBackLink = appProps.referrer && appProps.referrer.includes('/address'); const hasGoBackLink = appProps.referrer && appProps.referrer.includes('/address');
if (!hasGoBackLink) {
return;
}
return {
label: 'Back to address',
url: appProps.referrer,
};
}, [ appProps.referrer ]);
return ( return (
<Page> <Page>
<PageTitle <PageTitle
text="Solidity UML diagram" text="Solidity UML diagram"
backLinkUrl={ hasGoBackLink ? appProps.referrer : undefined } backLink={ backLink }
backLinkLabel="Back to address"
/> />
<Flex mb={ 10 }> <Flex mb={ 10 }>
<span>For contract</span> <span>For contract</span>
......
...@@ -41,8 +41,6 @@ const TokenPageContent = () => { ...@@ -41,8 +41,6 @@ const TokenPageContent = () => {
const appProps = useAppContext(); const appProps = useAppContext();
const hasGoBackLink = appProps.referrer && appProps.referrer.includes('/tokens');
const scrollRef = React.useRef<HTMLDivElement>(null); const scrollRef = React.useRef<HTMLDivElement>(null);
const hashString = router.query.hash?.toString(); const hashString = router.query.hash?.toString();
...@@ -194,6 +192,19 @@ const TokenPageContent = () => { ...@@ -194,6 +192,19 @@ const TokenPageContent = () => {
}; };
}, [ isMobile ]); }, [ isMobile ]);
const backLink = React.useMemo(() => {
const hasGoBackLink = appProps.referrer && appProps.referrer.includes('/tokens');
if (!hasGoBackLink) {
return;
}
return {
label: 'Back to tokens list',
url: appProps.referrer,
};
}, [ appProps.referrer ]);
return ( return (
<Page> <Page>
{ tokenQuery.isLoading ? ( { tokenQuery.isLoading ? (
...@@ -209,8 +220,7 @@ const TokenPageContent = () => { ...@@ -209,8 +220,7 @@ const TokenPageContent = () => {
<TextAd mb={ 6 }/> <TextAd mb={ 6 }/>
<PageTitle <PageTitle
text={ `${ tokenQuery.data?.name || 'Unnamed' }${ tokenSymbolText } token` } text={ `${ tokenQuery.data?.name || 'Unnamed' }${ tokenSymbolText } token` }
backLinkUrl={ hasGoBackLink ? appProps.referrer : undefined } backLink={ backLink }
backLinkLabel="Back to tokens list"
additionalsLeft={ ( additionalsLeft={ (
<TokenLogo hash={ tokenQuery.data?.address } name={ tokenQuery.data?.name } boxSize={ 6 }/> <TokenLogo hash={ tokenQuery.data?.address } name={ tokenQuery.data?.name } boxSize={ 6 }/>
) } ) }
......
...@@ -33,7 +33,6 @@ const TransactionPageContent = () => { ...@@ -33,7 +33,6 @@ const TransactionPageContent = () => {
const router = useRouter(); const router = useRouter();
const appProps = useAppContext(); const appProps = useAppContext();
const hasGoBackLink = appProps.referrer && appProps.referrer.includes('/txs');
const hash = getQueryParamString(router.query.hash); const hash = getQueryParamString(router.query.hash);
const { data } = useApiQuery('tx', { const { data } = useApiQuery('tx', {
...@@ -65,14 +64,26 @@ const TransactionPageContent = () => { ...@@ -65,14 +64,26 @@ const TransactionPageContent = () => {
</Flex> </Flex>
); );
const backLink = React.useMemo(() => {
const hasGoBackLink = appProps.referrer && appProps.referrer.includes('/txs');
if (!hasGoBackLink) {
return;
}
return {
label: 'Back to transactions list',
url: appProps.referrer,
};
}, [ appProps.referrer ]);
return ( return (
<Page> <Page>
<TextAd mb={ 6 }/> <TextAd mb={ 6 }/>
<PageTitle <PageTitle
text="Transaction details" text="Transaction details"
additionalsRight={ additionals } additionalsRight={ additionals }
backLinkUrl={ hasGoBackLink ? appProps.referrer : undefined } backLink={ backLink }
backLinkLabel="Back to transactions list"
/> />
<RoutedTabs tabs={ TABS }/> <RoutedTabs tabs={ TABS }/>
</Page> </Page>
......
import { OrderedList, ListItem, chakra, Button, useDisclosure, Show, Hide, Skeleton, Box } from '@chakra-ui/react';
import React from 'react';
import appConfig from 'configs/app/config';
import useApiQuery from 'lib/api/useApiQuery';
import useRedirectForInvalidAuthToken from 'lib/hooks/useRedirectForInvalidAuthToken';
import AddressVerificationModal from 'ui/addressVerification/AddressVerificationModal';
import AccountPageDescription from 'ui/shared/AccountPageDescription';
import DataListDisplay from 'ui/shared/DataListDisplay';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle';
import SkeletonListAccount from 'ui/shared/skeletons/SkeletonListAccount';
import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
import TokenInfoForm from 'ui/tokenInfo/TokenInfoForm';
import VerifiedAddressesListItem from 'ui/verifiedAddresses/VerifiedAddressesListItem';
import VerifiedAddressesTable from 'ui/verifiedAddresses/VerifiedAddressesTable';
const VerifiedAddresses = () => {
useRedirectForInvalidAuthToken();
const [ submissionId, setSubmissionId ] = React.useState<number>();
const modalProps = useDisclosure();
const { data, isLoading, isError } = useApiQuery('verified_addresses', {
pathParams: { chainId: appConfig.network.id },
});
const handleGoBack = React.useCallback(() => {
setSubmissionId(undefined);
}, []);
const handleItemAdd = React.useCallback(() => {
setSubmissionId(NaN);
}, []);
const handleItemEdit = React.useCallback(() => {}, []);
const addButton = (
<Box marginTop={ 8 }>
<Button size="lg" onClick={ modalProps.onOpen }>
Add address
</Button>
</Box>
);
const skeleton = (
<>
<Box display={{ base: 'block', lg: 'none' }}>
<SkeletonListAccount/>
<Skeleton height="44px" width="156px" marginTop={ 8 }/>
</Box>
<Box display={{ base: 'none', lg: 'block' }}>
<SkeletonTable columns={ [ '100%', '180px', '260px', '160px' ] }/>
<Skeleton height="44px" width="156px" marginTop={ 8 }/>
</Box>
</>
);
const backLink = React.useMemo(() => {
if (submissionId === undefined) {
return;
}
return {
label: 'Back to my verified addresses',
onClick: handleGoBack,
};
}, [ handleGoBack, submissionId ]);
if (submissionId !== undefined) {
return (
<Page>
<PageTitle text="Token info application form" backLink={ backLink }/>
<TokenInfoForm id={ submissionId }/>
</Page>
);
}
const content = data?.verifiedAddresses ? (
<>
<Show below="lg" key="content-mobile" ssr={ false }>
{ data.verifiedAddresses.map((item) => (
<VerifiedAddressesListItem
key={ item.contractAddress }
item={ item }
onAdd={ handleItemAdd }
onEdit={ handleItemEdit }
/>
)) }
</Show>
<Hide below="lg" key="content-desktop" ssr={ false }>
<VerifiedAddressesTable data={ data.verifiedAddresses } onItemEdit={ handleItemEdit } onItemAdd={ handleItemAdd }/>
</Hide>
</>
) : null;
return (
<Page>
<PageTitle text="My verified addresses"/>
<AccountPageDescription allowCut={ false }>
<span>
Verify ownership of a smart contract address to easily update information in Blockscout.
You will sign a single message to verify contract ownership.
Once verified, you can update token information, address name tags, and address labels from the
Blockscout console without needing to sign additional messages.
</span>
<chakra.p fontWeight={ 600 } mt={ 5 }>
Before starting, make sure that:
</chakra.p>
<OrderedList>
<ListItem>The source code for the smart contract is deployed on “Network Name”.</ListItem>
<ListItem>The source code is verified (if not yet verified, you can use this tool).</ListItem>
</OrderedList>
<chakra.div mt={ 5 }>
Once these steps are complete, click the Add address button below to get started.
</chakra.div>
</AccountPageDescription>
<DataListDisplay
isLoading={ isLoading }
isError={ isError }
items={ data?.verifiedAddresses }
content={ content }
emptyText=""
skeletonProps={{ customSkeleton: skeleton }}
/>
{ addButton }
<AddressVerificationModal isOpen={ modalProps.isOpen } onClose={ modalProps.onClose }/>
</Page>
);
};
export default VerifiedAddresses;
import { Box, Text, useColorModeValue } from '@chakra-ui/react'; import { Box, useColorModeValue } from '@chakra-ui/react';
import _debounce from 'lodash/debounce'; import _debounce from 'lodash/debounce';
import React, { useRef, useEffect, useState, useCallback } from 'react'; import React, { useRef, useEffect, useState, useCallback } from 'react';
const CUT_HEIGHT = 144; const CUT_HEIGHT = 144;
const AccountPageDescription = ({ children }: {children: React.ReactNode}) => { const AccountPageDescription = ({ children, allowCut = true }: { children: React.ReactNode; allowCut?: boolean }) => {
const ref = useRef<HTMLParagraphElement>(null); const ref = useRef<HTMLParagraphElement>(null);
const [ needCut, setNeedCut ] = useState(false); const [ needCut, setNeedCut ] = useState(false);
...@@ -20,6 +20,10 @@ const AccountPageDescription = ({ children }: {children: React.ReactNode}) => { ...@@ -20,6 +20,10 @@ const AccountPageDescription = ({ children }: {children: React.ReactNode}) => {
}, [ needCut ]); }, [ needCut ]);
useEffect(() => { useEffect(() => {
if (!allowCut) {
return;
}
calculateCut(); calculateCut();
const resizeHandler = _debounce(calculateCut, 300); const resizeHandler = _debounce(calculateCut, 300);
window.addEventListener('resize', resizeHandler); window.addEventListener('resize', resizeHandler);
...@@ -40,15 +44,14 @@ const AccountPageDescription = ({ children }: {children: React.ReactNode}) => { ...@@ -40,15 +44,14 @@ const AccountPageDescription = ({ children }: {children: React.ReactNode}) => {
return ( return (
<Box position="relative" marginBottom={{ base: 6, lg: 8 }}> <Box position="relative" marginBottom={{ base: 6, lg: 8 }}>
<Text <Box
ref={ ref } ref={ ref }
maxHeight={ needCut && !expanded ? `${ CUT_HEIGHT }px` : 'auto' } maxHeight={ needCut && !expanded ? `${ CUT_HEIGHT }px` : 'auto' }
overflow="hidden" overflow="hidden"
style={ needCut && !expanded ? { WebkitLineClamp: '6', WebkitBoxOrient: 'vertical', display: '-webkit-box' } : {} } style={ needCut && !expanded ? { WebkitLineClamp: '6', WebkitBoxOrient: 'vertical', display: '-webkit-box' } : {} }
> >
{ children } { children }
</Text> </Box>
{ needCut && !expanded && ( { needCut && !expanded && (
<Box <Box
position="absolute" position="absolute"
......
...@@ -9,7 +9,7 @@ import AddressLink from 'ui/shared/address/AddressLink'; ...@@ -9,7 +9,7 @@ import AddressLink from 'ui/shared/address/AddressLink';
import CopyToClipboard from 'ui/shared/CopyToClipboard'; import CopyToClipboard from 'ui/shared/CopyToClipboard';
interface Props { interface Props {
address: AddressParam; address: Pick<AddressParam, 'hash' | 'is_contract' | 'implementation_name'>;
subtitle?: string; subtitle?: string;
} }
......
...@@ -68,7 +68,7 @@ const DataListDisplay = (props: Props) => { ...@@ -68,7 +68,7 @@ const DataListDisplay = (props: Props) => {
} }
if (!props.items?.length) { if (!props.items?.length) {
return <Text as="span">{ props.emptyText }</Text>; return props.emptyText ? <Text as="span">{ props.emptyText }</Text> : null;
} }
return ( return (
......
import { Tooltip } from '@chakra-ui/react'; import { Tooltip } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import shortenString from 'lib/shortenString';
interface Props { interface Props {
hash: string; hash: string;
isTooltipDisabled?: boolean; isTooltipDisabled?: boolean;
...@@ -13,7 +15,7 @@ const HashStringShorten = ({ hash, isTooltipDisabled }: Props) => { ...@@ -13,7 +15,7 @@ const HashStringShorten = ({ hash, isTooltipDisabled }: Props) => {
return ( return (
<Tooltip label={ hash } isDisabled={ isTooltipDisabled }> <Tooltip label={ hash } isDisabled={ isTooltipDisabled }>
{ hash.slice(0, 4) + '...' + hash.slice(-4) } { shortenString(hash) }
</Tooltip> </Tooltip>
); );
}; };
......
...@@ -43,8 +43,7 @@ test('with text ad, back link and addons +@mobile +@dark-mode', async({ mount }) ...@@ -43,8 +43,7 @@ test('with text ad, back link and addons +@mobile +@dark-mode', async({ mount })
<PageTitle <PageTitle
text="Title" text="Title"
withTextAd withTextAd
backLinkLabel="Back" backLink={{ label: 'Back', url: 'back' }}
backLinkUrl="back"
// additionalsLeft={ left } // additionalsLeft={ left }
additionalsRight="Privet" additionalsRight="Privet"
/> />
...@@ -60,8 +59,7 @@ test('long title with text ad, back link and addons +@mobile', async({ mount }) ...@@ -60,8 +59,7 @@ test('long title with text ad, back link and addons +@mobile', async({ mount })
<PageTitle <PageTitle
text="This title is long, really long" text="This title is long, really long"
withTextAd withTextAd
backLinkLabel="Back" backLink={{ label: 'Back', url: 'back' }}
backLinkUrl="back"
additionalsRight="Privet, kak dela?" additionalsRight="Privet, kak dela?"
/> />
</TestApp>, </TestApp>,
......
import { Heading, Flex, Grid, Tooltip, Icon, chakra } from '@chakra-ui/react'; import { Heading, Flex, Grid, Tooltip, Icon, Link, chakra } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import eastArrowIcon from 'icons/arrows/east.svg'; import eastArrowIcon from 'icons/arrows/east.svg';
import TextAd from 'ui/shared/ad/TextAd'; import TextAd from 'ui/shared/ad/TextAd';
import LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/LinkInternal';
type BackLinkProp = { label: string; url: string } | { label: string; onClick: () => void };
type Props = { type Props = {
text: string; text: string;
additionalsLeft?: React.ReactNode; additionalsLeft?: React.ReactNode;
additionalsRight?: React.ReactNode; additionalsRight?: React.ReactNode;
withTextAd?: boolean; withTextAd?: boolean;
className?: string; className?: string;
backLinkLabel?: string; backLink?: BackLinkProp;
backLinkUrl?: string;
} }
const PageTitle = ({ text, additionalsLeft, additionalsRight, withTextAd, backLinkUrl, backLinkLabel, className }: Props) => { const PageTitle = ({ text, additionalsLeft, additionalsRight, withTextAd, backLink, className }: Props) => {
const title = ( const title = (
<Heading <Heading
as="h1" as="h1"
...@@ -27,6 +28,32 @@ const PageTitle = ({ text, additionalsLeft, additionalsRight, withTextAd, backLi ...@@ -27,6 +28,32 @@ const PageTitle = ({ text, additionalsLeft, additionalsRight, withTextAd, backLi
</Heading> </Heading>
); );
const backLinkComponent = (() => {
if (!backLink) {
return null;
}
const icon = <Icon as={ eastArrowIcon } boxSize={ 6 } transform="rotate(180deg)" margin="auto"/>;
if ('url' in backLink) {
return (
<Tooltip label={ backLink.label }>
<LinkInternal display="inline-flex" href={ backLink.url } h="40px">
{ icon }
</LinkInternal>
</Tooltip>
);
}
return (
<Tooltip label={ backLink.label }>
<Link display="inline-flex" onClick={ backLink.onClick } h="40px">
{ icon }
</Link>
</Tooltip>
);
})();
return ( return (
<Flex <Flex
columnGap={ 3 } columnGap={ 3 }
...@@ -39,16 +66,10 @@ const PageTitle = ({ text, additionalsLeft, additionalsRight, withTextAd, backLi ...@@ -39,16 +66,10 @@ const PageTitle = ({ text, additionalsLeft, additionalsRight, withTextAd, backLi
> >
<Flex flexWrap="wrap" columnGap={ 3 } alignItems="center" width={ withTextAd ? 'unset' : '100%' } flexShrink={ 0 }> <Flex flexWrap="wrap" columnGap={ 3 } alignItems="center" width={ withTextAd ? 'unset' : '100%' } flexShrink={ 0 }>
<Grid <Grid
templateColumns={ [ backLinkUrl && 'auto', additionalsLeft && 'auto', '1fr' ].filter(Boolean).join(' ') } templateColumns={ [ backLinkComponent && 'auto', additionalsLeft && 'auto', '1fr' ].filter(Boolean).join(' ') }
columnGap={ 3 } columnGap={ 3 }
> >
{ backLinkUrl && ( { backLinkComponent }
<Tooltip label={ backLinkLabel }>
<LinkInternal display="inline-flex" href={ backLinkUrl } h="40px">
<Icon as={ eastArrowIcon } boxSize={ 6 } transform="rotate(180deg)" margin="auto"/>
</LinkInternal>
</Tooltip>
) }
{ additionalsLeft !== undefined && ( { additionalsLeft !== undefined && (
<Flex h="40px" alignItems="center"> <Flex h="40px" alignItems="center">
{ additionalsLeft } { additionalsLeft }
......
import { useColorModeValue, useToken } from '@chakra-ui/react';
import {
EthereumClient,
modalConnectors,
walletConnectProvider,
} from '@web3modal/ethereum';
import { Web3Modal } from '@web3modal/react';
import React from 'react';
import type { Chain } from 'wagmi';
import { configureChains, createClient, WagmiConfig } from 'wagmi';
import appConfig from 'configs/app/config';
const { wagmiClient, ethereumClient } = (() => {
try {
const currentChain: Chain = {
id: Number(appConfig.network.id),
name: appConfig.network.name || '',
network: appConfig.network.name || '',
nativeCurrency: {
decimals: appConfig.network.currency.decimals,
name: appConfig.network.currency.name || '',
symbol: appConfig.network.currency.symbol || '',
},
rpcUrls: {
'default': {
http: [ appConfig.network.rpcUrl || '' ],
},
},
blockExplorers: {
'default': {
name: 'Blockscout',
url: appConfig.baseUrl,
},
},
};
const chains = [ currentChain ];
const { provider } = configureChains(chains, [
walletConnectProvider({ projectId: appConfig.walletConnect.projectId || '' }),
]);
const wagmiClient = createClient({
autoConnect: true,
connectors: modalConnectors({ appName: 'web3Modal', chains }),
provider,
});
const ethereumClient = new EthereumClient(wagmiClient, chains);
return { wagmiClient, ethereumClient };
} catch (error) {
return { wagmiClient: undefined, ethereumClient: undefined };
}
})();
interface Props {
children: React.ReactNode;
fallback?: () => JSX.Element;
}
const Web3ModalProvider = ({ children, fallback }: Props) => {
const modalZIndex = useToken<string>('zIndices', 'modal');
const web3ModalTheme = useColorModeValue('light', 'dark');
if (!wagmiClient || !ethereumClient) {
return fallback?.() || null;
}
return (
<WagmiConfig client={ wagmiClient }>
{ children }
<Web3Modal
projectId={ appConfig.walletConnect.projectId }
ethereumClient={ ethereumClient }
themeZIndex={ Number(modalZIndex) }
themeMode={ web3ModalTheme }
themeBackground="themeColor"
/>
</WagmiConfig>
);
};
export default Web3ModalProvider;
...@@ -32,7 +32,7 @@ test('no auth +@desktop-xl +@dark-mode-xl', async({ mount }) => { ...@@ -32,7 +32,7 @@ test('no auth +@desktop-xl +@dark-mode-xl', async({ mount }) => {
test.describe('auth', () => { test.describe('auth', () => {
const extendedTest = test.extend({ const extendedTest = test.extend({
context: ({ context }, use) => { context: async({ context }, use) => {
authFixture(context); authFixture(context);
use(context); use(context);
}, },
......
import { Box } from '@chakra-ui/react';
import React from 'react';
interface Props {
id: number;
}
const TokenInfoForm = ({ id }: Props) => {
return <Box>TokenInfoForm for { id }</Box>;
};
export default TokenInfoForm;
...@@ -32,8 +32,6 @@ const TokenInstanceContent = () => { ...@@ -32,8 +32,6 @@ const TokenInstanceContent = () => {
const id = router.query.id?.toString(); const id = router.query.id?.toString();
const tab = router.query.tab?.toString(); const tab = router.query.tab?.toString();
const hasGoBackLink = appProps.referrer && appProps.referrer.includes(`/token/${ hash }`) && !appProps.referrer.includes('instance');
const scrollRef = React.useRef<HTMLDivElement>(null); const scrollRef = React.useRef<HTMLDivElement>(null);
const tokenInstanceQuery = useApiQuery('token_instance', { const tokenInstanceQuery = useApiQuery('token_instance', {
...@@ -50,6 +48,19 @@ const TokenInstanceContent = () => { ...@@ -50,6 +48,19 @@ const TokenInstanceContent = () => {
}, },
}); });
const backLink = React.useMemo(() => {
const hasGoBackLink = appProps.referrer && appProps.referrer.includes(`/token/${ hash }`) && !appProps.referrer.includes('instance');
if (!hasGoBackLink) {
return;
}
return {
label: 'Back to token page',
url: appProps.referrer,
};
}, [ appProps.referrer, hash ]);
const tabs: Array<RoutedTab> = [ const tabs: Array<RoutedTab> = [
{ id: 'token_transfers', title: 'Token transfers', component: <TokenTransfer transfersQuery={ transfersQuery } tokenId={ id }/> }, { id: 'token_transfers', title: 'Token transfers', component: <TokenTransfer transfersQuery={ transfersQuery } tokenId={ id }/> },
// there is no api for this tab yet // there is no api for this tab yet
...@@ -104,8 +115,7 @@ const TokenInstanceContent = () => { ...@@ -104,8 +115,7 @@ const TokenInstanceContent = () => {
<TextAd mb={ 6 }/> <TextAd mb={ 6 }/>
<PageTitle <PageTitle
text={ `${ tokenInstanceQuery.data.token.name || 'Unnamed token' } #${ tokenInstanceQuery.data.id }` } text={ `${ tokenInstanceQuery.data.token.name || 'Unnamed token' } #${ tokenInstanceQuery.data.id }` }
backLinkUrl={ hasGoBackLink ? appProps.referrer : undefined } backLink={ backLink }
backLinkLabel="Back to token page"
additionalsLeft={ nftShieldIcon } additionalsLeft={ nftShieldIcon }
additionalsRight={ tokenTag } additionalsRight={ tokenTag }
/> />
......
import { Link } from '@chakra-ui/react';
import React from 'react';
import type { VerifiedAddress } from 'types/api/account';
import AddressSnippet from 'ui/shared/AddressSnippet';
import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
interface Props {
item: VerifiedAddress;
onAdd: (address: string) => void;
onEdit: (item: VerifiedAddress) => void;
}
const VerifiedAddressesListItem = ({ item, onAdd }: Props) => {
const handleAddClick = React.useCallback(() => {
onAdd(item.contractAddress);
}, [ item, onAdd ]);
return (
<ListItemMobileGrid.Container>
<ListItemMobileGrid.Label>Address</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value py="3px">
<AddressSnippet address={{ hash: item.contractAddress, is_contract: true, implementation_name: null }}/>
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label>Token Info</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<Link onClick={ handleAddClick }>Add details</Link>
</ListItemMobileGrid.Value>
</ListItemMobileGrid.Container>
);
};
export default React.memo(VerifiedAddressesListItem);
import { Table, Tbody, Th, Thead, Tr } from '@chakra-ui/react';
import React from 'react';
import type { VerifiedAddress } from 'types/api/account';
import VerifiedAddressesTableItem from './VerifiedAddressesTableItem';
interface Props {
data: Array<VerifiedAddress>;
onItemAdd: (address: string) => void;
onItemEdit: (item: VerifiedAddress) => void;
}
const VerifiedAddressesTable = ({ data, onItemEdit, onItemAdd }: Props) => {
return (
<Table variant="simple">
<Thead>
<Tr>
<Th>Address</Th>
<Th w="180px">Token info</Th>
<Th w="260px">Request status</Th>
<Th w="160px">Actions</Th>
</Tr>
</Thead>
<Tbody>
{ data.map((item) => (
<VerifiedAddressesTableItem
key={ item.contractAddress }
item={ item }
onAdd={ onItemAdd }
onEdit={ onItemEdit }
/>
)) }
</Tbody>
</Table>
);
};
export default React.memo(VerifiedAddressesTable);
import { Td, Tr, Link } from '@chakra-ui/react';
import React from 'react';
import type { VerifiedAddress } from 'types/api/account';
import AddressSnippet from 'ui/shared/AddressSnippet';
interface Props {
item: VerifiedAddress;
onAdd: (address: string) => void;
onEdit: (item: VerifiedAddress) => void;
}
const VerifiedAddressesTableItem = ({ item, onAdd }: Props) => {
const handleAddClick = React.useCallback(() => {
onAdd(item.contractAddress);
}, [ item, onAdd ]);
return (
<Tr>
<Td>
<AddressSnippet address={{ hash: item.contractAddress, is_contract: true, implementation_name: null }}/>
</Td>
<Td>
<Link onClick={ handleAddClick }>Add details</Link>
</Td>
<Td></Td>
<Td></Td>
</Tr>
);
};
export default React.memo(VerifiedAddressesTableItem);
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