diff --git a/apps/block_scout_web/assets/js/lib/async_listing_load.js b/apps/block_scout_web/assets/js/lib/async_listing_load.js index 0d215f21c9..8785273122 100644 --- a/apps/block_scout_web/assets/js/lib/async_listing_load.js +++ b/apps/block_scout_web/assets/js/lib/async_listing_load.js @@ -42,6 +42,8 @@ import '../app' export const asyncInitialState = { /* it will consider any query param in the current URI as paging */ beyondPageOne: (URI(window.location).query() !== ''), + /* will be sent along with { type: 'JSON' } to controller, useful for dynamically changing parameters */ + additionalParams: {}, /* an array with every html element of the list being shown */ items: [], /* the key for diffing the elements in the items array */ @@ -52,6 +54,8 @@ export const asyncInitialState = { requestError: false, /* if response has no items */ emptyResponse: false, + /* link to the current page */ + currentPagePath: null, /* link to the next page */ nextPagePath: null, /* link to the previous page */ @@ -63,7 +67,10 @@ export const asyncInitialState = { export function asyncReducer (state = asyncInitialState, action) { switch (action.type) { case 'ELEMENTS_LOAD': { - return Object.assign({}, state, { nextPagePath: action.nextPagePath }) + return Object.assign({}, state, { + nextPagePath: action.nextPagePath, + currentPagePath: action.nextPagePath + }) } case 'ADD_ITEM_KEY': { return Object.assign({}, state, { itemKey: action.itemKey }) @@ -71,7 +78,8 @@ export function asyncReducer (state = asyncInitialState, action) { case 'START_REQUEST': { return Object.assign({}, state, { loading: true, - requestError: false + requestError: false, + currentPagePath: action.path }) } case 'REQUEST_ERROR': { @@ -277,6 +285,18 @@ export function createAsyncLoadStore (reducer, initialState, itemKey) { return store } +export function refreshPage (store) { + loadPage(store, store.getState().currentPagePath) +} + +function loadPage (store, path) { + store.dispatch({type: 'START_REQUEST', path}) + $.getJSON(path, merge({type: 'JSON'}, store.getState().additionalParams)) + .done(response => store.dispatch(Object.assign({type: 'ITEMS_FETCHED'}, humps.camelizeKeys(response)))) + .fail(() => store.dispatch({type: 'REQUEST_ERROR'})) + .always(() => store.dispatch({type: 'FINISH_REQUEST'})) +} + function firstPageLoad (store) { const $element = $('[data-async-listing]') function loadItemsNext () { diff --git a/apps/block_scout_web/assets/js/pages/stakes.js b/apps/block_scout_web/assets/js/pages/stakes.js index 7e71f0066d..f9d97383b9 100644 --- a/apps/block_scout_web/assets/js/pages/stakes.js +++ b/apps/block_scout_web/assets/js/pages/stakes.js @@ -4,7 +4,7 @@ import $ from 'jquery' import _ from 'lodash' import { subscribeChannel } from '../socket' import { connectElements } from '../lib/redux_helpers.js' -import { createAsyncLoadStore } from '../lib/async_listing_load' +import { createAsyncLoadStore, refreshPage } from '../lib/async_listing_load' import Web3 from 'web3' export const initialState = { @@ -26,7 +26,11 @@ export function reducer (state = initialState, action) { return Object.assign({}, state, { web3: action.web3 }) } case 'ACCOUNT_UPDATED': { - return Object.assign({}, state, { account: action.account }) + return Object.assign({}, state, { + account: action.account, + additionalParams: { account: action.account } + }) + } } default: return state @@ -75,6 +79,7 @@ function initializeWeb3 (store) { function setAccount (account, store) { store.dispatch({ type: 'ACCOUNT_UPDATED', account }) store.getState().channel.push('set_account', account) + refreshPage(store) } async function loginByMetamask (event) { diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/stakes_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/stakes_controller.ex index 868b20f3bf..3d27df3b99 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/stakes_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/stakes_controller.ex @@ -48,7 +48,7 @@ defmodule BlockScoutWeb.StakesController do |> Map.get("position", "0") |> String.to_integer() - pools_plus_one = Chain.staking_pools(filter, options) + pools_plus_one = Chain.staking_pools(filter, options, params["account"]) {pools, next_page} = split_list_by_page(pools_plus_one) @@ -68,11 +68,12 @@ defmodule BlockScoutWeb.StakesController do average_block_time = AverageBlockTime.average_block_time() token = ContractState.get(:token, %Token{}) + epoch_number = ContractState.get(:epoch_number, 0) items = pools |> Enum.with_index(last_index + 1) - |> Enum.map(fn {pool, index} -> + |> Enum.map(fn {%{pool: pool, delegator: delegator}, index} -> View.render_to_string( StakesView, "_rows.html", @@ -80,7 +81,12 @@ defmodule BlockScoutWeb.StakesController do pool: pool, index: index, average_block_time: average_block_time, - pools_type: filter + pools_type: filter, + buttons: %{ + stake: true, + move: move_allowed?(delegator), + withdraw: withdraw_allowed?(delegator, epoch_number) + } ) end) @@ -113,4 +119,18 @@ defmodule BlockScoutWeb.StakesController do defp next_page_path(:inactive, conn, params) do inactive_pools_path(conn, :index, params) end + + defp move_allowed?(nil), do: false + + defp move_allowed?(delegator) do + delegator.is_active and Decimal.positive?(delegator.max_withdraw_allowed) + end + + defp withdraw_allowed?(nil, _epoch_number), do: false + + defp withdraw_allowed?(delegator, epoch_number) do + (delegator.is_active and Decimal.positive?(delegator.max_withdraw_allowed)) or + (delegator.is_active and Decimal.positive?(delegator.max_ordered_withdraw_allowed)) or + (Decimal.positive?(delegator.ordered_withdraw) and delegator.ordered_withdraw_epoch < epoch_number) + end end diff --git a/apps/explorer/lib/explorer/chain.ex b/apps/explorer/lib/explorer/chain.ex index 9f50e91072..033187b81f 100644 --- a/apps/explorer/lib/explorer/chain.ex +++ b/apps/explorer/lib/explorer/chain.ex @@ -7,12 +7,14 @@ defmodule Explorer.Chain do only: [ from: 2, join: 4, + join: 5, limit: 2, lock: 2, order_by: 2, order_by: 3, preload: 2, select: 2, + select: 3, subquery: 1, union: 2, union_all: 2, @@ -4721,34 +4723,50 @@ defmodule Explorer.Chain do end @doc "Get staking pools from the DB" - @spec staking_pools(filter :: :validator | :active | :inactive, options :: PagingOptions.t()) :: [map()] - def staking_pools(filter, paging_options \\ @default_paging_options) do - filter - |> staking_pools_query(paging_options) - |> Repo.all() - end - - defp staking_pools_query(filter, paging_options) do - page_size = paging_options.page_size - + @spec staking_pools( + filter :: :validator | :active | :inactive, + options :: PagingOptions.t(), + delegator_address_hash :: Hash.t() | nil + ) :: [map()] + def staking_pools(filter, paging_options \\ @default_paging_options, delegator_address_hash \\ nil) do base_query = StakingPool |> where(is_deleted: false) |> staking_pool_filter(filter) - |> limit(^page_size) + |> staking_pools_paging_query(paging_options) + + query = + if delegator_address_hash do + base_query + |> join(:left, [p], pd in StakingPoolsDelegator, + on: p.staking_address_hash == pd.pool_address_hash and pd.delegator_address_hash == ^delegator_address_hash + ) + |> select([p, pd], %{pool: p, delegator: pd}) + else + base_query + |> select([p], %{pool: p, delegator: nil}) + end + + Repo.all(query) + end + + defp staking_pools_paging_query(base_query, paging_options) do + paging_query = + base_query + |> limit(^paging_options.page_size) |> order_by(desc: :staked_ratio, asc: :staking_address_hash) case paging_options.key do {value, address_hash} -> where( - base_query, + paging_query, [p], p.staked_ratio < ^value or (p.staked_ratio == ^value and p.staking_address_hash > ^address_hash) ) _ -> - base_query + paging_query end end diff --git a/apps/explorer/test/explorer/chain_test.exs b/apps/explorer/test/explorer/chain_test.exs index cb5cfc6643..7aec34b1cf 100644 --- a/apps/explorer/test/explorer/chain_test.exs +++ b/apps/explorer/test/explorer/chain_test.exs @@ -4980,7 +4980,7 @@ defmodule Explorer.ChainTest do options = %PagingOptions{page_size: 20, page_number: 1} - assert [gotten_validator] = Chain.staking_pools(:validator, options) + assert [%{pool: gotten_validator}] = Chain.staking_pools(:validator, options) assert inserted_validator.staking_address_hash == gotten_validator.staking_address_hash end @@ -4990,7 +4990,7 @@ defmodule Explorer.ChainTest do options = %PagingOptions{page_size: 20, page_number: 1} - assert [gotten_pool] = Chain.staking_pools(:active, options) + assert [%{pool: gotten_pool}] = Chain.staking_pools(:active, options) assert inserted_pool.staking_address_hash == gotten_pool.staking_address_hash end @@ -5000,7 +5000,7 @@ defmodule Explorer.ChainTest do options = %PagingOptions{page_size: 20, page_number: 1} - assert [gotten_pool] = Chain.staking_pools(:inactive, options) + assert [%{pool: gotten_pool}] = Chain.staking_pools(:inactive, options) assert inserted_pool.staking_address_hash == gotten_pool.staking_address_hash end end