Write contracts: a new tab with the list of write methods

pull/3161/head
Victor Baranov 4 years ago
parent f77ff79a36
commit fe78cbfe39
  1. 1
      .dialyzer-ignore
  2. 1
      CHANGELOG.md
  3. 4
      apps/block_scout_web/assets/css/_helpers.scss
  4. 38
      apps/block_scout_web/assets/css/components/_modal.scss
  5. 23
      apps/block_scout_web/assets/css/components/_modal_status.scss
  6. 225
      apps/block_scout_web/assets/js/lib/modals.js
  7. 99
      apps/block_scout_web/assets/js/lib/smart_contract/functions.js
  8. 2
      apps/block_scout_web/assets/js/lib/smart_contract/index.js
  9. 54
      apps/block_scout_web/assets/js/lib/smart_contract/read_only_functions.js
  10. 29
      apps/block_scout_web/assets/js/lib/smart_contract/write.js
  11. 1
      apps/block_scout_web/assets/js/pages/write_contract.js
  12. 2159
      apps/block_scout_web/assets/package-lock.json
  13. 3
      apps/block_scout_web/assets/package.json
  14. 1
      apps/block_scout_web/assets/webpack.config.js
  15. 1
      apps/block_scout_web/lib/block_scout_web/controllers/address_read_contract_controller.ex
  16. 1
      apps/block_scout_web/lib/block_scout_web/controllers/address_read_proxy_controller.ex
  17. 45
      apps/block_scout_web/lib/block_scout_web/controllers/address_write_contract_controlle.ex
  18. 20
      apps/block_scout_web/lib/block_scout_web/controllers/smart_contract_controller.ex
  19. 7
      apps/block_scout_web/lib/block_scout_web/templates/address/_tabs.html.eex
  20. 2
      apps/block_scout_web/lib/block_scout_web/templates/address_read_contract/index.html.eex
  21. 2
      apps/block_scout_web/lib/block_scout_web/templates/address_read_proxy/index.html.eex
  22. 1
      apps/block_scout_web/lib/block_scout_web/templates/address_write_contract/_metatags.html.eex
  23. 20
      apps/block_scout_web/lib/block_scout_web/templates/address_write_contract/index.html.eex
  24. 31
      apps/block_scout_web/lib/block_scout_web/templates/common_components/_modal_status.html.eex
  25. 23
      apps/block_scout_web/lib/block_scout_web/templates/smart_contract/_functions.html.eex
  26. 8
      apps/block_scout_web/lib/block_scout_web/views/address_read_contract_view.ex
  27. 8
      apps/block_scout_web/lib/block_scout_web/views/address_read_proxy_view.ex
  28. 13
      apps/block_scout_web/lib/block_scout_web/views/address_view.ex
  29. 13
      apps/block_scout_web/lib/block_scout_web/views/address_write_contract_view.ex
  30. 17
      apps/block_scout_web/lib/block_scout_web/views/smart_contract_view.ex
  31. 7
      apps/block_scout_web/lib/block_scout_web/web_router.ex
  32. 46
      apps/block_scout_web/priv/gettext/default.pot
  33. 46
      apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po
  34. 16
      apps/block_scout_web/test/block_scout_web/controllers/smart_contract_controller_test.exs
  35. 2
      apps/explorer/lib/explorer/smart_contract/reader.ex
  36. 28
      apps/explorer/lib/explorer/smart_contract/writer.ex

@ -1,6 +1,7 @@
:0: Unknown function 'Elixir.ExUnit.Callbacks':'__merge__'/3
:0: Unknown function 'Elixir.ExUnit.CaseTemplate':'__proxy__'/2
:0: Unknown type 'Elixir.Map':t/0
:0: Unknown type 'Elixir.Hash':t/0
apps/ethereum_jsonrpc/lib/ethereum_jsonrpc.ex:400: Function timestamp_to_datetime/1 has no local return
lib/explorer/repo/prometheus_logger.ex:8
lib/block_scout_web/views/layout_view.ex:175

@ -1,6 +1,7 @@
## Current
### Features
- [#3160](https://github.com/poanetwork/blockscout/pull/3160) - Write contracts feature
- [#3157](https://github.com/poanetwork/blockscout/pull/3157) - Read methods of implementation on proxy contract
### Fixes

@ -29,3 +29,7 @@
height: 0;
visibility: hidden;
}
.hidden {
display: none!important;
}

@ -18,6 +18,10 @@
right: -35px;
top: -35px;
outline: none !important;
path {
fill: #F6F7F9;
}
}
.close {
@ -55,3 +59,37 @@
}
}
}
.modal-fullwidth-xs {
@include media-breakpoint-down(xs) {
padding-right: 0 !important;
.modal-dialog {
max-width: initial;
min-width: initial;
margin: 0.5rem 0;
}
.modal-content {
border-radius: 0;
> div {
border-radius: 0;
}
}
.close.close-modal {
right: 10px;
top: 5px;
path {
fill: #a3a9b5;
}
}
.modal-bottom-disclaimer {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
}
}

@ -15,8 +15,8 @@ $modal-status-graph-question: #329ae9 !default;
.modal-status-graph {
align-items: center;
border-top-left-radius: $modal-border-radius;
border-top-right-radius: $modal-border-radius;
border-top-left-radius: 8px;
border-top-right-radius: 8px;
display: flex;
height: 135px;
justify-content: center;
@ -66,6 +66,18 @@ $modal-status-graph-question: #329ae9 !default;
line-height: 1.5;
margin: 0 0 25px;
text-align: center;
&.m-b-0 {
margin-bottom: 0;
}
.link-helptip {
border-bottom-width: 1px;
border-bottom-style: dotted;
color: inherit;
cursor: help;
text-decoration: none;
}
}
.modal-status-button-wrapper {
@ -76,6 +88,13 @@ $modal-status-graph-question: #329ae9 !default;
.btn-line {
flex-grow: 1;
margin-right: 20px;
border-color: $primary;
color: $primary;
&:hover {
background-color: $primary;
color: $additional-font;
}
&:last-child {
margin-right: 0;

@ -1,75 +1,186 @@
import $ from 'jquery'
$(function () {
$('.js-become-candidate').on('click', function () {
$('#becomeCandidateModal').modal()
})
let $currentModal = null
let modalLocked = false
$('.js-validator-info-modal').on('click', function () {
$('#validatorInfoModal').modal()
})
const spinner =
`
<span class="loading-spinner-small mr-2">
<span class="loading-spinner-block-1"></span>
<span class="loading-spinner-block-2"></span>
</span>
`
$('.js-move-stake').on('click', function () {
$('#errorStatusModal').modal()
})
$(document.body).on('hide.bs.modal', e => {
if (modalLocked) {
e.preventDefault()
e.stopPropagation()
return false
}
$('.js-remove-pool').on('click', function () {
$('#warningStatusModal').modal()
$currentModal = null
})
$('.js-copy-address').on('click', function () {
$('#successStatusModal').modal()
export function currentModal () {
return $currentModal
}
export function openModal ($modal, unclosable) {
// Hide all tooltips before showing a modal,
// since they are sticking on top of modal
$('.tooltip').tooltip('hide')
if (unclosable) {
$('.close-modal, .modal-status-button-wrapper', $modal).addClass('hidden')
$('.modal-status-text', $modal).addClass('m-b-0')
}
if ($currentModal) {
modalLocked = false
$currentModal
.one('hidden.bs.modal', () => {
$modal.modal('show')
$currentModal = $modal
if (unclosable) {
modalLocked = true
}
})
.modal('hide')
} else {
$modal.modal('show')
$currentModal = $modal
if (unclosable) {
modalLocked = true
}
}
}
export function lockModal ($modal, $submitButton = null, spinnerText = '') {
$modal.find('.close-modal').attr('disabled', true)
const $button = $submitButton || $modal.find('.btn-add-full')
$button
.attr('data-text', $button.text())
.attr('disabled', true)
const $span = $('span', $button)
const waitHtml = spinner + (spinnerText ? ` ${spinnerText}` : '')
if ($span.length) {
$('svg', $button).hide()
$span.html(waitHtml)
} else {
$button.html(waitHtml)
}
modalLocked = true
}
$('.js-stake-stake').on('click', function () {
const modal = '#stakeModal'
const progress = parseInt($(`${modal} .js-stakes-progress-data-progress`).text())
const total = parseInt($(`${modal} .js-stakes-progress-data-total`).text())
export function unlockModal ($modal, $submitButton = null) {
$modal.find('.close-modal').attr('disabled', false)
$(modal).modal()
const $button = $submitButton || $modal.find('.btn-add-full')
const buttonText = $button.attr('data-text')
setupStakesProgress(progress, total, modal)
$button.attr('disabled', false)
const $span = $('span', $button)
if ($span.length) {
$('svg', $button).show()
$span.text(buttonText)
} else {
$button.text(buttonText)
}
modalLocked = false
}
export function openErrorModal (title, text, unclosable) {
const $modal = $('#errorStatusModal')
$modal.find('.modal-status-title').text(title)
$modal.find('.modal-status-text').html(text)
openModal($modal, unclosable)
}
export function openWarningModal (title, text) {
const $modal = $('#warningStatusModal')
$modal.find('.modal-status-title').text(title)
$modal.find('.modal-status-text').html(text)
openModal($modal)
}
export function openSuccessModal (title, text) {
const $modal = $('#successStatusModal')
$modal.find('.modal-status-title').text(title)
$modal.find('.modal-status-text').html(text)
openModal($modal)
}
export function openQuestionModal (title, text, acceptCallback = null, exceptCallback = null, acceptText = 'Yes', exceptText = 'No') {
const $modal = $('#questionStatusModal')
const $closeButton = $modal.find('.close-modal')
$closeButton.attr('disabled', false)
$modal.find('.modal-status-title').text(title)
$modal.find('.modal-status-text').text(text)
const $accept = $modal.find('.btn-line.accept')
const $except = $modal.find('.btn-line.except')
$accept
.removeAttr('data-dismiss')
.removeAttr('disabled')
.unbind('click')
.find('.btn-line-text').text(acceptText)
$except
.removeAttr('data-dismiss')
.removeAttr('disabled')
.unbind('click')
.find('.btn-line-text').text(exceptText)
if (acceptCallback) {
$accept.on('click', event => {
$closeButton.attr('disabled', true)
$accept
.unbind('click')
.attr('disabled', true)
.find('.btn-line-text').html(spinner)
$except
.unbind('click')
.removeAttr('data-dismiss')
.attr('disabled', true)
modalLocked = true
acceptCallback($modal, event)
})
} else {
$accept.attr('data-dismiss', 'modal')
}
$('.js-withdraw-stake').on('click', function () {
const modal = '#withdrawModal'
const progress = parseInt($(`${modal} .js-stakes-progress-data-progress`).text())
const total = parseInt($(`${modal} .js-stakes-progress-data-total`).text())
if (exceptCallback) {
$except.on('click', event => {
$closeButton.attr('disabled', true)
$(modal).modal()
$except
.unbind('click')
.attr('disabled', true)
.find('.btn-line-text').html(spinner)
$accept
.unbind('click')
.attr('disabled', true)
.removeAttr('data-dismiss')
setupStakesProgress(progress, total, modal)
modalLocked = true
exceptCallback($modal, event)
})
} else {
$except.attr('data-dismiss', 'modal')
}
function setupStakesProgress (progress, total, modal) {
// const stakeProgress = $(`${modal} .js-stakes-progress`)
// const primaryColor = $('.btn-full-primary').css('background-color')
// const backgroundColors = [
// primaryColor,
// 'rgba(202, 199, 226, 0.5)'
// ]
// const progressBackground = total - progress
// // eslint-disable-next-line no-unused-vars
// const myChart = new window.Chart(stakeProgress, {
// type: 'doughnut',
// data: {
// datasets: [{
// data: [progress, progressBackground],
// backgroundColor: backgroundColors,
// hoverBackgroundColor: backgroundColors,
// borderWidth: 0
// }]
// },
// options: {
// cutoutPercentage: 80,
// legend: {
// display: false
// },
// tooltips: {
// enabled: false
// }
// }
// })
openModal($modal)
}
})

@ -0,0 +1,99 @@
import $ from 'jquery'
import { walletEnabled, getCurrentAccount } from './write.js'
import { openErrorModal, openWarningModal, openSuccessModal } from '../modals.js'
const loadFunctions = (element) => {
const $element = $(element)
const url = $element.data('url')
const hash = $element.data('hash')
const type = $element.data('type')
const action = $element.data('action')
$.get(
url,
{ hash: hash, type: type, action: action },
response => $element.html(response)
)
.done(function () {
$('[data-function]').each((_, element) => {
readWriteFunction(element)
})
})
.fail(function (response) {
$element.html(response.statusText)
})
}
const readWriteFunction = (element) => {
const $element = $(element)
const $form = $element.find('[data-function-form]')
const $responseContainer = $element.find('[data-function-response]')
$form.on('submit', (event) => {
const action = $form.data('action')
event.preventDefault()
if (action === 'read') {
const url = $form.data('url')
const $functionName = $form.find('input[name=function_name]')
const $functionInputs = $form.find('input[name=function_input]')
const args = $.map($functionInputs, element => {
return $(element).val()
})
const data = {
function_name: $functionName.val(),
args
}
$.get(url, data, response => $responseContainer.html(response))
} else {
walletEnabled()
.then((isWalletEnabled) => {
if (isWalletEnabled) {
const functionName = $form.find('input[name=function_name]').val()
const $functionInputs = $form.find('input[name=function_input]')
const args = $.map($functionInputs, element => {
return $(element).val()
})
const contractAddress = $form.data('contract-address')
const contractAbi = $form.data('contract-abi')
getCurrentAccount()
.then(currentAccount => {
const TargetContract = new window.web3.eth.Contract(contractAbi, contractAddress)
TargetContract.methods[functionName](...args).send({ from: currentAccount })
.on('error', function (error) {
openErrorModal(`Error in sending transaction for method "${functionName}"`, error, false)
})
.on('transactionHash', function (txHash) {
const getTxReceipt = (txHash) => {
window.web3.eth.getTransactionReceipt(txHash)
.then(txReceipt => {
if (txReceipt) {
openSuccessModal('Success', `Successfully sent <a href="/tx/${txHash}">transaction</a> for method "${functionName}"`)
clearInterval(txReceiptPollingIntervalId)
}
})
}
const txReceiptPollingIntervalId = setInterval(() => { getTxReceipt(txHash) }, 5 * 1000)
})
})
} else {
openWarningModal('Unauthorized', 'You haven\'t approved the reading of account list from your MetaMask/Nifty wallet or MetaMask/Nifty wallet is not installed.')
}
})
}
})
}
const container = $('[data-smart-contract-functions]')
if (container.length) {
loadFunctions(container)
}

@ -1,3 +1,3 @@
import './read_only_functions'
import './functions'
import './wei_ether_converter'
import '../../app'

@ -1,54 +0,0 @@
import $ from 'jquery'
const loadFunctions = (element) => {
const $element = $(element)
const url = $element.data('url')
const hash = $element.data('hash')
const type = $element.data('type')
$.get(
url,
{ hash: hash, type: type },
response => $element.html(response)
)
.done(function () {
$('[data-function]').each((_, element) => {
readFunction(element)
})
})
.fail(function (response) {
$element.html(response.statusText)
})
}
const readFunction = (element) => {
const $element = $(element)
const $form = $element.find('[data-function-form]')
const $responseContainer = $element.find('[data-function-response]')
$form.on('submit', (event) => {
event.preventDefault()
const url = $form.data('url')
const $functionName = $form.find('input[name=function_name]')
const $functionInputs = $form.find('input[name=function_input]')
const args = $.map($functionInputs, element => {
return $(element).val()
})
const data = {
function_name: $functionName.val(),
args
}
$.get(url, data, response => $responseContainer.html(response))
})
}
const container = $('[data-smart-contract-functions]')
if (container.length) {
loadFunctions(container)
}

@ -0,0 +1,29 @@
import Web3 from 'web3'
export const walletEnabled = () => {
if (window.ethereum) {
window.web3 = new Web3(window.ethereum)
if (window.ethereum._state && window.ethereum._state.isUnlocked) { // Nifty Wallet
window.web3 = new Web3(window.web3.currentProvider)
return Promise.resolve(true)
} else if (window.ethereum._state && window.ethereum._state.isUnlocked === false) { // Nifty Wallet
return Promise.resolve(false)
} else {
window.ethereum.enable()
window.web3 = new Web3(window.web3.currentProvider)
return Promise.resolve(true)
}
} else if (window.web3) {
window.web3 = new Web3(window.web3.currentProvider)
return Promise.resolve(true)
} else {
return Promise.resolve(false)
}
}
export const getCurrentAccount = async () => {
const accounts = await window.web3.eth.getAccounts()
const account = accounts[0] ? accounts[0].toLowerCase() : null
return account
}

@ -0,0 +1 @@
import '../lib/smart_contract/write'

File diff suppressed because it is too large Load Diff

@ -39,7 +39,8 @@
"popper.js": "^1.14.7",
"reduce-reducers": "^0.4.3",
"redux": "^4.0.5",
"urijs": "^1.19.2"
"urijs": "^1.19.2",
"web3": "^1.2.9"
},
"devDependencies": {
"@babel/core": "^7.7.2",

@ -90,6 +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',
'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',

@ -32,6 +32,7 @@ defmodule BlockScoutWeb.AddressReadContractController do
"index.html",
address: address,
type: :regular,
action: :read,
coin_balance_status: CoinBalanceOnDemand.trigger_fetch(address),
exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(),
counters_path: address_path(conn, :address_counters, %{"id" => Address.checksum(address_hash)})

@ -26,6 +26,7 @@ defmodule BlockScoutWeb.AddressReadProxyController do
"index.html",
address: address,
type: :proxy,
action: :read,
coin_balance_status: CoinBalanceOnDemand.trigger_fetch(address),
exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(),
counters_path: address_path(conn, :address_counters, %{"id" => Address.checksum(address_hash)})

@ -0,0 +1,45 @@
# credo:disable-for-this-file
#
# When moving the calls to ajax, this controller became very similar to the
# `address_contract_controller`, but both are necessary until we are able to
# address a better way to organize the controllers.
#
# So, for now, I'm adding this comment to disable the credo check for this file.
defmodule BlockScoutWeb.AddressWriteContractController do
use BlockScoutWeb, :controller
alias Explorer.{Chain, Market}
alias Explorer.Chain.Address
alias Explorer.ExchangeRates.Token
alias Indexer.Fetcher.CoinBalanceOnDemand
def index(conn, %{"address_id" => address_hash_string}) do
address_options = [
necessity_by_association: %{
:contracts_creation_internal_transaction => :optional,
:names => :optional,
:smart_contract => :optional,
:token => :optional,
:contracts_creation_transaction => :optional
}
]
with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string),
{:ok, address} <- Chain.find_contract_address(address_hash, address_options, true),
false <- is_nil(address.smart_contract) do
render(
conn,
"index.html",
address: address,
type: :regular,
action: :write,
coin_balance_status: CoinBalanceOnDemand.trigger_fetch(address),
exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(),
counters_path: address_path(conn, :address_counters, %{"id" => Address.checksum(address_hash)})
)
else
_ ->
not_found(conn)
end
end
end

@ -2,25 +2,35 @@ defmodule BlockScoutWeb.SmartContractController do
use BlockScoutWeb, :controller
alias Explorer.Chain
alias Explorer.SmartContract.Reader
alias Explorer.SmartContract.{Reader, Writer}
def index(conn, %{"hash" => address_hash_string, "type" => contract_type, "action" => action}) do
address_options = [
necessity_by_association: %{
:smart_contract => :optional
}
]
def index(conn, %{"hash" => address_hash_string, "type" => contract_type}) do
with true <- ajax?(conn),
{:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string),
{:ok, address} <- Chain.find_contract_address(address_hash) do
read_only_functions =
{:ok, address} <- Chain.find_contract_address(address_hash, address_options, true) do
functions =
if action == "write" do
Writer.write_functions(address_hash)
else
if contract_type == "proxy" do
Reader.read_only_functions_proxy(address_hash)
else
Reader.read_only_functions(address_hash)
end
end
conn
|> put_status(200)
|> put_layout(false)
|> render(
"_functions.html",
read_only_functions: read_only_functions,
read_only_functions: functions,
address: address
)
else

@ -71,4 +71,11 @@
class: "card-tab #{tab_status("read_proxy", @conn.request_path)}")
%>
<% end %>
<%= if smart_contract_with_write_functions?(@address) do %>
<%= link(
gettext("Write Contract"),
to: address_write_contract_path(@conn, :index, @address.hash),
class: "card-tab #{tab_status("write_contract", @conn.request_path)}")
%>
<% end %>
</div>

@ -5,7 +5,7 @@
<div class="card">
<%= render BlockScoutWeb.AddressView, "_tabs.html", assigns %>
<!-- loaded via AJAX -->
<div class="card-body" data-smart-contract-functions data-hash="<%= to_string(@address.hash) %>" data-type="<%= @type %>" data-url="<%= smart_contract_path(@conn, :index) %>">
<div class="card-body" data-smart-contract-functions data-hash="<%= to_string(@address.hash) %>" data-type="<%= @type %>" data-action="<%= @action %>" data-url="<%= smart_contract_path(@conn, :index) %>">
<div>
<span class="loading-spinner-small mr-2">
<span class="loading-spinner-block-1"></span>

@ -5,7 +5,7 @@
<div class="card">
<%= render BlockScoutWeb.AddressView, "_tabs.html", assigns %>
<!-- loaded via AJAX -->
<div class="card-body" data-smart-contract-functions data-hash="<%= to_string(@address.hash) %>" data-type="<%= @type %>" data-url="<%= smart_contract_path(@conn, :index) %>">
<div class="card-body" data-smart-contract-functions data-hash="<%= to_string(@address.hash) %>" data-type="<%= @type %>" data-action="<%= @action %>" data-url="<%= smart_contract_path(@conn, :index) %>">
<div>
<span class="loading-spinner-small mr-2">
<span class="loading-spinner-block-1"></span>

@ -0,0 +1 @@
<%= render BlockScoutWeb.AddressView, "_metatags.html", conn: @conn, address: @address %>

@ -0,0 +1,20 @@
<section class="container">
<%= render BlockScoutWeb.AddressView, "overview.html", assigns %>
<div class="card">
<%= render BlockScoutWeb.AddressView, "_tabs.html", assigns %>
<!-- loaded via AJAX -->
<div class="card-body" data-smart-contract-functions data-hash="<%= to_string(@address.hash) %>" data-type="<%= @type %>" data-action="<%= @action %>" data-url="<%= smart_contract_path(@conn, :index) %>">
<div>
<span class="loading-spinner-small mr-2">
<span class="loading-spinner-block-1"></span>
<span class="loading-spinner-block-2"></span>
</span>
<%= gettext("Loading...") %>
</div>
</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>
</section>

@ -3,44 +3,25 @@
<div class="modal-content">
<div class="modal-status-graph modal-status-graph-<%= if assigns[:status] do @status end %>">
<%=
if @status == "error" do
render BlockScoutWeb.CommonComponentsView, "_icon_error_modal.html"
end
%>
<%=
if @status == "success" do
render BlockScoutWeb.CommonComponentsView, "_icon_success_modal.html"
end
%>
<%=
if @status == "warning" do
render BlockScoutWeb.CommonComponentsView, "_icon_warning_modal.html"
end
%>
<%=
if @status == "question" do
render BlockScoutWeb.CommonComponentsView, "_icon_question_modal.html"
if @status in ["error", "success", "warning", "question"] do
render BlockScoutWeb.CommonComponentsView, "_icon_#{@status}_modal.html"
end
%>
</div>
<%= render BlockScoutWeb.CommonComponentsView, "_modal_close_button.html" %>
<div class="modal-body modal-status-body">
<%= if assigns[:title] do %>
<h2 class="modal-status-title"><%= @title %></h2>
<% end %>
<%= if assigns[:text] do %>
<p class="modal-status-text"><%= @text %></p>
<% end %>
<h2 class="modal-status-title"><%= if assigns[:title] do @title end %></h2>
<p class="modal-status-text"><%= if assigns[:text] do @text end %></p>
<div class="modal-status-button-wrapper">
<%= if @status !== "question" do %>
<button class="btn-line" type="button" data-dismiss="modal">
<span class="btn-line-text">Ok</span>
</button>
<% else %>
<button class="btn-line except" type="button" data-dismiss="modal">
<button class="btn-line except" type="button">
<span class="btn-line-text">No</span>
</button>
<button class="btn-line accept" type="button" data-dismiss="modal">
<button class="btn-line accept" type="button">
<span class="btn-line-text">Yes</span>
</button>
<% end %>

@ -3,25 +3,39 @@
<div class="py-2 pr-2 text-nowrap">
<%= counter %>.
<%= function["name"] %>
<%= function["name"] || (if fallback?(function), do: "fallback", else: "") %>
&#8594;
</div>
<%= if queryable?(function["inputs"]) do %>
<%= if queryable?(function["inputs"]) || writeable?(function) do %>
<div style="width: 100%; overflow: hidden;">
<form class="form-inline" data-function-form data-url="<%= smart_contract_path(@conn, :show, @address.hash) %>">
<%=
for status <- ["error", "warning", "success", "question"] do
render BlockScoutWeb.CommonComponentsView, "_modal_status.html", status: status
end
%>
<form class="form-inline" data-function-form data-action="<%= if writeable?(function), do: :write, else: :read %>" data-url="<%= smart_contract_path(@conn, :show, @address.hash) %>" data-contract-address="<%= @address.hash %>" data-contract-abi="<%= Poison.encode!(@address.smart_contract.abi) %>">
<input type="hidden" name="function_name" value='<%= function["name"] %>' />
<%= if queryable?(function["inputs"]) do %>
<%= for input <- function["inputs"] do %>
<div class="form-group pr-2">
<input type="text" name="function_input" class="form-control form-control-sm address-input-sm mt-2" placeholder='<%= input["name"] %>(<%= input["type"] %>)' />
</div>
<% end %>
<% end %>
<%= if payable?(function) do %>
<div class="form-group pr-2">
<input type="text" name="function_input" class="form-control form-control-sm address-input-sm mt-2" placeholder='value(<%= gettext("ETH")%>)' />
</div>
<% end %>
<input type="submit" value='<%= gettext("Query")%>' class="button btn-line button-xs py-0 mt-2" style="padding: 6px 8px!important;height: 26px;font-size: 11px;" />
</form>
<%= if outputs?(function["outputs"]) do %>
<div class='p-2 text-muted <%= if (queryable?(function["inputs"]) == true), do: "w-100" %>'>
<%= if (queryable?(function["inputs"])), do: raw "&#8627;" %>
@ -29,11 +43,13 @@
<%= output["type"] %>
<% end %>
</div>
<% end %>
<div data-function-response></div>
</div>
<% else %>
<span class="py-2">
<%= if outputs?(function["outputs"]) do %>
<%= for output <- function["outputs"] do %>
<%= if address?(output["type"]) do %>
<%= link(
@ -55,6 +71,7 @@
<% end %>
<% end %>
<% end %>
<% end %>
</span>
<% end %>
</div>

@ -1,7 +1,13 @@
defmodule BlockScoutWeb.AddressReadContractView do
use BlockScoutWeb, :view
def queryable?(inputs), do: Enum.any?(inputs)
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,7 +1,13 @@
defmodule BlockScoutWeb.AddressReadProxyView do
use BlockScoutWeb, :view
def queryable?(inputs), do: Enum.any?(inputs)
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

@ -19,6 +19,7 @@ defmodule BlockScoutWeb.AddressView do
"token_transfers",
"read_contract",
"read_proxy",
"write_contract",
"tokens",
"transactions",
"validations"
@ -235,6 +236,17 @@ defmodule BlockScoutWeb.AddressView do
def smart_contract_is_proxy?(%Address{smart_contract: nil}), do: false
def smart_contract_with_write_functions?(%Address{smart_contract: %SmartContract{}} = address) do
Enum.any?(
address.smart_contract.abi,
&(&1["type"] !== "event" &&
(&1["stateMutability"] == "nonpayable" || &1["stateMutability"] == "payable" || &1["payable"] ||
(!&1["payable"] && !&1["constant"] && !&1["stateMutability"])))
)
end
def smart_contract_with_write_functions?(%Address{smart_contract: nil}), do: false
def has_decompiled_code?(address) do
address.has_decompiled_code? ||
(Ecto.assoc_loaded?(address.decompiled_smart_contracts) && Enum.count(address.decompiled_smart_contracts) > 0)
@ -334,6 +346,7 @@ defmodule BlockScoutWeb.AddressView do
defp tab_name(["decompiled_contracts"]), do: gettext("Decompiled Code")
defp tab_name(["read_contract"]), do: gettext("Read Contract")
defp tab_name(["read_proxy"]), do: gettext("Read Proxy")
defp tab_name(["write_contract"]), do: gettext("Write Contract")
defp tab_name(["coin_balances"]), do: gettext("Coin Balance History")
defp tab_name(["validations"]), do: gettext("Blocks Validated")
defp tab_name(["logs"]), do: gettext("Logs")

@ -0,0 +1,13 @@
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,7 +1,22 @@
defmodule BlockScoutWeb.SmartContractView do
use BlockScoutWeb, :view
def queryable?(inputs), do: Enum.any?(inputs)
def queryable?(inputs) when not is_nil(inputs), do: Enum.any?(inputs)
def queryable?(inputs) when is_nil(inputs), do: false
def writeable?(function),
do: payable?(function) || nonpayable?(function) || fallback?(function) || function["constant"] == false
def outputs?(outputs) when not is_nil(outputs), do: Enum.any?(outputs)
def outputs?(outputs) when is_nil(outputs), do: false
def fallback?(function), do: function["type"] == "fallback"
def payable?(function), do: function["stateMutability"] == "payable" || function["payable"]
def nonpayable?(function), do: function["stateMutability"] == "nonpayable"
def address?(type), do: type in ["address", "address payable"]

@ -132,6 +132,13 @@ defmodule BlockScoutWeb.WebRouter do
as: :read_proxy
)
resources(
"/write_contract",
AddressWriteContractController,
only: [:index, :show],
as: :write_contract
)
resources(
"/token_transfers",
AddressTokenTransferController,

@ -124,7 +124,7 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address/_validator_metadata_modal.html.eex:16
#: lib/block_scout_web/templates/transaction_log/_logs.html.eex:19
#: lib/block_scout_web/views/address_view.ex:101
#: lib/block_scout_web/views/address_view.ex:102
msgid "Address"
msgstr ""
@ -338,14 +338,14 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:18
#: lib/block_scout_web/views/address_view.ex:99
#: lib/block_scout_web/views/address_view.ex:100
msgid "Contract Address"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/transaction/_pending_tile.html.eex:16
#: lib/block_scout_web/views/address_view.ex:39
#: lib/block_scout_web/views/address_view.ex:73
#: lib/block_scout_web/views/address_view.ex:40
#: lib/block_scout_web/views/address_view.ex:74
msgid "Contract Address Pending"
msgstr ""
@ -568,7 +568,8 @@ msgid "ERC-721 "
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:50
#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:31
#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:66
msgid "ETH"
msgstr ""
@ -903,7 +904,7 @@ msgstr ""
#: lib/block_scout_web/templates/tokens/transfer/index.html.eex:14
#: 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:332
#: lib/block_scout_web/views/address_view.ex:344
#: lib/block_scout_web/views/tokens/instance/overview_view.ex:98
#: lib/block_scout_web/views/tokens/overview_view.ex:35
#: lib/block_scout_web/views/transaction_view.ex:394
@ -1012,6 +1013,7 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_read_contract/index.html.eex:14
#: lib/block_scout_web/templates/address_read_proxy/index.html.eex:14
#: lib/block_scout_web/templates/address_write_contract/index.html.eex:14
#: lib/block_scout_web/templates/tokens/read_contract/index.html.eex:20
msgid "Loading..."
msgstr ""
@ -1042,7 +1044,7 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/chain/show.html.eex:52
#: lib/block_scout_web/templates/layout/app.html.eex:30
#: lib/block_scout_web/views/address_view.ex:139
#: lib/block_scout_web/views/address_view.ex:140
msgid "Market Cap"
msgstr ""
@ -1192,7 +1194,7 @@ msgid "QR Code"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:22
#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:35
msgid "Query"
msgstr ""
@ -1646,7 +1648,7 @@ msgid "View transaction %{transaction} on %{subnetwork}"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:49
#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:65
msgid "WEI"
msgstr ""
@ -1808,7 +1810,7 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address/_tabs.html.eex:37
#: lib/block_scout_web/templates/address_validation/index.html.eex:13
#: lib/block_scout_web/views/address_view.ex:338
#: lib/block_scout_web/views/address_view.ex:351
msgid "Blocks Validated"
msgstr ""
@ -1818,18 +1820,18 @@ msgstr ""
#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:187
#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:126
#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:149
#: lib/block_scout_web/views/address_view.ex:333
#: lib/block_scout_web/views/address_view.ex:345
msgid "Code"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address/_tabs.html.eex:26
#: lib/block_scout_web/views/address_view.ex:337
#: lib/block_scout_web/views/address_view.ex:350
msgid "Coin Balance History"
msgstr ""
#, elixir-format
#: lib/block_scout_web/views/address_view.ex:334
#: lib/block_scout_web/views/address_view.ex:346
msgid "Decompiled Code"
msgstr ""
@ -1838,7 +1840,7 @@ msgstr ""
#: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:19
#: lib/block_scout_web/templates/transaction/_tabs.html.eex:11
#: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:6
#: lib/block_scout_web/views/address_view.ex:331
#: lib/block_scout_web/views/address_view.ex:343
#: lib/block_scout_web/views/transaction_view.ex:395
msgid "Internal Transactions"
msgstr ""
@ -1848,7 +1850,7 @@ msgstr ""
#: lib/block_scout_web/templates/address_logs/index.html.eex:8
#: lib/block_scout_web/templates/transaction/_tabs.html.eex:17
#: lib/block_scout_web/templates/transaction_log/index.html.eex:8
#: lib/block_scout_web/views/address_view.ex:339
#: lib/block_scout_web/views/address_view.ex:352
#: lib/block_scout_web/views/transaction_view.ex:396
msgid "Logs"
msgstr ""
@ -1856,7 +1858,7 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address/_tabs.html.eex:62
#: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:25
#: lib/block_scout_web/views/address_view.ex:335
#: lib/block_scout_web/views/address_view.ex:347
#: lib/block_scout_web/views/tokens/overview_view.ex:37
msgid "Read Contract"
msgstr ""
@ -1865,7 +1867,7 @@ msgstr ""
#: lib/block_scout_web/templates/address/_tabs.html.eex:14
#: lib/block_scout_web/templates/address_token/index.html.eex:8
#: lib/block_scout_web/templates/address_token_transfer/index.html.eex:11
#: lib/block_scout_web/views/address_view.ex:329
#: lib/block_scout_web/views/address_view.ex:341
msgid "Tokens"
msgstr ""
@ -1877,7 +1879,7 @@ msgstr ""
#: lib/block_scout_web/templates/block_transaction/index.html.eex:18
#: lib/block_scout_web/templates/chain/show.html.eex:184
#: lib/block_scout_web/templates/layout/_topnav.html.eex:50
#: lib/block_scout_web/views/address_view.ex:330
#: lib/block_scout_web/views/address_view.ex:342
msgid "Transactions"
msgstr ""
@ -1917,6 +1919,12 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address/_tabs.html.eex:69
#: lib/block_scout_web/views/address_view.ex:336
#: lib/block_scout_web/views/address_view.ex:348
msgid "Read Proxy"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address/_tabs.html.eex:76
#: lib/block_scout_web/views/address_view.ex:349
msgid "Write Contract"
msgstr ""

@ -124,7 +124,7 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address/_validator_metadata_modal.html.eex:16
#: lib/block_scout_web/templates/transaction_log/_logs.html.eex:19
#: lib/block_scout_web/views/address_view.ex:101
#: lib/block_scout_web/views/address_view.ex:102
msgid "Address"
msgstr ""
@ -338,14 +338,14 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:18
#: lib/block_scout_web/views/address_view.ex:99
#: lib/block_scout_web/views/address_view.ex:100
msgid "Contract Address"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/transaction/_pending_tile.html.eex:16
#: lib/block_scout_web/views/address_view.ex:39
#: lib/block_scout_web/views/address_view.ex:73
#: lib/block_scout_web/views/address_view.ex:40
#: lib/block_scout_web/views/address_view.ex:74
msgid "Contract Address Pending"
msgstr ""
@ -568,7 +568,8 @@ msgid "ERC-721 "
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:50
#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:31
#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:66
msgid "ETH"
msgstr ""
@ -903,7 +904,7 @@ msgstr ""
#: lib/block_scout_web/templates/tokens/transfer/index.html.eex:14
#: 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:332
#: lib/block_scout_web/views/address_view.ex:344
#: lib/block_scout_web/views/tokens/instance/overview_view.ex:98
#: lib/block_scout_web/views/tokens/overview_view.ex:35
#: lib/block_scout_web/views/transaction_view.ex:394
@ -1012,6 +1013,7 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_read_contract/index.html.eex:14
#: lib/block_scout_web/templates/address_read_proxy/index.html.eex:14
#: lib/block_scout_web/templates/address_write_contract/index.html.eex:14
#: lib/block_scout_web/templates/tokens/read_contract/index.html.eex:20
msgid "Loading..."
msgstr ""
@ -1042,7 +1044,7 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/chain/show.html.eex:52
#: lib/block_scout_web/templates/layout/app.html.eex:30
#: lib/block_scout_web/views/address_view.ex:139
#: lib/block_scout_web/views/address_view.ex:140
msgid "Market Cap"
msgstr ""
@ -1192,7 +1194,7 @@ msgid "QR Code"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:22
#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:35
msgid "Query"
msgstr ""
@ -1646,7 +1648,7 @@ msgid "View transaction %{transaction} on %{subnetwork}"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:49
#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:65
msgid "WEI"
msgstr ""
@ -1808,7 +1810,7 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address/_tabs.html.eex:37
#: lib/block_scout_web/templates/address_validation/index.html.eex:13
#: lib/block_scout_web/views/address_view.ex:338
#: lib/block_scout_web/views/address_view.ex:351
msgid "Blocks Validated"
msgstr ""
@ -1818,18 +1820,18 @@ msgstr ""
#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:187
#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:126
#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:149
#: lib/block_scout_web/views/address_view.ex:333
#: lib/block_scout_web/views/address_view.ex:345
msgid "Code"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address/_tabs.html.eex:26
#: lib/block_scout_web/views/address_view.ex:337
#: lib/block_scout_web/views/address_view.ex:350
msgid "Coin Balance History"
msgstr ""
#, elixir-format
#: lib/block_scout_web/views/address_view.ex:334
#: lib/block_scout_web/views/address_view.ex:346
msgid "Decompiled Code"
msgstr ""
@ -1838,7 +1840,7 @@ msgstr ""
#: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:19
#: lib/block_scout_web/templates/transaction/_tabs.html.eex:11
#: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:6
#: lib/block_scout_web/views/address_view.ex:331
#: lib/block_scout_web/views/address_view.ex:343
#: lib/block_scout_web/views/transaction_view.ex:395
msgid "Internal Transactions"
msgstr ""
@ -1848,7 +1850,7 @@ msgstr ""
#: lib/block_scout_web/templates/address_logs/index.html.eex:8
#: lib/block_scout_web/templates/transaction/_tabs.html.eex:17
#: lib/block_scout_web/templates/transaction_log/index.html.eex:8
#: lib/block_scout_web/views/address_view.ex:339
#: lib/block_scout_web/views/address_view.ex:352
#: lib/block_scout_web/views/transaction_view.ex:396
msgid "Logs"
msgstr ""
@ -1856,7 +1858,7 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address/_tabs.html.eex:62
#: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:25
#: lib/block_scout_web/views/address_view.ex:335
#: lib/block_scout_web/views/address_view.ex:347
#: lib/block_scout_web/views/tokens/overview_view.ex:37
msgid "Read Contract"
msgstr ""
@ -1865,7 +1867,7 @@ msgstr ""
#: lib/block_scout_web/templates/address/_tabs.html.eex:14
#: lib/block_scout_web/templates/address_token/index.html.eex:8
#: lib/block_scout_web/templates/address_token_transfer/index.html.eex:11
#: lib/block_scout_web/views/address_view.ex:329
#: lib/block_scout_web/views/address_view.ex:341
msgid "Tokens"
msgstr ""
@ -1877,7 +1879,7 @@ msgstr ""
#: lib/block_scout_web/templates/block_transaction/index.html.eex:18
#: lib/block_scout_web/templates/chain/show.html.eex:184
#: lib/block_scout_web/templates/layout/_topnav.html.eex:50
#: lib/block_scout_web/views/address_view.ex:330
#: lib/block_scout_web/views/address_view.ex:342
msgid "Transactions"
msgstr ""
@ -1917,6 +1919,12 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address/_tabs.html.eex:69
#: lib/block_scout_web/views/address_view.ex:336
#: lib/block_scout_web/views/address_view.ex:348
msgid "Read Proxy"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address/_tabs.html.eex:76
#: lib/block_scout_web/views/address_view.ex:349
msgid "Write Contract"
msgstr ""

@ -22,7 +22,7 @@ defmodule BlockScoutWeb.SmartContractControllerTest do
end
test "error for invalid address" do
path = smart_contract_path(BlockScoutWeb.Endpoint, :index, hash: "0x00", type: :regular)
path = smart_contract_path(BlockScoutWeb.Endpoint, :index, hash: "0x00", type: :regular, action: :read)
conn =
build_conn()
@ -49,7 +49,12 @@ defmodule BlockScoutWeb.SmartContractControllerTest do
blockchain_get_function_mock()
path = smart_contract_path(BlockScoutWeb.Endpoint, :index, hash: token_contract_address.hash, type: :regular)
path =
smart_contract_path(BlockScoutWeb.Endpoint, :index,
hash: token_contract_address.hash,
type: :regular,
action: :read
)
conn =
build_conn()
@ -65,7 +70,12 @@ defmodule BlockScoutWeb.SmartContractControllerTest do
insert(:smart_contract, address_hash: token_contract_address.hash)
path = smart_contract_path(BlockScoutWeb.Endpoint, :index, hash: token_contract_address.hash, type: :proxy)
path =
smart_contract_path(BlockScoutWeb.Endpoint, :index,
hash: token_contract_address.hash,
type: :proxy,
action: :read
)
conn =
build_conn()

@ -199,7 +199,7 @@ defmodule Explorer.SmartContract.Reader do
end
end
defp fetch_current_value_from_blockchain(function, abi, contract_address_hash) do
def fetch_current_value_from_blockchain(function, abi, contract_address_hash) do
values =
case function do
%{"inputs" => []} ->

@ -0,0 +1,28 @@
defmodule Explorer.SmartContract.Writer do
@moduledoc """
Generates smart-contract transactions
"""
alias Explorer.Chain
@spec write_functions(Hash.t()) :: [%{}]
def write_functions(contract_address_hash) do
abi =
contract_address_hash
|> Chain.address_hash_to_smart_contract()
|> Map.get(:abi)
case abi do
nil ->
[]
_ ->
abi
|> Enum.filter(
&(&1["type"] !== "event" &&
(&1["stateMutability"] == "nonpayable" || &1["stateMutability"] == "payable" || &1["payable"] ||
(!&1["payable"] && !&1["constant"] && !&1["stateMutability"])))
)
end
end
end
Loading…
Cancel
Save