Merge branch 'master' into fix-random-failing-test

pull/1175/head
Franco Victorio 6 years ago committed by GitHub
commit d2c4415dd8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 80
      apps/block_scout_web/assets/__tests__/pages/address.js
  2. 124
      apps/block_scout_web/assets/__tests__/pages/address/transactions.js
  3. 1
      apps/block_scout_web/assets/js/app.js
  4. 61
      apps/block_scout_web/assets/js/pages/address.js
  5. 73
      apps/block_scout_web/assets/js/pages/address/transactions.js
  6. 2
      apps/block_scout_web/lib/block_scout_web/controllers/address_controller.ex
  7. 36
      apps/block_scout_web/lib/block_scout_web/controllers/address_transaction_controller.ex
  8. 2
      apps/block_scout_web/lib/block_scout_web/controllers/chain_controller.ex
  9. 3
      apps/block_scout_web/lib/block_scout_web/endpoint.ex
  10. 2
      apps/block_scout_web/lib/block_scout_web/notifier.ex
  11. 6
      apps/block_scout_web/lib/block_scout_web/templates/address/index.html.eex
  12. 36
      apps/block_scout_web/lib/block_scout_web/templates/address_transaction/index.html.eex
  13. 2
      apps/block_scout_web/lib/block_scout_web/templates/chain/show.html.eex
  14. 2
      apps/block_scout_web/mix.exs
  15. 12
      apps/block_scout_web/priv/gettext/default.pot
  16. 12
      apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po
  17. 57
      apps/block_scout_web/test/block_scout_web/controllers/address_transaction_controller_test.exs
  18. 6
      apps/explorer/config/config.exs
  19. 8
      apps/explorer/config/test.exs
  20. 3
      apps/explorer/lib/explorer/application.ex
  21. 34
      apps/explorer/lib/explorer/chain.ex
  22. 11
      apps/explorer/lib/explorer/chain/address.ex
  23. 117
      apps/explorer/lib/explorer/counters/addresses_with_balance_counter.ex
  24. 33
      apps/explorer/lib/explorer/counters/block_validation_counter.ex
  25. 11
      apps/explorer/test/explorer/chain/address_test.exs
  26. 17
      apps/explorer/test/explorer/chain_test.exs
  27. 15
      apps/explorer/test/explorer/counters/addresses_with_balance_counter_test.exs
  28. 1
      mix.lock

@ -145,86 +145,36 @@ describe('RECEIVED_NEW_INTERNAL_TRANSACTION_BATCH', () => {
})
describe('RECEIVED_NEW_TRANSACTION', () => {
test('with new transaction', () => {
test('increment the transactions count', () => {
const state = Object.assign({}, initialState, {
transactions: [{ transactionHash: 1, transactionHtml: 'test 1' }]
addressHash: "0x001",
transactionCount: 1
})
const action = {
type: 'RECEIVED_NEW_TRANSACTION',
msg: { transactionHash: 2, transactionHtml: 'test 2' }
}
const output = reducer(state, action)
expect(output.transactions).toEqual([
{ transactionHash: 2, transactionHtml: 'test 2' },
{ transactionHash: 1, transactionHtml: 'test 1' }
])
})
test('when channel has been disconnected', () => {
const state = Object.assign({}, initialState, {
channelDisconnected: true,
transactions: [{ transactionHash: 1, transactionHtml: 'test 1' }]
})
const action = {
type: 'RECEIVED_NEW_TRANSACTION',
msg: { transactionHash: 2, transactionHtml: 'test 2' }
msg: { fromAddressHash: "0x001", transactionHash: 2, transactionHtml: 'test 2' }
}
const output = reducer(state, action)
expect(output.transactions).toEqual([
{ transactionHash: 1, transactionHtml: 'test 1' }
])
})
test('beyond page one', () => {
const state = Object.assign({}, initialState, {
beyondPageOne: true,
transactions: [{ transactionHash: 1, transactionHtml: 'test 1' }]
})
const action = {
type: 'RECEIVED_NEW_TRANSACTION',
msg: { transactionHash: 2, transactionHtml: 'test 2' }
}
const output = reducer(state, action)
const newState = reducer(state, action)
expect(output.transactions).toEqual([
{ transactionHash: 1, transactionHtml: 'test 1' }
])
expect(newState.transactionCount).toEqual(2)
})
test('with filtered out transaction', () => {
test('does not increment the count if the channel is disconnected', () => {
const state = Object.assign({}, initialState, {
filter: 'to'
addressHash: "0x001",
transactionCount: 1,
channelDisconnected: true
})
const action = {
type: 'RECEIVED_NEW_TRANSACTION',
msg: { transactionHash: 2, transactionHtml: 'test 2' }
msg: { fromAddressHash: "0x001", transactionHash: 2, transactionHtml: 'test 2' }
}
const output = reducer(state, action)
expect(output.transactions).toEqual([])
})
})
describe('RECEIVED_NEXT_PAGE', () => {
test('with new transaction page', () => {
const state = Object.assign({}, initialState, {
loadingNextPage: true,
nextPageUrl: '1',
transactions: [{ transactionHash: 1, transactionHtml: 'test 1' }]
})
const action = {
type: 'RECEIVED_NEXT_PAGE',
msg: {
nextPageUrl: '2',
transactions: [{ transactionHash: 2, transactionHtml: 'test 2' }]
}
}
const output = reducer(state, action)
const newState = reducer(state, action)
expect(output.loadingNextPage).toEqual(false)
expect(output.nextPageUrl).toEqual('2')
expect(output.transactions).toEqual([
{ transactionHash: 1, transactionHtml: 'test 1' },
{ transactionHash: 2, transactionHtml: 'test 2' }
])
expect(newState.transactionCount).toEqual(1)
})
})

@ -0,0 +1,124 @@
import { reducer, initialState } from '../../../js/pages/address/transactions'
describe('RECEIVED_NEW_TRANSACTION', () => {
test('with new transaction', () => {
const state = Object.assign({}, initialState, {
items: ['transaction html']
})
const action = {
type: 'RECEIVED_NEW_TRANSACTION',
msg: { transactionHtml: 'another transaction html' }
}
const output = reducer(state, action)
expect(output.items).toEqual([ 'another transaction html', 'transaction html' ])
})
test('when channel has been disconnected', () => {
const state = Object.assign({}, initialState, {
channelDisconnected: true,
items: ['transaction html']
})
const action = {
type: 'RECEIVED_NEW_TRANSACTION',
msg: { transactionHtml: 'another transaction html' }
}
const output = reducer(state, action)
expect(output.items).toEqual(['transaction html'])
})
test('beyond page one', () => {
const state = Object.assign({}, initialState, {
beyondPageOne: true,
items: ['transaction html']
})
const action = {
type: 'RECEIVED_NEW_TRANSACTION',
msg: { transactionHtml: 'another transaction html' }
}
const output = reducer(state, action)
expect(output.items).toEqual([ 'transaction html' ])
})
test('adds the new transaction to state even when it is filtered by to', () => {
const state = Object.assign({}, initialState, {
addressHash: '0x001',
filter: 'to',
items: []
})
const action = {
type: 'RECEIVED_NEW_TRANSACTION',
msg: {
fromAddressHash: '0x002',
transactionHtml: 'transaction html',
toAddressHash: '0x001'
}
}
const output = reducer(state, action)
expect(output.items).toEqual(['transaction html'])
})
test(
'does nothing when it is filtered by to but the toAddressHash is different from addressHash',
() => {
const state = Object.assign({}, initialState, {
addressHash: '0x001',
filter: 'to',
items: []
})
const action = {
type: 'RECEIVED_NEW_TRANSACTION',
msg: {
fromAddressHash: '0x003',
transactionHtml: 'transaction html',
toAddressHash: '0x002'
}
}
const output = reducer(state, action)
expect(output.items).toEqual([])
})
test('adds the new transaction to state even when it is filtered by from', () => {
const state = Object.assign({}, initialState, {
addressHash: '0x001',
filter: 'from',
items: []
})
const action = {
type: 'RECEIVED_NEW_TRANSACTION',
msg: {
fromAddressHash: '0x001',
transactionHtml: 'transaction html',
toAddressHash: '0x002'
}
}
const output = reducer(state, action)
expect(output.items).toEqual(['transaction html'])
})
test(
'does nothing when it is filtered by from but the fromAddressHash is different from addressHash',
() => {
const state = Object.assign({}, initialState, {
addressHash: '0x001',
filter: 'to',
items: []
})
const action = {
type: 'RECEIVED_NEW_TRANSACTION',
msg: {
addressHash: '0x001',
transactionHtml: 'transaction html',
fromAddressHash: '0x002'
}
}
const output = reducer(state, action)
expect(output.items).toEqual([])
})
})

@ -21,6 +21,7 @@ import 'bootstrap'
import './locale'
import './pages/address'
import './pages/address/transactions'
import './pages/address/validations'
import './pages/blocks'
import './pages/chain'

@ -6,7 +6,6 @@ import numeral from 'numeral'
import socket from '../socket'
import { createStore, connectElements } from '../lib/redux_helpers.js'
import { batchChannel } from '../lib/utils'
import { withInfiniteScroll, connectInfiniteScroll } from '../lib/infinite_scroll_helpers'
import listMorph from '../lib/list_morph'
import { updateAllCalculatedUsdValues } from '../lib/currency.js'
import { loadTokenBalanceDropdown } from '../lib/token_balance_dropdown'
@ -23,18 +22,13 @@ export const initialState = {
transactionCount: null,
validationCount: null,
transactions: [],
internalTransactions: [],
internalTransactionsBatch: [],
beyondPageOne: null,
nextPageUrl: $('[data-selector="transactions-list"]').length ? URI(window.location).addQuery({ type: 'JSON' }).toString() : null
validatedBlocks: [],
beyondPageOne: false
}
export const reducer = withInfiniteScroll(baseReducer)
function baseReducer (state = initialState, action) {
export function reducer (state = initialState, action) {
switch (action.type) {
case 'PAGE_LOAD':
case 'ELEMENTS_LOAD': {
@ -85,33 +79,13 @@ function baseReducer (state = initialState, action) {
const transactionCount = (action.msg.fromAddressHash === state.addressHash) ? state.transactionCount + 1 : state.transactionCount
if (state.beyondPageOne ||
(state.filter === 'to' && action.msg.toAddressHash !== state.addressHash) ||
(state.filter === 'from' && action.msg.fromAddressHash !== state.addressHash)) {
return Object.assign({}, state, { transactionCount })
}
return Object.assign({}, state, {
transactions: [
action.msg,
...state.transactions
],
transactionCount: transactionCount
})
return Object.assign({}, state, { transactionCount })
}
case 'RECEIVED_UPDATED_BALANCE': {
return Object.assign({}, state, {
balance: action.msg.balance
})
}
case 'RECEIVED_NEXT_PAGE': {
return Object.assign({}, state, {
transactions: [
...state.transactions,
...action.msg.transactions
]
})
}
default:
return state
}
@ -152,32 +126,6 @@ const elements = {
$el.empty().append(numeral(state.validationCount).format())
}
},
'[data-selector="empty-transactions-list"]': {
render ($el, state) {
if (state.transactions.length || state.loadingNextPage || state.pagingError) {
$el.hide()
} else {
$el.show()
}
}
},
'[data-selector="transactions-list"]': {
load ($el) {
return {
transactions: $el.children().map((index, el) => ({
transactionHash: el.dataset.transactionHash,
transactionHtml: el.outerHTML
})).toArray()
}
},
render ($el, state, oldState) {
if (oldState.transactions === state.transactions) return
const container = $el[0]
const newElements = _.map(state.transactions, ({ transactionHtml }) => $(transactionHtml)[0])
return listMorph(container, newElements, { key: 'dataset.transactionHash' })
}
},
'[data-selector="internal-transactions-list"]': {
load ($el) {
return {
@ -215,7 +163,6 @@ if ($addressDetailsPage.length) {
beyondPageOne: !!blockNumber
})
connectElements({ store, elements })
$('[data-selector="transactions-list"]').length && connectInfiniteScroll(store)
const addressChannel = socket.channel(`addresses:${addressHash}`, {})
addressChannel.join()

@ -0,0 +1,73 @@
import $ from 'jquery'
import _ from 'lodash'
import URI from 'urijs'
import humps from 'humps'
import socket from '../../socket'
import { connectElements } from '../../lib/redux_helpers.js'
import { createAsyncLoadStore } from '../../lib/async_listing_load'
export const initialState = {
addressHash: null,
channelDisconnected: false,
filter: null
}
export function reducer (state, action) {
switch (action.type) {
case 'PAGE_LOAD':
case 'ELEMENTS_LOAD': {
return Object.assign({}, state, _.omit(action, 'type'))
}
case 'CHANNEL_DISCONNECTED': {
if (state.beyondPageOne) return state
return Object.assign({}, state, { channelDisconnected: true })
}
case 'RECEIVED_NEW_TRANSACTION': {
if (state.channelDisconnected) return state
if (state.beyondPageOne ||
(state.filter === 'to' && action.msg.toAddressHash !== state.addressHash) ||
(state.filter === 'from' && action.msg.fromAddressHash !== state.addressHash)) {
return state
}
return Object.assign({}, state, { items: [ action.msg.transactionHtml, ...state.items ] })
}
default:
return state
}
}
const elements = {
'[data-selector="channel-disconnected-message"]': {
render ($el, state) {
if (state.channelDisconnected) $el.show()
}
}
}
if ($('[data-page="address-transactions"]').length) {
const store = createAsyncLoadStore(reducer, initialState, 'dataset.transactionHash')
const addressHash = $('[data-page="address-details"]')[0].dataset.pageAddressHash
const { filter, blockNumber } = humps.camelizeKeys(URI(window.location).query(true))
connectElements({ store, elements })
store.dispatch({
type: 'PAGE_LOAD',
addressHash,
filter,
beyondPageOne: !!blockNumber
})
const addressChannel = socket.channel(`addresses:${addressHash}`, {})
addressChannel.join()
addressChannel.onError(() => store.dispatch({ type: 'CHANNEL_DISCONNECTED' }))
addressChannel.on('transaction', (msg) => {
store.dispatch({
type: 'RECEIVED_NEW_TRANSACTION',
msg: humps.camelizeKeys(msg)
})
})
}

@ -8,7 +8,7 @@ defmodule BlockScoutWeb.AddressController do
def index(conn, _params) do
render(conn, "index.html",
address_tx_count_pairs: Chain.list_top_addresses(),
address_estimated_count: Chain.address_estimated_count(),
address_count: Chain.count_addresses_with_balance_from_cache(),
exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(),
total_supply: Chain.total_supply()
)

@ -10,7 +10,6 @@ defmodule BlockScoutWeb.AddressTransactionController do
alias BlockScoutWeb.TransactionView
alias Explorer.{Chain, Market}
alias Explorer.Chain.Hash
alias Explorer.ExchangeRates.Token
alias Phoenix.View
@ -48,29 +47,21 @@ defmodule BlockScoutWeb.AddressTransactionController do
conn,
:index,
address,
next_page_params
Map.delete(next_page_params, "type")
)
end
json(
conn,
%{
transactions:
Enum.map(transactions, fn transaction ->
%{
transaction_hash: Hash.to_string(transaction.hash),
transaction_html:
View.render_to_string(
TransactionView,
"_tile.html",
current_address: address,
transaction: transaction
)
}
end),
next_page_url: next_page_url
}
)
transactions_json =
Enum.map(transactions, fn transaction ->
View.render_to_string(
TransactionView,
"_tile.html",
current_address: address,
transaction: transaction
)
end)
json(conn, %{items: transactions_json, next_page_path: next_page_url})
else
:error ->
unprocessable_entity(conn)
@ -90,7 +81,8 @@ defmodule BlockScoutWeb.AddressTransactionController do
exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(),
filter: params["filter"],
transaction_count: transaction_count(address),
validation_count: validation_count(address)
validation_count: validation_count(address),
current_path: current_path(conn)
)
else
:error ->

@ -36,7 +36,7 @@ defmodule BlockScoutWeb.ChainController do
render(
conn,
"show.html",
address_estimated_count: Chain.address_estimated_count(),
address_count: Chain.count_addresses_with_balance_from_cache(),
average_block_time: Chain.average_block_time(),
blocks: blocks,
exchange_rate: exchange_rate,

@ -72,6 +72,9 @@ defmodule BlockScoutWeb.Endpoint do
plug(BlockScoutWeb.Prometheus.Exporter)
# 'x-apollo-tracing' header for https://www.graphqlbin.com to work with our GraphQL endpoint
plug(CORSPlug, headers: ["x-apollo-tracing" | CORSPlug.defaults()[:headers]])
plug(BlockScoutWeb.Router)
def init(_key, config) do

@ -10,7 +10,7 @@ defmodule BlockScoutWeb.Notifier do
alias Explorer.ExchangeRates.Token
def handle_event({:chain_event, :addresses, :realtime, addresses}) do
Endpoint.broadcast("addresses:new_address", "count", %{count: Chain.address_estimated_count()})
Endpoint.broadcast("addresses:new_address", "count", %{count: Chain.count_addresses_with_balance_from_cache()})
addresses
|> Stream.reject(fn %Address{fetched_coin_balance: fetched_coin_balance} -> is_nil(fetched_coin_balance) end)

@ -4,16 +4,16 @@
<h1><%= gettext "Addresses" %></h1>
<p>
<%= gettext "Showing 250 addresses of" %>
<%= Cldr.Number.to_string!(@address_estimated_count, format: "#,###") %>
<%= Cldr.Number.to_string!(@address_count, format: "#,###") %>
<%= gettext "total addresses with a balance" %>
</p>
<span data-selector="top-addresses-list">
<%= for {{address, tx_count}, index} <- Enum.with_index(@address_tx_count_pairs, 1) do %>
<%= render "_tile.html",
<%= render "_tile.html",
address: address, index: index, exchange_rate: @exchange_rate,
total_supply: @total_supply, tx_count: tx_count,
validation_count: validation_count(address) %>
validation_count: validation_count(address) %>
<% end %>
</span>
</div>

@ -2,13 +2,13 @@
<%= render BlockScoutWeb.AddressView, "overview.html", assigns %>
<section>
<section data-page="address-transactions">
<div class="card">
<div class="card-header">
<%= render BlockScoutWeb.AddressView, "_tabs.html", assigns %>
</div>
<div class="card-body">
<div class="card-body" data-async-listing="<%= @current_path %>">
<div data-selector="channel-disconnected-message" style="display: none;">
<div data-selector="reload-button" class="alert alert-danger">
<a href="#" class="alert-link"><%= gettext "Connection Lost, click to load newer transactions" %></a>
@ -51,20 +51,36 @@
</div>
</div>
<h2 class="card-title"><%= gettext "Transactions" %></h2>
<span data-selector="transactions-list">
</span>
<div data-selector="loading-next-page" class="tile tile-muted text-center mt-3">
<button data-error-message class="alert alert-danger col-12 text-left" style="display: none;">
<span href="#" class="alert-link"><%= gettext("Something went wrong, click to reload.") %></span>
</button>
<div data-empty-response-message style="display: none;">
<div class="tile tile-muted text-center" data-selector="empty-transactions-list">
<%= gettext "There are no transactions for this address." %>
</div>
</div>
<div data-loading-message class="tile tile-muted text-center mt-3">
<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 data-selector="paging-error-message" class="alert alert-danger text-center mt-3" style="display: none;">
<%= gettext("Error trying to fetch transactions.") %>
</div>
<div class="tile tile-muted text-center" data-selector="empty-transactions-list" style="display: none;">
<%= gettext "There are no transactions for this address." %>
<div data-items></div>
<a href="#" class="button button-secondary button-small float-right mt-4" data-next-page-button style="display: none;">
<%= gettext("Older") %>
</a>
<div class="button button-secondary button-small float-right mt-4" data-loading-button style="display: none;">
<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>

@ -43,7 +43,7 @@
<%= gettext "Wallet addresses" %>
</span>
<span class="dashboard-banner-network-stats-value" data-selector="address-count">
<%= Cldr.Number.to_string!(@address_estimated_count, format: "#,###") %>
<%= Cldr.Number.to_string!(@address_count, format: "#,###") %>
</span>
</div>
</div>

@ -68,6 +68,8 @@ defmodule BlockScoutWeb.Mixfile do
# Absinthe support for the Relay framework
{:absinthe_relay, "~> 1.4"},
{:bypass, "~> 0.8", only: :test},
# To add (CORS)(https://www.w3.org/TR/cors/)
{:cors_plug, "~> 2.0"},
{:credo, "0.10.2", only: [:dev, :test], runtime: false},
# For Absinthe to load data in batches
{:dataloader, "~> 1.0.0"},

@ -639,6 +639,7 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:76
#: lib/block_scout_web/templates/address_token_transfer/index.html.eex:31
#: lib/block_scout_web/templates/address_transaction/index.html.eex:75
#: lib/block_scout_web/templates/address_validation/index.html.eex:126
#: lib/block_scout_web/templates/block/index.html.eex:30
#: lib/block_scout_web/templates/block_transaction/index.html.eex:50
@ -851,7 +852,7 @@ msgid "There are no tokens."
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_transaction/index.html.eex:67
#: lib/block_scout_web/templates/address_transaction/index.html.eex:60
msgid "There are no transactions for this address."
msgstr ""
@ -1220,7 +1221,8 @@ msgstr ""
#: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:72
#: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:83
#: lib/block_scout_web/templates/address_token_transfer/index.html.eex:38
#: lib/block_scout_web/templates/address_transaction/index.html.eex:61
#: lib/block_scout_web/templates/address_transaction/index.html.eex:69
#: lib/block_scout_web/templates/address_transaction/index.html.eex:83
#: lib/block_scout_web/templates/block/index.html.eex:22
#: lib/block_scout_web/templates/pending_transaction/index.html.eex:33
#: lib/block_scout_web/templates/tokens/holder/index.html.eex:37
@ -1251,11 +1253,6 @@ msgstr ""
msgid "Show Raw Input"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_transaction/index.html.eex:64
msgid "Error trying to fetch transactions."
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/block_transaction/404.html.eex:7
msgid "Block Details"
@ -1419,6 +1416,7 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:60
#: lib/block_scout_web/templates/address_token_transfer/index.html.eex:26
#: lib/block_scout_web/templates/address_transaction/index.html.eex:55
#: lib/block_scout_web/templates/address_validation/index.html.eex:121
#: lib/block_scout_web/templates/tokens/holder/index.html.eex:23
#: lib/block_scout_web/templates/tokens/transfer/index.html.eex:21

@ -639,6 +639,7 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:76
#: lib/block_scout_web/templates/address_token_transfer/index.html.eex:31
#: lib/block_scout_web/templates/address_transaction/index.html.eex:75
#: lib/block_scout_web/templates/address_validation/index.html.eex:126
#: lib/block_scout_web/templates/block/index.html.eex:30
#: lib/block_scout_web/templates/block_transaction/index.html.eex:50
@ -851,7 +852,7 @@ msgid "There are no tokens."
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_transaction/index.html.eex:67
#: lib/block_scout_web/templates/address_transaction/index.html.eex:60
msgid "There are no transactions for this address."
msgstr ""
@ -1220,7 +1221,8 @@ msgstr ""
#: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:72
#: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:83
#: lib/block_scout_web/templates/address_token_transfer/index.html.eex:38
#: lib/block_scout_web/templates/address_transaction/index.html.eex:61
#: lib/block_scout_web/templates/address_transaction/index.html.eex:69
#: lib/block_scout_web/templates/address_transaction/index.html.eex:83
#: lib/block_scout_web/templates/block/index.html.eex:22
#: lib/block_scout_web/templates/pending_transaction/index.html.eex:33
#: lib/block_scout_web/templates/tokens/holder/index.html.eex:37
@ -1251,11 +1253,6 @@ msgstr ""
msgid "Show Raw Input"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_transaction/index.html.eex:64
msgid "Error trying to fetch transactions."
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/block_transaction/404.html.eex:7
msgid "Block Details"
@ -1419,6 +1416,7 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:60
#: lib/block_scout_web/templates/address_token_transfer/index.html.eex:26
#: lib/block_scout_web/templates/address_transaction/index.html.eex:55
#: lib/block_scout_web/templates/address_validation/index.html.eex:121
#: lib/block_scout_web/templates/tokens/holder/index.html.eex:23
#: lib/block_scout_web/templates/tokens/transfer/index.html.eex:21

@ -34,15 +34,14 @@ defmodule BlockScoutWeb.AddressTransactionControllerTest do
|> insert(to_address: address)
|> with_block(block)
conn = get(conn, address_transaction_path(conn, :index, address), %{"type" => "JSON"})
{:ok, %{"transactions" => transactions}} = conn.resp_body |> Poison.decode()
conn = get(conn, address_transaction_path(conn, :index, address, %{"type" => "JSON"}))
actual_transaction_hashes = Enum.map(transactions, & &1["transaction_hash"])
transaction_tiles = json_response(conn, 200)["items"]
transaction_hashes = Enum.map([to_transaction.hash, from_transaction.hash], &to_string(&1))
assert json_response(conn, 200)
assert Enum.member?(actual_transaction_hashes, to_string(from_transaction.hash))
assert Enum.member?(actual_transaction_hashes, to_string(to_transaction.hash))
assert Enum.all?(transaction_hashes, fn transaction_hash ->
Enum.any?(transaction_tiles, &String.contains?(&1, transaction_hash))
end)
end
test "includes USD exchange rate value for address in assigns", %{conn: conn} do
@ -60,7 +59,7 @@ defmodule BlockScoutWeb.AddressTransactionControllerTest do
50
|> insert_list(:transaction, from_address: address)
|> with_block()
|> Enum.map(&to_string(&1.hash))
|> Enum.map(& &1.hash)
%Transaction{block_number: block_number, index: index} =
:transaction
@ -69,19 +68,16 @@ defmodule BlockScoutWeb.AddressTransactionControllerTest do
conn =
get(conn, address_transaction_path(BlockScoutWeb.Endpoint, :index, address.hash), %{
"type" => "JSON",
"block_number" => Integer.to_string(block_number),
"index" => Integer.to_string(index)
"index" => Integer.to_string(index),
"type" => "JSON"
})
{:ok, %{"transactions" => transactions}} = conn.resp_body |> Poison.decode()
transaction_tiles = json_response(conn, 200)["items"]
actual_hashes =
transactions
|> Enum.map(& &1["transaction_hash"])
|> Enum.reverse()
assert second_page_hashes == actual_hashes
assert Enum.all?(second_page_hashes, fn address_hash ->
Enum.any?(transaction_tiles, &String.contains?(&1, to_string(address_hash)))
end)
end
test "next_page_params exist if not on last page", %{conn: conn} do
@ -92,18 +88,9 @@ defmodule BlockScoutWeb.AddressTransactionControllerTest do
|> insert_list(:transaction, from_address: address)
|> with_block(block)
conn = get(conn, address_transaction_path(BlockScoutWeb.Endpoint, :index, address.hash), %{"type" => "JSON"})
{:ok, %{"next_page_url" => actual_next_page_url}} = conn.resp_body |> Poison.decode()
conn = get(conn, address_transaction_path(conn, :index, address.hash, %{"type" => "JSON"}))
expected_next_page_url =
address_transaction_path(BlockScoutWeb.Endpoint, :index, address.hash, %{
"block_number" => number,
"index" => 10,
"type" => "JSON"
})
assert expected_next_page_url == actual_next_page_url
assert json_response(conn, 200)["next_page_path"]
end
test "next_page_params are empty if on last page", %{conn: conn} do
@ -113,11 +100,9 @@ defmodule BlockScoutWeb.AddressTransactionControllerTest do
|> insert(from_address: address)
|> with_block()
conn = get(conn, address_transaction_path(BlockScoutWeb.Endpoint, :index, address.hash), %{"type" => "JSON"})
conn = get(conn, address_transaction_path(conn, :index, address.hash, %{"type" => "JSON"}))
{:ok, %{"next_page_url" => next_page_url}} = conn.resp_body |> Poison.decode()
refute next_page_url
refute json_response(conn, 200)["next_page_path"]
end
test "returns parent transaction for a contract address", %{conn: conn} do
@ -140,11 +125,11 @@ defmodule BlockScoutWeb.AddressTransactionControllerTest do
conn = get(conn, address_transaction_path(conn, :index, address), %{"type" => "JSON"})
{:ok, %{"transactions" => transactions}} = conn.resp_body |> Poison.decode()
transaction_hashes = Enum.map(transactions, & &1["transaction_hash"])
transaction_tiles = json_response(conn, 200)["items"]
assert [to_string(transaction.hash)] == transaction_hashes
assert Enum.all?([transaction.hash], fn transaction_hash ->
Enum.any?(transaction_tiles, &String.contains?(&1, to_string(transaction_hash)))
end)
end
end
end

@ -17,8 +17,6 @@ config :explorer, Explorer.Integrations.EctoLogger, query_time_ms_threshold: 2_0
config :explorer, Explorer.ExchangeRates, enabled: true
config :explorer, Explorer.Counters.BlockValidationCounter, enabled: true
config :explorer, Explorer.Market.History.Cataloger, enabled: true
config :explorer, Explorer.Repo,
@ -32,8 +30,12 @@ config :explorer, Explorer.Tracer,
config :explorer, Explorer.Counters.TokenTransferCounter, enabled: true
config :explorer, Explorer.Counters.BlockValidationCounter, enabled: true, enable_consolidation: true
config :explorer, Explorer.Counters.TokenHoldersCounter, enabled: true, enable_consolidation: true
config :explorer, Explorer.Counters.AddessesWithBalanceCounter, enabled: true, enable_consolidation: true
if System.get_env("SUPPLY_MODULE") == "TransactionAndLog" do
config :explorer, supply: Explorer.Chain.Supply.TransactionAndLog
end

@ -3,8 +3,6 @@ use Mix.Config
# Lower hashing rounds for faster tests
config :bcrypt_elixir, log_rounds: 4
config :explorer, Explorer.Counters.TokenHoldersCounter, enabled: true, enable_consolidation: false
# Configure your database
config :explorer, Explorer.Repo,
adapter: Ecto.Adapters.Postgres,
@ -21,6 +19,12 @@ config :explorer, Explorer.Market.History.Cataloger, enabled: false
config :explorer, Explorer.Tracer, disabled?: false
config :explorer, Explorer.Counters.BlockValidationCounter, enabled: true, enable_consolidation: false
config :explorer, Explorer.Counters.TokenHoldersCounter, enabled: true, enable_consolidation: false
config :explorer, Explorer.Counters.AddessesWithBalanceCounter, enabled: true, enable_consolidation: false
config :logger, :explorer,
level: :warn,
path: Path.absname("logs/test/explorer.log")

@ -35,7 +35,8 @@ defmodule Explorer.Application do
configure(Explorer.Market.History.Cataloger),
configure(Explorer.Counters.TokenHoldersCounter),
configure(Explorer.Counters.TokenTransferCounter),
configure(Explorer.Counters.BlockValidationCounter)
configure(Explorer.Counters.BlockValidationCounter),
configure(Explorer.Counters.AddessesWithBalanceCounter)
]
|> List.flatten()
end

@ -40,7 +40,13 @@ defmodule Explorer.Chain do
alias Explorer.Chain.Block.Reward
alias Explorer.{PagingOptions, Repo}
alias Explorer.Counters.{BlockValidationCounter, TokenHoldersCounter, TokenTransferCounter}
alias Explorer.Counters.{
AddessesWithBalanceCounter,
BlockValidationCounter,
TokenHoldersCounter,
TokenTransferCounter
}
alias Dataloader.Ecto, as: DataloaderEcto
@ -82,18 +88,24 @@ defmodule Explorer.Chain do
@typep paging_options :: {:paging_options, PagingOptions.t()}
@doc """
Gets an estimated count of `t:Explorer.Chain.Address.t/0`'s where the `fetched_coin_balance` is > 0
Gets from the cache the count of `t:Explorer.Chain.Address.t/0`'s where the `fetched_coin_balance` is > 0
"""
@spec address_estimated_count :: non_neg_integer()
def address_estimated_count do
{:ok, %Postgrex.Result{rows: result}} =
Repo.query("""
EXPLAIN SELECT COUNT(a0.hash) FROM addresses AS a0 WHERE (a0.fetched_coin_balance > 0)
""")
@spec count_addresses_with_balance_from_cache :: non_neg_integer()
def count_addresses_with_balance_from_cache do
AddessesWithBalanceCounter.fetch()
end
@doc """
Counts the number of addresses with fetched coin balance > 0.
{[explain], _} = List.pop_at(result, 1)
[[_ | [rows]]] = Regex.scan(~r/rows=(\d+)/, explain)
String.to_integer(rows)
This function should be used with caution. In larger databases, it may take a
while to have the return back.
"""
def count_addresses_with_balance do
Repo.one(
Address.count_with_fetched_coin_balance(),
timeout: :infinity
)
end
@doc """

@ -104,4 +104,15 @@ defmodule Explorer.Chain.Address do
@protocol.to_string(hash)
end
end
@doc """
Counts all the addresses where the `fetched_coin_balance` is > 0.
"""
def count_with_fetched_coin_balance do
from(
a in Address,
select: fragment("COUNT(*)"),
where: a.fetched_coin_balance > ^0
)
end
end

@ -0,0 +1,117 @@
defmodule Explorer.Counters.AddessesWithBalanceCounter do
@moduledoc """
Caches the number of addresses with fetched coin balance > 0.
It loads the count asynchronously and in a time interval of 30 minutes.
"""
use GenServer
alias Explorer.Chain
@table :addresses_with_balance_counter
@cache_key "addresses_with_balance"
def table_name do
@table
end
def cache_key do
@cache_key
end
# It is undesirable to automatically start the consolidation in all environments.
# Consider the test environment: if the consolidation initiates but does not
# finish before a test ends, that test will fail. This way, hundreds of
# tests were failing before disabling the consolidation and the scheduler in
# the test env.
config = Application.get_env(:explorer, Explorer.Counters.AddessesWithBalanceCounter)
@enable_consolidation Keyword.get(config, :enable_consolidation)
@doc """
Starts a process to periodically update the counter of the token holders.
"""
@spec start_link(term()) :: GenServer.on_start()
def start_link(_) do
GenServer.start_link(__MODULE__, :ok, name: __MODULE__)
end
@impl true
def init(args) do
create_table()
if enable_consolidation?() do
Task.start_link(&consolidate/0)
schedule_next_consolidation()
end
{:ok, args}
end
def create_table do
opts = [
:set,
:named_table,
:public,
read_concurrency: true
]
:ets.new(table_name(), opts)
end
defp schedule_next_consolidation do
if enable_consolidation?() do
Process.send_after(self(), :consolidate, :timer.minutes(30))
end
end
@doc """
Inserts new items into the `:ets` table.
"""
def insert_counter({key, info}) do
:ets.insert(table_name(), {key, info})
end
@impl true
def handle_info(:consolidate, state) do
consolidate()
schedule_next_consolidation()
{:noreply, state}
end
@doc """
Fetches the info for a specific item from the `:ets` table.
"""
def fetch do
do_fetch(:ets.lookup(table_name(), cache_key()))
end
defp do_fetch([{_, result}]), do: result
defp do_fetch([]), do: 0
@doc """
Consolidates the info by populating the `:ets` table with the current database information.
"""
def consolidate do
counter = Chain.count_addresses_with_balance()
insert_counter({cache_key(), counter})
end
@doc """
Returns a boolean that indicates whether consolidation is enabled
In order to choose whether or not to enable the scheduler and the initial
consolidation, change the following Explorer config:
`config :explorer, Explorer.Counters.AddressesWithBalanceCounter, enable_consolidation: true`
to:
`config :explorer, Explorer.Counters.AddressesWithBalanceCounter, enable_consolidation: false`
"""
def enable_consolidation?, do: @enable_consolidation
end

@ -2,7 +2,7 @@ defmodule Explorer.Counters.BlockValidationCounter do
use GenServer
@moduledoc """
Module responsible for fetching and consolidating the number of
Module responsible for fetching and consolidating the number of
validations from an address.
"""
@ -15,6 +15,14 @@ defmodule Explorer.Counters.BlockValidationCounter do
@table
end
# It is undesirable to automatically start the consolidation in all environments.
# Consider the test environment: if the consolidation initiates but does not
# finish before a test ends, that test will fail. This way, hundreds of
# tests were failing before disabling the consolidation and the scheduler in
# the test env.
config = Application.get_env(:explorer, Explorer.Counters.BlockValidationCounter)
@enable_consolidation Keyword.get(config, :enable_consolidation)
@doc """
Creates a process to continually monitor the validation counts.
"""
@ -28,7 +36,9 @@ defmodule Explorer.Counters.BlockValidationCounter do
def init(args) do
create_table()
Task.start_link(&consolidate_blocks/0)
if enable_consolidation?() do
Task.start_link(&consolidate_blocks/0)
end
Chain.subscribe_to_events(:blocks)
@ -40,8 +50,7 @@ defmodule Explorer.Counters.BlockValidationCounter do
:set,
:named_table,
:public,
read_concurrency: true,
write_concurrency: true
read_concurrency: true
]
:ets.new(table_name(), opts)
@ -59,7 +68,7 @@ defmodule Explorer.Counters.BlockValidationCounter do
end
@doc """
Fetches the number of validations related to an `address_hash`.
Fetches the number of validations related to an `address_hash`.
"""
@spec fetch(Hash.Address.t()) :: non_neg_integer
def fetch(addr_hash) do
@ -91,4 +100,18 @@ defmodule Explorer.Counters.BlockValidationCounter do
:ets.update_counter(table_name(), string_addr, number, default)
end
@doc """
Returns a boolean that indicates whether consolidation is enabled
In order to choose whether or not to enable the scheduler and the initial
consolidation, change the following Explorer config:
`config :explorer, Explorer.Counters.BlockValidationCounter, enable_consolidation: true`
to:
`config :explorer, Explorer.Counters.BlockValidationCounter, enable_consolidation: false`
"""
def enable_consolidation?, do: @enable_consolidation
end

@ -2,6 +2,7 @@ defmodule Explorer.Chain.AddressTest do
use Explorer.DataCase
alias Explorer.Chain.Address
alias Explorer.Repo
describe "changeset/2" do
test "with valid attributes" do
@ -15,4 +16,14 @@ defmodule Explorer.Chain.AddressTest do
refute changeset.valid?
end
end
describe "count_with_fetched_coin_balance/0" do
test "returns the number of addresses with fetched_coin_balance greater than 0" do
insert(:address, fetched_coin_balance: 0)
insert(:address, fetched_coin_balance: 1)
insert(:address, fetched_coin_balance: 2)
assert Repo.one(Address.count_with_fetched_coin_balance()) == 2
end
end
end

@ -24,13 +24,22 @@ defmodule Explorer.ChainTest do
alias Explorer.Chain.Supply.ProofOfAuthority
alias Explorer.Counters.TokenHoldersCounter
alias Explorer.Counters.{AddessesWithBalanceCounter, TokenHoldersCounter}
doctest Explorer.Chain
describe "address_estimated_count/1" do
test "returns integer" do
assert is_integer(Chain.address_estimated_count())
describe "count_addresses_with_balance_from_cache/0" do
test "returns the number of addresses with fetched_coin_balance > 0" do
insert(:address, fetched_coin_balance: 0)
insert(:address, fetched_coin_balance: 1)
insert(:address, fetched_coin_balance: 2)
AddessesWithBalanceCounter.consolidate()
addresses_with_balance = Chain.count_addresses_with_balance_from_cache()
assert is_integer(addresses_with_balance)
assert addresses_with_balance == 2
end
end

@ -0,0 +1,15 @@
defmodule Explorer.Counters.AddessesWithBalanceCounterTest do
use Explorer.DataCase
alias Explorer.Counters.AddessesWithBalanceCounter
test "populates the cache with the number of addresses with fetched coin balance greater than 0" do
insert(:address, fetched_coin_balance: 0)
insert(:address, fetched_coin_balance: 1)
insert(:address, fetched_coin_balance: 2)
AddessesWithBalanceCounter.consolidate()
assert AddessesWithBalanceCounter.fetch() == 2
end
end

@ -17,6 +17,7 @@
"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], []},
"comeonin": {:hex, :comeonin, "4.1.1", "c7304fc29b45b897b34142a91122bc72757bc0c295e9e824999d5179ffc08416", [:mix], [{:argon2_elixir, "~> 1.2", [hex: :argon2_elixir, optional: true]}, {:bcrypt_elixir, "~> 0.12.1 or ~> 1.0", [hex: :bcrypt_elixir, optional: true]}, {:pbkdf2_elixir, "~> 0.12", [hex: :pbkdf2_elixir, optional: true]}]},
"connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], []},
"cors_plug": {:hex, :cors_plug, "2.0.0", "238ddb479f92b38f6dc1ae44b8d81f0387f9519101a6da442d543ab70ee0e482", [:mix], [{:plug, "~> 1.3 or ~> 1.4 or ~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
"cowboy": {:hex, :cowboy, "1.1.2", "61ac29ea970389a88eca5a65601460162d370a70018afe6f949a29dca91f3bb0", [:rebar3], [{:cowlib, "~> 1.0.2", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3.2", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm"},
"cowlib": {:hex, :cowlib, "1.0.2", "9d769a1d062c9c3ac753096f868ca121e2730b9a377de23dec0f7e08b1df84ee", [:make], []},
"credo": {:hex, :credo, "0.10.2", "03ad3a1eff79a16664ed42fc2975b5e5d0ce243d69318060c626c34720a49512", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"},

Loading…
Cancel
Save