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

@ -2,6 +2,8 @@
### Features
- [#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
### Fixes

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

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

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

@ -1,75 +1,191 @@
import $ from 'jquery'
$(function () {
$('.js-become-candidate').on('click', function () {
$('#becomeCandidateModal').modal()
})
$('.js-validator-info-modal').on('click', function () {
$('#validatorInfoModal').modal()
})
$('.js-move-stake').on('click', function () {
$('#errorStatusModal').modal()
})
$('.js-remove-pool').on('click', function () {
$('#warningStatusModal').modal()
})
$('.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
// }
// }
// })
let $currentModal = null
let modalLocked = false
const spinner =
`
<span class="loading-spinner-small mr-2">
<span class="loading-spinner-block-1"></span>
<span class="loading-spinner-block-2"></span>
</span>
`
$(document.body).on('hide.bs.modal', e => {
if (modalLocked) {
e.preventDefault()
e.stopPropagation()
return 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 '../../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",
"chart.js": "^2.9.3",
"clipboard": "^2.0.4",
"eth-net-props": "^1.0.33",
"highlight.js": "^9.16.2",
"highlightjs-solidity": "^1.0.8",
"humps": "^2.0.1",
@ -39,7 +40,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

@ -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
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
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
Reader.read_only_functions_proxy(address_hash)
address.hash
|> Chain.get_implementation_abi_from_proxy(address.smart_contract.abi)
|> Poison.encode!()
else
Reader.read_only_functions(address_hash)
[]
end
conn
@ -20,8 +45,11 @@ defmodule BlockScoutWeb.SmartContractController do
|> put_layout(false)
|> render(
"_functions.html",
read_only_functions: read_only_functions,
address: address
read_only_functions: functions,
address: address,
contract_abi: contract_abi,
implementation_abi: implementation_abi,
contract_type: contract_type
)
else
:error ->

@ -13,6 +13,8 @@ defmodule BlockScoutWeb.Tokens.ReadContractController do
render(
conn,
"index.html",
type: :regular,
action: :read,
token: Market.add_price(token),
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)}")
%>
<% 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>

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

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

@ -8,51 +8,69 @@
&#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
%>
<%= 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"] %>' />
<%= 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">
<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>
<% 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>
<div class='p-2 text-muted <%= if (queryable?(function["inputs"]) == true), do: "w-100" %>'>
<%= if (queryable?(function["inputs"])), do: raw "&#8627;" %>
<%= 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;" %>
<%= for output <- function["outputs"] do %>
<%= output["type"] %>
<% end %>
</div>
<%= for output <- function["outputs"] do %>
<%= output["type"] %>
<% end %>
</div>
<% end %>
<div data-function-response></div>
</div>
<% else %>
<span class="py-2">
<%= for output <- function["outputs"] do %>
<%= if address?(output["type"]) do %>
<%= link(
output["value"],
to: address_path(@conn, :show, output["value"])
) %>
<% else %>
<%= if output["type"] == "uint256" do %>
<div data-wei-ether-converter>
<span data-conversion-unit><%= output["value"] %></span>
<span class="py-2 px-2">
<input class="wei-ether" type="checkbox" autocomplete="off">
<span class="d-inline-block" data-conversion-text-wei><%= gettext("WEI")%></span>
<span class="d-none" data-conversion-text-eth><%= gettext("ETH")%></span>
</span>
</div>
<% else %>
<%= values(output["value"], output["type"]) %>
<% end %>
<%= if outputs?(function["outputs"]) do %>
<%= for output <- function["outputs"] do %>
<%= if address?(output["type"]) do %>
<%= link(
output["value"],
to: address_path(@conn, :show, output["value"])
) %>
<% else %>
<%= if output["type"] == "uint256" do %>
<div data-wei-ether-converter>
<span data-conversion-unit><%= output["value"] %></span>
<span class="py-2 px-2">
<input class="wei-ether" type="checkbox" autocomplete="off">
<span class="d-inline-block" data-conversion-text-wei><%= gettext("WEI")%></span>
<span class="d-none" data-conversion-text-eth><%= gettext("ETH")%></span>
</span>
</div>
<% else %>
<%= values(output["value"], output["type"]) %>
<% end %>
<% end %>
<% end %>
<% end %>
</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">
<%= render OverviewView, "_tabs.html", assigns %>
<!-- 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">
<span class="loading-spinner-small mr-2">
<span class="loading-spinner-block-1"></span>

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

@ -8,6 +8,7 @@ defmodule BlockScoutWeb.AddressView do
alias Explorer.Chain.{Address, Hash, InternalTransaction, SmartContract, Token, TokenTransfer, Transaction, Wei}
alias Explorer.Chain.Block.Reward
alias Explorer.ExchangeRates.Token, as: TokenExchangeRate
alias Explorer.SmartContract.Writer
@dialyzer :no_match
@ -19,6 +20,8 @@ defmodule BlockScoutWeb.AddressView do
"token_transfers",
"read_contract",
"read_proxy",
"write_contract",
"write_proxy",
"tokens",
"transactions",
"validations"
@ -235,6 +238,15 @@ 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,
&Writer.write_function?(&1)
)
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,8 @@ 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(["write_proxy"]), do: gettext("Write Proxy")
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

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

@ -132,6 +132,20 @@ defmodule BlockScoutWeb.WebRouter do
as: :read_proxy
)
resources(
"/write_contract",
AddressWriteContractController,
only: [:index, :show],
as: :write_contract
)
resources(
"/write_proxy",
AddressWriteProxyController,
only: [:index, :show],
as: :write_proxy
)
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:104
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:102
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:42
#: lib/block_scout_web/views/address_view.ex:76
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:32
#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:67
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,8 @@ 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/address_write_proxy/index.html.eex:14
#: lib/block_scout_web/templates/tokens/read_contract/index.html.eex:20
msgid "Loading..."
msgstr ""
@ -1042,7 +1045,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:142
msgid "Market Cap"
msgstr ""
@ -1192,7 +1195,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:36
msgid "Query"
msgstr ""
@ -1646,7 +1649,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:66
msgid "WEI"
msgstr ""
@ -1808,7 +1811,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:352
msgid "Blocks Validated"
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/_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:351
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 +1841,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 +1851,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:353
#: lib/block_scout_web/views/transaction_view.ex:396
msgid "Logs"
msgstr ""
@ -1856,7 +1859,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 +1868,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 +1880,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 +1920,29 @@ 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 ""
#, 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
#: 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:104
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:102
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:42
#: lib/block_scout_web/views/address_view.ex:76
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:32
#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:67
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,8 @@ 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/address_write_proxy/index.html.eex:14
#: lib/block_scout_web/templates/tokens/read_contract/index.html.eex:20
msgid "Loading..."
msgstr ""
@ -1042,7 +1045,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:142
msgid "Market Cap"
msgstr ""
@ -1192,7 +1195,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:36
msgid "Query"
msgstr ""
@ -1646,7 +1649,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:66
msgid "WEI"
msgstr ""
@ -1808,7 +1811,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:352
msgid "Blocks Validated"
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/_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:351
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 +1841,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 +1851,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:353
#: lib/block_scout_web/views/transaction_view.ex:396
msgid "Logs"
msgstr ""
@ -1856,7 +1859,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 +1868,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 +1880,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 +1920,29 @@ 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 ""
#, 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
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()

@ -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
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
test "returns true when the type is equal to the string 'address'" do
type = "address"

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