Contract reader/writer refactoring

pull/3736/head
Viktor Baranov 4 years ago
parent 323ddcb45c
commit 0a68609c49
  1. 2
      .dialyzer-ignore
  2. 1
      CHANGELOG.md
  3. 87
      apps/block_scout_web/assets/js/lib/smart_contract/common_helpers.js
  4. 180
      apps/block_scout_web/assets/js/lib/smart_contract/functions.js
  5. 97
      apps/block_scout_web/assets/js/lib/smart_contract/write.js
  6. 2
      apps/block_scout_web/assets/webpack.config.js
  7. 2
      apps/block_scout_web/lib/block_scout_web/templates/address_write_contract/index.html.eex
  8. 2
      apps/block_scout_web/lib/block_scout_web/templates/address_write_proxy/index.html.eex
  9. 13
      apps/block_scout_web/lib/block_scout_web/templates/smart_contract/_connect_container.html.eex
  10. 18
      apps/block_scout_web/lib/block_scout_web/templates/smart_contract/_functions.html.eex
  11. 10
      apps/block_scout_web/lib/block_scout_web/views/address_read_contract_view.ex
  12. 10
      apps/block_scout_web/lib/block_scout_web/views/address_read_proxy_view.ex
  13. 4
      apps/block_scout_web/lib/block_scout_web/views/address_view.ex
  14. 10
      apps/block_scout_web/lib/block_scout_web/views/address_write_contract_view.ex
  15. 10
      apps/block_scout_web/lib/block_scout_web/views/address_write_proxy_view.ex
  16. 27
      apps/block_scout_web/lib/block_scout_web/views/smart_contract_view.ex
  17. 3
      apps/block_scout_web/lib/block_scout_web/views/tokens/instance/overview_view.ex
  18. 3
      apps/block_scout_web/lib/block_scout_web/views/tokens/overview_view.ex
  19. 22
      apps/block_scout_web/priv/gettext/default.pot
  20. 27
      apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po
  21. 19
      apps/block_scout_web/test/block_scout_web/views/address_read_contract_view_test.exs
  22. 19
      apps/block_scout_web/test/block_scout_web/views/address_read_proxy_view_test.exs
  23. 19
      apps/block_scout_web/test/block_scout_web/views/address_write_contract_view_test.exs
  24. 19
      apps/block_scout_web/test/block_scout_web/views/address_write_proxy_view_test copy.exs
  25. 129
      apps/block_scout_web/test/block_scout_web/views/tokens/smart_contract_view_test.exs
  26. 24
      apps/explorer/lib/explorer/smart_contract/helper.ex
  27. 5
      apps/explorer/lib/explorer/smart_contract/reader.ex
  28. 14
      apps/explorer/lib/explorer/smart_contract/writer.ex
  29. 127
      apps/explorer/test/explorer/smart_contract/helper_test.exs

@ -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

@ -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

@ -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
}
}

@ -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 <a href="/tx/${txHash}">transaction</a> 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) {

@ -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 <a href="/tx/${txHash}">transaction</a> 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()
}
}

@ -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',

@ -16,5 +16,5 @@
</div>
</div>
<script defer data-cfasync="false" src="<%= static_path(@conn, "/js/smart-contract-helpers.js") %>"></script>
<script defer data-cfasync="false" src="<%= static_path(@conn, "/js/write_contract.js") %>"></script>
<script defer data-cfasync="false" src="<%= static_path(@conn, "/js/write-contract.js") %>"></script>
</section>

@ -16,5 +16,5 @@
</div>
</div>
<script defer data-cfasync="false" src="<%= static_path(@conn, "/js/smart-contract-helpers.js") %>"></script>
<script defer data-cfasync="false" src="<%= static_path(@conn, "/js/write_contract.js") %>"></script>
<script defer data-cfasync="false" src="<%= static_path(@conn, "/js/write-contract.js") %>"></script>
</section>

@ -0,0 +1,13 @@
<div connect-to class="connect-container">
<span style="margin-top: -2px;" class="mr-2">
<%= render BlockScoutWeb.IconsView, "_inactive_icon.html" %>
</span>
<h2 style="margin-top: -2px;">Disconnected</h2>
<button connect-metamask class="button btn-line ml-4">Connect to Metamask</button>
</div>
<div connected-to class="connect-container hidden">
<span style="margin-top: -2px;" class="mr-2">
<%= render BlockScoutWeb.IconsView, "_active_icon.html" %>
</span>
<h2 style="margin-top: -2px;">Connected to</h2><h3 connected-to-address class="ml-2"></h3>
</div>

@ -12,19 +12,7 @@ to: address_contract_path(@conn, :index, metadata_for_verification.address_hash)
<% end %>
<% end %>
<%= if @action== "write" do %>
<div connect-to class="connect-container">
<span style="margin-top: -2px;" class="mr-2">
<%= render BlockScoutWeb.IconsView, "_inactive_icon.html" %>
</span>
<h2 style="margin-top: -2px;">Disconnected</h2>
<button connect-metamask class="button btn-line ml-4">Connect to Metamask</button>
</div>
<div connected-to class="connect-container hidden">
<span style="margin-top: -2px;" class="mr-2">
<%= render BlockScoutWeb.IconsView, "_active_icon.html" %>
</span>
<h2 style="margin-top: -2px;">Connected to</h2><h3 connected-to-address class="ml-2"></h3>
</div>
<%= render BlockScoutWeb.SmartContractView, "_connect_container.html" %>
<% end %>
<%= if @contract_type == "proxy" do %>
<div class="implementation-container">
@ -52,7 +40,7 @@ to: address_contract_path(@conn, :index, metadata_for_verification.address_hash)
end
%>
<%= render BlockScoutWeb.SmartContractView, "_pending_contract_write.html" %>
<form class="form-inline" data-function-form data-action="<%= if writable?(function), do: :write, else: :read %>" data-type="<%= @contract_type %>" data-url="<%= smart_contract_path(@conn, :show, @address.hash) %>" data-contract-address="<%= @address.hash %>" data-contract-abi="<%= @contract_abi %>" data-implementation-abi="<%= @implementation_abi %>" data-chain-id="<%= Explorer.Chain.Cache.NetVersion.get_version() %>">
<form class="form-inline" data-function-form data-action="<%= if writable?(function), do: :write, else: :read %>" data-type="<%= @contract_type %>" data-url="<%= smart_contract_path(@conn, :show, Address.checksum(@address.hash)) %>" data-contract-address="<%= @address.hash %>" data-contract-abi="<%= @contract_abi %>" data-implementation-abi="<%= @implementation_abi %>" data-chain-id="<%= Explorer.Chain.Cache.NetVersion.get_version() %>">
<input type="hidden" name="function_name" value='<%= function["name"] %>' />
<input type="hidden" name="method_id" value='<%= function["method_id"] %>' />
@ -80,7 +68,7 @@ to: address_contract_path(@conn, :index, metadata_for_verification.address_hash)
<% end %>
<% end %>
<%= if payable?(function) do %>
<%= if Helper.payable?(function) do %>
<div class="form-group pr-3">
<input type="text" name="function_input" tx-value class="form-control form-control-sm address-input-sm mt-2" placeholder='value(<%= gettext("ETH")%>)' />
</div>

@ -1,13 +1,3 @@
defmodule BlockScoutWeb.AddressReadContractView do
use BlockScoutWeb, :view
def queryable?(inputs) when not is_nil(inputs), do: Enum.any?(inputs)
def queryable?(inputs) when is_nil(inputs), do: false
def outputs?(outputs) when not is_nil(outputs), do: Enum.any?(outputs)
def outputs?(outputs) when is_nil(outputs), do: false
def address?(type), do: type == "address"
end

@ -1,13 +1,3 @@
defmodule BlockScoutWeb.AddressReadProxyView do
use BlockScoutWeb, :view
def queryable?(inputs) when not is_nil(inputs), do: Enum.any?(inputs)
def queryable?(inputs) when is_nil(inputs), do: false
def outputs?(outputs) when not is_nil(outputs), do: Enum.any?(outputs)
def outputs?(outputs) when is_nil(outputs), do: false
def address?(type), do: type == "address"
end

@ -8,7 +8,7 @@ defmodule BlockScoutWeb.AddressView do
alias Explorer.Chain.{Address, Hash, InternalTransaction, SmartContract, Token, TokenTransfer, Transaction, Wei}
alias Explorer.Chain.Block.Reward
alias Explorer.ExchangeRates.Token, as: TokenExchangeRate
alias Explorer.SmartContract.Writer
alias Explorer.SmartContract.{Helper, Writer}
@dialyzer :no_match
@ -229,7 +229,7 @@ defmodule BlockScoutWeb.AddressView do
def smart_contract_verified?(%Address{smart_contract: nil}), do: false
def smart_contract_with_read_only_functions?(%Address{smart_contract: %SmartContract{}} = address) do
Enum.any?(address.smart_contract.abi, &(&1["constant"] || &1["stateMutability"] == "view"))
Enum.any?(address.smart_contract.abi, &Helper.queriable_method?(&1))
end
def smart_contract_with_read_only_functions?(%Address{smart_contract: nil}), do: false

@ -1,13 +1,3 @@
defmodule BlockScoutWeb.AddressWriteContractView do
use BlockScoutWeb, :view
def queryable?(inputs) when not is_nil(inputs), do: Enum.any?(inputs)
def queryable?(inputs) when is_nil(inputs), do: false
def outputs?(outputs) when not is_nil(outputs), do: Enum.any?(outputs)
def outputs?(outputs) when is_nil(outputs), do: false
def address?(type), do: type == "address"
end

@ -1,13 +1,3 @@
defmodule BlockScoutWeb.AddressWriteProxyView do
use BlockScoutWeb, :view
def queryable?(inputs) when not is_nil(inputs), do: Enum.any?(inputs)
def queryable?(inputs) when is_nil(inputs), do: false
def outputs?(outputs) when not is_nil(outputs), do: Enum.any?(outputs)
def outputs?(outputs) when is_nil(outputs), do: false
def address?(type), do: type == "address"
end

@ -2,7 +2,9 @@ defmodule BlockScoutWeb.SmartContractView do
use BlockScoutWeb, :view
alias Explorer.Chain
alias Explorer.Chain.Hash.Address
alias Explorer.Chain.Address
alias Explorer.Chain.Hash.Address, as: HashAddress
alias Explorer.SmartContract.Helper
def queryable?(inputs) when not is_nil(inputs), do: Enum.any?(inputs)
@ -10,8 +12,8 @@ defmodule BlockScoutWeb.SmartContractView do
def writable?(function) when not is_nil(function),
do:
!constructor?(function) && !event?(function) &&
(payable?(function) || nonpayable?(function))
!Helper.constructor?(function) && !Helper.event?(function) &&
(Helper.payable?(function) || Helper.nonpayable?(function))
def writable?(function) when is_nil(function), do: false
@ -19,21 +21,6 @@ defmodule BlockScoutWeb.SmartContractView do
def outputs?(outputs) when is_nil(outputs), do: false
defp event?(function), do: function["type"] == "event"
defp constructor?(function), do: function["type"] == "constructor"
def payable?(function), do: function["stateMutability"] == "payable" || function["payable"]
def nonpayable?(function) do
if function["type"] do
function["stateMutability"] == "nonpayable" ||
(!function["payable"] && !function["constant"] && !function["stateMutability"])
else
false
end
end
def address?(type), do: type in ["address", "address payable"]
def int?(type), do: String.contains?(type, "int") && !String.contains?(type, "[")
@ -94,7 +81,7 @@ defmodule BlockScoutWeb.SmartContractView do
end
def values_with_type(value, type, _components) when type in [:address, "address", "address payable"] do
case Address.cast(value) do
case HashAddress.cast(value) do
{:ok, address} ->
render_type_value("address", to_string(address))
@ -167,7 +154,7 @@ defmodule BlockScoutWeb.SmartContractView do
end
def values_only(value, type, _components) when type in [:address, "address", "address payable"] do
{:ok, address} = Address.cast(value)
{:ok, address} = HashAddress.cast(value)
to_string(address)
end

@ -3,6 +3,7 @@ defmodule BlockScoutWeb.Tokens.Instance.OverviewView do
alias BlockScoutWeb.CurrencyHelpers
alias Explorer.Chain.{Address, SmartContract, Token}
alias Explorer.SmartContract.Helper
import BlockScoutWeb.APIDocsView, only: [blockscout_url: 1, blockscout_url: 2]
@ -68,7 +69,7 @@ defmodule BlockScoutWeb.Tokens.Instance.OverviewView do
def smart_contract_with_read_only_functions?(
%Token{contract_address: %Address{smart_contract: %SmartContract{}}} = token
) do
Enum.any?(token.contract_address.smart_contract.abi, &(&1["constant"] || &1["stateMutability"] == "view"))
Enum.any?(token.contract_address.smart_contract.abi, &Helper.queriable_method?(&1))
end
def smart_contract_with_read_only_functions?(%Token{contract_address: %Address{smart_contract: nil}}), do: false

@ -3,6 +3,7 @@ defmodule BlockScoutWeb.Tokens.OverviewView do
alias Explorer.{Chain, CustomContractsHelpers}
alias Explorer.Chain.{Address, SmartContract, Token}
alias Explorer.SmartContract.Helper
alias BlockScoutWeb.{AccessHelpers, CurrencyHelpers, LayoutView}
@ -48,7 +49,7 @@ defmodule BlockScoutWeb.Tokens.OverviewView do
def smart_contract_with_read_only_functions?(
%Token{contract_address: %Address{smart_contract: %SmartContract{}}} = token
) do
Enum.any?(token.contract_address.smart_contract.abi, &(&1["constant"] || &1["stateMutability"] == "view"))
Enum.any?(token.contract_address.smart_contract.abi, &Helper.queriable_method?(&1))
end
def smart_contract_with_read_only_functions?(%Token{contract_address: %Address{smart_contract: nil}}), do: false

@ -555,8 +555,8 @@ msgid "ERC-721 "
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:85
#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:120
#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:73
#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:108
msgid "ETH"
msgstr ""
@ -868,7 +868,7 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/tokens/holder/index.html.eex:15
#: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:9
#: lib/block_scout_web/views/tokens/overview_view.ex:41
#: lib/block_scout_web/views/tokens/overview_view.ex:42
msgid "Token Holders"
msgstr ""
@ -894,8 +894,8 @@ msgstr ""
#: lib/block_scout_web/templates/transaction/_tabs.html.eex:4
#: lib/block_scout_web/templates/transaction_token_transfer/index.html.eex:7
#: lib/block_scout_web/views/address_view.ex:346
#: lib/block_scout_web/views/tokens/instance/overview_view.ex:124
#: lib/block_scout_web/views/tokens/overview_view.ex:40
#: lib/block_scout_web/views/tokens/instance/overview_view.ex:125
#: lib/block_scout_web/views/tokens/overview_view.ex:41
#: lib/block_scout_web/views/transaction_view.ex:405
msgid "Token Transfers"
msgstr ""
@ -944,7 +944,7 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/tokens/inventory/index.html.eex:15
#: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:17
#: lib/block_scout_web/views/tokens/overview_view.ex:43
#: lib/block_scout_web/views/tokens/overview_view.ex:44
msgid "Inventory"
msgstr ""
@ -1187,7 +1187,7 @@ msgid "QR Code"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:89
#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:77
msgid "Query"
msgstr ""
@ -1640,7 +1640,7 @@ msgid "View transaction %{transaction} on %{subnetwork}"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:119
#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:107
msgid "WEI"
msgstr ""
@ -1775,7 +1775,7 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/tokens/instance/metadata/index.html.eex:18
#: lib/block_scout_web/templates/tokens/instance/overview/_tabs.html.eex:10
#: lib/block_scout_web/views/tokens/instance/overview_view.ex:125
#: lib/block_scout_web/views/tokens/instance/overview_view.ex:126
msgid "Metadata"
msgstr ""
@ -1852,7 +1852,7 @@ msgstr ""
#: lib/block_scout_web/templates/address/_tabs.html.eex:81
#: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:25
#: lib/block_scout_web/views/address_view.ex:349
#: lib/block_scout_web/views/tokens/overview_view.ex:42
#: lib/block_scout_web/views/tokens/overview_view.ex:43
msgid "Read Contract"
msgstr ""
@ -1931,7 +1931,7 @@ msgid "Waiting for transaction's confirmation..."
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:89
#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:77
msgid "Write"
msgstr ""

@ -555,8 +555,8 @@ msgid "ERC-721 "
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:85
#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:120
#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:73
#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:108
msgid "ETH"
msgstr ""
@ -868,7 +868,7 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/tokens/holder/index.html.eex:15
#: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:9
#: lib/block_scout_web/views/tokens/overview_view.ex:41
#: lib/block_scout_web/views/tokens/overview_view.ex:42
msgid "Token Holders"
msgstr ""
@ -894,8 +894,8 @@ msgstr ""
#: lib/block_scout_web/templates/transaction/_tabs.html.eex:4
#: lib/block_scout_web/templates/transaction_token_transfer/index.html.eex:7
#: lib/block_scout_web/views/address_view.ex:346
#: lib/block_scout_web/views/tokens/instance/overview_view.ex:124
#: lib/block_scout_web/views/tokens/overview_view.ex:40
#: lib/block_scout_web/views/tokens/instance/overview_view.ex:125
#: lib/block_scout_web/views/tokens/overview_view.ex:41
#: lib/block_scout_web/views/transaction_view.ex:405
msgid "Token Transfers"
msgstr ""
@ -944,7 +944,7 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/tokens/inventory/index.html.eex:15
#: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:17
#: lib/block_scout_web/views/tokens/overview_view.ex:43
#: lib/block_scout_web/views/tokens/overview_view.ex:44
msgid "Inventory"
msgstr ""
@ -1187,7 +1187,7 @@ msgid "QR Code"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:89
#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:77
msgid "Query"
msgstr ""
@ -1640,7 +1640,7 @@ msgid "View transaction %{transaction} on %{subnetwork}"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:119
#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:107
msgid "WEI"
msgstr ""
@ -1775,7 +1775,7 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/tokens/instance/metadata/index.html.eex:18
#: lib/block_scout_web/templates/tokens/instance/overview/_tabs.html.eex:10
#: lib/block_scout_web/views/tokens/instance/overview_view.ex:125
#: lib/block_scout_web/views/tokens/instance/overview_view.ex:126
msgid "Metadata"
msgstr ""
@ -1852,7 +1852,7 @@ msgstr ""
#: lib/block_scout_web/templates/address/_tabs.html.eex:81
#: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:25
#: lib/block_scout_web/views/address_view.ex:349
#: lib/block_scout_web/views/tokens/overview_view.ex:42
#: lib/block_scout_web/views/tokens/overview_view.ex:43
msgid "Read Contract"
msgstr ""
@ -1931,7 +1931,7 @@ msgid "Waiting for transaction's confirmation..."
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:89
#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:77
msgid "Write"
msgstr ""
@ -2702,11 +2702,6 @@ msgstr ""
msgid "Swap STAKE on Honeyswap"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/stakes/_stakes_top.html.eex:34
msgid "Bridge to Ethereum"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/stakes/_stakes_top.html.eex:52
msgid "Trade STAKE on BitMax"

@ -1,19 +0,0 @@
defmodule BlockScoutWeb.AddressReadContractViewTest do
use BlockScoutWeb.ConnCase, async: true
alias BlockScoutWeb.AddressReadContractView
describe "queryable?/1" do
test "returns true if list of inputs is not empty" do
assert AddressReadContractView.queryable?([%{"name" => "argument_name", "type" => "uint256"}]) == true
assert AddressReadContractView.queryable?([]) == false
end
end
describe "address?/1" do
test "returns true if type equals `address`" do
assert AddressReadContractView.address?("address") == true
assert AddressReadContractView.address?("uint256") == false
end
end
end

@ -1,19 +0,0 @@
defmodule BlockScoutWeb.AddressReadProxyViewTest do
use BlockScoutWeb.ConnCase, async: true
alias BlockScoutWeb.AddressReadProxyView
describe "queryable?/1" do
test "returns true if list of inputs is not empty" do
assert AddressReadProxyView.queryable?([%{"name" => "argument_name", "type" => "uint256"}]) == true
assert AddressReadProxyView.queryable?([]) == false
end
end
describe "address?/1" do
test "returns true if type equals `address`" do
assert AddressReadProxyView.address?("address") == true
assert AddressReadProxyView.address?("uint256") == false
end
end
end

@ -1,19 +0,0 @@
defmodule BlockScoutWeb.AddressWriteContractViewTest do
use BlockScoutWeb.ConnCase, async: true
alias BlockScoutWeb.AddressWriteContractView
describe "queryable?/1" do
test "returns true if list of inputs is not empty" do
assert AddressWriteContractView.queryable?([%{"name" => "argument_name", "type" => "uint256"}]) == true
assert AddressWriteContractView.queryable?([]) == false
end
end
describe "address?/1" do
test "returns true if type equals `address`" do
assert AddressWriteContractView.address?("address") == true
assert AddressWriteContractView.address?("uint256") == false
end
end
end

@ -1,19 +0,0 @@
defmodule BlockScoutWeb.AddressWriteProxyViewTest do
use BlockScoutWeb.ConnCase, async: true
alias BlockScoutWeb.AddressWriteProxyView
describe "queryable?/1" do
test "returns true if list of inputs is not empty" do
assert AddressWriteProxyView.queryable?([%{"name" => "argument_name", "type" => "uint256"}]) == true
assert AddressWriteProxyView.queryable?([]) == false
end
end
describe "address?/1" do
test "returns true if type equals `address`" do
assert AddressWriteProxyView.address?("address") == true
assert AddressWriteProxyView.address?("uint256") == false
end
end
end

@ -15,6 +15,11 @@ defmodule BlockScoutWeb.SmartContractViewTest do
refute SmartContractView.queryable?(inputs)
end
test "returns true if list of inputs is not empty" do
assert SmartContractView.queryable?([%{"name" => "argument_name", "type" => "uint256"}]) == true
assert SmartContractView.queryable?([]) == false
end
end
describe "writable?" do
@ -73,128 +78,12 @@ defmodule BlockScoutWeb.SmartContractViewTest do
end
end
describe "payable?" do
test "returns true when there is payable function" do
function = %{
"type" => "function",
"stateMutability" => "payable",
"payable" => true,
"outputs" => [],
"name" => "upgradeToAndCall",
"inputs" => [
%{"type" => "uint256", "name" => "version"},
%{"type" => "address", "name" => "implementation"},
%{"type" => "bytes", "name" => "data"}
],
"constant" => false
}
assert SmartContractView.payable?(function)
end
test "returns true when there is old-style payable function" do
function = %{
"type" => "function",
"payable" => true,
"outputs" => [],
"name" => "upgradeToAndCall",
"inputs" => [
%{"type" => "uint256", "name" => "version"},
%{"type" => "address", "name" => "implementation"},
%{"type" => "bytes", "name" => "data"}
],
"constant" => false
}
assert SmartContractView.payable?(function)
end
test "returns false when it is nonpayable function" do
function = %{
"type" => "function",
"stateMutability" => "nonpayable",
"payable" => false,
"outputs" => [],
"name" => "transferProxyOwnership",
"inputs" => [%{"type" => "address", "name" => "newOwner"}],
"constant" => false
}
refute SmartContractView.payable?(function)
end
test "returns false when there is no function" do
function = %{}
refute SmartContractView.payable?(function)
end
test "returns false when function is nil" do
function = nil
refute SmartContractView.payable?(function)
end
end
describe "nonpayable?" do
test "returns true when there is nonpayable function" do
function = %{
"type" => "function",
"stateMutability" => "nonpayable",
"payable" => false,
"outputs" => [],
"name" => "transferProxyOwnership",
"inputs" => [%{"type" => "address", "name" => "newOwner"}],
"constant" => false
}
assert SmartContractView.nonpayable?(function)
end
test "returns true when there is old-style nonpayable function" do
function = %{
"type" => "function",
"outputs" => [],
"name" => "test",
"inputs" => [%{"type" => "address", "name" => "newOwner"}],
"constant" => false
}
assert SmartContractView.nonpayable?(function)
end
test "returns false when it is payable function" do
function = %{
"type" => "function",
"stateMutability" => "payable",
"payable" => true,
"outputs" => [],
"name" => "upgradeToAndCall",
"inputs" => [
%{"type" => "uint256", "name" => "version"},
%{"type" => "address", "name" => "implementation"},
%{"type" => "bytes", "name" => "data"}
],
"constant" => false
}
refute SmartContractView.nonpayable?(function)
end
test "returns true when there is no function" do
function = %{}
refute SmartContractView.nonpayable?(function)
end
test "returns false when function is nil" do
function = nil
refute SmartContractView.nonpayable?(function)
describe "address?" do
test "returns true if type equals `address`" do
assert SmartContractView.address?("address") == true
assert SmartContractView.address?("uint256") == false
end
end
describe "address?" do
test "returns true when the type is equal to the string 'address'" do
type = "address"

@ -0,0 +1,24 @@
defmodule Explorer.SmartContract.Helper do
@moduledoc """
SmartContract helper functions
"""
def queriable_method?(method) do
method["constant"] || method["stateMutability"] == "view"
end
def constructor?(function), do: function["type"] == "constructor"
def event?(function), do: function["type"] == "event"
def payable?(function), do: function["stateMutability"] == "payable" || function["payable"]
def nonpayable?(function) do
if function["type"] do
function["stateMutability"] == "nonpayable" ||
(!function["payable"] && !function["constant"] && !function["stateMutability"])
else
false
end
end
end

@ -9,6 +9,7 @@ defmodule Explorer.SmartContract.Reader do
alias EthereumJSONRPC.Contract
alias Explorer.Chain
alias Explorer.Chain.{Hash, SmartContract}
alias Explorer.SmartContract.Helper
@typedoc """
Map of functions to call with the values for the function to be called with.
@ -203,7 +204,7 @@ defmodule Explorer.SmartContract.Reader do
abi_with_method_id = get_abi_with_method_id(abi)
abi_with_method_id
|> Enum.filter(&(&1["constant"] || &1["stateMutability"] == "view"))
|> Enum.filter(&Helper.queriable_method?(&1))
|> Enum.map(&fetch_current_value_from_blockchain(&1, abi_with_method_id, contract_address_hash))
end
end
@ -219,7 +220,7 @@ defmodule Explorer.SmartContract.Reader do
implementation_abi_with_method_id = get_abi_with_method_id(implementation_abi)
implementation_abi_with_method_id
|> Enum.filter(&(&1["constant"] || &1["stateMutability"] == "view"))
|> Enum.filter(&Helper.queriable_method?(&1))
|> Enum.map(&fetch_current_value_from_blockchain(&1, implementation_abi_with_method_id, contract_address_hash))
end
end

@ -4,6 +4,7 @@ defmodule Explorer.SmartContract.Writer do
"""
alias Explorer.Chain
alias Explorer.SmartContract.Helper
@spec write_functions(Hash.t()) :: [%{}]
def write_functions(contract_address_hash) do
@ -37,21 +38,12 @@ defmodule Explorer.SmartContract.Writer do
end
def write_function?(function) do
!event?(function) && !constructor?(function) &&
(payable?(function) || nonpayable?(function))
!Helper.event?(function) && !Helper.constructor?(function) &&
(Helper.payable?(function) || Helper.nonpayable?(function))
end
defp filter_write_functions(abi) do
abi
|> Enum.filter(&write_function?(&1))
end
defp event?(function), do: function["type"] == "event"
defp constructor?(function), do: function["type"] == "constructor"
defp payable?(function), do: function["stateMutability"] == "payable" || function["payable"]
defp nonpayable?(function),
do:
function["stateMutability"] == "nonpayable" ||
(!function["payable"] && !function["constant"] && !function["stateMutability"])
end

@ -0,0 +1,127 @@
defmodule Explorer.SmartContract.HelperTest do
use ExUnit.Case, async: true
use Explorer.DataCase
alias Explorer.SmartContract.Helper
describe "payable?" do
test "returns true when there is payable function" do
function = %{
"type" => "function",
"stateMutability" => "payable",
"payable" => true,
"outputs" => [],
"name" => "upgradeToAndCall",
"inputs" => [
%{"type" => "uint256", "name" => "version"},
%{"type" => "address", "name" => "implementation"},
%{"type" => "bytes", "name" => "data"}
],
"constant" => false
}
assert Helper.payable?(function)
end
test "returns true when there is old-style payable function" do
function = %{
"type" => "function",
"payable" => true,
"outputs" => [],
"name" => "upgradeToAndCall",
"inputs" => [
%{"type" => "uint256", "name" => "version"},
%{"type" => "address", "name" => "implementation"},
%{"type" => "bytes", "name" => "data"}
],
"constant" => false
}
assert Helper.payable?(function)
end
test "returns false when it is nonpayable function" do
function = %{
"type" => "function",
"stateMutability" => "nonpayable",
"payable" => false,
"outputs" => [],
"name" => "transferProxyOwnership",
"inputs" => [%{"type" => "address", "name" => "newOwner"}],
"constant" => false
}
refute Helper.payable?(function)
end
test "returns false when there is no function" do
function = %{}
refute Helper.payable?(function)
end
test "returns false when function is nil" do
function = nil
refute Helper.payable?(function)
end
end
describe "nonpayable?" do
test "returns true when there is nonpayable function" do
function = %{
"type" => "function",
"stateMutability" => "nonpayable",
"payable" => false,
"outputs" => [],
"name" => "transferProxyOwnership",
"inputs" => [%{"type" => "address", "name" => "newOwner"}],
"constant" => false
}
assert Helper.nonpayable?(function)
end
test "returns true when there is old-style nonpayable function" do
function = %{
"type" => "function",
"outputs" => [],
"name" => "test",
"inputs" => [%{"type" => "address", "name" => "newOwner"}],
"constant" => false
}
assert Helper.nonpayable?(function)
end
test "returns false when it is payable function" do
function = %{
"type" => "function",
"stateMutability" => "payable",
"payable" => true,
"outputs" => [],
"name" => "upgradeToAndCall",
"inputs" => [
%{"type" => "uint256", "name" => "version"},
%{"type" => "address", "name" => "implementation"},
%{"type" => "bytes", "name" => "data"}
],
"constant" => false
}
refute Helper.nonpayable?(function)
end
test "returns true when there is no function" do
function = %{}
refute Helper.nonpayable?(function)
end
test "returns false when function is nil" do
function = nil
refute Helper.nonpayable?(function)
end
end
end
Loading…
Cancel
Save