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 = {
result: {
message: 'Some shit happened',
code: -32017,
raw: '49276d20616c7761797320726576657274696e67207769746820616e206572726f72',
},
};
......
......@@ -241,6 +241,7 @@ const ContractCode = ({ addressHash, noSocket }: Props) => {
<RawDataSnippet
data={ data.deployed_bytecode }
title="Deployed ByteCode"
rightSlot={ !data?.creation_bytecode && !(data.is_verified || data.is_self_destructed) ? verificationButton : null }
textareaMaxHeight="200px"
isLoading={ isPlaceholderData }
/>
......
......@@ -11,25 +11,37 @@ import Address from 'ui/shared/address/Address';
import AddressLink from 'ui/shared/address/AddressLink';
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 {
data: SmartContractMethodOutput;
}
const ContractMethodStatic = ({ data }: Props) => {
const isBigInt = data.type.includes('int256') || data.type.includes('int128');
const [ value, setValue ] = React.useState(isBigInt && data.value && typeof data.value === 'string' ? BigNumber(data.value).toFixed() : data.value);
const [ value, setValue ] = React.useState<string>(castValueToString(data.value));
const [ label, setLabel ] = React.useState('WEI');
const handleCheckboxChange = React.useCallback((event: ChangeEvent<HTMLInputElement>) => {
if (!data.value || typeof data.value !== 'string') {
return;
}
const initialValue = castValueToString(data.value);
if (event.target.checked) {
setValue(BigNumber(data.value).div(WEI).toFixed());
setValue(BigNumber(initialValue).div(WEI).toFixed());
setLabel(appConfig.network.currency.symbol || 'ETH');
} else {
setValue(BigNumber(data.value).toFixed());
setValue(BigNumber(initialValue).toFixed());
setLabel('WEI');
}
}, [ data.value ]);
......@@ -50,7 +62,7 @@ const ContractMethodStatic = ({ data }: Props) => {
return (
<Flex flexDir={{ base: 'column', lg: 'row' }} columnGap={ 2 } rowGap={ 2 }>
{ content }
{ isBigInt && <Checkbox onChange={ handleCheckboxChange }>{ label }</Checkbox> }
{ (data.type.includes('int256') || data.type.includes('int128')) && <Checkbox onChange={ handleCheckboxChange }>{ label }</Checkbox> }
</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 React from 'react';
import { scroller } from 'react-scroll';
import type { SmartContractMethod } from 'types/api/contract';
import Hint from 'ui/shared/Hint';
import ContractMethodsAccordionItem from './ContractMethodsAccordionItem';
interface Props<T extends SmartContractMethod> {
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 [ expandedSections, setExpandedSections ] = React.useState<Array<number>>([]);
const ContractMethodsAccordion = <T extends SmartContractMethod>({ data, addressHash, renderItemContent }: Props<T>) => {
const [ expandedSections, setExpandedSections ] = React.useState<Array<number>>(data.length === 1 ? [ 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>) => {
setExpandedSections(newValue);
}, []);
......@@ -42,46 +62,25 @@ const ContractMethodsAccordion = <T extends SmartContractMethod>({ data, renderC
return (
<>
<Flex mb={ 3 }>
<Box fontWeight={ 500 }>Contract information</Box>
<Link onClick={ handleExpandAll } ml="auto">{ expandedSections.length === data.length ? 'Collapse' : 'Expand' } all</Link>
<Box fontWeight={ 500 } mr="auto">Contract information</Box>
{ data.length > 1 && (
<Link onClick={ handleExpandAll }>
{ expandedSections.length === data.length ? 'Collapse' : 'Expand' } all
</Link>
) }
<Link onClick={ handleReset } ml={ 3 }>Reset</Link>
</Flex>
<Accordion allowMultiple position="relative" onChange={ handleAccordionStateChange } index={ expandedSections }>
{ data.map((item, index) => {
return (
<AccordionItem key={ index } as="section" _first={{ borderTopWidth: '0' }}>
<h2>
<AccordionButton px={ 0 } py={ 3 } _hover={{ bgColor: 'inherit' }} wordBreak="break-all" textAlign="left">
<Box as="span" fontWeight={ 500 } mr={ 1 }>
{ index + 1 }. { item.type === 'fallback' || item.type === 'receive' ? item.type : item.name }
</Box>
{ 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>
);
}) }
{ data.map((item, index) => (
<ContractMethodsAccordionItem
key={ index }
data={ item }
id={ id }
index={ index }
addressHash={ addressHash }
renderContent={ renderItemContent as (item: SmartContractMethod, index: number, id: number) => React.ReactNode }
/>
)) }
</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) => {
});
}, [ 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) {
return <Alert status="error" fontSize="sm" wordBreak="break-word">{ item.error }</Alert>;
}
......@@ -96,7 +96,7 @@ const ContractRead = ({ addressHash, isProxy, isCustomAbi }: Props) => {
{ isCustomAbi && <ContractCustomAbiAlert/> }
<ContractConnectWallet/>
{ isProxy && <ContractImplementationAddress hash={ addressHash }/> }
<ContractMethodsAccordion data={ data } renderContent={ renderContent }/>
<ContractMethodsAccordion data={ data } addressHash={ addressHash } renderItemContent={ renderItemContent }/>
</>
);
};
......
......@@ -4,6 +4,8 @@ import React from 'react';
import type { ContractMethodReadResult } from './types';
import type { SmartContractReadMethod } from 'types/api/contract';
import hexToUtf8 from 'lib/hexToUtf8';
interface Props {
item: SmartContractReadMethod;
result: ContractMethodReadResult;
......@@ -23,7 +25,14 @@ const ContractReadResult = ({ item, result, onSettle }: Props) => {
if (result.is_error) {
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 (
......
......@@ -83,7 +83,7 @@ const ContractWrite = ({ addressHash, isProxy, isCustomAbi }: Props) => {
return { hash };
}, [ 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 (
<ContractMethodCallable
key={ id + '_' + index }
......@@ -112,7 +112,7 @@ const ContractWrite = ({ addressHash, isProxy, isCustomAbi }: Props) => {
{ isCustomAbi && <ContractCustomAbiAlert/> }
<ContractConnectWallet/>
{ 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