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 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 * as stubs from 'stubs/contract';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
......@@ -18,19 +22,23 @@ 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>
<Text>{ value }</Text>
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 } alignItems="baseline">
<Skeleton isLoaded={ !isLoading } w="170px" flexShrink={ 0 } fontWeight={ 500 }>{ label }</Skeleton>
<Skeleton isLoaded={ !isLoading }>{ value }</Skeleton>
</GridItem>
));
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 },
queryOptions: {
enabled: Boolean(addressHash),
refetchOnMount: false,
placeholderData: addressInfo?.is_verified ? stubs.CONTRACT_CODE_VERIFIED : stubs.CONTRACT_CODE_UNVERIFIED,
},
});
......@@ -38,24 +46,7 @@ const ContractCode = ({ addressHash }: Props) => {
return <DataFetchAlert/>;
}
if (isLoading) {
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 = (
const verificationButton = isPlaceholderData ? <Skeleton w="130px" h={ 8 } mr={ 3 } ml="auto" borderRadius="base"/> : (
<Button
size="sm"
ml="auto"
......@@ -68,8 +59,8 @@ const ContractCode = ({ addressHash }: Props) => {
);
const constructorArgs = (() => {
if (!data.decoded_constructor_args) {
return data.constructor_args;
if (!data?.decoded_constructor_args) {
return data?.constructor_args;
}
const decoded = data.decoded_constructor_args
......@@ -95,7 +86,7 @@ const ContractCode = ({ addressHash }: Props) => {
})();
const externalLibraries = (() => {
if (!data.external_libraries || data.external_libraries.length === 0) {
if (!data?.external_libraries || data?.external_libraries.length === 0) {
return null;
}
......@@ -110,19 +101,23 @@ const ContractCode = ({ addressHash }: Props) => {
return (
<>
<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_via_sourcify && (
{ data?.is_verified && (
<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">
<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> }
</Alert>
) }
{ data.is_changed_bytecode && (
{ data?.is_changed_bytecode && (
<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.
</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">
<span>Contract is not verified. However, we found a verified contract with the same bytecode in Blockscout DB </span>
<Address>
......@@ -136,7 +131,7 @@ const ContractCode = ({ addressHash }: Props) => {
<span> page</span>
</Alert>
) }
{ data.minimal_proxy_address_hash && (
{ data?.minimal_proxy_address_hash && (
<Alert status="warning" flexWrap="wrap" whiteSpace="pre-wrap">
<span>Minimal Proxy Contract for </span>
<Address>
......@@ -151,14 +146,16 @@ const ContractCode = ({ addressHash }: Props) => {
</Alert>
) }
</Flex>
{ data.is_verified && (
{ data?.is_verified && (
<Grid templateColumns={{ base: '1fr', lg: '1fr 1fr' }} rowGap={ 4 } columnGap={ 6 } mb={ 8 }>
{ data.name && <InfoItem label="Contract name" value={ data.name }/> }
{ data.compiler_version && <InfoItem label="Compiler version" value={ data.compiler_version }/> }
{ data.evm_version && <InfoItem label="EVM version" value={ data.evm_version } textTransform="capitalize"/> }
{ typeof data.optimization_enabled === 'boolean' && <InfoItem label="Optimization enabled" value={ data.optimization_enabled ? 'true' : 'false' }/> }
{ data.optimization_runs && <InfoItem label="Optimization runs" value={ String(data.optimization_runs) }/> }
{ data.verified_at && <InfoItem label="Verified at" value={ dayjs(data.verified_at).format('LLLL') } wordBreak="break-word"/> }
{ data.name && <InfoItem label="Contract name" value={ data.name } isLoading={ isPlaceholderData }/> }
{ 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" isLoading={ isPlaceholderData }/> }
{ typeof data.optimization_enabled === 'boolean' &&
<InfoItem label="Optimization enabled" value={ data.optimization_enabled ? 'true' : 'false' } isLoading={ isPlaceholderData }/> }
{ 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>
) }
<Flex flexDir="column" rowGap={ 6 }>
......@@ -167,9 +164,10 @@ const ContractCode = ({ addressHash }: Props) => {
data={ constructorArgs }
title="Constructor Arguments"
textareaMaxHeight="200px"
isLoading={ isPlaceholderData }
/>
) }
{ data.source_code && (
{ data?.source_code && (
<ContractSourceCode
data={ data.source_code }
hasSol2Yml={ Boolean(data.can_be_visualized_via_sol2uml) }
......@@ -177,23 +175,26 @@ const ContractCode = ({ addressHash }: Props) => {
isViper={ Boolean(data.is_vyper_contract) }
filePath={ data.file_path }
additionalSource={ data.additional_sources }
isLoading={ isPlaceholderData }
/>
) }
{ Boolean(data.compiler_settings) && (
{ data?.compiler_settings ? (
<RawDataSnippet
data={ JSON.stringify(data.compiler_settings) }
title="Compiler Settings"
textareaMaxHeight="200px"
isLoading={ isPlaceholderData }
/>
) }
{ data.abi && (
) : null }
{ data?.abi && (
<RawDataSnippet
data={ JSON.stringify(data.abi) }
title="Contract ABI"
textareaMaxHeight="200px"
isLoading={ isPlaceholderData }
/>
) }
{ data.creation_bytecode && (
{ data?.creation_bytecode && (
<RawDataSnippet
data={ data.creation_bytecode }
title="Contract creation code"
......@@ -205,13 +206,15 @@ const ContractCode = ({ addressHash }: Props) => {
</Alert>
) : null }
textareaMaxHeight="200px"
isLoading={ isPlaceholderData }
/>
) }
{ data.deployed_bytecode && (
{ data?.deployed_bytecode && (
<RawDataSnippet
data={ data.deployed_bytecode }
title="Deployed ByteCode"
textareaMaxHeight="200px"
isLoading={ isPlaceholderData }
/>
) }
{ externalLibraries && (
......@@ -219,6 +222,7 @@ const ContractCode = ({ addressHash }: Props) => {
data={ externalLibraries }
title="External Libraries"
textareaMaxHeight="200px"
isLoading={ isPlaceholderData }
/>
) }
</Flex>
......
import { Flex, Text, Tooltip } from '@chakra-ui/react';
import { Flex, Skeleton, Text, Tooltip } from '@chakra-ui/react';
import { route } from 'nextjs-routes';
import React from 'react';
......@@ -16,14 +16,15 @@ interface Props {
isViper: boolean;
filePath?: string;
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 = (
<Text fontWeight={ 500 }>
<Skeleton isLoaded={ !isLoading } fontWeight={ 500 }>
<span>Contract source code</span>
<Text whiteSpace="pre" as="span" variant="secondary"> ({ isViper ? 'Vyper' : 'Solidity' })</Text>
</Text>
</Skeleton>
);
const diagramLink = hasSol2Yml && address ? (
......@@ -31,9 +32,10 @@ const ContractSourceCode = ({ data, hasSol2Yml, address, isViper, filePath, addi
<LinkInternal
href={ route({ pathname: '/visualize/sol2uml', query: { address } }) }
ml="auto"
mr={ 3 }
>
View UML diagram
<Skeleton isLoaded={ !isLoading }>
View UML diagram
</Skeleton>
</LinkInternal>
</Tooltip>
) : null;
......@@ -46,7 +48,7 @@ const ContractSourceCode = ({ data, hasSol2Yml, address, isViper, filePath, addi
}, [ additionalSource, data, filePath, isViper ]);
const copyToClipboard = editorData.length === 1 ?
<CopyToClipboard text={ editorData[0].source_code }/> :
<CopyToClipboard text={ editorData[0].source_code } isLoading={ isLoading } ml={ 3 }/> :
null;
return (
......@@ -56,7 +58,7 @@ const ContractSourceCode = ({ data, hasSol2Yml, address, isViper, filePath, addi
{ diagramLink }
{ copyToClipboard }
</Flex>
<CodeEditor data={ editorData }/>
{ isLoading ? <Skeleton h="557px" w="100%"/> : <CodeEditor data={ editorData }/> }
</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 CopyToClipboard from './CopyToClipboard';
......@@ -11,33 +11,36 @@ interface Props {
beforeSlot?: React.ReactNode;
textareaMaxHeight?: string;
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
const bgColor = useColorModeValue('#f5f5f6', '#1a1b1b');
return (
<Box className={ className } as="section" title={ title }>
{ (title || rightSlot || showCopy) && (
<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 }
{ typeof data === 'string' && showCopy && <CopyToClipboard text={ data }/> }
{ typeof data === 'string' && showCopy && <CopyToClipboard text={ data } isLoading={ isLoading }/> }
</Flex>
) }
{ beforeSlot }
<Box
<Skeleton
p={ 4 }
bgColor={ bgColor }
maxH={ textareaMaxHeight || '400px' }
minH={ isLoading ? '200px' : undefined }
fontSize="sm"
borderRadius="md"
wordBreak="break-all"
whiteSpace="pre-wrap"
overflowY="auto"
isLoaded={ !isLoading }
>
{ data }
</Box>
</Skeleton>
</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