import { useCallback } from 'react' ; import debounce from 'lodash/debounce' ; import { CREATION_ADDRESS , DEFAULT_NETWORK } from '@config' ; import { makeBasicTxConfig , makePendingTxReceipt , makeTxFromForm , toTxReceipt } from '@helpers' ; import { EtherscanService , getGasEstimate , isValidETHAddress , ProviderHandler , useAccounts , useContracts , useNetworks } from '@services' ; import { AbiFunction } from '@services/EthService/contracts/ABIFunction' ; import { getIsValidENSAddressFunction } from '@services/EthService/ens' ; import { translateRaw } from '@translations' ; import { Contract , ExtendedContract , ISimpleTxForm , ITxHash , ITxStatus , ITxType , NetworkId , StoreAccount , TAddress , TUuid } from '@types' ; import { inputGasLimitToHex , isSameAddress , isWeb3Wallet , TUseStateReducerFactory } from '@utils' ; import { CUSTOM_CONTRACT_ADDRESS , customContract } from './constants' ; import { constructGasCallProps , reduceInputParams } from './helpers' ; import { ABIItem , InteractWithContractState } from './types' ; const interactWithContractsInitialState = { network : { } , addressOrDomainInput : '' , resolvingDomain : false , contractAddress : '' , contract : undefined , customContractName : '' , contracts : [ ] , abi : '' , showGeneratedForm : false , submitedFunction : undefined , data : undefined , account : undefined , gasPrice : '0xee6b2800' , gasLimit : '21000' , nonce : '0' , txConfig : undefined , txReceipt : undefined } ; const InteractWithContractsFactory = ( { state , setState } ) => { const { getContractsByNetwork , createContract , deleteContract } = useContracts ( ) ; const { getNetworkById } = useNetworks ( ) ; const { addTxToAccount } = useAccounts ( ) ; const ethNetwork = getNetworkById ( DEFAULT_NETWORK ) ; const handleNetworkSelected = ( networkId ) => { setState ( ( prevState ) => ( { ... prevState , account : undefined , network : getNetworkById ( networkId ) , contract : undefined , contractAddress : '' , addressOrDomainInput : '' , abi : '' , customContractName : '' , resolvingDomain : false } ) ) ; } ; const updateNetworkContractOptions = ( ) => { const networkContracts = getContractsByNetwork ( state . network . id ) ; const contracts = networkContracts . map ( ( x ) => Object . assign ( { } , x , { label : x . name } ) ) ; setState ( ( prevState ) => ( { ... prevState , contracts } ) ) ; } ; const handleContractSelected = ( contract ) => { let contractAddress = '' ; let addressOrDomainInput = '' ; let contractAbi = '' ; if ( contract . address !== CUSTOM_CONTRACT_ADDRESS && contract . abi ) { contractAddress = contract . address ; addressOrDomainInput = contract . address ; contractAbi = contract . abi ; } setState ( ( prevState ) => ( { ... prevState , contract , contractAddress , addressOrDomainInput , abi : contractAbi , customContractName : '' , resolvingDomain : false } ) ) ; } ; const debouncedResolveAddressFromDomain = useCallback ( debounce ( ( value ) => resolveAddressFromDomain ( value ) , 1500 ) , [ state . contracts ] ) ; const handleAddressOrDomainChanged = ( value ) => { if ( selectExistingContract ( value ) || value === '' || value === undefined ) { return ; } setState ( ( prevState ) => ( { ... prevState , addressOrDomainInput : value , contractAddress : value , contract : customContract , abi : '' } ) ) ; if ( getIsValidENSAddressFunction ( state . network . chainId ) ( value ) ) { debouncedResolveAddressFromDomain ( value ) ; } if ( isValidETHAddress ( value ) ) { fetchABI ( value ) ; } } ; const resolveAddressFromDomain = async ( domain ) => { setState ( ( prevState ) => ( { ... prevState , resolvingDomain : true } ) ) ; const provider = new ProviderHandler ( ethNetwork ) ; const resolvedAddress = ( await provider . resolveName ( domain , state . network ) ) || CREATION_ADDRESS ; setState ( ( prevState ) => ( { ... prevState , contractAddress : resolvedAddress , resolvingDomain : false } ) ) ; const exists = selectExistingContract ( resolvedAddress ) ; if ( ! exists ) { fetchABI ( resolvedAddress ) ; } } ; const selectExistingContract = ( address ) => { const existingContract = state . contracts . find ( ( c ) => isSameAddress ( c . address , address as TAddress ) ) ; if ( existingContract ) { handleContractSelected ( existingContract ) ; return true ; } return false ; } ; const fetchABI = async ( address ) => { const fetchedAbi = await EtherscanService . instance . getContractAbi ( address , state . network . id ) ; if ( fetchedAbi ) { setState ( ( prevState ) => ( { ... prevState , abi : fetchedAbi } ) ) ; } } ; const handleAbiChanged = ( abi ) => { setState ( ( prevState ) => ( { ... prevState , abi } ) ) ; } ; const handleCustomContractNameChanged = ( customContractName ) => { setState ( ( prevState ) => ( { ... prevState , customContractName } ) ) ; } ; const handleSaveContractSubmit = ( ) => { if ( ! state . contractAddress || ! state . customContractName || ! state . abi ) { throw new Error ( translateRaw ( 'INTERACT_WRITE_ERROR_MISSING_DATA' ) ) ; } if ( ! isValidETHAddress ( state . contractAddress ) ) { throw new Error ( translateRaw ( 'INTERACT_ERROR_INVALID_ADDRESS' ) ) ; } try { JSON . parse ( state . abi ) ; } catch ( e ) { throw new Error ( `ABI Error: ${ e . message }` ) ; } if ( state . contracts . find ( ( item ) => item . name === state . customContractName ) ) { throw new Error ( translateRaw ( 'INTERACT_SAVE_ERROR_NAME_EXISTS' ) ) ; } const contract = createContract ( { abi : state . abi , address : state . contractAddress as TAddress , name : state . customContractName , label : state . customContractName , networkId : state . network . id , isCustom : true } ) ; updateNetworkContractOptions ( ) ; handleContractSelected ( contract ) ; } ; const handleDeleteContract = ( contractUuid ) => { deleteContract ( contractUuid ) ; updateNetworkContractOptions ( ) ; handleContractSelected ( customContract ) ; } ; const displayGeneratedForm = ( visible ) => { if ( visible ) { if ( ! state . contractAddress || ! state . abi ) { throw new Error ( translateRaw ( 'INTERACT_ERROR_NO_CONTRACT_SELECTED' ) ) ; } if ( ! isValidETHAddress ( state . contractAddress ) ) { throw new Error ( translateRaw ( 'INTERACT_ERROR_INVALID_ADDRESS' ) ) ; } try { JSON . parse ( state . abi ) ; } catch ( e ) { throw new Error ( `ABI Error: ${ e . message }` ) ; } } setState ( ( prevState ) => ( { ... prevState , showGeneratedForm : visible } ) ) ; } ; const handleInteractionFormSubmit = async ( submitedFunction ) => { const { network , contractAddress } = state ; const { encodeInput , decodeOutput } = new AbiFunction ( submitedFunction , [ ] ) ; const parsedInputs = reduceInputParams ( submitedFunction ) ; const providerHandler = new ProviderHandler ( network ) ; const data = { to : contractAddress , data : encodeInput ( parsedInputs ) } ; const result = await providerHandler . call ( data ) ; return decodeOutput ( result , network . chainId ) ; } ; const handleInteractionFormWriteSubmit = async ( submittedFunction , after ) => { const { contractAddress , account , nonce , gasLimit , gasPrice , maxFeePerGas , maxPriorityFeePerGas } = state ; if ( ! account ) { throw new Error ( translateRaw ( 'INTERACT_WRITE_ERROR_NO_ACCOUNT' ) ) ; } const { network } = account ; const { value , data } = constructGasCallProps ( contractAddress , submittedFunction , account ) ; const { gasLimit : unusedGasLimit , ... transaction } = makeTxFromForm ( { gasPrice , gasLimit , maxFeePerGas , maxPriorityFeePerGas , nonce , account , address : contractAddress , network } , value ! , data ! ) ; await getGasEstimate ( network , transaction ) ; const tx = { ... transaction , gasLimit : inputGasLimitToHex ( gasLimit ) } ; const txConfig = makeBasicTxConfig ( tx , account , submittedFunction . payAmount ) ; setState ( ( prevState ) => ( { ... prevState , txConfig } ) ) ; after ( ) ; } ; const handleAccountSelected = ( account ? ) => { setState ( ( prevState ) => ( { ... prevState , account } ) ) ; } ; const handleTxSigned = async ( signResponse , after ) => { const { account , txConfig } = state ; if ( ! account ) { return ; } if ( isWeb3Wallet ( account . wallet ) ) { const txReceipt = signResponse && signResponse . hash ? signResponse : toTxReceipt ( signResponse , ITxStatus . PENDING ) ( ITxType . CONTRACT_INTERACT , txConfig ) ; addTxToAccount ( state . txConfig . senderAccount , { ... txReceipt , to : state . txConfig . receiverAddress , from : state . txConfig . senderAccount . address , amount : state . txConfig . amount } ) ; setState ( ( prevState ) => ( { ... prevState , txReceipt } ) ) ; after ( ) ; } else { const provider = new ProviderHandler ( account . network ) ; provider . sendRawTx ( signResponse ) . then ( ( retrievedTxReceipt ) => retrievedTxReceipt . hash as ITxHash ) . catch ( ( hash ) => hash as ITxHash ) . then ( ( txHash ) => { const pendingTxReceipt = makePendingTxReceipt ( txHash ) ( ITxType . CONTRACT_INTERACT , state . txConfig ) ; addTxToAccount ( state . txConfig . senderAccount , pendingTxReceipt ) ; setState ( ( prevState ) => ( { ... prevState , txReceipt : pendingTxReceipt } ) ) ; } ) . finally ( after ) ; } } ; const handleGasSelectorChange = ( payload ) => { setState ( ( prevState ) => ( { ... prevState , ... payload } ) ) ; } ; const handleGasLimitChange = ( gasLimit ) => { setState ( ( prevState ) => ( { ... prevState , gasLimit } ) ) ; } ; const handleNonceChange = ( nonce ) => { setState ( ( prevState ) => ( { ... prevState , nonce } ) ) ; } ; return { handleNetworkSelected , handleAddressOrDomainChanged , handleContractSelected , handleAbiChanged , handleCustomContractNameChanged , handleSaveContractSubmit , updateNetworkContractOptions , displayGeneratedForm , handleInteractionFormSubmit , handleInteractionFormWriteSubmit , handleAccountSelected , handleTxSigned , handleGasSelectorChange , handleDeleteContract , handleGasLimitChange , handleNonceChange , interactWithContractsState : state } ; } ; export { interactWithContractsInitialState , InteractWithContractsFactory } ;