import React, { useContext, useEffect, useState } from 'react'; import PropTypes from 'prop-types'; import inspect from 'browser-util-inspect'; import { forAddress } from '@truffle/decoder'; import { useSelector } from 'react-redux'; import * as Codec from '@truffle/codec'; import Spinner from '../../ui/spinner'; import ErrorMessage from '../../ui/error-message'; import fetchWithCache from '../../../../shared/lib/fetch-with-cache'; import { getSelectedAccount, getCurrentChainId } from '../../../selectors'; import { I18nContext } from '../../../contexts/i18n'; import { toChecksumHexAddress } from '../../../../shared/modules/hexstring-utils'; import { hexToDecimal } from '../../../../shared/modules/conversion.utils'; import { transformTxDecoding } from './transaction-decoding.util'; import { FETCH_PROJECT_INFO_URI, FETCH_SUPPORTED_NETWORKS_URI, } from './constants'; import Address from './components/decoding/address'; import CopyRawData from './components/ui/copy-raw-data'; import Accreditation from './components/ui/accreditation'; export default function TransactionDecoding({ to = '', inputData: data = '' }) { const t = useContext(I18nContext); const [tx, setTx] = useState([]); const [sourceAddress, setSourceAddress] = useState(''); const [sourceFetchedVia, setSourceFetchedVia] = useState(''); const { address: from } = useSelector(getSelectedAccount); const network = hexToDecimal(useSelector(getCurrentChainId)); const [loading, setLoading] = useState(false); const [hasError, setError] = useState(false); const [errorMessage, setErrorMessage] = useState(''); useEffect(() => { (async () => { setLoading(true); try { const networks = await fetchWithCache(FETCH_SUPPORTED_NETWORKS_URI, { method: 'GET', }); if ( !networks.some( (n) => n.active && Number(n.chainId) === Number(network), ) ) { throw new Error( t('transactionDecodingUnsupportedNetworkError', [network]), ); } const requestUrl = `${FETCH_PROJECT_INFO_URI}?${new URLSearchParams({ to, 'network-id': network, })}`; const response = await fetchWithCache(requestUrl, { method: 'GET' }); const { info: projectInfo, fetchedVia, address } = response; // update source information if (address) { setSourceAddress(address); } if (fetchedVia) { setSourceFetchedVia(fetchedVia); } // creating instance of the truffle decoder const decoder = await forAddress(to, { provider: global.ethereumProvider, projectInfo, }); // decode tx input data const decoding = await decoder.decodeTransaction({ from, to, input: data, blockNumber: null, }); // transform tx decoding arguments into tree data const params = transformTxDecoding(decoding?.arguments); setTx(params); setLoading(false); } catch (error) { setLoading(false); setError(true); if (error?.message.match('400')) { setErrorMessage(t('txInsightsNotSupported')); } else { setErrorMessage(error?.message); } } })(); }, [t, from, to, network, data]); // *********************************************************** // component rendering methods // *********************************************************** const renderLeaf = ({ name, kind, typeClass, value }) => { switch (kind) { case 'error': return ( <span className="sol-item solidity-error"> <span>{t('malformedData')}</span> </span> ); default: switch (typeClass) { case 'int': return ( <span className="sol-item solidity-int"> {[value.asBN || value.asString].toString()} </span> ); case 'uint': return ( <span className="sol-item solidity-uint"> {[value.asBN || value.asString].toString()} </span> ); case 'bool': return <span className="sol-item">{String(value.asBoolean)}</span>; case 'bytes': return ( <span className="sol-item solidity-bytes">{value.asHex}</span> ); case 'string': return ( <span className="sol-item solidity-string">{value.asString}</span> ); case 'array': return ( <details> <summary className="typography--color-black">{name}: </summary> <ol> {value.map((itemValue, index) => { return ( <li key={`${itemValue.type?.typeClass}-${index}`}> {renderLeaf({ typeClass: itemValue.type?.typeClass, value: itemValue.value, kind: itemValue.kind, })} </li> ); })} </ol> </details> ); case 'address': { const address = value?.asAddress; return ( <Address addressOnly checksummedRecipientAddress={toChecksumHexAddress(address)} /> ); } default: return ( <pre className="sol-item solidity-raw"> {inspect(new Codec.Format.Utils.Inspect.ResultInspector(value))} </pre> ); } } }; const renderTree = ( { name, kind, typeClass, type, value, children }, index, ) => { return children ? ( <li key={`${typeClass}-${index}`}> <details open={index === 0 ? 'open' : ''}> <summary>{name}: </summary> <ol>{children.map(renderTree)}</ol> </details> </li> ) : ( <li className="solidity-value" key={`solidity-value-${index}`}> <div className="solidity-named-item solidity-item"> {typeClass !== 'array' && !Array.isArray(value) ? ( <span className="param-name typography--color-black">{name}: </span> ) : null} <span className="sol-item solidity-uint"> {renderLeaf({ name, typeClass, type, value, kind })} </span> </div> </li> ); }; const renderTransactionDecoding = () => { if (loading) { return ( <div className="tx-insight-loading"> <Spinner color="var(--color-warning-default)" /> </div> ); } if (hasError) { return ( <div className="tx-insight-error"> <ErrorMessage errorMessage={errorMessage} /> </div> ); } return ( <div className="tx-insight-content"> <div className="tx-insight-content__tree-component"> <ol>{tx.map(renderTree)}</ol> </div> <div className="tx-insight-content__copy-raw-tx"> <CopyRawData data={data} /> </div> {sourceFetchedVia && sourceAddress ? ( <div className="tx-insight-content__accreditation"> <Accreditation address={sourceAddress} fetchVia={sourceFetchedVia} /> </div> ) : null} </div> ); }; return <div className="tx-insight">{renderTransactionDecoding()}</div>; } TransactionDecoding.propTypes = { to: PropTypes.string, inputData: PropTypes.string.isRequired, };