Merge pull request #3160 from poanetwork/vb-write-contracts

Write contracts feature
pull/3173/head
Victor Baranov 4 years ago committed by GitHub
commit 69fccf530a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      .dialyzer-ignore
  2. 2
      CHANGELOG.md
  3. 4
      apps/block_scout_web/assets/css/_helpers.scss
  4. 53
      apps/block_scout_web/assets/css/components/_modal.scss
  5. 23
      apps/block_scout_web/assets/css/components/_modal_status.scss
  6. 6
      apps/block_scout_web/assets/css/theme/_base_variables.scss
  7. 258
      apps/block_scout_web/assets/js/lib/modals.js
  8. 141
      apps/block_scout_web/assets/js/lib/smart_contract/functions.js
  9. 2
      apps/block_scout_web/assets/js/lib/smart_contract/index.js
  10. 54
      apps/block_scout_web/assets/js/lib/smart_contract/read_only_functions.js
  11. 29
      apps/block_scout_web/assets/js/lib/smart_contract/write.js
  12. 1
      apps/block_scout_web/assets/js/pages/write_contract.js
  13. 2203
      apps/block_scout_web/assets/package-lock.json
  14. 4
      apps/block_scout_web/assets/package.json
  15. 1
      apps/block_scout_web/assets/webpack.config.js
  16. 1
      apps/block_scout_web/lib/block_scout_web/controllers/address_read_contract_controller.ex
  17. 1
      apps/block_scout_web/lib/block_scout_web/controllers/address_read_proxy_controller.ex
  18. 45
      apps/block_scout_web/lib/block_scout_web/controllers/address_write_contract_controller.ex
  19. 39
      apps/block_scout_web/lib/block_scout_web/controllers/address_write_proxy_controller.ex
  20. 44
      apps/block_scout_web/lib/block_scout_web/controllers/smart_contract_controller.ex
  21. 2
      apps/block_scout_web/lib/block_scout_web/controllers/tokens/read_contract_controller.ex
  22. 14
      apps/block_scout_web/lib/block_scout_web/templates/address/_tabs.html.eex
  23. 2
      apps/block_scout_web/lib/block_scout_web/templates/address_read_contract/index.html.eex
  24. 2
      apps/block_scout_web/lib/block_scout_web/templates/address_read_proxy/index.html.eex
  25. 1
      apps/block_scout_web/lib/block_scout_web/templates/address_write_contract/_metatags.html.eex
  26. 20
      apps/block_scout_web/lib/block_scout_web/templates/address_write_contract/index.html.eex
  27. 1
      apps/block_scout_web/lib/block_scout_web/templates/address_write_proxy/_metatags.html.eex
  28. 20
      apps/block_scout_web/lib/block_scout_web/templates/address_write_proxy/index.html.eex
  29. 31
      apps/block_scout_web/lib/block_scout_web/templates/common_components/_modal_status.html.eex
  30. 78
      apps/block_scout_web/lib/block_scout_web/templates/smart_contract/_functions.html.eex
  31. 23
      apps/block_scout_web/lib/block_scout_web/templates/smart_contract/_pending_contract_write.html.eex
  32. 2
      apps/block_scout_web/lib/block_scout_web/templates/tokens/read_contract/index.html.eex
  33. 8
      apps/block_scout_web/lib/block_scout_web/views/address_read_contract_view.ex
  34. 8
      apps/block_scout_web/lib/block_scout_web/views/address_read_proxy_view.ex
  35. 14
      apps/block_scout_web/lib/block_scout_web/views/address_view.ex
  36. 13
      apps/block_scout_web/lib/block_scout_web/views/address_write_contract_view.ex
  37. 13
      apps/block_scout_web/lib/block_scout_web/views/address_write_proxy_view.ex
  38. 30
      apps/block_scout_web/lib/block_scout_web/views/smart_contract_view.ex
  39. 14
      apps/block_scout_web/lib/block_scout_web/web_router.ex
  40. 64
      apps/block_scout_web/priv/gettext/default.pot
  41. 64
      apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po
  42. 81
      apps/block_scout_web/test/block_scout_web/controllers/address_write_contract_controller_test.exs
  43. 81
      apps/block_scout_web/test/block_scout_web/controllers/address_write_proxy_controller_test.exs
  44. 16
      apps/block_scout_web/test/block_scout_web/controllers/smart_contract_controller_test.exs
  45. 19
      apps/block_scout_web/test/block_scout_web/views/address_write_contract_view_test.exs
  46. 19
      apps/block_scout_web/test/block_scout_web/views/address_write_proxy_view_test copy.exs
  47. 177
      apps/block_scout_web/test/block_scout_web/views/tokens/smart_contract_view_test.exs
  48. 2
      apps/explorer/lib/explorer/smart_contract/reader.ex
  49. 62
      apps/explorer/lib/explorer/smart_contract/writer.ex
  50. 344
      apps/explorer/test/explorer/smart_contract/writer_test.exs

@ -1,6 +1,7 @@
:0: Unknown function 'Elixir.ExUnit.Callbacks':'__merge__'/3 :0: Unknown function 'Elixir.ExUnit.Callbacks':'__merge__'/3
:0: Unknown function 'Elixir.ExUnit.CaseTemplate':'__proxy__'/2 :0: Unknown function 'Elixir.ExUnit.CaseTemplate':'__proxy__'/2
:0: Unknown type 'Elixir.Map':t/0 :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 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/explorer/repo/prometheus_logger.ex:8
lib/block_scout_web/views/layout_view.ex:175 lib/block_scout_web/views/layout_view.ex:175

@ -2,6 +2,8 @@
### Features ### Features
- [#3171](https://github.com/poanetwork/blockscout/pull/3171) - Import accounts/contracts/balances from Geth genesis.json - [#3171](https://github.com/poanetwork/blockscout/pull/3171) - Import accounts/contracts/balances from Geth genesis.json
- [#3161](https://github.com/poanetwork/blockscout/pull/3161) - Write proxy contracts feature
- [#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 - [#3157](https://github.com/poanetwork/blockscout/pull/3157) - Read methods of implementation on proxy contract
### Fixes ### Fixes

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

@ -11,6 +11,11 @@
padding: #{$modal-vertical-padding} #{$modal-horizontal-padding}; padding: #{$modal-vertical-padding} #{$modal-horizontal-padding};
} }
.modal-header-group {
display: inline-flex;
margin: 0 auto;
}
.close.close-modal{ .close.close-modal{
left: auto; left: auto;
opacity: 1; opacity: 1;
@ -18,6 +23,10 @@
right: -35px; right: -35px;
top: -35px; top: -35px;
outline: none !important; outline: none !important;
path {
fill: #F6F7F9;
}
} }
.close { .close {
@ -35,6 +44,10 @@
text-align: left; text-align: left;
white-space: normal; white-space: normal;
word-break: break-word; word-break: break-word;
.centered {
margin: 0 auto;
}
} }
.modal-content { .modal-content {
@ -55,3 +68,43 @@
} }
} }
} }
.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;
}
}
}
@media (min-width: 576px) {
.modal-dialog {
max-width: 530px !important;
}
}

@ -15,8 +15,8 @@ $modal-status-graph-question: #329ae9 !default;
.modal-status-graph { .modal-status-graph {
align-items: center; align-items: center;
border-top-left-radius: $modal-border-radius; border-top-left-radius: 8px;
border-top-right-radius: $modal-border-radius; border-top-right-radius: 8px;
display: flex; display: flex;
height: 135px; height: 135px;
justify-content: center; justify-content: center;
@ -66,6 +66,18 @@ $modal-status-graph-question: #329ae9 !default;
line-height: 1.5; line-height: 1.5;
margin: 0 0 25px; margin: 0 0 25px;
text-align: center; 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 { .modal-status-button-wrapper {
@ -76,6 +88,13 @@ $modal-status-graph-question: #329ae9 !default;
.btn-line { .btn-line {
flex-grow: 1; flex-grow: 1;
margin-right: 20px; margin-right: 20px;
border-color: $primary;
color: $primary;
&:hover {
background-color: $primary;
color: $additional-font;
}
&:last-child { &:last-child {
margin-right: 0; margin-right: 0;

@ -215,6 +215,12 @@ $container-max-widths: (
@include _assert-ascending($container-max-widths, "$container-max-widths"); @include _assert-ascending($container-max-widths, "$container-max-widths");
@media (min-width: 1200px) {
.container {
max-width: 1260px !important;
}
}
// Grid columns // Grid columns
// //
// Set the number of columns and specify the width of the gutters. // Set the number of columns and specify the width of the gutters.

@ -1,75 +1,191 @@
import $ from 'jquery' import $ from 'jquery'
$(function () { let $currentModal = null
$('.js-become-candidate').on('click', function () { let modalLocked = false
$('#becomeCandidateModal').modal()
}) const spinner =
`
$('.js-validator-info-modal').on('click', function () { <span class="loading-spinner-small mr-2">
$('#validatorInfoModal').modal() <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) {
$('.js-remove-pool').on('click', function () { e.preventDefault()
$('#warningStatusModal').modal() e.stopPropagation()
}) return false
$('.js-copy-address').on('click', function () {
$('#successStatusModal').modal()
})
$('.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())
$(modal).modal()
setupStakesProgress(progress, total, 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())
$(modal).modal()
setupStakesProgress(progress, total, 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
// }
// }
// })
} }
$currentModal = null
}) })
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 openModalWithMessage ($modal, unclosable, message) {
$modal.find('.modal-message').text(message)
openModal($modal, unclosable)
}
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
}
export function unlockModal ($modal, $submitButton = null) {
$modal.find('.close-modal').attr('disabled', false)
const $button = $submitButton || $modal.find('.btn-add-full')
const buttonText = $button.attr('data-text')
$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')
}
if (exceptCallback) {
$except.on('click', event => {
$closeButton.attr('disabled', true)
$except
.unbind('click')
.attr('disabled', true)
.find('.btn-line-text').html(spinner)
$accept
.unbind('click')
.attr('disabled', true)
.removeAttr('data-dismiss')
modalLocked = true
exceptCallback($modal, event)
})
} else {
$except.attr('data-dismiss', 'modal')
}
openModal($modal)
}

@ -0,0 +1,141 @@
import $ from 'jquery'
import ethNetProps from 'eth-net-props'
import { walletEnabled, getCurrentAccount } from './write.js'
import { openErrorModal, openWarningModal, openSuccessModal, openModalWithMessage } from '../modals.js'
const WEI_MULTIPLIER = 10 ** 18
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 if (action === 'write') {
const chainId = $form.data('chainId')
walletEnabled()
.then((isWalletEnabled) => {
if (isWalletEnabled) {
const functionName = $form.find('input[name=function_name]').val()
const $functionInputs = $form.find('input[name=function_input]')
const $functionInputsExceptTxValue = $functionInputs.filter(':not([tx-value])')
const args = $.map($functionInputsExceptTxValue, element => $(element).val())
const $txValue = $functionInputs.filter('[tx-value]:first')
const txValue = $txValue && $txValue.val() && parseFloat($txValue.val()) * WEI_MULTIPLIER
const contractAddress = $form.data('contract-address')
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
window.web3.eth.getChainId()
.then(chainIdFromWallet => {
if (chainId !== chainIdFromWallet) {
const networkDisplayNameFromWallet = ethNetProps.props.getNetworkDisplayName(chainIdFromWallet)
const networkDisplayName = ethNetProps.props.getNetworkDisplayName(chainId)
return Promise.reject(new Error(`You connected to ${networkDisplayNameFromWallet} chain in the wallet, but the current instance of Blockscout is for ${networkDisplayName} chain`))
} else {
return getCurrentAccount()
}
})
.then(currentAccount => {
let methodToCall
if (functionName) {
const TargetContract = new window.web3.eth.Contract(contractAbi, contractAddress)
methodToCall = TargetContract.methods[functionName](...args).send({ from: currentAccount, value: txValue || 0 })
} else {
const txParams = {
from: currentAccount,
to: contractAddress,
value: txValue || 0
}
methodToCall = window.web3.eth.sendTransaction(txParams)
}
methodToCall
.on('error', function (error) {
openErrorModal(`Error in sending transaction for method "${functionName}"`, formatError(error), false)
})
.on('transactionHash', function (txHash) {
openModalWithMessage($element.find('#pending-contract-write'), true, 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)
})
})
.catch(error => {
openWarningModal('Unauthorized', formatError(error))
})
} else {
openWarningModal('Unauthorized', 'You haven\'t approved the reading of account list from your MetaMask or MetaMask/Nifty wallet is locked or is not installed.')
}
})
}
})
}
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) {
loadFunctions(container)
}

@ -1,3 +1,3 @@
import './read_only_functions' import './functions'
import './wei_ether_converter' import './wei_ether_converter'
import '../../app' 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

@ -25,6 +25,7 @@
"bootstrap": "^4.3.1", "bootstrap": "^4.3.1",
"chart.js": "^2.9.3", "chart.js": "^2.9.3",
"clipboard": "^2.0.4", "clipboard": "^2.0.4",
"eth-net-props": "^1.0.33",
"highlight.js": "^9.16.2", "highlight.js": "^9.16.2",
"highlightjs-solidity": "^1.0.8", "highlightjs-solidity": "^1.0.8",
"humps": "^2.0.1", "humps": "^2.0.1",
@ -39,7 +40,8 @@
"popper.js": "^1.14.7", "popper.js": "^1.14.7",
"reduce-reducers": "^0.4.3", "reduce-reducers": "^0.4.3",
"redux": "^4.0.5", "redux": "^4.0.5",
"urijs": "^1.19.2" "urijs": "^1.19.2",
"web3": "^1.2.9"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.7.2", "@babel/core": "^7.7.2",

@ -90,6 +90,7 @@ const appJs =
'admin-tasks': './js/pages/admin/tasks.js', 'admin-tasks': './js/pages/admin/tasks.js',
'read-token-contract': './js/pages/read_token_contract.js', 'read-token-contract': './js/pages/read_token_contract.js',
'smart-contract-helpers': './js/lib/smart_contract/index.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', 'token-transfers-toggle': './js/lib/token_transfers_toggle.js',
'try-api': './js/lib/try_api.js', 'try-api': './js/lib/try_api.js',
'try-eth-api': './js/lib/try_eth_api.js', 'try-eth-api': './js/lib/try_eth_api.js',

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

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

@ -0,0 +1,39 @@
# credo:disable-for-this-file
defmodule BlockScoutWeb.AddressWriteProxyController 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: :proxy,
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,17 +2,42 @@ defmodule BlockScoutWeb.SmartContractController do
use BlockScoutWeb, :controller use BlockScoutWeb, :controller
alias Explorer.Chain 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), with true <- ajax?(conn),
{:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string), {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string),
{:ok, address} <- Chain.find_contract_address(address_hash) do {:ok, address} <- Chain.find_contract_address(address_hash, address_options, true) do
read_only_functions = functions =
if action == "write" do
if contract_type == "proxy" do
Writer.write_functions_proxy(address_hash)
else
Writer.write_functions(address_hash)
end
else
if contract_type == "proxy" do
Reader.read_only_functions_proxy(address_hash)
else
Reader.read_only_functions(address_hash)
end
end
contract_abi = Poison.encode!(address.smart_contract.abi)
implementation_abi =
if contract_type == "proxy" do if contract_type == "proxy" do
Reader.read_only_functions_proxy(address_hash) address.hash
|> Chain.get_implementation_abi_from_proxy(address.smart_contract.abi)
|> Poison.encode!()
else else
Reader.read_only_functions(address_hash) []
end end
conn conn
@ -20,8 +45,11 @@ defmodule BlockScoutWeb.SmartContractController do
|> put_layout(false) |> put_layout(false)
|> render( |> render(
"_functions.html", "_functions.html",
read_only_functions: read_only_functions, read_only_functions: functions,
address: address address: address,
contract_abi: contract_abi,
implementation_abi: implementation_abi,
contract_type: contract_type
) )
else else
:error -> :error ->

@ -13,6 +13,8 @@ defmodule BlockScoutWeb.Tokens.ReadContractController do
render( render(
conn, conn,
"index.html", "index.html",
type: :regular,
action: :read,
token: Market.add_price(token), token: Market.add_price(token),
counters_path: token_path(conn, :token_counters, %{"id" => Address.checksum(address_hash)}) counters_path: token_path(conn, :token_counters, %{"id" => Address.checksum(address_hash)})
) )

@ -71,4 +71,18 @@
class: "card-tab #{tab_status("read_proxy", @conn.request_path)}") class: "card-tab #{tab_status("read_proxy", @conn.request_path)}")
%> %>
<% end %> <% 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 %>
<%= if smart_contract_with_write_functions?(@address) && smart_contract_is_proxy?(@address) do %>
<%= link(
gettext("Write Proxy"),
to: address_write_proxy_path(@conn, :index, @address.hash),
class: "card-tab #{tab_status("write_proxy", @conn.request_path)}")
%>
<% end %>
</div> </div>

@ -5,7 +5,7 @@
<div class="card"> <div class="card">
<%= render BlockScoutWeb.AddressView, "_tabs.html", assigns %> <%= render BlockScoutWeb.AddressView, "_tabs.html", assigns %>
<!-- loaded via AJAX --> <!-- 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> <div>
<span class="loading-spinner-small mr-2"> <span class="loading-spinner-small mr-2">
<span class="loading-spinner-block-1"></span> <span class="loading-spinner-block-1"></span>

@ -5,7 +5,7 @@
<div class="card"> <div class="card">
<%= render BlockScoutWeb.AddressView, "_tabs.html", assigns %> <%= render BlockScoutWeb.AddressView, "_tabs.html", assigns %>
<!-- loaded via AJAX --> <!-- 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> <div>
<span class="loading-spinner-small mr-2"> <span class="loading-spinner-small mr-2">
<span class="loading-spinner-block-1"></span> <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>

@ -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-content">
<div class="modal-status-graph modal-status-graph-<%= if assigns[:status] do @status end %>"> <div class="modal-status-graph modal-status-graph-<%= if assigns[:status] do @status end %>">
<%= <%=
if @status == "error" do if @status in ["error", "success", "warning", "question"] do
render BlockScoutWeb.CommonComponentsView, "_icon_error_modal.html" render BlockScoutWeb.CommonComponentsView, "_icon_#{@status}_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"
end end
%> %>
</div> </div>
<%= render BlockScoutWeb.CommonComponentsView, "_modal_close_button.html" %> <%= render BlockScoutWeb.CommonComponentsView, "_modal_close_button.html" %>
<div class="modal-body modal-status-body"> <div class="modal-body modal-status-body">
<%= if assigns[:title] do %> <h2 class="modal-status-title"><%= if assigns[:title] do @title end %></h2>
<h2 class="modal-status-title"><%= @title %></h2> <p class="modal-status-text"><%= if assigns[:text] do @text end %></p>
<% end %>
<%= if assigns[:text] do %>
<p class="modal-status-text"><%= @text %></p>
<% end %>
<div class="modal-status-button-wrapper"> <div class="modal-status-button-wrapper">
<%= if @status !== "question" do %> <%= if @status !== "question" do %>
<button class="btn-line" type="button" data-dismiss="modal"> <button class="btn-line" type="button" data-dismiss="modal">
<span class="btn-line-text">Ok</span> <span class="btn-line-text">Ok</span>
</button> </button>
<% else %> <% 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> <span class="btn-line-text">No</span>
</button> </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> <span class="btn-line-text">Yes</span>
</button> </button>
<% end %> <% end %>

@ -8,51 +8,69 @@
&#8594; &#8594;
</div> </div>
<%= if queryable?(function["inputs"]) do %> <%= if queryable?(function["inputs"]) || writeable?(function) do %>
<div style="width: 100%; overflow: hidden;"> <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
%>
<%= render BlockScoutWeb.SmartContractView, "_pending_contract_write.html" %>
<form class="form-inline" data-function-form data-action="<%= if writeable?(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() %>">
<input type="hidden" name="function_name" value='<%= function["name"] %>' /> <input type="hidden" name="function_name" value='<%= function["name"] %>' />
<%= for input <- function["inputs"] do %> <%= 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"> <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"] %>)' /> <input type="text" name="function_input" tx-value class="form-control form-control-sm address-input-sm mt-2" placeholder='value(<%= gettext("ETH")%>)' />
</div> </div>
<% end %> <% 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;" /> <input type="submit" value='<%= if writeable?(function), do: gettext("Write"), else: gettext("Query")%>' class="button btn-line button-xs py-0 mt-2" style="padding: 6px 8px!important;height: 26px;font-size: 11px;" />
</form> </form>
<div class='p-2 text-muted <%= if (queryable?(function["inputs"]) == true), do: "w-100" %>'> <%= if outputs?(function["outputs"]) do %>
<%= if (queryable?(function["inputs"])), do: raw "&#8627;" %> <div class='p-2 text-muted <%= if (queryable?(function["inputs"]) == true), do: "w-100" %>'>
<%= if (queryable?(function["inputs"])), do: raw "&#8627;" %>
<%= for output <- function["outputs"] do %> <%= for output <- function["outputs"] do %>
<%= output["type"] %> <%= output["type"] %>
<% end %> <% end %>
</div> </div>
<% end %>
<div data-function-response></div> <div data-function-response></div>
</div> </div>
<% else %> <% else %>
<span class="py-2"> <span class="py-2">
<%= for output <- function["outputs"] do %> <%= if outputs?(function["outputs"]) do %>
<%= if address?(output["type"]) do %> <%= for output <- function["outputs"] do %>
<%= link( <%= if address?(output["type"]) do %>
output["value"], <%= link(
to: address_path(@conn, :show, output["value"]) output["value"],
) %> to: address_path(@conn, :show, output["value"])
<% else %> ) %>
<%= if output["type"] == "uint256" do %> <% else %>
<div data-wei-ether-converter> <%= if output["type"] == "uint256" do %>
<span data-conversion-unit><%= output["value"] %></span> <div data-wei-ether-converter>
<span class="py-2 px-2"> <span data-conversion-unit><%= output["value"] %></span>
<input class="wei-ether" type="checkbox" autocomplete="off"> <span class="py-2 px-2">
<span class="d-inline-block" data-conversion-text-wei><%= gettext("WEI")%></span> <input class="wei-ether" type="checkbox" autocomplete="off">
<span class="d-none" data-conversion-text-eth><%= gettext("ETH")%></span> <span class="d-inline-block" data-conversion-text-wei><%= gettext("WEI")%></span>
</span> <span class="d-none" data-conversion-text-eth><%= gettext("ETH")%></span>
</div> </span>
<% else %> </div>
<%= values(output["value"], output["type"]) %> <% else %>
<% end %> <%= values(output["value"], output["type"]) %>
<% end %>
<% end %>
<% end %> <% end %>
<% end %> <% end %>
</span> </span>

@ -0,0 +1,23 @@
<div id="pending-contract-write" class="modal modal-fullwidth-xs fade" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-stake" role="document">
<div class="modal-content">
<%= render BlockScoutWeb.CommonComponentsView, "_modal_close_button.html" %>
<div class="modal-stake-two-cols">
<div class="modal-header">
<div class="modal-header-group">
<span class="loading-spinner-small mr-2">
<span class="loading-spinner-block-1"></span>
<span class="loading-spinner-block-2"></span>
</span>
<h5 class="modal-title centered"><%= gettext("Waiting for transaction's confirmation...") %></h5>
</div>
</div>
<div class="modal-body">
<form>
<p class="form-p modal-status-text modal-message"></p>
</form>
</div>
</div>
</div>
</div>
</div>

@ -11,7 +11,7 @@
<div class="card"> <div class="card">
<%= render OverviewView, "_tabs.html", assigns %> <%= render OverviewView, "_tabs.html", assigns %>
<!-- loaded via AJAX --> <!-- loaded via AJAX -->
<div class="card-body" data-smart-contract-functions data-hash="<%= Address.checksum(@token.contract_address.hash) %>" data-url="<%= smart_contract_path(@conn, :index) %>"> <div class="card-body" data-smart-contract-functions data-hash="<%= Address.checksum(@token.contract_address.hash) %>" data-type="<%= @type %>" data-action="<%= @action %>" data-url="<%= smart_contract_path(@conn, :index) %>">
<div class="tile tile-muted text-center"> <div class="tile tile-muted text-center">
<span class="loading-spinner-small mr-2"> <span class="loading-spinner-small mr-2">
<span class="loading-spinner-block-1"></span> <span class="loading-spinner-block-1"></span>

@ -1,7 +1,13 @@
defmodule BlockScoutWeb.AddressReadContractView do defmodule BlockScoutWeb.AddressReadContractView do
use BlockScoutWeb, :view 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" def address?(type), do: type == "address"
end end

@ -1,7 +1,13 @@
defmodule BlockScoutWeb.AddressReadProxyView do defmodule BlockScoutWeb.AddressReadProxyView do
use BlockScoutWeb, :view 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" def address?(type), do: type == "address"
end end

@ -8,6 +8,7 @@ defmodule BlockScoutWeb.AddressView do
alias Explorer.Chain.{Address, Hash, InternalTransaction, SmartContract, Token, TokenTransfer, Transaction, Wei} alias Explorer.Chain.{Address, Hash, InternalTransaction, SmartContract, Token, TokenTransfer, Transaction, Wei}
alias Explorer.Chain.Block.Reward alias Explorer.Chain.Block.Reward
alias Explorer.ExchangeRates.Token, as: TokenExchangeRate alias Explorer.ExchangeRates.Token, as: TokenExchangeRate
alias Explorer.SmartContract.Writer
@dialyzer :no_match @dialyzer :no_match
@ -19,6 +20,8 @@ defmodule BlockScoutWeb.AddressView do
"token_transfers", "token_transfers",
"read_contract", "read_contract",
"read_proxy", "read_proxy",
"write_contract",
"write_proxy",
"tokens", "tokens",
"transactions", "transactions",
"validations" "validations"
@ -235,6 +238,15 @@ defmodule BlockScoutWeb.AddressView do
def smart_contract_is_proxy?(%Address{smart_contract: nil}), do: false 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,
&Writer.write_function?(&1)
)
end
def smart_contract_with_write_functions?(%Address{smart_contract: nil}), do: false
def has_decompiled_code?(address) do def has_decompiled_code?(address) do
address.has_decompiled_code? || address.has_decompiled_code? ||
(Ecto.assoc_loaded?(address.decompiled_smart_contracts) && Enum.count(address.decompiled_smart_contracts) > 0) (Ecto.assoc_loaded?(address.decompiled_smart_contracts) && Enum.count(address.decompiled_smart_contracts) > 0)
@ -334,6 +346,8 @@ defmodule BlockScoutWeb.AddressView do
defp tab_name(["decompiled_contracts"]), do: gettext("Decompiled Code") defp tab_name(["decompiled_contracts"]), do: gettext("Decompiled Code")
defp tab_name(["read_contract"]), do: gettext("Read Contract") defp tab_name(["read_contract"]), do: gettext("Read Contract")
defp tab_name(["read_proxy"]), do: gettext("Read Proxy") defp tab_name(["read_proxy"]), do: gettext("Read Proxy")
defp tab_name(["write_contract"]), do: gettext("Write Contract")
defp tab_name(["write_proxy"]), do: gettext("Write Proxy")
defp tab_name(["coin_balances"]), do: gettext("Coin Balance History") defp tab_name(["coin_balances"]), do: gettext("Coin Balance History")
defp tab_name(["validations"]), do: gettext("Blocks Validated") defp tab_name(["validations"]), do: gettext("Blocks Validated")
defp tab_name(["logs"]), do: gettext("Logs") 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

@ -0,0 +1,13 @@
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

@ -1,7 +1,35 @@
defmodule BlockScoutWeb.SmartContractView do defmodule BlockScoutWeb.SmartContractView do
use BlockScoutWeb, :view 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) when not is_nil(function),
do:
!constructor?(function) && !event?(function) &&
(payable?(function) || nonpayable?(function))
def writeable?(function) when is_nil(function), do: false
def outputs?(outputs) when not is_nil(outputs), do: Enum.any?(outputs)
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 address?(type), do: type in ["address", "address payable"]

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

@ -124,7 +124,7 @@ msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/address/_validator_metadata_modal.html.eex:16 #: 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/templates/transaction_log/_logs.html.eex:19
#: lib/block_scout_web/views/address_view.ex:101 #: lib/block_scout_web/views/address_view.ex:104
msgid "Address" msgid "Address"
msgstr "" msgstr ""
@ -338,14 +338,14 @@ msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:18 #: 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:102
msgid "Contract Address" msgid "Contract Address"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/transaction/_pending_tile.html.eex:16 #: 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:42
#: lib/block_scout_web/views/address_view.ex:73 #: lib/block_scout_web/views/address_view.ex:76
msgid "Contract Address Pending" msgid "Contract Address Pending"
msgstr "" msgstr ""
@ -568,7 +568,8 @@ msgid "ERC-721 "
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:50 #: lib/block_scout_web/templates/smart_contract/_functions.html.eex:32
#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:67
msgid "ETH" msgid "ETH"
msgstr "" msgstr ""
@ -903,7 +904,7 @@ msgstr ""
#: lib/block_scout_web/templates/tokens/transfer/index.html.eex:14 #: 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/_tabs.html.eex:4
#: lib/block_scout_web/templates/transaction_token_transfer/index.html.eex:7 #: 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/instance/overview_view.ex:98
#: lib/block_scout_web/views/tokens/overview_view.ex:35 #: lib/block_scout_web/views/tokens/overview_view.ex:35
#: lib/block_scout_web/views/transaction_view.ex:394 #: lib/block_scout_web/views/transaction_view.ex:394
@ -1012,6 +1013,8 @@ msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/address_read_contract/index.html.eex:14 #: 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_read_proxy/index.html.eex:14
#: lib/block_scout_web/templates/address_write_contract/index.html.eex:14
#: lib/block_scout_web/templates/address_write_proxy/index.html.eex:14
#: lib/block_scout_web/templates/tokens/read_contract/index.html.eex:20 #: lib/block_scout_web/templates/tokens/read_contract/index.html.eex:20
msgid "Loading..." msgid "Loading..."
msgstr "" msgstr ""
@ -1042,7 +1045,7 @@ msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/chain/show.html.eex:52 #: lib/block_scout_web/templates/chain/show.html.eex:52
#: lib/block_scout_web/templates/layout/app.html.eex:30 #: 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:142
msgid "Market Cap" msgid "Market Cap"
msgstr "" msgstr ""
@ -1192,7 +1195,7 @@ msgid "QR Code"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:22 #: lib/block_scout_web/templates/smart_contract/_functions.html.eex:36
msgid "Query" msgid "Query"
msgstr "" msgstr ""
@ -1646,7 +1649,7 @@ msgid "View transaction %{transaction} on %{subnetwork}"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:49 #: lib/block_scout_web/templates/smart_contract/_functions.html.eex:66
msgid "WEI" msgid "WEI"
msgstr "" msgstr ""
@ -1808,7 +1811,7 @@ msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/address/_tabs.html.eex:37 #: 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/templates/address_validation/index.html.eex:13
#: lib/block_scout_web/views/address_view.ex:338 #: lib/block_scout_web/views/address_view.ex:352
msgid "Blocks Validated" msgid "Blocks Validated"
msgstr "" msgstr ""
@ -1818,18 +1821,18 @@ msgstr ""
#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:187 #: 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:126
#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:149 #: 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" msgid "Code"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/address/_tabs.html.eex:26 #: 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:351
msgid "Coin Balance History" msgid "Coin Balance History"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/views/address_view.ex:334 #: lib/block_scout_web/views/address_view.ex:346
msgid "Decompiled Code" msgid "Decompiled Code"
msgstr "" msgstr ""
@ -1838,7 +1841,7 @@ msgstr ""
#: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:19 #: 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/_tabs.html.eex:11
#: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:6 #: 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 #: lib/block_scout_web/views/transaction_view.ex:395
msgid "Internal Transactions" msgid "Internal Transactions"
msgstr "" msgstr ""
@ -1848,7 +1851,7 @@ msgstr ""
#: lib/block_scout_web/templates/address_logs/index.html.eex:8 #: 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/_tabs.html.eex:17
#: lib/block_scout_web/templates/transaction_log/index.html.eex:8 #: 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:353
#: lib/block_scout_web/views/transaction_view.ex:396 #: lib/block_scout_web/views/transaction_view.ex:396
msgid "Logs" msgid "Logs"
msgstr "" msgstr ""
@ -1856,7 +1859,7 @@ msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/address/_tabs.html.eex:62 #: 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/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 #: lib/block_scout_web/views/tokens/overview_view.ex:37
msgid "Read Contract" msgid "Read Contract"
msgstr "" msgstr ""
@ -1865,7 +1868,7 @@ msgstr ""
#: lib/block_scout_web/templates/address/_tabs.html.eex:14 #: 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/index.html.eex:8
#: lib/block_scout_web/templates/address_token_transfer/index.html.eex:11 #: 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" msgid "Tokens"
msgstr "" msgstr ""
@ -1877,7 +1880,7 @@ msgstr ""
#: lib/block_scout_web/templates/block_transaction/index.html.eex:18 #: 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/chain/show.html.eex:184
#: lib/block_scout_web/templates/layout/_topnav.html.eex:50 #: 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" msgid "Transactions"
msgstr "" msgstr ""
@ -1917,6 +1920,29 @@ msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/address/_tabs.html.eex:69 #: 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" msgid "Read Proxy"
msgstr "" 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 ""
#, elixir-format
#:
#: lib/block_scout_web/templates/smart_contract/_pending_contract_write.html.eex:12
msgid "Waiting for transaction's confirmation..."
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:36
msgid "Write"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address/_tabs.html.eex:83
#: lib/block_scout_web/views/address_view.ex:350
msgid "Write Proxy"
msgstr ""

@ -124,7 +124,7 @@ msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/address/_validator_metadata_modal.html.eex:16 #: 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/templates/transaction_log/_logs.html.eex:19
#: lib/block_scout_web/views/address_view.ex:101 #: lib/block_scout_web/views/address_view.ex:104
msgid "Address" msgid "Address"
msgstr "" msgstr ""
@ -338,14 +338,14 @@ msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:18 #: 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:102
msgid "Contract Address" msgid "Contract Address"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/transaction/_pending_tile.html.eex:16 #: 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:42
#: lib/block_scout_web/views/address_view.ex:73 #: lib/block_scout_web/views/address_view.ex:76
msgid "Contract Address Pending" msgid "Contract Address Pending"
msgstr "" msgstr ""
@ -568,7 +568,8 @@ msgid "ERC-721 "
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:50 #: lib/block_scout_web/templates/smart_contract/_functions.html.eex:32
#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:67
msgid "ETH" msgid "ETH"
msgstr "" msgstr ""
@ -903,7 +904,7 @@ msgstr ""
#: lib/block_scout_web/templates/tokens/transfer/index.html.eex:14 #: 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/_tabs.html.eex:4
#: lib/block_scout_web/templates/transaction_token_transfer/index.html.eex:7 #: 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/instance/overview_view.ex:98
#: lib/block_scout_web/views/tokens/overview_view.ex:35 #: lib/block_scout_web/views/tokens/overview_view.ex:35
#: lib/block_scout_web/views/transaction_view.ex:394 #: lib/block_scout_web/views/transaction_view.ex:394
@ -1012,6 +1013,8 @@ msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/address_read_contract/index.html.eex:14 #: 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_read_proxy/index.html.eex:14
#: lib/block_scout_web/templates/address_write_contract/index.html.eex:14
#: lib/block_scout_web/templates/address_write_proxy/index.html.eex:14
#: lib/block_scout_web/templates/tokens/read_contract/index.html.eex:20 #: lib/block_scout_web/templates/tokens/read_contract/index.html.eex:20
msgid "Loading..." msgid "Loading..."
msgstr "" msgstr ""
@ -1042,7 +1045,7 @@ msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/chain/show.html.eex:52 #: lib/block_scout_web/templates/chain/show.html.eex:52
#: lib/block_scout_web/templates/layout/app.html.eex:30 #: 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:142
msgid "Market Cap" msgid "Market Cap"
msgstr "" msgstr ""
@ -1192,7 +1195,7 @@ msgid "QR Code"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:22 #: lib/block_scout_web/templates/smart_contract/_functions.html.eex:36
msgid "Query" msgid "Query"
msgstr "" msgstr ""
@ -1646,7 +1649,7 @@ msgid "View transaction %{transaction} on %{subnetwork}"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:49 #: lib/block_scout_web/templates/smart_contract/_functions.html.eex:66
msgid "WEI" msgid "WEI"
msgstr "" msgstr ""
@ -1808,7 +1811,7 @@ msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/address/_tabs.html.eex:37 #: 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/templates/address_validation/index.html.eex:13
#: lib/block_scout_web/views/address_view.ex:338 #: lib/block_scout_web/views/address_view.ex:352
msgid "Blocks Validated" msgid "Blocks Validated"
msgstr "" msgstr ""
@ -1818,18 +1821,18 @@ msgstr ""
#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:187 #: 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:126
#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:149 #: 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" msgid "Code"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/address/_tabs.html.eex:26 #: 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:351
msgid "Coin Balance History" msgid "Coin Balance History"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/views/address_view.ex:334 #: lib/block_scout_web/views/address_view.ex:346
msgid "Decompiled Code" msgid "Decompiled Code"
msgstr "" msgstr ""
@ -1838,7 +1841,7 @@ msgstr ""
#: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:19 #: 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/_tabs.html.eex:11
#: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:6 #: 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 #: lib/block_scout_web/views/transaction_view.ex:395
msgid "Internal Transactions" msgid "Internal Transactions"
msgstr "" msgstr ""
@ -1848,7 +1851,7 @@ msgstr ""
#: lib/block_scout_web/templates/address_logs/index.html.eex:8 #: 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/_tabs.html.eex:17
#: lib/block_scout_web/templates/transaction_log/index.html.eex:8 #: 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:353
#: lib/block_scout_web/views/transaction_view.ex:396 #: lib/block_scout_web/views/transaction_view.ex:396
msgid "Logs" msgid "Logs"
msgstr "" msgstr ""
@ -1856,7 +1859,7 @@ msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/address/_tabs.html.eex:62 #: 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/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 #: lib/block_scout_web/views/tokens/overview_view.ex:37
msgid "Read Contract" msgid "Read Contract"
msgstr "" msgstr ""
@ -1865,7 +1868,7 @@ msgstr ""
#: lib/block_scout_web/templates/address/_tabs.html.eex:14 #: 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/index.html.eex:8
#: lib/block_scout_web/templates/address_token_transfer/index.html.eex:11 #: 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" msgid "Tokens"
msgstr "" msgstr ""
@ -1877,7 +1880,7 @@ msgstr ""
#: lib/block_scout_web/templates/block_transaction/index.html.eex:18 #: 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/chain/show.html.eex:184
#: lib/block_scout_web/templates/layout/_topnav.html.eex:50 #: 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" msgid "Transactions"
msgstr "" msgstr ""
@ -1917,6 +1920,29 @@ msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/address/_tabs.html.eex:69 #: 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" msgid "Read Proxy"
msgstr "" 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 ""
#, elixir-format
#:
#: lib/block_scout_web/templates/smart_contract/_pending_contract_write.html.eex:12
msgid "Waiting for transaction's confirmation..."
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:36
msgid "Write"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address/_tabs.html.eex:83
#: lib/block_scout_web/views/address_view.ex:350
msgid "Write Proxy"
msgstr ""

@ -0,0 +1,81 @@
defmodule BlockScoutWeb.AddressWriteContractControllerTest do
use BlockScoutWeb.ConnCase, async: true
alias Explorer.ExchangeRates.Token
alias Explorer.Chain.Address
import Mox
describe "GET index/3" do
setup :set_mox_global
setup do
configuration = Application.get_env(:explorer, :checksum_function)
Application.put_env(:explorer, :checksum_function, :eth)
:ok
on_exit(fn ->
Application.put_env(:explorer, :checksum_function, configuration)
end)
end
test "with invalid address hash", %{conn: conn} do
conn = get(conn, address_write_contract_path(BlockScoutWeb.Endpoint, :index, "invalid_address"))
assert html_response(conn, 404)
end
test "with valid address that is not a contract", %{conn: conn} do
address = insert(:address)
conn = get(conn, address_write_contract_path(BlockScoutWeb.Endpoint, :index, Address.checksum(address.hash)))
assert html_response(conn, 404)
end
test "successfully renders the page when the address is a contract", %{conn: conn} do
contract_address = insert(:contract_address)
transaction = insert(:transaction, from_address: contract_address) |> with_block()
insert(
:internal_transaction_create,
index: 0,
transaction: transaction,
created_contract_address: contract_address,
block_hash: transaction.block_hash,
block_index: 0
)
insert(:smart_contract, address_hash: contract_address.hash)
conn =
get(conn, address_write_contract_path(BlockScoutWeb.Endpoint, :index, Address.checksum(contract_address.hash)))
assert html_response(conn, 200)
assert contract_address.hash == conn.assigns.address.hash
assert %Token{} = conn.assigns.exchange_rate
end
test "returns not found for an unverified contract", %{conn: conn} do
contract_address = insert(:contract_address)
transaction = insert(:transaction, from_address: contract_address) |> with_block()
insert(
:internal_transaction_create,
index: 0,
transaction: transaction,
created_contract_address: contract_address,
block_hash: transaction.block_hash,
block_index: 0
)
conn =
get(conn, address_write_contract_path(BlockScoutWeb.Endpoint, :index, Address.checksum(contract_address.hash)))
assert html_response(conn, 404)
end
end
end

@ -0,0 +1,81 @@
defmodule BlockScoutWeb.AddressWriteProxyControllerTest do
use BlockScoutWeb.ConnCase, async: true
alias Explorer.ExchangeRates.Token
alias Explorer.Chain.Address
import Mox
describe "GET index/3" do
setup :set_mox_global
setup do
configuration = Application.get_env(:explorer, :checksum_function)
Application.put_env(:explorer, :checksum_function, :eth)
:ok
on_exit(fn ->
Application.put_env(:explorer, :checksum_function, configuration)
end)
end
test "with invalid address hash", %{conn: conn} do
conn = get(conn, address_write_proxy_path(BlockScoutWeb.Endpoint, :index, "invalid_address"))
assert html_response(conn, 404)
end
test "with valid address that is not a contract", %{conn: conn} do
address = insert(:address)
conn = get(conn, address_write_proxy_path(BlockScoutWeb.Endpoint, :index, Address.checksum(address.hash)))
assert html_response(conn, 404)
end
test "successfully renders the page when the address is a contract", %{conn: conn} do
contract_address = insert(:contract_address)
transaction = insert(:transaction, from_address: contract_address) |> with_block()
insert(
:internal_transaction_create,
index: 0,
transaction: transaction,
created_contract_address: contract_address,
block_hash: transaction.block_hash,
block_index: 0
)
insert(:smart_contract, address_hash: contract_address.hash)
conn =
get(conn, address_write_proxy_path(BlockScoutWeb.Endpoint, :index, Address.checksum(contract_address.hash)))
assert html_response(conn, 200)
assert contract_address.hash == conn.assigns.address.hash
assert %Token{} = conn.assigns.exchange_rate
end
test "returns not found for an unverified contract", %{conn: conn} do
contract_address = insert(:contract_address)
transaction = insert(:transaction, from_address: contract_address) |> with_block()
insert(
:internal_transaction_create,
index: 0,
transaction: transaction,
created_contract_address: contract_address,
block_hash: transaction.block_hash,
block_index: 0
)
conn =
get(conn, address_write_proxy_path(BlockScoutWeb.Endpoint, :index, Address.checksum(contract_address.hash)))
assert html_response(conn, 404)
end
end
end

@ -22,7 +22,7 @@ defmodule BlockScoutWeb.SmartContractControllerTest do
end end
test "error for invalid address" do 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 = conn =
build_conn() build_conn()
@ -49,7 +49,12 @@ defmodule BlockScoutWeb.SmartContractControllerTest do
blockchain_get_function_mock() 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 = conn =
build_conn() build_conn()
@ -65,7 +70,12 @@ defmodule BlockScoutWeb.SmartContractControllerTest do
insert(:smart_contract, address_hash: token_contract_address.hash) 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 = conn =
build_conn() build_conn()

@ -0,0 +1,19 @@
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

@ -0,0 +1,19 @@
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

@ -17,6 +17,183 @@ defmodule BlockScoutWeb.SmartContractViewTest do
end end
end end
describe "writeable?" do
test "returns true when there is write function" do
function = %{
"type" => "function",
"stateMutability" => "nonpayable",
"payable" => false,
"outputs" => [],
"name" => "upgradeTo",
"inputs" => [%{"type" => "uint256", "name" => "version"}, %{"type" => "address", "name" => "implementation"}],
"constant" => false
}
assert SmartContractView.writeable?(function)
end
test "returns false when it is not a write function" do
function = %{
"type" => "function",
"stateMutability" => "view",
"payable" => false,
"outputs" => [%{"type" => "uint256", "name" => ""}],
"name" => "version",
"inputs" => [],
"constant" => true
}
refute SmartContractView.writeable?(function)
end
test "returns false when there is no function" do
function = %{}
refute SmartContractView.writeable?(function)
end
test "returns false when there function is nil" do
function = nil
refute SmartContractView.writeable?(function)
end
end
describe "outputs?" do
test "returns true when there are outputs" do
outputs = [%{"name" => "_narcoId", "type" => "uint256"}]
assert SmartContractView.outputs?(outputs)
end
test "returns false when there are no outputs" do
outputs = []
refute SmartContractView.outputs?(outputs)
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)
end
end
describe "address?" do describe "address?" do
test "returns true when the type is equal to the string 'address'" do test "returns true when the type is equal to the string 'address'" do
type = "address" type = "address"

@ -199,7 +199,7 @@ defmodule Explorer.SmartContract.Reader do
end end
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 = values =
case function do case function do
%{"inputs" => []} -> %{"inputs" => []} ->

@ -0,0 +1,62 @@
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
|> filter_write_functions()
end
end
@spec write_functions_proxy(Hash.t()) :: [%{}]
def write_functions_proxy(contract_address_hash) do
abi =
contract_address_hash
|> Chain.address_hash_to_smart_contract()
|> Map.get(:abi)
implementation_abi = Chain.get_implementation_abi_from_proxy(contract_address_hash, abi)
case implementation_abi do
nil ->
[]
_ ->
implementation_abi
|> filter_write_functions()
end
end
def write_function?(function) do
!event?(function) && !constructor?(function) &&
(payable?(function) || 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,344 @@
defmodule Explorer.SmartContract.WriterTest do
use EthereumJSONRPC.Case
use Explorer.DataCase
import Mox
alias Explorer.SmartContract.Writer
@abi [
%{
"type" => "function",
"stateMutability" => "nonpayable",
"payable" => false,
"outputs" => [],
"name" => "upgradeTo",
"inputs" => [%{"type" => "uint256", "name" => "version"}, %{"type" => "address", "name" => "implementation"}],
"constant" => false
},
%{
"type" => "function",
"stateMutability" => "view",
"payable" => false,
"outputs" => [%{"type" => "uint256", "name" => ""}],
"name" => "version",
"inputs" => [],
"constant" => true
},
%{
"type" => "function",
"stateMutability" => "view",
"payable" => false,
"outputs" => [%{"type" => "address", "name" => ""}],
"name" => "implementation",
"inputs" => [],
"constant" => true
},
%{
"type" => "function",
"stateMutability" => "view",
"payable" => false,
"outputs" => [%{"type" => "address", "name" => ""}],
"name" => "upgradeabilityOwner",
"inputs" => [],
"constant" => true
},
%{
"type" => "function",
"stateMutability" => "payable",
"payable" => true,
"outputs" => [],
"name" => "upgradeToAndCall",
"inputs" => [
%{"type" => "uint256", "name" => "version"},
%{"type" => "address", "name" => "implementation"},
%{"type" => "bytes", "name" => "data"}
],
"constant" => false
},
%{
"type" => "function",
"stateMutability" => "nonpayable",
"payable" => false,
"outputs" => [],
"name" => "transferProxyOwnership",
"inputs" => [%{"type" => "address", "name" => "newOwner"}],
"constant" => false
},
%{"type" => "fallback", "stateMutability" => "payable", "payable" => true},
%{
"type" => "event",
"name" => "ProxyOwnershipTransferred",
"inputs" => [
%{"type" => "address", "name" => "previousOwner", "indexed" => false},
%{"type" => "address", "name" => "newOwner", "indexed" => false}
],
"anonymous" => false
},
%{
"type" => "event",
"name" => "Upgraded",
"inputs" => [
%{"type" => "uint256", "name" => "version", "indexed" => false},
%{"type" => "address", "name" => "implementation", "indexed" => true}
],
"anonymous" => false
}
]
@implementation_abi [
%{
"type" => "function",
"stateMutability" => "view",
"payable" => false,
"outputs" => [%{"type" => "uint256", "name" => ""}],
"name" => "extraReceiverAmount",
"inputs" => [%{"type" => "address", "name" => "_receiver"}],
"constant" => true
},
%{
"type" => "function",
"stateMutability" => "view",
"payable" => false,
"outputs" => [%{"type" => "uint256", "name" => ""}],
"name" => "bridgesAllowedLength",
"inputs" => [],
"constant" => true
},
%{
"type" => "function",
"stateMutability" => "pure",
"payable" => false,
"outputs" => [%{"type" => "bytes4", "name" => ""}],
"name" => "blockRewardContractId",
"inputs" => [],
"constant" => true
},
%{
"type" => "function",
"stateMutability" => "view",
"payable" => false,
"outputs" => [%{"type" => "uint256", "name" => ""}],
"name" => "mintedForAccountInBlock",
"inputs" => [%{"type" => "address", "name" => "_account"}, %{"type" => "uint256", "name" => "_blockNumber"}],
"constant" => true
},
%{
"type" => "function",
"stateMutability" => "view",
"payable" => false,
"outputs" => [%{"type" => "uint256", "name" => ""}],
"name" => "mintedForAccount",
"inputs" => [%{"type" => "address", "name" => "_account"}],
"constant" => true
},
%{
"type" => "function",
"stateMutability" => "view",
"payable" => false,
"outputs" => [%{"type" => "uint256", "name" => ""}],
"name" => "mintedInBlock",
"inputs" => [%{"type" => "uint256", "name" => "_blockNumber"}],
"constant" => true
},
%{
"type" => "function",
"stateMutability" => "view",
"payable" => false,
"outputs" => [%{"type" => "uint256", "name" => ""}],
"name" => "mintedTotally",
"inputs" => [],
"constant" => true
},
%{
"type" => "function",
"stateMutability" => "pure",
"payable" => false,
"outputs" => [%{"type" => "address[1]", "name" => ""}],
"name" => "bridgesAllowed",
"inputs" => [],
"constant" => true
},
%{
"type" => "function",
"stateMutability" => "nonpayable",
"payable" => false,
"outputs" => [],
"name" => "addExtraReceiver",
"inputs" => [%{"type" => "uint256", "name" => "_amount"}, %{"type" => "address", "name" => "_receiver"}],
"constant" => false
},
%{
"type" => "function",
"stateMutability" => "view",
"payable" => false,
"outputs" => [%{"type" => "uint256", "name" => ""}],
"name" => "mintedTotallyByBridge",
"inputs" => [%{"type" => "address", "name" => "_bridge"}],
"constant" => true
},
%{
"type" => "function",
"stateMutability" => "view",
"payable" => false,
"outputs" => [%{"type" => "address", "name" => ""}],
"name" => "extraReceiverByIndex",
"inputs" => [%{"type" => "uint256", "name" => "_index"}],
"constant" => true
},
%{
"type" => "function",
"stateMutability" => "view",
"payable" => false,
"outputs" => [%{"type" => "uint256", "name" => ""}],
"name" => "bridgeAmount",
"inputs" => [%{"type" => "address", "name" => "_bridge"}],
"constant" => true
},
%{
"type" => "function",
"stateMutability" => "view",
"payable" => false,
"outputs" => [%{"type" => "uint256", "name" => ""}],
"name" => "extraReceiversLength",
"inputs" => [],
"constant" => true
},
%{
"type" => "function",
"stateMutability" => "nonpayable",
"payable" => false,
"outputs" => [%{"type" => "address[]", "name" => ""}, %{"type" => "uint256[]", "name" => ""}],
"name" => "reward",
"inputs" => [%{"type" => "address[]", "name" => "benefactors"}, %{"type" => "uint16[]", "name" => "kind"}],
"constant" => false
},
%{
"type" => "event",
"name" => "AddedReceiver",
"inputs" => [
%{"type" => "uint256", "name" => "amount", "indexed" => false},
%{"type" => "address", "name" => "receiver", "indexed" => true},
%{"type" => "address", "name" => "bridge", "indexed" => true}
],
"anonymous" => false
}
]
doctest Explorer.SmartContract.Writer
setup :verify_on_exit!
describe "write_functions/1" do
test "fetches the smart contract write functions" do
smart_contract =
insert(
:smart_contract,
abi: @abi
)
response = Writer.write_functions(smart_contract.address_hash)
assert [
%{
"type" => "function",
"stateMutability" => "nonpayable",
"payable" => false,
"outputs" => [],
"name" => "upgradeTo",
"inputs" => [
%{"type" => "uint256", "name" => "version"},
%{"type" => "address", "name" => "implementation"}
],
"constant" => false
},
%{
"type" => "function",
"stateMutability" => "payable",
"payable" => true,
"outputs" => [],
"name" => "upgradeToAndCall",
"inputs" => [
%{"type" => "uint256", "name" => "version"},
%{"type" => "address", "name" => "implementation"},
%{"type" => "bytes", "name" => "data"}
],
"constant" => false
},
%{
"type" => "function",
"stateMutability" => "nonpayable",
"payable" => false,
"outputs" => [],
"name" => "transferProxyOwnership",
"inputs" => [%{"type" => "address", "name" => "newOwner"}],
"constant" => false
},
%{"type" => "fallback", "stateMutability" => "payable", "payable" => true}
] = response
end
end
describe "write_functions_proxy/1" do
test "fetches the smart contract proxy write functions" do
proxy_smart_contract =
insert(:smart_contract,
abi: @abi
)
implementation_contract_address = insert(:contract_address)
insert(:smart_contract,
address_hash: implementation_contract_address.hash,
abi: @implementation_abi
)
implementation_contract_address_hash_string =
Base.encode16(implementation_contract_address.hash.bytes, case: :lower)
expect(
EthereumJSONRPC.Mox,
:json_rpc,
fn [%{id: id, method: _, params: [%{data: _, to: _}, _]}], _options ->
{:ok,
[
%{
id: id,
jsonrpc: "2.0",
result: "0x000000000000000000000000" <> implementation_contract_address_hash_string
}
]}
end
)
response = Writer.write_functions_proxy(proxy_smart_contract.address_hash)
assert [
%{
"type" => "function",
"stateMutability" => "nonpayable",
"payable" => false,
"outputs" => [],
"name" => "addExtraReceiver",
"inputs" => [
%{"type" => "uint256", "name" => "_amount"},
%{"type" => "address", "name" => "_receiver"}
],
"constant" => false
},
%{
"type" => "function",
"stateMutability" => "nonpayable",
"payable" => false,
"outputs" => [%{"type" => "address[]", "name" => ""}, %{"type" => "uint256[]", "name" => ""}],
"name" => "reward",
"inputs" => [
%{"type" => "address[]", "name" => "benefactors"},
%{"type" => "uint16[]", "name" => "kind"}
],
"constant" => false
}
] = response
end
end
end
Loading…
Cancel
Save