commit
b3e1f3902e
@ -1,16 +1,22 @@ |
|||||||
:0: Unknown function 'Elixir.ExUnit.Callbacks':'__merge__'/3 |
:0: Unknown function 'Elixir.ExUnit.Callbacks':'__merge__'/3 |
||||||
:0: Unknown function 'Elixir.ExUnit.CaseTemplate':'__proxy__'/2 |
:0: Unknown function 'Elixir.ExUnit.CaseTemplate':'__proxy__'/2 |
||||||
:0: Unknown type 'Elixir.Map':t/0 |
:0: Unknown type 'Elixir.Map':t/0 |
||||||
|
:0: Unknown type 'Elixir.Hash':t/0 |
||||||
apps/ethereum_jsonrpc/lib/ethereum_jsonrpc.ex:400: Function timestamp_to_datetime/1 has no local return |
apps/ethereum_jsonrpc/lib/ethereum_jsonrpc.ex:400: Function timestamp_to_datetime/1 has no local return |
||||||
lib/explorer/repo/prometheus_logger.ex:8 |
lib/explorer/repo/prometheus_logger.ex:8 |
||||||
lib/block_scout_web/views/layout_view.ex:175 |
lib/block_scout_web/views/layout_view.ex:175 |
||||||
lib/explorer/smart_contract/publisher_worker.ex:6 |
lib/explorer/smart_contract/publisher_worker.ex:6 |
||||||
|
lib/explorer/smart_contract/verifier.ex:84 |
||||||
apps/explorer/lib/explorer/repo/prometheus_logger.ex:8: Function microseconds_time/1 has no local return |
apps/explorer/lib/explorer/repo/prometheus_logger.ex:8: Function microseconds_time/1 has no local return |
||||||
apps/explorer/lib/explorer/repo/prometheus_logger.ex:8: The call 'Elixir.System':convert_time_unit(__@1::any(),'native','microseconds') breaks the contract (integer(),time_unit() | 'native',time_unit() | 'native') -> integer() |
apps/explorer/lib/explorer/repo/prometheus_logger.ex:8: The call 'Elixir.System':convert_time_unit(__@1::any(),'native','microseconds') breaks the contract (integer(),time_unit() | 'native',time_unit() | 'native') -> integer() |
||||||
lib/block_scout_web/views/layout_view.ex:172: The call 'Elixir.Poison.Parser':'parse!'(any(),#{'keys':='atoms!'}) will never return since the success typing is (binary() | maybe_improper_list(binary() | maybe_improper_list(any(),binary() | []) | byte(),binary() | []),[{atom(),_}]) -> 'false' | 'nil' | 'true' | binary() | ['false' | 'nil' | 'true' | binary() | [any()] | number() | map()] | number() | map() and the contract is (iodata(),'Elixir.Keyword':t()) -> t() |
lib/block_scout_web/views/layout_view.ex:172: The call 'Elixir.Poison.Parser':'parse!'(any(),#{'keys':='atoms!'}) will never return since the success typing is (binary() | maybe_improper_list(binary() | maybe_improper_list(any(),binary() | []) | byte(),binary() | []),[{atom(),_}]) -> 'false' | 'nil' | 'true' | binary() | ['false' | 'nil' | 'true' | binary() | [any()] | number() | map()] | number() | map() and the contract is (iodata(),'Elixir.Keyword':t()) -> t() |
||||||
apps/explorer/lib/explorer/smart_contract/publisher_worker.ex:6: The pattern 'false' can never match the type 'true' |
apps/explorer/lib/explorer/smart_contract/publisher_worker.ex:6: The pattern 'false' can never match the type 'true' |
||||||
apps/explorer/lib/explorer/smart_contract/publisher_worker.ex:6: The test 5 == 'infinity' can never evaluate to 'true' |
apps/explorer/lib/explorer/smart_contract/publisher_worker.ex:6: The test 5 == 'infinity' can never evaluate to 'true' |
||||||
lib/block_scout_web/router.ex:1 |
lib/block_scout_web/router.ex:1 |
||||||
|
lib/block_scout_web/schema/types.ex:31 |
||||||
|
lib/phoenix/router.ex:324 |
||||||
lib/phoenix/router.ex:402 |
lib/phoenix/router.ex:402 |
||||||
lib/block_scout_web/views/layout_view.ex:143 |
lib/block_scout_web/views/layout_view.ex:138: The call 'Elixir.Poison.Parser':'parse!' |
||||||
lib/block_scout_web/schema/types.ex:31 |
lib/block_scout_web/views/layout_view.ex:224: The call 'Elixir.Poison.Parser':'parse!' |
||||||
|
lib/block_scout_web/controllers/api/rpc/transaction_controller.ex:21 |
||||||
|
lib/block_scout_web/controllers/api/rpc/transaction_controller.ex:22 |
||||||
|
@ -1,3 +1,3 @@ |
|||||||
elixir 1.10.3 |
elixir 1.10.3 |
||||||
erlang 22.2 |
erlang 22.2 |
||||||
nodejs 12.14.1 |
nodejs 12.18.2 |
||||||
|
@ -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,154 @@ |
|||||||
|
import $ from 'jquery' |
||||||
|
import ethNetProps from 'eth-net-props' |
||||||
|
import { walletEnabled, connectToWallet, getCurrentAccount, hideConnectButton } from './write.js' |
||||||
|
import { openErrorModal, openWarningModal, openSuccessModal, openModalWithMessage } from '../modals.js' |
||||||
|
import '../../pages/address' |
||||||
|
|
||||||
|
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 () { |
||||||
|
const $connect = $('[connect-metamask]') |
||||||
|
|
||||||
|
if (hideConnectButton()) { |
||||||
|
$connect.addClass('hidden') |
||||||
|
} else { |
||||||
|
$connect.removeClass('hidden') |
||||||
|
} |
||||||
|
|
||||||
|
$connect.on('click', () => { |
||||||
|
connectToWallet() |
||||||
|
}) |
||||||
|
|
||||||
|
$('[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,53 +0,0 @@ |
|||||||
import $ from 'jquery' |
|
||||||
|
|
||||||
const loadFunctions = (element) => { |
|
||||||
const $element = $(element) |
|
||||||
const url = $element.data('url') |
|
||||||
const hash = $element.data('hash') |
|
||||||
|
|
||||||
$.get( |
|
||||||
url, |
|
||||||
{ hash: hash }, |
|
||||||
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,54 @@ |
|||||||
|
import Web3 from 'web3' |
||||||
|
|
||||||
|
export const walletEnabled = () => { |
||||||
|
if (window.ethereum) { |
||||||
|
window.web3 = new Web3(window.ethereum) |
||||||
|
if (window.ethereum.isUnlocked && window.ethereum.isNiftyWallet) { // Nifty Wallet
|
||||||
|
window.web3 = new Web3(window.web3.currentProvider) |
||||||
|
return Promise.resolve(true) |
||||||
|
} else if (window.ethereum.isUnlocked === false && window.ethereum.isNiftyWallet) { // 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 connectToWallet = () => { |
||||||
|
if (window.ethereum) { |
||||||
|
window.ethereum.enable() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export const getCurrentAccount = async () => { |
||||||
|
const accounts = await window.web3.eth.getAccounts() |
||||||
|
const account = accounts[0] ? accounts[0].toLowerCase() : null |
||||||
|
|
||||||
|
return account |
||||||
|
} |
||||||
|
|
||||||
|
export const hideConnectButton = () => { |
||||||
|
if (window.ethereum) { |
||||||
|
window.web3 = new Web3(window.ethereum) |
||||||
|
if (window.ethereum.isNiftyWallet) { |
||||||
|
return true |
||||||
|
} else if (window.ethereum.isMetaMask) { |
||||||
|
if (window.ethereum.selectedAddress) { |
||||||
|
return true |
||||||
|
} else { |
||||||
|
return false |
||||||
|
} |
||||||
|
} else { |
||||||
|
return true |
||||||
|
} |
||||||
|
} else { |
||||||
|
return false |
||||||
|
} |
||||||
|
} |
@ -0,0 +1 @@ |
|||||||
|
import '../lib/smart_contract/write' |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,39 @@ |
|||||||
|
# credo:disable-for-this-file |
||||||
|
defmodule BlockScoutWeb.AddressReadProxyController 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: :read, |
||||||
|
coin_balance_status: CoinBalanceOnDemand.trigger_fetch(address), |
||||||
|
exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(), |
||||||
|
counters_path: address_path(conn, :address_counters, %{"id" => Address.checksum(address_hash)}) |
||||||
|
) |
||||||
|
else |
||||||
|
_ -> |
||||||
|
not_found(conn) |
||||||
|
end |
||||||
|
end |
||||||
|
end |
@ -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,19 @@ |
|||||||
|
<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> |
||||||
|
</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 @@ |
|||||||
|
<%= 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> |
After Width: | Height: | Size: 1.0 KiB |
@ -0,0 +1,37 @@ |
|||||||
|
<div class="table-responsive text-center"> |
||||||
|
<table style="color: black;table-layout: fixed;" summary="<%= gettext "Log Data" %>" class="table thead-light table-bordered"> |
||||||
|
<tr> |
||||||
|
<th scope="col" style="width: 110px;"><%= gettext "Name" %></th> |
||||||
|
<th scope="col" style="width: 100px;"><%= gettext "Type" %></th> |
||||||
|
<th scope="col" style="width: 75px;"><%= gettext "Indexed?" %></th> |
||||||
|
<th scope="col"><%= gettext "Data" %></th> |
||||||
|
<tr> |
||||||
|
<%= for {name, type, indexed?, value} <- @mapping do %> |
||||||
|
<tr> |
||||||
|
<td><%= name %></td> |
||||||
|
<td><%= type %></td> |
||||||
|
<td><%= indexed? %></td> |
||||||
|
<td align=left> |
||||||
|
<%= case BlockScoutWeb.ABIEncodedValueView.copy_text(type, value) do %> |
||||||
|
<% :error -> %> |
||||||
|
<%= nil %> |
||||||
|
<% copy_text -> %> |
||||||
|
<span |
||||||
|
aria-label='<%= gettext "Copy Value" %>' |
||||||
|
class="btn-copy-ico" |
||||||
|
data-clipboard-text="<%= copy_text %>" |
||||||
|
data-placement="top" |
||||||
|
data-toggle="tooltip" |
||||||
|
style="float: left;height: 20px;" |
||||||
|
> |
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32.5 32.5" width="25" height="25"> |
||||||
|
<path fill-rule="evenodd" d="M23.5 20.5a1 1 0 0 1-1-1v-9h-9a1 1 0 0 1 0-2h10a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1zm-3-7v10a1 1 0 0 1-1 1h-10a1 1 0 0 1-1-1v-10a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1zm-2 1h-8v8h8v-8z"/> |
||||||
|
</svg> |
||||||
|
</span> |
||||||
|
<% end %> |
||||||
|
<pre class="transaction-input-text pre-wrap"><code><%= BlockScoutWeb.ABIEncodedValueView.value_html(type, value) %></code></pre> |
||||||
|
</td> |
||||||
|
</tr> |
||||||
|
<% end %> |
||||||
|
</table> |
||||||
|
</div> |
@ -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 |
||||||
|
@ -0,0 +1,13 @@ |
|||||||
|
defmodule BlockScoutWeb.AddressReadProxyView 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.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,3 @@ |
|||||||
|
defmodule BlockScoutWeb.LogView do |
||||||
|
use BlockScoutWeb, :view |
||||||
|
end |
@ -0,0 +1,79 @@ |
|||||||
|
defmodule BlockScoutWeb.AddressReadProxyControllerTest 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_read_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_read_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_read_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_read_proxy_path(BlockScoutWeb.Endpoint, :index, Address.checksum(contract_address.hash))) |
||||||
|
|
||||||
|
assert html_response(conn, 404) |
||||||
|
end |
||||||
|
end |
||||||
|
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.AddressReadProxyViewTest do |
||||||
|
use BlockScoutWeb.ConnCase, async: true |
||||||
|
|
||||||
|
alias BlockScoutWeb.AddressReadProxyView |
||||||
|
|
||||||
|
describe "queryable?/1" do |
||||||
|
test "returns true if list of inputs is not empty" do |
||||||
|
assert AddressReadProxyView.queryable?([%{"name" => "argument_name", "type" => "uint256"}]) == true |
||||||
|
assert AddressReadProxyView.queryable?([]) == false |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
describe "address?/1" do |
||||||
|
test "returns true if type equals `address`" do |
||||||
|
assert AddressReadProxyView.address?("address") == true |
||||||
|
assert AddressReadProxyView.address?("uint256") == false |
||||||
|
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,305 @@ |
|||||||
|
# credo:disable-for-this-file |
||||||
|
defmodule EthereumJSONRPC.Besu do |
||||||
|
@moduledoc """ |
||||||
|
Ethereum JSONRPC methods that are only supported by [Besu](https://besu.hyperledger.org/en/stable/Reference/API-Methods). |
||||||
|
""" |
||||||
|
require Logger |
||||||
|
import EthereumJSONRPC, only: [id_to_params: 1, integer_to_quantity: 1, json_rpc: 2, request: 1] |
||||||
|
|
||||||
|
alias EthereumJSONRPC.Parity.{FetchedBeneficiaries, Traces} |
||||||
|
alias EthereumJSONRPC.{Transaction, Transactions} |
||||||
|
|
||||||
|
@behaviour EthereumJSONRPC.Variant |
||||||
|
|
||||||
|
@impl EthereumJSONRPC.Variant |
||||||
|
def fetch_beneficiaries(block_numbers, json_rpc_named_arguments) |
||||||
|
when is_list(block_numbers) and is_list(json_rpc_named_arguments) do |
||||||
|
id_to_params = |
||||||
|
block_numbers |
||||||
|
|> block_numbers_to_params_list() |
||||||
|
|> id_to_params() |
||||||
|
|
||||||
|
with {:ok, responses} <- |
||||||
|
id_to_params |
||||||
|
|> FetchedBeneficiaries.requests() |
||||||
|
|> json_rpc(json_rpc_named_arguments) do |
||||||
|
{:ok, FetchedBeneficiaries.from_responses(responses, id_to_params)} |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
@impl EthereumJSONRPC.Variant |
||||||
|
def fetch_internal_transactions(_transactions_params, _json_rpc_named_arguments), do: :ignore |
||||||
|
|
||||||
|
@doc """ |
||||||
|
Fetches the `t:Explorer.Chain.InternalTransaction.changeset/2` params from the Besu trace URL. |
||||||
|
""" |
||||||
|
@impl EthereumJSONRPC.Variant |
||||||
|
def fetch_block_internal_transactions(block_numbers, json_rpc_named_arguments) when is_list(block_numbers) do |
||||||
|
id_to_params = id_to_params(block_numbers) |
||||||
|
|
||||||
|
with {:ok, responses} <- |
||||||
|
id_to_params |
||||||
|
|> trace_replay_block_transactions_requests() |
||||||
|
|> json_rpc(json_rpc_named_arguments) do |
||||||
|
trace_replay_block_transactions_responses_to_internal_transactions_params(responses, id_to_params) |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
@impl EthereumJSONRPC.Variant |
||||||
|
def fetch_first_trace(transactions_params, json_rpc_named_arguments) when is_list(transactions_params) do |
||||||
|
id_to_params = id_to_params(transactions_params) |
||||||
|
|
||||||
|
trace_replay_transaction_response = |
||||||
|
id_to_params |
||||||
|
|> trace_replay_transaction_requests() |
||||||
|
|> json_rpc(json_rpc_named_arguments) |
||||||
|
|
||||||
|
case trace_replay_transaction_response do |
||||||
|
{:ok, responses} -> |
||||||
|
case trace_replay_transaction_responses_to_first_trace_params(responses, id_to_params) do |
||||||
|
{:ok, [first_trace]} -> |
||||||
|
%{block_hash: block_hash} = |
||||||
|
transactions_params |
||||||
|
|> Enum.at(0) |
||||||
|
|
||||||
|
{:ok, |
||||||
|
[%{first_trace: first_trace, block_hash: block_hash, json_rpc_named_arguments: json_rpc_named_arguments}]} |
||||||
|
|
||||||
|
{:error, error} -> |
||||||
|
Logger.error(inspect(error)) |
||||||
|
{:error, error} |
||||||
|
end |
||||||
|
|
||||||
|
{:error, :econnrefused} -> |
||||||
|
{:error, :econnrefused} |
||||||
|
|
||||||
|
{:error, [error]} -> |
||||||
|
Logger.error(inspect(error)) |
||||||
|
{:error, error} |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
@doc """ |
||||||
|
Fetches the pending transactions from the Besu node. |
||||||
|
|
||||||
|
*NOTE*: The pending transactions are local to the node that is contacted and may not be consistent across nodes based |
||||||
|
on the transactions that each node has seen and how each node prioritizes collating transactions into the next block. |
||||||
|
""" |
||||||
|
@impl EthereumJSONRPC.Variant |
||||||
|
@spec fetch_pending_transactions(EthereumJSONRPC.json_rpc_named_arguments()) :: |
||||||
|
{:ok, [Transaction.params()]} | {:error, reason :: term} |
||||||
|
def fetch_pending_transactions(json_rpc_named_arguments) do |
||||||
|
with {:ok, transactions} <- |
||||||
|
%{id: 1, method: "txpool_besuTransactions", params: []} |
||||||
|
|> request() |
||||||
|
|> json_rpc(json_rpc_named_arguments) do |
||||||
|
transactions_params = |
||||||
|
transactions |
||||||
|
|> Transactions.to_elixir() |
||||||
|
|> Transactions.elixir_to_params() |
||||||
|
|
||||||
|
{:ok, transactions_params} |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
defp block_numbers_to_params_list(block_numbers) when is_list(block_numbers) do |
||||||
|
Enum.map(block_numbers, &%{block_quantity: integer_to_quantity(&1)}) |
||||||
|
end |
||||||
|
|
||||||
|
defp trace_replay_block_transactions_responses_to_internal_transactions_params(responses, id_to_params) |
||||||
|
when is_list(responses) and is_map(id_to_params) do |
||||||
|
with {:ok, traces} <- trace_replay_block_transactions_responses_to_traces(responses, id_to_params) do |
||||||
|
params = |
||||||
|
traces |
||||||
|
|> Traces.to_elixir() |
||||||
|
|> Traces.elixir_to_params() |
||||||
|
|
||||||
|
{:ok, params} |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
defp trace_replay_block_transactions_responses_to_traces(responses, id_to_params) |
||||||
|
when is_list(responses) and is_map(id_to_params) do |
||||||
|
responses |
||||||
|
|> Enum.map(&trace_replay_block_transactions_response_to_traces(&1, id_to_params)) |
||||||
|
|> Enum.reduce( |
||||||
|
{:ok, []}, |
||||||
|
fn |
||||||
|
{:ok, traces}, {:ok, acc_traces_list} -> |
||||||
|
{:ok, [traces | acc_traces_list]} |
||||||
|
|
||||||
|
{:ok, _}, {:error, _} = acc_error -> |
||||||
|
acc_error |
||||||
|
|
||||||
|
{:error, reason}, {:ok, _} -> |
||||||
|
{:error, [reason]} |
||||||
|
|
||||||
|
{:error, reason}, {:error, acc_reason} -> |
||||||
|
{:error, [reason | acc_reason]} |
||||||
|
end |
||||||
|
) |
||||||
|
|> case do |
||||||
|
{:ok, traces_list} -> |
||||||
|
traces = |
||||||
|
traces_list |
||||||
|
|> Enum.reverse() |
||||||
|
|> List.flatten() |
||||||
|
|
||||||
|
{:ok, traces} |
||||||
|
|
||||||
|
{:error, reverse_reasons} -> |
||||||
|
reasons = Enum.reverse(reverse_reasons) |
||||||
|
{:error, reasons} |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
defp trace_replay_block_transactions_response_to_traces(%{id: id, result: results}, id_to_params) |
||||||
|
when is_list(results) and is_map(id_to_params) do |
||||||
|
block_number = Map.fetch!(id_to_params, id) |
||||||
|
|
||||||
|
annotated_traces = |
||||||
|
results |
||||||
|
|> Stream.with_index() |
||||||
|
|> Enum.flat_map(fn {%{"trace" => traces, "transactionHash" => transaction_hash}, transaction_index} -> |
||||||
|
traces |
||||||
|
|> Stream.with_index() |
||||||
|
|> Enum.map(fn {trace, index} -> |
||||||
|
Map.merge(trace, %{ |
||||||
|
"blockNumber" => block_number, |
||||||
|
"transactionHash" => transaction_hash, |
||||||
|
"transactionIndex" => transaction_index, |
||||||
|
"index" => index |
||||||
|
}) |
||||||
|
end) |
||||||
|
end) |
||||||
|
|
||||||
|
{:ok, annotated_traces} |
||||||
|
end |
||||||
|
|
||||||
|
defp trace_replay_block_transactions_response_to_traces(%{id: id, error: error}, id_to_params) |
||||||
|
when is_map(id_to_params) do |
||||||
|
block_number = Map.fetch!(id_to_params, id) |
||||||
|
|
||||||
|
annotated_error = |
||||||
|
Map.put(error, :data, %{ |
||||||
|
"blockNumber" => block_number |
||||||
|
}) |
||||||
|
|
||||||
|
{:error, annotated_error} |
||||||
|
end |
||||||
|
|
||||||
|
defp trace_replay_block_transactions_requests(id_to_params) when is_map(id_to_params) do |
||||||
|
Enum.map(id_to_params, fn {id, block_number} -> |
||||||
|
trace_replay_block_transactions_request(%{id: id, block_number: block_number}) |
||||||
|
end) |
||||||
|
end |
||||||
|
|
||||||
|
defp trace_replay_block_transactions_request(%{id: id, block_number: block_number}) do |
||||||
|
request(%{id: id, method: "trace_replayBlockTransactions", params: [integer_to_quantity(block_number), ["trace"]]}) |
||||||
|
end |
||||||
|
|
||||||
|
def trace_replay_transaction_responses_to_first_trace_params(responses, id_to_params) |
||||||
|
when is_list(responses) and is_map(id_to_params) do |
||||||
|
with {:ok, traces} <- trace_replay_transaction_responses_to_first_trace(responses, id_to_params) do |
||||||
|
params = |
||||||
|
traces |
||||||
|
|> Traces.to_elixir() |
||||||
|
|> Traces.elixir_to_params() |
||||||
|
|
||||||
|
{:ok, params} |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
defp trace_replay_transaction_responses_to_first_trace(responses, id_to_params) |
||||||
|
when is_list(responses) and is_map(id_to_params) do |
||||||
|
responses |
||||||
|
|> Enum.map(&trace_replay_transaction_response_to_first_trace(&1, id_to_params)) |
||||||
|
|> Enum.reduce( |
||||||
|
{:ok, []}, |
||||||
|
fn |
||||||
|
{:ok, traces}, {:ok, acc_traces_list} -> |
||||||
|
{:ok, [traces | acc_traces_list]} |
||||||
|
|
||||||
|
{:ok, _}, {:error, _} = acc_error -> |
||||||
|
acc_error |
||||||
|
|
||||||
|
{:error, reason}, {:ok, _} -> |
||||||
|
{:error, [reason]} |
||||||
|
|
||||||
|
{:error, reason}, {:error, acc_reason} -> |
||||||
|
{:error, [reason | acc_reason]} |
||||||
|
end |
||||||
|
) |
||||||
|
|> case do |
||||||
|
{:ok, traces_list} -> |
||||||
|
traces = |
||||||
|
traces_list |
||||||
|
|> Enum.reverse() |
||||||
|
|> List.flatten() |
||||||
|
|
||||||
|
{:ok, traces} |
||||||
|
|
||||||
|
{:error, reverse_reasons} -> |
||||||
|
reasons = Enum.reverse(reverse_reasons) |
||||||
|
{:error, reasons} |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
defp trace_replay_transaction_response_to_first_trace(%{id: id, result: %{"trace" => traces}}, id_to_params) |
||||||
|
when is_list(traces) and is_map(id_to_params) do |
||||||
|
%{ |
||||||
|
block_hash: block_hash, |
||||||
|
block_number: block_number, |
||||||
|
hash_data: transaction_hash, |
||||||
|
transaction_index: transaction_index |
||||||
|
} = Map.fetch!(id_to_params, id) |
||||||
|
|
||||||
|
first_trace = |
||||||
|
traces |
||||||
|
|> Stream.with_index() |
||||||
|
|> Enum.map(fn {trace, index} -> |
||||||
|
Map.merge(trace, %{ |
||||||
|
"blockHash" => block_hash, |
||||||
|
"blockNumber" => block_number, |
||||||
|
"index" => index, |
||||||
|
"transactionIndex" => transaction_index, |
||||||
|
"transactionHash" => transaction_hash |
||||||
|
}) |
||||||
|
end) |
||||||
|
|> Enum.filter(fn trace -> |
||||||
|
Map.get(trace, "index") == 0 |
||||||
|
end) |
||||||
|
|
||||||
|
{:ok, first_trace} |
||||||
|
end |
||||||
|
|
||||||
|
defp trace_replay_transaction_response_to_first_trace(%{id: id, error: error}, id_to_params) |
||||||
|
when is_map(id_to_params) do |
||||||
|
%{ |
||||||
|
block_hash: block_hash, |
||||||
|
block_number: block_number, |
||||||
|
hash_data: transaction_hash, |
||||||
|
transaction_index: transaction_index |
||||||
|
} = Map.fetch!(id_to_params, id) |
||||||
|
|
||||||
|
annotated_error = |
||||||
|
Map.put(error, :data, %{ |
||||||
|
"blockHash" => block_hash, |
||||||
|
"blockNumber" => block_number, |
||||||
|
"transactionIndex" => transaction_index, |
||||||
|
"transactionHash" => transaction_hash |
||||||
|
}) |
||||||
|
|
||||||
|
{:error, annotated_error} |
||||||
|
end |
||||||
|
|
||||||
|
defp trace_replay_transaction_requests(id_to_params) when is_map(id_to_params) do |
||||||
|
Enum.map(id_to_params, fn {id, %{hash_data: hash_data}} -> |
||||||
|
trace_replay_transaction_request(%{id: id, hash_data: hash_data}) |
||||||
|
end) |
||||||
|
end |
||||||
|
|
||||||
|
defp trace_replay_transaction_request(%{id: id, hash_data: hash_data}) do |
||||||
|
request(%{id: id, method: "trace_replayTransaction", params: [hash_data, ["trace"]]}) |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,196 @@ |
|||||||
|
defmodule EthereumJSONRPC.Besu.FetchedBeneficiaries do |
||||||
|
@moduledoc """ |
||||||
|
Beneficiaries and errors from batch requests to `trace_block`. |
||||||
|
""" |
||||||
|
|
||||||
|
import EthereumJSONRPC, only: [quantity_to_integer: 1] |
||||||
|
|
||||||
|
@doc """ |
||||||
|
Converts `responses` to `EthereumJSONRPC.FetchedBeneficiaries.t()`. |
||||||
|
|
||||||
|
responses - List with trace_block responses |
||||||
|
id_to_params - Maps request id to query params |
||||||
|
|
||||||
|
## Examples |
||||||
|
iex> EthereumJSONRPC.Besu.FetchedBeneficiaries.from_responses( |
||||||
|
...> [ |
||||||
|
...> %{ |
||||||
|
...> id: 0, |
||||||
|
...> result: [ |
||||||
|
...> %{ |
||||||
|
...> "action" => %{"author" => "0x1", "rewardType" => "external", "value" => "0x0"}, |
||||||
|
...> "blockHash" => "0xFFF", |
||||||
|
...> "blockNumber" => 12, |
||||||
|
...> "result" => nil, |
||||||
|
...> "subtraces" => 0, |
||||||
|
...> "traceAddress" => [], |
||||||
|
...> "transactionHash" => nil, |
||||||
|
...> "transactionPosition" => nil, |
||||||
|
...> "type" => "reward" |
||||||
|
...> }, |
||||||
|
...> %{ |
||||||
|
...> "action" => %{"author" => "0x2", "rewardType" => "external", "value" => "0x0"}, |
||||||
|
...> "blockHash" => "0xFFF", |
||||||
|
...> "blockNumber" => 12, |
||||||
|
...> "result" => nil, |
||||||
|
...> "subtraces" => 0, |
||||||
|
...> "traceAddress" => [], |
||||||
|
...> "transactionHash" => nil, |
||||||
|
...> "transactionPosition" => nil, |
||||||
|
...> "type" => "reward" |
||||||
|
...> } |
||||||
|
...> ] |
||||||
|
...> } |
||||||
|
...> ], |
||||||
|
...> %{0 => %{block_quantity: "0xC"}} |
||||||
|
...> ) |
||||||
|
%EthereumJSONRPC.FetchedBeneficiaries{ |
||||||
|
errors: [], |
||||||
|
params_set: #MapSet<[ |
||||||
|
%{ |
||||||
|
address_hash: "0x1", |
||||||
|
address_type: :validator, |
||||||
|
block_hash: "0xFFF", |
||||||
|
block_number: 12, |
||||||
|
reward: "0x0" |
||||||
|
}, |
||||||
|
%{ |
||||||
|
address_hash: "0x2", |
||||||
|
address_type: :emission_funds, |
||||||
|
block_hash: "0xFFF", |
||||||
|
block_number: 12, |
||||||
|
reward: "0x0" |
||||||
|
} |
||||||
|
]> |
||||||
|
} |
||||||
|
""" |
||||||
|
def from_responses(responses, id_to_params) when is_list(responses) and is_map(id_to_params) do |
||||||
|
responses |
||||||
|
|> Enum.map(&response_to_params_set(&1, id_to_params)) |
||||||
|
|> Enum.reduce( |
||||||
|
%EthereumJSONRPC.FetchedBeneficiaries{}, |
||||||
|
fn |
||||||
|
{:ok, params_set}, %EthereumJSONRPC.FetchedBeneficiaries{params_set: acc_params_set} = acc -> |
||||||
|
%EthereumJSONRPC.FetchedBeneficiaries{acc | params_set: MapSet.union(acc_params_set, params_set)} |
||||||
|
|
||||||
|
{:error, reason}, %EthereumJSONRPC.FetchedBeneficiaries{errors: errors} = acc -> |
||||||
|
%EthereumJSONRPC.FetchedBeneficiaries{acc | errors: [reason | errors]} |
||||||
|
end |
||||||
|
) |
||||||
|
end |
||||||
|
|
||||||
|
@doc """ |
||||||
|
`trace_block` requests for `id_to_params`. |
||||||
|
""" |
||||||
|
def requests(id_to_params) when is_map(id_to_params) do |
||||||
|
Enum.map(id_to_params, fn {id, %{block_quantity: block_quantity}} -> |
||||||
|
request(%{id: id, block_quantity: block_quantity}) |
||||||
|
end) |
||||||
|
end |
||||||
|
|
||||||
|
@spec response_to_params_set(%{id: id, result: nil}, %{id => %{block_quantity: block_quantity}}) :: |
||||||
|
{:error, %{code: 404, message: String.t(), data: %{block_quantity: block_quantity}}} |
||||||
|
when id: non_neg_integer(), block_quantity: String.t() |
||||||
|
defp response_to_params_set(%{id: id, result: nil}, id_to_params) when is_map(id_to_params) do |
||||||
|
%{block_quantity: block_quantity} = Map.fetch!(id_to_params, id) |
||||||
|
|
||||||
|
{:error, %{code: 404, message: "Not Found", data: %{block_quantity: block_quantity}}} |
||||||
|
end |
||||||
|
|
||||||
|
@spec response_to_params_set(%{id: id, result: list(map())}, %{id => %{block_quantity: block_quantity}}) :: |
||||||
|
{:ok, MapSet.t(EthereumJSONRPC.FetchedBeneficiary.params())} |
||||||
|
when id: non_neg_integer(), block_quantity: String.t() |
||||||
|
defp response_to_params_set(%{id: id, result: traces}, id_to_params) when is_list(traces) and is_map(id_to_params) do |
||||||
|
%{block_quantity: block_quantity} = Map.fetch!(id_to_params, id) |
||||||
|
block_number = quantity_to_integer(block_quantity) |
||||||
|
params_set = traces_to_params_set(traces, block_number) |
||||||
|
|
||||||
|
{:ok, params_set} |
||||||
|
end |
||||||
|
|
||||||
|
@spec response_to_params_set(%{id: id, error: %{code: code, message: message}}, %{ |
||||||
|
id => %{block_quantity: block_quantity} |
||||||
|
}) :: {:error, %{code: code, message: message, data: %{block_quantity: block_quantity}}} |
||||||
|
when id: non_neg_integer(), code: integer(), message: String.t(), block_quantity: String.t() |
||||||
|
defp response_to_params_set(%{id: id, error: error}, id_to_params) when is_map(id_to_params) do |
||||||
|
%{block_quantity: block_quantity} = Map.fetch!(id_to_params, id) |
||||||
|
|
||||||
|
annotated_error = Map.put(error, :data, %{block_quantity: block_quantity}) |
||||||
|
|
||||||
|
{:error, annotated_error} |
||||||
|
end |
||||||
|
|
||||||
|
defp request(%{id: id, block_quantity: block_quantity}) when is_integer(id) and is_binary(block_quantity) do |
||||||
|
EthereumJSONRPC.request(%{id: id, method: "trace_block", params: [block_quantity]}) |
||||||
|
end |
||||||
|
|
||||||
|
defp traces_to_params_set(traces, block_number) when is_list(traces) and is_integer(block_number) do |
||||||
|
traces |
||||||
|
|> Stream.filter(&(&1["type"] == "reward")) |
||||||
|
|> Stream.with_index() |
||||||
|
|> Enum.reduce(MapSet.new(), fn {trace, index}, acc -> |
||||||
|
MapSet.union(acc, trace_to_params_set(trace, block_number, index)) |
||||||
|
end) |
||||||
|
end |
||||||
|
|
||||||
|
defp trace_to_params_set( |
||||||
|
%{ |
||||||
|
"action" => %{ |
||||||
|
"rewardType" => reward_type, |
||||||
|
"author" => address_hash_data, |
||||||
|
"value" => reward_value |
||||||
|
}, |
||||||
|
"blockHash" => block_hash, |
||||||
|
"blockNumber" => block_number |
||||||
|
}, |
||||||
|
block_number, |
||||||
|
index |
||||||
|
) |
||||||
|
when is_integer(block_number) and reward_type in ~w(block external uncle) do |
||||||
|
MapSet.new([ |
||||||
|
%{ |
||||||
|
address_hash: address_hash_data, |
||||||
|
block_hash: block_hash, |
||||||
|
block_number: block_number, |
||||||
|
reward: reward_value, |
||||||
|
address_type: get_address_type(reward_type, index) |
||||||
|
} |
||||||
|
]) |
||||||
|
end |
||||||
|
|
||||||
|
# Beneficiary's address type will depend on the responses' action.rewardType, |
||||||
|
# which will vary depending on which network is being indexed |
||||||
|
# |
||||||
|
# On POA networks, rewardType will always be external and the type of the address being |
||||||
|
# rewarded will depend on its position. |
||||||
|
# First address will always be the validator's while the second will be the EmissionsFunds address |
||||||
|
# |
||||||
|
# On PoW networks, like Ethereum, the reward type will already specify the type for the |
||||||
|
# address being rewarded |
||||||
|
# The rewardType "block" will show the reward for the consensus block validator |
||||||
|
# The rewardType "uncle" will show reward for validating an uncle block |
||||||
|
defp get_address_type(reward_type, index) when reward_type == "external" and index == 0, do: :validator |
||||||
|
defp get_address_type(reward_type, index) when reward_type == "external" and index == 1, do: :emission_funds |
||||||
|
defp get_address_type(reward_type, index) when reward_type == "external" and index == 2, do: :validator |
||||||
|
defp get_address_type(reward_type, index) when reward_type == "external" and index == 3, do: :validator |
||||||
|
defp get_address_type(reward_type, index) when reward_type == "external" and index == 4, do: :validator |
||||||
|
defp get_address_type(reward_type, index) when reward_type == "external" and index == 5, do: :validator |
||||||
|
defp get_address_type(reward_type, index) when reward_type == "external" and index == 6, do: :validator |
||||||
|
defp get_address_type(reward_type, index) when reward_type == "external" and index == 7, do: :validator |
||||||
|
defp get_address_type(reward_type, index) when reward_type == "external" and index == 8, do: :validator |
||||||
|
defp get_address_type(reward_type, index) when reward_type == "external" and index == 9, do: :validator |
||||||
|
defp get_address_type(reward_type, index) when reward_type == "external" and index == 10, do: :validator |
||||||
|
defp get_address_type(reward_type, index) when reward_type == "external" and index == 11, do: :validator |
||||||
|
defp get_address_type(reward_type, index) when reward_type == "external" and index == 12, do: :validator |
||||||
|
defp get_address_type(reward_type, index) when reward_type == "external" and index == 13, do: :validator |
||||||
|
defp get_address_type(reward_type, index) when reward_type == "external" and index == 14, do: :validator |
||||||
|
defp get_address_type(reward_type, index) when reward_type == "external" and index == 15, do: :validator |
||||||
|
defp get_address_type(reward_type, index) when reward_type == "external" and index == 16, do: :validator |
||||||
|
defp get_address_type(reward_type, index) when reward_type == "external" and index == 17, do: :validator |
||||||
|
defp get_address_type(reward_type, index) when reward_type == "external" and index == 18, do: :validator |
||||||
|
defp get_address_type(reward_type, index) when reward_type == "external" and index == 19, do: :validator |
||||||
|
defp get_address_type(reward_type, index) when reward_type == "external" and index == 20, do: :validator |
||||||
|
defp get_address_type(reward_type, _index) when reward_type == "block", do: :validator |
||||||
|
defp get_address_type(reward_type, _index) when reward_type == "uncle", do: :uncle |
||||||
|
defp get_address_type(reward_type, _index) when reward_type == "emptyStep", do: :validator |
||||||
|
end |
@ -0,0 +1,25 @@ |
|||||||
|
use Mix.Config |
||||||
|
|
||||||
|
config :explorer, |
||||||
|
json_rpc_named_arguments: [ |
||||||
|
transport: EthereumJSONRPC.HTTP, |
||||||
|
transport_options: [ |
||||||
|
http: EthereumJSONRPC.HTTP.HTTPoison, |
||||||
|
url: System.get_env("ETHEREUM_JSONRPC_HTTP_URL") || "http://localhost:8545", |
||||||
|
method_to_url: [ |
||||||
|
eth_call: System.get_env("ETHEREUM_JSONRPC_TRACE_URL") || "http://localhost:8545", |
||||||
|
eth_getBalance: System.get_env("ETHEREUM_JSONRPC_TRACE_URL") || "http://localhost:8545", |
||||||
|
trace_replayTransaction: System.get_env("ETHEREUM_JSONRPC_TRACE_URL") || "http://localhost:8545" |
||||||
|
], |
||||||
|
http_options: [recv_timeout: :timer.minutes(1), timeout: :timer.minutes(1), hackney: [pool: :ethereum_jsonrpc]] |
||||||
|
], |
||||||
|
variant: EthereumJSONRPC.Besu |
||||||
|
], |
||||||
|
subscribe_named_arguments: [ |
||||||
|
transport: EthereumJSONRPC.WebSocket, |
||||||
|
transport_options: [ |
||||||
|
web_socket: EthereumJSONRPC.WebSocket.WebSocketClient, |
||||||
|
url: System.get_env("ETHEREUM_JSONRPC_WS_URL") |
||||||
|
], |
||||||
|
variant: EthereumJSONRPC.Besu |
||||||
|
] |
@ -0,0 +1,25 @@ |
|||||||
|
use Mix.Config |
||||||
|
|
||||||
|
config :explorer, |
||||||
|
json_rpc_named_arguments: [ |
||||||
|
transport: EthereumJSONRPC.HTTP, |
||||||
|
transport_options: [ |
||||||
|
http: EthereumJSONRPC.HTTP.HTTPoison, |
||||||
|
url: System.get_env("ETHEREUM_JSONRPC_HTTP_URL"), |
||||||
|
method_to_url: [ |
||||||
|
eth_call: System.get_env("ETHEREUM_JSONRPC_TRACE_URL"), |
||||||
|
eth_getBalance: System.get_env("ETHEREUM_JSONRPC_TRACE_URL"), |
||||||
|
trace_replayTransaction: System.get_env("ETHEREUM_JSONRPC_TRACE_URL") |
||||||
|
], |
||||||
|
http_options: [recv_timeout: :timer.minutes(1), timeout: :timer.minutes(1), hackney: [pool: :ethereum_jsonrpc]] |
||||||
|
], |
||||||
|
variant: EthereumJSONRPC.Besu |
||||||
|
], |
||||||
|
subscribe_named_arguments: [ |
||||||
|
transport: EthereumJSONRPC.WebSocket, |
||||||
|
transport_options: [ |
||||||
|
web_socket: EthereumJSONRPC.WebSocket.WebSocketClient, |
||||||
|
url: System.get_env("ETHEREUM_JSONRPC_WS_URL") |
||||||
|
], |
||||||
|
variant: EthereumJSONRPC.Besu |
||||||
|
] |
@ -0,0 +1,14 @@ |
|||||||
|
use Mix.Config |
||||||
|
|
||||||
|
config :explorer, |
||||||
|
transport: EthereumJSONRPC.HTTP, |
||||||
|
json_rpc_named_arguments: [ |
||||||
|
transport: EthereumJSONRPC.Mox, |
||||||
|
transport_options: [], |
||||||
|
variant: EthereumJSONRPC.Besu |
||||||
|
], |
||||||
|
subscribe_named_arguments: [ |
||||||
|
transport: EthereumJSONRPC.Mox, |
||||||
|
transport_options: [], |
||||||
|
variant: EthereumJSONRPC.Besu |
||||||
|
] |
@ -0,0 +1,93 @@ |
|||||||
|
# credo:disable-for-this-file |
||||||
|
defmodule Explorer.ChainSpec.Geth.Importer do |
||||||
|
@moduledoc """ |
||||||
|
Imports data from Geth genesis.json. |
||||||
|
""" |
||||||
|
|
||||||
|
require Logger |
||||||
|
|
||||||
|
alias EthereumJSONRPC.Blocks |
||||||
|
alias Explorer.Chain |
||||||
|
alias Explorer.Chain.Hash.Address, as: AddressHash |
||||||
|
|
||||||
|
def import_genesis_accounts(chain_spec) do |
||||||
|
balance_params = |
||||||
|
chain_spec |
||||||
|
|> genesis_accounts() |
||||||
|
|> Stream.map(fn balance_map -> |
||||||
|
Map.put(balance_map, :block_number, 0) |
||||||
|
end) |
||||||
|
|> Enum.to_list() |
||||||
|
|
||||||
|
json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments) |
||||||
|
|
||||||
|
{:ok, %Blocks{blocks_params: [%{timestamp: timestamp}]}} = |
||||||
|
EthereumJSONRPC.fetch_blocks_by_range(1..1, json_rpc_named_arguments) |
||||||
|
|
||||||
|
day = DateTime.to_date(timestamp) |
||||||
|
|
||||||
|
balance_daily_params = |
||||||
|
chain_spec |
||||||
|
|> genesis_accounts() |
||||||
|
|> Stream.map(fn balance_map -> |
||||||
|
Map.put(balance_map, :day, day) |
||||||
|
end) |
||||||
|
|> Enum.to_list() |
||||||
|
|
||||||
|
address_params = |
||||||
|
balance_params |
||||||
|
|> Stream.map(fn %{address_hash: hash} = map -> |
||||||
|
Map.put(map, :hash, hash) |
||||||
|
end) |
||||||
|
|> Enum.to_list() |
||||||
|
|
||||||
|
params = %{ |
||||||
|
address_coin_balances: %{params: balance_params}, |
||||||
|
address_coin_balances_daily: %{params: balance_daily_params}, |
||||||
|
addresses: %{params: address_params} |
||||||
|
} |
||||||
|
|
||||||
|
Chain.import(params) |
||||||
|
end |
||||||
|
|
||||||
|
def genesis_accounts(chain_spec) do |
||||||
|
accounts = chain_spec["alloc"] |
||||||
|
|
||||||
|
if accounts do |
||||||
|
parse_accounts(accounts) |
||||||
|
else |
||||||
|
Logger.warn(fn -> "No accounts are defined in genesis" end) |
||||||
|
|
||||||
|
[] |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
defp parse_accounts(accounts) do |
||||||
|
accounts |
||||||
|
|> Stream.filter(fn {_address, map} -> |
||||||
|
!is_nil(map["balance"]) |
||||||
|
end) |
||||||
|
|> Stream.map(fn {address, %{"balance" => value} = params} -> |
||||||
|
formatted_address = if String.starts_with?(address, "0x"), do: address, else: "0x" <> address |
||||||
|
{:ok, address_hash} = AddressHash.cast(formatted_address) |
||||||
|
balance = parse_number(value) |
||||||
|
|
||||||
|
code = params["code"] |
||||||
|
|
||||||
|
%{address_hash: address_hash, value: balance, contract_code: code} |
||||||
|
end) |
||||||
|
|> Enum.to_list() |
||||||
|
end |
||||||
|
|
||||||
|
defp parse_number("0x" <> hex_number) do |
||||||
|
{number, ""} = Integer.parse(hex_number, 16) |
||||||
|
|
||||||
|
number |
||||||
|
end |
||||||
|
|
||||||
|
defp parse_number(string_number) do |
||||||
|
{number, ""} = Integer.parse(string_number, 10) |
||||||
|
|
||||||
|
number |
||||||
|
end |
||||||
|
end |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue