diff --git a/apps/block_scout_web/assets/__tests__/pages/chain.js b/apps/block_scout_web/assets/__tests__/pages/chain.js index a27487756d..e7f69fc861 100644 --- a/apps/block_scout_web/assets/__tests__/pages/chain.js +++ b/apps/block_scout_web/assets/__tests__/pages/chain.js @@ -214,8 +214,8 @@ test('RECEIVED_NEW_EXCHANGE_RATE', () => { }) describe('RECEIVED_NEW_TRANSACTION_BATCH', () => { - test('single transaction', () => { - const state = initialState + test('single transaction with no loading or errors', () => { + const state = Object.assign(initialState, { transactionsLoading: false, transactionError: false } ) const action = { type: 'RECEIVED_NEW_TRANSACTION_BATCH', msgs: [{ @@ -228,6 +228,34 @@ describe('RECEIVED_NEW_TRANSACTION_BATCH', () => { expect(output.transactionsBatch.length).toEqual(0) expect(output.transactionCount).toEqual(1) }) + test('single transaction with error loading first transactions', () => { + const state = Object.assign({}, initialState, { transactionsError: true } ) + const action = { + type: 'RECEIVED_NEW_TRANSACTION_BATCH', + msgs: [{ + transactionHtml: 'test' + }] + } + const output = reducer(state, action) + + expect(output.transactions).toEqual([]) + expect(output.transactionsBatch.length).toEqual(0) + expect(output.transactionCount).toEqual(1) + }) + test('single transaction while loading', () => { + const state = Object.assign({}, initialState, { transactionsLoading: true } ) + const action = { + type: 'RECEIVED_NEW_TRANSACTION_BATCH', + msgs: [{ + transactionHtml: 'test' + }] + } + const output = reducer(state, action) + + expect(output.transactions).toEqual([]) + expect(output.transactionsBatch.length).toEqual(0) + expect(output.transactionCount).toEqual(1) + }) test('large batch of transactions', () => { const state = initialState const action = { diff --git a/apps/block_scout_web/assets/js/pages/chain.js b/apps/block_scout_web/assets/js/pages/chain.js index 17217f4b47..185c52021b 100644 --- a/apps/block_scout_web/assets/js/pages/chain.js +++ b/apps/block_scout_web/assets/js/pages/chain.js @@ -19,6 +19,8 @@ export const initialState = { blocks: [], transactions: [], transactionsBatch: [], + transactionsError: false, + transactionsLoading: true, transactionCount: null, usdMarketCap: null } @@ -62,6 +64,10 @@ function baseReducer (state = initialState, action) { const transactionCount = state.transactionCount + action.msgs.length + if (state.transactionsLoading || state.transactionsError) { + return Object.assign({}, state, { transactionCount }) + } + if (!state.transactionsBatch.length && action.msgs.length < BATCH_THRESHOLD) { return Object.assign({}, state, { transactions: [ @@ -80,6 +86,14 @@ function baseReducer (state = initialState, action) { }) } } + 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 } @@ -158,14 +172,19 @@ const elements = { listMorph(container, newElements, { key: 'dataset.blockNumber', horizontal: true }) } }, + '[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) { + $el.toggle(state.transactionsLoading) + } + }, '[data-selector="transactions-list"]': { load ($el) { - return { - transactions: $el.children().map((index, el) => ({ - transactionHash: el.dataset.identifierHash, - transactionHtml: el.outerHTML - })).toArray() - } + return { transactionsPath: $el[0].dataset.transactionsPath } }, render ($el, state, oldState) { if (oldState.transactions === state.transactions) return @@ -188,6 +207,8 @@ const $chainDetailsPage = $('[data-page="chain-details"]') if ($chainDetailsPage.length) { const store = createStore(reducer) connectElements({ store, elements }) + loadTransactions(store) + $('[data-selector="transactions-list"] [data-selector="error-message"]').on('click', _event => loadTransactions(store)) exchangeRateChannel.on('new_rate', (msg) => store.dispatch({ type: 'RECEIVED_NEW_EXCHANGE_RATE', @@ -216,6 +237,15 @@ if ($chainDetailsPage.length) { }))) } +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'})) +} + export function placeHolderBlock (blockNumber) { return `
:required, - [created_contract_address: :names] => :optional, - [from_address: :names] => :required, - [to_address: :names] => :optional - }, - paging_options: %PagingOptions{page_size: 5} - ) - exchange_rate = Market.get_exchange_rate(Explorer.coin()) || Token.null() market_history_data = @@ -43,7 +32,7 @@ defmodule BlockScoutWeb.ChainController do available_supply: available_supply(Chain.supply_for_days(30), exchange_rate), market_history_data: market_history_data, transaction_estimated_count: transaction_estimated_count, - transactions: transactions + transactions_path: recent_transactions_path(conn, :index) ) end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/recent_transactions_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/recent_transactions_controller.ex new file mode 100644 index 0000000000..4f66597bcb --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/controllers/recent_transactions_controller.ex @@ -0,0 +1,35 @@ +defmodule BlockScoutWeb.RecentTransactionsController do + use BlockScoutWeb, :controller + + alias Explorer.{Chain, PagingOptions} + alias Explorer.Chain.Hash + alias Phoenix.View + + def index(conn, _params) do + with true <- ajax?(conn) do + recent_transactions = + Chain.recent_collated_transactions( + necessity_by_association: %{ + :block => :required, + [created_contract_address: :names] => :optional, + [from_address: :names] => :required, + [to_address: :names] => :optional + }, + paging_options: %PagingOptions{page_size: 5} + ) + + transactions = + Enum.map(recent_transactions, fn transaction -> + %{ + transaction_hash: Hash.to_string(transaction.hash), + transaction_html: + View.render_to_string(BlockScoutWeb.TransactionView, "_tile.html", transaction: transaction) + } + end) + + json(conn, %{transactions: transactions}) + else + _ -> unprocessable_entity(conn) + end + end +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 c17327f0c5..4a589b1193 100644 --- a/apps/block_scout_web/lib/block_scout_web/router.ex +++ b/apps/block_scout_web/lib/block_scout_web/router.ex @@ -76,6 +76,8 @@ defmodule BlockScoutWeb.Router do resources("/pending_transactions", PendingTransactionController, only: [:index]) + resources("/recent_transactions", RecentTransactionsController, only: [:index]) + get("/txs", TransactionController, :index) resources "/tx", TransactionController, only: [:show] do 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 7fc1df3efd..b05091828b 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 @@ -72,10 +72,19 @@ <%= gettext "More transactions have come in" %>
- - <%= for transaction <- @transactions do %> - <%= render BlockScoutWeb.TransactionView, "_tile.html", transaction: transaction %> - <% end %> + + +
+ + + + + <%= gettext("Loading...") %> +
diff --git a/apps/block_scout_web/priv/gettext/default.pot b/apps/block_scout_web/priv/gettext/default.pot index faee112aad..5e0b85aea5 100644 --- a/apps/block_scout_web/priv/gettext/default.pot +++ b/apps/block_scout_web/priv/gettext/default.pot @@ -1189,6 +1189,7 @@ msgstr "" #: lib/block_scout_web/templates/address_token_transfer/index.html.eex:19 #: lib/block_scout_web/templates/address_validation/index.html.eex:63 #: lib/block_scout_web/templates/address_validation/index.html.eex:82 +#: lib/block_scout_web/templates/chain/show.html.eex:86 #: lib/block_scout_web/templates/tokens/read_contract/index.html.eex:25 msgid "Loading..." msgstr "" @@ -1532,3 +1533,8 @@ msgstr "" #: lib/block_scout_web/templates/transaction/_emission_reward_tile.html.eex:5 msgid "Emission Contract" msgstr "" + +#, elixir-format +#: lib/block_scout_web/templates/chain/show.html.eex:78 +msgid "Something went wrong, click to retry." +msgstr "" diff --git a/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po b/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po index a44e25393a..b6a91afd7a 100644 --- a/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po +++ b/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po @@ -1189,6 +1189,7 @@ msgstr "" #: lib/block_scout_web/templates/address_token_transfer/index.html.eex:19 #: lib/block_scout_web/templates/address_validation/index.html.eex:63 #: lib/block_scout_web/templates/address_validation/index.html.eex:82 +#: lib/block_scout_web/templates/chain/show.html.eex:86 #: lib/block_scout_web/templates/tokens/read_contract/index.html.eex:25 msgid "Loading..." msgstr "" @@ -1532,3 +1533,8 @@ msgstr "" #: lib/block_scout_web/templates/transaction/_emission_reward_tile.html.eex:5 msgid "Emission Contract" msgstr "" + +#, elixir-format +#: lib/block_scout_web/templates/chain/show.html.eex:78 +msgid "Something went wrong, click to retry." +msgstr "" diff --git a/apps/block_scout_web/test/block_scout_web/controllers/chain_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/chain_controller_test.exs index 1a71add242..7fd54c4c60 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/chain_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/chain_controller_test.exs @@ -5,7 +5,7 @@ defmodule BlockScoutWeb.ChainControllerTest do import BlockScoutWeb.Router.Helpers, only: [chain_path: 2, block_path: 3, transaction_path: 3, address_path: 3] - alias Explorer.Chain.Block + alias Explorer.Chain.{Block, Hash} alias Explorer.Counters.AddressesWithBalanceCounter describe "GET index/2" do @@ -41,39 +41,6 @@ defmodule BlockScoutWeb.ChainControllerTest do refute(Enum.member?(conn.assigns.blocks, old_block)) end - test "only returns transactions with an associated block", %{conn: conn} do - associated = - :transaction - |> insert() - |> with_block() - - unassociated = insert(:transaction) - - start_supervised!(AddressesWithBalanceCounter) - AddressesWithBalanceCounter.consolidate() - - conn = get(conn, "/") - - transaction_hashes = Enum.map(conn.assigns.transactions, fn transaction -> transaction.hash end) - - assert(Enum.member?(transaction_hashes, associated.hash)) - refute(Enum.member?(transaction_hashes, unassociated.hash)) - end - - test "returns a transaction", %{conn: conn} do - transaction = - :transaction - |> insert() - |> with_block() - - start_supervised!(AddressesWithBalanceCounter) - AddressesWithBalanceCounter.consolidate() - - conn = get(conn, "/") - - assert(List.first(conn.assigns.transactions).hash == transaction.hash) - end - test "returns market history data", %{conn: conn} do today = Date.utc_today() for day <- -40..0, do: insert(:market_history, date: Date.add(today, day)) diff --git a/apps/block_scout_web/test/block_scout_web/controllers/recent_transactions_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/recent_transactions_controller_test.exs new file mode 100644 index 0000000000..a35ea19491 --- /dev/null +++ b/apps/block_scout_web/test/block_scout_web/controllers/recent_transactions_controller_test.exs @@ -0,0 +1,54 @@ +defmodule BlockScoutWeb.RecentTransactionsControllerTest do + use BlockScoutWeb.ConnCase + + import BlockScoutWeb.Router.Helpers, only: [recent_transactions_path: 2] + + alias Explorer.Chain.Hash + + describe "GET index/2" do + test "returns a transaction", %{conn: conn} do + transaction = + :transaction + |> insert() + |> with_block() + + conn = + conn + |> put_req_header("x-requested-with", "xmlhttprequest") + |> get(recent_transactions_path(conn, :index)) + + assert response = json_response(conn, 200)["transactions"] + + response_hashes = Enum.map(response, & &1["transaction_hash"]) + + assert Enum.member?(response_hashes, Hash.to_string(transaction.hash)) + end + + test "only returns transactions with an associated block", %{conn: conn} do + associated = + :transaction + |> insert() + |> with_block() + + unassociated = insert(:transaction) + + conn = + conn + |> put_req_header("x-requested-with", "xmlhttprequest") + |> get(recent_transactions_path(conn, :index)) + + assert response = json_response(conn, 200)["transactions"] + + response_hashes = Enum.map(response, & &1["transaction_hash"]) + + assert Enum.member?(response_hashes, Hash.to_string(associated.hash)) + refute Enum.member?(response_hashes, Hash.to_string(unassociated.hash)) + end + + test "only responds to ajax requests", %{conn: conn} do + conn = get(conn, recent_transactions_path(conn, :index)) + + assert conn.status == 422 + end + end +end