diff --git a/.dialyzer-ignore b/.dialyzer-ignore index a5d8c8f6c1..899009bca9 100644 --- a/.dialyzer-ignore +++ b/.dialyzer-ignore @@ -18,7 +18,7 @@ lib/block_scout_web/views/layout_view.ex:145: The call 'Elixir.Poison.Parser':'p lib/block_scout_web/views/layout_view.ex:237: The call 'Elixir.Poison.Parser':'parse!' lib/block_scout_web/controllers/api/rpc/transaction_controller.ex:21 lib/block_scout_web/controllers/api/rpc/transaction_controller.ex:22 -lib/explorer/smart_contract/reader.ex:347 +lib/explorer/smart_contract/reader.ex:348 lib/indexer/fetcher/token_total_supply_on_demand.ex:16 lib/explorer/exchange_rates/source.ex:110 lib/explorer/exchange_rates/source.ex:113 diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e450d4fc8..adddd59e46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,7 @@ - [#3577](https://github.com/poanetwork/blockscout/pull/3577) - Eliminate GraphiQL page XSS attack ### Chore +- [#3736](https://github.com/blockscout/blockscout/pull/3736) - Contract writer: Fix sending a transaction with tuple input type - [#3719](https://github.com/poanetwork/blockscout/pull/3719) - Rename ethprice API endpoint - [#3717](https://github.com/poanetwork/blockscout/pull/3717) - Update alpine-elixir-phoenix 1.11.3 - [#3714](https://github.com/poanetwork/blockscout/pull/3714) - Application announcements management: whole explorer, staking dapp diff --git a/apps/block_scout_web/assets/js/lib/smart_contract/common_helpers.js b/apps/block_scout_web/assets/js/lib/smart_contract/common_helpers.js new file mode 100644 index 0000000000..57de5b7477 --- /dev/null +++ b/apps/block_scout_web/assets/js/lib/smart_contract/common_helpers.js @@ -0,0 +1,87 @@ +import $ from 'jquery' + +export function getContractABI ($form) { + const implementationAbi = $form.data('implementation-abi') + const parentAbi = $form.data('contract-abi') + const $parent = $('[data-smart-contract-functions]') + const contractType = $parent.data('type') + const contractAbi = contractType === 'proxy' ? implementationAbi : parentAbi + return contractAbi +} + +export function getMethodInputs (contractAbi, functionName) { + const functionAbi = contractAbi.find(abi => + abi.name === functionName + ) + return functionAbi && functionAbi.inputs +} + +export function prepareMethodArgs ($functionInputs, inputs) { + return $.map($functionInputs, (element, ind) => { + const inputValue = $(element).val() + const inputType = inputs[ind] && inputs[ind].type + let sanitizedInputValue + sanitizedInputValue = replaceSpaces(inputValue, inputType) + sanitizedInputValue = replaceDoubleQuotes(sanitizedInputValue, inputType) + + if (isArrayInputType(inputType) || isTupleInputType(inputType)) { + if (sanitizedInputValue === '') { + return [[]] + } else { + if (sanitizedInputValue.startsWith('[') && sanitizedInputValue.endsWith(']')) { + sanitizedInputValue = sanitizedInputValue.substring(1, sanitizedInputValue.length - 1) + } + const inputValueElements = sanitizedInputValue.split(',') + const sanitizedInputValueElements = inputValueElements.map(elementValue => { + const elementInputType = inputType.split('[')[0] + return replaceDoubleQuotes(elementValue, elementInputType) + }) + return [sanitizedInputValueElements] + } + } else { return sanitizedInputValue } + }) +} + +function isArrayInputType (inputType) { + return inputType && inputType.includes('[') && inputType.includes(']') +} + +function isTupleInputType (inputType) { + return inputType.includes('tuple') && !isArrayInputType(inputType) +} + +function isAddressInputType (inputType) { + return inputType.includes('address') && !isArrayInputType(inputType) +} + +function isUintInputType (inputType) { + return inputType.includes('int') && !isArrayInputType(inputType) +} + +function isStringInputType (inputType) { + return inputType.includes('string') && !isArrayInputType(inputType) +} + +function isNonSpaceInputType (inputType) { + return isAddressInputType(inputType) || inputType.includes('int') || inputType.includes('bool') +} + +function replaceSpaces (value, type) { + if (isNonSpaceInputType(type)) { + return value.replace(/\s/g, '') + } else { + return value + } +} + +function replaceDoubleQuotes (value, type) { + if (isAddressInputType(type) || isUintInputType(type) || isStringInputType(type)) { + if (typeof value.replaceAll === 'function') { + return value.replaceAll('"', '') + } else { + return value.replace(/"/g, '') + } + } else { + return value + } +} diff --git a/apps/block_scout_web/assets/js/lib/smart_contract/functions.js b/apps/block_scout_web/assets/js/lib/smart_contract/functions.js index 6d96023b49..55312c1429 100644 --- a/apps/block_scout_web/assets/js/lib/smart_contract/functions.js +++ b/apps/block_scout_web/assets/js/lib/smart_contract/functions.js @@ -1,7 +1,6 @@ import $ from 'jquery' -import { props } from 'eth-net-props' -import { walletEnabled, connectToWallet, getCurrentAccount, shouldHideConnectButton } from './write.js' -import { openErrorModal, openWarningModal, openSuccessModal, openModalWithMessage } from '../modals.js' +import { getContractABI, getMethodInputs, prepareMethodArgs } from './common_helpers' +import { walletEnabled, connectToWallet, shouldHideConnectButton, callMethod } from './write' import '../../pages/address' const loadFunctions = (element) => { @@ -137,181 +136,6 @@ const readWriteFunction = (element) => { }) } -function callMethod (isWalletEnabled, $functionInputs, explorerChainId, $form, functionName, $element) { - if (!isWalletEnabled) { - const warningMsg = 'You haven\'t approved the reading of account list from your MetaMask or MetaMask/Nifty wallet is locked or is not installed.' - return openWarningModal('Unauthorized', warningMsg) - } - const contractAbi = getContractABI($form) - const inputs = getMethodInputs(contractAbi, functionName) - - const $functionInputsExceptTxValue = $functionInputs.filter(':not([tx-value])') - const args = prepareMethodArgs($functionInputsExceptTxValue, inputs) - - const txValue = getTxValue($functionInputs) - const contractAddress = $form.data('contract-address') - - const { chainId: walletChainIdHex } = window.ethereum - compareChainIDs(explorerChainId, walletChainIdHex) - .then(currentAccount => { - if (functionName) { - const TargetContract = new window.web3.eth.Contract(contractAbi, contractAddress) - const sendParams = { from: currentAccount, value: txValue || 0 } - const methodToCall = TargetContract.methods[functionName](...args).send(sendParams) - methodToCall - .on('error', function (error) { - openErrorModal(`Error in sending transaction for method "${functionName}"`, formatError(error), false) - }) - .on('transactionHash', function (txHash) { - onTransactionHash(txHash, $element, functionName) - }) - } else { - const txParams = { - from: currentAccount, - to: contractAddress, - value: txValue || 0 - } - window.ethereum.request({ - method: 'eth_sendTransaction', - params: [txParams] - }) - .then(function (txHash) { - onTransactionHash(txHash, $element, functionName) - }) - .catch(function (error) { - openErrorModal('Error in sending transaction for fallback method', formatError(error), false) - }) - } - }) - .catch(error => { - openWarningModal('Unauthorized', formatError(error)) - }) -} - -function getMethodInputs (contractAbi, functionName) { - const functionAbi = contractAbi.find(abi => - abi.name === functionName - ) - return functionAbi && functionAbi.inputs -} - -function prepareMethodArgs ($functionInputs, inputs) { - return $.map($functionInputs, (element, ind) => { - const inputValue = $(element).val() - const inputType = inputs[ind] && inputs[ind].type - let sanitizedInputValue - sanitizedInputValue = replaceSpaces(inputValue, inputType) - sanitizedInputValue = replaceDoubleQuotes(sanitizedInputValue, inputType) - if (isArrayInputType(inputType)) { - if (sanitizedInputValue === '') { - return [[]] - } else { - if (sanitizedInputValue.startsWith('[') && sanitizedInputValue.endsWith(']')) { - sanitizedInputValue = sanitizedInputValue.substring(1, sanitizedInputValue.length - 1) - } - const inputValueElements = sanitizedInputValue.split(',') - const sanitizedInputValueElements = inputValueElements.map(elementValue => { - const elementInputType = inputType.split('[')[0] - return replaceDoubleQuotes(elementValue, elementInputType) - }) - return [sanitizedInputValueElements] - } - } else { return sanitizedInputValue } - }) -} - -function isArrayInputType (inputType) { - return inputType && inputType.includes('[') && inputType.includes(']') -} - -function isAddressInputType (inputType) { - return inputType.includes('address') -} - -function isUintInputType (inputType) { - return inputType.includes('int') && !inputType.includes('[') -} - -function isStringInputType (inputType) { - return inputType.includes('string') && !inputType.includes('[') -} - -function isNonSpaceInputType (inputType) { - return isAddressInputType(inputType) || inputType.includes('int') || inputType.includes('bool') -} - -function getTxValue ($functionInputs) { - const WEI_MULTIPLIER = 10 ** 18 - const $txValue = $functionInputs.filter('[tx-value]:first') - const txValue = $txValue && $txValue.val() && parseFloat($txValue.val()) * WEI_MULTIPLIER - const txValueStr = txValue && txValue.toString(16) - return txValueStr -} - -function getContractABI ($form) { - const implementationAbi = $form.data('implementation-abi') - const parentAbi = $form.data('contract-abi') - const $parent = $('[data-smart-contract-functions]') - const contractType = $parent.data('type') - const contractAbi = contractType === 'proxy' ? implementationAbi : parentAbi - return contractAbi -} - -function compareChainIDs (explorerChainId, walletChainIdHex) { - if (explorerChainId !== parseInt(walletChainIdHex)) { - const networkDisplayNameFromWallet = props.getNetworkDisplayName(walletChainIdHex) - const networkDisplayName = props.getNetworkDisplayName(explorerChainId) - const errorMsg = `You connected to ${networkDisplayNameFromWallet} chain in the wallet, but the current instance of Blockscout is for ${networkDisplayName} chain` - return Promise.reject(new Error(errorMsg)) - } else { - return getCurrentAccount() - } -} - -function onTransactionHash (txHash, $element, functionName) { - openModalWithMessage($element.find('#pending-contract-write'), true, txHash) - const getTxReceipt = (txHash) => { - window.ethereum.request({ - method: 'eth_getTransactionReceipt', - params: [txHash] - }) - .then(txReceipt => { - if (txReceipt) { - const successMsg = `Successfully sent transaction for method "${functionName}"` - openSuccessModal('Success', successMsg) - clearInterval(txReceiptPollingIntervalId) - } - }) - } - const txReceiptPollingIntervalId = setInterval(() => { getTxReceipt(txHash) }, 5 * 1000) -} - -function replaceSpaces (value, type) { - if (isNonSpaceInputType(type)) { - return value.replace(/\s/g, '') - } else { - return value - } -} - -function replaceDoubleQuotes (value, type) { - if (isAddressInputType(type) || isUintInputType(type) || isStringInputType(type)) { - if (typeof value.replaceAll === 'function') { - return value.replaceAll('"', '') - } else { - return value.replace(/"/g, '') - } - } else { - return value - } -} - -const formatError = (error) => { - let { message } = error - message = message && message.split('Error: ').length > 1 ? message.split('Error: ')[1] : message - return message -} - const container = $('[data-smart-contract-functions]') if (container.length) { diff --git a/apps/block_scout_web/assets/js/lib/smart_contract/write.js b/apps/block_scout_web/assets/js/lib/smart_contract/write.js index 73e50a6f2e..14ada95f25 100644 --- a/apps/block_scout_web/assets/js/lib/smart_contract/write.js +++ b/apps/block_scout_web/assets/js/lib/smart_contract/write.js @@ -1,4 +1,7 @@ import Web3 from 'web3' +import { props } from 'eth-net-props' +import { openErrorModal, openWarningModal, openSuccessModal, openModalWithMessage } from '../modals' +import { getContractABI, getMethodInputs, prepareMethodArgs } from './common_helpers' export const walletEnabled = () => { return new Promise((resolve) => { @@ -80,3 +83,97 @@ export const shouldHideConnectButton = () => { } }) } + +export function callMethod (isWalletEnabled, $functionInputs, explorerChainId, $form, functionName, $element) { + if (!isWalletEnabled) { + const warningMsg = 'You haven\'t approved the reading of account list from your MetaMask or MetaMask/Nifty wallet is locked or is not installed.' + return openWarningModal('Unauthorized', warningMsg) + } + const contractAbi = getContractABI($form) + const inputs = getMethodInputs(contractAbi, functionName) + + const $functionInputsExceptTxValue = $functionInputs.filter(':not([tx-value])') + const args = prepareMethodArgs($functionInputsExceptTxValue, inputs) + + const txValue = getTxValue($functionInputs) + const contractAddress = $form.data('contract-address') + + const { chainId: walletChainIdHex } = window.ethereum + compareChainIDs(explorerChainId, walletChainIdHex) + .then(currentAccount => { + if (functionName) { + const TargetContract = new window.web3.eth.Contract(contractAbi, contractAddress) + const sendParams = { from: currentAccount, value: txValue || 0 } + const methodToCall = TargetContract.methods[functionName](...args).send(sendParams) + methodToCall + .on('error', function (error) { + openErrorModal(`Error in sending transaction for method "${functionName}"`, formatError(error), false) + }) + .on('transactionHash', function (txHash) { + onTransactionHash(txHash, $element, functionName) + }) + } else { + const txParams = { + from: currentAccount, + to: contractAddress, + value: txValue || 0 + } + window.ethereum.request({ + method: 'eth_sendTransaction', + params: [txParams] + }) + .then(function (txHash) { + onTransactionHash(txHash, $element, functionName) + }) + .catch(function (error) { + openErrorModal('Error in sending transaction for fallback method', formatError(error), false) + }) + } + }) + .catch(error => { + openWarningModal('Unauthorized', formatError(error)) + }) +} + +function onTransactionHash (txHash, $element, functionName) { + openModalWithMessage($element.find('#pending-contract-write'), true, txHash) + const getTxReceipt = (txHash) => { + window.ethereum.request({ + method: 'eth_getTransactionReceipt', + params: [txHash] + }) + .then(txReceipt => { + if (txReceipt) { + const successMsg = `Successfully sent transaction for method "${functionName}"` + openSuccessModal('Success', successMsg) + clearInterval(txReceiptPollingIntervalId) + } + }) + } + const txReceiptPollingIntervalId = setInterval(() => { getTxReceipt(txHash) }, 5 * 1000) +} + +const formatError = (error) => { + let { message } = error + message = message && message.split('Error: ').length > 1 ? message.split('Error: ')[1] : message + return message +} + +function getTxValue ($functionInputs) { + const WEI_MULTIPLIER = 10 ** 18 + const $txValue = $functionInputs.filter('[tx-value]:first') + const txValue = $txValue && $txValue.val() && parseFloat($txValue.val()) * WEI_MULTIPLIER + const txValueStr = txValue && txValue.toString(16) + return txValueStr +} + +function compareChainIDs (explorerChainId, walletChainIdHex) { + if (explorerChainId !== parseInt(walletChainIdHex)) { + const networkDisplayNameFromWallet = props.getNetworkDisplayName(walletChainIdHex) + const networkDisplayName = props.getNetworkDisplayName(explorerChainId) + const errorMsg = `You connected to ${networkDisplayNameFromWallet} chain in the wallet, but the current instance of Blockscout is for ${networkDisplayName} chain` + return Promise.reject(new Error(errorMsg)) + } else { + return getCurrentAccount() + } +} diff --git a/apps/block_scout_web/assets/webpack.config.js b/apps/block_scout_web/assets/webpack.config.js index 0f52fa95bb..d0d8414d43 100644 --- a/apps/block_scout_web/assets/webpack.config.js +++ b/apps/block_scout_web/assets/webpack.config.js @@ -90,7 +90,7 @@ const appJs = 'admin-tasks': './js/pages/admin/tasks.js', 'read-token-contract': './js/pages/read_token_contract.js', 'smart-contract-helpers': './js/lib/smart_contract/index.js', - 'write_contract': './js/pages/write_contract.js', + 'write-contract': './js/pages/write_contract.js', 'token-transfers-toggle': './js/lib/token_transfers_toggle.js', 'try-api': './js/lib/try_api.js', 'try-eth-api': './js/lib/try_eth_api.js', diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address_write_contract/index.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address_write_contract/index.html.eex index 34c5dcffcf..0c7874b6e9 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/address_write_contract/index.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/address_write_contract/index.html.eex @@ -16,5 +16,5 @@ - + diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address_write_proxy/index.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address_write_proxy/index.html.eex index 34c5dcffcf..0c7874b6e9 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/address_write_proxy/index.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/address_write_proxy/index.html.eex @@ -16,5 +16,5 @@ - + diff --git a/apps/block_scout_web/lib/block_scout_web/templates/smart_contract/_connect_container.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/smart_contract/_connect_container.html.eex new file mode 100644 index 0000000000..d2ad941524 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/templates/smart_contract/_connect_container.html.eex @@ -0,0 +1,13 @@ +