Commit 420246dc authored by tom goriunov's avatar tom goriunov Committed by GitHub

contract ABI: improvements and bugfixes (#1011)

* add link to contract method

* scroll to and expand method from url

* expand method if it is the only one

* fix "to WEI" checkbox

* display verification button for precompiled contracts

* add decoded revert reason to contract read result

* update screenshots
parent f42ec931
...@@ -90,6 +90,7 @@ export const readResultError: SmartContractQueryMethodReadError = { ...@@ -90,6 +90,7 @@ export const readResultError: SmartContractQueryMethodReadError = {
result: { result: {
message: 'Some shit happened', message: 'Some shit happened',
code: -32017, code: -32017,
raw: '49276d20616c7761797320726576657274696e67207769746820616e206572726f72',
}, },
}; };
......
...@@ -241,6 +241,7 @@ const ContractCode = ({ addressHash, noSocket }: Props) => { ...@@ -241,6 +241,7 @@ const ContractCode = ({ addressHash, noSocket }: Props) => {
<RawDataSnippet <RawDataSnippet
data={ data.deployed_bytecode } data={ data.deployed_bytecode }
title="Deployed ByteCode" title="Deployed ByteCode"
rightSlot={ !data?.creation_bytecode && !(data.is_verified || data.is_self_destructed) ? verificationButton : null }
textareaMaxHeight="200px" textareaMaxHeight="200px"
isLoading={ isPlaceholderData } isLoading={ isPlaceholderData }
/> />
......
...@@ -11,25 +11,37 @@ import Address from 'ui/shared/address/Address'; ...@@ -11,25 +11,37 @@ import Address from 'ui/shared/address/Address';
import AddressLink from 'ui/shared/address/AddressLink'; import AddressLink from 'ui/shared/address/AddressLink';
import CopyToClipboard from 'ui/shared/CopyToClipboard'; import CopyToClipboard from 'ui/shared/CopyToClipboard';
function castValueToString(value: number | string | boolean | bigint | undefined): string {
switch (typeof value) {
case 'string':
return value;
case 'boolean':
return String(value);
case 'undefined':
return '';
case 'number':
return value.toLocaleString(undefined, { useGrouping: false });
case 'bigint':
return value.toString();
}
}
interface Props { interface Props {
data: SmartContractMethodOutput; data: SmartContractMethodOutput;
} }
const ContractMethodStatic = ({ data }: Props) => { const ContractMethodStatic = ({ data }: Props) => {
const isBigInt = data.type.includes('int256') || data.type.includes('int128'); const [ value, setValue ] = React.useState<string>(castValueToString(data.value));
const [ value, setValue ] = React.useState(isBigInt && data.value && typeof data.value === 'string' ? BigNumber(data.value).toFixed() : data.value);
const [ label, setLabel ] = React.useState('WEI'); const [ label, setLabel ] = React.useState('WEI');
const handleCheckboxChange = React.useCallback((event: ChangeEvent<HTMLInputElement>) => { const handleCheckboxChange = React.useCallback((event: ChangeEvent<HTMLInputElement>) => {
if (!data.value || typeof data.value !== 'string') { const initialValue = castValueToString(data.value);
return;
}
if (event.target.checked) { if (event.target.checked) {
setValue(BigNumber(data.value).div(WEI).toFixed()); setValue(BigNumber(initialValue).div(WEI).toFixed());
setLabel(appConfig.network.currency.symbol || 'ETH'); setLabel(appConfig.network.currency.symbol || 'ETH');
} else { } else {
setValue(BigNumber(data.value).toFixed()); setValue(BigNumber(initialValue).toFixed());
setLabel('WEI'); setLabel('WEI');
} }
}, [ data.value ]); }, [ data.value ]);
...@@ -50,7 +62,7 @@ const ContractMethodStatic = ({ data }: Props) => { ...@@ -50,7 +62,7 @@ const ContractMethodStatic = ({ data }: Props) => {
return ( return (
<Flex flexDir={{ base: 'column', lg: 'row' }} columnGap={ 2 } rowGap={ 2 }> <Flex flexDir={{ base: 'column', lg: 'row' }} columnGap={ 2 } rowGap={ 2 }>
{ content } { content }
{ isBigInt && <Checkbox onChange={ handleCheckboxChange }>{ label }</Checkbox> } { (data.type.includes('int256') || data.type.includes('int128')) && <Checkbox onChange={ handleCheckboxChange }>{ label }</Checkbox> }
</Flex> </Flex>
); );
}; };
......
import { Accordion, AccordionButton, AccordionIcon, AccordionItem, AccordionPanel, Box, Flex, Link } from '@chakra-ui/react'; import { Accordion, Box, Flex, Link } from '@chakra-ui/react';
import _range from 'lodash/range'; import _range from 'lodash/range';
import React from 'react'; import React from 'react';
import { scroller } from 'react-scroll';
import type { SmartContractMethod } from 'types/api/contract'; import type { SmartContractMethod } from 'types/api/contract';
import Hint from 'ui/shared/Hint'; import ContractMethodsAccordionItem from './ContractMethodsAccordionItem';
interface Props<T extends SmartContractMethod> { interface Props<T extends SmartContractMethod> {
data: Array<T>; data: Array<T>;
renderContent: (item: T, index: number, id: number) => React.ReactNode; addressHash?: string;
renderItemContent: (item: T, index: number, id: number) => React.ReactNode;
} }
const ContractMethodsAccordion = <T extends SmartContractMethod>({ data, renderContent }: Props<T>) => { const ContractMethodsAccordion = <T extends SmartContractMethod>({ data, addressHash, renderItemContent }: Props<T>) => {
const [ expandedSections, setExpandedSections ] = React.useState<Array<number>>([]); const [ expandedSections, setExpandedSections ] = React.useState<Array<number>>(data.length === 1 ? [ 0 ] : []);
const [ id, setId ] = React.useState(0); const [ id, setId ] = React.useState(0);
React.useEffect(() => {
const hash = window.location.hash.replace('#', '');
if (!hash) {
return;
}
const index = data.findIndex((item) => 'method_id' in item && item.method_id === hash);
if (index > -1) {
scroller.scrollTo(`method_${ hash }`, {
duration: 500,
smooth: true,
offset: -100,
});
setExpandedSections([ index ]);
}
}, [ data ]);
const handleAccordionStateChange = React.useCallback((newValue: Array<number>) => { const handleAccordionStateChange = React.useCallback((newValue: Array<number>) => {
setExpandedSections(newValue); setExpandedSections(newValue);
}, []); }, []);
...@@ -42,46 +62,25 @@ const ContractMethodsAccordion = <T extends SmartContractMethod>({ data, renderC ...@@ -42,46 +62,25 @@ const ContractMethodsAccordion = <T extends SmartContractMethod>({ data, renderC
return ( return (
<> <>
<Flex mb={ 3 }> <Flex mb={ 3 }>
<Box fontWeight={ 500 }>Contract information</Box> <Box fontWeight={ 500 } mr="auto">Contract information</Box>
<Link onClick={ handleExpandAll } ml="auto">{ expandedSections.length === data.length ? 'Collapse' : 'Expand' } all</Link> { data.length > 1 && (
<Link onClick={ handleExpandAll }>
{ expandedSections.length === data.length ? 'Collapse' : 'Expand' } all
</Link>
) }
<Link onClick={ handleReset } ml={ 3 }>Reset</Link> <Link onClick={ handleReset } ml={ 3 }>Reset</Link>
</Flex> </Flex>
<Accordion allowMultiple position="relative" onChange={ handleAccordionStateChange } index={ expandedSections }> <Accordion allowMultiple position="relative" onChange={ handleAccordionStateChange } index={ expandedSections }>
{ data.map((item, index) => { { data.map((item, index) => (
return ( <ContractMethodsAccordionItem
<AccordionItem key={ index } as="section" _first={{ borderTopWidth: '0' }}> key={ index }
<h2> data={ item }
<AccordionButton px={ 0 } py={ 3 } _hover={{ bgColor: 'inherit' }} wordBreak="break-all" textAlign="left"> id={ id }
<Box as="span" fontWeight={ 500 } mr={ 1 }> index={ index }
{ index + 1 }. { item.type === 'fallback' || item.type === 'receive' ? item.type : item.name } addressHash={ addressHash }
</Box> renderContent={ renderItemContent as (item: SmartContractMethod, index: number, id: number) => React.ReactNode }
{ item.type === 'fallback' && ( />
<Hint )) }
label={
`The fallback function is executed on a call to the contract if none of the other functions match
the given function signature, or if no data was supplied at all and there is no receive Ether function.
The fallback function always receives data, but in order to also receive Ether it must be marked payable.`
}/>
) }
{ item.type === 'receive' && (
<Hint
label={
`The receive function is executed on a call to the contract with empty calldata.
This is the function that is executed on plain Ether transfers (e.g. via .send() or .transfer()).
If no such function exists, but a payable fallback function exists, the fallback function will be called on a plain Ether transfer.
If neither a receive Ether nor a payable fallback function is present,
the contract cannot receive Ether through regular transactions and throws an exception.`
}/>
) }
<AccordionIcon/>
</AccordionButton>
</h2>
<AccordionPanel pb={ 4 } px={ 0 }>
{ renderContent(item, index, id) }
</AccordionPanel>
</AccordionItem>
);
}) }
</Accordion> </Accordion>
</> </>
); );
......
import { AccordionButton, AccordionIcon, AccordionItem, AccordionPanel, Box, Icon, Tooltip, useClipboard, useDisclosure } from '@chakra-ui/react';
import { route } from 'nextjs-routes';
import React from 'react';
import { Element } from 'react-scroll';
import type { SmartContractMethod } from 'types/api/contract';
import config from 'configs/app/config';
import iconLink from 'icons/link.svg';
import Hint from 'ui/shared/Hint';
interface Props<T extends SmartContractMethod> {
data: T;
index: number;
id: number;
addressHash?: string;
renderContent: (item: T, index: number, id: number) => React.ReactNode;
}
const ContractMethodsAccordionItem = <T extends SmartContractMethod>({ data, index, id, addressHash, renderContent }: Props<T>) => {
const url = React.useMemo(() => {
if (!('method_id' in data)) {
return '';
}
return config.baseUrl + route({
pathname: '/address/[hash]',
query: {
hash: addressHash ?? '',
tab: 'read_contract',
},
hash: data.method_id,
});
}, [ addressHash, data ]);
const { hasCopied, onCopy } = useClipboard(url, 1000);
const { isOpen, onOpen, onClose } = useDisclosure();
const handleCopyLinkClick = React.useCallback((event: React.MouseEvent) => {
event.stopPropagation();
onCopy();
}, [ onCopy ]);
return (
<AccordionItem as="section" _first={{ borderTopWidth: 0 }} _last={{ borderBottomWidth: 0 }}>
<Element as="h2" name={ 'method_id' in data ? `method_${ data.method_id }` : '' }>
<AccordionButton px={ 0 } py={ 3 } _hover={{ bgColor: 'inherit' }} wordBreak="break-all" textAlign="left">
{ 'method_id' in data && (
<Tooltip label={ hasCopied ? 'Copied!' : 'Copy link' } isOpen={ isOpen || hasCopied } onClose={ onClose }>
<Box
boxSize={ 5 }
color="text_secondary"
_hover={{ color: 'link_hovered' }}
mr={ 2 }
onClick={ handleCopyLinkClick }
onMouseEnter={ onOpen }
onMouseLeave={ onClose }
>
<Icon as={ iconLink } boxSize={ 5 }/>
</Box>
</Tooltip>
) }
<Box as="span" fontWeight={ 500 } mr={ 1 }>
{ index + 1 }. { data.type === 'fallback' || data.type === 'receive' ? data.type : data.name }
</Box>
{ data.type === 'fallback' && (
<Hint
label={
`The fallback function is executed on a call to the contract if none of the other functions match
the given function signature, or if no data was supplied at all and there is no receive Ether function.
The fallback function always receives data, but in order to also receive Ether it must be marked payable.`
}/>
) }
{ data.type === 'receive' && (
<Hint
label={
`The receive function is executed on a call to the contract with empty calldata.
This is the function that is executed on plain Ether transfers (e.g. via .send() or .transfer()).
If no such function exists, but a payable fallback function exists, the fallback function will be called on a plain Ether transfer.
If neither a receive Ether nor a payable fallback function is present,
the contract cannot receive Ether through regular transactions and throws an exception.`
}/>
) }
<AccordionIcon/>
</AccordionButton>
</Element>
<AccordionPanel pb={ 4 } px={ 0 }>
{ renderContent(data, index, id) }
</AccordionPanel>
</AccordionItem>
);
};
export default React.memo(ContractMethodsAccordionItem);
...@@ -56,7 +56,7 @@ const ContractRead = ({ addressHash, isProxy, isCustomAbi }: Props) => { ...@@ -56,7 +56,7 @@ const ContractRead = ({ addressHash, isProxy, isCustomAbi }: Props) => {
}); });
}, [ addressHash, apiFetch, isCustomAbi, isProxy, userAddress ]); }, [ addressHash, apiFetch, isCustomAbi, isProxy, userAddress ]);
const renderContent = React.useCallback((item: SmartContractReadMethod, index: number, id: number) => { const renderItemContent = React.useCallback((item: SmartContractReadMethod, index: number, id: number) => {
if (item.error) { if (item.error) {
return <Alert status="error" fontSize="sm" wordBreak="break-word">{ item.error }</Alert>; return <Alert status="error" fontSize="sm" wordBreak="break-word">{ item.error }</Alert>;
} }
...@@ -96,7 +96,7 @@ const ContractRead = ({ addressHash, isProxy, isCustomAbi }: Props) => { ...@@ -96,7 +96,7 @@ const ContractRead = ({ addressHash, isProxy, isCustomAbi }: Props) => {
{ isCustomAbi && <ContractCustomAbiAlert/> } { isCustomAbi && <ContractCustomAbiAlert/> }
<ContractConnectWallet/> <ContractConnectWallet/>
{ isProxy && <ContractImplementationAddress hash={ addressHash }/> } { isProxy && <ContractImplementationAddress hash={ addressHash }/> }
<ContractMethodsAccordion data={ data } renderContent={ renderContent }/> <ContractMethodsAccordion data={ data } addressHash={ addressHash } renderItemContent={ renderItemContent }/>
</> </>
); );
}; };
......
...@@ -4,6 +4,8 @@ import React from 'react'; ...@@ -4,6 +4,8 @@ import React from 'react';
import type { ContractMethodReadResult } from './types'; import type { ContractMethodReadResult } from './types';
import type { SmartContractReadMethod } from 'types/api/contract'; import type { SmartContractReadMethod } from 'types/api/contract';
import hexToUtf8 from 'lib/hexToUtf8';
interface Props { interface Props {
item: SmartContractReadMethod; item: SmartContractReadMethod;
result: ContractMethodReadResult; result: ContractMethodReadResult;
...@@ -23,7 +25,14 @@ const ContractReadResult = ({ item, result, onSettle }: Props) => { ...@@ -23,7 +25,14 @@ const ContractReadResult = ({ item, result, onSettle }: Props) => {
if (result.is_error) { if (result.is_error) {
const message = 'error' in result.result ? result.result.error : result.result.message; const message = 'error' in result.result ? result.result.error : result.result.message;
return <Alert status="error" mt={ 3 } p={ 4 } borderRadius="md" fontSize="sm" wordBreak="break-word">{ message }</Alert>; const decoded = 'raw' in result.result && result.result.raw ? `\nRevert reason: ${ hexToUtf8(result.result.raw) }` : '';
return (
<Alert status="error" mt={ 3 } p={ 4 } borderRadius="md" fontSize="sm" wordBreak="break-word" whiteSpace="pre-wrap">
{ message }
{ decoded }
</Alert>
);
} }
return ( return (
......
...@@ -83,7 +83,7 @@ const ContractWrite = ({ addressHash, isProxy, isCustomAbi }: Props) => { ...@@ -83,7 +83,7 @@ const ContractWrite = ({ addressHash, isProxy, isCustomAbi }: Props) => {
return { hash }; return { hash };
}, [ isConnected, chain, contractAbi, walletClient, addressHash, switchNetworkAsync ]); }, [ isConnected, chain, contractAbi, walletClient, addressHash, switchNetworkAsync ]);
const renderContent = React.useCallback((item: SmartContractWriteMethod, index: number, id: number) => { const renderItemContent = React.useCallback((item: SmartContractWriteMethod, index: number, id: number) => {
return ( return (
<ContractMethodCallable <ContractMethodCallable
key={ id + '_' + index } key={ id + '_' + index }
...@@ -112,7 +112,7 @@ const ContractWrite = ({ addressHash, isProxy, isCustomAbi }: Props) => { ...@@ -112,7 +112,7 @@ const ContractWrite = ({ addressHash, isProxy, isCustomAbi }: Props) => {
{ isCustomAbi && <ContractCustomAbiAlert/> } { isCustomAbi && <ContractCustomAbiAlert/> }
<ContractConnectWallet/> <ContractConnectWallet/>
{ isProxy && <ContractImplementationAddress hash={ addressHash }/> } { isProxy && <ContractImplementationAddress hash={ addressHash }/> }
<ContractMethodsAccordion data={ data } renderContent={ renderContent }/> <ContractMethodsAccordion data={ data } addressHash={ addressHash } renderItemContent={ renderItemContent }/>
</> </>
); );
}; };
......
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