diff --git a/apps/explorer/config/config.exs b/apps/explorer/config/config.exs index 00902c24c2..2bf03ccd6b 100644 --- a/apps/explorer/config/config.exs +++ b/apps/explorer/config/config.exs @@ -40,8 +40,8 @@ else config :explorer, Explorer.Validator.MetadataProcessor, enabled: false end -if System.get_env("SUPPLY_MODULE") == "TransactionAndLog" do - config :explorer, supply: Explorer.Chain.Supply.TransactionAndLog +if System.get_env("SUPPLY_MODULE") == "TokenBridge" do + config :explorer, supply: Explorer.Chain.Supply.TokenBridge end if System.get_env("SOURCE_MODULE") == "TransactionAndLog" do diff --git a/apps/explorer/lib/explorer/chain/supply/token_bridge.ex b/apps/explorer/lib/explorer/chain/supply/token_bridge.ex new file mode 100644 index 0000000000..e67106baec --- /dev/null +++ b/apps/explorer/lib/explorer/chain/supply/token_bridge.ex @@ -0,0 +1,133 @@ +defmodule Explorer.Chain.Supply.TokenBridge do + @moduledoc """ + Defines the supply API for calculating the supply based on Token Bridge. + """ + + use Explorer.Chain.Supply + + alias Explorer.Chain.Wei + alias Explorer.SmartContract.Reader + + @token_bridge_contract_address "0x7301CFA0e1756B71869E93d4e4Dca5c7d0eb0AA6" + @total_burned_coins_abi %{ + "type" => "function", + "stateMutability" => "view", + "payable" => false, + "outputs" => [%{"type" => "uint256", "name" => ""}], + "name" => "totalBurntCoins", + "inputs" => [], + "constant" => true + } + @total_burned_coins_params %{"totalBurntCoins" => []} + + @block_reward_contract_address "0x867305d19606aadba405ce534e303d0e225f9556" + @total_minted_coins_abi %{ + "type" => "function", + "stateMutability" => "view", + "payable" => false, + "outputs" => [%{"type" => "uint256", "name" => ""}], + "name" => "mintedTotally", + "inputs" => [], + "constant" => true + } + @total_minted_coins_params %{"mintedTotally" => []} + + @ets_table :token_bridge_contract_coin_cache + # 30 minutes + @cache_period 1_000 * 60 * 30 + @cache_key "coins_with_period" + + def circulating, do: total_coins() + + def total, do: total_coins() + + def total_coins(opts \\ []) do + cache_period = Keyword.get(opts, :cache_period) || @cache_period + + cached_total_coins(cache_period) + end + + defp minted_coins do + address = System.get_env("BLOCK_REWARD_CONTRACT") || @block_reward_contract_address + + call_contract(address, @total_minted_coins_abi, @total_minted_coins_params) + end + + defp burned_coins do + address = System.get_env("TOKEN_BRIDGE_CONTRACT") || @token_bridge_contract_address + + call_contract(address, @total_burned_coins_abi, @total_burned_coins_params) + end + + defp call_contract(address, abi, params) do + abi = [abi] + + method_name = + params + |> Enum.map(fn {key, _value} -> key end) + |> List.first() + + value = + case Reader.query_contract(address, abi, params) do + %{^method_name => {:ok, [result]}} -> result + _ -> 0 + end + + %Wei{value: Decimal.new(value)} + end + + def cached_total_coins(cache_period) do + setup_cache() + + {value, cache_time} = cached_values() + + if current_time() - cache_time > cache_period do + {current_value, _} = update_cache() + current_value + else + value + end + end + + defp cached_values do + cache_key = @cache_key + + case :ets.lookup(@ets_table, @cache_key) do + [{^cache_key, {coins, time}}] -> + {coins, time} + + _ -> + update_cache() + end + end + + defp update_cache do + current_total_coins = + minted_coins() + |> Wei.sub(burned_coins()) + |> Wei.to(:ether) + + current_time = current_time() + + :ets.insert(@ets_table, {@cache_key, {current_total_coins, current_time}}) + + {current_total_coins, current_time} + end + + defp setup_cache do + if :ets.whereis(@ets_table) == :undefined do + :ets.new(@ets_table, [ + :set, + :named_table, + :public, + write_concurrency: true + ]) + end + end + + defp current_time do + utc_now = DateTime.utc_now() + + DateTime.to_unix(utc_now, :millisecond) + end +end diff --git a/apps/explorer/test/explorer/chain/supply/token_bridge_test.exs b/apps/explorer/test/explorer/chain/supply/token_bridge_test.exs new file mode 100644 index 0000000000..07f2b5086e --- /dev/null +++ b/apps/explorer/test/explorer/chain/supply/token_bridge_test.exs @@ -0,0 +1,66 @@ +defmodule Explorer.Chain.Supply.TokenBridgeTest do + use EthereumJSONRPC.Case, async: false + + import Mox + + alias Explorer.Chain.Supply.TokenBridge + + @moduletag :capture_log + + setup :set_mox_global + + setup :verify_on_exit! + + describe "total_coins/1" do + @tag :no_parity + @tag :no_geth + test "calculates total coins", %{json_rpc_named_arguments: json_rpc_named_arguments} do + if json_rpc_named_arguments[:transport] == EthereumJSONRPC.Mox do + EthereumJSONRPC.Mox + |> expect(:json_rpc, fn [ + %{ + id: "mintedTotally", + method: "eth_call", + params: [ + %{data: "0x553a5c85", to: "0x867305d19606aadba405ce534e303d0e225f9556"}, + "latest" + ] + } + ], + _options -> + {:ok, + [ + %{ + id: "mintedTotally", + jsonrpc: "2.0", + result: "0x00000000000000000000000000000000000000000000042aa8fe57ebb112dcc8" + } + ]} + end) + |> expect(:json_rpc, fn [ + %{ + id: "totalBurntCoins", + jsonrpc: "2.0", + method: "eth_call", + params: [ + %{data: "0x0e8162ba", to: "0x7301CFA0e1756B71869E93d4e4Dca5c7d0eb0AA6"}, + "latest" + ] + } + ], + _options -> + {:ok, + [ + %{ + id: "totalBurntCoins", + jsonrpc: "2.0", + result: "0x00000000000000000000000000000000000000000000033cc192839185166fc6" + } + ]} + end) + end + + assert Decimal.round(TokenBridge.total_coins(), 2, :down) == Decimal.from_float(4388.55) + end + end +end