Merge pull request #951 from poanetwork/gsf-dai-chain-supply-and-market-cap

dai chain supply and market cap
pull/955/head
Andrew Cravenho 6 years ago committed by GitHub
commit b92c632e2c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 17
      apps/block_scout_web/assets/js/lib/market_history_chart.js
  2. 14
      apps/block_scout_web/lib/block_scout_web/controllers/chain_controller.ex
  3. 2
      apps/block_scout_web/lib/block_scout_web/templates/chain/show.html.eex
  4. 8
      apps/explorer/config/config.exs
  5. 3
      apps/explorer/config/test.exs
  6. 5
      apps/explorer/lib/explorer/chain.ex
  7. 18
      apps/explorer/lib/explorer/chain/supply.ex
  8. 6
      apps/explorer/lib/explorer/chain/supply/proof_of_authority.ex
  9. 78
      apps/explorer/lib/explorer/chain/supply/transaction_and_log.ex
  10. 16
      apps/explorer/lib/explorer/chain/wei.ex
  11. 50
      apps/explorer/lib/explorer/exchange_rates/source/transaction_and_log.ex
  12. 122
      apps/explorer/test/explorer/chain/supply/transaction_and_log_test.exs
  13. 46
      apps/explorer/test/explorer/chain/wei_test.exs
  14. 11
      apps/explorer/test/explorer/exchange_rates/source/transaction_and_log_test.exs
  15. 25
      apps/explorer/test/support/fakes/one_coin_source.ex

@ -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)

@ -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

@ -3,7 +3,7 @@
<div class="container">
<div class="dashboard-banner">
<div class="dashboard-banner-chart">
<canvas data-chart="marketHistoryChart" data-available_supply='<%= @exchange_rate.available_supply %>' data-market_history_data='<%=raw encode_market_history_data(@market_history_data) %>' width="350" height="152"></canvas>
<canvas data-chart="marketHistoryChart" data-available_supply='<%=raw @available_supply %>' data-market_history_data='<%=raw encode_market_history_data(@market_history_data) %>' width="350" height="152"></canvas>
</div>
<div class="dashboard-banner-chart-legend">
<div class="dashboard-banner-chart-legend-item">

@ -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"

@ -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"

@ -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.
"""

@ -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

@ -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()

@ -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

@ -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.

@ -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

@ -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

@ -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

@ -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

@ -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
Loading…
Cancel
Save