diff --git a/CHANGELOG.md b/CHANGELOG.md index c9c11688e2..0cb74f2e2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## Current ### Features +- [#2862](https://github.com/poanetwork/blockscout/pull/2862) - Coin total supply from DB API endpoint - [#2825](https://github.com/poanetwork/blockscout/pull/2825) - separate token transfers and transactions - [#2787](https://github.com/poanetwork/blockscout/pull/2787) - async fetching of address counters - [#2791](https://github.com/poanetwork/blockscout/pull/2791) - add ipc client diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/stats_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/stats_controller.ex index 182ad38ea5..b490151cf9 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/stats_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/stats_controller.ex @@ -1,7 +1,10 @@ defmodule BlockScoutWeb.API.RPC.StatsController do use BlockScoutWeb, :controller + use Explorer.Schema + alias Explorer.{Chain, ExchangeRates} + alias Explorer.Chain.Cache.AddressSum alias Explorer.Chain.Wei def tokensupply(conn, params) do @@ -21,7 +24,7 @@ defmodule BlockScoutWeb.API.RPC.StatsController do end end - def ethsupply(conn, _params) do + def ethsupplyexchange(conn, _params) do wei_total_supply = Chain.total_supply() |> Decimal.new() @@ -29,7 +32,13 @@ defmodule BlockScoutWeb.API.RPC.StatsController do |> Wei.to(:wei) |> Decimal.to_string() - render(conn, "ethsupply.json", total_supply: wei_total_supply) + render(conn, "ethsupplyexchange.json", total_supply: wei_total_supply) + end + + def ethsupply(conn, _params) do + cached_wei_total_supply = AddressSum.get_sum() + + render(conn, "ethsupply.json", total_supply: cached_wei_total_supply) end def ethprice(conn, _params) do diff --git a/apps/block_scout_web/lib/block_scout_web/etherscan.ex b/apps/block_scout_web/lib/block_scout_web/etherscan.ex index 1037013d66..abb358179c 100644 --- a/apps/block_scout_web/lib/block_scout_web/etherscan.ex +++ b/apps/block_scout_web/lib/block_scout_web/etherscan.ex @@ -261,6 +261,12 @@ defmodule BlockScoutWeb.Etherscan do "result" => "21265524714464" } + @stats_ethsupplyexchange_example_value %{ + "status" => "1", + "message" => "OK", + "result" => "101959776311500000000000000" + } + @stats_ethsupply_example_value %{ "status" => "1", "message" => "OK", @@ -1772,9 +1778,35 @@ defmodule BlockScoutWeb.Etherscan do ] } + @stats_ethsupplyexchange_action %{ + name: "ethsupplyexchange", + description: "Get total supply in Wei from exchange.", + required_params: [], + optional_params: [], + responses: [ + %{ + code: "200", + description: "successful operation", + example_value: Jason.encode!(@stats_ethsupplyexchange_example_value), + model: %{ + name: "Result", + fields: %{ + status: @status_type, + message: @message_type, + result: %{ + type: "integer", + description: "The total supply.", + example: ~s("101959776311500000000000000") + } + } + } + } + ] + } + @stats_ethsupply_action %{ name: "ethsupply", - description: "Get total supply in Wei.", + description: "Get total supply in Wei from DB.", required_params: [], optional_params: [], responses: [ @@ -2302,6 +2334,7 @@ defmodule BlockScoutWeb.Etherscan do name: "stats", actions: [ @stats_tokensupply_action, + @stats_ethsupplyexchange_action, @stats_ethsupply_action, @stats_ethprice_action ] diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/rpc/stats_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/rpc/stats_view.ex index bdc5e28b0e..544f04ae43 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/rpc/stats_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/rpc/stats_view.ex @@ -7,6 +7,10 @@ defmodule BlockScoutWeb.API.RPC.StatsView do RPCView.render("show.json", data: Decimal.to_string(token_supply)) end + def render("ethsupplyexchange.json", %{total_supply: total_supply}) do + RPCView.render("show.json", data: total_supply) + end + def render("ethsupply.json", %{total_supply: total_supply}) do RPCView.render("show.json", data: total_supply) end diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/stats_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/stats_controller_test.exs index bf3b82a3cb..a57db2221b 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/stats_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/stats_controller_test.exs @@ -85,8 +85,27 @@ defmodule BlockScoutWeb.API.RPC.StatsControllerTest do end end + describe "ethsupplyexchange" do + test "returns total supply from exchange", %{conn: conn} do + params = %{ + "module" => "stats", + "action" => "ethsupplyexchange" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["result"] == "252460800000000000000000000" + assert response["status"] == "1" + assert response["message"] == "OK" + assert :ok = ExJsonSchema.Validator.validate(ethsupplyexchange_schema(), response) + end + end + describe "ethsupply" do - test "returns total supply", %{conn: conn} do + test "returns total supply from DB", %{conn: conn} do params = %{ "module" => "stats", "action" => "ethsupply" @@ -97,7 +116,7 @@ defmodule BlockScoutWeb.API.RPC.StatsControllerTest do |> get("/api", params) |> json_response(200) - assert response["result"] == "252460800000000000000000000" + assert response["result"] == "6" assert response["status"] == "1" assert response["message"] == "OK" assert :ok = ExJsonSchema.Validator.validate(ethsupply_schema(), response) @@ -179,6 +198,12 @@ defmodule BlockScoutWeb.API.RPC.StatsControllerTest do }) end + defp ethsupplyexchange_schema do + resolve_schema(%{ + "type" => ["string", "null"] + }) + end + defp ethprice_schema do resolve_schema(%{ "type" => "object", diff --git a/apps/explorer/config/config.exs b/apps/explorer/config/config.exs index 60b0afdb48..b33ecf9981 100644 --- a/apps/explorer/config/config.exs +++ b/apps/explorer/config/config.exs @@ -52,6 +52,20 @@ config :explorer, Explorer.Chain.Cache.BlockNumber, ttl_check_interval: if(System.get_env("DISABLE_INDEXER") == "true", do: :timer.seconds(1), else: false), global_ttl: if(System.get_env("DISABLE_INDEXER") == "true", do: :timer.seconds(5)) +address_sum_global_ttl = + "ADDRESS_SUM_CACHE_PERIOD" + |> System.get_env("") + |> Integer.parse() + |> case do + {integer, ""} -> :timer.seconds(integer) + _ -> :timer.minutes(60) + end + +config :explorer, Explorer.Chain.Cache.AddressSum, + enabled: true, + ttl_check_interval: :timer.seconds(1), + global_ttl: address_sum_global_ttl + balances_update_interval = if System.get_env("ADDRESS_WITH_BALANCES_UPDATE_INTERVAL") do case Integer.parse(System.get_env("ADDRESS_WITH_BALANCES_UPDATE_INTERVAL")) do diff --git a/apps/explorer/lib/explorer/application.ex b/apps/explorer/lib/explorer/application.ex index 4f3e5585f9..eafa460719 100644 --- a/apps/explorer/lib/explorer/application.ex +++ b/apps/explorer/lib/explorer/application.ex @@ -9,6 +9,7 @@ defmodule Explorer.Application do alias Explorer.Chain.Cache.{ Accounts, + AddressSum, BlockCount, BlockNumber, Blocks, @@ -46,6 +47,7 @@ defmodule Explorer.Application do {Registry, keys: :duplicate, name: Registry.ChainEvents, id: Registry.ChainEvents}, {Admin.Recovery, [[], [name: Admin.Recovery]]}, TransactionCount, + AddressSum, BlockCount, Blocks, NetVersion, diff --git a/apps/explorer/lib/explorer/chain.ex b/apps/explorer/lib/explorer/chain.ex index ba6e3969e1..a95a6f45f7 100644 --- a/apps/explorer/lib/explorer/chain.ex +++ b/apps/explorer/lib/explorer/chain.ex @@ -1322,6 +1322,17 @@ defmodule Explorer.Chain do Repo.one!(query) end + @spec fetch_sum_coin_total_supply() :: non_neg_integer + def fetch_sum_coin_total_supply do + query = + from( + a0 in Address, + select: fragment("SUM(a0.fetched_coin_balance)") + ) + + Repo.one!(query) || 0 + end + @doc """ The number of `t:Explorer.Chain.InternalTransaction.t/0`. diff --git a/apps/explorer/lib/explorer/chain/cache/address_sum.ex b/apps/explorer/lib/explorer/chain/cache/address_sum.ex new file mode 100644 index 0000000000..b0d3326312 --- /dev/null +++ b/apps/explorer/lib/explorer/chain/cache/address_sum.ex @@ -0,0 +1,53 @@ +defmodule Explorer.Chain.Cache.AddressSum do + @moduledoc """ + Cache for address sum. + """ + + require Logger + + use Explorer.Chain.MapCache, + name: :address_sum, + key: :sum, + key: :async_task, + ttl_check_interval: Application.get_env(:explorer, __MODULE__)[:ttl_check_interval], + global_ttl: Application.get_env(:explorer, __MODULE__)[:global_ttl], + callback: &async_task_on_deletion(&1) + + alias Explorer.Chain + + defp handle_fallback(:sum) do + # This will get the task PID if one exists and launch a new task if not + # See next `handle_fallback` definition + get_async_task() + + {:return, nil} + end + + defp handle_fallback(:async_task) do + # If this gets called it means an async task was requested, but none exists + # so a new one needs to be launched + {:ok, task} = + Task.start(fn -> + try do + result = Chain.fetch_sum_coin_total_supply() + + set_sum(result) + rescue + e -> + Logger.debug([ + "Coudn't update address sum test #{inspect(e)}" + ]) + end + + set_async_task(nil) + end) + + {:update, task} + end + + # By setting this as a `callback` an async task will be started each time the + # `sum` expires (unless there is one already running) + defp async_task_on_deletion({:delete, _, :sum}), do: get_async_task() + + defp async_task_on_deletion(_data), do: nil +end diff --git a/apps/explorer/test/explorer/chain/cache/address_sum_test.exs b/apps/explorer/test/explorer/chain/cache/address_sum_test.exs new file mode 100644 index 0000000000..707c70635f --- /dev/null +++ b/apps/explorer/test/explorer/chain/cache/address_sum_test.exs @@ -0,0 +1,56 @@ +defmodule Explorer.Chain.Cache.AddressSumTest do + use Explorer.DataCase + + alias Explorer.Chain.Cache.AddressSum + + setup do + Supervisor.terminate_child(Explorer.Supervisor, AddressSum.child_id()) + Supervisor.restart_child(Explorer.Supervisor, AddressSum.child_id()) + :ok + end + + test "returns default address sum" do + result = AddressSum.get_sum() + + assert is_nil(result) + end + + test "updates cache if initial value is zero" do + insert(:address, fetched_coin_balance: 1) + insert(:address, fetched_coin_balance: 2) + insert(:address, fetched_coin_balance: 3) + + _result = AddressSum.get_sum() + + Process.sleep(1000) + + updated_value = Decimal.to_integer(AddressSum.get_sum()) + + assert updated_value == 6 + end + + test "does not update cache if cache period did not pass" do + insert(:address, fetched_coin_balance: 1) + insert(:address, fetched_coin_balance: 2) + insert(:address, fetched_coin_balance: 3) + + _result = AddressSum.get_sum() + + Process.sleep(1000) + + updated_value = Decimal.to_integer(AddressSum.get_sum()) + + assert updated_value == 6 + + insert(:address, fetched_coin_balance: 4) + insert(:address, fetched_coin_balance: 5) + + _updated_value = AddressSum.get_sum() + + Process.sleep(1000) + + updated_value = Decimal.to_integer(AddressSum.get_sum()) + + assert updated_value == 6 + end +end diff --git a/apps/explorer/test/explorer/chain/cache/block_count_test.exs b/apps/explorer/test/explorer/chain/cache/block_count_test.exs index 108c929a75..19573a7a16 100644 --- a/apps/explorer/test/explorer/chain/cache/block_count_test.exs +++ b/apps/explorer/test/explorer/chain/cache/block_count_test.exs @@ -9,7 +9,7 @@ defmodule Explorer.Chain.Cache.BlockCountTest do :ok end - test "returns default transaction count" do + test "returns default block count" do result = BlockCount.get_count() assert is_nil(result) diff --git a/apps/explorer/test/explorer/chain_test.exs b/apps/explorer/test/explorer/chain_test.exs index 88cd8e216d..acdd7836ee 100644 --- a/apps/explorer/test/explorer/chain_test.exs +++ b/apps/explorer/test/explorer/chain_test.exs @@ -1121,6 +1121,20 @@ defmodule Explorer.ChainTest do end end + describe "fetch_sum_coin_total_supply/0" do + test "fetches coin total supply" do + for index <- 0..4 do + insert(:address, fetched_coin_balance: index) + end + + assert "10" = Decimal.to_string(Chain.fetch_sum_coin_total_supply()) + end + + test "fetches coin total supply when there are no blocks" do + assert 0 = Chain.fetch_sum_coin_total_supply() + end + end + describe "address_hash_to_token_transfers/2" do test "returns just the token transfers related to the given contract address" do contract_address =