From 20317a5154b0adaa6afa86363e9c56427a33db71 Mon Sep 17 00:00:00 2001 From: slightlycyborg Date: Mon, 22 Jul 2019 14:51:03 +0000 Subject: [PATCH 1/4] Transaction.History.Historian broadcasts transaction_stats update event --- apps/explorer/lib/explorer/chain/events/subscriber.ex | 2 +- .../lib/explorer/chain/transaction/history/historian.ex | 4 +++- .../explorer/chain/transaction/history/historian_test.exs | 5 +++++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/apps/explorer/lib/explorer/chain/events/subscriber.ex b/apps/explorer/lib/explorer/chain/events/subscriber.ex index 6a9df99438..32e8a9e605 100644 --- a/apps/explorer/lib/explorer/chain/events/subscriber.ex +++ b/apps/explorer/lib/explorer/chain/events/subscriber.ex @@ -7,7 +7,7 @@ defmodule Explorer.Chain.Events.Subscriber do @allowed_broadcast_types ~w(catchup realtime on_demand)a - @allowed_events ~w(exchange_rate)a + @allowed_events ~w(exchange_rate transaction_stats)a @type broadcast_type :: :realtime | :catchup | :on_demand diff --git a/apps/explorer/lib/explorer/chain/transaction/history/historian.ex b/apps/explorer/lib/explorer/chain/transaction/history/historian.ex index dbe823328d..474f6fd391 100644 --- a/apps/explorer/lib/explorer/chain/transaction/history/historian.ex +++ b/apps/explorer/lib/explorer/chain/transaction/history/historian.ex @@ -4,6 +4,7 @@ defmodule Explorer.Chain.Transaction.History.Historian do alias Explorer.Repo alias Explorer.Chain.Block alias Explorer.Chain.Transaction.History.TransactionStats + alias Explorer.Chain.Events.Publisher import Ecto.Query, only: [from: 2] @@ -37,7 +38,8 @@ defmodule Explorer.Chain.Transaction.History.Historian do @impl Historian def save_records(records) do - {num_inserted, _} = Repo.insert_all(TransactionStats, records, on_conflict: :replace_all, conflict_target: [:date]) + {num_inserted, _} = Repo.insert_all(TransactionStats, records, on_conflict: :replace_all_except_primary_key, conflict_target: [:date]) + Publisher.broadcast(:transaction_stats) num_inserted end diff --git a/apps/explorer/test/explorer/chain/transaction/history/historian_test.exs b/apps/explorer/test/explorer/chain/transaction/history/historian_test.exs index db43995c15..603d0142ca 100644 --- a/apps/explorer/test/explorer/chain/transaction/history/historian_test.exs +++ b/apps/explorer/test/explorer/chain/transaction/history/historian_test.exs @@ -68,6 +68,11 @@ defmodule Explorer.Chain.Transaction.History.HistorianTest do Historian.save_records(records) + records = List.replace_at(records, 0, Map.put(Enum.at(records, 0), :number_of_transactions, 4)) + single_record = [Enum.at(records, 0)] + + Historian.save_records(single_record) + query = from( stats in TransactionStats, select: %{date: stats.date, number_of_transactions: stats.number_of_transactions}, From 449e4cfaf681aa7d597831427277ea098b3c1159 Mon Sep 17 00:00:00 2001 From: slightlycyborg Date: Mon, 22 Jul 2019 14:54:01 +0000 Subject: [PATCH 2/4] Transaction Chart added to BlockScoutWeb --- apps/block_scout_web/assets/css/app.scss | 1 + .../css/components/_dashboard-banner.scss | 9 +- apps/block_scout_web/assets/js/app.js | 2 +- ...rket_history_chart.js => history_chart.js} | 114 ++++++++++++++---- .../js/lib/transaction_history_chart.js | 114 ++++++++++++++++++ apps/block_scout_web/assets/js/pages/chain.js | 24 +++- apps/block_scout_web/config/config.exs | 8 ++ .../channels/transaction_channel.ex | 4 + .../transaction_history_chart_controller.ex | 38 ++++++ .../controllers/chain_controller.ex | 25 +++- .../lib/block_scout_web/notifier.ex | 12 ++ .../block_scout_web/realtime_event_handler.ex | 1 + .../lib/block_scout_web/router.ex | 5 + .../templates/chain/show.html.eex | 74 +++++++++--- .../templates/layout/app.html.eex | 3 +- apps/block_scout_web/priv/gettext/default.pot | 9 ++ ...nsaction_history_chart_controller_test.exs | 48 ++++++++ 17 files changed, 444 insertions(+), 47 deletions(-) rename apps/block_scout_web/assets/js/lib/{market_history_chart.js => history_chart.js} (54%) create mode 100644 apps/block_scout_web/assets/js/lib/transaction_history_chart.js create mode 100644 apps/block_scout_web/lib/block_scout_web/controllers/chain/transaction_history_chart_controller.ex create mode 100644 apps/block_scout_web/test/block_scout_web/controllers/chain/transaction_history_chart_controller_test.exs diff --git a/apps/block_scout_web/assets/css/app.scss b/apps/block_scout_web/assets/css/app.scss index cf79ee46e8..394a6a5b1f 100644 --- a/apps/block_scout_web/assets/css/app.scss +++ b/apps/block_scout_web/assets/css/app.scss @@ -133,6 +133,7 @@ $fa-font-path: "~@fortawesome/fontawesome-free/webfonts"; dashboardBannerChartAxisFontColor: $dashboard-banner-chart-axis-font-color; dashboardLineColorMarket: $dashboard-line-color-market; dashboardLineColorPrice: $dashboard-line-color-price; + dashboardLineColorTransactions: $dashboard-line-color-transactions; primary: $primary; secondary: $secondary; } diff --git a/apps/block_scout_web/assets/css/components/_dashboard-banner.scss b/apps/block_scout_web/assets/css/components/_dashboard-banner.scss index 33d104a9fd..11c43d5583 100644 --- a/apps/block_scout_web/assets/css/components/_dashboard-banner.scss +++ b/apps/block_scout_web/assets/css/components/_dashboard-banner.scss @@ -6,6 +6,7 @@ $dashboard-banner-gradient-end: lighten( $dashboard-banner-network-plain-container-background-color: lighten($dashboard-banner-gradient-end, 5%)!default; $dashboard-line-color-price: lighten($dashboard-banner-gradient-end, 5%) !default; $dashboard-line-color-market: $secondary !default; +$dashboard-line-color-transactions: $warning !default; $dashboard-stats-item-label-color: #fff !default; $dashboard-stats-item-value-color: rgba(#fff, 0.8) !default; $dashboard-banner-chart-legend-label-color: #fff !default; @@ -66,7 +67,7 @@ $dashboard-banner-chart-axis-font-color: $dashboard-stats-item-value-color !defa .dashboard-banner-chart-legend { display: grid; - grid-template-columns: 1fr 1fr; + grid-template-columns: repeat(var(--numChartData), 0.9fr); padding-bottom: 12px; .dashboard-banner-chart-legend-item { @@ -103,6 +104,10 @@ $dashboard-banner-chart-axis-font-color: $dashboard-stats-item-value-color !defa &:nth-child(2)::before { background-color: $dashboard-line-color-market; } + + &:nth-child(3)::before { + background-color: $dashboard-line-color-transactions; + } } .dashboard-banner-chart-legend-label { @@ -219,4 +224,4 @@ $dashboard-banner-chart-axis-font-color: $dashboard-stats-item-value-color !defa font-size: 0.9rem; } } -} \ No newline at end of file +} diff --git a/apps/block_scout_web/assets/js/app.js b/apps/block_scout_web/assets/js/app.js index cd690f31db..aa01e24b10 100644 --- a/apps/block_scout_web/assets/js/app.js +++ b/apps/block_scout_web/assets/js/app.js @@ -42,7 +42,7 @@ import './lib/currency' import './lib/from_now' import './lib/indexing' import './lib/loading_element' -import './lib/market_history_chart' +import './lib/history_chart' import './lib/pending_transactions_toggle' import './lib/pretty_json' import './lib/reload_button' diff --git a/apps/block_scout_web/assets/js/lib/market_history_chart.js b/apps/block_scout_web/assets/js/lib/history_chart.js similarity index 54% rename from apps/block_scout_web/assets/js/lib/market_history_chart.js rename to apps/block_scout_web/assets/js/lib/history_chart.js index 615f888d8a..59397843eb 100644 --- a/apps/block_scout_web/assets/js/lib/market_history_chart.js +++ b/apps/block_scout_web/assets/js/lib/history_chart.js @@ -44,7 +44,7 @@ const config = { } }, { id: 'marketCap', - position: 'right', + display: false, gridLines: { display: false, drawBorder: false @@ -54,6 +54,20 @@ const config = { maxTicksLimit: 6, drawOnChartArea: false } + }, { + id: 'numTransactions', + display: false, + position: 'right', + gridLines: { + display: false, + drawBorder: false + }, + ticks: { + beginAtZero: true, + callback: (value, index, values) => `${numeral(value).format('0,0')}`, + maxTicksLimit: 4, + fontColor: sassVariables.dashboardBannerChartAxisFontColor + } }] }, tooltips: { @@ -66,6 +80,8 @@ const config = { return `${label}: ${formatUsdValue(yLabel)}` } else if (datasets[datasetIndex].yAxisID === 'marketCap') { return `${label}: ${formatUsdValue(yLabel)}` + } else if (datasets[datasetIndex].yAxisID === 'numTransactions') { + return `${label}: ${yLabel}` } else { return yLabel } @@ -88,32 +104,65 @@ function getMarketCapData (marketHistoryData, availableSupply) { } class MarketHistoryChart { - constructor (el, availableSupply, marketHistoryData) { + constructor (el, availableSupply, marketHistoryData, dataConfig) { + + var axes = config.options.scales.yAxes.reduce(function(solution, elem){ + solution[elem.id] = elem + return solution + }, + {}) + this.price = { label: window.localized['Price'], yAxisID: 'price', - data: getPriceData(marketHistoryData), + data: [], fill: false, pointRadius: 0, backgroundColor: sassVariables.dashboardLineColorPrice, borderColor: sassVariables.dashboardLineColorPrice, lineTension: 0 } + if (dataConfig.market == undefined || dataConfig.market.indexOf("price") == -1){ + this.price.hidden = true + axes["price"].display = false + } + this.marketCap = { label: window.localized['Market Cap'], yAxisID: 'marketCap', - data: getMarketCapData(marketHistoryData, availableSupply), + data: [], fill: false, pointRadius: 0, backgroundColor: sassVariables.dashboardLineColorMarket, borderColor: sassVariables.dashboardLineColorMarket, lineTension: 0 } + if (dataConfig.market == undefined || dataConfig.market.indexOf("market_cap") == -1){ + this.marketCap.hidden = true + axes["marketCap"].display = false + } + + this.numTransactions = { + label: window.localized['Tx/day'], + yAxisID: 'numTransactions', + data: [], + fill: false, + pointRadius: 0, + backgroundColor: sassVariables.dashboardLineColorMarket, + borderColor: sassVariables.dashboardLineColorTransactions, + lineTension: 0, + } + if (dataConfig.transactions == undefined || dataConfig.transactions.indexOf("transactions_per_day") == -1){ + this.numTransactions.hidden = true + axes["numTransactions"].display = false + } + this.availableSupply = availableSupply - config.data.datasets = [this.price, this.marketCap] + //TODO: This is where we dynamically append datasets + config.data.datasets = [this.price, this.marketCap, this.numTransactions] this.chart = new Chart(el, config) } - update (availableSupply, marketHistoryData) { + updateMarketHistory (availableSupply, marketHistoryData) { this.price.data = getPriceData(marketHistoryData) if (this.availableSupply !== null && typeof this.availableSupply === 'object') { const today = new Date().toJSON().slice(0, 10) @@ -124,31 +173,50 @@ class MarketHistoryChart { } this.chart.update() } + updateTransactionHistory (transaction_history) { + this.numTransactions.data = transaction_history.map(dataPoint => { + return {x:dataPoint.date, y:dataPoint.number_of_transactions} + }) + this.chart.update() + } } export function createMarketHistoryChart (el) { - const dataPath = el.dataset.market_history_chart_path + const dataPaths = $(el).data('history_chart_paths') + const dataConfig = $(el).data('history_chart_config') + const $chartLoading = $('[data-chart-loading-message]') const $chartError = $('[data-chart-error-message]') - const chart = new MarketHistoryChart(el, 0, []) - $.getJSON(dataPath, {type: 'JSON'}) - .done(data => { - const availableSupply = JSON.parse(data.supply_data) - const marketHistoryData = humps.camelizeKeys(JSON.parse(data.history_data)) - $(el).show() - chart.update(availableSupply, marketHistoryData) - }) - .fail(() => { - $chartError.show() - }) - .always(() => { - $chartLoading.hide() - }) - return chart + const chart = new MarketHistoryChart(el, 0, [], dataConfig) + Object.keys(dataPaths).forEach(function(history_source){ + $.getJSON(dataPaths[history_source], {type: 'JSON'}) + .done(data => { + switch(history_source){ + case "market": + const availableSupply = JSON.parse(data.supply_data) + const marketHistoryData = humps.camelizeKeys(JSON.parse(data.history_data)) + $(el).show() + chart.updateMarketHistory(availableSupply, marketHistoryData) + break; + case "transaction": + const transaction_history = JSON.parse(data.history_data) + + $(el).show() + chart.updateTransactionHistory(transaction_history) + break; + } + }) + .fail(() => { + $chartError.show() + }) + .always(() => { + $chartLoading.hide() + })}) + return chart; } $('[data-chart-error-message]').on('click', _event => { $('[data-chart-loading-message]').show() $('[data-chart-error-message]').hide() - createMarketHistoryChart($('[data-chart="marketHistoryChart"]')[0]) + createMarketHistoryChart($('[data-chart="historyChart"]')[0]) }) diff --git a/apps/block_scout_web/assets/js/lib/transaction_history_chart.js b/apps/block_scout_web/assets/js/lib/transaction_history_chart.js new file mode 100644 index 0000000000..1c0d7fb668 --- /dev/null +++ b/apps/block_scout_web/assets/js/lib/transaction_history_chart.js @@ -0,0 +1,114 @@ +import $ from 'jquery' +import Chart from 'chart.js' +import humps from 'humps' +import numeral from 'numeral' +import { formatUsdValue } from '../lib/currency' +import sassVariables from '../../css/app.scss' + +const config = { + type: 'line', + responsive: true, + data: { + datasets: [] + }, + options: { + legend: { + display: false + }, + scales: { + xAxes: [{ + gridLines: { + display: false, + drawBorder: false + }, + type: 'time', + time: { + unit: 'day', + stepSize: 14 + }, + ticks: { + fontColor: sassVariables.dashboardBannerChartAxisFontColor + } + }], + yAxes: [{ + id: 'num_transactions', + gridLines: { + display: false, + drawBorder: false + }, + ticks: { + beginAtZero: true, + callback: (value, index, values) => `$${numeral(value).format('0,0.00')}`, + maxTicksLimit: 4, + fontColor: sassVariables.dashboardBannerChartAxisFontColor + } + }] + }, + tooltips: { + mode: 'index', + intersect: false, + callbacks: { + label: ({datasetIndex, yLabel}, {datasets}) => { + const label = datasets[datasetIndex].label + if (datasets[datasetIndex].yAxisID === 'num_transactions') { + return `${label}: ${formatUsdValue(yLabel)}` + } else { + return yLabel + } + } + } + } + } +} + +function transformData (marketHistoryData) { + return marketHistoryData.map( + ({ date, num_transactions }) => ({x: date, y: num_transactions})) +} + +class TransactionHistoryChart { + constructor (el, transactionHistoryData) { + this.num_transactions = { + label: window.localized['Price'], + yAxisID: 'num_transactions', + data: transformData(transactionHistoryData), + fill: false, + pointRadius: 0, + backgroundColor: sassVariables.dashboardLineColorPrice, + borderColor: sassVariables.dashboardLineColorPrice, + lineTension: 0 + } + config.data.datasets = [this.num_transactions] + this.chart = new Chart(el, config) + } + update (transactionHistoryData) { + this.num_transactions.data = transformData(TransactionHistoryData) + this.chart.update() + } +} + +export function createTransactionHistoryChart (el) { + const dataPath = el.dataset.transaction_history_chart_path + const $chartLoading = $('[data-chart-loading-message]') + const $chartError = $('[data-chart-error-message]') + const chart = new TransactionHistoryChart(el, 0, []) + $.getJSON(dataPath, {type: 'JSON'}) + .done(data => { + const transactionStats = JSON.parse(data.history_data) + $(el).show() + chart.update(transactionStats) + }) + .fail(() => { + $chartError.show() + }) + .always(() => { + $chartLoading.hide() + }) + return chart +} + +$('[data-chart-error-message]').on('click', _event => { + $('[data-chart-loading-message]').show() + $('[data-chart-error-message]').hide() + createTransactionHistoryChart($('[data-chart="marketHistoryChart"]')[0]) +}) diff --git a/apps/block_scout_web/assets/js/pages/chain.js b/apps/block_scout_web/assets/js/pages/chain.js index f4cd0e6628..5358ced62d 100644 --- a/apps/block_scout_web/assets/js/pages/chain.js +++ b/apps/block_scout_web/assets/js/pages/chain.js @@ -7,7 +7,7 @@ import { exchangeRateChannel, formatUsdValue } from '../lib/currency' import { createStore, connectElements } from '../lib/redux_helpers.js' import { batchChannel } from '../lib/utils' import listMorph from '../lib/list_morph' -import { createMarketHistoryChart } from '../lib/market_history_chart' +import { createMarketHistoryChart } from '../lib/history_chart' const BATCH_THRESHOLD = 6 @@ -103,6 +103,11 @@ function baseReducer (state = initialState, action) { }) } } + case 'RECIEVED_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': @@ -137,13 +142,17 @@ function withMissingBlocks (reducer) { let chart const elements = { - '[data-chart="marketHistoryChart"]': { + '[data-chart="historyChart"]': { load ($el) { chart = createMarketHistoryChart($el[0]) }, render ($el, state, oldState) { - if (!chart || (oldState.availableSupply === state.availableSupply && oldState.marketHistoryData === state.marketHistoryData) || !state.availableSupply) return - chart.update(state.availableSupply, state.marketHistoryData) + if (chart && !(oldState.availableSupply === state.availableSupply && oldState.marketHistoryData === state.marketHistoryData) && state.availableSupply) { + chart.updateMarketHistory(state.availableSupply, state.marketHistoryData) + } + if (chart && !(_.isEqual(oldState.transactionStats, state.transactionStats))){ + chart.updateTransactionHistory(state.transactionStats) + } } }, '[data-selector="transaction-count"]': { @@ -284,6 +293,13 @@ if ($chainDetailsPage.length) { type: 'RECEIVED_NEW_TRANSACTION_BATCH', msgs: humps.camelizeKeys(msgs) }))) + + const transactionStatsChannel = socket.channel('transactions:stats') + transactionStatsChannel.join() + transactionStatsChannel.on('update', msg => store.dispatch({ + type: 'RECIEVED_UPDATED_TRANSACTION_STATS', + msg: msg + })) } function loadTransactions (store) { diff --git a/apps/block_scout_web/config/config.exs b/apps/block_scout_web/config/config.exs index 28d142eb5c..88f08c73dc 100644 --- a/apps/block_scout_web/config/config.exs +++ b/apps/block_scout_web/config/config.exs @@ -61,6 +61,14 @@ config :block_scout_web, BlockScoutWeb.SocialMedia, facebook: "PoaNetwork", instagram: "PoaNetwork" +# Configures History +config :block_scout_web, +chart_config: %{market: [:price,:market_cap], transactions: [:transactions_per_day]} + +config :block_scout_web, BlockScoutWeb.Chain.TransactionHistoryChartController, + history_size: 30 #days + + config :ex_cldr, default_locale: "en", locales: ["en"], diff --git a/apps/block_scout_web/lib/block_scout_web/channels/transaction_channel.ex b/apps/block_scout_web/lib/block_scout_web/channels/transaction_channel.ex index e5ce03b86f..54c2da9fbf 100644 --- a/apps/block_scout_web/lib/block_scout_web/channels/transaction_channel.ex +++ b/apps/block_scout_web/lib/block_scout_web/channels/transaction_channel.ex @@ -18,6 +18,10 @@ defmodule BlockScoutWeb.TransactionChannel do {:ok, %{}, socket} end + def join("transactions:stats", _params, socket) do + {:ok, %{}, socket} + end + def join("transactions:" <> _transaction_hash, _params, socket) do {:ok, %{}, socket} end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/chain/transaction_history_chart_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/chain/transaction_history_chart_controller.ex new file mode 100644 index 0000000000..ca1346ba56 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/controllers/chain/transaction_history_chart_controller.ex @@ -0,0 +1,38 @@ +defmodule BlockScoutWeb.Chain.TransactionHistoryChartController do + use BlockScoutWeb, :controller + + alias Explorer.Chain.Transaction.History.TransactionStats + + def show(conn, _params) do + with true <- ajax?(conn) do + [{:history_size, history_size}] = Application.get_env(:block_scout_web, __MODULE__, 30) + + latest = Date.utc_today() + earliest = Date.add(latest, -1 * history_size) + + transaction_history_data = TransactionStats.by_date_range(earliest, latest) + |> extract_history + |> encode_transaction_history_data + + json(conn, %{ + history_data: transaction_history_data, + }) + else + _ -> unprocessable_entity(conn) + end + end + + defp extract_history(db_results) do + Enum.map(db_results, fn row -> + %{date: row.date, number_of_transactions: row.number_of_transactions} end) + end + + defp encode_transaction_history_data(transaction_history_data) do + transaction_history_data + |> Jason.encode() + |> case do + {:ok, data} -> data + _ -> [] + end + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/chain_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/chain_controller.ex index 40a0fd3bbb..303c7cb6fe 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/chain_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/chain_controller.ex @@ -4,6 +4,7 @@ defmodule BlockScoutWeb.ChainController do alias BlockScoutWeb.ChainView alias Explorer.{Chain, PagingOptions, Repo} alias Explorer.Chain.{Address, Block, Transaction} + alias Explorer.Chain.Transaction.History.TransactionStats alias Explorer.Chain.Supply.RSK alias Explorer.Counters.AverageBlockTime alias Explorer.ExchangeRates.Token @@ -14,6 +15,7 @@ defmodule BlockScoutWeb.ChainController do transaction_estimated_count = Chain.transaction_estimated_count() block_count = Chain.block_estimated_count() + market_cap_calculation = case Application.get_env(:explorer, :supply) do RSK -> @@ -25,20 +27,41 @@ defmodule BlockScoutWeb.ChainController do exchange_rate = Market.get_exchange_rate(Explorer.coin()) || Token.null() + transaction_stats = get_transaction_stats() + + chart_data_paths = %{market: market_history_chart_path(conn, :show), + transaction: transaction_history_chart_path(conn, :show)} + + chart_config = Application.get_env(:block_scout_web, :chart_config, %{}) + render( conn, "show.html", address_count: Chain.count_addresses_with_balance_from_cache(), average_block_time: AverageBlockTime.average_block_time(), exchange_rate: exchange_rate, - chart_data_path: market_history_chart_path(conn, :show), + chart_config: chart_config, + chart_config_json: Jason.encode!(chart_config), + chart_data_paths: chart_data_paths, market_cap_calculation: market_cap_calculation, transaction_estimated_count: transaction_estimated_count, transactions_path: recent_transactions_path(conn, :index), + transaction_stats: transaction_stats, block_count: block_count ) end + def get_transaction_stats() do + stats_scale = date_range(1) + TransactionStats.by_date_range(stats_scale.earliest, stats_scale.latest) + end + + def date_range(num_days) do + today = Date.utc_today() + x_days_back = Date.add(today, -1*(num_days-1)) + %{earliest: x_days_back, latest: today} + end + def search(conn, %{"q" => query}) do query |> String.trim() diff --git a/apps/block_scout_web/lib/block_scout_web/notifier.ex b/apps/block_scout_web/lib/block_scout_web/notifier.ex index a5bc99e242..0139a9020a 100644 --- a/apps/block_scout_web/lib/block_scout_web/notifier.ex +++ b/apps/block_scout_web/lib/block_scout_web/notifier.ex @@ -7,6 +7,7 @@ defmodule BlockScoutWeb.Notifier do alias BlockScoutWeb.Endpoint alias Explorer.{Chain, Market, Repo} alias Explorer.Chain.{Address, InternalTransaction, Transaction} + alias Explorer.Chain.Transaction.History.TransactionStats alias Explorer.Counters.AverageBlockTime alias Explorer.ExchangeRates.Token @@ -85,6 +86,17 @@ defmodule BlockScoutWeb.Notifier do |> Enum.each(&broadcast_transaction/1) end + def handle_event({:chain_event, :transaction_stats}) do + today = Date.utc_today() + [{:history_size, history_size}] = Application.get_env(:block_scout_web, BlockScoutWeb.Chain.TransactionHistoryChartController, 30) + + x_days_back = Date.add(today, -1 * history_size) + stats = TransactionStats.by_date_range(x_days_back, today) + |> Enum.map(fn item -> Map.drop(item, [:__meta__]) end) + + Endpoint.broadcast("transactions:stats", "update", %{stats: stats}) + end + def handle_event(_), do: nil @doc """ diff --git a/apps/block_scout_web/lib/block_scout_web/realtime_event_handler.ex b/apps/block_scout_web/lib/block_scout_web/realtime_event_handler.ex index f0f73f402c..508ba310c1 100644 --- a/apps/block_scout_web/lib/block_scout_web/realtime_event_handler.ex +++ b/apps/block_scout_web/lib/block_scout_web/realtime_event_handler.ex @@ -25,6 +25,7 @@ defmodule BlockScoutWeb.RealtimeEventHandler do Subscriber.to(:address_coin_balances, :on_demand) # Does not come from the indexer Subscriber.to(:exchange_rate) + Subscriber.to(:transaction_stats) {:ok, []} end diff --git a/apps/block_scout_web/lib/block_scout_web/router.ex b/apps/block_scout_web/lib/block_scout_web/router.ex index d13dc9c43e..2ec112cda7 100644 --- a/apps/block_scout_web/lib/block_scout_web/router.ex +++ b/apps/block_scout_web/lib/block_scout_web/router.ex @@ -81,6 +81,11 @@ defmodule BlockScoutWeb.Router do singleton: true ) + resources("/transaction_history_chart", Chain.TransactionHistoryChartController, + only: [:show], + singleton: true + ) + resources "/blocks", BlockController, only: [:index, :show], param: "hash_or_number" do resources("/transactions", BlockTransactionController, only: [:index], as: :transaction) end diff --git a/apps/block_scout_web/lib/block_scout_web/templates/chain/show.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/chain/show.html.eex index 0c29001697..d5f0fcab0f 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/chain/show.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/chain/show.html.eex @@ -15,24 +15,68 @@ - + + "<%= key %>":"<%= value %>" + <%= if x<(map_size(@chart_data_paths)-1) do %> + , + <% end %> + <% end %>}' + + data-history_chart_config = '<%= @chart_config_json %>' + + width="350" height="152" style="display: none;"> +
-
- - <%= gettext "Price" %> - - - -
-
- - <%= gettext "Market Cap" %> - - - -
+ <%= if Map.has_key?(@chart_config, :market) do %> + + <%# THE FOLLOWING LINE PREVENTS COPY/PASTE ERRORS %> + <%# Explicity put @chart_config.market in a variable %> + <%# This is done so that when people add a new chart source, x, %> + <%# They wont just access @chart_config.x w/o first checking if x exists%> + <% market_chart_config = @chart_config.market%> + + <%= if Enum.member?(market_chart_config, :price) do %> +
+ + <%= gettext "Price" %> + + + +
+ <% end %> + <%= if Enum.member?(@chart_config.market, :market_cap) do %> +
+ + <%= gettext "Market Cap" %> + + + +
+ <% end %> + <% end %> + <%= if Map.has_key?(@chart_config, :transactions) do %> + + <% transaction_chart_config = @chart_config.transactions%> + <%= if Enum.member?(transaction_chart_config, :transactions_per_day) do %> +
+ + <%= gettext "Tx/day" %> + + + <%= Enum.at(@transaction_stats, 0).number_of_transactions %> + +
+ <% end %> + <% end %>
diff --git a/apps/block_scout_web/lib/block_scout_web/templates/layout/app.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/layout/app.html.eex index b3b6ba0665..c4cdfcb788 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/layout/app.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/layout/app.html.eex @@ -52,7 +52,8 @@ 'Less than': '<%= gettext("Less than") %>', 'Market Cap': '<%= gettext("Market Cap") %>', 'Price': '<%= gettext("Price") %>', - 'Ether': '<%= gettext("Ether") %>' + 'Ether': '<%= gettext("Ether") %>', + 'Tx/day': '<%= gettext("Tx/day") %>' } diff --git a/apps/block_scout_web/priv/gettext/default.pot b/apps/block_scout_web/priv/gettext/default.pot index 41aa7bf553..96ed671590 100644 --- a/apps/block_scout_web/priv/gettext/default.pot +++ b/apps/block_scout_web/priv/gettext/default.pot @@ -695,6 +695,15 @@ msgstr "" msgid "Success" msgstr "" +#, elixir-format +#: lib/block_scout_web/templates/transaction/_pending_tile.html.eex:21 +#: lib/block_scout_web/templates/transaction/_tile.html.eex:34 +#: lib/block_scout_web/templates/transaction/overview.html.eex:84 +msgid "Tx/day" +msgstr "" + + + #, elixir-format #: lib/block_scout_web/templates/transaction/_pending_tile.html.eex:21 #: lib/block_scout_web/templates/transaction/_tile.html.eex:34 diff --git a/apps/block_scout_web/test/block_scout_web/controllers/chain/transaction_history_chart_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/chain/transaction_history_chart_controller_test.exs new file mode 100644 index 0000000000..618cb469c8 --- /dev/null +++ b/apps/block_scout_web/test/block_scout_web/controllers/chain/transaction_history_chart_controller_test.exs @@ -0,0 +1,48 @@ +defmodule BlockScoutWeb.Chain.TransactionHistoryChartControllerTest do + use BlockScoutWeb.ConnCase + + alias BlockScoutWeb.Chain.TransactionHistoryChartController + alias Explorer.Chain.Transaction.History.TransactionStats + alias Explorer.Repo + + describe "GET show/2" do + test "returns error when not an ajax request" do + path = transaction_history_chart_path(BlockScoutWeb.Endpoint, :show) + + conn = get(build_conn(), path) + + assert conn.status == 422 + end + + test "returns ok when request is ajax" do + path = transaction_history_chart_path(BlockScoutWeb.Endpoint, :show) + + conn = + build_conn() + |> put_req_header("x-requested-with", "xmlhttprequest") + |> get(path) + + assert json_response(conn, 200) + end + + test "returns appropriate json data" do + td = Date.utc_today() + dts = [Date.utc_today(), Date.add(td, -1), Date.add(td, -2)] + some_transaction_stats = [%{date: Enum.at(dts, 0), number_of_transactions: 10}, + %{date: Enum.at(dts, 1), number_of_transactions: 20}, + %{date: Enum.at(dts, 2), number_of_transactions: 30}] + + {num, _} = Repo.insert_all(TransactionStats, some_transaction_stats) + assert num == 3 + + conn = + build_conn() + |> put_req_header("x-requested-with", "xmlhttprequest") + |> TransactionHistoryChartController.show([]) + + expected = "{\"history_data\":\"[{\\\"date\\\":\\\"2019-07-12\\\",\\\"number_of_transactions\\\":10},{\\\"date\\\":\\\"2019-07-11\\\",\\\"number_of_transactions\\\":20},{\\\"date\\\":\\\"2019-07-10\\\",\\\"number_of_transactions\\\":30}]\"}" + + assert conn.resp_body =~ expected + end + end +end From e9ac019d70c44f2c698651bf066079e4edce14ab Mon Sep 17 00:00:00 2001 From: slightlycyborg Date: Thu, 25 Jul 2019 10:31:02 +0000 Subject: [PATCH 3/4] Fixed formatting --- .../assets/js/lib/history_chart.js | 59 +++++---- .../js/lib/transaction_history_chart.js | 114 ------------------ apps/block_scout_web/assets/js/pages/chain.js | 7 +- .../transaction_history_chart_controller.ex | 11 +- .../controllers/chain_controller.ex | 13 +- .../lib/block_scout_web/notifier.ex | 9 +- .../chain/transaction/history/historian.ex | 39 +++--- .../transaction/history/transaction_stats.ex | 9 +- .../lib/explorer/history/historian.ex | 8 +- apps/explorer/lib/explorer/history/process.ex | 10 +- .../lib/explorer/market/history/historian.ex | 14 ++- .../transaction/history/historian_test.exs | 32 +++-- .../history/transaction_stats_test.exs | 10 +- .../test/explorer/history/process_test.exs | 8 +- 14 files changed, 124 insertions(+), 219 deletions(-) delete mode 100644 apps/block_scout_web/assets/js/lib/transaction_history_chart.js diff --git a/apps/block_scout_web/assets/js/lib/history_chart.js b/apps/block_scout_web/assets/js/lib/history_chart.js index 59397843eb..f1463cf096 100644 --- a/apps/block_scout_web/assets/js/lib/history_chart.js +++ b/apps/block_scout_web/assets/js/lib/history_chart.js @@ -105,12 +105,11 @@ function getMarketCapData (marketHistoryData, availableSupply) { class MarketHistoryChart { constructor (el, availableSupply, marketHistoryData, dataConfig) { - - var axes = config.options.scales.yAxes.reduce(function(solution, elem){ + var axes = config.options.scales.yAxes.reduce(function (solution, elem) { solution[elem.id] = elem return solution }, - {}) + {}) this.price = { label: window.localized['Price'], @@ -122,9 +121,9 @@ class MarketHistoryChart { borderColor: sassVariables.dashboardLineColorPrice, lineTension: 0 } - if (dataConfig.market == undefined || dataConfig.market.indexOf("price") == -1){ + if (dataConfig.market === undefined || dataConfig.market.indexOf('price') === -1) { this.price.hidden = true - axes["price"].display = false + axes['price'].display = false } this.marketCap = { @@ -137,9 +136,9 @@ class MarketHistoryChart { borderColor: sassVariables.dashboardLineColorMarket, lineTension: 0 } - if (dataConfig.market == undefined || dataConfig.market.indexOf("market_cap") == -1){ + if (dataConfig.market === undefined || dataConfig.market.indexOf('market_cap') === -1) { this.marketCap.hidden = true - axes["marketCap"].display = false + axes['marketCap'].display = false } this.numTransactions = { @@ -150,15 +149,14 @@ class MarketHistoryChart { pointRadius: 0, backgroundColor: sassVariables.dashboardLineColorMarket, borderColor: sassVariables.dashboardLineColorTransactions, - lineTension: 0, + lineTension: 0 } - if (dataConfig.transactions == undefined || dataConfig.transactions.indexOf("transactions_per_day") == -1){ + if (dataConfig.transactions === undefined || dataConfig.transactions.indexOf('transactions_per_day') === -1) { this.numTransactions.hidden = true - axes["numTransactions"].display = false + axes['numTransactions'].display = false } this.availableSupply = availableSupply - //TODO: This is where we dynamically append datasets config.data.datasets = [this.price, this.marketCap, this.numTransactions] this.chart = new Chart(el, config) } @@ -173,9 +171,9 @@ class MarketHistoryChart { } this.chart.update() } - updateTransactionHistory (transaction_history) { - this.numTransactions.data = transaction_history.map(dataPoint => { - return {x:dataPoint.date, y:dataPoint.number_of_transactions} + updateTransactionHistory (transactionHistory) { + this.numTransactions.data = transactionHistory.map(dataPoint => { + return {x: dataPoint.date, y: dataPoint.number_of_transactions} }) this.chart.update() } @@ -188,22 +186,22 @@ export function createMarketHistoryChart (el) { const $chartLoading = $('[data-chart-loading-message]') const $chartError = $('[data-chart-error-message]') const chart = new MarketHistoryChart(el, 0, [], dataConfig) - Object.keys(dataPaths).forEach(function(history_source){ - $.getJSON(dataPaths[history_source], {type: 'JSON'}) + Object.keys(dataPaths).forEach(function (historySource) { + $.getJSON(dataPaths[historySource], {type: 'JSON'}) .done(data => { - switch(history_source){ - case "market": - const availableSupply = JSON.parse(data.supply_data) - const marketHistoryData = humps.camelizeKeys(JSON.parse(data.history_data)) - $(el).show() - chart.updateMarketHistory(availableSupply, marketHistoryData) - break; - case "transaction": - const transaction_history = JSON.parse(data.history_data) + switch (historySource) { + case 'market': + const availableSupply = JSON.parse(data.supply_data) + const marketHistoryData = humps.camelizeKeys(JSON.parse(data.history_data)) + $(el).show() + chart.updateMarketHistory(availableSupply, marketHistoryData) + break + case 'transaction': + const transactionHistory = JSON.parse(data.history_data) - $(el).show() - chart.updateTransactionHistory(transaction_history) - break; + $(el).show() + chart.updateTransactionHistory(transactionHistory) + break } }) .fail(() => { @@ -211,8 +209,9 @@ export function createMarketHistoryChart (el) { }) .always(() => { $chartLoading.hide() - })}) - return chart; + }) + }) + return chart } $('[data-chart-error-message]').on('click', _event => { diff --git a/apps/block_scout_web/assets/js/lib/transaction_history_chart.js b/apps/block_scout_web/assets/js/lib/transaction_history_chart.js deleted file mode 100644 index 1c0d7fb668..0000000000 --- a/apps/block_scout_web/assets/js/lib/transaction_history_chart.js +++ /dev/null @@ -1,114 +0,0 @@ -import $ from 'jquery' -import Chart from 'chart.js' -import humps from 'humps' -import numeral from 'numeral' -import { formatUsdValue } from '../lib/currency' -import sassVariables from '../../css/app.scss' - -const config = { - type: 'line', - responsive: true, - data: { - datasets: [] - }, - options: { - legend: { - display: false - }, - scales: { - xAxes: [{ - gridLines: { - display: false, - drawBorder: false - }, - type: 'time', - time: { - unit: 'day', - stepSize: 14 - }, - ticks: { - fontColor: sassVariables.dashboardBannerChartAxisFontColor - } - }], - yAxes: [{ - id: 'num_transactions', - gridLines: { - display: false, - drawBorder: false - }, - ticks: { - beginAtZero: true, - callback: (value, index, values) => `$${numeral(value).format('0,0.00')}`, - maxTicksLimit: 4, - fontColor: sassVariables.dashboardBannerChartAxisFontColor - } - }] - }, - tooltips: { - mode: 'index', - intersect: false, - callbacks: { - label: ({datasetIndex, yLabel}, {datasets}) => { - const label = datasets[datasetIndex].label - if (datasets[datasetIndex].yAxisID === 'num_transactions') { - return `${label}: ${formatUsdValue(yLabel)}` - } else { - return yLabel - } - } - } - } - } -} - -function transformData (marketHistoryData) { - return marketHistoryData.map( - ({ date, num_transactions }) => ({x: date, y: num_transactions})) -} - -class TransactionHistoryChart { - constructor (el, transactionHistoryData) { - this.num_transactions = { - label: window.localized['Price'], - yAxisID: 'num_transactions', - data: transformData(transactionHistoryData), - fill: false, - pointRadius: 0, - backgroundColor: sassVariables.dashboardLineColorPrice, - borderColor: sassVariables.dashboardLineColorPrice, - lineTension: 0 - } - config.data.datasets = [this.num_transactions] - this.chart = new Chart(el, config) - } - update (transactionHistoryData) { - this.num_transactions.data = transformData(TransactionHistoryData) - this.chart.update() - } -} - -export function createTransactionHistoryChart (el) { - const dataPath = el.dataset.transaction_history_chart_path - const $chartLoading = $('[data-chart-loading-message]') - const $chartError = $('[data-chart-error-message]') - const chart = new TransactionHistoryChart(el, 0, []) - $.getJSON(dataPath, {type: 'JSON'}) - .done(data => { - const transactionStats = JSON.parse(data.history_data) - $(el).show() - chart.update(transactionStats) - }) - .fail(() => { - $chartError.show() - }) - .always(() => { - $chartLoading.hide() - }) - return chart -} - -$('[data-chart-error-message]').on('click', _event => { - $('[data-chart-loading-message]').show() - $('[data-chart-error-message]').hide() - createTransactionHistoryChart($('[data-chart="marketHistoryChart"]')[0]) -}) diff --git a/apps/block_scout_web/assets/js/pages/chain.js b/apps/block_scout_web/assets/js/pages/chain.js index 363803f4ba..8a71893171 100644 --- a/apps/block_scout_web/assets/js/pages/chain.js +++ b/apps/block_scout_web/assets/js/pages/chain.js @@ -1,3 +1,4 @@ +/* global _ */ import $ from 'jquery' import omit from 'lodash/omit' import first from 'lodash/first' @@ -107,9 +108,9 @@ function baseReducer (state = initialState, action) { }) } } - case 'RECIEVED_UPDATED_TRANSACTION_STATS':{ + case 'RECIEVED_UPDATED_TRANSACTION_STATS': { return Object.assign({}, state, { - transactionStats: action.msg.stats, + transactionStats: action.msg.stats }) } case 'START_TRANSACTIONS_FETCH': @@ -154,7 +155,7 @@ const elements = { if (chart && !(oldState.availableSupply === state.availableSupply && oldState.marketHistoryData === state.marketHistoryData) && state.availableSupply) { chart.updateMarketHistory(state.availableSupply, state.marketHistoryData) } - if (chart && !(_.isEqual(oldState.transactionStats, state.transactionStats))){ + if (chart && !(_.isEqual(oldState.transactionStats, state.transactionStats))) { chart.updateTransactionHistory(state.transactionStats) } } diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/chain/transaction_history_chart_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/chain/transaction_history_chart_controller.ex index ca1346ba56..9b1649baba 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/chain/transaction_history_chart_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/chain/transaction_history_chart_controller.ex @@ -5,17 +5,19 @@ defmodule BlockScoutWeb.Chain.TransactionHistoryChartController do def show(conn, _params) do with true <- ajax?(conn) do - [{:history_size, history_size}] = Application.get_env(:block_scout_web, __MODULE__, 30) + [{:history_size, history_size}] = Application.get_env(:block_scout_web, __MODULE__, 30) latest = Date.utc_today() earliest = Date.add(latest, -1 * history_size) - transaction_history_data = TransactionStats.by_date_range(earliest, latest) + date_range = TransactionStats.by_date_range(earliest, latest) + + transaction_history_data = date_range |> extract_history |> encode_transaction_history_data json(conn, %{ - history_data: transaction_history_data, + history_data: transaction_history_data }) else _ -> unprocessable_entity(conn) @@ -24,7 +26,8 @@ defmodule BlockScoutWeb.Chain.TransactionHistoryChartController do defp extract_history(db_results) do Enum.map(db_results, fn row -> - %{date: row.date, number_of_transactions: row.number_of_transactions} end) + %{date: row.date, number_of_transactions: row.number_of_transactions} + end) end defp encode_transaction_history_data(transaction_history_data) do diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/chain_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/chain_controller.ex index 303c7cb6fe..5571334214 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/chain_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/chain_controller.ex @@ -4,8 +4,8 @@ defmodule BlockScoutWeb.ChainController do alias BlockScoutWeb.ChainView alias Explorer.{Chain, PagingOptions, Repo} alias Explorer.Chain.{Address, Block, Transaction} - alias Explorer.Chain.Transaction.History.TransactionStats alias Explorer.Chain.Supply.RSK + alias Explorer.Chain.Transaction.History.TransactionStats alias Explorer.Counters.AverageBlockTime alias Explorer.ExchangeRates.Token alias Explorer.Market @@ -15,7 +15,6 @@ defmodule BlockScoutWeb.ChainController do transaction_estimated_count = Chain.transaction_estimated_count() block_count = Chain.block_estimated_count() - market_cap_calculation = case Application.get_env(:explorer, :supply) do RSK -> @@ -29,8 +28,10 @@ defmodule BlockScoutWeb.ChainController do transaction_stats = get_transaction_stats() - chart_data_paths = %{market: market_history_chart_path(conn, :show), - transaction: transaction_history_chart_path(conn, :show)} + chart_data_paths = %{ + market: market_history_chart_path(conn, :show), + transaction: transaction_history_chart_path(conn, :show) + } chart_config = Application.get_env(:block_scout_web, :chart_config, %{}) @@ -51,14 +52,14 @@ defmodule BlockScoutWeb.ChainController do ) end - def get_transaction_stats() do + def get_transaction_stats do stats_scale = date_range(1) TransactionStats.by_date_range(stats_scale.earliest, stats_scale.latest) end def date_range(num_days) do today = Date.utc_today() - x_days_back = Date.add(today, -1*(num_days-1)) + x_days_back = Date.add(today, -1 * (num_days - 1)) %{earliest: x_days_back, latest: today} end diff --git a/apps/block_scout_web/lib/block_scout_web/notifier.ex b/apps/block_scout_web/lib/block_scout_web/notifier.ex index 40ce428144..194bf67198 100644 --- a/apps/block_scout_web/lib/block_scout_web/notifier.ex +++ b/apps/block_scout_web/lib/block_scout_web/notifier.ex @@ -122,11 +122,14 @@ defmodule BlockScoutWeb.Notifier do def handle_event({:chain_event, :transaction_stats}) do today = Date.utc_today() - [{:history_size, history_size}] = Application.get_env(:block_scout_web, BlockScoutWeb.Chain.TransactionHistoryChartController, 30) + + [{:history_size, history_size}] = + Application.get_env(:block_scout_web, BlockScoutWeb.Chain.TransactionHistoryChartController, 30) x_days_back = Date.add(today, -1 * history_size) - stats = TransactionStats.by_date_range(x_days_back, today) - |> Enum.map(fn item -> Map.drop(item, [:__meta__]) end) + + date_range = TransactionStats.by_date_range(x_days_back, today) + stats = Enum.map(date_range, fn item -> Map.drop(item, [:__meta__]) end) Endpoint.broadcast("transactions:stats", "update", %{stats: stats}) end diff --git a/apps/explorer/lib/explorer/chain/transaction/history/historian.ex b/apps/explorer/lib/explorer/chain/transaction/history/historian.ex index 474f6fd391..696ba9ff1a 100644 --- a/apps/explorer/lib/explorer/chain/transaction/history/historian.ex +++ b/apps/explorer/lib/explorer/chain/transaction/history/historian.ex @@ -1,44 +1,49 @@ defmodule Explorer.Chain.Transaction.History.Historian do + @moduledoc """ + Implements behaviour Historian which will compile TransactionStats from Block/Transaction data and then save the TransactionStats into the database for later retrevial. + """ use Explorer.History.Historian + + alias Explorer.Chain.{Block, Transaction} + alias Explorer.Chain.Events.Publisher + alias Explorer.Chain.Transaction.History.TransactionStats alias Explorer.History.Process, as: HistoryProcess alias Explorer.Repo - alias Explorer.Chain.Block - alias Explorer.Chain.Transaction.History.TransactionStats - alias Explorer.Chain.Events.Publisher import Ecto.Query, only: [from: 2] - alias Explorer.Chain.Transaction @behaviour Historian @impl Historian def compile_records(num_days, records \\ []) do - if num_days == 0 do - #base case + # base case {:ok, records} else - day_to_fetch = Date.add(date_today(), -1*(num_days-1)) + day_to_fetch = Date.add(date_today(), -1 * (num_days - 1)) earliest = datetime(day_to_fetch, ~T[00:00:00]) latest = datetime(day_to_fetch, ~T[23:59:59]) - query = from( - block in Block, - where: (block.timestamp >= ^earliest and block.timestamp <= ^latest), - join: transaction in Transaction, - on: block.hash == transaction.block_hash) + query = + from(block in Block, + where: block.timestamp >= ^earliest and block.timestamp <= ^latest, + join: transaction in Transaction, + on: block.hash == transaction.block_hash + ) - num_transactions = Repo.aggregate query, :count, :hash - records = [%{date: day_to_fetch, number_of_transactions: num_transactions} | records ] - compile_records(num_days-1, records) + num_transactions = Repo.aggregate(query, :count, :hash) + records = [%{date: day_to_fetch, number_of_transactions: num_transactions} | records] + compile_records(num_days - 1, records) end end @impl Historian def save_records(records) do - {num_inserted, _} = Repo.insert_all(TransactionStats, records, on_conflict: :replace_all_except_primary_key, conflict_target: [:date]) + {num_inserted, _} = + Repo.insert_all(TransactionStats, records, on_conflict: :replace_all_except_primary_key, conflict_target: [:date]) + Publisher.broadcast(:transaction_stats) num_inserted end @@ -49,7 +54,7 @@ defmodule Explorer.Chain.Transaction.History.Historian do DateTime.from_naive!(naive_dt, "Etc/UTC") end - defp date_today() do + defp date_today do HistoryProcess.config_or_default(:utc_today, Date.utc_today(), __MODULE__) end end diff --git a/apps/explorer/lib/explorer/chain/transaction/history/transaction_stats.ex b/apps/explorer/lib/explorer/chain/transaction/history/transaction_stats.ex index e50511646a..4077533976 100644 --- a/apps/explorer/lib/explorer/chain/transaction/history/transaction_stats.ex +++ b/apps/explorer/lib/explorer/chain/transaction/history/transaction_stats.ex @@ -9,7 +9,6 @@ defmodule Explorer.Chain.Transaction.History.TransactionStats do alias Explorer.Repo - schema "transaction_stats" do field(:date, :date) field(:number_of_transactions, :integer) @@ -28,9 +27,11 @@ defmodule Explorer.Chain.Transaction.History.TransactionStats do @spec by_date_range(Date.t(), Date.t()) :: [__MODULE__] def by_date_range(earliest, latest) do # Create a query - query = from stat in __MODULE__, - where: (stat.date >= ^earliest and stat.date<=^latest), - order_by: [desc: :date] + query = + from(stat in __MODULE__, + where: stat.date >= ^earliest and stat.date <= ^latest, + order_by: [desc: :date] + ) Repo.all(query) end diff --git a/apps/explorer/lib/explorer/history/historian.ex b/apps/explorer/lib/explorer/history/historian.ex index 953f9289aa..3fe41702c1 100644 --- a/apps/explorer/lib/explorer/history/historian.ex +++ b/apps/explorer/lib/explorer/history/historian.ex @@ -7,8 +7,8 @@ defmodule Explorer.History.Historian do Record of historical values for a specific date. """ @type record :: %{ - date: Date.t() - } + date: Date.t() + } @doc """ Compile history for a specified amount of units in the past. Units are defined by historian impl @@ -25,8 +25,8 @@ defmodule Explorer.History.Historian do alias Explorer.History.Historian def start_link(_) do - #Expansion: - #HistoryProcess.start_link(Explorer.History.Process, [:ok, __MODULE__], name: __MODULE__) + # Expansion: + # HistoryProcess.start_link(Explorer.History.Process, [:ok, __MODULE__], name: __MODULE__) GenServer.start_link(Explorer.History.Process, [:ok, __MODULE__], name: __MODULE__) end diff --git a/apps/explorer/lib/explorer/history/process.ex b/apps/explorer/lib/explorer/history/process.ex index e3594db61a..dafb8caeb8 100644 --- a/apps/explorer/lib/explorer/history/process.ex +++ b/apps/explorer/lib/explorer/history/process.ex @@ -1,5 +1,8 @@ defmodule Explorer.History.Process do - + @moduledoc """ + Creates the GenServer process used by a Historian to compile_history and to save_records. + Specifically used by Market.History.Historian and Transaction.History.Historian + """ use GenServer require Logger @@ -34,6 +37,7 @@ defmodule Explorer.History.Process do def handle_info({:DOWN, _, :process, _, _}, state) do {:noreply, state} end + # Actions @spec successful_compilation(Historian.record(), module()) :: any() @@ -42,9 +46,9 @@ defmodule Explorer.History.Process do schedule_next_compilation() end - defp schedule_next_compilation() do + defp schedule_next_compilation do delay = config_or_default(:history_fetch_interval, :timer.minutes(60)) - Process.send_after(self(), {:compile_historical_records, 1,}, delay) + Process.send_after(self(), {:compile_historical_records, 1}, delay) end @spec failed_compilation(non_neg_integer(), module(), non_neg_integer()) :: any() diff --git a/apps/explorer/lib/explorer/market/history/historian.ex b/apps/explorer/lib/explorer/market/history/historian.ex index f6d9593cf8..2c6c1d7d83 100644 --- a/apps/explorer/lib/explorer/market/history/historian.ex +++ b/apps/explorer/lib/explorer/market/history/historian.ex @@ -1,4 +1,7 @@ defmodule Explorer.Market.History.Historian do + @moduledoc """ + Implements behaviour Historian for Market History. Compiles market history from a source and then saves it into the database. + """ use Explorer.History.Historian alias Explorer.History.Process, as: HistoryProcess alias Explorer.Market @@ -7,11 +10,12 @@ defmodule Explorer.Market.History.Historian do @impl Historian def compile_records(previous_days) do - source = HistoryProcess.config_or_default( - :source, - Explorer.Market.History.Source.CryptoCompare, - Explorer.Market.History.Historian - ) + source = + HistoryProcess.config_or_default( + :source, + Explorer.Market.History.Source.CryptoCompare, + Explorer.Market.History.Historian + ) source.fetch_history(previous_days) end diff --git a/apps/explorer/test/explorer/chain/transaction/history/historian_test.exs b/apps/explorer/test/explorer/chain/transaction/history/historian_test.exs index 603d0142ca..8f9d8261ed 100644 --- a/apps/explorer/test/explorer/chain/transaction/history/historian_test.exs +++ b/apps/explorer/test/explorer/chain/transaction/history/historian_test.exs @@ -6,29 +6,25 @@ defmodule Explorer.Chain.Transaction.History.HistorianTest do import Ecto.Query, only: [from: 2] - - setup do Application.put_env(:explorer, Historian, utc_today: ~D[1970-01-04]) :ok end defp days_to_secs(days) do - 60*60*24*days + 60 * 60 * 24 * days end describe "compile_records/1" do - test "fetches transactions from blocks mined in the past num_days" do - blocks = [ - #1970-01-03 00:00:60 + # 1970-01-03 00:00:60 insert(:block, timestamp: DateTime.from_unix!(days_to_secs(2) + 60)), - #1970-01-03 04:00:00 - insert(:block, timestamp: DateTime.from_unix!(days_to_secs(2) + (4*60*60))), + # 1970-01-03 04:00:00 + insert(:block, timestamp: DateTime.from_unix!(days_to_secs(2) + 4 * 60 * 60)), - #1970-01-02 00:00:00 + # 1970-01-02 00:00:00 insert(:block, timestamp: DateTime.from_unix!(days_to_secs(1))) ] @@ -39,22 +35,23 @@ defmodule Explorer.Chain.Transaction.History.HistorianTest do expected = [ %{date: ~D[1970-01-04], number_of_transactions: 0} ] - assert {:ok, ^expected} = Historian.compile_records 1 + assert {:ok, ^expected} = Historian.compile_records(1) expected = [ %{date: ~D[1970-01-04], number_of_transactions: 0}, - %{date: ~D[1970-01-03], number_of_transactions: 2}, + %{date: ~D[1970-01-03], number_of_transactions: 2} ] - assert {:ok, ^expected} = Historian.compile_records 2 + assert {:ok, ^expected} = Historian.compile_records(2) expected = [ %{date: ~D[1970-01-04], number_of_transactions: 0}, %{date: ~D[1970-01-03], number_of_transactions: 2}, %{date: ~D[1970-01-02], number_of_transactions: 1} ] - assert {:ok, ^expected} = Historian.compile_records 3 + + assert {:ok, ^expected} = Historian.compile_records(3) end end @@ -73,10 +70,11 @@ defmodule Explorer.Chain.Transaction.History.HistorianTest do Historian.save_records(single_record) - query = from( - stats in TransactionStats, - select: %{date: stats.date, number_of_transactions: stats.number_of_transactions}, - order_by: [desc: stats.date]) + query = + from(stats in TransactionStats, + select: %{date: stats.date, number_of_transactions: stats.number_of_transactions}, + order_by: [desc: stats.date] + ) results = Repo.all(query) diff --git a/apps/explorer/test/explorer/chain/transaction/history/transaction_stats_test.exs b/apps/explorer/test/explorer/chain/transaction/history/transaction_stats_test.exs index b30143a450..72401440fd 100644 --- a/apps/explorer/test/explorer/chain/transaction/history/transaction_stats_test.exs +++ b/apps/explorer/test/explorer/chain/transaction/history/transaction_stats_test.exs @@ -5,10 +5,11 @@ defmodule Explorer.Chain.Transaction.History.TransactionStatsTest do alias Explorer.Repo test "by_date_range()" do - - some_transaction_stats = [%{date: ~D[2019-07-09], number_of_transactions: 10}, - %{date: ~D[2019-07-08], number_of_transactions: 20}, - %{date: ~D[2019-07-07], number_of_transactions: 30}] + some_transaction_stats = [ + %{date: ~D[2019-07-09], number_of_transactions: 10}, + %{date: ~D[2019-07-08], number_of_transactions: 20}, + %{date: ~D[2019-07-07], number_of_transactions: 30} + ] Repo.insert_all(TransactionStats, some_transaction_stats) @@ -22,7 +23,6 @@ defmodule Explorer.Chain.Transaction.History.TransactionStatsTest do assert ~D[2019-07-07] = Enum.at(all3, 2).date assert 30 == Enum.at(all3, 2).number_of_transactions - just2 = TransactionStats.by_date_range(~D[2019-07-08], ~D[2019-07-09]) assert 2 == length(just2) assert ~D[2019-07-08] = Enum.at(just2, 1).date diff --git a/apps/explorer/test/explorer/history/process_test.exs b/apps/explorer/test/explorer/history/process_test.exs index 4f037150ac..fca1b483f6 100644 --- a/apps/explorer/test/explorer/history/process_test.exs +++ b/apps/explorer/test/explorer/history/process_test.exs @@ -13,6 +13,7 @@ defmodule Explorer.History.ProcessTest do test "handle_info with `{:compile_historical_records, days}`" do records = [%{date: ~D[2018-04-01], closing_price: Decimal.new(10), opening_price: Decimal.new(5)}] + TestHistorian |> expect(:compile_records, fn 1 -> {:ok, records} end) |> expect(:save_records, fn _ -> :ok end) @@ -43,7 +44,7 @@ defmodule Explorer.History.ProcessTest do assert {:noreply, state} == HistoryProcess.handle_info({nil, {1, 0, {:ok, [record]}}}, state) # Message isn't sent before interval is up - refute_receive {:compile_historical_records, 1}, history_fetch_interval-1 + refute_receive {:compile_historical_records, 1}, history_fetch_interval - 1 # Now message is sent assert_receive {:compile_historical_records, 1} @@ -54,10 +55,9 @@ defmodule Explorer.History.ProcessTest do |> expect(:compile_records, fn 1 -> :error end) |> expect(:save_records, fn _ -> :ok end) - #Process will restart, so this is needed + # Process will restart, so this is needed set_mox_global() - state = %{historian: TestHistorian} # base_backoff should be short enough that it doesn't slow down testing... @@ -69,7 +69,7 @@ defmodule Explorer.History.ProcessTest do assert {:noreply, state} == HistoryProcess.handle_info({nil, {1, 0, :error}}, state) # Message isn't sent before interval is up - refute_receive {_ref, {1, 1, :error}}, base_backoff-1 + refute_receive {_ref, {1, 1, :error}}, base_backoff - 1 # Now message is sent assert_receive {_ref, {1, 1, :error}} From bb9e32e8bdab4d8b04f1883f6d1f54dd547b6659 Mon Sep 17 00:00:00 2001 From: slightlycyborg Date: Thu, 25 Jul 2019 19:50:26 +0000 Subject: [PATCH 4/4] Fixed function nil.number_of_transactions/0 is undefined in tests --- .../lib/block_scout_web/controllers/chain_controller.ex | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/chain_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/chain_controller.ex index 5571334214..12b440f713 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/chain_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/chain_controller.ex @@ -28,6 +28,14 @@ defmodule BlockScoutWeb.ChainController do transaction_stats = get_transaction_stats() + # Hack for render during testing + transaction_stats = + if length(transaction_stats) == 0 do + [%{number_of_transactions: 0}] + else + transaction_stats + end + chart_data_paths = %{ market: market_history_chart_path(conn, :show), transaction: transaction_history_chart_path(conn, :show)