Blockchain explorer for Ethereum based network and a tool for inspecting and analyzing EVM based blockchains.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
blockscout/apps/block_scout_web/assets/js/pages/chain.js

444 lines
14 KiB

import $ from 'jquery'
import omit from 'lodash.omit'
import first from 'lodash.first'
import rangeRight from 'lodash.rangeright'
import find from 'lodash.find'
import map from 'lodash.map'
import humps from 'humps'
import numeral from 'numeral'
import socket from '../socket'
import { updateAllCalculatedUsdValues, formatUsdValue } from '../lib/currency'
import { createStore, connectElements } from '../lib/redux_helpers.js'
import { batchChannel, showLoader } from '../lib/utils'
import listMorph from '../lib/list_morph'
import '../app'
import { openErrorModal, openSuccessModal, openWarningModal } from '../lib/modals'
const BATCH_THRESHOLD = 6
const BLOCKS_PER_PAGE = 4
export const initialState = {
addressCount: null,
availableSupply: null,
averageBlockTime: null,
marketHistoryData: null,
blocks: [],
blocksLoading: true,
blocksError: false,
transactions: [],
transactionsBatch: [],
transactionsError: false,
transactionsLoading: true,
transactionCount: null,
totalGasUsageCount: null,
usdMarketCap: null,
blockCount: null
}
export const reducer = withMissingBlocks(baseReducer)
function baseReducer (state = initialState, action) {
switch (action.type) {
case 'ELEMENTS_LOAD': {
return Object.assign({}, state, omit(action, 'type'))
}
case 'RECEIVED_NEW_ADDRESS_COUNT': {
return Object.assign({}, state, {
addressCount: action.msg.count
})
}
case 'RECEIVED_NEW_BLOCK': {
const firstBlock = ($('#indexer-first-block').text() && parseInt($('#indexer-first-block').text(), 10)) || 0
const blockCount = (action.msg.blockNumber - firstBlock) + 1
// @ts-ignore
if (!state.blocks.length || state.blocks[0].blockNumber < action.msg.blockNumber) {
let pastBlocks
if (state.blocks.length < BLOCKS_PER_PAGE) {
pastBlocks = state.blocks
} else {
$('.miner-address-tooltip').tooltip('hide')
pastBlocks = state.blocks.slice(0, -1)
}
return Object.assign({}, state, {
averageBlockTime: action.msg.averageBlockTime,
blocks: [
action.msg,
...pastBlocks
],
blockCount
})
} else {
return Object.assign({}, state, {
// @ts-ignore
blocks: state.blocks.map((block) => block.blockNumber === action.msg.blockNumber ? action.msg : block),
blockCount
})
}
}
case 'START_BLOCKS_FETCH': {
return Object.assign({}, state, { blocksError: false, blocksLoading: true })
}
case 'BLOCKS_FINISH_REQUEST': {
return Object.assign({}, state, { blocksLoading: false })
}
case 'BLOCKS_FETCHED': {
return Object.assign({}, state, { blocks: [...action.msg.blocks], blocksLoading: false })
}
case 'BLOCKS_REQUEST_ERROR': {
return Object.assign({}, state, { blocksError: true, blocksLoading: false })
}
case 'RECEIVED_NEW_EXCHANGE_RATE': {
return Object.assign({}, state, {
availableSupply: action.msg.exchangeRate.availableSupply,
marketHistoryData: action.msg.marketHistoryData,
usdMarketCap: action.msg.exchangeRate.marketCapUsd
})
}
case 'RECEIVED_NEW_TRANSACTION_BATCH': {
if (state.channelDisconnected) return state
const transactionCount = state.transactionCount + action.msgs.length
if (state.transactionsLoading || state.transactionsError) {
return Object.assign({}, state, { transactionCount })
}
const transactionsLength = state.transactions.length + action.msgs.length
if (transactionsLength < BATCH_THRESHOLD) {
return Object.assign({}, state, {
transactions: [
...action.msgs.reverse(),
...state.transactions
],
transactionCount
})
} else if (!state.transactionsBatch.length && action.msgs.length < BATCH_THRESHOLD) {
return Object.assign({}, state, {
transactions: [
...action.msgs.reverse(),
...state.transactions.slice(0, -1 * action.msgs.length)
],
transactionCount
})
} else {
return Object.assign({}, state, {
transactionsBatch: [
...action.msgs.reverse(),
...state.transactionsBatch
],
transactionCount
})
}
}
case 'TRANSACTION_BATCH_EXPANDED': {
return Object.assign({}, state, {
transactionsBatch: []
})
}
case 'RECEIVED_UPDATED_TRANSACTION_STATS': {
return Object.assign({}, state, {
transactionStats: action.msg.stats
})
}
case 'START_TRANSACTIONS_FETCH':
return Object.assign({}, state, { transactionsError: false, transactionsLoading: true })
case 'TRANSACTIONS_FETCHED':
return Object.assign({}, state, { transactions: [...action.msg.transactions] })
case 'TRANSACTIONS_FETCH_ERROR':
return Object.assign({}, state, { transactionsError: true })
case 'FINISH_TRANSACTIONS_FETCH':
return Object.assign({}, state, { transactionsLoading: false })
default:
return state
}
}
function withMissingBlocks (reducer) {
return (...args) => {
const result = reducer(...args)
if (!result.blocks || result.blocks.length < 2) return result
const maxBlock = first(result.blocks).blockNumber
const minBlock = maxBlock - (result.blocks.length - 1)
return Object.assign({}, result, {
blocks: rangeRight(minBlock, maxBlock + 1)
.map((blockNumber) => find(result.blocks, ['blockNumber', blockNumber]) || {
blockNumber,
chainBlockHtml: placeHolderBlock(blockNumber)
})
})
}
}
let chart
const elements = {
'[data-chart="historyChart"]': {
load () {
// @ts-ignore
chart = window.dashboardChart
},
render (_$el, state, oldState) {
if (!chart || (oldState.availableSupply === state.availableSupply && oldState.marketHistoryData === state.marketHistoryData) || !state.availableSupply) return
chart.updateMarketHistory(state.availableSupply, state.marketHistoryData)
if (!chart || (JSON.stringify(oldState.transactionStats) === JSON.stringify(state.transactionStats))) return
chart.updateTransactionHistory(state.transactionStats)
}
},
'[data-selector="transaction-count"]': {
load ($el) {
return { transactionCount: numeral($el.text()).value() }
},
render ($el, state, oldState) {
if (oldState.transactionCount === state.transactionCount) return
$el.empty().append(numeral(state.transactionCount).format())
}
},
'[data-selector="total-gas-usage"]': {
load ($el) {
return { totalGasUsageCount: numeral($el.text()).value() }
},
render ($el, state, oldState) {
if (oldState.totalGasUsageCount === state.totalGasUsageCount) return
$el.empty().append(numeral(state.totalGasUsageCount).format())
}
},
'[data-selector="block-count"]': {
load ($el) {
return { blockCount: numeral($el.text()).value() }
},
render ($el, state, oldState) {
if (oldState.blockCount === state.blockCount) return
$el.empty().append(numeral(state.blockCount).format())
}
},
'[data-selector="address-count"]': {
render ($el, state, oldState) {
if (oldState.addressCount === state.addressCount) return
$el.empty().append(state.addressCount)
}
},
'[data-selector="average-block-time"]': {
render ($el, state, oldState) {
if (oldState.averageBlockTime === state.averageBlockTime) return
$el.empty().append(state.averageBlockTime)
}
},
'[data-selector="market-cap"]': {
render ($el, state, oldState) {
if (oldState.usdMarketCap === state.usdMarketCap) return
$el.empty().append(formatUsdValue(state.usdMarketCap))
}
},
'[data-selector="tx_per_day"]': {
render ($el, state, oldState) {
if (!(JSON.stringify(oldState.transactionStats) === JSON.stringify(state.transactionStats))) {
$el.empty().append(numeral(state.transactionStats[0].number_of_transactions).format('0,0'))
}
}
},
'[data-selector="chain-block-list"]': {
load ($el) {
return {
blocksPath: $el[0].dataset.url
}
},
render ($el, state, oldState) {
if (oldState.blocks === state.blocks) return
const container = $el[0]
if (state.blocksLoading === false) {
const blocks = map(state.blocks, ({ chainBlockHtml }) => $(chainBlockHtml)[0])
listMorph(container, blocks, { key: 'dataset.blockNumber', horizontal: true })
}
}
},
'[data-selector="chain-block-list"] [data-selector="error-message"]': {
render ($el, state, _oldState) {
if (state.blocksError) {
$el.show()
} else {
$el.hide()
}
}
},
'[data-selector="chain-block-list"] [data-selector="loading-message"]': {
render ($el, state, _oldState) {
showLoader(state.blocksLoading, $el)
}
},
'[data-selector="transactions-list"] [data-selector="error-message"]': {
render ($el, state, _oldState) {
$el.toggle(state.transactionsError)
}
},
'[data-selector="transactions-list"] [data-selector="loading-message"]': {
render ($el, state, _oldState) {
showLoader(state.transactionsLoading, $el)
}
},
'[data-selector="transactions-list"]': {
load ($el) {
return { transactionsPath: $el[0].dataset.transactionsPath }
},
render ($el, state, oldState) {
if (oldState.transactions === state.transactions) return
const container = $el[0]
const newElements = map(state.transactions, ({ transactionHtml }) => $(transactionHtml)[0])
listMorph(container, newElements, { key: 'dataset.identifierHash', horizontal: null })
}
},
'[data-selector="channel-batching-count"]': {
render ($el, state, _oldState) {
const $channelBatching = $('[data-selector="channel-batching-message"]')
if (!state.transactionsBatch.length) return $channelBatching.hide()
$channelBatching.show()
$el[0].innerHTML = numeral(state.transactionsBatch.length).format()
}
}
}
const $chainDetailsPage = $('[data-page="chain-details"]')
if ($chainDetailsPage.length) {
const store = createStore(reducer)
connectElements({ store, elements })
loadTransactions(store)
bindTransactionErrorMessage(store)
loadBlocks(store)
bindBlockErrorMessage(store)
const exchangeRateChannel = socket.channel('exchange_rate:new_rate')
exchangeRateChannel.join()
exchangeRateChannel.on('new_rate', (msg) => {
updateAllCalculatedUsdValues(humps.camelizeKeys(msg).exchangeRate.usdValue)
store.dispatch({
type: 'RECEIVED_NEW_EXCHANGE_RATE',
msg: humps.camelizeKeys(msg)
})
})
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')
blocksChannel.join()
blocksChannel.on('new_block', msg => store.dispatch({
type: 'RECEIVED_NEW_BLOCK',
msg: humps.camelizeKeys(msg)
}))
const transactionsChannel = socket.channel('transactions:new_transaction')
transactionsChannel.join()
transactionsChannel.on('transaction', batchChannel((msgs) => store.dispatch({
type: 'RECEIVED_NEW_TRANSACTION_BATCH',
msgs: humps.camelizeKeys(msgs)
})))
const transactionStatsChannel = socket.channel('transactions:stats')
transactionStatsChannel.join()
transactionStatsChannel.on('update', msg => store.dispatch({
type: 'RECEIVED_UPDATED_TRANSACTION_STATS',
msg
}))
const $txReloadButton = $('[data-selector="reload-transactions-button"]')
const $channelBatching = $('[data-selector="channel-batching-message"]')
$txReloadButton.on('click', (event) => {
event.preventDefault()
loadTransactions(store)
$channelBatching.hide()
store.dispatch({
type: 'TRANSACTION_BATCH_EXPANDED'
})
})
}
function loadTransactions (store) {
const path = store.getState().transactionsPath
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' }))
}
function bindTransactionErrorMessage (store) {
$('[data-selector="transactions-list"] [data-selector="error-message"]').on('click', _event => loadTransactions(store))
}
export function placeHolderBlock (blockNumber) {
return `
<div
class="col-lg-3 d-flex fade-up-blocks-chain"
data-block-number="${blockNumber}"
data-selector="place-holder"
>
<div
class="tile tile-type-block d-flex align-items-center fade-up"
>
<span class="loading-spinner-small ml-1 mr-4">
<span class="loading-spinner-block-1"></span>
<span class="loading-spinner-block-2"></span>
</span>
<div>
<span class="tile-title pr-0 pl-0">${blockNumber}</span>
<div class="tile-transactions">${
// @ts-ignore
window.localized['Block Processing']
}</div>
</div>
</div>
</div>
`
}
function loadBlocks (store) {
const url = store.getState().blocksPath
store.dispatch({ type: 'START_BLOCKS_FETCH' })
$.getJSON(url)
.done(response => {
store.dispatch({ type: 'BLOCKS_FETCHED', msg: humps.camelizeKeys(response) })
})
.fail(() => store.dispatch({ type: 'BLOCKS_REQUEST_ERROR' }))
.always(() => store.dispatch({ type: 'BLOCKS_FINISH_REQUEST' }))
}
function bindBlockErrorMessage (store) {
$('[data-selector="chain-block-list"] [data-selector="error-message"]').on('click', _event => loadBlocks(store))
}
$('a.ajax').on('click', (event) => {
event.preventDefault()
event.currentTarget.classList.add('disabled')
$.get($(event.currentTarget).attr('href'), () => {
openSuccessModal('Success', 'Email successfully resent', () => { window.location.reload() })
}).fail((error) => {
if (error.responseJSON && error.responseJSON.message) {
if (error.status === 429) {
openWarningModal('Warning', error.responseJSON.message)
} else {
openErrorModal('Error', error.responseJSON.message, false)
}
} else {
openErrorModal('Error', 'Email resend failed', false)
}
})
.always(() => {
event.currentTarget.classList.remove('disabled')
})
}
)