commit
8a5e2c1c48
@ -0,0 +1,46 @@ |
||||
defmodule BlockScoutWeb.AddressLogsController do |
||||
@moduledoc """ |
||||
Manages events logs tab. |
||||
""" |
||||
|
||||
import BlockScoutWeb.AddressController, only: [transaction_count: 1, validation_count: 1] |
||||
import BlockScoutWeb.Chain, only: [paging_options: 1, next_page_params: 3, split_list_by_page: 1] |
||||
|
||||
alias Explorer.{Chain, Market} |
||||
alias Explorer.ExchangeRates.Token |
||||
alias Indexer.Fetcher.CoinBalanceOnDemand |
||||
|
||||
use BlockScoutWeb, :controller |
||||
|
||||
def index(conn, %{"address_id" => address_hash_string} = params) do |
||||
with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string), |
||||
{:ok, address} <- Chain.hash_to_address(address_hash) do |
||||
logs_plus_one = Chain.address_to_logs(address, paging_options(params)) |
||||
{results, next_page} = split_list_by_page(logs_plus_one) |
||||
|
||||
next_page_url = |
||||
case next_page_params(next_page, results, params) do |
||||
nil -> |
||||
nil |
||||
|
||||
next_page_params -> |
||||
address_logs_path(conn, :index, address, next_page_params) |
||||
end |
||||
|
||||
render( |
||||
conn, |
||||
"index.html", |
||||
address: address, |
||||
logs: results, |
||||
coin_balance_status: CoinBalanceOnDemand.trigger_fetch(address), |
||||
exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(), |
||||
transaction_count: transaction_count(address), |
||||
validation_count: validation_count(address), |
||||
next_page_url: next_page_url |
||||
) |
||||
else |
||||
_ -> |
||||
not_found(conn) |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,82 @@ |
||||
<section class="container"> |
||||
<%= render BlockScoutWeb.AddressView, "overview.html", assigns %> |
||||
<div class="card"> |
||||
<%= render BlockScoutWeb.AddressView, "_tabs.html", assigns %> |
||||
|
||||
<div class="card-body"> |
||||
|
||||
<h2 class="card-title"><%= gettext "Logs" %></h2> |
||||
|
||||
<%= if @next_page_url do %> |
||||
<%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "top", cur_page_number: "1", show_pagination_limit: true, next_page_path: @next_page_url %> |
||||
<% end %> |
||||
|
||||
<%= if !@next_page_url do %> |
||||
<%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "top", cur_page_number: "1", show_pagination_limit: true %> |
||||
<% end %> |
||||
|
||||
<%= if Enum.count(@logs) > 0 do %> |
||||
<%= for log <- @logs do %> |
||||
<div data-test="transaction_log" class="tile tile-muted"> |
||||
<dl class="row"> |
||||
<dt class="col-md-2"> <%= gettext "Transaction" %> </dt> |
||||
<dd class="col-md-10"> |
||||
<h3 class=""> |
||||
<%= link( |
||||
log.transaction, |
||||
to: transaction_path(@conn, :show, log.transaction), |
||||
"data-test": "log_address_link", |
||||
"data-address-hash": log.transaction |
||||
) %> |
||||
</h3> |
||||
</dd> |
||||
<dt class="col-md-2"><%= gettext "Topics" %></dt> |
||||
<dd class="col-md-10"> |
||||
<div class="raw-transaction-log-topics"> |
||||
<%= unless is_nil(log.first_topic) do %> |
||||
<div class="text-dark"> |
||||
<span class="text-dark">[0]</span> |
||||
<%= log.first_topic %> |
||||
</div> |
||||
<% end %> |
||||
<%= unless is_nil(log.second_topic) do %> |
||||
<div class="text-dark"> |
||||
<span class="">[1] </span> |
||||
<%= log.second_topic %> |
||||
</div> |
||||
<% end %> |
||||
<%= unless is_nil(log.third_topic) do %> |
||||
<div class="text-dark"> |
||||
<span>[2]</span> |
||||
<%= log.third_topic %> |
||||
</div> |
||||
<% end %> |
||||
<%= unless is_nil(log.fourth_topic) do %> |
||||
<div class="text-dark"> |
||||
<span>[3]</span> |
||||
<%= log.fourth_topic %> |
||||
</div> |
||||
<% end %> |
||||
</div> |
||||
</dd> |
||||
<dt class="col-md-2"> |
||||
<%= gettext "Data" %> |
||||
</dt> |
||||
<dd class="col-md-10"> |
||||
<%= unless is_nil(log.data) do %> |
||||
<div class="text-dark raw-transaction-log-data"> |
||||
<%= log.data %> |
||||
</div> |
||||
<% end %> |
||||
</dd> |
||||
</dl> |
||||
</div> |
||||
<% end %> |
||||
<% else %> |
||||
<div class="tile tile-muted text-center"> |
||||
<span><%= gettext "There are no logs for this address." %></span> |
||||
</div> |
||||
<% end %> |
||||
</div> |
||||
</div> |
||||
</section> |
@ -0,0 +1,3 @@ |
||||
defmodule BlockScoutWeb.AddressLogsView do |
||||
use BlockScoutWeb, :view |
||||
end |
@ -0,0 +1,124 @@ |
||||
defmodule Explorer.Staking.EpochCounter do |
||||
@moduledoc """ |
||||
Fetches current staking epoch number and the epoch end block number. |
||||
It subscribes to handle new blocks and conclude whether the epoch is over. |
||||
""" |
||||
|
||||
use GenServer |
||||
|
||||
alias Explorer.Chain.Events.Subscriber |
||||
alias Explorer.SmartContract.Reader |
||||
|
||||
@table_name __MODULE__ |
||||
@epoch_key "epoch_num" |
||||
@epoch_end_key "epoch_end_block" |
||||
|
||||
@doc "Current staking epoch number" |
||||
def epoch_number do |
||||
if :ets.info(@table_name) != :undefined do |
||||
case :ets.lookup(@table_name, @epoch_key) do |
||||
[{_, epoch_num}] -> |
||||
epoch_num |
||||
|
||||
_ -> |
||||
0 |
||||
end |
||||
end |
||||
end |
||||
|
||||
@doc "Block number on which will start new epoch" |
||||
def epoch_end_block do |
||||
if :ets.info(@table_name) != :undefined do |
||||
case :ets.lookup(@table_name, @epoch_end_key) do |
||||
[{_, epoch_end}] -> |
||||
epoch_end |
||||
|
||||
_ -> |
||||
0 |
||||
end |
||||
end |
||||
end |
||||
|
||||
def start_link([]) do |
||||
GenServer.start_link(__MODULE__, [], name: __MODULE__) |
||||
end |
||||
|
||||
def init([]) do |
||||
:ets.new(@table_name, [ |
||||
:set, |
||||
:named_table, |
||||
:public, |
||||
write_concurrency: true |
||||
]) |
||||
|
||||
Subscriber.to(:blocks, :realtime) |
||||
{:ok, [], {:continue, :epoch_info}} |
||||
end |
||||
|
||||
def handle_continue(:epoch_info, state) do |
||||
fetch_epoch_info() |
||||
{:noreply, state} |
||||
end |
||||
|
||||
@doc "Handles new blocks and decides to fetch new epoch info" |
||||
def handle_info({:chain_event, :blocks, :realtime, blocks}, state) do |
||||
new_block_number = |
||||
blocks |
||||
|> Enum.map(&Map.get(&1, :number, 0)) |
||||
|> Enum.max(fn -> 0 end) |
||||
|
||||
case :ets.lookup(@table_name, @epoch_end_key) do |
||||
[] -> |
||||
fetch_epoch_info() |
||||
|
||||
[{_, epoch_end_block}] when epoch_end_block < new_block_number -> |
||||
fetch_epoch_info() |
||||
|
||||
_ -> |
||||
:ok |
||||
end |
||||
|
||||
{:noreply, state} |
||||
end |
||||
|
||||
defp fetch_epoch_info do |
||||
with data <- get_epoch_info(), |
||||
{:ok, [epoch_num]} <- data["stakingEpoch"], |
||||
{:ok, [epoch_end_block]} <- data["stakingEpochEndBlock"] do |
||||
:ets.insert(@table_name, {@epoch_key, epoch_num}) |
||||
:ets.insert(@table_name, {@epoch_end_key, epoch_end_block}) |
||||
end |
||||
end |
||||
|
||||
defp get_epoch_info do |
||||
contract_abi = abi("staking.json") |
||||
|
||||
functions = ["stakingEpoch", "stakingEpochEndBlock"] |
||||
|
||||
functions |
||||
|> Enum.map(fn function -> |
||||
%{ |
||||
contract_address: staking_address(), |
||||
function_name: function, |
||||
args: [] |
||||
} |
||||
end) |
||||
|> Reader.query_contracts(contract_abi) |
||||
|> Enum.zip(functions) |
||||
|> Enum.into(%{}, fn {response, function} -> |
||||
{function, response} |
||||
end) |
||||
end |
||||
|
||||
defp staking_address do |
||||
Application.get_env(:explorer, __MODULE__, [])[:staking_contract_address] |
||||
end |
||||
|
||||
# sobelow_skip ["Traversal"] |
||||
defp abi(file_name) do |
||||
:explorer |
||||
|> Application.app_dir("priv/contracts_abi/pos/#{file_name}") |
||||
|> File.read!() |
||||
|> Jason.decode!() |
||||
end |
||||
end |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,97 @@ |
||||
defmodule Explorer.Staking.EpochCounterTest do |
||||
use ExUnit.Case, async: false |
||||
|
||||
import Mox |
||||
|
||||
alias Explorer.Staking.EpochCounter |
||||
alias Explorer.Chain.Events.Publisher |
||||
|
||||
setup :verify_on_exit! |
||||
setup :set_mox_global |
||||
|
||||
test "when disabled, it returns nil" do |
||||
assert EpochCounter.epoch_number() == nil |
||||
assert EpochCounter.epoch_end_block() == nil |
||||
end |
||||
|
||||
test "fetch epoch data" do |
||||
set_mox(10, 880) |
||||
Application.put_env(:explorer, EpochCounter, enabled: true) |
||||
start_supervised!(EpochCounter) |
||||
|
||||
Process.sleep(1_000) |
||||
|
||||
assert EpochCounter.epoch_number() == 10 |
||||
assert EpochCounter.epoch_end_block() == 880 |
||||
end |
||||
|
||||
test "fetch new epoch data" do |
||||
set_mox(10, 880) |
||||
Application.put_env(:explorer, EpochCounter, enabled: true) |
||||
start_supervised!(EpochCounter) |
||||
|
||||
Process.sleep(1_000) |
||||
|
||||
assert EpochCounter.epoch_number() == 10 |
||||
assert EpochCounter.epoch_end_block() == 880 |
||||
|
||||
event_type = :blocks |
||||
broadcast_type = :realtime |
||||
event_data = [%Explorer.Chain.Block{number: 881}] |
||||
|
||||
set_mox(11, 960) |
||||
Publisher.broadcast([{event_type, event_data}], broadcast_type) |
||||
|
||||
Process.sleep(1_000) |
||||
|
||||
assert EpochCounter.epoch_number() == 11 |
||||
assert EpochCounter.epoch_end_block() == 960 |
||||
end |
||||
|
||||
defp set_mox(epoch_num, end_block_num) do |
||||
expect( |
||||
EthereumJSONRPC.Mox, |
||||
:json_rpc, |
||||
fn [ |
||||
%{ |
||||
id: 0, |
||||
jsonrpc: "2.0", |
||||
method: "eth_call", |
||||
params: _ |
||||
}, |
||||
%{ |
||||
id: 1, |
||||
jsonrpc: "2.0", |
||||
method: "eth_call", |
||||
params: _ |
||||
} |
||||
], |
||||
_options -> |
||||
{:ok, |
||||
[ |
||||
%{ |
||||
id: 0, |
||||
jsonrpc: "2.0", |
||||
result: encode_num(epoch_num) |
||||
}, |
||||
%{ |
||||
id: 1, |
||||
jsonrpc: "2.0", |
||||
result: encode_num(end_block_num) |
||||
} |
||||
]} |
||||
end |
||||
) |
||||
end |
||||
|
||||
defp encode_num(num) do |
||||
selector = %ABI.FunctionSelector{function: nil, types: [uint: 32]} |
||||
|
||||
encoded_num = |
||||
[num] |
||||
|> ABI.TypeEncoder.encode(selector) |
||||
|> Base.encode16(case: :lower) |
||||
|
||||
"0x" <> encoded_num |
||||
end |
||||
end |
Loading…
Reference in new issue