commit
69fccf530a
@ -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
@ -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 |
@ -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> |
@ -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> |
@ -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 |
||||
|
@ -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 |
@ -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 |
@ -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 |
@ -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…
Reference in new issue