commit
2cebce70b0
@ -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