Consolidate POSDAO contract reading (#2371)

Previously, staking contract reading was arbitrarily spread across
Explorer.Staking.ContractState, Explorer.Staking.PoolsReader and
Indexer.Fetcher.StakingPools. Using async fetcher infrastructure for staking
contracts is inadequate, as blocks may arrive out of order, while we want
fetching to be triggered by newer block arrival. Also, contract calls were
not batched enough, and it was hard to follow their sequence across involved modules.

Now, on each incoming block, which is newer than the last seen, we fully update
not only global contract state, but all validators, pools and delegators.

All requests are intelligently compiled into four batches.
These are defined together to give better overview of performed actions.
Chain.import infrastructure is still used for inserting data into DB
to leverage smart batching of INSERT queries.

Likelihood calculation is added: it zips results of getPoolsToBeElected
and getPoolsLikelihood calls to make a map of addresses.

Only POS_STAKING_CONTRACT env variable is required now.
Other contract addresses are fetched from it during launch.
ABIs are concatenated together during launch as well, as we don't support
batch requests against distinct ABIs yet. This should be addressed in further PRs.

Up-to-date ABIs copied from build artifacts of posdao-contracts repo.
File names now correspond to contract names, README directs to their origin.

* Fetch inactive delegators, mark pools and delegators deleted (#2205)
* Fetch min stakes and token contract address from StakingAuRa (#2313)
* Fetch block reward ratio using validatorRewardPercent getter (#2424)
* Add missing `banned_until` field in ON CONFLICT clause for staking pools.
staking
Paul Tsupikoff 5 years ago committed by Victor Baranov
parent 30109ca4bc
commit 6a5916764d
  1. 5
      apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/encoder.ex
  2. 4
      apps/explorer/config/config.exs
  3. 2
      apps/explorer/config/test.exs
  4. 2
      apps/explorer/lib/explorer/application.ex
  5. 40
      apps/explorer/lib/explorer/chain/import/runner/staking_pools.ex
  6. 29
      apps/explorer/lib/explorer/chain/import/runner/staking_pools_delegators.ex
  7. 6
      apps/explorer/lib/explorer/chain/staking_pool.ex
  8. 15
      apps/explorer/lib/explorer/chain/staking_pools_delegator.ex
  9. 100
      apps/explorer/lib/explorer/staking/contract_reader.ex
  10. 205
      apps/explorer/lib/explorer/staking/contract_state.ex
  11. 126
      apps/explorer/lib/explorer/staking/epoch_counter.ex
  12. 174
      apps/explorer/lib/explorer/staking/pools_reader.ex
  13. 632
      apps/explorer/priv/contracts_abi/posdao/BlockRewardAuRa.json
  14. 2
      apps/explorer/priv/contracts_abi/posdao/README.md
  15. 909
      apps/explorer/priv/contracts_abi/posdao/StakingAuRa.json
  16. 458
      apps/explorer/priv/contracts_abi/posdao/ValidatorSetAuRa.json
  17. 10
      apps/explorer/priv/repo/migrations/20190605125829_delegators_deleted_colum.exs
  18. 9
      apps/explorer/priv/repo/migrations/20190718175620_add_block_reward_to_pools.exs
  19. 12
      apps/explorer/test/explorer/chain/import/runner/staking_pools_test.exs
  20. 208
      apps/explorer/test/explorer/staking/contract_state_test.exs
  21. 97
      apps/explorer/test/explorer/staking/epoch_counter_test.exs
  22. 361
      apps/explorer/test/explorer/staking/pools_reader_test.exs
  23. 5
      apps/indexer/config/config.exs
  24. 5
      apps/indexer/lib/indexer/block/fetcher.ex
  25. 4
      apps/indexer/lib/indexer/block/realtime/fetcher.ex
  26. 140
      apps/indexer/lib/indexer/fetcher/staking_pools.ex
  27. 2
      apps/indexer/lib/indexer/supervisor.ex
  28. 224
      apps/indexer/test/indexer/fetcher/staking_pools_test.exs

@ -66,6 +66,11 @@ defmodule EthereumJSONRPC.Encoder do
|> String.slice(2..-1) |> String.slice(2..-1)
|> Base.decode16!(case: :lower) |> Base.decode16!(case: :lower)
|> TypeDecoder.decode_raw(types_list) |> TypeDecoder.decode_raw(types_list)
|> Enum.zip(types_list)
|> Enum.map(fn
{value, :address} -> "0x" <> Base.encode16(value, case: :lower)
{value, _} -> value
end)
{id, {:ok, decoded_data}} {id, {:ok, decoded_data}}
rescue rescue

@ -171,11 +171,11 @@ config :explorer, Explorer.Staking.PoolsReader,
staking_contract_address: System.get_env("POS_STAKING_CONTRACT") staking_contract_address: System.get_env("POS_STAKING_CONTRACT")
if System.get_env("POS_STAKING_CONTRACT") do if System.get_env("POS_STAKING_CONTRACT") do
config :explorer, Explorer.Staking.EpochCounter, config :explorer, Explorer.Staking.ContractState,
enabled: true, enabled: true,
staking_contract_address: System.get_env("POS_STAKING_CONTRACT") staking_contract_address: System.get_env("POS_STAKING_CONTRACT")
else else
config :explorer, Explorer.Staking.EpochCounter, enabled: false config :explorer, Explorer.Staking.ContractState, enabled: false
end end
case System.get_env("SUPPLY_MODULE") do case System.get_env("SUPPLY_MODULE") do

@ -32,6 +32,8 @@ config :explorer, Explorer.Market.History.Cataloger, enabled: false
config :explorer, Explorer.Tracer, disabled?: false config :explorer, Explorer.Tracer, disabled?: false
config :explorer, Explorer.Staking.ContractState, enabled: false
config :logger, :explorer, config :logger, :explorer,
level: :warn, level: :warn,
path: Path.absname("logs/test/explorer.log") path: Path.absname("logs/test/explorer.log")

@ -82,7 +82,7 @@ defmodule Explorer.Application do
configure(Explorer.Counters.AverageBlockTime), configure(Explorer.Counters.AverageBlockTime),
configure(Explorer.Counters.Bridge), configure(Explorer.Counters.Bridge),
configure(Explorer.Validator.MetadataProcessor), configure(Explorer.Validator.MetadataProcessor),
configure(Explorer.Staking.EpochCounter) configure(Explorer.Staking.ContractState)
] ]
|> List.flatten() |> List.flatten()
end end

@ -51,9 +51,6 @@ defmodule Explorer.Chain.Import.Runner.StakingPools do
|> Multi.run(:insert_staking_pools, fn repo, _ -> |> Multi.run(:insert_staking_pools, fn repo, _ ->
insert(repo, changes_list, insert_options) insert(repo, changes_list, insert_options)
end) end)
|> Multi.run(:calculate_stakes_ratio, fn repo, _ ->
calculate_stakes_ratio(repo, insert_options)
end)
end end
@impl Import.Runner @impl Import.Runner
@ -131,51 +128,18 @@ defmodule Explorer.Chain.Import.Runner.StakingPools do
is_banned: fragment("EXCLUDED.is_banned"), is_banned: fragment("EXCLUDED.is_banned"),
is_validator: fragment("EXCLUDED.is_validator"), is_validator: fragment("EXCLUDED.is_validator"),
likelihood: fragment("EXCLUDED.likelihood"), likelihood: fragment("EXCLUDED.likelihood"),
block_reward_ratio: fragment("EXCLUDED.block_reward_ratio"),
staked_ratio: fragment("EXCLUDED.staked_ratio"), staked_ratio: fragment("EXCLUDED.staked_ratio"),
self_staked_amount: fragment("EXCLUDED.self_staked_amount"), self_staked_amount: fragment("EXCLUDED.self_staked_amount"),
staked_amount: fragment("EXCLUDED.staked_amount"), staked_amount: fragment("EXCLUDED.staked_amount"),
was_banned_count: fragment("EXCLUDED.was_banned_count"), was_banned_count: fragment("EXCLUDED.was_banned_count"),
was_validator_count: fragment("EXCLUDED.was_validator_count"), was_validator_count: fragment("EXCLUDED.was_validator_count"),
is_deleted: fragment("EXCLUDED.is_deleted"), is_deleted: fragment("EXCLUDED.is_deleted"),
banned_until: fragment("EXCLUDED.banned_until"),
inserted_at: fragment("LEAST(?, EXCLUDED.inserted_at)", pool.inserted_at), inserted_at: fragment("LEAST(?, EXCLUDED.inserted_at)", pool.inserted_at),
updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", pool.updated_at) updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", pool.updated_at)
] ]
] ]
) )
end end
defp calculate_stakes_ratio(repo, %{timeout: timeout}) do
total_query =
from(
pool in StakingPool,
where: pool.is_active == true,
select: sum(pool.staked_amount)
)
total = repo.one!(total_query)
if total.value > Decimal.new(0) do
update_query =
from(
p in StakingPool,
where: p.is_active == true,
# ShareLocks order already enforced by `acquire_all_staking_pools` (see docs: sharelocks.md)
update: [
set: [
staked_ratio: p.staked_amount / ^total.value * 100,
likelihood: p.staked_amount / ^total.value * 100
]
]
)
{count, _} = repo.update_all(update_query, [], timeout: timeout)
{:ok, count}
else
{:ok, 1}
end
rescue
postgrex_error in Postgrex.Error ->
{:error, %{exception: postgrex_error}}
end
end end

@ -41,6 +41,9 @@ defmodule Explorer.Chain.Import.Runner.StakingPoolsDelegators do
|> Map.put(:timestamps, timestamps) |> Map.put(:timestamps, timestamps)
multi multi
|> Multi.run(:delete_delegators, fn repo, _ ->
mark_as_deleted(repo, insert_options)
end)
|> Multi.run(:insert_staking_pools_delegators, fn repo, _ -> |> Multi.run(:insert_staking_pools_delegators, fn repo, _ ->
insert(repo, changes_list, insert_options) insert(repo, changes_list, insert_options)
end) end)
@ -49,6 +52,28 @@ defmodule Explorer.Chain.Import.Runner.StakingPoolsDelegators do
@impl Import.Runner @impl Import.Runner
def timeout, do: @timeout def timeout, do: @timeout
defp mark_as_deleted(repo, %{timeout: timeout}) do
query =
from(
d in StakingPoolsDelegator,
update: [
set: [
is_active: false,
is_deleted: true
]
]
)
try do
{_, result} = repo.update_all(query, [], timeout: timeout)
{:ok, result}
rescue
postgrex_error in Postgrex.Error ->
{:error, %{exception: postgrex_error}}
end
end
@spec insert(Repo.t(), [map()], %{ @spec insert(Repo.t(), [map()], %{
optional(:on_conflict) => Import.Runner.on_conflict(), optional(:on_conflict) => Import.Runner.on_conflict(),
required(:timeout) => timeout, required(:timeout) => timeout,
@ -86,7 +111,9 @@ defmodule Explorer.Chain.Import.Runner.StakingPoolsDelegators do
max_ordered_withdraw_allowed: fragment("EXCLUDED.max_ordered_withdraw_allowed"), max_ordered_withdraw_allowed: fragment("EXCLUDED.max_ordered_withdraw_allowed"),
ordered_withdraw_epoch: fragment("EXCLUDED.ordered_withdraw_epoch"), ordered_withdraw_epoch: fragment("EXCLUDED.ordered_withdraw_epoch"),
inserted_at: fragment("LEAST(?, EXCLUDED.inserted_at)", delegator.inserted_at), inserted_at: fragment("LEAST(?, EXCLUDED.inserted_at)", delegator.inserted_at),
updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", delegator.updated_at) updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", delegator.updated_at),
is_active: fragment("EXCLUDED.is_active"),
is_deleted: fragment("EXCLUDED.is_deleted")
] ]
] ]
) )

@ -21,7 +21,8 @@ defmodule Explorer.Chain.StakingPool do
is_active: boolean, is_active: boolean,
is_banned: boolean, is_banned: boolean,
is_validator: boolean, is_validator: boolean,
likelihood: integer, likelihood: Decimal.t(),
block_reward_ratio: Decimal.t(),
staked_ratio: Decimal.t(), staked_ratio: Decimal.t(),
self_staked_amount: Wei.t(), self_staked_amount: Wei.t(),
staked_amount: Wei.t(), staked_amount: Wei.t(),
@ -33,7 +34,7 @@ defmodule Explorer.Chain.StakingPool do
@attrs ~w( @attrs ~w(
is_active delegators_count staked_amount self_staked_amount is_validator is_active delegators_count staked_amount self_staked_amount is_validator
was_validator_count is_banned was_banned_count banned_until likelihood was_validator_count is_banned was_banned_count banned_until likelihood
staked_ratio staking_address_hash mining_address_hash staked_ratio staking_address_hash mining_address_hash block_reward_ratio
)a )a
@req_attrs ~w( @req_attrs ~w(
is_active delegators_count staked_amount self_staked_amount is_validator is_active delegators_count staked_amount self_staked_amount is_validator
@ -48,6 +49,7 @@ defmodule Explorer.Chain.StakingPool do
field(:is_banned, :boolean, default: false) field(:is_banned, :boolean, default: false)
field(:is_validator, :boolean, default: false) field(:is_validator, :boolean, default: false)
field(:likelihood, :decimal) field(:likelihood, :decimal)
field(:block_reward_ratio, :decimal)
field(:staked_ratio, :decimal) field(:staked_ratio, :decimal)
field(:self_staked_amount, Wei) field(:self_staked_amount, Wei)
field(:staked_amount, Wei) field(:staked_amount, Wei)

@ -20,12 +20,21 @@ defmodule Explorer.Chain.StakingPoolsDelegator do
max_withdraw_allowed: Wei.t(), max_withdraw_allowed: Wei.t(),
ordered_withdraw: Wei.t(), ordered_withdraw: Wei.t(),
stake_amount: Wei.t(), stake_amount: Wei.t(),
ordered_withdraw_epoch: integer() ordered_withdraw_epoch: integer(),
is_active: boolean(),
is_deleted: boolean()
} }
@attrs ~w( @attrs ~w(
pool_address_hash delegator_address_hash max_ordered_withdraw_allowed pool_address_hash delegator_address_hash max_ordered_withdraw_allowed
max_withdraw_allowed ordered_withdraw stake_amount ordered_withdraw_epoch max_withdraw_allowed ordered_withdraw stake_amount ordered_withdraw_epoch
is_active is_deleted
)a
@req_attrs ~w(
pool_address_hash delegator_address_hash max_ordered_withdraw_allowed
max_withdraw_allowed ordered_withdraw stake_amount ordered_withdraw_epoch
)a )a
schema "staking_pools_delegators" do schema "staking_pools_delegators" do
@ -34,6 +43,8 @@ defmodule Explorer.Chain.StakingPoolsDelegator do
field(:ordered_withdraw, Wei) field(:ordered_withdraw, Wei)
field(:ordered_withdraw_epoch, :integer) field(:ordered_withdraw_epoch, :integer)
field(:stake_amount, Wei) field(:stake_amount, Wei)
field(:is_active, :boolean, default: true)
field(:is_deleted, :boolean, default: false)
belongs_to( belongs_to(
:staking_pool, :staking_pool,
@ -58,7 +69,7 @@ defmodule Explorer.Chain.StakingPoolsDelegator do
def changeset(staking_pools_delegator, attrs) do def changeset(staking_pools_delegator, attrs) do
staking_pools_delegator staking_pools_delegator
|> cast(attrs, @attrs) |> cast(attrs, @attrs)
|> validate_required(@attrs) |> validate_required(@req_attrs)
|> unique_constraint(:pool_address_hash, name: :pools_delegator_index) |> unique_constraint(:pool_address_hash, name: :pools_delegator_index)
end end
end end

@ -0,0 +1,100 @@
defmodule Explorer.Staking.ContractReader do
@moduledoc """
Routines for batched fetching of information from POSDAO contracts
"""
alias Explorer.SmartContract.Reader
def global_requests do
[
token_contract_address: {:staking, "erc20TokenContract", []},
min_candidate_stake: {:staking, "candidateMinStake", []},
min_delegator_stake: {:staking, "delegatorMinStake", []},
epoch_number: {:staking, "stakingEpoch", []},
epoch_end_block: {:staking, "stakingEpochEndBlock", []},
active_pools: {:staking, "getPools", []},
inactive_pools: {:staking, "getPoolsInactive", []},
pools_likely: {:staking, "getPoolsToBeElected", []},
pools_likelihood: {:staking, "getPoolsLikelihood", []},
validators: {:validator_set, "getValidators", []}
]
end
def pool_staking_requests(staking_address) do
[
mining_address_hash: {:validator_set, "miningByStakingAddress", [staking_address]},
is_active: {:staking, "isPoolActive", [staking_address]},
active_delegators: {:staking, "poolDelegators", [staking_address]},
inactive_delegators: {:staking, "poolDelegatorsInactive", [staking_address]},
staked_amount: {:staking, "stakeAmountTotalMinusOrderedWithdraw", [staking_address]},
self_staked_amount: {:staking, "stakeAmountMinusOrderedWithdraw", [staking_address, staking_address]},
block_reward: {:block_reward, "validatorRewardPercent", [staking_address]}
]
end
def pool_mining_requests(mining_address) do
[
is_validator: {:validator_set, "isValidator", [mining_address]},
was_validator_count: {:validator_set, "validatorCounter", [mining_address]},
is_banned: {:validator_set, "isValidatorBanned", [mining_address]},
banned_until: {:validator_set, "bannedUntil", [mining_address]},
was_banned_count: {:validator_set, "banCounter", [mining_address]}
]
end
def delegator_requests(pool_address, delegator_address) do
[
stake_amount: {:staking, "stakeAmount", [pool_address, delegator_address]},
ordered_withdraw: {:staking, "orderedWithdrawAmount", [pool_address, delegator_address]},
max_withdraw_allowed: {:staking, "maxWithdrawAllowed", [pool_address, delegator_address]},
max_ordered_withdraw_allowed: {:staking, "maxWithdrawOrderAllowed", [pool_address, delegator_address]},
ordered_withdraw_epoch: {:staking, "orderWithdrawEpoch", [pool_address, delegator_address]}
]
end
def perform_requests(requests, contracts, abi) do
requests
|> generate_requests(contracts)
|> Reader.query_contracts(abi)
|> parse_responses(requests)
end
def perform_grouped_requests(requests, keys, contracts, abi) do
requests
|> List.flatten()
|> generate_requests(contracts)
|> Reader.query_contracts(abi)
|> parse_grouped_responses(keys, requests)
end
defp generate_requests(functions, contracts) do
Enum.map(functions, fn {_, {contract, function, args}} ->
%{
contract_address: contracts[contract],
function_name: function,
args: args
}
end)
end
defp parse_responses(responses, requests) do
requests
|> Enum.zip(responses)
|> Enum.into(%{}, fn {{key, _}, {:ok, response}} ->
case response do
[item] -> {key, item}
items -> {key, items}
end
end)
end
defp parse_grouped_responses(responses, keys, grouped_requests) do
{grouped_responses, _} = Enum.map_reduce(grouped_requests, responses, &Enum.split(&2, length(&1)))
[keys, grouped_requests, grouped_responses]
|> Enum.zip()
|> Enum.into(%{}, fn {key, requests, responses} ->
{key, parse_responses(responses, requests)}
end)
end
end

@ -0,0 +1,205 @@
defmodule Explorer.Staking.ContractState do
@moduledoc """
Fetches all information from POSDAO staking contracts.
All contract calls are batched into four requests, according to their dependencies.
Subscribes to new block notifications and refreshes when previously unseen block arrives.
"""
use GenServer
alias Explorer.Chain
alias Explorer.Chain.Events.Subscriber
alias Explorer.SmartContract.Reader
alias Explorer.Staking.ContractReader
@table_name __MODULE__
@table_keys [
:token_contract_address,
:min_candidate_stake,
:min_delegator_stake,
:epoch_number,
:epoch_end_block
]
defstruct [
:seen_block,
:contracts,
:abi
]
@spec get(atom(), value) :: value when value: any()
def get(key, default \\ nil) when key in @table_keys do
with info when info != :undefined <- :ets.info(@table_name),
[{_, value}] <- :ets.lookup(@table_name, key) do
value
else
_ -> default
end
end
def start_link([]) do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
def init([]) do
:ets.new(@table_name, [
:set,
:named_table,
:public,
read_concurrency: true,
write_concurrency: true
])
Subscriber.to(:blocks, :realtime)
staking_abi = abi("StakingAuRa")
validator_set_abi = abi("ValidatorSetAuRa")
block_reward_abi = abi("BlockRewardAuRa")
staking_contract_address = Application.get_env(:explorer, __MODULE__)[:staking_contract_address]
%{"validatorSetContract" => {:ok, [validator_set_contract_address]}} =
Reader.query_contract(staking_contract_address, staking_abi, %{"validatorSetContract" => []})
%{"blockRewardContract" => {:ok, [block_reward_contract_address]}} =
Reader.query_contract(validator_set_contract_address, validator_set_abi, %{"blockRewardContract" => []})
state = %__MODULE__{
seen_block: 0,
contracts: %{
staking: staking_contract_address,
validator_set: validator_set_contract_address,
block_reward: block_reward_contract_address
},
abi: staking_abi ++ validator_set_abi ++ block_reward_abi
}
{:ok, state, {:continue, []}}
end
def handle_continue(_, state) do
fetch_state(state.contracts, state.abi)
{:noreply, state}
end
@doc "Handles new blocks and decides to fetch fresh chain info"
def handle_info({:chain_event, :blocks, :realtime, blocks}, state) do
latest_block = Enum.max_by(blocks, & &1.number)
if latest_block.number > state.seen_block do
fetch_state(state.contracts, state.abi)
{:noreply, %{state | seen_block: latest_block.number}}
else
{:noreply, state}
end
end
defp fetch_state(contracts, abi) do
global_responses = ContractReader.perform_requests(ContractReader.global_requests(), contracts, abi)
settings =
global_responses
|> Map.take([
:token_contract_address,
:min_candidate_stake,
:min_delegator_stake,
:epoch_number,
:epoch_end_block
])
|> Map.to_list()
:ets.insert(@table_name, settings)
pools = global_responses.active_pools ++ global_responses.inactive_pools
pool_staking_responses =
pools
|> Enum.map(&ContractReader.pool_staking_requests/1)
|> ContractReader.perform_grouped_requests(pools, contracts, abi)
pool_mining_responses =
pools
|> Enum.map(&ContractReader.pool_mining_requests(pool_staking_responses[&1].mining_address_hash))
|> ContractReader.perform_grouped_requests(pools, contracts, abi)
delegators =
Enum.flat_map(pool_staking_responses, fn {pool_address, responses} ->
Enum.map(responses.active_delegators, &{pool_address, &1, true}) ++
Enum.map(responses.inactive_delegators, &{pool_address, &1, false})
end)
delegator_responses =
delegators
|> Enum.map(fn {pool_address, delegator_address, _} ->
ContractReader.delegator_requests(pool_address, delegator_address)
end)
|> ContractReader.perform_grouped_requests(delegators, contracts, abi)
staked_total = Enum.sum(for {_, pool} <- pool_staking_responses, pool.is_active, do: pool.staked_amount)
[likelihood_values, total_likelihood] = global_responses.pools_likelihood
likelihood =
global_responses.pools_likely
|> Enum.zip(likelihood_values)
|> Enum.into(%{})
pool_entries =
Enum.map(pools, fn staking_address ->
staking_response = pool_staking_responses[staking_address]
mining_response = pool_mining_responses[staking_address]
%{
staking_address_hash: staking_address,
delegators_count: length(staking_response.active_delegators),
staked_ratio: ratio(staking_response.staked_amount, staked_total),
likelihood: ratio(likelihood[staking_address] || 0, total_likelihood),
block_reward_ratio: staking_response.block_reward / 10_000,
is_deleted: false
}
|> Map.merge(
Map.take(staking_response, [
:mining_address_hash,
:is_active,
:staked_amount,
:self_staked_amount
])
)
|> Map.merge(
Map.take(mining_response, [
:is_validator,
:was_validator_count,
:is_banned,
:banned_until,
:was_banned_count
])
)
end)
delegator_entries =
Enum.map(delegator_responses, fn {{pool_address, delegator_address, is_active}, response} ->
Map.merge(response, %{
delegator_address_hash: delegator_address,
pool_address_hash: pool_address,
is_active: is_active
})
end)
{:ok, _} =
Chain.import(%{
staking_pools: %{params: pool_entries},
staking_pools_delegators: %{params: delegator_entries},
timeout: :infinity
})
end
defp ratio(_numerator, 0), do: 0
defp ratio(numerator, denominator), do: numerator / denominator * 100
# sobelow_skip ["Traversal"]
defp abi(file_name) do
:explorer
|> Application.app_dir("priv/contracts_abi/posdao/#{file_name}.json")
|> File.read!()
|> Jason.decode!()
end
end

@ -1,126 +0,0 @@
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
# 794c0c68 = keccak256(stakingEpoch())
# 8c2243ae = keccak256(stakingEpochEndBlock())
with data <- get_epoch_info(),
{:ok, [epoch_num]} <- data["794c0c68"],
{:ok, [epoch_end_block]} <- data["8c2243ae"] 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")
method_ids = ["794c0c68", "8c2243ae"]
method_ids
|> Enum.map(fn method_id ->
%{
contract_address: staking_address(),
method_id: method_id,
args: []
}
end)
|> Reader.query_contracts(contract_abi)
|> Enum.zip(method_ids)
|> Enum.into(%{}, fn {response, method_id} ->
{method_id, 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

@ -1,174 +0,0 @@
defmodule Explorer.Staking.PoolsReader do
@moduledoc """
Reads staking pools using Smart Contract functions from the blockchain.
"""
alias Explorer.SmartContract.Reader
@spec get_pools() :: [String.t()]
def get_pools do
get_active_pools() ++ get_inactive_pools()
end
@spec get_active_pools() :: [String.t()]
def get_active_pools do
# 673a2a1f = keccak256(getPools())
{:ok, [active_pools]} = call_staking_method("673a2a1f", [])
active_pools
end
@spec get_inactive_pools() :: [String.t()]
def get_inactive_pools do
# df6f55f5 = keccak256(getPoolsInactive())
{:ok, [inactive_pools]} = call_staking_method("df6f55f5", [])
inactive_pools
end
@spec pool_data(String.t()) :: {:ok, map()} | :error
def pool_data(staking_address) do
# 00535175 = keccak256(miningByStakingAddress(address))
with {:ok, [mining_address]} <- call_validators_method("00535175", [staking_address]),
data = fetch_pool_data(staking_address, mining_address),
{:ok, [is_active]} <- data["a711e6a1"],
{:ok, [delegator_addresses]} <- data["9ea8082b"],
delegators_count = Enum.count(delegator_addresses),
delegators = delegators_data(delegator_addresses, staking_address),
{:ok, [staked_amount]} <- data["234fbf2b"],
{:ok, [self_staked_amount]} <- data["58daab6a"],
{:ok, [is_validator]} <- data["facd743b"],
{:ok, [was_validator_count]} <- data["b41832e4"],
{:ok, [is_banned]} <- data["a92252ae"],
{:ok, [banned_until]} <- data["5836d08a"],
{:ok, [was_banned_count]} <- data["1d0cd4c6"] do
{
:ok,
%{
staking_address_hash: staking_address,
mining_address_hash: mining_address,
is_active: is_active,
delegators_count: delegators_count,
staked_amount: staked_amount,
self_staked_amount: self_staked_amount,
is_validator: is_validator,
was_validator_count: was_validator_count,
is_banned: is_banned,
banned_until: banned_until,
was_banned_count: was_banned_count,
delegators: delegators
}
}
else
_ ->
:error
end
end
defp delegators_data(delegators, pool_address) do
Enum.map(delegators, fn address ->
# a697ecff = keccak256(stakeAmount(address,address))
# e9ab0300 = keccak256(orderedWithdrawAmount(address,address))
# 6bda1577 = keccak256(maxWithdrawAllowed(address,address))
# 950a6513 = keccak256(maxWithdrawOrderAllowed(address,address))
# a4205967 = keccak256(orderWithdrawEpoch(address,address))
data =
call_methods([
{:staking, "a697ecff", [pool_address, address]},
{:staking, "e9ab0300", [pool_address, address]},
{:staking, "6bda1577", [pool_address, address]},
{:staking, "950a6513", [pool_address, address]},
{:staking, "a4205967", [pool_address, address]}
])
{:ok, [stake_amount]} = data["a697ecff"]
{:ok, [ordered_withdraw]} = data["e9ab0300"]
{:ok, [max_withdraw_allowed]} = data["6bda1577"]
{:ok, [max_ordered_withdraw_allowed]} = data["950a6513"]
{:ok, [ordered_withdraw_epoch]} = data["a4205967"]
%{
delegator_address_hash: address,
pool_address_hash: pool_address,
stake_amount: stake_amount,
ordered_withdraw: ordered_withdraw,
max_withdraw_allowed: max_withdraw_allowed,
max_ordered_withdraw_allowed: max_ordered_withdraw_allowed,
ordered_withdraw_epoch: ordered_withdraw_epoch
}
end)
end
defp call_staking_method(method, params) do
%{^method => resp} =
Reader.query_contract(config(:staking_contract_address), abi("staking.json"), %{
method => params
})
resp
end
defp call_validators_method(method, params) do
%{^method => resp} =
Reader.query_contract(config(:validators_contract_address), abi("validators.json"), %{
method => params
})
resp
end
defp fetch_pool_data(staking_address, mining_address) do
# a711e6a1 = keccak256(isPoolActive(address))
# 9ea8082b = keccak256(poolDelegators(address))
# 234fbf2b = keccak256(stakeAmountTotalMinusOrderedWithdraw(address))
# 58daab6a = keccak256(stakeAmountMinusOrderedWithdraw(address,address))
# facd743b = keccak256(isValidator(address))
# b41832e4 = keccak256(validatorCounter(address))
# a92252ae = keccak256(isValidatorBanned(address))
# 5836d08a = keccak256(bannedUntil(address))
# 1d0cd4c6 = keccak256(banCounter(address))
call_methods([
{:staking, "a711e6a1", [staking_address]},
{:staking, "9ea8082b", [staking_address]},
{:staking, "234fbf2b", [staking_address]},
{:staking, "58daab6a", [staking_address, staking_address]},
{:validators, "facd743b", [mining_address]},
{:validators, "b41832e4", [mining_address]},
{:validators, "a92252ae", [mining_address]},
{:validators, "5836d08a", [mining_address]},
{:validators, "1d0cd4c6", [mining_address]}
])
end
defp call_methods(methods) do
contract_abi = abi("staking.json") ++ abi("validators.json")
methods
|> Enum.map(&format_request/1)
|> Reader.query_contracts(contract_abi)
|> Enum.zip(methods)
|> Enum.into(%{}, fn {response, {_, method_id, _}} ->
{method_id, response}
end)
end
defp format_request({contract_name, method_id, params}) do
%{
contract_address: contract(contract_name),
method_id: method_id,
args: params
}
end
defp contract(:staking), do: config(:staking_contract_address)
defp contract(:validators), do: config(:validators_contract_address)
defp config(key) do
Application.get_env(:explorer, __MODULE__, [])[key]
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

@ -0,0 +1,632 @@
[
{
"constant": true,
"inputs": [],
"name": "DELEGATORS_ALIQUOT",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "bridgeTokenFee",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "isRewarding",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "",
"type": "address"
},
{
"name": "",
"type": "uint256"
}
],
"name": "mintedForAccountInBlock",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "",
"type": "uint256"
},
{
"name": "",
"type": "address"
}
],
"name": "epochPoolNativeReward",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "",
"type": "address"
}
],
"name": "mintedForAccount",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "",
"type": "uint256"
}
],
"name": "mintedInBlock",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "mintedTotally",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "tokenRewardUndistributed",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "nativeRewardUndistributed",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "isSnapshotting",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "",
"type": "address"
}
],
"name": "mintedTotallyByBridge",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "",
"type": "uint256"
},
{
"name": "",
"type": "address"
}
],
"name": "epochPoolTokenReward",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "validatorSetContract",
"outputs": [
{
"name": "",
"type": "address"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "bridgeNativeFee",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "snapshotTotalStakeAmount",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "",
"type": "uint256"
},
{
"name": "",
"type": "address"
}
],
"name": "blocksCreated",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"name": "amount",
"type": "uint256"
},
{
"indexed": true,
"name": "receiver",
"type": "address"
},
{
"indexed": true,
"name": "bridge",
"type": "address"
}
],
"name": "AddedReceiver",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"name": "receivers",
"type": "address[]"
},
{
"indexed": false,
"name": "rewards",
"type": "uint256[]"
}
],
"name": "MintedNative",
"type": "event"
},
{
"constant": false,
"inputs": [
{
"name": "_amount",
"type": "uint256"
}
],
"name": "addBridgeNativeFeeReceivers",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_amount",
"type": "uint256"
}
],
"name": "addBridgeTokenFeeReceivers",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_amount",
"type": "uint256"
},
{
"name": "_receiver",
"type": "address"
}
],
"name": "addExtraReceiver",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_validatorSet",
"type": "address"
}
],
"name": "initialize",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_bridge",
"type": "address"
},
{
"name": "_prevBlockRewardContract",
"type": "address"
}
],
"name": "migrateMintingStatistics",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "benefactors",
"type": "address[]"
},
{
"name": "kind",
"type": "uint16[]"
}
],
"name": "reward",
"outputs": [
{
"name": "receiversNative",
"type": "address[]"
},
{
"name": "rewardsNative",
"type": "uint256[]"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_bridgesAllowed",
"type": "address[]"
}
],
"name": "setErcToNativeBridgesAllowed",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_bridgesAllowed",
"type": "address[]"
}
],
"name": "setNativeToErcBridgesAllowed",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_bridgesAllowed",
"type": "address[]"
}
],
"name": "setErcToErcBridgesAllowed",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "blockRewardContractId",
"outputs": [
{
"name": "",
"type": "bytes4"
}
],
"payable": false,
"stateMutability": "pure",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "ercToErcBridgesAllowed",
"outputs": [
{
"name": "",
"type": "address[]"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "ercToNativeBridgesAllowed",
"outputs": [
{
"name": "",
"type": "address[]"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "extraReceiversQueueSize",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "isInitialized",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "nativeToErcBridgesAllowed",
"outputs": [
{
"name": "",
"type": "address[]"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "_validatorStakingAddress",
"type": "address"
}
],
"name": "snapshotRewardPercents",
"outputs": [
{
"name": "result",
"type": "uint256[]"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "_validatorStakingAddress",
"type": "address"
}
],
"name": "snapshotStakers",
"outputs": [
{
"name": "result",
"type": "address[]"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "snapshotStakingAddresses",
"outputs": [
{
"name": "",
"type": "address[]"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "_stakingAddress",
"type": "address"
}
],
"name": "validatorRewardPercent",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
}
]

@ -0,0 +1,2 @@
ABIs are taken from compiled contract JSONs in the `build/` directory of https://github.com/poanetwork/posdao-contracts.
Docs: https://poanetwork.github.io/posdao-contracts/docs/

@ -1,30 +1,31 @@
[ [
{ {
"constant": false, "constant": true,
"inputs": [], "inputs": [
"name": "newValidatorSet",
"outputs": [
{ {
"name": "", "name": "",
"type": "bool" "type": "address"
}, }
],
"name": "miningByStakingAddress",
"outputs": [
{ {
"name": "", "name": "",
"type": "uint256" "type": "address"
} }
], ],
"payable": false, "payable": false,
"stateMutability": "nonpayable", "stateMutability": "view",
"type": "function" "type": "function"
}, },
{ {
"constant": true, "constant": true,
"inputs": [], "inputs": [],
"name": "MAX_VALIDATORS", "name": "initiateChangeAllowed",
"outputs": [ "outputs": [
{ {
"name": "", "name": "",
"type": "uint256" "type": "bool"
} }
], ],
"payable": false, "payable": false,
@ -32,110 +33,75 @@
"type": "function" "type": "function"
}, },
{ {
"constant": true,
"inputs": [ "inputs": [
{ {
"indexed": true, "name": "",
"name": "parentHash", "type": "address"
"type": "bytes32" }
}, ],
"name": "banCounter",
"outputs": [
{ {
"indexed": false, "name": "",
"name": "newSet", "type": "uint256"
"type": "address[]"
} }
], ],
"name": "InitiateChange",
"type": "event",
"anonymous": false
},
{
"inputs": [],
"name": "clearUnremovableValidator",
"type": "function",
"constant": false,
"outputs": [],
"payable": false, "payable": false,
"stateMutability": "nonpayable" "stateMutability": "view",
}, "type": "function"
{
"inputs": [],
"name": "emitInitiateChange",
"type": "function",
"constant": false,
"outputs": [],
"payable": false,
"stateMutability": "nonpayable"
},
{
"inputs": [],
"name": "finalizeChange",
"type": "function",
"constant": false,
"outputs": [],
"payable": false,
"stateMutability": "nonpayable"
}, },
{ {
"constant": true,
"inputs": [ "inputs": [
{ {
"name": "_blockRewardContract", "name": "",
"type": "address"
},
{
"name": "_randomContract",
"type": "address" "type": "address"
}, }
],
"name": "stakingByMiningAddress",
"outputs": [
{ {
"name": "_stakingContract", "name": "",
"type": "address" "type": "address"
},
{
"name": "_initialMiningAddresses",
"type": "address[]"
},
{
"name": "_initialStakingAddresses",
"type": "address[]"
},
{
"name": "_firstValidatorIsUnremovable",
"type": "bool"
} }
], ],
"name": "initialize",
"type": "function",
"constant": false,
"outputs": [],
"payable": false, "payable": false,
"stateMutability": "nonpayable" "stateMutability": "view",
"type": "function"
}, },
{ {
"constant": true,
"inputs": [ "inputs": [
{ {
"name": "_miningAddress", "name": "",
"type": "address" "type": "address"
}, }
],
"name": "isValidatorOnPreviousEpoch",
"outputs": [
{ {
"name": "_stakingAddress", "name": "",
"type": "address" "type": "bool"
} }
], ],
"name": "setStakingAddress",
"type": "function",
"constant": false,
"outputs": [],
"payable": false, "payable": false,
"stateMutability": "nonpayable" "stateMutability": "view",
"type": "function"
}, },
{ {
"constant": true, "constant": true,
"inputs": [ "inputs": [
{ {
"name": "_miningAddress", "name": "",
"type": "address" "type": "address"
},
{
"name": "",
"type": "uint256"
} }
], ],
"name": "banCounter", "name": "reportingCounter",
"outputs": [ "outputs": [
{ {
"name": "", "name": "",
@ -146,11 +112,25 @@
"stateMutability": "view", "stateMutability": "view",
"type": "function" "type": "function"
}, },
{
"constant": true,
"inputs": [],
"name": "blockRewardContract",
"outputs": [
{
"name": "",
"type": "address"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{ {
"constant": true, "constant": true,
"inputs": [ "inputs": [
{ {
"name": "_miningAddress", "name": "",
"type": "address" "type": "address"
} }
], ],
@ -168,7 +148,7 @@
{ {
"constant": true, "constant": true,
"inputs": [], "inputs": [],
"name": "blockRewardContract", "name": "unremovableValidator",
"outputs": [ "outputs": [
{ {
"name": "", "name": "",
@ -182,7 +162,7 @@
{ {
"constant": true, "constant": true,
"inputs": [], "inputs": [],
"name": "changeRequestCount", "name": "MAX_VALIDATORS",
"outputs": [ "outputs": [
{ {
"name": "", "name": "",
@ -195,12 +175,17 @@
}, },
{ {
"constant": true, "constant": true,
"inputs": [], "inputs": [
"name": "emitInitiateChangeCallable", {
"name": "",
"type": "address"
}
],
"name": "validatorIndex",
"outputs": [ "outputs": [
{ {
"name": "", "name": "",
"type": "bool" "type": "uint256"
} }
], ],
"payable": false, "payable": false,
@ -209,12 +194,17 @@
}, },
{ {
"constant": true, "constant": true,
"inputs": [], "inputs": [
"name": "getPreviousValidators", {
"name": "",
"type": "address"
}
],
"name": "validatorCounter",
"outputs": [ "outputs": [
{ {
"name": "", "name": "",
"type": "address[]" "type": "uint256"
} }
], ],
"payable": false, "payable": false,
@ -224,11 +214,11 @@
{ {
"constant": true, "constant": true,
"inputs": [], "inputs": [],
"name": "getPendingValidators", "name": "validatorSetApplyBlock",
"outputs": [ "outputs": [
{ {
"name": "", "name": "",
"type": "address[]" "type": "uint256"
} }
], ],
"payable": false, "payable": false,
@ -238,15 +228,11 @@
{ {
"constant": true, "constant": true,
"inputs": [], "inputs": [],
"name": "getQueueValidators", "name": "randomContract",
"outputs": [ "outputs": [
{ {
"name": "", "name": "",
"type": "address[]" "type": "address"
},
{
"name": "",
"type": "bool"
} }
], ],
"payable": false, "payable": false,
@ -256,11 +242,11 @@
{ {
"constant": true, "constant": true,
"inputs": [], "inputs": [],
"name": "getValidators", "name": "changeRequestCount",
"outputs": [ "outputs": [
{ {
"name": "", "name": "",
"type": "address[]" "type": "uint256"
} }
], ],
"payable": false, "payable": false,
@ -269,12 +255,17 @@
}, },
{ {
"constant": true, "constant": true,
"inputs": [], "inputs": [
"name": "initiateChangeAllowed", {
"name": "",
"type": "uint256"
}
],
"name": "reportingCounterTotal",
"outputs": [ "outputs": [
{ {
"name": "", "name": "",
"type": "bool" "type": "uint256"
} }
], ],
"payable": false, "payable": false,
@ -285,15 +276,37 @@
"constant": true, "constant": true,
"inputs": [ "inputs": [
{ {
"name": "_miningAddress", "name": "",
"type": "address" "type": "address"
},
{
"name": "",
"type": "uint256"
},
{
"name": "",
"type": "uint256"
} }
], ],
"name": "isReportValidatorValid", "name": "maliceReportedForBlock",
"outputs": [ "outputs": [
{ {
"name": "", "name": "",
"type": "bool" "type": "address"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "stakingContract",
"outputs": [
{
"name": "",
"type": "address"
} }
], ],
"payable": false, "payable": false,
@ -304,7 +317,7 @@
"constant": true, "constant": true,
"inputs": [ "inputs": [
{ {
"name": "_miningAddress", "name": "",
"type": "address" "type": "address"
} }
], ],
@ -320,56 +333,185 @@
"type": "function" "type": "function"
}, },
{ {
"constant": true, "anonymous": false,
"inputs": [ "inputs": [
{ {
"name": "_miningAddress", "indexed": true,
"name": "parentHash",
"type": "bytes32"
},
{
"indexed": false,
"name": "newSet",
"type": "address[]"
}
],
"name": "InitiateChange",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"name": "reportingValidator",
"type": "address"
},
{
"indexed": false,
"name": "maliciousValidator",
"type": "address" "type": "address"
},
{
"indexed": false,
"name": "blockNumber",
"type": "uint256"
} }
], ],
"name": "isValidatorOnPreviousEpoch", "name": "ReportedMalicious",
"type": "event"
},
{
"constant": false,
"inputs": [],
"name": "clearUnremovableValidator",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [],
"name": "emitInitiateChange",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [],
"name": "finalizeChange",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_blockRewardContract",
"type": "address"
},
{
"name": "_randomContract",
"type": "address"
},
{
"name": "_stakingContract",
"type": "address"
},
{
"name": "_initialMiningAddresses",
"type": "address[]"
},
{
"name": "_initialStakingAddresses",
"type": "address[]"
},
{
"name": "_firstValidatorIsUnremovable",
"type": "bool"
}
],
"name": "initialize",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [],
"name": "newValidatorSet",
"outputs": [ "outputs": [
{ {
"name": "", "name": "called",
"type": "bool" "type": "bool"
},
{
"name": "poolsToBeElectedLength",
"type": "uint256"
} }
], ],
"payable": false, "payable": false,
"stateMutability": "view", "stateMutability": "nonpayable",
"type": "function" "type": "function"
}, },
{ {
"constant": true, "constant": false,
"inputs": [ "inputs": [
{ {
"name": "_miningAddress", "name": "_miningAddresses",
"type": "address" "type": "address[]"
} }
], ],
"name": "isValidatorBanned", "name": "removeMaliciousValidators",
"outputs": [ "outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_maliciousMiningAddress",
"type": "address"
},
{
"name": "_blockNumber",
"type": "uint256"
},
{ {
"name": "", "name": "",
"type": "bool" "type": "bytes"
} }
], ],
"name": "reportMalicious",
"outputs": [],
"payable": false, "payable": false,
"stateMutability": "view", "stateMutability": "nonpayable",
"type": "function" "type": "function"
}, },
{ {
"constant": true, "constant": false,
"inputs": [ "inputs": [
{
"name": "_miningAddress",
"type": "address"
},
{ {
"name": "_stakingAddress", "name": "_stakingAddress",
"type": "address" "type": "address"
} }
], ],
"name": "miningByStakingAddress", "name": "setStakingAddress",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "emitInitiateChangeCallable",
"outputs": [ "outputs": [
{ {
"name": "", "name": "",
"type": "address" "type": "bool"
} }
], ],
"payable": false, "payable": false,
@ -379,11 +521,11 @@
{ {
"constant": true, "constant": true,
"inputs": [], "inputs": [],
"name": "randomContract", "name": "getPreviousValidators",
"outputs": [ "outputs": [
{ {
"name": "", "name": "",
"type": "address" "type": "address[]"
} }
], ],
"payable": false, "payable": false,
@ -392,17 +534,30 @@
}, },
{ {
"constant": true, "constant": true,
"inputs": [ "inputs": [],
"name": "getPendingValidators",
"outputs": [
{ {
"name": "_miningAddress", "name": "",
"type": "address" "type": "address[]"
} }
], ],
"name": "stakingByMiningAddress", "payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "getQueueValidators",
"outputs": [ "outputs": [
{ {
"name": "", "name": "miningAddresses",
"type": "address" "type": "address[]"
},
{
"name": "newStakingEpoch",
"type": "bool"
} }
], ],
"payable": false, "payable": false,
@ -412,11 +567,11 @@
{ {
"constant": true, "constant": true,
"inputs": [], "inputs": [],
"name": "stakingContract", "name": "getValidators",
"outputs": [ "outputs": [
{ {
"name": "", "name": "",
"type": "address" "type": "address[]"
} }
], ],
"payable": false, "payable": false,
@ -426,11 +581,11 @@
{ {
"constant": true, "constant": true,
"inputs": [], "inputs": [],
"name": "unremovableValidator", "name": "isInitialized",
"outputs": [ "outputs": [
{ {
"name": "stakingAddress", "name": "",
"type": "address" "type": "bool"
} }
], ],
"payable": false, "payable": false,
@ -445,11 +600,11 @@
"type": "address" "type": "address"
} }
], ],
"name": "validatorCounter", "name": "isReportValidatorValid",
"outputs": [ "outputs": [
{ {
"name": "", "name": "",
"type": "uint256" "type": "bool"
} }
], ],
"payable": false, "payable": false,
@ -464,11 +619,11 @@
"type": "address" "type": "address"
} }
], ],
"name": "validatorIndex", "name": "isValidatorBanned",
"outputs": [ "outputs": [
{ {
"name": "", "name": "",
"type": "uint256" "type": "bool"
} }
], ],
"payable": false, "payable": false,
@ -477,14 +632,31 @@
}, },
{ {
"constant": true, "constant": true,
"inputs": [], "inputs": [
"name": "validatorSetApplyBlock",
"outputs": [
{ {
"name": "", "name": "_reportingMiningAddress",
"type": "address"
},
{
"name": "_maliciousMiningAddress",
"type": "address"
},
{
"name": "_blockNumber",
"type": "uint256" "type": "uint256"
} }
], ],
"name": "reportMaliciousCallable",
"outputs": [
{
"name": "callable",
"type": "bool"
},
{
"name": "removeReportingValidator",
"type": "bool"
}
],
"payable": false, "payable": false,
"stateMutability": "view", "stateMutability": "view",
"type": "function" "type": "function"

@ -0,0 +1,10 @@
defmodule Explorer.Repo.Migrations.DelegatorsDeletedColum do
use Ecto.Migration
def change do
alter table(:staking_pools_delegators) do
add(:is_active, :boolean, default: true)
add(:is_deleted, :boolean, default: false)
end
end
end

@ -0,0 +1,9 @@
defmodule Explorer.Repo.Migrations.AddBlockRewardToPools do
use Ecto.Migration
def change do
alter table(:staking_pools) do
add(:block_reward_ratio, :decimal, precision: 5, scale: 2)
end
end
end

@ -10,7 +10,7 @@ defmodule Explorer.Chain.Import.Runner.StakingPoolsTest do
describe "run/1" do describe "run/1" do
test "insert new pools list" do test "insert new pools list" do
pools = pools =
[pool1, pool2] = [_pool1, _pool2] =
[params_for(:staking_pool), params_for(:staking_pool)] [params_for(:staking_pool), params_for(:staking_pool)]
|> Enum.map(fn param -> |> Enum.map(fn param ->
changeset = StakingPool.changeset(%StakingPool{}, param) changeset = StakingPool.changeset(%StakingPool{}, param)
@ -19,16 +19,6 @@ defmodule Explorer.Chain.Import.Runner.StakingPoolsTest do
assert {:ok, %{insert_staking_pools: list}} = run_changes(pools) assert {:ok, %{insert_staking_pools: list}} = run_changes(pools)
assert Enum.count(list) == Enum.count(pools) assert Enum.count(list) == Enum.count(pools)
saved_list =
Explorer.Chain.StakingPool
|> Repo.all()
|> Enum.reduce(%{}, fn pool, acc ->
Map.put(acc, pool.staking_address_hash, pool)
end)
assert saved_list[pool1.staking_address_hash].staked_ratio == Decimal.new("50.00")
assert saved_list[pool2.staking_address_hash].staked_ratio == Decimal.new("50.00")
end end
end end

@ -0,0 +1,208 @@
defmodule Explorer.Staking.ContractStateTest do
use EthereumJSONRPC.Case
use Explorer.DataCase
import Mox
alias Explorer.Chain.{StakingPool, StakingPoolsDelegator}
alias Explorer.Chain.Events.Publisher
alias Explorer.Repo
alias Explorer.Staking.ContractState
setup :verify_on_exit!
setup :set_mox_global
test "when disabled, returns default values" do
assert ContractState.get(:epoch_number, 0) == 0
assert ContractState.get(:epoch_end_block, 0) == 0
assert ContractState.get(:min_delegator_stake, 1) == 1
assert ContractState.get(:min_candidate_stake, 1) == 1
assert ContractState.get(:token_contract_address) == nil
end
test "fetch new epoch data" do
set_init_mox()
set_mox()
Application.put_env(:explorer, ContractState,
enabled: true,
staking_contract_address: "0x1100000000000000000000000000000000000001"
)
start_supervised!(ContractState)
set_mox()
Publisher.broadcast([{:blocks, [%Explorer.Chain.Block{number: 6000}]}], :realtime)
Publisher.broadcast([{:blocks, [%Explorer.Chain.Block{number: 5999}]}], :realtime)
Publisher.broadcast([{:blocks, [%Explorer.Chain.Block{number: 6000}]}], :realtime)
set_mox()
Publisher.broadcast([{:blocks, [%Explorer.Chain.Block{number: 6001}]}], :realtime)
Process.sleep(500)
assert ContractState.get(:epoch_number) == 74
assert ContractState.get(:epoch_end_block) == 6000
assert ContractState.get(:min_delegator_stake) == 1_000_000_000_000_000_000
assert ContractState.get(:min_candidate_stake) == 1_000_000_000_000_000_000
assert ContractState.get(:token_contract_address) == "0x6f7a73c96bd56f8b0debc795511eda135e105ea3"
assert Repo.aggregate(StakingPool, :count, :id) == 4
assert Repo.aggregate(StakingPoolsDelegator, :count, :id) == 3
end
defp set_init_mox() do
expect(
EthereumJSONRPC.Mox,
:json_rpc,
fn requests, _opts ->
assert length(requests) == 1
{:ok, format_responses(["0x0000000000000000000000001000000000000000000000000000000000000001"])}
end
)
expect(
EthereumJSONRPC.Mox,
:json_rpc,
fn requests, _opts ->
assert length(requests) == 1
{:ok, format_responses(["0x0000000000000000000000002000000000000000000000000000000000000001"])}
end
)
end
defp set_mox() do
expect(
EthereumJSONRPC.Mox,
:json_rpc,
fn requests, _opts ->
assert length(requests) == 10
{:ok,
format_responses([
"0x0000000000000000000000006f7a73c96bd56f8b0debc795511eda135e105ea3",
"0x0000000000000000000000000000000000000000000000000de0b6b3a7640000",
"0x0000000000000000000000000000000000000000000000000de0b6b3a7640000",
"0x000000000000000000000000000000000000000000000000000000000000004a",
"0x0000000000000000000000000000000000000000000000000000000000001770",
"0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000030000000000000000000000000b2f5e2f3cbd864eaa2c642e3769c1582361caf6000000000000000000000000b916e7e1f4bcb13549602ed042d36746fd0d96c9000000000000000000000000db9cb2478d917719c53862008672166808258577",
"0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000001000000000000000000000000b6695f5c2e3f5eff8036b5f5f3a9d83a5310e51e",
"0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000002000000000000000000000000b916e7e1f4bcb13549602ed042d36746fd0d96c9000000000000000000000000db9cb2478d917719c53862008672166808258577",
"0x00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000514000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000c8000000000000000000000000000000000000000000000000000000000000044c",
"0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003000000000000000000000000bbcaa8d48289bb1ffcf9808d9aa4b1d215054c78000000000000000000000000f67cc5231c5858ad6cc87b105217426e17b824bb000000000000000000000000be69eb0968226a1808975e1a1f2127667f2bffb3"
])}
end
)
expect(
EthereumJSONRPC.Mox,
:json_rpc,
fn requests, _opts ->
assert length(requests) == 28
{:ok,
format_responses([
"0x000000000000000000000000bbcaa8d48289bb1ffcf9808d9aa4b1d215054c78",
"0x0000000000000000000000000000000000000000000000000000000000000001",
"0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000",
"0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000",
"0x0000000000000000000000000000000000000000000000000000000000000000",
"0x0000000000000000000000000000000000000000000000000000000000000000",
"0x000000000000000000000000000000000000000000000000000000000003d090",
"0x000000000000000000000000f67cc5231c5858ad6cc87b105217426e17b824bb",
"0x0000000000000000000000000000000000000000000000000000000000000001",
"0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000",
"0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000",
"0x0000000000000000000000000000000000000000000000001bc16d674ec80000",
"0x0000000000000000000000000000000000000000000000001bc16d674ec80000",
"0x0000000000000000000000000000000000000000000000000000000000051615",
"0x000000000000000000000000be69eb0968226a1808975e1a1f2127667f2bffb3",
"0x0000000000000000000000000000000000000000000000000000000000000001",
"0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000200000000000000000000000009d99f80d3b59cca783f11918311fb31212fb7500000000000000000000000008d6867958e1cab5c39160a1d30fbc68ac55b45ef",
"0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000",
"0x00000000000000000000000000000000000000000000000098a7d9b8314c0000",
"0x0000000000000000000000000000000000000000000000001bc16d674ec80000",
"0x0000000000000000000000000000000000000000000000000000000000051615",
"0x000000000000000000000000720e118ab1006cc97ed2ef6b4b49ac04bb3aa6d9",
"0x0000000000000000000000000000000000000000000000000000000000000000",
"0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000001000000000000000000000000e4978fac7adfc925352dbc7e1962e6545142eeee",
"0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000",
"0x00000000000000000000000000000000000000000000000029a2241af62c0000",
"0x0000000000000000000000000000000000000000000000001bc16d674ec80000",
"0x0000000000000000000000000000000000000000000000000000000000051615"
])}
end
)
expect(
EthereumJSONRPC.Mox,
:json_rpc,
fn requests, _opts ->
assert length(requests) == 20
{:ok,
format_responses([
"0x0000000000000000000000000000000000000000000000000000000000000001",
"0x000000000000000000000000000000000000000000000000000000000000004b",
"0x0000000000000000000000000000000000000000000000000000000000000000",
"0x0000000000000000000000000000000000000000000000000000000000000000",
"0x0000000000000000000000000000000000000000000000000000000000000000",
"0x0000000000000000000000000000000000000000000000000000000000000000",
"0x0000000000000000000000000000000000000000000000000000000000000002",
"0x0000000000000000000000000000000000000000000000000000000000000000",
"0x0000000000000000000000000000000000000000000000000000000000000000",
"0x0000000000000000000000000000000000000000000000000000000000000000",
"0x0000000000000000000000000000000000000000000000000000000000000001",
"0x000000000000000000000000000000000000000000000000000000000000004a",
"0x0000000000000000000000000000000000000000000000000000000000000000",
"0x0000000000000000000000000000000000000000000000000000000000000000",
"0x0000000000000000000000000000000000000000000000000000000000000000",
"0x0000000000000000000000000000000000000000000000000000000000000001",
"0x000000000000000000000000000000000000000000000000000000000000004a",
"0x0000000000000000000000000000000000000000000000000000000000000000",
"0x0000000000000000000000000000000000000000000000000000000000000000",
"0x0000000000000000000000000000000000000000000000000000000000000000"
])}
end
)
expect(
EthereumJSONRPC.Mox,
:json_rpc,
fn requests, _opts ->
assert length(requests) == 15
{:ok,
format_responses([
"0x0000000000000000000000000000000000000000000000000de0b6b3a7640000",
"0x0000000000000000000000000000000000000000000000000000000000000000",
"0x0000000000000000000000000000000000000000000000000000000000000000",
"0x0000000000000000000000000000000000000000000000000000000000000000",
"0x0000000000000000000000000000000000000000000000000000000000000000",
"0x0000000000000000000000000000000000000000000000000de0b6b3a7640000",
"0x0000000000000000000000000000000000000000000000000000000000000000",
"0x0000000000000000000000000000000000000000000000000000000000000000",
"0x0000000000000000000000000000000000000000000000000000000000000000",
"0x0000000000000000000000000000000000000000000000000000000000000000",
"0x0000000000000000000000000000000000000000000000000de0b6b3a7640000",
"0x0000000000000000000000000000000000000000000000000000000000000000",
"0x0000000000000000000000000000000000000000000000000000000000000000",
"0x0000000000000000000000000000000000000000000000000000000000000000",
"0x0000000000000000000000000000000000000000000000000000000000000000"
])}
end
)
end
defp format_responses(responses) do
responses
|> Enum.with_index()
|> Enum.map(fn {response, index} ->
%{
id: index,
jsonrpc: "2.0",
result: response
}
end)
end
end

@ -1,97 +0,0 @@
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

@ -1,361 +0,0 @@
defmodule Explorer.Token.PoolsReaderTest do
use EthereumJSONRPC.Case
alias Explorer.Staking.PoolsReader
import Mox
setup :verify_on_exit!
setup :set_mox_global
describe "get_pools_list" do
test "get_active_pools success" do
get_pools_from_blockchain()
result = PoolsReader.get_active_pools()
assert Enum.count(result) == 3
end
test "get_active_pools error" do
fetch_from_blockchain_with_error()
assert_raise MatchError, fn ->
PoolsReader.get_active_pools()
end
end
end
describe "get_pools_data" do
test "get_pool_data success" do
get_pool_data_from_blockchain()
address = <<219, 156, 178, 71, 141, 145, 119, 25, 197, 56, 98, 0, 134, 114, 22, 104, 8, 37, 133, 119>>
response =
{:ok,
%{
banned_until: 0,
is_active: true,
is_banned: false,
is_validator: true,
was_banned_count: 0,
was_validator_count: 2,
delegators: [
%{
delegator_address_hash:
<<243, 231, 124, 74, 245, 235, 47, 51, 175, 255, 118, 25, 216, 209, 231, 81, 215, 24, 164, 145>>,
max_ordered_withdraw_allowed: 1_000_000_000_000_000_000,
max_withdraw_allowed: 1_000_000_000_000_000_000,
ordered_withdraw: 0,
ordered_withdraw_epoch: 0,
pool_address_hash:
<<219, 156, 178, 71, 141, 145, 119, 25, 197, 56, 98, 0, 134, 114, 22, 104, 8, 37, 133, 119>>,
stake_amount: 1_000_000_000_000_000_000
}
],
delegators_count: 1,
mining_address_hash:
<<190, 105, 235, 9, 104, 34, 106, 24, 8, 151, 94, 26, 31, 33, 39, 102, 127, 43, 255, 179>>,
self_staked_amount: 2_000_000_000_000_000_000,
staked_amount: 3_000_000_000_000_000_000,
staking_address_hash:
<<219, 156, 178, 71, 141, 145, 119, 25, 197, 56, 98, 0, 134, 114, 22, 104, 8, 37, 133, 119>>
}}
assert PoolsReader.pool_data(address) == response
end
test "get_pool_data error" do
fetch_from_blockchain_with_error()
address = <<11, 47, 94, 47, 60, 189, 134, 78, 170, 44, 100, 46, 55, 105, 193, 88, 35, 97, 202, 246>>
assert :error = PoolsReader.pool_data(address)
end
end
defp get_pools_from_blockchain() do
expect(
EthereumJSONRPC.Mox,
:json_rpc,
fn [%{id: id, method: "eth_call", params: _}], _options ->
{:ok,
[
%{
id: id,
jsonrpc: "2.0",
result:
"0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000030000000000000000000000000b2f5e2f3cbd864eaa2c642e3769c1582361caf6000000000000000000000000aa94b687d3f9552a453b81b2834ca53778980dc0000000000000000000000000312c230e7d6db05224f60208a656e3541c5c42ba"
}
]}
end
)
end
defp fetch_from_blockchain_with_error() do
expect(
EthereumJSONRPC.Mox,
:json_rpc,
fn [%{id: id, method: "eth_call", params: _}], _options ->
{:ok,
[
%{
error: %{code: -32015, data: "Reverted 0x", message: "VM execution error."},
id: id,
jsonrpc: "2.0"
}
]}
end
)
end
defp get_pool_data_from_blockchain() do
expect(
EthereumJSONRPC.Mox,
:json_rpc,
3,
fn requests, _opts ->
{:ok,
Enum.map(requests, fn
# miningByStakingAddress
%{
id: id,
method: "eth_call",
params: [
%{data: "0x00535175000000000000000000000000db9cb2478d917719c53862008672166808258577", to: _},
"latest"
]
} ->
%{
id: id,
result: "0x000000000000000000000000be69eb0968226a1808975e1a1f2127667f2bffb3"
}
# isPoolActive
%{
id: id,
method: "eth_call",
params: [
%{data: "0xa711e6a1000000000000000000000000db9cb2478d917719c53862008672166808258577", to: _},
"latest"
]
} ->
%{
id: id,
result: "0x0000000000000000000000000000000000000000000000000000000000000001"
}
# poolDelegators
%{
id: id,
method: "eth_call",
params: [
%{data: "0x9ea8082b000000000000000000000000db9cb2478d917719c53862008672166808258577", to: _},
"latest"
]
} ->
%{
id: id,
result:
"0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000001000000000000000000000000f3e77c4af5eb2f33afff7619d8d1e751d718a491"
}
# stakeAmountTotalMinusOrderedWithdraw
%{
id: id,
method: "eth_call",
params: [
%{data: "0x234fbf2b000000000000000000000000db9cb2478d917719c53862008672166808258577", to: _},
"latest"
]
} ->
%{
id: id,
result: "0x00000000000000000000000000000000000000000000000029a2241af62c0000"
}
# stakeAmountMinusOrderedWithdraw
%{
id: id,
jsonrpc: "2.0",
method: "eth_call",
params: [
%{
data:
"0x58daab6a000000000000000000000000db9cb2478d917719c53862008672166808258577000000000000000000000000db9cb2478d917719c53862008672166808258577",
to: _
},
"latest"
]
} ->
%{
id: id,
result: "0x0000000000000000000000000000000000000000000000001bc16d674ec80000"
}
# isValidator
%{
id: id,
method: "eth_call",
params: [
%{data: "0xfacd743b000000000000000000000000be69eb0968226a1808975e1a1f2127667f2bffb3", to: _},
"latest"
]
} ->
%{
id: id,
result: "0x0000000000000000000000000000000000000000000000000000000000000001"
}
# validatorCounter
%{
id: id,
method: "eth_call",
params: [
%{data: "0xb41832e4000000000000000000000000be69eb0968226a1808975e1a1f2127667f2bffb3", to: _},
"latest"
]
} ->
%{
id: id,
result: "0x0000000000000000000000000000000000000000000000000000000000000002"
}
# isValidatorBanned
%{
id: id,
method: "eth_call",
params: [
%{data: "0xa92252ae000000000000000000000000be69eb0968226a1808975e1a1f2127667f2bffb3", to: _},
"latest"
]
} ->
%{
id: id,
result: "0x0000000000000000000000000000000000000000000000000000000000000000"
}
# bannedUntil
%{
id: id,
method: "eth_call",
params: [
%{data: "0x5836d08a000000000000000000000000be69eb0968226a1808975e1a1f2127667f2bffb3", to: _},
"latest"
]
} ->
%{
id: id,
result: "0x0000000000000000000000000000000000000000000000000000000000000000"
}
# banCounter
%{
id: id,
method: "eth_call",
params: [
%{data: "0x1d0cd4c6000000000000000000000000be69eb0968226a1808975e1a1f2127667f2bffb3", to: _},
"latest"
]
} ->
%{
id: id,
result: "0x0000000000000000000000000000000000000000000000000000000000000000"
}
# DELEGATOR
# stakeAmount
%{
id: id,
method: "eth_call",
params: [
%{
data:
"0xa697ecff000000000000000000000000db9cb2478d917719c53862008672166808258577000000000000000000000000f3e77c4af5eb2f33afff7619d8d1e751d718a491",
to: _
},
"latest"
]
} ->
%{
id: id,
result: "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000"
}
# orderedWithdrawAmount
%{
id: id,
method: "eth_call",
params: [
%{
data:
"0xe9ab0300000000000000000000000000db9cb2478d917719c53862008672166808258577000000000000000000000000f3e77c4af5eb2f33afff7619d8d1e751d718a491",
to: _
},
"latest"
]
} ->
%{
id: id,
result: "0x0000000000000000000000000000000000000000000000000000000000000000"
}
# maxWithdrawAllowed
%{
id: id,
method: "eth_call",
params: [
%{
data:
"0x6bda1577000000000000000000000000db9cb2478d917719c53862008672166808258577000000000000000000000000f3e77c4af5eb2f33afff7619d8d1e751d718a491",
to: _
},
"latest"
]
} ->
%{
id: id,
result: "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000"
}
# maxWithdrawOrderAllowed
%{
id: id,
method: "eth_call",
params: [
%{
data:
"0x950a6513000000000000000000000000db9cb2478d917719c53862008672166808258577000000000000000000000000f3e77c4af5eb2f33afff7619d8d1e751d718a491",
to: _
},
"latest"
]
} ->
%{
id: id,
result: "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000"
}
# orderWithdrawEpoch
%{
id: id,
method: "eth_call",
params: [
%{
data:
"0xa4205967000000000000000000000000db9cb2478d917719c53862008672166808258577000000000000000000000000f3e77c4af5eb2f33afff7619d8d1e751d718a491",
to: _
},
"latest"
]
} ->
%{
id: id,
result: "0x0000000000000000000000000000000000000000000000000000000000000000"
}
end)}
end
)
end
end

@ -42,8 +42,9 @@ config :indexer, Indexer.Fetcher.PendingTransaction.Supervisor,
disabled?: System.get_env("ETHEREUM_JSONRPC_VARIANT") == "besu" disabled?: System.get_env("ETHEREUM_JSONRPC_VARIANT") == "besu"
# config :indexer, Indexer.Fetcher.ReplacedTransaction.Supervisor, disabled?: true # config :indexer, Indexer.Fetcher.ReplacedTransaction.Supervisor, disabled?: true
# config :indexer, Indexer.Fetcher.BlockReward.Supervisor, disabled?: true if System.get_env("POS_STAKING_CONTRACT") do
config :indexer, Indexer.Fetcher.StakingPools.Supervisor, disabled?: true config :indexer, Indexer.Fetcher.BlockReward.Supervisor, disabled?: true
end
config :indexer, Indexer.Supervisor, enabled: System.get_env("DISABLE_INDEXER") != "true" config :indexer, Indexer.Supervisor, enabled: System.get_env("DISABLE_INDEXER") != "true"

@ -23,7 +23,6 @@ defmodule Indexer.Block.Fetcher do
ContractCode, ContractCode,
InternalTransaction, InternalTransaction,
ReplacedTransaction, ReplacedTransaction,
StakingPools,
Token, Token,
TokenBalance, TokenBalance,
TokenInstance, TokenInstance,
@ -307,10 +306,6 @@ defmodule Indexer.Block.Fetcher do
def async_import_token_balances(_), do: :ok def async_import_token_balances(_), do: :ok
def async_import_staking_pools do
StakingPools.async_fetch()
end
def async_import_uncles(%{block_second_degree_relations: block_second_degree_relations}) do def async_import_uncles(%{block_second_degree_relations: block_second_degree_relations}) do
UncleBlock.async_fetch_blocks(block_second_degree_relations) UncleBlock.async_fetch_blocks(block_second_degree_relations)
end end

@ -21,8 +21,7 @@ defmodule Indexer.Block.Realtime.Fetcher do
async_import_token_balances: 1, async_import_token_balances: 1,
async_import_token_instances: 1, async_import_token_instances: 1,
async_import_uncles: 1, async_import_uncles: 1,
fetch_and_import_range: 2, fetch_and_import_range: 2
async_import_staking_pools: 0
] ]
alias Ecto.Changeset alias Ecto.Changeset
@ -369,7 +368,6 @@ defmodule Indexer.Block.Realtime.Fetcher do
async_import_token_instances(imported) async_import_token_instances(imported)
async_import_uncles(imported) async_import_uncles(imported)
async_import_replaced_transactions(imported) async_import_replaced_transactions(imported)
async_import_staking_pools()
end end
defp balances( defp balances(

@ -1,140 +0,0 @@
defmodule Indexer.Fetcher.StakingPools do
@moduledoc """
Fetches staking pools and send to be imported in `Address.Name` table
"""
use Indexer.Fetcher
use Spandex.Decorators
require Logger
alias Explorer.Chain
alias Explorer.Chain.StakingPool
alias Explorer.Staking.PoolsReader
alias Indexer.BufferedTask
alias Indexer.Fetcher.StakingPools.Supervisor, as: StakingPoolsSupervisor
@behaviour BufferedTask
@defaults [
flush_interval: 300,
max_batch_size: 100,
max_concurrency: 10,
task_supervisor: Indexer.Fetcher.StakingPools.TaskSupervisor
]
@max_retries 3
@spec async_fetch() :: :ok
def async_fetch do
if StakingPoolsSupervisor.disabled?() do
:ok
else
pools =
PoolsReader.get_pools()
|> Enum.map(&entry/1)
BufferedTask.buffer(__MODULE__, pools, :infinity)
end
end
@doc false
def child_spec([init_options, gen_server_options]) do
merged_init_opts =
@defaults
|> Keyword.merge(init_options)
|> Keyword.put(:state, {0, []})
Supervisor.child_spec({BufferedTask, [{__MODULE__, merged_init_opts}, gen_server_options]}, id: __MODULE__)
end
@impl BufferedTask
def init(_initial, reducer, acc) do
PoolsReader.get_pools()
|> Enum.map(&entry/1)
|> Enum.reduce(acc, &reducer.(&1, &2))
end
@impl BufferedTask
def run(pools, _json_rpc_named_arguments) do
failed_list =
pools
|> Enum.map(&Map.put(&1, :retries_count, &1.retries_count + 1))
|> fetch_from_blockchain()
|> import_pools()
if failed_list == [] do
:ok
else
{:retry, failed_list}
end
end
def entry(pool_address) do
%{
staking_address_hash: pool_address,
retries_count: 0
}
end
defp fetch_from_blockchain(addresses) do
addresses
|> Enum.filter(&(&1.retries_count <= @max_retries))
|> Enum.map(fn %{staking_address_hash: staking_address} = pool ->
case PoolsReader.pool_data(staking_address) do
{:ok, data} ->
Map.merge(pool, data)
error ->
Map.put(pool, :error, error)
end
end)
end
defp import_pools(pools) do
{failed, success} =
Enum.reduce(pools, {[], []}, fn
%{error: _error} = pool, {failed, success} ->
{[pool | failed], success}
pool, {failed, success} ->
changeset = StakingPool.changeset(%StakingPool{}, pool)
if changeset.valid? do
{failed, [changeset.changes | success]}
else
{[pool | failed], success}
end
end)
import_params = %{
staking_pools: %{params: remove_assoc(success)},
staking_pools_delegators: %{params: delegators_list(success)},
timeout: :infinity
}
case Chain.import(import_params) do
{:ok, _} ->
:ok
{:error, reason} ->
Logger.debug(fn -> ["failed to import staking pools: ", inspect(reason)] end,
error_count: Enum.count(pools)
)
end
failed
end
defp delegators_list(pools) do
Enum.reduce(pools, [], fn pool, acc ->
pool.delegators
|> Enum.map(&Map.get(&1, :changes))
|> Enum.concat(acc)
end)
end
defp remove_assoc(pools) do
Enum.map(pools, &Map.delete(&1, :delegators))
end
end

@ -16,7 +16,6 @@ defmodule Indexer.Supervisor do
InternalTransaction, InternalTransaction,
PendingTransaction, PendingTransaction,
ReplacedTransaction, ReplacedTransaction,
StakingPools,
Token, Token,
TokenBalance, TokenBalance,
TokenInstance, TokenInstance,
@ -114,7 +113,6 @@ defmodule Indexer.Supervisor do
{TokenBalance.Supervisor, [[json_rpc_named_arguments: json_rpc_named_arguments, memory_monitor: memory_monitor]]}, {TokenBalance.Supervisor, [[json_rpc_named_arguments: json_rpc_named_arguments, memory_monitor: memory_monitor]]},
{TokenUpdater.Supervisor, [[json_rpc_named_arguments: json_rpc_named_arguments, memory_monitor: memory_monitor]]}, {TokenUpdater.Supervisor, [[json_rpc_named_arguments: json_rpc_named_arguments, memory_monitor: memory_monitor]]},
{ReplacedTransaction.Supervisor, [[memory_monitor: memory_monitor]]}, {ReplacedTransaction.Supervisor, [[memory_monitor: memory_monitor]]},
{StakingPools.Supervisor, [[memory_monitor: memory_monitor]]},
# Out-of-band fetchers # Out-of-band fetchers
{CoinBalanceOnDemand.Supervisor, [json_rpc_named_arguments]}, {CoinBalanceOnDemand.Supervisor, [json_rpc_named_arguments]},

@ -1,224 +0,0 @@
defmodule Indexer.Fetcher.StakingPoolsTest do
use EthereumJSONRPC.Case
use Explorer.DataCase
import Mox
alias Indexer.Fetcher.StakingPools
alias Explorer.Staking.PoolsReader
alias Explorer.Chain.StakingPool
@moduletag :capture_log
setup :verify_on_exit!
describe "init/3" do
test "returns pools addresses" do
get_pools_from_blockchain(2)
list = StakingPools.init([], &[&1 | &2], [])
assert Enum.count(list) == 6
end
end
describe "run/3" do
test "one success import from pools" do
get_pools_from_blockchain(1)
list =
PoolsReader.get_active_pools()
|> Enum.map(&StakingPools.entry/1)
success_address =
list
|> List.first()
|> Map.get(:staking_address_hash)
get_pool_data_from_blockchain()
assert {:retry, retry_list} = StakingPools.run(list, nil)
assert Enum.count(retry_list) == 2
pool = Explorer.Repo.get_by(StakingPool, staking_address_hash: success_address)
assert pool.is_active == true
end
end
defp get_pools_from_blockchain(n) do
expect(
EthereumJSONRPC.Mox,
:json_rpc,
n,
fn [%{id: id, method: "eth_call", params: _}], _options ->
{:ok,
[
%{
id: id,
jsonrpc: "2.0",
result:
"0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000030000000000000000000000000b2f5e2f3cbd864eaa2c642e3769c1582361caf6000000000000000000000000aa94b687d3f9552a453b81b2834ca53778980dc0000000000000000000000000312c230e7d6db05224f60208a656e3541c5c42ba"
}
]}
end
)
end
defp get_pool_data_from_blockchain() do
expect(
EthereumJSONRPC.Mox,
:json_rpc,
4,
fn requests, _opts ->
{:ok,
Enum.map(requests, fn
# miningByStakingAddress
%{
id: id,
method: "eth_call",
params: [
%{data: "0x005351750000000000000000000000000b2f5e2f3cbd864eaa2c642e3769c1582361caf6", to: _},
"latest"
]
} ->
%{
id: id,
result: "0x000000000000000000000000bbcaa8d48289bb1ffcf9808d9aa4b1d215054c78"
}
# isPoolActive
%{
id: id,
method: "eth_call",
params: [
%{data: "0xa711e6a10000000000000000000000000b2f5e2f3cbd864eaa2c642e3769c1582361caf6", to: _},
"latest"
]
} ->
%{
id: id,
result: "0x0000000000000000000000000000000000000000000000000000000000000001"
}
# poolDelegators
%{
id: id,
method: "eth_call",
params: [
%{data: "0x9ea8082b0000000000000000000000000b2f5e2f3cbd864eaa2c642e3769c1582361caf6", to: _},
"latest"
]
} ->
%{
id: id,
result:
"0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000"
}
# stakeAmountTotalMinusOrderedWithdraw
%{
id: id,
method: "eth_call",
params: [
%{data: "0x234fbf2b0000000000000000000000000b2f5e2f3cbd864eaa2c642e3769c1582361caf6", to: _},
"latest"
]
} ->
%{
id: id,
result: "0x0000000000000000000000000000000000000000000000000000000000000000"
}
# stakeAmountMinusOrderedWithdraw
%{
id: id,
jsonrpc: "2.0",
method: "eth_call",
params: [
%{
data:
"0x58daab6a0000000000000000000000000b2f5e2f3cbd864eaa2c642e3769c1582361caf60000000000000000000000000b2f5e2f3cbd864eaa2c642e3769c1582361caf6",
to: _
},
"latest"
]
} ->
%{
id: id,
result: "0x0000000000000000000000000000000000000000000000000000000000000000"
}
# isValidator
%{
id: id,
method: "eth_call",
params: [
%{data: "0xfacd743b000000000000000000000000bbcaa8d48289bb1ffcf9808d9aa4b1d215054c78", to: _},
"latest"
]
} ->
%{
id: id,
result: "0x0000000000000000000000000000000000000000000000000000000000000001"
}
# validatorCounter
%{
id: id,
method: "eth_call",
params: [
%{data: "0xb41832e4000000000000000000000000bbcaa8d48289bb1ffcf9808d9aa4b1d215054c78", to: _},
"latest"
]
} ->
%{
id: id,
result: "0x0000000000000000000000000000000000000000000000000000000000000002"
}
# isValidatorBanned
%{
id: id,
method: "eth_call",
params: [
%{data: "0xa92252ae000000000000000000000000bbcaa8d48289bb1ffcf9808d9aa4b1d215054c78", to: _},
"latest"
]
} ->
%{
id: id,
result: "0x0000000000000000000000000000000000000000000000000000000000000000"
}
# bannedUntil
%{
id: id,
method: "eth_call",
params: [
%{data: "0x5836d08a000000000000000000000000bbcaa8d48289bb1ffcf9808d9aa4b1d215054c78", to: _},
"latest"
]
} ->
%{
id: id,
result: "0x0000000000000000000000000000000000000000000000000000000000000000"
}
# banCounter
%{
id: id,
method: "eth_call",
params: [
%{data: "0x1d0cd4c6000000000000000000000000bbcaa8d48289bb1ffcf9808d9aa4b1d215054c78", to: _},
"latest"
]
} ->
%{
id: id,
result: "0x0000000000000000000000000000000000000000000000000000000000000000"
}
end)}
end
)
end
end
Loading…
Cancel
Save