The number of token holders is now available in the token page again. It is cached by the TokenHoldersCounter GenServer that periodically updateds an ETS with all pairs of tokens and token holders.pull/904/head
parent
234128a923
commit
ea3da7fd07
@ -0,0 +1,108 @@ |
||||
defmodule Explorer.Counters.TokenHoldersCounter do |
||||
use GenServer |
||||
|
||||
@moduledoc """ |
||||
Caches the number of token holders of a token. |
||||
""" |
||||
|
||||
alias Explorer.Chain.Address.TokenBalance |
||||
alias Explorer.Repo |
||||
|
||||
@table :token_holders_counter |
||||
|
||||
config = Application.get_env(:explorer, Explorer.Counters.TokenHoldersCounter) |
||||
@enable_consolidation Keyword.get(config, :enable_consolidation) |
||||
|
||||
def table_name do |
||||
@table |
||||
end |
||||
|
||||
@doc """ |
||||
Starts a process to periodically update the counter of the token holders. |
||||
""" |
||||
@spec start_link(term()) :: GenServer.on_start() |
||||
def start_link(_) do |
||||
GenServer.start_link(__MODULE__, :ok, name: __MODULE__) |
||||
end |
||||
|
||||
## Server |
||||
@impl true |
||||
def init(args) do |
||||
create_table() |
||||
|
||||
if enable_consolidation?() do |
||||
Task.start_link(&consolidate/0) |
||||
schedule_next_consolidation() |
||||
end |
||||
|
||||
{:ok, args} |
||||
end |
||||
|
||||
def create_table do |
||||
opts = [ |
||||
:set, |
||||
:named_table, |
||||
:public, |
||||
read_concurrency: true |
||||
] |
||||
|
||||
:ets.new(table_name(), opts) |
||||
end |
||||
|
||||
@doc """ |
||||
Consolidates the token holders info, by populating the `:ets` table with the current database information. |
||||
""" |
||||
def consolidate do |
||||
TokenBalance.tokens_grouped_by_number_of_holders() |
||||
|> Repo.all() |
||||
|> Enum.map(fn {token, number_of_holders} -> |
||||
{token.bytes, number_of_holders} |
||||
end) |
||||
|> insert_counter() |
||||
end |
||||
|
||||
defp schedule_next_consolidation do |
||||
if enable_consolidation?() do |
||||
# Schedule next consolidation to be run in 30 minutes |
||||
Process.send_after(self(), :consolidate, 30 * 60 * 1000) |
||||
end |
||||
end |
||||
|
||||
@doc """ |
||||
Fetches the token holders info for a specific token from the `:ets` table. |
||||
""" |
||||
def fetch(token_hash) do |
||||
do_fetch(:ets.lookup(table_name(), token_hash.bytes)) |
||||
end |
||||
|
||||
defp do_fetch([{_, result}]), do: result |
||||
defp do_fetch([]), do: 0 |
||||
|
||||
@doc """ |
||||
Inserts new items into the `:ets` table. |
||||
""" |
||||
def insert_counter(token_holders) do |
||||
:ets.insert(table_name(), token_holders) |
||||
end |
||||
|
||||
@impl true |
||||
def handle_info(:consolidate, state) do |
||||
consolidate() |
||||
|
||||
schedule_next_consolidation() |
||||
|
||||
{:noreply, state} |
||||
end |
||||
|
||||
# We don't want to automatically start the consolidation in all environments. |
||||
# Consider the test environment: if the consolidation initiates but does not |
||||
# finishes before a test ends, that test will fail. This way, hundreds o |
||||
# tests were failing before disabling the consolidation and the scheduler in |
||||
# the test env. |
||||
# |
||||
# In order to choose whether or not to enable the scheduler and the initial |
||||
# consolidation, change the following Explorer config: |
||||
# |
||||
# config :explorer, Explorer.Counters.TokenHoldersCounter, enable_consolidation: false |
||||
defp enable_consolidation?, do: @enable_consolidation |
||||
end |
@ -0,0 +1,51 @@ |
||||
defmodule Explorer.Counters.TokenHoldersCounterTest do |
||||
use Explorer.DataCase |
||||
|
||||
alias Explorer.Chain.Token |
||||
alias Explorer.Counters.TokenHoldersCounter |
||||
|
||||
describe "consolidate/0" do |
||||
test "consolidates the token holders info with the most current database info" do |
||||
address_a = insert(:address, hash: "0xe49fedd93960a0267b3c3b2c1e2d66028e013fee") |
||||
address_b = insert(:address, hash: "0x5f26097334b6a32b7951df61fd0c5803ec5d8354") |
||||
|
||||
%Token{contract_address_hash: contract_address_hash} = insert(:token) |
||||
|
||||
insert( |
||||
:token_balance, |
||||
address: address_a, |
||||
block_number: 1000, |
||||
token_contract_address_hash: contract_address_hash, |
||||
value: 5000 |
||||
) |
||||
|
||||
TokenHoldersCounter.consolidate() |
||||
|
||||
assert TokenHoldersCounter.fetch(contract_address_hash) == 1 |
||||
|
||||
insert( |
||||
:token_balance, |
||||
address: address_b, |
||||
block_number: 1002, |
||||
token_contract_address_hash: contract_address_hash, |
||||
value: 1000 |
||||
) |
||||
|
||||
TokenHoldersCounter.consolidate() |
||||
|
||||
assert TokenHoldersCounter.fetch(contract_address_hash) == 2 |
||||
end |
||||
end |
||||
|
||||
describe "fetch/1" do |
||||
test "fetchs the total token holders by token contract address hash" do |
||||
token = insert(:token) |
||||
|
||||
assert TokenHoldersCounter.fetch(token.contract_address_hash) == 0 |
||||
|
||||
TokenHoldersCounter.insert_counter({token.contract_address_hash.bytes, 15}) |
||||
|
||||
assert TokenHoldersCounter.fetch(token.contract_address_hash) == 15 |
||||
end |
||||
end |
||||
end |
Loading…
Reference in new issue