Commit d07b3303 authored by tom's avatar tom

skeleton for contract tab

parent a6c5f0c4
import type { SmartContract } from 'types/api/contract';
export const CONTRACT_CODE_UNVERIFIED = {
creation_bytecode: '0x60806040526e',
deployed_bytecode: '0x608060405233',
is_self_destructed: false,
} as SmartContract;
export const CONTRACT_CODE_VERIFIED = {
abi: [],
additional_sources: [],
can_be_visualized_via_sol2uml: true,
compiler_settings: {
compilationTarget: {
'contracts/StubContract.sol': 'StubContract',
},
evmVersion: 'london',
libraries: {},
metadata: {
bytecodeHash: 'ipfs',
},
optimizer: {
enabled: false,
runs: 200,
},
remappings: [],
},
compiler_version: 'v0.8.7+commit.e28d00a7',
creation_bytecode: '0x6080604052348',
deployed_bytecode: '0x60806040',
evm_version: 'london',
external_libraries: [],
file_path: 'contracts/StubContract.sol',
is_verified: true,
name: 'StubContract',
optimization_enabled: false,
optimization_runs: 200,
source_code: 'source_code',
verified_at: '2023-02-21T14:39:16.906760Z',
} as unknown as SmartContract;
import { Flex, Skeleton, Button, Grid, GridItem, Text, Alert, Link, chakra, Box } from '@chakra-ui/react'; import { Flex, Skeleton, Button, Grid, GridItem, Alert, Link, chakra, Box } from '@chakra-ui/react';
import { useQueryClient } from '@tanstack/react-query';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
import useApiQuery from 'lib/api/useApiQuery'; import type { Address as AddressInfo } from 'types/api/address';
import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery';
import dayjs from 'lib/date/dayjs'; import dayjs from 'lib/date/dayjs';
import * as stubs from 'stubs/contract';
import Address from 'ui/shared/address/Address'; import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon'; import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink'; import AddressLink from 'ui/shared/address/AddressLink';
...@@ -18,19 +22,23 @@ type Props = { ...@@ -18,19 +22,23 @@ type Props = {
addressHash?: string; addressHash?: string;
} }
const InfoItem = chakra(({ label, value, className }: { label: string; value: string; className?: string }) => ( const InfoItem = chakra(({ label, value, className, isLoading }: { label: string; value: string; className?: string; isLoading: boolean }) => (
<GridItem display="flex" columnGap={ 6 } wordBreak="break-all" className={ className }> <GridItem display="flex" columnGap={ 6 } wordBreak="break-all" className={ className } alignItems="baseline">
<Text w="170px" flexShrink={ 0 } fontWeight={ 500 }>{ label }</Text> <Skeleton isLoaded={ !isLoading } w="170px" flexShrink={ 0 } fontWeight={ 500 }>{ label }</Skeleton>
<Text>{ value }</Text> <Skeleton isLoaded={ !isLoading }>{ value }</Skeleton>
</GridItem> </GridItem>
)); ));
const ContractCode = ({ addressHash }: Props) => { const ContractCode = ({ addressHash }: Props) => {
const { data, isLoading, isError } = useApiQuery('contract', { const queryClient = useQueryClient();
const addressInfo = queryClient.getQueryData<AddressInfo>(getResourceKey('address', { pathParams: { hash: addressHash } }));
const { data, isPlaceholderData, isError } = useApiQuery('contract', {
pathParams: { hash: addressHash }, pathParams: { hash: addressHash },
queryOptions: { queryOptions: {
enabled: Boolean(addressHash), enabled: Boolean(addressHash),
refetchOnMount: false, refetchOnMount: false,
placeholderData: addressInfo?.is_verified ? stubs.CONTRACT_CODE_VERIFIED : stubs.CONTRACT_CODE_UNVERIFIED,
}, },
}); });
...@@ -38,24 +46,7 @@ const ContractCode = ({ addressHash }: Props) => { ...@@ -38,24 +46,7 @@ const ContractCode = ({ addressHash }: Props) => {
return <DataFetchAlert/>; return <DataFetchAlert/>;
} }
if (isLoading) { const verificationButton = isPlaceholderData ? <Skeleton w="130px" h={ 8 } mr={ 3 } ml="auto" borderRadius="base"/> : (
return (
<>
<Flex justifyContent="space-between" mb={ 2 }>
<Skeleton w="180px" h={ 5 } borderRadius="full"/>
<Skeleton w={ 5 } h={ 5 }/>
</Flex>
<Skeleton w="100%" h="250px" borderRadius="md"/>
<Flex justifyContent="space-between" mb={ 2 } mt={ 6 }>
<Skeleton w="180px" h={ 5 } borderRadius="full"/>
<Skeleton w={ 5 } h={ 5 }/>
</Flex>
<Skeleton w="100%" h="400px" borderRadius="md"/>
</>
);
}
const verificationButton = (
<Button <Button
size="sm" size="sm"
ml="auto" ml="auto"
...@@ -68,8 +59,8 @@ const ContractCode = ({ addressHash }: Props) => { ...@@ -68,8 +59,8 @@ const ContractCode = ({ addressHash }: Props) => {
); );
const constructorArgs = (() => { const constructorArgs = (() => {
if (!data.decoded_constructor_args) { if (!data?.decoded_constructor_args) {
return data.constructor_args; return data?.constructor_args;
} }
const decoded = data.decoded_constructor_args const decoded = data.decoded_constructor_args
...@@ -95,7 +86,7 @@ const ContractCode = ({ addressHash }: Props) => { ...@@ -95,7 +86,7 @@ const ContractCode = ({ addressHash }: Props) => {
})(); })();
const externalLibraries = (() => { const externalLibraries = (() => {
if (!data.external_libraries || data.external_libraries.length === 0) { if (!data?.external_libraries || data?.external_libraries.length === 0) {
return null; return null;
} }
...@@ -110,19 +101,23 @@ const ContractCode = ({ addressHash }: Props) => { ...@@ -110,19 +101,23 @@ const ContractCode = ({ addressHash }: Props) => {
return ( return (
<> <>
<Flex flexDir="column" rowGap={ 2 } mb={ 6 } _empty={{ display: 'none' }}> <Flex flexDir="column" rowGap={ 2 } mb={ 6 } _empty={{ display: 'none' }}>
{ data.is_verified && <Alert status="success">Contract Source Code Verified (Exact Match)</Alert> } { data?.is_verified && (
{ data.is_verified_via_sourcify && ( <Skeleton isLoaded={ !isPlaceholderData }>
<Alert status="success">Contract Source Code Verified (Exact Match)</Alert>
</Skeleton>
) }
{ data?.is_verified_via_sourcify && (
<Alert status="warning" whiteSpace="pre-wrap" flexWrap="wrap"> <Alert status="warning" whiteSpace="pre-wrap" flexWrap="wrap">
<span>This contract has been { data.is_partially_verified ? 'partially ' : '' }verified via Sourcify. </span> <span>This contract has been { data.is_partially_verified ? 'partially ' : '' }verified via Sourcify. </span>
{ data.sourcify_repo_url && <LinkExternal href={ data.sourcify_repo_url } fontSize="md">View contract in Sourcify repository</LinkExternal> } { data.sourcify_repo_url && <LinkExternal href={ data.sourcify_repo_url } fontSize="md">View contract in Sourcify repository</LinkExternal> }
</Alert> </Alert>
) } ) }
{ data.is_changed_bytecode && ( { data?.is_changed_bytecode && (
<Alert status="warning"> <Alert status="warning">
Warning! Contract bytecode has been changed and does not match the verified one. Therefore, interaction with this smart contract may be risky. Warning! Contract bytecode has been changed and does not match the verified one. Therefore, interaction with this smart contract may be risky.
</Alert> </Alert>
) } ) }
{ !data.is_verified && data.verified_twin_address_hash && !data.minimal_proxy_address_hash && ( { !data?.is_verified && data?.verified_twin_address_hash && !data?.minimal_proxy_address_hash && (
<Alert status="warning" whiteSpace="pre-wrap" flexWrap="wrap"> <Alert status="warning" whiteSpace="pre-wrap" flexWrap="wrap">
<span>Contract is not verified. However, we found a verified contract with the same bytecode in Blockscout DB </span> <span>Contract is not verified. However, we found a verified contract with the same bytecode in Blockscout DB </span>
<Address> <Address>
...@@ -136,7 +131,7 @@ const ContractCode = ({ addressHash }: Props) => { ...@@ -136,7 +131,7 @@ const ContractCode = ({ addressHash }: Props) => {
<span> page</span> <span> page</span>
</Alert> </Alert>
) } ) }
{ data.minimal_proxy_address_hash && ( { data?.minimal_proxy_address_hash && (
<Alert status="warning" flexWrap="wrap" whiteSpace="pre-wrap"> <Alert status="warning" flexWrap="wrap" whiteSpace="pre-wrap">
<span>Minimal Proxy Contract for </span> <span>Minimal Proxy Contract for </span>
<Address> <Address>
...@@ -151,14 +146,16 @@ const ContractCode = ({ addressHash }: Props) => { ...@@ -151,14 +146,16 @@ const ContractCode = ({ addressHash }: Props) => {
</Alert> </Alert>
) } ) }
</Flex> </Flex>
{ data.is_verified && ( { data?.is_verified && (
<Grid templateColumns={{ base: '1fr', lg: '1fr 1fr' }} rowGap={ 4 } columnGap={ 6 } mb={ 8 }> <Grid templateColumns={{ base: '1fr', lg: '1fr 1fr' }} rowGap={ 4 } columnGap={ 6 } mb={ 8 }>
{ data.name && <InfoItem label="Contract name" value={ data.name }/> } { data.name && <InfoItem label="Contract name" value={ data.name } isLoading={ isPlaceholderData }/> }
{ data.compiler_version && <InfoItem label="Compiler version" value={ data.compiler_version }/> } { data.compiler_version && <InfoItem label="Compiler version" value={ data.compiler_version } isLoading={ isPlaceholderData }/> }
{ data.evm_version && <InfoItem label="EVM version" value={ data.evm_version } textTransform="capitalize"/> } { data.evm_version && <InfoItem label="EVM version" value={ data.evm_version } textTransform="capitalize" isLoading={ isPlaceholderData }/> }
{ typeof data.optimization_enabled === 'boolean' && <InfoItem label="Optimization enabled" value={ data.optimization_enabled ? 'true' : 'false' }/> } { typeof data.optimization_enabled === 'boolean' &&
{ data.optimization_runs && <InfoItem label="Optimization runs" value={ String(data.optimization_runs) }/> } <InfoItem label="Optimization enabled" value={ data.optimization_enabled ? 'true' : 'false' } isLoading={ isPlaceholderData }/> }
{ data.verified_at && <InfoItem label="Verified at" value={ dayjs(data.verified_at).format('LLLL') } wordBreak="break-word"/> } { data.optimization_runs && <InfoItem label="Optimization runs" value={ String(data.optimization_runs) } isLoading={ isPlaceholderData }/> }
{ data.verified_at &&
<InfoItem label="Verified at" value={ dayjs(data.verified_at).format('LLLL') } wordBreak="break-word" isLoading={ isPlaceholderData }/> }
</Grid> </Grid>
) } ) }
<Flex flexDir="column" rowGap={ 6 }> <Flex flexDir="column" rowGap={ 6 }>
...@@ -167,9 +164,10 @@ const ContractCode = ({ addressHash }: Props) => { ...@@ -167,9 +164,10 @@ const ContractCode = ({ addressHash }: Props) => {
data={ constructorArgs } data={ constructorArgs }
title="Constructor Arguments" title="Constructor Arguments"
textareaMaxHeight="200px" textareaMaxHeight="200px"
isLoading={ isPlaceholderData }
/> />
) } ) }
{ data.source_code && ( { data?.source_code && (
<ContractSourceCode <ContractSourceCode
data={ data.source_code } data={ data.source_code }
hasSol2Yml={ Boolean(data.can_be_visualized_via_sol2uml) } hasSol2Yml={ Boolean(data.can_be_visualized_via_sol2uml) }
...@@ -177,23 +175,26 @@ const ContractCode = ({ addressHash }: Props) => { ...@@ -177,23 +175,26 @@ const ContractCode = ({ addressHash }: Props) => {
isViper={ Boolean(data.is_vyper_contract) } isViper={ Boolean(data.is_vyper_contract) }
filePath={ data.file_path } filePath={ data.file_path }
additionalSource={ data.additional_sources } additionalSource={ data.additional_sources }
isLoading={ isPlaceholderData }
/> />
) } ) }
{ Boolean(data.compiler_settings) && ( { data?.compiler_settings ? (
<RawDataSnippet <RawDataSnippet
data={ JSON.stringify(data.compiler_settings) } data={ JSON.stringify(data.compiler_settings) }
title="Compiler Settings" title="Compiler Settings"
textareaMaxHeight="200px" textareaMaxHeight="200px"
isLoading={ isPlaceholderData }
/> />
) } ) : null }
{ data.abi && ( { data?.abi && (
<RawDataSnippet <RawDataSnippet
data={ JSON.stringify(data.abi) } data={ JSON.stringify(data.abi) }
title="Contract ABI" title="Contract ABI"
textareaMaxHeight="200px" textareaMaxHeight="200px"
isLoading={ isPlaceholderData }
/> />
) } ) }
{ data.creation_bytecode && ( { data?.creation_bytecode && (
<RawDataSnippet <RawDataSnippet
data={ data.creation_bytecode } data={ data.creation_bytecode }
title="Contract creation code" title="Contract creation code"
...@@ -205,13 +206,15 @@ const ContractCode = ({ addressHash }: Props) => { ...@@ -205,13 +206,15 @@ const ContractCode = ({ addressHash }: Props) => {
</Alert> </Alert>
) : null } ) : null }
textareaMaxHeight="200px" textareaMaxHeight="200px"
isLoading={ isPlaceholderData }
/> />
) } ) }
{ data.deployed_bytecode && ( { data?.deployed_bytecode && (
<RawDataSnippet <RawDataSnippet
data={ data.deployed_bytecode } data={ data.deployed_bytecode }
title="Deployed ByteCode" title="Deployed ByteCode"
textareaMaxHeight="200px" textareaMaxHeight="200px"
isLoading={ isPlaceholderData }
/> />
) } ) }
{ externalLibraries && ( { externalLibraries && (
...@@ -219,6 +222,7 @@ const ContractCode = ({ addressHash }: Props) => { ...@@ -219,6 +222,7 @@ const ContractCode = ({ addressHash }: Props) => {
data={ externalLibraries } data={ externalLibraries }
title="External Libraries" title="External Libraries"
textareaMaxHeight="200px" textareaMaxHeight="200px"
isLoading={ isPlaceholderData }
/> />
) } ) }
</Flex> </Flex>
......
import { Flex, Text, Tooltip } from '@chakra-ui/react'; import { Flex, Skeleton, Text, Tooltip } from '@chakra-ui/react';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
...@@ -16,14 +16,15 @@ interface Props { ...@@ -16,14 +16,15 @@ interface Props {
isViper: boolean; isViper: boolean;
filePath?: string; filePath?: string;
additionalSource?: SmartContract['additional_sources']; additionalSource?: SmartContract['additional_sources'];
isLoading?: boolean;
} }
const ContractSourceCode = ({ data, hasSol2Yml, address, isViper, filePath, additionalSource }: Props) => { const ContractSourceCode = ({ data, hasSol2Yml, address, isViper, filePath, additionalSource, isLoading }: Props) => {
const heading = ( const heading = (
<Text fontWeight={ 500 }> <Skeleton isLoaded={ !isLoading } fontWeight={ 500 }>
<span>Contract source code</span> <span>Contract source code</span>
<Text whiteSpace="pre" as="span" variant="secondary"> ({ isViper ? 'Vyper' : 'Solidity' })</Text> <Text whiteSpace="pre" as="span" variant="secondary"> ({ isViper ? 'Vyper' : 'Solidity' })</Text>
</Text> </Skeleton>
); );
const diagramLink = hasSol2Yml && address ? ( const diagramLink = hasSol2Yml && address ? (
...@@ -31,9 +32,10 @@ const ContractSourceCode = ({ data, hasSol2Yml, address, isViper, filePath, addi ...@@ -31,9 +32,10 @@ const ContractSourceCode = ({ data, hasSol2Yml, address, isViper, filePath, addi
<LinkInternal <LinkInternal
href={ route({ pathname: '/visualize/sol2uml', query: { address } }) } href={ route({ pathname: '/visualize/sol2uml', query: { address } }) }
ml="auto" ml="auto"
mr={ 3 }
> >
View UML diagram <Skeleton isLoaded={ !isLoading }>
View UML diagram
</Skeleton>
</LinkInternal> </LinkInternal>
</Tooltip> </Tooltip>
) : null; ) : null;
...@@ -46,7 +48,7 @@ const ContractSourceCode = ({ data, hasSol2Yml, address, isViper, filePath, addi ...@@ -46,7 +48,7 @@ const ContractSourceCode = ({ data, hasSol2Yml, address, isViper, filePath, addi
}, [ additionalSource, data, filePath, isViper ]); }, [ additionalSource, data, filePath, isViper ]);
const copyToClipboard = editorData.length === 1 ? const copyToClipboard = editorData.length === 1 ?
<CopyToClipboard text={ editorData[0].source_code }/> : <CopyToClipboard text={ editorData[0].source_code } isLoading={ isLoading } ml={ 3 }/> :
null; null;
return ( return (
...@@ -56,7 +58,7 @@ const ContractSourceCode = ({ data, hasSol2Yml, address, isViper, filePath, addi ...@@ -56,7 +58,7 @@ const ContractSourceCode = ({ data, hasSol2Yml, address, isViper, filePath, addi
{ diagramLink } { diagramLink }
{ copyToClipboard } { copyToClipboard }
</Flex> </Flex>
<CodeEditor data={ editorData }/> { isLoading ? <Skeleton h="557px" w="100%"/> : <CodeEditor data={ editorData }/> }
</section> </section>
); );
}; };
......
import { Box, Flex, Text, chakra, useColorModeValue } from '@chakra-ui/react'; import { Box, Flex, chakra, useColorModeValue, Skeleton } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import CopyToClipboard from './CopyToClipboard'; import CopyToClipboard from './CopyToClipboard';
...@@ -11,33 +11,36 @@ interface Props { ...@@ -11,33 +11,36 @@ interface Props {
beforeSlot?: React.ReactNode; beforeSlot?: React.ReactNode;
textareaMaxHeight?: string; textareaMaxHeight?: string;
showCopy?: boolean; showCopy?: boolean;
isLoading?: boolean;
} }
const RawDataSnippet = ({ data, className, title, rightSlot, beforeSlot, textareaMaxHeight, showCopy = true }: Props) => { const RawDataSnippet = ({ data, className, title, rightSlot, beforeSlot, textareaMaxHeight, showCopy = true, isLoading }: Props) => {
// see issue in theme/components/Textarea.ts // see issue in theme/components/Textarea.ts
const bgColor = useColorModeValue('#f5f5f6', '#1a1b1b'); const bgColor = useColorModeValue('#f5f5f6', '#1a1b1b');
return ( return (
<Box className={ className } as="section" title={ title }> <Box className={ className } as="section" title={ title }>
{ (title || rightSlot || showCopy) && ( { (title || rightSlot || showCopy) && (
<Flex justifyContent={ title ? 'space-between' : 'flex-end' } alignItems="center" mb={ 3 }> <Flex justifyContent={ title ? 'space-between' : 'flex-end' } alignItems="center" mb={ 3 }>
{ title && <Text fontWeight={ 500 }>{ title }</Text> } { title && <Skeleton fontWeight={ 500 } isLoaded={ !isLoading }>{ title }</Skeleton> }
{ rightSlot } { rightSlot }
{ typeof data === 'string' && showCopy && <CopyToClipboard text={ data }/> } { typeof data === 'string' && showCopy && <CopyToClipboard text={ data } isLoading={ isLoading }/> }
</Flex> </Flex>
) } ) }
{ beforeSlot } { beforeSlot }
<Box <Skeleton
p={ 4 } p={ 4 }
bgColor={ bgColor } bgColor={ bgColor }
maxH={ textareaMaxHeight || '400px' } maxH={ textareaMaxHeight || '400px' }
minH={ isLoading ? '200px' : undefined }
fontSize="sm" fontSize="sm"
borderRadius="md" borderRadius="md"
wordBreak="break-all" wordBreak="break-all"
whiteSpace="pre-wrap" whiteSpace="pre-wrap"
overflowY="auto" overflowY="auto"
isLoaded={ !isLoading }
> >
{ data } { data }
</Box> </Skeleton>
</Box> </Box>
); );
}; };
......
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