commit
4849be8200
@ -1,53 +1,53 @@ |
||||
.transaction-bottom-panel { |
||||
display: flex; |
||||
flex-direction: column; |
||||
@media (min-width: 768px) { |
||||
flex-direction: row; |
||||
justify-content: space-between; |
||||
align-items: flex-end; |
||||
} |
||||
display: flex; |
||||
flex-direction: column; |
||||
@media (min-width: 768px) { |
||||
flex-direction: row; |
||||
justify-content: space-between; |
||||
align-items: flex-end; |
||||
} |
||||
} |
||||
|
||||
.transaction-bottom-panel { |
||||
display: flex; |
||||
flex-direction: column; |
||||
@media (min-width: 768px) { |
||||
flex-direction: row; |
||||
justify-content: space-between; |
||||
align-items: flex-end; |
||||
} |
||||
display: flex; |
||||
flex-direction: column; |
||||
@media (min-width: 768px) { |
||||
flex-direction: row; |
||||
justify-content: space-between; |
||||
align-items: flex-end; |
||||
} |
||||
} |
||||
|
||||
.download-all-transactions { |
||||
text-align: center; |
||||
color: #a3a9b5; |
||||
font-size: 13px; |
||||
margin-top: 10px; |
||||
@media (min-width: 768px) { |
||||
margin-top: 30px; |
||||
} |
||||
.download-all-transactions-link { |
||||
display: inline-flex; |
||||
align-items: center; |
||||
text-decoration: none; |
||||
svg { |
||||
position: relative; |
||||
margin-left: 2px; |
||||
top: -3px; |
||||
left: 3px; |
||||
path { |
||||
fill: $primary; |
||||
} |
||||
} |
||||
&:hover { |
||||
text-decoration: underline; |
||||
} |
||||
} |
||||
text-align: center; |
||||
color: #a3a9b5; |
||||
font-size: 13px; |
||||
margin-top: 10px; |
||||
@media (min-width: 768px) { |
||||
margin-top: 30px; |
||||
} |
||||
.download-all-transactions-link { |
||||
display: inline-flex; |
||||
align-items: center; |
||||
text-decoration: none; |
||||
svg { |
||||
position: relative; |
||||
margin-left: 2px; |
||||
top: -3px; |
||||
left: 3px; |
||||
path { |
||||
fill: $primary; |
||||
} |
||||
} |
||||
&:hover { |
||||
text-decoration: underline; |
||||
} |
||||
} |
||||
} |
||||
|
||||
.block-detail-number { |
||||
width: 25%; |
||||
@include media-breakpoint-down(sm) { |
||||
width: 60%; |
||||
} |
||||
width: 25%; |
||||
@include media-breakpoint-down(sm) { |
||||
width: 60%; |
||||
} |
||||
} |
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,217 @@ |
||||
defmodule Explorer.Chain.MapCache do |
||||
@moduledoc """ |
||||
Behaviour for a map-like cache of elements. |
||||
|
||||
A macro based on `ConCache` is provided as well, at its minimum it can be used as; |
||||
``` |
||||
use Explorer.Chain.MapCache, |
||||
name: :name, |
||||
keys: [:fst, :snd] |
||||
``` |
||||
Note: `keys` can also be set singularly with the option `key`, e.g.: |
||||
``` |
||||
use Explorer.Chain.MapCache, |
||||
name: :cache, |
||||
key: :fst, |
||||
key: :snd |
||||
``` |
||||
Additionally all of the options accepted by `ConCache.start_link/1` can be |
||||
provided as well. By default only `ttl_check_interval:` is set (to `false`). |
||||
|
||||
## Named functions |
||||
Apart from the functions defined in the behaviour, the macro will also create |
||||
3 named function for each key, for instance for the key `:fst`: |
||||
- `get_fst` |
||||
- `set_fst` |
||||
- `update_fst` |
||||
These all work as their respective counterparts with the `t:key/0` parameter. |
||||
|
||||
## Callbacks |
||||
Apart from the `callback` that can be set as part of the `ConCache` options, |
||||
two callbacks esist and can be overridden: |
||||
|
||||
`c:handle_update/3` will be called whenever an update is issued. It will receive |
||||
the `t:key/0` that is going to be updated, the current `t:value/0` that is |
||||
stored for said key and the new `t:value/0` to evaluate. |
||||
This allows to select what value to keep and do additional processing. |
||||
By default this just stores the new `t:value/0`. |
||||
|
||||
`c:handle_fallback/1` will be called whenever a get is performed and there is no |
||||
stored value for the given `t:key/0` (or when the value is `nil`). |
||||
It can return 2 different tuples: |
||||
- `{:update, value}` that will cause the value to be returned and the `t:key/0` |
||||
to be `c:update/2`d |
||||
- `{:return, value}` that will cause the value to be returned but not stored |
||||
This allows to define of a default value or perform some actions. |
||||
By default it will simply `{:return, nil}` |
||||
""" |
||||
|
||||
@type key :: atom() |
||||
|
||||
@type value :: term() |
||||
|
||||
@doc """ |
||||
An atom that identifies this cache |
||||
""" |
||||
@callback cache_name :: atom() |
||||
|
||||
@doc """ |
||||
List of `t:key/0`s that the cache contains |
||||
""" |
||||
@callback cache_keys :: [key()] |
||||
|
||||
@doc """ |
||||
Gets everything in a map |
||||
""" |
||||
@callback get_all :: map() |
||||
|
||||
@doc """ |
||||
Gets the stored `t:value/0` for a given `t:key/0` |
||||
""" |
||||
@callback get(atom()) :: value() |
||||
|
||||
@doc """ |
||||
Stores the same `t:value/0` for every `t:key/0` |
||||
""" |
||||
@callback set_all(value()) :: :ok |
||||
|
||||
@doc """ |
||||
Stores the given `t:value/0` for the given `t:key/0` |
||||
""" |
||||
@callback set(key(), value()) :: :ok |
||||
|
||||
@doc """ |
||||
Updates every `t:key/0` with the given `t:value/0` |
||||
""" |
||||
@callback update_all(value()) :: :ok |
||||
|
||||
@doc """ |
||||
Updates the given `t:key/0` (or every `t:key/0` in a list) using the given `t:value/0` |
||||
""" |
||||
@callback update(key() | [key()], value()) :: :ok |
||||
|
||||
@doc """ |
||||
Gets called during an update for the given `t:key/0` |
||||
""" |
||||
@callback handle_update(key(), value(), value()) :: {:ok, value()} | {:error, term()} |
||||
|
||||
@doc """ |
||||
Gets called when a `c:get/1` finds no `t:value/0` |
||||
""" |
||||
@callback handle_fallback(key()) :: {:update, value()} | {:return, value()} |
||||
|
||||
# credo:disable-for-next-line /Complexity/ |
||||
defmacro __using__(opts) when is_list(opts) do |
||||
# name is necessary |
||||
name = Keyword.fetch!(opts, :name) |
||||
keys = Keyword.get(opts, :keys) || Keyword.get_values(opts, :key) |
||||
|
||||
concache_params = |
||||
opts |
||||
|> Keyword.drop([:keys, :key]) |
||||
|> Keyword.put_new(:ttl_check_interval, false) |
||||
|
||||
# credo:disable-for-next-line Credo.Check.Refactor.LongQuoteBlocks |
||||
quote do |
||||
alias Explorer.Chain.MapCache |
||||
|
||||
@behaviour MapCache |
||||
|
||||
@dialyzer {:nowarn_function, handle_fallback: 1} |
||||
|
||||
@impl MapCache |
||||
def cache_name, do: unquote(name) |
||||
|
||||
@impl MapCache |
||||
def cache_keys, do: unquote(keys) |
||||
|
||||
@impl MapCache |
||||
def get_all do |
||||
Map.new(cache_keys(), fn key -> {key, get(key)} end) |
||||
end |
||||
|
||||
@impl MapCache |
||||
def get(key) do |
||||
case ConCache.get(cache_name(), key) do |
||||
nil -> |
||||
case handle_fallback(key) do |
||||
{:update, new_value} -> |
||||
update(key, new_value) |
||||
new_value |
||||
|
||||
{:return, new_value} -> |
||||
new_value |
||||
end |
||||
|
||||
value -> |
||||
value |
||||
end |
||||
end |
||||
|
||||
@impl MapCache |
||||
def set_all(value) do |
||||
Enum.each(cache_keys(), &set(&1, value)) |
||||
end |
||||
|
||||
@impl MapCache |
||||
def set(key, value) do |
||||
ConCache.put(cache_name(), key, value) |
||||
end |
||||
|
||||
@impl MapCache |
||||
def update_all(value), do: update(cache_keys(), value) |
||||
|
||||
@impl MapCache |
||||
def update(keys, value) when is_list(keys) do |
||||
Enum.each(keys, &update(&1, value)) |
||||
end |
||||
|
||||
@impl MapCache |
||||
def update(key, value) do |
||||
ConCache.update(cache_name(), key, fn old_val -> handle_update(key, old_val, value) end) |
||||
end |
||||
|
||||
### Autogenerated named functions |
||||
|
||||
unquote(Enum.map(keys, &named_functions(&1))) |
||||
|
||||
### Overridable callback functions |
||||
|
||||
@impl MapCache |
||||
def handle_update(_key, _old_value, new_value), do: {:ok, new_value} |
||||
|
||||
@impl MapCache |
||||
def handle_fallback(_key), do: {:return, nil} |
||||
|
||||
defoverridable handle_update: 3, handle_fallback: 1 |
||||
|
||||
### Supervisor's child specification |
||||
|
||||
@doc """ |
||||
The child specification for a Supervisor. Note that all the `params` |
||||
provided to this function will override the ones set by using the macro |
||||
""" |
||||
def child_spec(params \\ []) do |
||||
params = Keyword.merge(unquote(concache_params), params) |
||||
|
||||
Supervisor.child_spec({ConCache, params}, id: child_id()) |
||||
end |
||||
|
||||
def child_id, do: {ConCache, cache_name()} |
||||
end |
||||
end |
||||
|
||||
# sobelow_skip ["DOS"] |
||||
defp named_functions(key) do |
||||
quote do |
||||
# sobelow_skip ["DOS"] |
||||
def unquote(:"get_#{key}")(), do: get(unquote(key)) |
||||
|
||||
# sobelow_skip ["DOS"] |
||||
def unquote(:"set_#{key}")(value), do: set(unquote(key), value) |
||||
|
||||
# sobelow_skip ["DOS"] |
||||
def unquote(:"update_#{key}")(value), do: update(unquote(key), value) |
||||
end |
||||
end |
||||
end |
@ -1,6 +1,6 @@ |
||||
defmodule Explorer.Chain.Supply.CoinMarketCap do |
||||
defmodule Explorer.Chain.Supply.ExchangeRate do |
||||
@moduledoc """ |
||||
Defines the supply API for calculating supply for coins from coinmarketcap. |
||||
Defines the supply API for calculating supply for coins from exchange_rate.. |
||||
""" |
||||
|
||||
use Explorer.Chain.Supply |
@ -0,0 +1,89 @@ |
||||
defmodule Explorer.ChainSpec.POA.Importer do |
||||
@moduledoc """ |
||||
Imports emission reward range for POA chain. |
||||
""" |
||||
|
||||
require Logger |
||||
|
||||
alias Explorer.Chain.Wei |
||||
alias Explorer.Repo |
||||
alias Explorer.SmartContract.Reader |
||||
alias Explorer.Chain.Block.{EmissionReward, Range} |
||||
alias Explorer.ChainSpec.GenesisData |
||||
|
||||
@block_reward_amount_abi %{ |
||||
"type" => "function", |
||||
"stateMutability" => "view", |
||||
"payable" => false, |
||||
"outputs" => [%{"type" => "uint256", "name" => ""}], |
||||
"name" => "blockRewardAmount", |
||||
"inputs" => [], |
||||
"constant" => true |
||||
} |
||||
@block_reward_amount_params %{"blockRewardAmount" => []} |
||||
@emission_funds_amount_abi %{ |
||||
"type" => "function", |
||||
"stateMutability" => "view", |
||||
"payable" => false, |
||||
"outputs" => [%{"type" => "uint256", "name" => ""}], |
||||
"name" => "emissionFundsAmount", |
||||
"inputs" => [], |
||||
"constant" => true |
||||
} |
||||
@emission_funds_amount_params %{"emissionFundsAmount" => []} |
||||
@emission_funds_block_start 5_098_087 |
||||
|
||||
def import_emission_rewards do |
||||
if is_nil(rewards_contract_address()) do |
||||
Logger.warn(fn -> "No rewards contract address is defined" end) |
||||
else |
||||
block_reward = block_reward_amount() |
||||
emission_funds = emission_funds_amount() |
||||
|
||||
rewards = [ |
||||
%{ |
||||
block_range: %Range{from: 0, to: @emission_funds_block_start}, |
||||
reward: %Wei{value: block_reward} |
||||
}, |
||||
%{ |
||||
block_range: %Range{from: @emission_funds_block_start + 1, to: :infinity}, |
||||
reward: %Wei{value: Decimal.add(block_reward, emission_funds)} |
||||
} |
||||
] |
||||
|
||||
{_, nil} = Repo.delete_all(EmissionReward) |
||||
{_, nil} = Repo.insert_all(EmissionReward, rewards) |
||||
end |
||||
end |
||||
|
||||
def block_reward_amount do |
||||
call_contract(rewards_contract_address(), @block_reward_amount_abi, @block_reward_amount_params) |
||||
end |
||||
|
||||
def emission_funds_amount do |
||||
call_contract(rewards_contract_address(), @emission_funds_amount_abi, @emission_funds_amount_params) |
||||
end |
||||
|
||||
defp rewards_contract_address do |
||||
Application.get_env(:explorer, GenesisData)[:rewards_contract_address] |
||||
end |
||||
|
||||
defp call_contract(address, abi, params) do |
||||
abi = [abi] |
||||
|
||||
method_name = |
||||
params |
||||
|> Enum.map(fn {key, _value} -> key end) |
||||
|> List.first() |
||||
|
||||
Reader.query_contract(address, abi, params) |
||||
|
||||
value = |
||||
case Reader.query_contract(address, abi, params) do |
||||
%{^method_name => {:ok, [result]}} -> result |
||||
_ -> 0 |
||||
end |
||||
|
||||
Decimal.new(value) |
||||
end |
||||
end |
@ -1,52 +0,0 @@ |
||||
defmodule Explorer.ExchangeRates.Source.CoinMarketCap do |
||||
@moduledoc """ |
||||
Adapter for fetching exchange rates from https://coinmarketcap.com. |
||||
""" |
||||
|
||||
alias Explorer.ExchangeRates.{Source, Token} |
||||
|
||||
import Source, only: [decode_json: 1, to_decimal: 1] |
||||
|
||||
@behaviour Source |
||||
|
||||
@impl Source |
||||
def format_data(data) do |
||||
for item <- decode_json(data), not is_nil(item["last_updated"]) do |
||||
{last_updated_as_unix, _} = Integer.parse(item["last_updated"]) |
||||
last_updated = DateTime.from_unix!(last_updated_as_unix) |
||||
|
||||
%Token{ |
||||
available_supply: to_decimal(item["available_supply"]), |
||||
total_supply: to_decimal(item["total_supply"]), |
||||
btc_value: to_decimal(item["price_btc"]), |
||||
id: item["id"], |
||||
last_updated: last_updated, |
||||
market_cap_usd: to_decimal(item["market_cap_usd"]), |
||||
name: item["name"], |
||||
symbol: item["symbol"], |
||||
usd_value: to_decimal(item["price_usd"]), |
||||
volume_24h_usd: to_decimal(item["24h_volume_usd"]) |
||||
} |
||||
end |
||||
end |
||||
|
||||
@impl Source |
||||
def source_url do |
||||
source_url(1) |
||||
end |
||||
|
||||
def source_url(page) do |
||||
"#{base_url()}/v1/ticker/?start=#{page - 1}00" |
||||
end |
||||
|
||||
def max_page_number, do: config(:pages) |
||||
|
||||
defp base_url do |
||||
config(:base_url) || "https://api.coinmarketcap.com" |
||||
end |
||||
|
||||
@spec config(atom()) :: term |
||||
defp config(key) do |
||||
Application.get_env(:explorer, __MODULE__, [])[key] |
||||
end |
||||
end |
@ -1,59 +0,0 @@ |
||||
defmodule Explorer.ExchangeRates.Source.CoinMarketCapTest do |
||||
use ExUnit.Case |
||||
|
||||
alias Explorer.ExchangeRates.Token |
||||
alias Explorer.ExchangeRates.Source.CoinMarketCap |
||||
|
||||
@json """ |
||||
[ |
||||
{ |
||||
"id": "poa-network", |
||||
"name": "POA Network", |
||||
"symbol": "POA", |
||||
"rank": "103", |
||||
"price_usd": "0.485053", |
||||
"price_btc": "0.00007032", |
||||
"24h_volume_usd": "20185000.0", |
||||
"market_cap_usd": "98941986.0", |
||||
"available_supply": "203981804.0", |
||||
"total_supply": "254473964.0", |
||||
"max_supply": null, |
||||
"percent_change_1h": "-0.66", |
||||
"percent_change_24h": "12.34", |
||||
"percent_change_7d": "49.15", |
||||
"last_updated": "1523473200" |
||||
} |
||||
] |
||||
""" |
||||
|
||||
describe "format_data/1" do |
||||
test "returns valid tokens with valid data" do |
||||
expected_date = ~N[2018-04-11 19:00:00] |> DateTime.from_naive!("Etc/UTC") |
||||
|
||||
expected = [ |
||||
%Token{ |
||||
available_supply: Decimal.new("203981804.0"), |
||||
total_supply: Decimal.new("254473964.0"), |
||||
btc_value: Decimal.new("0.00007032"), |
||||
id: "poa-network", |
||||
last_updated: expected_date, |
||||
market_cap_usd: Decimal.new("98941986.0"), |
||||
name: "POA Network", |
||||
symbol: "POA", |
||||
usd_value: Decimal.new("0.485053"), |
||||
volume_24h_usd: Decimal.new("20185000.0") |
||||
} |
||||
] |
||||
|
||||
assert expected == CoinMarketCap.format_data(@json) |
||||
end |
||||
|
||||
test "returns nothing when given bad data" do |
||||
bad_data = """ |
||||
[{"id": "poa-network"}] |
||||
""" |
||||
|
||||
assert [] = CoinMarketCap.format_data(bad_data) |
||||
end |
||||
end |
||||
end |
File diff suppressed because it is too large
Load Diff
Loading…
Reference in new issue