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' 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 `