Merge branch 'master' into ab-autoappending-constructor-arguments

pull/2821/head
Victor Baranov 5 years ago committed by GitHub
commit 566796552e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      CHANGELOG.md
  2. 2
      apps/block_scout_web/README.md
  3. 4
      apps/block_scout_web/assets/.babelrc
  4. 2
      apps/block_scout_web/assets/css/_typography.scss
  5. 1
      apps/block_scout_web/assets/css/app.scss
  6. 1
      apps/block_scout_web/assets/css/components/_tooltip.scss
  7. 1
      apps/block_scout_web/assets/css/non-critical.scss
  8. 4
      apps/block_scout_web/assets/css/theme/_base_variables.scss
  9. 28
      apps/block_scout_web/assets/js/lib/async_listing_load.js
  10. 29
      apps/block_scout_web/assets/js/lib/awesomplete-util.js
  11. 2
      apps/block_scout_web/assets/js/lib/card_tabs.js
  12. 2
      apps/block_scout_web/assets/js/lib/clipboard_buttons.js
  13. 4
      apps/block_scout_web/assets/js/lib/coin_balance_history_chart.js
  14. 3
      apps/block_scout_web/assets/js/lib/currency.js
  15. 2
      apps/block_scout_web/assets/js/lib/indexing.js
  16. 13
      apps/block_scout_web/assets/js/lib/market_history_chart.js
  17. 2
      apps/block_scout_web/assets/js/lib/modals.js
  18. 18
      apps/block_scout_web/assets/js/lib/network_selector.js
  19. 4
      apps/block_scout_web/assets/js/lib/pretty_json.js
  20. 4
      apps/block_scout_web/assets/js/pages/address.js
  21. 2
      apps/block_scout_web/assets/js/pages/address/coin_balances.js
  22. 4
      apps/block_scout_web/assets/js/pages/address/internal_transactions.js
  23. 18
      apps/block_scout_web/assets/js/pages/address/logs.js
  24. 4
      apps/block_scout_web/assets/js/pages/address/transactions.js
  25. 2
      apps/block_scout_web/assets/js/pages/blocks.js
  26. 22
      apps/block_scout_web/assets/js/pages/chain.js
  27. 4
      apps/block_scout_web/assets/js/pages/pending_transactions.js
  28. 8
      apps/block_scout_web/assets/js/pages/token_counters.js
  29. 2
      apps/block_scout_web/assets/js/pages/transaction.js
  30. 2
      apps/block_scout_web/assets/js/pages/transactions.js
  31. 4
      apps/block_scout_web/assets/js/pages/verification_form.js
  32. 6
      apps/block_scout_web/assets/js/socket.js
  33. 18141
      apps/block_scout_web/assets/package-lock.json
  34. 60
      apps/block_scout_web/assets/package.json
  35. BIN
      apps/block_scout_web/assets/static/android-chrome-192x192.png
  36. BIN
      apps/block_scout_web/assets/static/android-chrome-512x512.png
  37. BIN
      apps/block_scout_web/assets/static/favicon-16x16.png
  38. BIN
      apps/block_scout_web/assets/static/favicon-32x32.png
  39. BIN
      apps/block_scout_web/assets/static/images/favicon-16x16.png
  40. BIN
      apps/block_scout_web/assets/static/images/favicon-32x32.png
  41. 0
      apps/block_scout_web/assets/static/images/favicon.ico
  42. BIN
      apps/block_scout_web/assets/static/mstile-150x150.png
  43. 62
      apps/block_scout_web/assets/webpack.config.js
  44. 13
      apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/stats_controller.ex
  45. 3
      apps/block_scout_web/lib/block_scout_web/endpoint.ex
  46. 37
      apps/block_scout_web/lib/block_scout_web/etherscan.ex
  47. 8
      apps/block_scout_web/lib/block_scout_web/templates/layout/_topnav.html.eex
  48. 10
      apps/block_scout_web/lib/block_scout_web/templates/layout/app.html.eex
  49. 23
      apps/block_scout_web/lib/block_scout_web/views/api/rpc/block_view.ex
  50. 4
      apps/block_scout_web/lib/block_scout_web/views/api/rpc/stats_view.ex
  51. 29
      apps/block_scout_web/test/block_scout_web/controllers/api/rpc/stats_controller_test.exs
  52. 14
      apps/explorer/config/config.exs
  53. 2
      apps/explorer/lib/explorer/application.ex
  54. 11
      apps/explorer/lib/explorer/chain.ex
  55. 53
      apps/explorer/lib/explorer/chain/cache/address_sum.ex
  56. 42
      apps/explorer/lib/explorer/eth_rpc.ex
  57. 6
      apps/explorer/lib/explorer/token/instance_metadata_retriever.ex
  58. 56
      apps/explorer/test/explorer/chain/cache/address_sum_test.exs
  59. 2
      apps/explorer/test/explorer/chain/cache/block_count_test.exs
  60. 14
      apps/explorer/test/explorer/chain_test.exs

@ -1,6 +1,7 @@
## Current
### Features
- [#2862](https://github.com/poanetwork/blockscout/pull/2862) - Coin total supply from DB API endpoint
- [#2822](https://github.com/poanetwork/blockscout/pull/2822) - Estimated address count on the main page, if cache is empty
- [#2821](https://github.com/poanetwork/blockscout/pull/2821) - add autodetection of constructor arguments
- [#2825](https://github.com/poanetwork/blockscout/pull/2825) - separate token transfers and transactions
@ -10,6 +11,10 @@
### Fixes
- [#2864](https://github.com/poanetwork/blockscout/pull/2864) - add token instance metadata type check
- [#2855](https://github.com/poanetwork/blockscout/pull/2855) - Fix favicons load
- [#2854](https://github.com/poanetwork/blockscout/pull/2854) - Fix all npm vulnerabilities
- [#2851](https://github.com/poanetwork/blockscout/pull/2851) - Fix paths for front assets
- [#2843](https://github.com/poanetwork/blockscout/pull/2843) - fix realtime fetcher small skips feature
- [#2841](https://github.com/poanetwork/blockscout/pull/2841) - LUKSO dashboard height fix
- [#2837](https://github.com/poanetwork/blockscout/pull/2837) - fix txlist ordering issue
@ -21,6 +26,7 @@
- [#2803](https://github.com/poanetwork/blockscout/pull/2803) - Fix block validator custom tooltip
### Chore
- [#2859](https://github.com/poanetwork/blockscout/pull/2859) - Add eth_blockNumber API endpoint to eth_rpc section
- [#2846](https://github.com/poanetwork/blockscout/pull/2846) - Remove networks images preload
- [#2845](https://github.com/poanetwork/blockscout/pull/2845) - Set outline none for nav dropdown item in mobile view (fix for Safari)
- [#2844](https://github.com/poanetwork/blockscout/pull/2844) - Extend external reward types up to 20

@ -21,7 +21,7 @@ This is a tool for inspecting and analyzing the POA Network blockchain from a we
To get BlockScout Web interface up and running locally:
* Setup `../explorer`
* Set up some default configuration with: `$ cp config/dev.secret.exs.example config/dev.secret.esx`
* Set up some default configuration with: `$ cp config/dev.secret.exs.example config/dev.secret.exs`
* Install Node.js dependencies with `$ cd assets && npm install && cd ..`
* Start Phoenix with `$ mix phx.server` (This can be run from this directory or the project root: the project root is recommended.)

@ -1,5 +1,3 @@
{
presets: [
'env'
]
"presets": ["@babel/preset-env"]
}

@ -3,7 +3,7 @@ $blue: #4b89fb !default;
$success: #34c0ad !default;
body {
font-family: $font-family-sans-serif;
font-family: $font-family;
font-size: 12px;
}

@ -16,6 +16,7 @@ $fa-font-path: "~@fortawesome/fontawesome-free/webfonts";
// Bootstrap Core CSS
@import "node_modules/bootstrap/scss/functions";
@import "node_modules/bootstrap/scss/variables";
@import "node_modules/bootstrap/scss/mixins";
@import "theme/variables";

@ -10,6 +10,7 @@ $tooltip-color: #fff !default;
border-radius: 5px;
color: $tooltip-color;
padding: 15px;
font-size: 12px;
}
.arrow::before {

@ -1,5 +1,6 @@
// Bootstrap Core CSS
@import "node_modules/bootstrap/scss/functions";
@import "node_modules/bootstrap/scss/variables";
@import "node_modules/bootstrap/scss/mixins";
@import "theme/variables-non-critical";

@ -255,11 +255,11 @@ $transition-cont: all 0.4s ease-in-out !default;
// Font, line-height, and color for body text, headings, and more.
// stylelint-disable value-keyword-case
$font-family-sans-serif: Nunito, "Helvetica Neue", Arial, sans-serif,
$font-family: Nunito, "Helvetica Neue", Arial, sans-serif,
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol" !default;
$font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas,
"Liberation Mono", "Courier New", monospace !default;
$font-family-base: $font-family-sans-serif !default;
$font-family-base: $font-family !default;
// stylelint-enable value-keyword-case
$font-size-base: 1rem !default; // Assumes the browser default, typically `16px`

@ -271,7 +271,7 @@ export function createAsyncLoadStore (reducer, initialState, itemKey) {
})
}
connectElements({store, elements})
connectElements({ store, elements })
firstPageLoad(store)
return store
}
@ -280,20 +280,20 @@ function firstPageLoad (store) {
const $element = $('[data-async-listing]')
function loadItemsNext () {
const path = store.getState().nextPagePath
store.dispatch({type: 'START_REQUEST'})
$.getJSON(path, {type: 'JSON'})
.done(response => store.dispatch(Object.assign({type: 'ITEMS_FETCHED'}, humps.camelizeKeys(response))))
.fail(() => store.dispatch({type: 'REQUEST_ERROR'}))
.always(() => store.dispatch({type: 'FINISH_REQUEST'}))
store.dispatch({ type: 'START_REQUEST' })
$.getJSON(path, { type: 'JSON' })
.done(response => store.dispatch(Object.assign({ type: 'ITEMS_FETCHED' }, humps.camelizeKeys(response))))
.fail(() => store.dispatch({ type: 'REQUEST_ERROR' }))
.always(() => store.dispatch({ type: 'FINISH_REQUEST' }))
}
function loadItemsPrev () {
const path = store.getState().prevPagePath
store.dispatch({type: 'START_REQUEST'})
$.getJSON(path, {type: 'JSON'})
.done(response => store.dispatch(Object.assign({type: 'ITEMS_FETCHED'}, humps.camelizeKeys(response))))
.fail(() => store.dispatch({type: 'REQUEST_ERROR'}))
.always(() => store.dispatch({type: 'FINISH_REQUEST'}))
store.dispatch({ type: 'START_REQUEST' })
$.getJSON(path, { type: 'JSON' })
.done(response => store.dispatch(Object.assign({ type: 'ITEMS_FETCHED' }, humps.camelizeKeys(response))))
.fail(() => store.dispatch({ type: 'REQUEST_ERROR' }))
.always(() => store.dispatch({ type: 'FINISH_REQUEST' }))
}
loadItemsNext()
@ -305,14 +305,14 @@ function firstPageLoad (store) {
$element.on('click', '[data-next-page-button]', (event) => {
event.preventDefault()
loadItemsNext()
store.dispatch({type: 'NAVIGATE_TO_OLDER'})
store.dispatch({ type: 'NAVIGATE_TO_OLDER' })
event.stopImmediatePropagation()
})
$element.on('click', '[data-prev-page-button]', (event) => {
event.preventDefault()
loadItemsPrev()
store.dispatch({type: 'NAVIGATE_TO_NEWER'})
store.dispatch({ type: 'NAVIGATE_TO_NEWER' })
event.stopImmediatePropagation()
})
}
@ -320,6 +320,6 @@ function firstPageLoad (store) {
const $element = $('[data-async-load]')
if ($element.length) {
const store = createStore(asyncReducer)
connectElements({store, elements})
connectElements({ store, elements })
firstPageLoad(store)
}

@ -38,13 +38,13 @@ window.AwesompleteUtil = (function () {
var lv = Array.isArray(data)
? { label: data[0], value: data[1] }
: typeof data === 'object' && 'label' in data && 'value' in data ? data : { label: data, value: data }
return {label: lv.label || lv.value, value: lv.value}
return { label: lv.label || lv.value, value: lv.value }
}
// Helper to send events with detail property.
function _fire (target, name, detail) {
// $.fire uses deprecated methods but other methods don't work in IE11.
return $.fire(target, name, {detail: detail})
return $.fire(target, name, { detail: detail })
}
// Look if there is an exact match or a mismatch, set awe-found, awe-not-found css class and send match events.
@ -74,7 +74,7 @@ window.AwesompleteUtil = (function () {
}
}
// Don't want to change the real input field, emulate a fake one.
fake = {input: {value: ''}}
fake = { input: { value: '' } }
// Determine how this suggestion would look like if it is replaced in the input field,
// it is an exact match if somebody types exactly that.
// Use the fake input here. fake.input.value will contain the result of the replace function.
@ -222,7 +222,7 @@ window.AwesompleteUtil = (function () {
awe.utilprops.url,
awe.utilprops.urlEnd,
awe.utilprops.loadall ? '' : val,
_onLoad.bind({awe: awe, xhr: xhr, queryVal: val}),
_onLoad.bind({ awe: awe, xhr: xhr, queryVal: val }),
xhr
)
} else {
@ -401,15 +401,15 @@ window.AwesompleteUtil = (function () {
// for arrays at top and subtop level
if (level < 2 && prop) {
// if a 'value' is specified and found a mathing property, create extra 'value' property.
if (value && (prop + '.').lastIndexOf(value + '.', 0) === 0) { result['value'] = result[prop] }
if (value && (prop + '.').lastIndexOf(value + '.', 0) === 0) { result.value = result[prop] }
// if a 'label' is specified and found a mathing property, create extra 'label' property.
if (label && (prop + '.').lastIndexOf(label + '.', 0) === 0) { result['label'] = result[prop] }
if (label && (prop + '.').lastIndexOf(label + '.', 0) === 0) { result.label = result[prop] }
}
if (level === 0) {
// Make sure that both value and label properties exist, even if they are nil.
// This is handy with limit 0 or 1 when the result doesn't have to contain an array.
if (value && !('value' in result)) { result['value'] = null }
if (label && !('label' in result)) { result['label'] = null }
if (value && !('value' in result)) { result.value = null }
if (label && !('label' in result)) { result.label = null }
}
return result
}
@ -506,17 +506,18 @@ window.AwesompleteUtil = (function () {
var boundOnKeydown = _onKeydown.bind(awe)
var boundOnInput = _onInput.bind(awe)
var boundSelect = _select.bind(awe)
var boundDetach = _detach.bind({awe: awe,
var boundDetach = _detach.bind({
awe: awe,
boundMatch: boundMatch,
boundOnInput: boundOnInput,
boundOnKeydown: boundOnKeydown,
boundSelect: boundSelect
})
var events = {
'keydown': boundOnKeydown,
'input': boundOnInput
keydown: boundOnKeydown,
input: boundOnInput
}
events['blur'] = events[_AWE_CLOSE] = events[_AWE_LOAD] = boundMatch
events.blur = events[_AWE_CLOSE] = events[_AWE_LOAD] = boundMatch
events[_AWE_SELECT] = boundSelect
$.bind(elem, events)
@ -552,7 +553,7 @@ window.AwesompleteUtil = (function () {
// Create function to copy a field from the selected autocomplete item to another DOM element.
// dataField can be null.
createCopyFun: function (sourceId, dataField, targetId) {
return _copyFun.bind({sourceId: sourceId, dataField: dataField, targetId: $(targetId) || targetId})
return _copyFun.bind({ sourceId: sourceId, dataField: dataField, targetId: $(targetId) || targetId })
},
// attach copy function to event listeners. prepop is optional and by default true.
@ -585,7 +586,7 @@ window.AwesompleteUtil = (function () {
// Create function for combobox button (btnId) to toggle dropdown list.
createClickFun: function (btnId, awe) {
return _clickFun.bind({btnId: btnId, awe: awe})
return _clickFun.bind({ btnId: btnId, awe: awe })
},
// Attach click function for combobox to click event.

@ -17,7 +17,7 @@ $(function () {
const siblings = $(this).siblings()
if (siblings.is(':hidden')) {
siblings.css({ 'display': 'flex' })
siblings.css({ display: 'flex' })
} else {
siblings.hide()
}

@ -3,7 +3,7 @@ import $ from 'jquery'
const clipboard = new ClipboardJS('[data-clipboard-text]')
clipboard.on('success', ({trigger}) => {
clipboard.on('success', ({ trigger }) => {
const copyButton = $(trigger)
copyButton.tooltip('dispose')

@ -8,7 +8,7 @@ export function createCoinBalanceHistoryChart (el) {
const $chartError = $('[data-chart-error-message]')
const dataPath = el.dataset.coin_balance_history_data_path
$.getJSON(dataPath, {type: 'JSON'})
$.getJSON(dataPath, { type: 'JSON' })
.done(data => {
$chartContainer.show()
@ -53,7 +53,7 @@ export function createCoinBalanceHistoryChart (el) {
},
scaleLabel: {
display: true,
labelString: window.localized['Ether']
labelString: window.localized.Ether
}
}]
}

@ -45,6 +45,7 @@ export function formatAllUsdValues (root) {
formatAllUsdValues()
function tryUpdateCalculatedUsdValues (el, usdExchangeRate = el.dataset.usdExchangeRate) {
// eslint-disable-next-line no-prototype-builtins
if (!el.dataset.hasOwnProperty('weiValue')) return
const ether = weiToEther(el.dataset.weiValue)
const usd = etherToUSD(ether, usdExchangeRate)
@ -63,6 +64,6 @@ export function updateAllCalculatedUsdValues (usdExchangeRate) {
}
updateAllCalculatedUsdValues()
export const exchangeRateChannel = socket.channel(`exchange_rate:new_rate`)
export const exchangeRateChannel = socket.channel('exchange_rate:new_rate')
exchangeRateChannel.join()
exchangeRateChannel.on('new_rate', (msg) => updateAllCalculatedUsdValues(humps.camelizeKeys(msg).exchangeRate.usdValue))

@ -20,6 +20,6 @@ export function updateIndexStatus (msg = {}) {
}
updateIndexStatus()
const indexingChannel = socket.channel(`blocks:indexing`)
const indexingChannel = socket.channel('blocks:indexing')
indexingChannel.join()
indexingChannel.on('index_status', (msg) => updateIndexStatus(humps.camelizeKeys(msg)))

@ -61,7 +61,7 @@ const config = {
mode: 'index',
intersect: false,
callbacks: {
label: ({datasetIndex, yLabel}, {datasets}) => {
label: ({ datasetIndex, yLabel }, { datasets }) => {
const label = datasets[datasetIndex].label
if (datasets[datasetIndex].yAxisID === 'price') {
return `${label}: ${formatUsdValue(yLabel)}`
@ -77,14 +77,14 @@ const config = {
}
function getPriceData (marketHistoryData) {
return marketHistoryData.map(({ date, closingPrice }) => ({x: date, y: closingPrice}))
return marketHistoryData.map(({ date, closingPrice }) => ({ x: date, y: closingPrice }))
}
function getMarketCapData (marketHistoryData, availableSupply) {
if (availableSupply !== null && typeof availableSupply === 'object') {
return marketHistoryData.map(({ date, closingPrice }) => ({x: date, y: closingPrice * availableSupply[date]}))
return marketHistoryData.map(({ date, closingPrice }) => ({ x: date, y: closingPrice * availableSupply[date] }))
} else {
return marketHistoryData.map(({ date, closingPrice }) => ({x: date, y: closingPrice * availableSupply}))
return marketHistoryData.map(({ date, closingPrice }) => ({ x: date, y: closingPrice * availableSupply }))
}
}
@ -102,7 +102,7 @@ if (localStorage.getItem('current-color-mode') === 'dark') {
class MarketHistoryChart {
constructor (el, availableSupply, marketHistoryData) {
this.price = {
label: window.localized['Price'],
label: window.localized.Price,
yAxisID: 'price',
data: getPriceData(marketHistoryData),
fill: false,
@ -125,6 +125,7 @@ class MarketHistoryChart {
config.data.datasets = [this.price, this.marketCap]
this.chart = new Chart(el, config)
}
update (availableSupply, marketHistoryData) {
this.price.data = getPriceData(marketHistoryData)
if (this.availableSupply !== null && typeof this.availableSupply === 'object') {
@ -147,7 +148,7 @@ export function createMarketHistoryChart (el) {
const $chartError = $('[data-chart-error-message]')
const chart = new MarketHistoryChart(el, 0, [])
$.getJSON(dataPath, {type: 'JSON'})
$.getJSON(dataPath, { type: 'JSON' })
.done(data => {
const availableSupply = JSON.parse(data.supply_data)
const marketHistoryData = humps.camelizeKeys(JSON.parse(data.history_data))

@ -52,7 +52,7 @@ $(function () {
const progressBackground = total - progress
// eslint-disable-next-line no-unused-vars
let myChart = new Chart(stakeProgress, {
const myChart = new Chart(stakeProgress, {
type: 'doughnut',
data: {
datasets: [{

@ -36,7 +36,7 @@ $(function () {
window.location = $(this).attr('network-selector-item-url')
})
let setNetworkTab = (currentTab) => {
const setNetworkTab = (currentTab) => {
if (currentTab.hasClass('active')) return
networkSelectorTab.removeClass('active')
@ -45,31 +45,31 @@ $(function () {
$(`[network-selector-tab="${currentTab.attr('network-selector-tab-filter')}"]`).addClass('active')
}
let openNetworkSelector = () => {
const openNetworkSelector = () => {
mainBody.addClass('network-selector-visible')
networkSelectorOverlay.fadeIn(FADE_IN_DELAY)
setNetworkSelectorVisiblePosition()
}
let closeNetworkSelector = () => {
const closeNetworkSelector = () => {
mainBody.removeClass('network-selector-visible')
networkSelectorOverlay.fadeOut(FADE_IN_DELAY)
setNetworkSelectorHiddenPosition()
}
let getNetworkSelectorWidth = () => {
const getNetworkSelectorWidth = () => {
return parseInt(networkSelector.css('width')) || parseInt(networkSelector.css('max-width'))
}
let setNetworkSelectorHiddenPosition = () => {
return networkSelector.css({ 'right': `-${getNetworkSelectorWidth()}px` })
const setNetworkSelectorHiddenPosition = () => {
return networkSelector.css({ right: `-${getNetworkSelectorWidth()}px` })
}
let setNetworkSelectorVisiblePosition = () => {
return networkSelector.css({ 'right': '0' })
const setNetworkSelectorVisiblePosition = () => {
return networkSelector.css({ right: '0' })
}
let init = () => {
const init = () => {
setNetworkSelectorHiddenPosition()
}

@ -1,8 +1,8 @@
import $ from 'jquery'
function prettyPrint (element) {
let jsonString = element.dataset.json
let pretty = JSON.stringify(JSON.parse(jsonString), undefined, 2)
const jsonString = element.dataset.json
const pretty = JSON.stringify(JSON.parse(jsonString), undefined, 2)
element.innerHTML = pretty
}

@ -102,7 +102,7 @@ const elements = {
},
'[data-selector="fetched-coin-balance-block-number"]': {
load ($el) {
return {fetchedCoinBalanceBlockNumber: numeral($el.text()).value()}
return { fetchedCoinBalanceBlockNumber: numeral($el.text()).value() }
},
render ($el, state, oldState) {
if (oldState.fetchedCoinBalanceBlockNumber === state.fetchedCoinBalanceBlockNumber) return
@ -131,7 +131,7 @@ function loadCounters (store) {
function fetchCounters () {
$.getJSON(path)
.done(response => store.dispatch(Object.assign({type: 'COUNTERS_FETCHED'}, humps.camelizeKeys(response))))
.done(response => store.dispatch(Object.assign({ type: 'COUNTERS_FETCHED' }, humps.camelizeKeys(response))))
}
fetchCounters()

@ -47,7 +47,7 @@ if ($('[data-page="coin-balance-history"]').length) {
const store = createAsyncLoadStore(reducer, initialState, 'dataset.blockNumber')
const addressHash = $('[data-page="address-details"]')[0].dataset.pageAddressHash
store.dispatch({type: 'PAGE_LOAD', addressHash})
store.dispatch({ type: 'PAGE_LOAD', addressHash })
connectElements({ store, elements })
const addressChannel = socket.channel(`addresses:${addressHash}`, {})

@ -34,7 +34,7 @@ export function reducer (state, action) {
if (state.channelDisconnected || state.beyondPageOne) return state
const incomingInternalTransactions = action.msgs
.filter(({toAddressHash, fromAddressHash}) => (
.filter(({ toAddressHash, fromAddressHash }) => (
!state.filter ||
(state.filter === 'to' && toAddressHash === state.addressHash) ||
(state.filter === 'from' && fromAddressHash === state.addressHash)
@ -81,7 +81,7 @@ if ($('[data-page="address-internal-transactions"]').length) {
const store = createAsyncLoadStore(reducer, initialState, 'dataset.key')
const addressHash = $('[data-page="address-details"]')[0].dataset.pageAddressHash
store.dispatch({type: 'PAGE_LOAD', addressHash})
store.dispatch({ type: 'PAGE_LOAD', addressHash })
connectElements({ store, elements })
const addressChannel = socket.channel(`addresses:${addressHash}`, {})

@ -16,7 +16,7 @@ export function reducer (state, action) {
return Object.assign({}, state, omit(action, 'type'))
}
case 'START_SEARCH': {
return Object.assign({}, state, {pagesStack: [], isSearch: true})
return Object.assign({}, state, { pagesStack: [], isSearch: true })
}
default:
return state
@ -63,19 +63,21 @@ if ($('[data-page="address-logs"]').length) {
store.dispatch({
type: 'PAGE_LOAD',
addressHash: addressHash})
addressHash: addressHash
})
$element.on('click', '[data-search-button]', (event) => {
store.dispatch({
type: 'START_SEARCH',
addressHash: addressHash})
addressHash: addressHash
})
var topic = $('[data-search-field]').val()
var path = '/search_logs?topic=' + topic + '&address_id=' + store.getState().addressHash
store.dispatch({type: 'START_REQUEST'})
$.getJSON(path, {type: 'JSON'})
.done(response => store.dispatch(Object.assign({type: 'ITEMS_FETCHED'}, humps.camelizeKeys(response))))
.fail(() => store.dispatch({type: 'REQUEST_ERROR'}))
.always(() => store.dispatch({type: 'FINISH_REQUEST'}))
store.dispatch({ type: 'START_REQUEST' })
$.getJSON(path, { type: 'JSON' })
.done(response => store.dispatch(Object.assign({ type: 'ITEMS_FETCHED' }, humps.camelizeKeys(response))))
.fail(() => store.dispatch({ type: 'REQUEST_ERROR' }))
.always(() => store.dispatch({ type: 'FINISH_REQUEST' }))
})
$element.on('click', '[data-cancel-search-button]', (event) => {

@ -32,12 +32,12 @@ export function reducer (state, action) {
return state
}
return Object.assign({}, state, { items: [ action.msg.transactionHtml, ...state.items ] })
return Object.assign({}, state, { items: [action.msg.transactionHtml, ...state.items] })
}
case 'RECEIVED_NEW_REWARD': {
if (state.channelDisconnected) return state
return Object.assign({}, state, { items: [ action.msg.rewardHtml, ...state.items ] })
return Object.assign({}, state, { items: [action.msg.rewardHtml, ...state.items] })
}
default:
return state

@ -91,7 +91,7 @@ if ($blockListPage.length || $uncleListPage.length || $reorgListPage.length) {
)
connectElements({ store, elements })
const blocksChannel = socket.channel(`blocks:new_block`, {})
const blocksChannel = socket.channel('blocks:new_block', {})
blocksChannel.join()
blocksChannel.onError(() => store.dispatch({
type: 'CHANNEL_DISCONNECTED'

@ -264,21 +264,21 @@ if ($chainDetailsPage.length) {
msg: humps.camelizeKeys(msg)
}))
const addressesChannel = socket.channel(`addresses:new_address`)
const addressesChannel = socket.channel('addresses:new_address')
addressesChannel.join()
addressesChannel.on('count', msg => store.dispatch({
type: 'RECEIVED_NEW_ADDRESS_COUNT',
msg: humps.camelizeKeys(msg)
}))
const blocksChannel = socket.channel(`blocks:new_block`)
const blocksChannel = socket.channel('blocks:new_block')
blocksChannel.join()
blocksChannel.on('new_block', msg => store.dispatch({
type: 'RECEIVED_NEW_BLOCK',
msg: humps.camelizeKeys(msg)
}))
const transactionsChannel = socket.channel(`transactions:new_transaction`)
const transactionsChannel = socket.channel('transactions:new_transaction')
transactionsChannel.join()
transactionsChannel.on('transaction', batchChannel((msgs) => store.dispatch({
type: 'RECEIVED_NEW_TRANSACTION_BATCH',
@ -288,11 +288,11 @@ if ($chainDetailsPage.length) {
function loadTransactions (store) {
const path = store.getState().transactionsPath
store.dispatch({type: 'START_TRANSACTIONS_FETCH'})
store.dispatch({ type: 'START_TRANSACTIONS_FETCH' })
$.getJSON(path)
.done(response => store.dispatch({type: 'TRANSACTIONS_FETCHED', msg: humps.camelizeKeys(response)}))
.fail(() => store.dispatch({type: 'TRANSACTIONS_FETCH_ERROR'}))
.always(() => store.dispatch({type: 'FINISH_TRANSACTIONS_FETCH'}))
.done(response => store.dispatch({ type: 'TRANSACTIONS_FETCHED', msg: humps.camelizeKeys(response) }))
.fail(() => store.dispatch({ type: 'TRANSACTIONS_FETCH_ERROR' }))
.always(() => store.dispatch({ type: 'FINISH_TRANSACTIONS_FETCH' }))
}
function bindTransactionErrorMessage (store) {
@ -325,14 +325,14 @@ export function placeHolderBlock (blockNumber) {
function loadBlocks (store) {
const url = store.getState().blocksPath
store.dispatch({type: 'START_BLOCKS_FETCH'})
store.dispatch({ type: 'START_BLOCKS_FETCH' })
$.getJSON(url)
.done(response => {
store.dispatch({type: 'BLOCKS_FETCHED', msg: humps.camelizeKeys(response)})
store.dispatch({ type: 'BLOCKS_FETCHED', msg: humps.camelizeKeys(response) })
})
.fail(() => store.dispatch({type: 'BLOCKS_REQUEST_ERROR'}))
.always(() => store.dispatch({type: 'BLOCKS_FINISH_REQUEST'}))
.fail(() => store.dispatch({ type: 'BLOCKS_REQUEST_ERROR' }))
.always(() => store.dispatch({ type: 'BLOCKS_FINISH_REQUEST' }))
}
function bindBlockErrorMessage (store) {

@ -102,7 +102,7 @@ if ($transactionPendingListPage.length) {
const store = createAsyncLoadStore(reducer, initialState, 'dataset.identifierHash')
connectElements({ store, elements })
const transactionsChannel = socket.channel(`transactions:new_transaction`)
const transactionsChannel = socket.channel('transactions:new_transaction')
transactionsChannel.join()
transactionsChannel.onError(() => store.dispatch({
type: 'CHANNEL_DISCONNECTED'
@ -118,7 +118,7 @@ if ($transactionPendingListPage.length) {
}), 1000)
})
const pendingTransactionsChannel = socket.channel(`transactions:new_pending_transaction`)
const pendingTransactionsChannel = socket.channel('transactions:new_pending_transaction')
pendingTransactionsChannel.join()
pendingTransactionsChannel.onError(() => store.dispatch({
type: 'CHANNEL_DISCONNECTED'

@ -68,11 +68,11 @@ function loadCounters (store) {
const $element = $('[data-async-counters]')
const path = $element.data() && $element.data().asyncCounters
function fetchCounters () {
store.dispatch({type: 'START_REQUEST'})
store.dispatch({ type: 'START_REQUEST' })
$.getJSON(path)
.done(response => store.dispatch(Object.assign({type: 'COUNTERS_FETCHED'}, humps.camelizeKeys(response))))
.fail(() => store.dispatch({type: 'REQUEST_ERROR'}))
.always(() => store.dispatch({type: 'FINISH_REQUEST'}))
.done(response => store.dispatch(Object.assign({ type: 'COUNTERS_FETCHED' }, humps.camelizeKeys(response))))
.fail(() => store.dispatch({ type: 'REQUEST_ERROR' }))
.always(() => store.dispatch({ type: 'FINISH_REQUEST' }))
}
fetchCounters()

@ -47,7 +47,7 @@ if ($transactionDetailsPage.length) {
const store = createStore(reducer)
connectElements({ store, elements })
const blocksChannel = socket.channel(`blocks:new_block`, {})
const blocksChannel = socket.channel('blocks:new_block', {})
blocksChannel.join()
blocksChannel.on('new_block', (msg) => store.dispatch({
type: 'RECEIVED_NEW_BLOCK',

@ -85,7 +85,7 @@ if ($transactionListPage.length) {
connectElements({ store, elements })
const transactionsChannel = socket.channel(`transactions:new_transaction`)
const transactionsChannel = socket.channel('transactions:new_transaction')
transactionsChannel.join()
transactionsChannel.onError(() => store.dispatch({
type: 'CHANNEL_DISCONNECTED'

@ -67,7 +67,7 @@ const elements = {
})
$('.js-btn-add-contract-library').on('click', function () {
let nextContractLibrary = $('.js-contract-library-form-group.active').next('.js-contract-library-form-group')
const nextContractLibrary = $('.js-contract-library-form-group.active').next('.js-contract-library-form-group')
if (nextContractLibrary) {
nextContractLibrary.addClass('active')
@ -138,7 +138,7 @@ if ($contractVerificationPage.length) {
})
$('.js-btn-add-contract-library').on('click', function () {
let nextContractLibrary = $('.js-contract-library-form-group.active').next('.js-contract-library-form-group')
const nextContractLibrary = $('.js-contract-library-form-group.active').next('.js-contract-library-form-group')
if (nextContractLibrary) {
nextContractLibrary.addClass('active')

@ -1,7 +1,7 @@
import {Socket} from 'phoenix'
import {locale} from './locale'
import { Socket } from 'phoenix'
import { locale } from './locale'
const socket = new Socket('/socket', {params: {locale: locale}})
const socket = new Socket('/socket', { params: { locale: locale } })
socket.connect()
export default socket

File diff suppressed because it is too large Load Diff

@ -21,51 +21,51 @@
"dependencies": {
"@fortawesome/fontawesome-free": "^5.1.0-4",
"awesomplete": "1.1.2",
"bignumber.js": "^7.2.1",
"bootstrap": "^4.1.3",
"chart.js": "^2.7.2",
"clipboard": "^2.0.1",
"highlight.js": "^9.13.1",
"highlightjs-solidity": "^1.0.6",
"bignumber.js": "^9.0.0",
"bootstrap": "^4.3.1",
"chart.js": "^2.9.2",
"clipboard": "^2.0.4",
"highlight.js": "^9.16.2",
"highlightjs-solidity": "^1.0.8",
"humps": "^2.0.1",
"jquery": "^3.4.0",
"lodash": "^4.17.15",
"moment": "^2.22.1",
"nanomorph": "^5.1.3",
"moment": "^2.24.0",
"nanomorph": "^5.4.0",
"numeral": "^2.0.6",
"path-parser": "^4.1.1",
"path-parser": "^4.2.0",
"phoenix": "file:../../../deps/phoenix",
"phoenix_html": "file:../../../deps/phoenix_html",
"popper.js": "^1.14.3",
"popper.js": "^1.14.7",
"reduce-reducers": "^0.4.3",
"redux": "^4.0.0",
"urijs": "^1.19.1"
"urijs": "^1.19.2"
},
"devDependencies": {
"@babel/polyfill": "^7.0.0-beta.46",
"@babel/core": "^7.7.2",
"@babel/polyfill": "^7.7.0",
"@babel/preset-env": "^7.7.1",
"autoprefixer": "^8.4.1",
"babel-core": "^6.26.3",
"babel-loader": "^7.1.4",
"babel-preset-env": "^1.6.1",
"copy-webpack-plugin": "^4.5.1",
"babel-loader": "^8.0.6",
"copy-webpack-plugin": "^5.0.5",
"css-loader": "^3.1.0",
"eslint": "^4.15.0",
"eslint-config-standard": "^11.0.0-beta.0",
"eslint-plugin-import": "^2.8.0",
"eslint-plugin-node": "^5.2.1",
"eslint-plugin-promise": "^3.6.0",
"eslint-plugin-standard": "^3.0.1",
"file-loader": "^1.1.11",
"jest": "^23.2.0",
"eslint": "^6.6.0",
"eslint-config-standard": "^14.1.0",
"eslint-plugin-import": "^2.18.2",
"eslint-plugin-node": "^10.0.0",
"eslint-plugin-promise": "^4.2.1",
"eslint-plugin-standard": "^4.0.1",
"file-loader": "^4.2.0",
"jest": "^24.9.0",
"mini-css-extract-plugin": "^0.8.0",
"node-sass": "^4.12.0",
"optimize-css-assets-webpack-plugin": "^5.0.3",
"postcss-loader": "^2.1.4",
"sass-loader": "^7.1.0",
"style-loader": "^0.21.0",
"terser-webpack-plugin": "^1.3.0",
"webpack": "^4.6.0",
"webpack-cli": "^3.0.8"
"postcss-loader": "^3.0.0",
"sass-loader": "^8.0.0",
"style-loader": "^1.0.0",
"terser-webpack-plugin": "^2.2.1",
"webpack": "^4.41.2",
"webpack-cli": "^3.3.10"
},
"jest": {
"moduleNameMapper": {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 424 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 794 B

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

@ -1,10 +1,10 @@
const path = require('path');
const TerserJSPlugin = require('terser-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const { ContextReplacementPlugin } = require('webpack');
const glob = require("glob");
const path = require('path')
const TerserJSPlugin = require('terser-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const { ContextReplacementPlugin } = require('webpack')
const glob = require('glob')
function transpileViewScript(file) {
return {
@ -19,9 +19,12 @@ function transpileViewScript(file) {
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader'
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
},
}
]
}
}
@ -30,7 +33,7 @@ function transpileViewScript(file) {
const jsOptimizationParams = {
cache: true,
parallel: true,
sourceMap: true,
sourceMap: true
}
const awesompleteJs = {
@ -51,19 +54,19 @@ const awesompleteJs = {
{
loader: "css-loader",
}
],
},
],
]
}
]
},
optimization: {
minimizer: [
new TerserJSPlugin(jsOptimizationParams),
],
]
},
plugins: [
new MiniCssExtractPlugin({
filename: '../css/awesomplete.css'
}),
})
]
}
@ -72,7 +75,7 @@ const appJs =
entry: {
app: './js/app.js',
stakes: './js/pages/stakes.js',
'non-critical': './css/non-critical.scss',
'non-critical': './css/non-critical.scss'
},
output: {
filename: '[name].js',
@ -87,7 +90,10 @@ const appJs =
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader'
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
},
{
@ -95,17 +101,19 @@ const appJs =
use: [
MiniCssExtractPlugin.loader,
{
loader: "css-loader"
loader: 'css-loader'
}, {
loader: "postcss-loader"
loader: 'postcss-loader'
}, {
loader: "sass-loader",
loader: 'sass-loader',
options: {
precision: 8,
includePaths: [
'node_modules/bootstrap/scss',
'node_modules/@fortawesome/fontawesome-free/scss'
]
sassOptions: {
precision: 8,
includePaths: [
'node_modules/bootstrap/scss',
'node_modules/@fortawesome/fontawesome-free/scss'
]
}
}
}
]
@ -131,6 +139,6 @@ const appJs =
]
}
const viewScripts = glob.sync('./js/view_specific/**/*.js').map(transpileViewScript);
const viewScripts = glob.sync('./js/view_specific/**/*.js').map(transpileViewScript)
module.exports = viewScripts.concat(appJs, awesompleteJs);
module.exports = viewScripts.concat(appJs, awesompleteJs)

@ -1,7 +1,10 @@
defmodule BlockScoutWeb.API.RPC.StatsController do
use BlockScoutWeb, :controller
use Explorer.Schema
alias Explorer.{Chain, ExchangeRates}
alias Explorer.Chain.Cache.AddressSum
alias Explorer.Chain.Wei
def tokensupply(conn, params) do
@ -21,7 +24,7 @@ defmodule BlockScoutWeb.API.RPC.StatsController do
end
end
def ethsupply(conn, _params) do
def ethsupplyexchange(conn, _params) do
wei_total_supply =
Chain.total_supply()
|> Decimal.new()
@ -29,7 +32,13 @@ defmodule BlockScoutWeb.API.RPC.StatsController do
|> Wei.to(:wei)
|> Decimal.to_string()
render(conn, "ethsupply.json", total_supply: wei_total_supply)
render(conn, "ethsupplyexchange.json", total_supply: wei_total_supply)
end
def ethsupply(conn, _params) do
cached_wei_total_supply = AddressSum.get_sum()
render(conn, "ethsupply.json", total_supply: cached_wei_total_supply)
end
def ethprice(conn, _params) do

@ -27,9 +27,6 @@ defmodule BlockScoutWeb.Endpoint do
android-chrome-512x512.png
apple-touch-icon.png
browserconfig.xml
favicon.ico
favicon-16x16.png
favicon-32x32.png
mstile-150x150.png
safari-pinned-tab.svg
site.manifest

@ -261,6 +261,12 @@ defmodule BlockScoutWeb.Etherscan do
"result" => "21265524714464"
}
@stats_ethsupplyexchange_example_value %{
"status" => "1",
"message" => "OK",
"result" => "101959776311500000000000000"
}
@stats_ethsupply_example_value %{
"status" => "1",
"message" => "OK",
@ -299,7 +305,7 @@ defmodule BlockScoutWeb.Etherscan do
@block_eth_block_number_example_value %{
"jsonrpc" => "2.0",
"result" => "767969",
"result" => "0xb33bf1",
"id" => 1
}
@ -1772,9 +1778,35 @@ defmodule BlockScoutWeb.Etherscan do
]
}
@stats_ethsupplyexchange_action %{
name: "ethsupplyexchange",
description: "Get total supply in Wei from exchange.",
required_params: [],
optional_params: [],
responses: [
%{
code: "200",
description: "successful operation",
example_value: Jason.encode!(@stats_ethsupplyexchange_example_value),
model: %{
name: "Result",
fields: %{
status: @status_type,
message: @message_type,
result: %{
type: "integer",
description: "The total supply.",
example: ~s("101959776311500000000000000")
}
}
}
}
]
}
@stats_ethsupply_action %{
name: "ethsupply",
description: "Get total supply in Wei.",
description: "Get total supply in Wei from DB.",
required_params: [],
optional_params: [],
responses: [
@ -2302,6 +2334,7 @@ defmodule BlockScoutWeb.Etherscan do
name: "stats",
actions: [
@stats_tokensupply_action,
@stats_ethsupplyexchange_action,
@stats_ethsupply_action,
@stats_ethprice_action
]

@ -1,7 +1,7 @@
<link rel="preload" href="/css/awesomplete.css" as="style" onload="this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="/css/awesomplete.css"></noscript>
<script src="/js/awesomplete.min.js"></script>
<script src="/js/awesomplete-util.min.js"></script>
<link rel="preload" href="<%= static_path(@conn, "/css/awesomplete.css") %>" as="style" onload="this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="<%= static_path(@conn, "/css/awesomplete.css") %>"></noscript>
<script src="<%= static_path(@conn, "/js/awesomplete.min.js") %>"></script>
<script src="<%= static_path(@conn, "/js/awesomplete-util.min.js") %>"></script>
<nav class="navbar navbar-dark navbar-expand-lg navbar-primary" data-selector="navbar" id="top-navbar">
<script>
if (localStorage.getItem("current-color-mode") === "dark") {

@ -5,15 +5,15 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="<%= static_path(@conn, "/css/app.css") %>">
<link rel="preload" href="/css/non-critical.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="/css/non-critical.css"></noscript>
<link rel="preload" href="<%= static_path(@conn, "/css/non-critical.css") %>" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="<%= static_path(@conn, "/css/non-critical.css") %>"></noscript>
<link rel="apple-touch-icon" sizes="180x180" href="<%= static_path(@conn, "/apple-touch-icon.png") %>">
<link rel="icon" type="image/png" sizes="32x32" href="<%= static_path(@conn, "/favicon-32x32.png") %>">
<link rel="icon" type="image/png" sizes="16x16" href="<%= static_path(@conn, "/favicon-16x16.png") %>">
<link rel="icon" type="image/png" sizes="32x32" href="<%= static_path(@conn, "/images/favicon-32x32.png") %>">
<link rel="icon" type="image/png" sizes="16x16" href="<%= static_path(@conn, "/images/favicon-16x16.png") %>">
<link rel="manifest" href="<%= static_path(@conn, "/site.webmanifest") %>">
<link rel="mask-icon" href="<%= static_path(@conn, "/safari-pinned-tab.svg") %>" color="#5bbad5">
<link rel="shortcut icon" href="<%= static_path(@conn, "/favicon.ico") %>">
<link rel="shortcut icon" type='image/x-icon' href="<%= static_path(@conn, "/images/favicon.ico") %>">
<meta name="msapplication-TileColor" content="#7dd79f">
<meta name="msapplication-config" content="<%= static_path(@conn, "/browserconfig.xml") %>">
<meta name="theme-color" content="#ffffff">

@ -3,6 +3,7 @@ defmodule BlockScoutWeb.API.RPC.BlockView do
alias BlockScoutWeb.API.RPC.{EthRPCView, RPCView}
alias Explorer.Chain.{Hash, Wei}
alias Explorer.EthRPC, as: EthRPC
def render("block_reward.json", %{block: block, reward: reward}) do
reward_as_string =
@ -23,7 +24,7 @@ defmodule BlockScoutWeb.API.RPC.BlockView do
end
def render("eth_block_number.json", %{number: number, id: id}) do
result = encode_quantity(number)
result = EthRPC.encode_quantity(number)
EthRPCView.render("show.json", %{result: result, id: id})
end
@ -31,24 +32,4 @@ defmodule BlockScoutWeb.API.RPC.BlockView do
def render("error.json", %{error: error}) do
RPCView.render("error.json", error: error)
end
defp encode_quantity(binary) when is_binary(binary) do
hex_binary = Base.encode16(binary, case: :lower)
result = String.replace_leading(hex_binary, "0", "")
final_result = if result == "", do: "0", else: result
"0x#{final_result}"
end
defp encode_quantity(value) when is_integer(value) do
value
|> :binary.encode_unsigned()
|> encode_quantity()
end
defp encode_quantity(value) when is_nil(value) do
nil
end
end

@ -7,6 +7,10 @@ defmodule BlockScoutWeb.API.RPC.StatsView do
RPCView.render("show.json", data: Decimal.to_string(token_supply))
end
def render("ethsupplyexchange.json", %{total_supply: total_supply}) do
RPCView.render("show.json", data: total_supply)
end
def render("ethsupply.json", %{total_supply: total_supply}) do
RPCView.render("show.json", data: total_supply)
end

@ -85,8 +85,27 @@ defmodule BlockScoutWeb.API.RPC.StatsControllerTest do
end
end
describe "ethsupplyexchange" do
test "returns total supply from exchange", %{conn: conn} do
params = %{
"module" => "stats",
"action" => "ethsupplyexchange"
}
assert response =
conn
|> get("/api", params)
|> json_response(200)
assert response["result"] == "252460800000000000000000000"
assert response["status"] == "1"
assert response["message"] == "OK"
assert :ok = ExJsonSchema.Validator.validate(ethsupplyexchange_schema(), response)
end
end
describe "ethsupply" do
test "returns total supply", %{conn: conn} do
test "returns total supply from DB", %{conn: conn} do
params = %{
"module" => "stats",
"action" => "ethsupply"
@ -97,7 +116,7 @@ defmodule BlockScoutWeb.API.RPC.StatsControllerTest do
|> get("/api", params)
|> json_response(200)
assert response["result"] == "252460800000000000000000000"
assert response["result"] == "6"
assert response["status"] == "1"
assert response["message"] == "OK"
assert :ok = ExJsonSchema.Validator.validate(ethsupply_schema(), response)
@ -179,6 +198,12 @@ defmodule BlockScoutWeb.API.RPC.StatsControllerTest do
})
end
defp ethsupplyexchange_schema do
resolve_schema(%{
"type" => ["string", "null"]
})
end
defp ethprice_schema do
resolve_schema(%{
"type" => "object",

@ -52,6 +52,20 @@ config :explorer, Explorer.Chain.Cache.BlockNumber,
ttl_check_interval: if(System.get_env("DISABLE_INDEXER") == "true", do: :timer.seconds(1), else: false),
global_ttl: if(System.get_env("DISABLE_INDEXER") == "true", do: :timer.seconds(5))
address_sum_global_ttl =
"ADDRESS_SUM_CACHE_PERIOD"
|> System.get_env("")
|> Integer.parse()
|> case do
{integer, ""} -> :timer.seconds(integer)
_ -> :timer.minutes(60)
end
config :explorer, Explorer.Chain.Cache.AddressSum,
enabled: true,
ttl_check_interval: :timer.seconds(1),
global_ttl: address_sum_global_ttl
balances_update_interval =
if System.get_env("ADDRESS_WITH_BALANCES_UPDATE_INTERVAL") do
case Integer.parse(System.get_env("ADDRESS_WITH_BALANCES_UPDATE_INTERVAL")) do

@ -9,6 +9,7 @@ defmodule Explorer.Application do
alias Explorer.Chain.Cache.{
Accounts,
AddressSum,
BlockCount,
BlockNumber,
Blocks,
@ -46,6 +47,7 @@ defmodule Explorer.Application do
{Registry, keys: :duplicate, name: Registry.ChainEvents, id: Registry.ChainEvents},
{Admin.Recovery, [[], [name: Admin.Recovery]]},
TransactionCount,
AddressSum,
BlockCount,
Blocks,
NetVersion,

@ -1322,6 +1322,17 @@ defmodule Explorer.Chain do
Repo.one!(query)
end
@spec fetch_sum_coin_total_supply() :: non_neg_integer
def fetch_sum_coin_total_supply do
query =
from(
a0 in Address,
select: fragment("SUM(a0.fetched_coin_balance)")
)
Repo.one!(query) || 0
end
@doc """
The number of `t:Explorer.Chain.InternalTransaction.t/0`.

@ -0,0 +1,53 @@
defmodule Explorer.Chain.Cache.AddressSum do
@moduledoc """
Cache for address sum.
"""
require Logger
use Explorer.Chain.MapCache,
name: :address_sum,
key: :sum,
key: :async_task,
ttl_check_interval: Application.get_env(:explorer, __MODULE__)[:ttl_check_interval],
global_ttl: Application.get_env(:explorer, __MODULE__)[:global_ttl],
callback: &async_task_on_deletion(&1)
alias Explorer.Chain
defp handle_fallback(:sum) do
# This will get the task PID if one exists and launch a new task if not
# See next `handle_fallback` definition
get_async_task()
{:return, nil}
end
defp handle_fallback(:async_task) do
# If this gets called it means an async task was requested, but none exists
# so a new one needs to be launched
{:ok, task} =
Task.start(fn ->
try do
result = Chain.fetch_sum_coin_total_supply()
set_sum(result)
rescue
e ->
Logger.debug([
"Coudn't update address sum test #{inspect(e)}"
])
end
set_async_task(nil)
end)
{:update, task}
end
# By setting this as a `callback` an async task will be started each time the
# `sum` expires (unless there is one already running)
defp async_task_on_deletion({:delete, _, :sum}), do: get_async_task()
defp async_task_on_deletion(_data), do: nil
end

@ -6,9 +6,21 @@ defmodule Explorer.EthRPC do
alias Ecto.Type, as: EctoType
alias Explorer.{Chain, Repo}
alias Explorer.Chain.{Block, Data, Hash, Hash.Address, Wei}
alias Explorer.Chain.Cache.BlockNumber
alias Explorer.Etherscan.Logs
@methods %{
"eth_blockNumber" => %{
action: :eth_block_number,
notes: nil,
example: """
{"id": 0, "jsonrpc": "2.0", "method": "eth_blockNumber", "params": []}
""",
params: [],
result: """
{"id": 0, "jsonrpc": "2.0", "result": "0xb3415c"}
"""
},
"eth_getBalance" => %{
action: :eth_get_balance,
notes: """
@ -94,6 +106,16 @@ defmodule Explorer.EthRPC do
end)
end
def eth_block_number do
max_block_number = BlockNumber.get_max()
max_block_number_hex =
max_block_number
|> encode_quantity()
{:ok, max_block_number_hex}
end
def eth_get_balance(address_param, block_param \\ nil) do
with {:address, {:ok, address}} <- {:address, Chain.string_to_address_hash(address_param)},
{:block, {:ok, block}} <- {:block, block_param(block_param)},
@ -405,5 +427,25 @@ defmodule Explorer.EthRPC do
defp block_param(nil), do: {:ok, :latest}
defp block_param(_), do: :error
def encode_quantity(binary) when is_binary(binary) do
hex_binary = Base.encode16(binary, case: :lower)
result = String.replace_leading(hex_binary, "0", "")
final_result = if result == "", do: "0", else: result
"0x#{final_result}"
end
def encode_quantity(value) when is_integer(value) do
value
|> :binary.encode_unsigned()
|> encode_quantity()
end
def encode_quantity(value) when is_nil(value) do
nil
end
def methods, do: @methods
end

@ -101,7 +101,11 @@ defmodule Explorer.Token.InstanceMetadataRetriever do
{:ok, %Response{body: body, status_code: 200}} ->
{:ok, json} = decode_json(body)
{:ok, %{metadata: json}}
if is_map(json) do
{:ok, %{metadata: json}}
else
{:error, :wrong_metadata_type}
end
{:ok, %Response{body: body}} ->
{:error, body}

@ -0,0 +1,56 @@
defmodule Explorer.Chain.Cache.AddressSumTest do
use Explorer.DataCase
alias Explorer.Chain.Cache.AddressSum
setup do
Supervisor.terminate_child(Explorer.Supervisor, AddressSum.child_id())
Supervisor.restart_child(Explorer.Supervisor, AddressSum.child_id())
:ok
end
test "returns default address sum" do
result = AddressSum.get_sum()
assert is_nil(result)
end
test "updates cache if initial value is zero" do
insert(:address, fetched_coin_balance: 1)
insert(:address, fetched_coin_balance: 2)
insert(:address, fetched_coin_balance: 3)
_result = AddressSum.get_sum()
Process.sleep(1000)
updated_value = Decimal.to_integer(AddressSum.get_sum())
assert updated_value == 6
end
test "does not update cache if cache period did not pass" do
insert(:address, fetched_coin_balance: 1)
insert(:address, fetched_coin_balance: 2)
insert(:address, fetched_coin_balance: 3)
_result = AddressSum.get_sum()
Process.sleep(1000)
updated_value = Decimal.to_integer(AddressSum.get_sum())
assert updated_value == 6
insert(:address, fetched_coin_balance: 4)
insert(:address, fetched_coin_balance: 5)
_updated_value = AddressSum.get_sum()
Process.sleep(1000)
updated_value = Decimal.to_integer(AddressSum.get_sum())
assert updated_value == 6
end
end

@ -9,7 +9,7 @@ defmodule Explorer.Chain.Cache.BlockCountTest do
:ok
end
test "returns default transaction count" do
test "returns default block count" do
result = BlockCount.get_count()
assert is_nil(result)

@ -1121,6 +1121,20 @@ defmodule Explorer.ChainTest do
end
end
describe "fetch_sum_coin_total_supply/0" do
test "fetches coin total supply" do
for index <- 0..4 do
insert(:address, fetched_coin_balance: index)
end
assert "10" = Decimal.to_string(Chain.fetch_sum_coin_total_supply())
end
test "fetches coin total supply when there are no blocks" do
assert 0 = Chain.fetch_sum_coin_total_supply()
end
end
describe "address_hash_to_token_transfers/2" do
test "returns just the token transfers related to the given contract address" do
contract_address =

Loading…
Cancel
Save