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

Merge pull request #882 from blockscout/feat/contract-code-source-type

contract code: source type selector
parents d0e132e8 22990d6a
...@@ -2,7 +2,6 @@ import React from 'react'; ...@@ -2,7 +2,6 @@ import React from 'react';
import type { RoutedSubTab } from 'ui/shared/Tabs/types'; import type { RoutedSubTab } from 'ui/shared/Tabs/types';
import { ContractContextProvider } from 'ui/address/contract/context';
import RoutedTabs from 'ui/shared/Tabs/RoutedTabs'; import RoutedTabs from 'ui/shared/Tabs/RoutedTabs';
import Web3ModalProvider from 'ui/shared/Web3ModalProvider'; import Web3ModalProvider from 'ui/shared/Web3ModalProvider';
...@@ -15,17 +14,17 @@ const TAB_LIST_PROPS = { ...@@ -15,17 +14,17 @@ const TAB_LIST_PROPS = {
columnGap: 3, columnGap: 3,
}; };
const AddressContract = ({ addressHash, tabs }: Props) => { const AddressContract = ({ tabs }: Props) => {
const fallback = React.useCallback(() => { const fallback = React.useCallback(() => {
const noProviderTabs = tabs.filter(({ id }) => id === 'contact_code'); const noProviderTabs = tabs.filter(({ id }) => id === 'contact_code');
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 ]); }, [ tabs ]);
return ( return (
<Web3ModalProvider fallback={ fallback }> <Web3ModalProvider fallback={ fallback }>
<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>
</Web3ModalProvider> </Web3ModalProvider>
); );
}; };
......
...@@ -212,16 +212,10 @@ const ContractCode = ({ addressHash, noSocket }: Props) => { ...@@ -212,16 +212,10 @@ const ContractCode = ({ addressHash, noSocket }: Props) => {
isLoading={ isPlaceholderData } isLoading={ isPlaceholderData }
/> />
) } ) }
{ data?.source_code && ( { data?.is_verified && (
<ContractSourceCode <ContractSourceCode
data={ data.source_code }
hasSol2Yml={ Boolean(data.can_be_visualized_via_sol2uml) }
address={ addressHash } address={ addressHash }
isViper={ Boolean(data.is_vyper_contract) } implementationAddress={ addressInfo?.implementation_address ?? undefined }
filePath={ data.file_path }
additionalSource={ data.additional_sources }
remappings={ data.compiler_settings?.remappings }
isLoading={ isPlaceholderData }
/> />
) } ) }
{ data?.compiler_settings ? ( { data?.compiler_settings ? (
......
import { Flex, Skeleton, Text, Tooltip } from '@chakra-ui/react'; import { Box, Flex, Select, 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';
import type { SmartContract } from 'types/api/contract'; import type { SmartContract } from 'types/api/contract';
import type { ArrayElement } from 'types/utils';
import useApiQuery from 'lib/api/useApiQuery';
import * as stubs from 'stubs/contract';
import CopyToClipboard from 'ui/shared/CopyToClipboard'; import CopyToClipboard from 'ui/shared/CopyToClipboard';
import LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/LinkInternal';
import CodeEditor from 'ui/shared/monaco/CodeEditor'; import CodeEditor from 'ui/shared/monaco/CodeEditor';
import formatFilePath from 'ui/shared/monaco/utils/formatFilePath'; import formatFilePath from 'ui/shared/monaco/utils/formatFilePath';
const SOURCE_CODE_OPTIONS = [
{ id: 'primary', label: 'Proxy' } as const,
{ id: 'secondary', label: 'Implementation' } as const,
];
type SourceCodeType = ArrayElement<typeof SOURCE_CODE_OPTIONS>['id'];
function getEditorData(contractInfo: SmartContract | undefined) {
if (!contractInfo || !contractInfo.source_code) {
return undefined;
}
const defaultName = contractInfo.is_vyper_contract ? '/index.vy' : '/index.sol';
return [
{ file_path: formatFilePath(contractInfo.file_path || defaultName), source_code: contractInfo.source_code },
...(contractInfo.additional_sources || []).map((source) => ({ ...source, file_path: formatFilePath(source.file_path) })),
];
}
interface Props { interface Props {
data: string;
hasSol2Yml: boolean;
address?: string; address?: string;
isViper: boolean; implementationAddress?: string;
filePath?: string;
additionalSource?: SmartContract['additional_sources'];
remappings?: Array<string>;
isLoading?: boolean;
} }
const ContractSourceCode = ({ data, hasSol2Yml, address, isViper, filePath, additionalSource, remappings, isLoading }: Props) => { const ContractSourceCode = ({ address, implementationAddress }: Props) => {
const [ sourceType, setSourceType ] = React.useState<SourceCodeType>('primary');
const primaryContractQuery = useApiQuery('contract', {
pathParams: { hash: address },
queryOptions: {
enabled: Boolean(address),
refetchOnMount: false,
placeholderData: stubs.CONTRACT_CODE_VERIFIED,
},
});
const secondaryContractQuery = useApiQuery('contract', {
pathParams: { hash: implementationAddress },
queryOptions: {
enabled: Boolean(implementationAddress),
refetchOnMount: false,
placeholderData: stubs.CONTRACT_CODE_VERIFIED,
},
});
const isLoading = implementationAddress ?
primaryContractQuery.isPlaceholderData || secondaryContractQuery.isPlaceholderData :
primaryContractQuery.isPlaceholderData;
const primaryEditorData = React.useMemo(() => {
return getEditorData(primaryContractQuery.data);
}, [ primaryContractQuery.data ]);
const secondaryEditorData = React.useMemo(() => {
return getEditorData(secondaryContractQuery.data);
}, [ secondaryContractQuery.data ]);
const activeContract = sourceType === 'secondary' ? secondaryContractQuery.data : primaryContractQuery.data;
const activeContractData = sourceType === 'secondary' ? secondaryEditorData : primaryEditorData;
const heading = ( const heading = (
<Skeleton isLoaded={ !isLoading } 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"> ({ activeContract?.is_vyper_contract ? 'Vyper' : 'Solidity' })</Text>
</Skeleton> </Skeleton>
); );
const diagramLink = hasSol2Yml && address ? ( const diagramLinkAddress = (() => {
if (!activeContract?.can_be_visualized_via_sol2uml) {
return;
}
return sourceType === 'secondary' ? implementationAddress : address;
})();
const diagramLink = diagramLinkAddress ? (
<Tooltip label="Visualize contract code using Sol2Uml JS library"> <Tooltip label="Visualize contract code using Sol2Uml JS library">
<LinkInternal <LinkInternal
href={ route({ pathname: '/visualize/sol2uml', query: { address } }) } href={ route({ pathname: '/visualize/sol2uml', query: { address: diagramLinkAddress } }) }
ml="auto" ml="auto"
> >
<Skeleton isLoaded={ !isLoading }> <Skeleton isLoaded={ !isLoading }>
...@@ -39,27 +96,66 @@ const ContractSourceCode = ({ data, hasSol2Yml, address, isViper, filePath, addi ...@@ -39,27 +96,66 @@ const ContractSourceCode = ({ data, hasSol2Yml, address, isViper, filePath, addi
</Skeleton> </Skeleton>
</LinkInternal> </LinkInternal>
</Tooltip> </Tooltip>
) : <Box ml="auto"/>;
const copyToClipboard = activeContractData?.length === 1 ?
<CopyToClipboard text={ activeContractData[0].source_code } isLoading={ isLoading } ml={ 3 }/> :
null;
const handleSelectChange = React.useCallback((event: React.ChangeEvent<HTMLSelectElement>) => {
setSourceType(event.target.value as SourceCodeType);
}, []);
const editorSourceTypeSelector = !secondaryContractQuery.isPlaceholderData && secondaryContractQuery.data?.source_code ? (
<Select
size="xs"
value={ sourceType }
onChange={ handleSelectChange }
focusBorderColor="none"
w="auto"
ml={ 3 }
borderRadius="base"
>
{ SOURCE_CODE_OPTIONS.map((option) => <option key={ option.id } value={ option.id }>{ option.label }</option>) }
</Select>
) : null; ) : null;
const editorData = React.useMemo(() => { const content = (() => {
const defaultName = isViper ? '/index.vy' : '/index.sol'; if (isLoading) {
return [ return <Skeleton h="557px" w="100%"/>;
{ file_path: formatFilePath(filePath || defaultName), source_code: data }, }
...(additionalSource || []).map((source) => ({ ...source, file_path: formatFilePath(source.file_path) })) ];
}, [ additionalSource, data, filePath, isViper ]);
const copyToClipboard = editorData.length === 1 ? if (!primaryEditorData) {
<CopyToClipboard text={ editorData[0].source_code } isLoading={ isLoading } ml={ 3 }/> : return null;
null; }
return (
<>
<Box display={ sourceType === 'primary' ? 'block' : 'none' }>
<CodeEditor data={ primaryEditorData } remappings={ primaryContractQuery.data?.compiler_settings?.remappings }/>
</Box>
{ secondaryEditorData && (
<Box display={ sourceType === 'secondary' ? 'block' : 'none' }>
<CodeEditor data={ secondaryEditorData } remappings={ secondaryContractQuery.data?.compiler_settings?.remappings }/>
</Box>
) }
</>
);
})();
if (!primaryEditorData) {
return null;
}
return ( return (
<section> <section>
<Flex justifyContent="space-between" alignItems="center" mb={ 3 }> <Flex justifyContent="space-between" alignItems="center" mb={ 3 }>
{ heading } { heading }
{ editorSourceTypeSelector }
{ diagramLink } { diagramLink }
{ copyToClipboard } { copyToClipboard }
</Flex> </Flex>
{ isLoading ? <Skeleton h="557px" w="100%"/> : <CodeEditor data={ editorData } remappings={ remappings }/> } { content }
</section> </section>
); );
}; };
......
...@@ -9,12 +9,12 @@ import ContractMethodsAccordion from 'ui/address/contract/ContractMethodsAccordi ...@@ -9,12 +9,12 @@ import ContractMethodsAccordion from 'ui/address/contract/ContractMethodsAccordi
import ContentLoader from 'ui/shared/ContentLoader'; import ContentLoader from 'ui/shared/ContentLoader';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
import { useContractContext } from './context';
import ContractConnectWallet from './ContractConnectWallet'; import ContractConnectWallet from './ContractConnectWallet';
import ContractCustomAbiAlert from './ContractCustomAbiAlert'; import ContractCustomAbiAlert from './ContractCustomAbiAlert';
import ContractImplementationAddress from './ContractImplementationAddress'; import ContractImplementationAddress from './ContractImplementationAddress';
import ContractMethodCallable from './ContractMethodCallable'; import ContractMethodCallable from './ContractMethodCallable';
import ContractWriteResult from './ContractWriteResult'; import ContractWriteResult from './ContractWriteResult';
import useContractAbi from './useContractAbi';
import { getNativeCoinValue } from './utils'; import { getNativeCoinValue } from './utils';
interface Props { interface Props {
...@@ -39,18 +39,7 @@ const ContractWrite = ({ addressHash, isProxy, isCustomAbi }: Props) => { ...@@ -39,18 +39,7 @@ const ContractWrite = ({ addressHash, isProxy, isCustomAbi }: Props) => {
}, },
}); });
const { contractInfo, customInfo, proxyInfo } = useContractContext(); const contractAbi = useContractAbi({ addressHash, isProxy, isCustomAbi });
const abi = (() => {
if (isProxy) {
return proxyInfo?.abi;
}
if (isCustomAbi) {
return customInfo?.abi;
}
return contractInfo?.abi;
})();
const handleMethodFormSubmit = React.useCallback(async(item: SmartContractWriteMethod, args: Array<string | Array<unknown>>) => { const handleMethodFormSubmit = React.useCallback(async(item: SmartContractWriteMethod, args: Array<string | Array<unknown>>) => {
if (!isConnected) { if (!isConnected) {
...@@ -61,7 +50,7 @@ const ContractWrite = ({ addressHash, isProxy, isCustomAbi }: Props) => { ...@@ -61,7 +50,7 @@ const ContractWrite = ({ addressHash, isProxy, isCustomAbi }: Props) => {
await switchNetworkAsync?.(Number(config.network.id)); await switchNetworkAsync?.(Number(config.network.id));
} }
if (!abi) { if (!contractAbi) {
throw new Error('Something went wrong. Try again later.'); throw new Error('Something went wrong. Try again later.');
} }
...@@ -84,14 +73,14 @@ const ContractWrite = ({ addressHash, isProxy, isCustomAbi }: Props) => { ...@@ -84,14 +73,14 @@ const ContractWrite = ({ addressHash, isProxy, isCustomAbi }: Props) => {
const hash = await walletClient?.writeContract({ const hash = await walletClient?.writeContract({
args: _args, args: _args,
abi: abi, abi: contractAbi,
functionName: methodName, functionName: methodName,
address: addressHash as `0x${ string }`, address: addressHash as `0x${ string }`,
value: value as undefined, value: value as undefined,
}); });
return { hash }; return { hash };
}, [ isConnected, chain, abi, walletClient, addressHash, switchNetworkAsync ]); }, [ isConnected, chain, contractAbi, walletClient, addressHash, switchNetworkAsync ]);
const renderContent = React.useCallback((item: SmartContractWriteMethod, index: number, id: number) => { const renderContent = React.useCallback((item: SmartContractWriteMethod, index: number, id: number) => {
return ( return (
......
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import type { Abi } from 'abitype';
import React from 'react'; import React from 'react';
import type { Address } from 'types/api/address'; import type { Address } from 'types/api/address';
import type { SmartContract } from 'types/api/contract';
import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery'; import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery';
type ProviderProps = { interface Params {
addressHash?: string; addressHash?: string;
children: React.ReactNode; isProxy?: boolean;
isCustomAbi?: boolean;
} }
type TContractContext = { export default function useContractAbi({ addressHash, isProxy, isCustomAbi }: Params): Abi | undefined {
contractInfo: SmartContract | undefined;
proxyInfo: SmartContract | undefined;
customInfo: SmartContract | undefined;
};
const ContractContext = React.createContext<TContractContext>({
proxyInfo: undefined,
contractInfo: undefined,
customInfo: undefined,
});
export function ContractContextProvider({ addressHash, children }: ProviderProps) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { data: contractInfo } = useApiQuery('contract', { const { data: contractInfo } = useApiQuery('contract', {
...@@ -55,23 +44,15 @@ export function ContractContextProvider({ addressHash, children }: ProviderProps ...@@ -55,23 +44,15 @@ export function ContractContextProvider({ addressHash, children }: ProviderProps
}, },
}); });
const value = React.useMemo(() => ({ return React.useMemo(() => {
proxyInfo, if (isProxy) {
contractInfo, return proxyInfo?.abi ?? undefined;
customInfo, }
} as TContractContext), [ proxyInfo, contractInfo, customInfo ]);
return ( if (isCustomAbi) {
<ContractContext.Provider value={ value }> return customInfo;
{ children } }
</ContractContext.Provider>
);
}
export function useContractContext() { return contractInfo?.abi ?? undefined;
const context = React.useContext(ContractContext); }, [ contractInfo?.abi, customInfo, isCustomAbi, isProxy, proxyInfo?.abi ]);
if (context === undefined) {
throw new Error('useContractContext must be used within a ContractContextProvider');
}
return context;
} }
...@@ -86,11 +86,11 @@ const AddressPageContent = () => { ...@@ -86,11 +86,11 @@ const AddressPageContent = () => {
return 'Contract'; return 'Contract';
}, },
component: <AddressContract tabs={ contractTabs } addressHash={ hash }/>, component: <AddressContract tabs={ contractTabs }/>,
subTabs: contractTabs.map(tab => tab.id), subTabs: contractTabs.map(tab => tab.id),
} : undefined, } : undefined,
].filter(Boolean); ].filter(Boolean);
}, [ addressQuery.data, contractTabs, hash ]); }, [ addressQuery.data, contractTabs ]);
const tags = ( const tags = (
<EntityTags <EntityTags
......
...@@ -178,7 +178,7 @@ const TokenPageContent = () => { ...@@ -178,7 +178,7 @@ const TokenPageContent = () => {
return 'Contract'; return 'Contract';
}, },
component: <AddressContract tabs={ contractTabs } addressHash={ hashString }/>, component: <AddressContract tabs={ contractTabs }/>,
subTabs: contractTabs.map(tab => tab.id), subTabs: contractTabs.map(tab => tab.id),
} : undefined, } : undefined,
].filter(Boolean); ].filter(Boolean);
......
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