diff --git a/apps/block_scout_web/assets/js/lib/market_history_chart.js b/apps/block_scout_web/assets/js/lib/market_history_chart.js index 7cc6527fcf..c863b6aa51 100644 --- a/apps/block_scout_web/assets/js/lib/market_history_chart.js +++ b/apps/block_scout_web/assets/js/lib/market_history_chart.js @@ -75,7 +75,11 @@ function getPriceData (marketHistoryData) { } function getMarketCapData (marketHistoryData, availableSupply) { - return marketHistoryData.map(({ date, closingPrice }) => ({x: date, y: closingPrice * availableSupply})) + if (availableSupply !== null && typeof availableSupply === 'object') { + return marketHistoryData.map(({ date, closingPrice }) => ({x: date, y: closingPrice * availableSupply[date]})) + } else { + return marketHistoryData.map(({ date, closingPrice }) => ({x: date, y: closingPrice * availableSupply})) + } } class MarketHistoryChart { @@ -100,18 +104,25 @@ class MarketHistoryChart { borderColor: sassVariables.secondary, lineTension: 0 } + this.availableSupply = availableSupply config.data.datasets = [this.price, this.marketCap] this.chart = new Chart(el, config) } update (availableSupply, marketHistoryData) { this.price.data = getPriceData(marketHistoryData) - this.marketCap.data = getMarketCapData(marketHistoryData, availableSupply) + if (this.availableSupply !== null && typeof this.availableSupply === 'object') { + const today = new Date().toJSON().slice(0, 10) + this.availableSupply[today] = availableSupply + this.marketCap.data = getMarketCapData(marketHistoryData, this.availableSupply) + } else { + this.marketCap.data = getMarketCapData(marketHistoryData, availableSupply) + } this.chart.update() } } export function createMarketHistoryChart (ctx) { - const availableSupply = ctx.dataset.available_supply + const availableSupply = JSON.parse(ctx.dataset.available_supply) const marketHistoryData = humps.camelizeKeys(JSON.parse(ctx.dataset.market_history_data)) return new MarketHistoryChart(ctx, availableSupply, marketHistoryData) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/chain_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/chain_controller.ex index 6aa4cb7470..73bd85915e 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/chain_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/chain_controller.ex @@ -40,6 +40,7 @@ defmodule BlockScoutWeb.ChainController do average_block_time: Chain.average_block_time(), blocks: blocks, exchange_rate: exchange_rate, + available_supply: available_supply(Chain.supply_for_days(30), exchange_rate), market_history_data: market_history_data, transaction_estimated_count: transaction_estimated_count, transactions: transactions @@ -78,4 +79,17 @@ defmodule BlockScoutWeb.ChainController do ) ) end + + defp available_supply(:ok, exchange_rate) do + to_string(exchange_rate.available_supply || 0) + end + + defp available_supply({:ok, supply_for_days}, _exchange_rate) do + supply_for_days + |> Jason.encode() + |> case do + {:ok, data} -> data + _ -> [] + end + end end diff --git a/apps/block_scout_web/lib/block_scout_web/templates/chain/show.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/chain/show.html.eex index ff0c15f50e..c0f4051811 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/chain/show.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/chain/show.html.eex @@ -3,7 +3,7 @@
- +
diff --git a/apps/explorer/config/config.exs b/apps/explorer/config/config.exs index 87f0c2e1b4..db41d70124 100644 --- a/apps/explorer/config/config.exs +++ b/apps/explorer/config/config.exs @@ -28,6 +28,14 @@ config :explorer, Explorer.Counters.TokenTransferCounter, enabled: true config :explorer, Explorer.Counters.TokenHoldersCounter, enabled: true, enable_consolidation: true +if System.get_env("SUPPLY_MODULE") == "TransactionAndLog" do + config :explorer, supply: Explorer.Chain.Supply.TransactionAndLog +end + +if System.get_env("SOURCE_MODULE") == "TransactionAndLog" do + config :explorer, Explorer.ExchangeRates, source: Explorer.ExchangeRates.Source.TransactionAndLog +end + config :explorer, solc_bin_api_url: "https://solc-bin.ethereum.org" diff --git a/apps/explorer/config/test.exs b/apps/explorer/config/test.exs index 7f6d19ae82..9626facf30 100644 --- a/apps/explorer/config/test.exs +++ b/apps/explorer/config/test.exs @@ -27,6 +27,9 @@ if File.exists?(file = "test.secret.exs") do import_config file end +config :explorer, Explorer.ExchangeRates.Source.TransactionAndLog, + secondary_source: Explorer.ExchangeRates.Source.OneCoinSource + variant = if is_nil(System.get_env("ETHEREUM_JSONRPC_VARIANT")) do "parity" diff --git a/apps/explorer/lib/explorer/chain.ex b/apps/explorer/lib/explorer/chain.ex index 52c86d89e2..9538a862c5 100644 --- a/apps/explorer/lib/explorer/chain.ex +++ b/apps/explorer/lib/explorer/chain.ex @@ -1929,6 +1929,11 @@ defmodule Explorer.Chain do Application.get_env(:explorer, :supply, Explorer.Chain.Supply.ProofOfAuthority) end + @doc """ + Calls supply_for_days from the configured supply_module + """ + def supply_for_days(days_count), do: supply_module().supply_for_days(days_count) + @doc """ Streams a lists token contract addresses that haven't been cataloged. """ diff --git a/apps/explorer/lib/explorer/chain/supply.ex b/apps/explorer/lib/explorer/chain/supply.ex index 7d365f2784..b442e40125 100644 --- a/apps/explorer/lib/explorer/chain/supply.ex +++ b/apps/explorer/lib/explorer/chain/supply.ex @@ -9,10 +9,24 @@ defmodule Explorer.Chain.Supply do @doc """ The current total number of coins minted minus verifiably burned coins. """ - @callback total :: non_neg_integer() + @callback total :: non_neg_integer() | %Decimal{sign: 1} @doc """ The current number coins in the market for trading. """ - @callback circulating :: non_neg_integer() + @callback circulating :: non_neg_integer() | %Decimal{sign: 1} + + @doc """ + A map of total supplies per day, optional. + """ + @callback supply_for_days(days_count :: integer) :: {:ok, term} | {:error, term} | :ok + + defmacro __using__(_opts) do + quote do + @behaviour Explorer.Chain.Supply + def supply_for_days(_days_count), do: :ok + + defoverridable supply_for_days: 1 + end + end end diff --git a/apps/explorer/lib/explorer/chain/supply/proof_of_authority.ex b/apps/explorer/lib/explorer/chain/supply/proof_of_authority.ex index 6d8824849c..f9923eeca3 100644 --- a/apps/explorer/lib/explorer/chain/supply/proof_of_authority.ex +++ b/apps/explorer/lib/explorer/chain/supply/proof_of_authority.ex @@ -12,11 +12,9 @@ defmodule Explorer.Chain.Supply.ProofOfAuthority do See https://github.com/poanetwork/wiki/wiki/POA-Token-Supply for more information. """ + use Explorer.Chain.Supply alias Explorer.Chain - alias Explorer.Chain.Supply - - @behaviour Supply @initial_supply 252_460_800 @reserved_for_vesting 50_492_160 @@ -31,12 +29,10 @@ defmodule Explorer.Chain.Supply.ProofOfAuthority do ~D[2019-12-15] => 0.125 } - @impl Supply def circulating do total() - reserved_supply(Date.utc_today()) end - @impl Supply def total do initial_supply = initial_supply() block_height = block_height() diff --git a/apps/explorer/lib/explorer/chain/supply/transaction_and_log.ex b/apps/explorer/lib/explorer/chain/supply/transaction_and_log.ex new file mode 100644 index 0000000000..03e63ad91e --- /dev/null +++ b/apps/explorer/lib/explorer/chain/supply/transaction_and_log.ex @@ -0,0 +1,78 @@ +defmodule Explorer.Chain.Supply.TransactionAndLog do + @moduledoc """ + Defines the supply API for calculating the supply for smaller chains with + specific mint and burn events + """ + use Explorer.Chain.Supply + + alias Explorer.Chain.{InternalTransaction, Log, Wei} + alias Explorer.{Repo, Chain} + + {:ok, base_wei} = Wei.cast(0) + @base_wei base_wei + + {:ok, burn_address} = Chain.string_to_address_hash("0x0000000000000000000000000000000000000000") + + @burn_address burn_address + @bridge_edge "0x3c798bbcf33115b42c728b8504cff11dd58736e9fa789f1cda2738db7d696b2a" + + import Ecto.Query, only: [from: 2] + + def circulating, do: total(Timex.now()) + + def total, do: total(Timex.now()) + + @doc false + @spec total(DateTime.t()) :: %Decimal{sign: 1} + def total(on_date) do + on_date + |> minted_value + |> Wei.sub(burned_value(on_date)) + |> Wei.to(:ether) + end + + def supply_for_days(days_count) when is_integer(days_count) and days_count > 0 do + past_days = -(days_count - 1) + + result = + for i <- past_days..0, into: %{} do + datetime = Timex.shift(Timex.now(), days: i) + {DateTime.to_date(datetime), total(datetime)} + end + + {:ok, result} + end + + defp minted_value(on_date) do + query = + from( + l in Log, + join: t in assoc(l, :transaction), + join: b in assoc(t, :block), + where: b.timestamp <= ^on_date and l.first_topic == @bridge_edge, + select: fragment("concat('0x', encode(?, 'hex'))", l.data) + ) + + query + |> Repo.all() + |> Enum.reduce(@base_wei, fn data, acc -> + {:ok, wei_value} = Wei.cast(data) + Wei.sum(wei_value, acc) + end) + end + + defp burned_value(on_date) do + query = + from( + it in InternalTransaction, + join: t in assoc(it, :transaction), + join: b in assoc(t, :block), + where: b.timestamp <= ^on_date and it.to_address_hash == ^@burn_address, + select: it.value + ) + + query + |> Repo.all() + |> Enum.reduce(@base_wei, fn data, acc -> Wei.sum(data, acc) end) + end +end diff --git a/apps/explorer/lib/explorer/chain/wei.ex b/apps/explorer/lib/explorer/chain/wei.ex index 33f76a91a8..bd1c42c09c 100644 --- a/apps/explorer/lib/explorer/chain/wei.ex +++ b/apps/explorer/lib/explorer/chain/wei.ex @@ -130,6 +130,22 @@ defmodule Explorer.Chain.Wei do |> from(:wei) end + @doc """ + Subtracts two Wei values. + + ## Example + + iex> first = %Explorer.Chain.Wei{value: Decimal.new(1_123)} + iex> second = %Explorer.Chain.Wei{value: Decimal.new(1_000)} + iex> Explorer.Chain.Wei.sub(first, second) + %Explorer.Chain.Wei{value: Decimal.new(123)} + """ + def sub(%Wei{value: wei_1}, %Wei{value: wei_2}) do + wei_1 + |> Decimal.sub(wei_2) + |> from(:wei) + end + @doc """ Converts `Decimal` representations of various wei denominations (wei, Gwei, ether) to a wei base unit. diff --git a/apps/explorer/lib/explorer/exchange_rates/source/transaction_and_log.ex b/apps/explorer/lib/explorer/exchange_rates/source/transaction_and_log.ex new file mode 100644 index 0000000000..3dd11f0355 --- /dev/null +++ b/apps/explorer/lib/explorer/exchange_rates/source/transaction_and_log.ex @@ -0,0 +1,50 @@ +defmodule Explorer.ExchangeRates.Source.TransactionAndLog do + @moduledoc """ + Adapter for calculating the market cap and total supply from logs and transactions + while still getting other info like price in dollars and bitcoin from a secondary source + """ + + alias Explorer.ExchangeRates.{Source, Token} + alias Explorer.Chain + + @behaviour Source + + @impl Source + def fetch_exchange_rates do + token_data = + secondary_source().fetch_exchange_rates() + |> elem(1) + |> Enum.find(fn token -> token.symbol == Explorer.coin() end) + |> build_struct + + {:ok, [token_data]} + end + + defp build_struct(original_token) do + %Token{ + available_supply: to_decimal(Chain.circulating_supply()), + btc_value: original_token.btc_value, + id: original_token.id, + last_updated: original_token.last_updated, + market_cap_usd: Decimal.mult(to_decimal(Chain.circulating_supply()), original_token.usd_value), + name: original_token.name, + symbol: original_token.symbol, + usd_value: original_token.usd_value, + volume_24h_usd: original_token.volume_24h_usd + } + end + + defp to_decimal(value) do + Decimal.new(value) + end + + @spec secondary_source() :: module() + defp secondary_source do + config(:secondary_source) || Explorer.ExchangeRates.Source.CoinMarketCap + end + + @spec config(atom()) :: term + defp config(key) do + Application.get_env(:explorer, __MODULE__, [])[key] + end +end diff --git a/apps/explorer/test/explorer/chain/supply/transaction_and_log_test.exs b/apps/explorer/test/explorer/chain/supply/transaction_and_log_test.exs new file mode 100644 index 0000000000..5126d7acaf --- /dev/null +++ b/apps/explorer/test/explorer/chain/supply/transaction_and_log_test.exs @@ -0,0 +1,122 @@ +defmodule Explorer.Chain.Supply.TransactionAndLogTest do + use Explorer.DataCase + alias Explorer.Chain + alias Explorer.Chain.Supply.TransactionAndLog + + setup do + {:ok, burn_address_hash} = Chain.string_to_address_hash("0x0000000000000000000000000000000000000000") + + burn_address = + case Chain.hash_to_address(burn_address_hash) do + {:ok, burn_address} -> burn_address + {:error, :not_found} -> insert(:address, hash: "0x0000000000000000000000000000000000000000") + end + + {:ok, %{burn_address: burn_address}} + end + + describe "total/1" do + test "today with no mints or burns brings zero" do + assert TransactionAndLog.total(Timex.now()) == Decimal.new(0) + end + + test "today with mints and burns calculates a value", %{burn_address: burn_address} do + old_block = insert(:block, timestamp: Timex.shift(Timex.now(), days: -1), number: 1000) + + insert(:log, + transaction: + insert(:transaction, block: old_block, block_number: 1000, cumulative_gas_used: 1, gas_used: 1, index: 2), + first_topic: "0x3c798bbcf33115b42c728b8504cff11dd58736e9fa789f1cda2738db7d696b2a", + data: "0x0000000000000000000000000000000000000000000000008ac7230489e80000" + ) + + insert(:internal_transaction, + index: 527, + transaction: + insert(:transaction, block: old_block, block_number: 1000, cumulative_gas_used: 1, gas_used: 1, index: 3), + to_address: burn_address, + value: "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000" + ) + + assert TransactionAndLog.total(Timex.now()) == Decimal.new(9) + end + + test "yesterday with mints and burns calculates a value ignoring whatever happened today", %{ + burn_address: burn_address + } do + old_block = insert(:block, timestamp: Timex.shift(Timex.now(), days: -1), number: 1000) + + insert(:log, + transaction: + insert(:transaction, block: old_block, block_number: 1000, cumulative_gas_used: 1, gas_used: 1, index: 2), + first_topic: "0x3c798bbcf33115b42c728b8504cff11dd58736e9fa789f1cda2738db7d696b2a", + data: "0x0000000000000000000000000000000000000000000000008ac7230489e80000" + ) + + new_block = insert(:block, timestamp: Timex.now(), number: 1001) + + insert(:internal_transaction, + index: 527, + transaction: + insert(:transaction, block: new_block, block_number: 1000, cumulative_gas_used: 1, gas_used: 1, index: 3), + to_address: burn_address, + value: "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000" + ) + + assert TransactionAndLog.total(Timex.shift(Timex.now(), days: -1)) == Decimal.new(10) + end + end + + describe "total/0" do + test "calculates the same value as total/1 receiving today's date", %{burn_address: burn_address} do + old_block = insert(:block, timestamp: Timex.shift(Timex.now(), days: -1), number: 1000) + + insert(:log, + transaction: + insert(:transaction, block: old_block, block_number: 1000, cumulative_gas_used: 1, gas_used: 1, index: 2), + first_topic: "0x3c798bbcf33115b42c728b8504cff11dd58736e9fa789f1cda2738db7d696b2a", + data: "0x0000000000000000000000000000000000000000000000008ac7230489e80000" + ) + + insert(:internal_transaction, + index: 527, + transaction: + insert(:transaction, block: old_block, block_number: 1000, cumulative_gas_used: 1, gas_used: 1, index: 3), + to_address: burn_address, + value: "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000" + ) + + assert TransactionAndLog.total() == TransactionAndLog.total(Timex.now()) + end + end + + describe "supply_for_days/1" do + test "bring the supply of today and yesterday when receiving 2", %{burn_address: burn_address} do + old_block = insert(:block, timestamp: Timex.shift(Timex.now(), days: -1), number: 1000) + + insert(:log, + transaction: + insert(:transaction, block: old_block, block_number: 1000, cumulative_gas_used: 1, gas_used: 1, index: 2), + first_topic: "0x3c798bbcf33115b42c728b8504cff11dd58736e9fa789f1cda2738db7d696b2a", + data: "0x0000000000000000000000000000000000000000000000008ac7230489e80000" + ) + + new_block = insert(:block, timestamp: Timex.now(), number: 1001) + + insert(:internal_transaction, + index: 527, + transaction: + insert(:transaction, block: new_block, block_number: 1000, cumulative_gas_used: 1, gas_used: 1, index: 3), + to_address: burn_address, + value: "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000" + ) + + expected_result = %{ + Timex.shift(Timex.today(), days: -1) => Decimal.new(10), + Timex.today() => Decimal.new(9) + } + + assert TransactionAndLog.supply_for_days(2) == {:ok, expected_result} + end + end +end diff --git a/apps/explorer/test/explorer/chain/wei_test.exs b/apps/explorer/test/explorer/chain/wei_test.exs index 003d85e97e..782d736928 100644 --- a/apps/explorer/test/explorer/chain/wei_test.exs +++ b/apps/explorer/test/explorer/chain/wei_test.exs @@ -54,4 +54,50 @@ defmodule Explorer.Chain.WeiTest do test "type/0" do assert Wei.type() == :decimal end + + describe "sum/1" do + test "with two positive values return the sum of them" do + first = %Explorer.Chain.Wei{value: Decimal.new(123)} + second = %Explorer.Chain.Wei{value: Decimal.new(1_000)} + + assert Explorer.Chain.Wei.sum(first, second) == %Explorer.Chain.Wei{value: Decimal.new(1_123)} + end + + test "with a positive and a negative value return the positive minus the negative's absolute" do + first = %Explorer.Chain.Wei{value: Decimal.new(123)} + second = %Explorer.Chain.Wei{value: Decimal.new(-100)} + + assert Explorer.Chain.Wei.sum(first, second) == %Explorer.Chain.Wei{value: Decimal.new(23)} + end + end + + describe "sub/1" do + test "with a negative second parameter return the sum of the absolute values" do + first = %Explorer.Chain.Wei{value: Decimal.new(123)} + second = %Explorer.Chain.Wei{value: Decimal.new(-100)} + + assert Explorer.Chain.Wei.sub(first, second) == %Explorer.Chain.Wei{value: Decimal.new(223)} + end + + test "with a negative first parameter return the negative of the sum of the absolute values" do + first = %Explorer.Chain.Wei{value: Decimal.new(-123)} + second = %Explorer.Chain.Wei{value: Decimal.new(100)} + + assert Explorer.Chain.Wei.sub(first, second) == %Explorer.Chain.Wei{value: Decimal.new(-223)} + end + + test "with a larger first parameter return a positive number" do + first = %Explorer.Chain.Wei{value: Decimal.new(123)} + second = %Explorer.Chain.Wei{value: Decimal.new(100)} + + assert Explorer.Chain.Wei.sub(first, second) == %Explorer.Chain.Wei{value: Decimal.new(23)} + end + + test "with a larger second parameter return a negative number" do + first = %Explorer.Chain.Wei{value: Decimal.new(23)} + second = %Explorer.Chain.Wei{value: Decimal.new(100)} + + assert Explorer.Chain.Wei.sub(first, second) == %Explorer.Chain.Wei{value: Decimal.new(-77)} + end + end end diff --git a/apps/explorer/test/explorer/exchange_rates/source/transaction_and_log_test.exs b/apps/explorer/test/explorer/exchange_rates/source/transaction_and_log_test.exs new file mode 100644 index 0000000000..f5821ed2f4 --- /dev/null +++ b/apps/explorer/test/explorer/exchange_rates/source/transaction_and_log_test.exs @@ -0,0 +1,11 @@ +defmodule Explorer.ExchangeRates.Source.TransactionAndLogTest do + use Explorer.DataCase + alias Explorer.ExchangeRates.Source.TransactionAndLog + alias Explorer.ExchangeRates.Token + + describe "fetch_exchange_rates/1" do + test "bring a list with one %Token{}" do + assert {:ok, [%Token{}]} = TransactionAndLog.fetch_exchange_rates() + end + end +end diff --git a/apps/explorer/test/support/fakes/one_coin_source.ex b/apps/explorer/test/support/fakes/one_coin_source.ex new file mode 100644 index 0000000000..fc9781abe0 --- /dev/null +++ b/apps/explorer/test/support/fakes/one_coin_source.ex @@ -0,0 +1,25 @@ +defmodule Explorer.ExchangeRates.Source.OneCoinSource do + @moduledoc false + + alias Explorer.ExchangeRates.Source + alias Explorer.ExchangeRates.Token + + @behaviour Source + + @impl Source + def fetch_exchange_rates do + pseudo_token = %Token{ + available_supply: Decimal.new(10_000_000), + btc_value: Decimal.new(1), + id: "", + last_updated: Timex.now(), + name: "", + market_cap_usd: Decimal.new(10_000_000), + symbol: Explorer.coin(), + usd_value: Decimal.new(1), + volume_24h_usd: Decimal.new(1) + } + + {:ok, [pseudo_token]} + end +end