Commit afd1ae6a authored by Igor Stuev's avatar Igor Stuev Committed by GitHub

Merge pull request #598 from blockscout/contract-tabs-token

token contract tabs
parents c9cfe3cc 508f38b8
import React from 'react';
import type { Address } from 'types/api/address';
import notEmpty from 'lib/notEmpty';
import ContractCode from 'ui/address/contract/ContractCode';
import ContractRead from 'ui/address/contract/ContractRead';
import ContractWrite from 'ui/address/contract/ContractWrite';
export default function useContractTabs(data: Address | undefined) {
return React.useMemo(() => {
return [
{ id: 'contact_code', title: 'Code', component: <ContractCode addressHash={ data?.hash }/> },
// this is not implemented in api yet
// data?.has_decompiled_code ?
// { id: 'contact_decompiled_code', title: 'Decompiled code', component: <div>Decompiled code</div> } :
// undefined,
data?.has_methods_read ?
{ id: 'read_contract', title: 'Read contract', component: <ContractRead addressHash={ data?.hash }/> } :
undefined,
data?.has_methods_read_proxy ?
{ id: 'read_proxy', title: 'Read proxy', component: <ContractRead addressHash={ data?.hash } isProxy/> } :
undefined,
data?.has_custom_methods_read ?
{ id: 'read_custom_methods', title: 'Read custom', component: <ContractRead addressHash={ data?.hash } isCustomAbi/> } :
undefined,
data?.has_methods_write ?
{ id: 'write_contract', title: 'Write contract', component: <ContractWrite addressHash={ data?.hash }/> } :
undefined,
data?.has_methods_write_proxy ?
{ id: 'write_proxy', title: 'Write proxy', component: <ContractWrite addressHash={ data?.hash } isProxy/> } :
undefined,
data?.has_custom_methods_write ?
{ id: 'write_custom_methods', title: 'Write custom', component: <ContractWrite addressHash={ data?.hash } isCustomAbi/> } :
undefined,
].filter(notEmpty);
}, [ data ]);
}
......@@ -17,6 +17,7 @@ import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
interface Props {
tabs: Array<RoutedSubTab>;
addressHash?: string;
}
export const currentChain: Chain = {
......@@ -58,12 +59,12 @@ const TAB_LIST_PROPS = {
columnGap: 3,
};
const AddressContract = ({ tabs }: Props) => {
const AddressContract = ({ addressHash, tabs }: Props) => {
const modalZIndex = useToken<string>('zIndices', 'modal');
return (
<WagmiConfig client={ wagmiClient }>
<ContractContextProvider>
<ContractContextProvider addressHash={ addressHash }>
<RoutedTabs tabs={ tabs } variant="outline" colorScheme="gray" size="sm" tabListProps={ TAB_LIST_PROPS }/>
</ContractContextProvider>
<Web3Modal
......
......@@ -23,7 +23,7 @@ test('verified with changed byte code +@mobile +@dark-mode', async({ mount, page
const component = await mount(
<TestApp>
<ContractCode/>
<ContractCode addressHash={ addressHash }/>
</TestApp>,
{ hooksConfig },
);
......@@ -39,7 +39,7 @@ test('verified with multiple sources +@mobile', async({ mount, page }) => {
await mount(
<TestApp>
<ContractCode/>
<ContractCode addressHash={ addressHash }/>
</TestApp>,
{ hooksConfig },
);
......@@ -57,7 +57,7 @@ test('verified via sourcify', async({ mount, page }) => {
await mount(
<TestApp>
<ContractCode/>
<ContractCode addressHash={ addressHash }/>
</TestApp>,
{ hooksConfig },
);
......@@ -73,7 +73,7 @@ test('self destructed', async({ mount, page }) => {
await mount(
<TestApp>
<ContractCode/>
<ContractCode addressHash={ addressHash }/>
</TestApp>,
{ hooksConfig },
);
......@@ -90,7 +90,7 @@ test('with twin address alert +@mobile', async({ mount, page }) => {
const component = await mount(
<TestApp>
<ContractCode/>
<ContractCode addressHash={ addressHash }/>
</TestApp>,
{ hooksConfig },
);
......@@ -106,7 +106,7 @@ test('with proxy address alert +@mobile', async({ mount, page }) => {
const component = await mount(
<TestApp>
<ContractCode/>
<ContractCode addressHash={ addressHash }/>
</TestApp>,
{ hooksConfig },
);
......@@ -122,7 +122,7 @@ test('non verified', async({ mount, page }) => {
const component = await mount(
<TestApp>
<ContractCode/>
<ContractCode addressHash={ addressHash }/>
</TestApp>,
{ hooksConfig },
);
......
import { Flex, Skeleton, Button, Grid, GridItem, Text, Alert, Link, chakra, Box } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import { route } from 'nextjs-routes';
import React from 'react';
import useApiQuery from 'lib/api/useApiQuery';
import dayjs from 'lib/date/dayjs';
import getQueryParamString from 'lib/router/getQueryParamString';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
......@@ -16,6 +14,10 @@ import RawDataSnippet from 'ui/shared/RawDataSnippet';
import ContractSourceCode from './ContractSourceCode';
type Props = {
addressHash?: string;
}
const InfoItem = chakra(({ label, value, className }: { label: string; value: string; className?: string }) => (
<GridItem display="flex" columnGap={ 6 } wordBreak="break-all" className={ className }>
<Text w="170px" flexShrink={ 0 } fontWeight={ 500 }>{ label }</Text>
......@@ -23,10 +25,7 @@ const InfoItem = chakra(({ label, value, className }: { label: string; value: st
</GridItem>
));
const ContractCode = () => {
const router = useRouter();
const addressHash = getQueryParamString(router.query.hash);
const ContractCode = ({ addressHash }: Props) => {
const { data, isLoading, isError } = useApiQuery('contract', {
pathParams: { hash: addressHash },
queryOptions: {
......@@ -62,7 +61,7 @@ const ContractCode = () => {
ml="auto"
mr={ 3 }
as="a"
href={ route({ pathname: '/address/[hash]/contract_verification', query: { hash: addressHash } }) }
href={ route({ pathname: '/address/[hash]/contract_verification', query: { hash: addressHash || '' } }) }
>
Verify & publish
</Button>
......@@ -131,7 +130,9 @@ const ContractCode = () => {
<AddressLink type="address" hash={ data.verified_twin_address_hash } truncation="constant" ml={ 2 }/>
</Address>
<chakra.span mt={ 1 }>All functions displayed below are from ABI of that contract. In order to verify current contract, proceed with </chakra.span>
<LinkInternal href={ route({ pathname: '/address/[hash]/contract_verification', query: { hash: addressHash } }) }>Verify & Publish</LinkInternal>
<LinkInternal href={ route({ pathname: '/address/[hash]/contract_verification', query: { hash: addressHash || '' } }) }>
Verify & Publish
</LinkInternal>
<span> page</span>
</Alert>
) }
......
......@@ -28,7 +28,7 @@ test('base view +@mobile +@dark-mode', async({ mount, page }) => {
const component = await mount(
<TestApp>
<ContractRead/>
<ContractRead addressHash={ addressHash }/>
</TestApp>,
{ hooksConfig },
);
......@@ -57,7 +57,7 @@ test('error result', async({ mount, page }) => {
const component = await mount(
<TestApp>
<ContractRead/>
<ContractRead addressHash={ addressHash }/>
</TestApp>,
{ hooksConfig },
);
......
import { Alert, Flex } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
import { useAccount } from 'wagmi';
......@@ -7,7 +6,6 @@ import type { SmartContractReadMethod, SmartContractQueryMethodRead } from 'type
import useApiFetch from 'lib/api/useApiFetch';
import useApiQuery from 'lib/api/useApiQuery';
import getQueryParamString from 'lib/router/getQueryParamString';
import ContractMethodsAccordion from 'ui/address/contract/ContractMethodsAccordion';
import ContentLoader from 'ui/shared/ContentLoader';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
......@@ -20,17 +18,15 @@ import ContractMethodConstant from './ContractMethodConstant';
import ContractReadResult from './ContractReadResult';
interface Props {
addressHash?: string;
isProxy?: boolean;
isCustomAbi?: boolean;
}
const ContractRead = ({ isProxy, isCustomAbi }: Props) => {
const router = useRouter();
const ContractRead = ({ addressHash, isProxy, isCustomAbi }: Props) => {
const apiFetch = useApiFetch();
const { address: userAddress } = useAccount();
const addressHash = getQueryParamString(router.query.hash);
const { data, isLoading, isError } = useApiQuery(isProxy ? 'contract_methods_read_proxy' : 'contract_methods_read', {
pathParams: { hash: addressHash },
queryParams: {
......
......@@ -23,7 +23,7 @@ test('base view +@mobile', async({ mount, page }) => {
const component = await mount(
<TestApp>
<ContractWrite/>
<ContractWrite addressHash={ addressHash }/>
</TestApp>,
{ hooksConfig },
);
......
import _capitalize from 'lodash/capitalize';
import { useRouter } from 'next/router';
import React from 'react';
import { useAccount, useSigner } from 'wagmi';
......@@ -7,7 +6,6 @@ import type { SmartContractWriteMethod } from 'types/api/contract';
import config from 'configs/app/config';
import useApiQuery from 'lib/api/useApiQuery';
import getQueryParamString from 'lib/router/getQueryParamString';
import ContractMethodsAccordion from 'ui/address/contract/ContractMethodsAccordion';
import ContentLoader from 'ui/shared/ContentLoader';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
......@@ -21,14 +19,12 @@ import ContractWriteResult from './ContractWriteResult';
import { getNativeCoinValue, isExtendedError } from './utils';
interface Props {
addressHash?: string;
isProxy?: boolean;
isCustomAbi?: boolean;
}
const ContractWrite = ({ isProxy, isCustomAbi }: Props) => {
const router = useRouter();
const addressHash = getQueryParamString(router.query.hash);
const ContractWrite = ({ addressHash, isProxy, isCustomAbi }: Props) => {
const { data: signer } = useSigner();
const { isConnected } = useAccount();
......
import { useQueryClient } from '@tanstack/react-query';
import type { Contract } from 'ethers';
import { useRouter } from 'next/router';
import React from 'react';
import { useContract, useProvider, useSigner } from 'wagmi';
......@@ -9,6 +8,7 @@ import type { Address } from 'types/api/address';
import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery';
type ProviderProps = {
addressHash?: string;
children: React.ReactNode;
}
......@@ -24,13 +24,11 @@ const ContractContext = React.createContext<TContractContext>({
custom: null,
});
export function ContractContextProvider({ children }: ProviderProps) {
const router = useRouter();
export function ContractContextProvider({ addressHash, children }: ProviderProps) {
const provider = useProvider();
const { data: signer } = useSigner();
const queryClient = useQueryClient();
const addressHash = router.query.hash?.toString();
const { data: contractInfo } = useApiQuery('contract', {
pathParams: { hash: addressHash },
queryOptions: {
......
......@@ -8,6 +8,7 @@ import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
import iconSuccess from 'icons/status/success.svg';
import useApiQuery from 'lib/api/useApiQuery';
import { useAppContext } from 'lib/appContext';
import useContractTabs from 'lib/hooks/useContractTabs';
import notEmpty from 'lib/notEmpty';
import getQueryParamString from 'lib/router/getQueryParamString';
import AddressBlocksValidated from 'ui/address/AddressBlocksValidated';
......@@ -19,9 +20,6 @@ import AddressLogs from 'ui/address/AddressLogs';
import AddressTokens from 'ui/address/AddressTokens';
import AddressTokenTransfers from 'ui/address/AddressTokenTransfers';
import AddressTxs from 'ui/address/AddressTxs';
import ContractCode from 'ui/address/contract/ContractCode';
import ContractRead from 'ui/address/contract/ContractRead';
import ContractWrite from 'ui/address/contract/ContractWrite';
import AdBanner from 'ui/shared/ad/AdBanner';
import TextAd from 'ui/shared/ad/TextAd';
import Page from 'ui/shared/Page/Page';
......@@ -58,33 +56,7 @@ const AddressPageContent = () => {
...(addressQuery.data?.watchlist_names || []),
].map((tag) => <Tag key={ tag.label }>{ tag.display_name }</Tag>);
const contractTabs = React.useMemo(() => {
return [
{ id: 'contact_code', title: 'Code', component: <ContractCode/> },
// this is not implemented in api yet
// addressQuery.data?.has_decompiled_code ?
// { id: 'contact_decompiled_code', title: 'Decompiled code', component: <div>Decompiled code</div> } :
// undefined,
addressQuery.data?.has_methods_read ?
{ id: 'read_contract', title: 'Read contract', component: <ContractRead/> } :
undefined,
addressQuery.data?.has_methods_read_proxy ?
{ id: 'read_proxy', title: 'Read proxy', component: <ContractRead isProxy/> } :
undefined,
addressQuery.data?.has_custom_methods_read ?
{ id: 'read_custom_methods', title: 'Read custom', component: <ContractRead isCustomAbi/> } :
undefined,
addressQuery.data?.has_methods_write ?
{ id: 'write_contract', title: 'Write contract', component: <ContractWrite/> } :
undefined,
addressQuery.data?.has_methods_write_proxy ?
{ id: 'write_proxy', title: 'Write proxy', component: <ContractWrite isProxy/> } :
undefined,
addressQuery.data?.has_custom_methods_write ?
{ id: 'write_custom_methods', title: 'Write custom', component: <ContractWrite isCustomAbi/> } :
undefined,
].filter(notEmpty);
}, [ addressQuery.data ]);
const contractTabs = useContractTabs(addressQuery.data);
const tabs: Array<RoutedTab> = React.useMemo(() => {
return [
......@@ -113,11 +85,11 @@ const AddressPageContent = () => {
return 'Contract';
},
component: <AddressContract tabs={ contractTabs }/>,
component: <AddressContract tabs={ contractTabs } addressHash={ hash }/>,
subTabs: contractTabs.map(tab => tab.id),
} : undefined,
].filter(notEmpty);
}, [ addressQuery.data, contractTabs ]);
}, [ addressQuery.data, contractTabs, hash ]);
const tagsNode = tags.length > 0 ? <Flex columnGap={ 2 }>{ tags }</Flex> : null;
......
import { Skeleton, Box, Flex, SkeletonCircle } from '@chakra-ui/react';
import { Skeleton, Box, Flex, SkeletonCircle, Icon } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React, { useEffect } from 'react';
import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
import iconSuccess from 'icons/status/success.svg';
import useApiQuery from 'lib/api/useApiQuery';
import { useAppContext } from 'lib/appContext';
import useContractTabs from 'lib/hooks/useContractTabs';
import useIsMobile from 'lib/hooks/useIsMobile';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import notEmpty from 'lib/notEmpty';
import trimTokenSymbol from 'lib/token/trimTokenSymbol';
import AddressContract from 'ui/address/AddressContract';
import AdBanner from 'ui/shared/ad/AdBanner';
import TextAd from 'ui/shared/ad/TextAd';
import Page from 'ui/shared/Page/Page';
......@@ -36,8 +40,10 @@ const TokenPageContent = () => {
const scrollRef = React.useRef<HTMLDivElement>(null);
const hashString = router.query.hash?.toString();
const tokenQuery = useApiQuery('token', {
pathParams: { hash: router.query.hash?.toString() },
pathParams: { hash: hashString },
queryOptions: { enabled: Boolean(router.query.hash) },
});
......@@ -58,7 +64,7 @@ const TokenPageContent = () => {
const transfersQuery = useQueryWithPages({
resourceName: 'token_transfers',
pathParams: { hash: router.query.hash?.toString() },
pathParams: { hash: hashString },
scrollRef,
options: {
enabled: Boolean(router.query.hash && (!router.query.tab || router.query.tab === 'token_transfers') && tokenQuery.data),
......@@ -67,7 +73,7 @@ const TokenPageContent = () => {
const holdersQuery = useQueryWithPages({
resourceName: 'token_holders',
pathParams: { hash: router.query.hash?.toString() },
pathParams: { hash: hashString },
scrollRef,
options: {
enabled: Boolean(router.query.hash && router.query.tab === 'holders' && tokenQuery.data),
......@@ -76,21 +82,44 @@ const TokenPageContent = () => {
const inventoryQuery = useQueryWithPages({
resourceName: 'token_inventory',
pathParams: { hash: router.query.hash?.toString() },
pathParams: { hash: hashString },
scrollRef,
options: {
enabled: Boolean(router.query.hash && router.query.tab === 'inventory' && tokenQuery.data),
},
});
const contractQuery = useApiQuery('address', {
pathParams: { hash: hashString },
queryOptions: { enabled: Boolean(router.query.hash) },
});
const contractTabs = useContractTabs(contractQuery.data);
const tabs: Array<RoutedTab> = [
{ id: 'token_transfers', title: 'Token transfers', component: <TokenTransfer transfersQuery={ transfersQuery }/> },
{ id: 'holders', title: 'Holders', component: <TokenHolders tokenQuery={ tokenQuery } holdersQuery={ holdersQuery }/> },
];
if (tokenQuery.data?.type === 'ERC-1155' || tokenQuery.data?.type === 'ERC-721') {
tabs.push({ id: 'inventory', title: 'Inventory', component: <TokenInventory inventoryQuery={ inventoryQuery }/> });
}
(tokenQuery.data?.type === 'ERC-1155' || tokenQuery.data?.type === 'ERC-721') ?
{ id: 'inventory', title: 'Inventory', component: <TokenInventory inventoryQuery={ inventoryQuery }/> } :
undefined,
contractQuery.data?.is_contract ? {
id: 'contract',
title: () => {
if (contractQuery.data.is_verified) {
return (
<>
<span>Contract</span>
<Icon as={ iconSuccess } boxSize="14px" color="green.500" ml={ 1 }/>
</>
);
}
return 'Contract';
},
component: <AddressContract tabs={ contractTabs } addressHash={ hashString }/>,
subTabs: contractTabs.map(tab => tab.id),
} : undefined,
].filter(notEmpty);
let hasPagination;
let pagination;
......@@ -138,7 +167,7 @@ const TokenPageContent = () => {
{ /* should stay before tabs to scroll up whith pagination */ }
<Box ref={ scrollRef }></Box>
{ tokenQuery.isLoading ? <SkeletonTabs/> : (
{ tokenQuery.isLoading || contractQuery.isLoading ? <SkeletonTabs/> : (
<RoutedTabs
tabs={ tabs }
tabListProps={ isMobile ? { mt: 8 } : { mt: 3, py: 5, marginBottom: 0 } }
......
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