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