commit
69fccf530a
@ -1,75 +1,191 @@ |
|||||||
import $ from 'jquery' |
import $ from 'jquery' |
||||||
|
|
||||||
$(function () { |
let $currentModal = null |
||||||
$('.js-become-candidate').on('click', function () { |
let modalLocked = false |
||||||
$('#becomeCandidateModal').modal() |
|
||||||
}) |
const spinner = |
||||||
|
` |
||||||
$('.js-validator-info-modal').on('click', function () { |
<span class="loading-spinner-small mr-2"> |
||||||
$('#validatorInfoModal').modal() |
<span class="loading-spinner-block-1"></span> |
||||||
}) |
<span class="loading-spinner-block-2"></span> |
||||||
|
</span> |
||||||
$('.js-move-stake').on('click', function () { |
` |
||||||
$('#errorStatusModal').modal() |
|
||||||
}) |
$(document.body).on('hide.bs.modal', e => { |
||||||
|
if (modalLocked) { |
||||||
$('.js-remove-pool').on('click', function () { |
e.preventDefault() |
||||||
$('#warningStatusModal').modal() |
e.stopPropagation() |
||||||
}) |
return false |
||||||
|
|
||||||
$('.js-copy-address').on('click', function () { |
|
||||||
$('#successStatusModal').modal() |
|
||||||
}) |
|
||||||
|
|
||||||
$('.js-stake-stake').on('click', function () { |
|
||||||
const modal = '#stakeModal' |
|
||||||
const progress = parseInt($(`${modal} .js-stakes-progress-data-progress`).text()) |
|
||||||
const total = parseInt($(`${modal} .js-stakes-progress-data-total`).text()) |
|
||||||
|
|
||||||
$(modal).modal() |
|
||||||
|
|
||||||
setupStakesProgress(progress, total, modal) |
|
||||||
}) |
|
||||||
|
|
||||||
$('.js-withdraw-stake').on('click', function () { |
|
||||||
const modal = '#withdrawModal' |
|
||||||
const progress = parseInt($(`${modal} .js-stakes-progress-data-progress`).text()) |
|
||||||
const total = parseInt($(`${modal} .js-stakes-progress-data-total`).text()) |
|
||||||
|
|
||||||
$(modal).modal() |
|
||||||
|
|
||||||
setupStakesProgress(progress, total, modal) |
|
||||||
}) |
|
||||||
|
|
||||||
function setupStakesProgress (progress, total, modal) { |
|
||||||
// const stakeProgress = $(`${modal} .js-stakes-progress`)
|
|
||||||
// const primaryColor = $('.btn-full-primary').css('background-color')
|
|
||||||
// const backgroundColors = [
|
|
||||||
// primaryColor,
|
|
||||||
// 'rgba(202, 199, 226, 0.5)'
|
|
||||||
// ]
|
|
||||||
// const progressBackground = total - progress
|
|
||||||
|
|
||||||
// // eslint-disable-next-line no-unused-vars
|
|
||||||
// const myChart = new window.Chart(stakeProgress, {
|
|
||||||
// type: 'doughnut',
|
|
||||||
// data: {
|
|
||||||
// datasets: [{
|
|
||||||
// data: [progress, progressBackground],
|
|
||||||
// backgroundColor: backgroundColors,
|
|
||||||
// hoverBackgroundColor: backgroundColors,
|
|
||||||
// borderWidth: 0
|
|
||||||
// }]
|
|
||||||
// },
|
|
||||||
// options: {
|
|
||||||
// cutoutPercentage: 80,
|
|
||||||
// legend: {
|
|
||||||
// display: false
|
|
||||||
// },
|
|
||||||
// tooltips: {
|
|
||||||
// enabled: false
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
} |
} |
||||||
|
|
||||||
|
$currentModal = null |
||||||
}) |
}) |
||||||
|
|
||||||
|
export function currentModal () { |
||||||
|
return $currentModal |
||||||
|
} |
||||||
|
|
||||||
|
export function openModal ($modal, unclosable) { |
||||||
|
// Hide all tooltips before showing a modal,
|
||||||
|
// since they are sticking on top of modal
|
||||||
|
$('.tooltip').tooltip('hide') |
||||||
|
|
||||||
|
if (unclosable) { |
||||||
|
$('.close-modal, .modal-status-button-wrapper', $modal).addClass('hidden') |
||||||
|
$('.modal-status-text', $modal).addClass('m-b-0') |
||||||
|
} |
||||||
|
|
||||||
|
if ($currentModal) { |
||||||
|
modalLocked = false |
||||||
|
|
||||||
|
$currentModal |
||||||
|
.one('hidden.bs.modal', () => { |
||||||
|
$modal.modal('show') |
||||||
|
$currentModal = $modal |
||||||
|
if (unclosable) { |
||||||
|
modalLocked = true |
||||||
|
} |
||||||
|
}) |
||||||
|
.modal('hide') |
||||||
|
} else { |
||||||
|
$modal.modal('show') |
||||||
|
$currentModal = $modal |
||||||
|
if (unclosable) { |
||||||
|
modalLocked = true |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export function openModalWithMessage ($modal, unclosable, message) { |
||||||
|
$modal.find('.modal-message').text(message) |
||||||
|
openModal($modal, unclosable) |
||||||
|
} |
||||||
|
|
||||||
|
export function lockModal ($modal, $submitButton = null, spinnerText = '') { |
||||||
|
$modal.find('.close-modal').attr('disabled', true) |
||||||
|
|
||||||
|
const $button = $submitButton || $modal.find('.btn-add-full') |
||||||
|
|
||||||
|
$button |
||||||
|
.attr('data-text', $button.text()) |
||||||
|
.attr('disabled', true) |
||||||
|
|
||||||
|
const $span = $('span', $button) |
||||||
|
const waitHtml = spinner + (spinnerText ? ` ${spinnerText}` : '') |
||||||
|
|
||||||
|
if ($span.length) { |
||||||
|
$('svg', $button).hide() |
||||||
|
$span.html(waitHtml) |
||||||
|
} else { |
||||||
|
$button.html(waitHtml) |
||||||
|
} |
||||||
|
|
||||||
|
modalLocked = true |
||||||
|
} |
||||||
|
|
||||||
|
export function unlockModal ($modal, $submitButton = null) { |
||||||
|
$modal.find('.close-modal').attr('disabled', false) |
||||||
|
|
||||||
|
const $button = $submitButton || $modal.find('.btn-add-full') |
||||||
|
const buttonText = $button.attr('data-text') |
||||||
|
|
||||||
|
$button.attr('disabled', false) |
||||||
|
|
||||||
|
const $span = $('span', $button) |
||||||
|
if ($span.length) { |
||||||
|
$('svg', $button).show() |
||||||
|
$span.text(buttonText) |
||||||
|
} else { |
||||||
|
$button.text(buttonText) |
||||||
|
} |
||||||
|
|
||||||
|
modalLocked = false |
||||||
|
} |
||||||
|
|
||||||
|
export function openErrorModal (title, text, unclosable) { |
||||||
|
const $modal = $('#errorStatusModal') |
||||||
|
$modal.find('.modal-status-title').text(title) |
||||||
|
$modal.find('.modal-status-text').html(text) |
||||||
|
openModal($modal, unclosable) |
||||||
|
} |
||||||
|
|
||||||
|
export function openWarningModal (title, text) { |
||||||
|
const $modal = $('#warningStatusModal') |
||||||
|
$modal.find('.modal-status-title').text(title) |
||||||
|
$modal.find('.modal-status-text').html(text) |
||||||
|
openModal($modal) |
||||||
|
} |
||||||
|
|
||||||
|
export function openSuccessModal (title, text) { |
||||||
|
const $modal = $('#successStatusModal') |
||||||
|
$modal.find('.modal-status-title').text(title) |
||||||
|
$modal.find('.modal-status-text').html(text) |
||||||
|
openModal($modal) |
||||||
|
} |
||||||
|
|
||||||
|
export function openQuestionModal (title, text, acceptCallback = null, exceptCallback = null, acceptText = 'Yes', exceptText = 'No') { |
||||||
|
const $modal = $('#questionStatusModal') |
||||||
|
const $closeButton = $modal.find('.close-modal') |
||||||
|
|
||||||
|
$closeButton.attr('disabled', false) |
||||||
|
|
||||||
|
$modal.find('.modal-status-title').text(title) |
||||||
|
$modal.find('.modal-status-text').text(text) |
||||||
|
|
||||||
|
const $accept = $modal.find('.btn-line.accept') |
||||||
|
const $except = $modal.find('.btn-line.except') |
||||||
|
|
||||||
|
$accept |
||||||
|
.removeAttr('data-dismiss') |
||||||
|
.removeAttr('disabled') |
||||||
|
.unbind('click') |
||||||
|
.find('.btn-line-text').text(acceptText) |
||||||
|
|
||||||
|
$except |
||||||
|
.removeAttr('data-dismiss') |
||||||
|
.removeAttr('disabled') |
||||||
|
.unbind('click') |
||||||
|
.find('.btn-line-text').text(exceptText) |
||||||
|
|
||||||
|
if (acceptCallback) { |
||||||
|
$accept.on('click', event => { |
||||||
|
$closeButton.attr('disabled', true) |
||||||
|
|
||||||
|
$accept |
||||||
|
.unbind('click') |
||||||
|
.attr('disabled', true) |
||||||
|
.find('.btn-line-text').html(spinner) |
||||||
|
$except |
||||||
|
.unbind('click') |
||||||
|
.removeAttr('data-dismiss') |
||||||
|
.attr('disabled', true) |
||||||
|
|
||||||
|
modalLocked = true |
||||||
|
acceptCallback($modal, event) |
||||||
|
}) |
||||||
|
} else { |
||||||
|
$accept.attr('data-dismiss', 'modal') |
||||||
|
} |
||||||
|
|
||||||
|
if (exceptCallback) { |
||||||
|
$except.on('click', event => { |
||||||
|
$closeButton.attr('disabled', true) |
||||||
|
|
||||||
|
$except |
||||||
|
.unbind('click') |
||||||
|
.attr('disabled', true) |
||||||
|
.find('.btn-line-text').html(spinner) |
||||||
|
$accept |
||||||
|
.unbind('click') |
||||||
|
.attr('disabled', true) |
||||||
|
.removeAttr('data-dismiss') |
||||||
|
|
||||||
|
modalLocked = true |
||||||
|
exceptCallback($modal, event) |
||||||
|
}) |
||||||
|
} else { |
||||||
|
$except.attr('data-dismiss', 'modal') |
||||||
|
} |
||||||
|
|
||||||
|
openModal($modal) |
||||||
|
} |
||||||
|
@ -0,0 +1,141 @@ |
|||||||
|
import $ from 'jquery' |
||||||
|
import ethNetProps from 'eth-net-props' |
||||||
|
import { walletEnabled, getCurrentAccount } from './write.js' |
||||||
|
import { openErrorModal, openWarningModal, openSuccessModal, openModalWithMessage } from '../modals.js' |
||||||
|
|
||||||
|
const WEI_MULTIPLIER = 10 ** 18 |
||||||
|
|
||||||
|
const loadFunctions = (element) => { |
||||||
|
const $element = $(element) |
||||||
|
const url = $element.data('url') |
||||||
|
const hash = $element.data('hash') |
||||||
|
const type = $element.data('type') |
||||||
|
const action = $element.data('action') |
||||||
|
|
||||||
|
$.get( |
||||||
|
url, |
||||||
|
{ hash: hash, type: type, action: action }, |
||||||
|
response => $element.html(response) |
||||||
|
) |
||||||
|
.done(function () { |
||||||
|
$('[data-function]').each((_, element) => { |
||||||
|
readWriteFunction(element) |
||||||
|
}) |
||||||
|
}) |
||||||
|
.fail(function (response) { |
||||||
|
$element.html(response.statusText) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
const readWriteFunction = (element) => { |
||||||
|
const $element = $(element) |
||||||
|
const $form = $element.find('[data-function-form]') |
||||||
|
|
||||||
|
const $responseContainer = $element.find('[data-function-response]') |
||||||
|
|
||||||
|
$form.on('submit', (event) => { |
||||||
|
const action = $form.data('action') |
||||||
|
event.preventDefault() |
||||||
|
|
||||||
|
if (action === 'read') { |
||||||
|
const url = $form.data('url') |
||||||
|
const $functionName = $form.find('input[name=function_name]') |
||||||
|
const $functionInputs = $form.find('input[name=function_input]') |
||||||
|
|
||||||
|
const args = $.map($functionInputs, element => { |
||||||
|
return $(element).val() |
||||||
|
}) |
||||||
|
|
||||||
|
const data = { |
||||||
|
function_name: $functionName.val(), |
||||||
|
args |
||||||
|
} |
||||||
|
|
||||||
|
$.get(url, data, response => $responseContainer.html(response)) |
||||||
|
} else if (action === 'write') { |
||||||
|
const chainId = $form.data('chainId') |
||||||
|
walletEnabled() |
||||||
|
.then((isWalletEnabled) => { |
||||||
|
if (isWalletEnabled) { |
||||||
|
const functionName = $form.find('input[name=function_name]').val() |
||||||
|
|
||||||
|
const $functionInputs = $form.find('input[name=function_input]') |
||||||
|
const $functionInputsExceptTxValue = $functionInputs.filter(':not([tx-value])') |
||||||
|
const args = $.map($functionInputsExceptTxValue, element => $(element).val()) |
||||||
|
|
||||||
|
const $txValue = $functionInputs.filter('[tx-value]:first') |
||||||
|
|
||||||
|
const txValue = $txValue && $txValue.val() && parseFloat($txValue.val()) * WEI_MULTIPLIER |
||||||
|
|
||||||
|
const contractAddress = $form.data('contract-address') |
||||||
|
const implementationAbi = $form.data('implementation-abi') |
||||||
|
const parentAbi = $form.data('contract-abi') |
||||||
|
const $parent = $('[data-smart-contract-functions]') |
||||||
|
const contractType = $parent.data('type') |
||||||
|
const contractAbi = contractType === 'proxy' ? implementationAbi : parentAbi |
||||||
|
|
||||||
|
window.web3.eth.getChainId() |
||||||
|
.then(chainIdFromWallet => { |
||||||
|
if (chainId !== chainIdFromWallet) { |
||||||
|
const networkDisplayNameFromWallet = ethNetProps.props.getNetworkDisplayName(chainIdFromWallet) |
||||||
|
const networkDisplayName = ethNetProps.props.getNetworkDisplayName(chainId) |
||||||
|
return Promise.reject(new Error(`You connected to ${networkDisplayNameFromWallet} chain in the wallet, but the current instance of Blockscout is for ${networkDisplayName} chain`)) |
||||||
|
} else { |
||||||
|
return getCurrentAccount() |
||||||
|
} |
||||||
|
}) |
||||||
|
.then(currentAccount => { |
||||||
|
let methodToCall |
||||||
|
|
||||||
|
if (functionName) { |
||||||
|
const TargetContract = new window.web3.eth.Contract(contractAbi, contractAddress) |
||||||
|
methodToCall = TargetContract.methods[functionName](...args).send({ from: currentAccount, value: txValue || 0 }) |
||||||
|
} else { |
||||||
|
const txParams = { |
||||||
|
from: currentAccount, |
||||||
|
to: contractAddress, |
||||||
|
value: txValue || 0 |
||||||
|
} |
||||||
|
methodToCall = window.web3.eth.sendTransaction(txParams) |
||||||
|
} |
||||||
|
|
||||||
|
methodToCall |
||||||
|
.on('error', function (error) { |
||||||
|
openErrorModal(`Error in sending transaction for method "${functionName}"`, formatError(error), false) |
||||||
|
}) |
||||||
|
.on('transactionHash', function (txHash) { |
||||||
|
openModalWithMessage($element.find('#pending-contract-write'), true, txHash) |
||||||
|
const getTxReceipt = (txHash) => { |
||||||
|
window.web3.eth.getTransactionReceipt(txHash) |
||||||
|
.then(txReceipt => { |
||||||
|
if (txReceipt) { |
||||||
|
openSuccessModal('Success', `Successfully sent <a href="/tx/${txHash}">transaction</a> for method "${functionName}"`) |
||||||
|
clearInterval(txReceiptPollingIntervalId) |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
const txReceiptPollingIntervalId = setInterval(() => { getTxReceipt(txHash) }, 5 * 1000) |
||||||
|
}) |
||||||
|
}) |
||||||
|
.catch(error => { |
||||||
|
openWarningModal('Unauthorized', formatError(error)) |
||||||
|
}) |
||||||
|
} else { |
||||||
|
openWarningModal('Unauthorized', 'You haven\'t approved the reading of account list from your MetaMask or MetaMask/Nifty wallet is locked or is not installed.') |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
const formatError = (error) => { |
||||||
|
let { message } = error |
||||||
|
message = message && message.split('Error: ').length > 1 ? message.split('Error: ')[1] : message |
||||||
|
return message |
||||||
|
} |
||||||
|
|
||||||
|
const container = $('[data-smart-contract-functions]') |
||||||
|
|
||||||
|
if (container.length) { |
||||||
|
loadFunctions(container) |
||||||
|
} |
@ -1,3 +1,3 @@ |
|||||||
import './read_only_functions' |
import './functions' |
||||||
import './wei_ether_converter' |
import './wei_ether_converter' |
||||||
import '../../app' |
import '../../app' |
||||||
|
@ -1,54 +0,0 @@ |
|||||||
import $ from 'jquery' |
|
||||||
|
|
||||||
const loadFunctions = (element) => { |
|
||||||
const $element = $(element) |
|
||||||
const url = $element.data('url') |
|
||||||
const hash = $element.data('hash') |
|
||||||
const type = $element.data('type') |
|
||||||
|
|
||||||
$.get( |
|
||||||
url, |
|
||||||
{ hash: hash, type: type }, |
|
||||||
response => $element.html(response) |
|
||||||
) |
|
||||||
.done(function () { |
|
||||||
$('[data-function]').each((_, element) => { |
|
||||||
readFunction(element) |
|
||||||
}) |
|
||||||
}) |
|
||||||
.fail(function (response) { |
|
||||||
$element.html(response.statusText) |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
const readFunction = (element) => { |
|
||||||
const $element = $(element) |
|
||||||
const $form = $element.find('[data-function-form]') |
|
||||||
|
|
||||||
const $responseContainer = $element.find('[data-function-response]') |
|
||||||
|
|
||||||
$form.on('submit', (event) => { |
|
||||||
event.preventDefault() |
|
||||||
|
|
||||||
const url = $form.data('url') |
|
||||||
const $functionName = $form.find('input[name=function_name]') |
|
||||||
const $functionInputs = $form.find('input[name=function_input]') |
|
||||||
|
|
||||||
const args = $.map($functionInputs, element => { |
|
||||||
return $(element).val() |
|
||||||
}) |
|
||||||
|
|
||||||
const data = { |
|
||||||
function_name: $functionName.val(), |
|
||||||
args |
|
||||||
} |
|
||||||
|
|
||||||
$.get(url, data, response => $responseContainer.html(response)) |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
const container = $('[data-smart-contract-functions]') |
|
||||||
|
|
||||||
if (container.length) { |
|
||||||
loadFunctions(container) |
|
||||||
} |
|
@ -0,0 +1,29 @@ |
|||||||
|
import Web3 from 'web3' |
||||||
|
|
||||||
|
export const walletEnabled = () => { |
||||||
|
if (window.ethereum) { |
||||||
|
window.web3 = new Web3(window.ethereum) |
||||||
|
if (window.ethereum._state && window.ethereum._state.isUnlocked) { // Nifty Wallet
|
||||||
|
window.web3 = new Web3(window.web3.currentProvider) |
||||||
|
return Promise.resolve(true) |
||||||
|
} else if (window.ethereum._state && window.ethereum._state.isUnlocked === false) { // Nifty Wallet
|
||||||
|
return Promise.resolve(false) |
||||||
|
} else { |
||||||
|
window.ethereum.enable() |
||||||
|
window.web3 = new Web3(window.web3.currentProvider) |
||||||
|
return Promise.resolve(true) |
||||||
|
} |
||||||
|
} else if (window.web3) { |
||||||
|
window.web3 = new Web3(window.web3.currentProvider) |
||||||
|
return Promise.resolve(true) |
||||||
|
} else { |
||||||
|
return Promise.resolve(false) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export const getCurrentAccount = async () => { |
||||||
|
const accounts = await window.web3.eth.getAccounts() |
||||||
|
const account = accounts[0] ? accounts[0].toLowerCase() : null |
||||||
|
|
||||||
|
return account |
||||||
|
} |
@ -0,0 +1 @@ |
|||||||
|
import '../lib/smart_contract/write' |
File diff suppressed because it is too large
Load Diff
@ -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 |
defmodule BlockScoutWeb.AddressReadContractView do |
||||||
use BlockScoutWeb, :view |
use BlockScoutWeb, :view |
||||||
|
|
||||||
def queryable?(inputs), do: Enum.any?(inputs) |
def queryable?(inputs) when not is_nil(inputs), do: Enum.any?(inputs) |
||||||
|
|
||||||
|
def queryable?(inputs) when is_nil(inputs), do: false |
||||||
|
|
||||||
|
def outputs?(outputs) when not is_nil(outputs), do: Enum.any?(outputs) |
||||||
|
|
||||||
|
def outputs?(outputs) when is_nil(outputs), do: false |
||||||
|
|
||||||
def address?(type), do: type == "address" |
def address?(type), do: type == "address" |
||||||
end |
end |
||||||
|
@ -1,7 +1,13 @@ |
|||||||
defmodule BlockScoutWeb.AddressReadProxyView do |
defmodule BlockScoutWeb.AddressReadProxyView do |
||||||
use BlockScoutWeb, :view |
use BlockScoutWeb, :view |
||||||
|
|
||||||
def queryable?(inputs), do: Enum.any?(inputs) |
def queryable?(inputs) when not is_nil(inputs), do: Enum.any?(inputs) |
||||||
|
|
||||||
|
def queryable?(inputs) when is_nil(inputs), do: false |
||||||
|
|
||||||
|
def outputs?(outputs) when not is_nil(outputs), do: Enum.any?(outputs) |
||||||
|
|
||||||
|
def outputs?(outputs) when is_nil(outputs), do: false |
||||||
|
|
||||||
def address?(type), do: type == "address" |
def address?(type), do: type == "address" |
||||||
end |
end |
||||||
|
@ -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