Store and display native coin market cap from the DB (#7585)

* Store and display market cap from the DB

* Fix reviwer comments

* Fix reviewer comment

* Fix reviewer comments

* Process reviwer comments

* Fix failing incompatible test

* Fix test

* Fix history_chart.js

Return available_supply for backward compatibility
fix-missing-ranges-collector-test
Victor Baranov 1 year ago committed by GitHub
parent ed4553daf9
commit 171a81a45b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      CHANGELOG.md
  2. 31
      apps/block_scout_web/assets/js/lib/history_chart.js
  3. 7
      apps/block_scout_web/lib/block_scout_web/channels/address_channel.ex
  4. 7
      apps/block_scout_web/lib/block_scout_web/controllers/account/api/v1/user_controller.ex
  5. 5
      apps/block_scout_web/lib/block_scout_web/controllers/address_coin_balance_controller.ex
  6. 3
      apps/block_scout_web/lib/block_scout_web/controllers/address_contract_controller.ex
  7. 7
      apps/block_scout_web/lib/block_scout_web/controllers/address_controller.ex
  8. 3
      apps/block_scout_web/lib/block_scout_web/controllers/address_decompiled_contract_controller.ex
  9. 5
      apps/block_scout_web/lib/block_scout_web/controllers/address_internal_transaction_controller.ex
  10. 3
      apps/block_scout_web/lib/block_scout_web/controllers/address_logs_controller.ex
  11. 3
      apps/block_scout_web/lib/block_scout_web/controllers/address_read_contract_controller.ex
  12. 3
      apps/block_scout_web/lib/block_scout_web/controllers/address_read_proxy_controller.ex
  13. 3
      apps/block_scout_web/lib/block_scout_web/controllers/address_token_controller.ex
  14. 5
      apps/block_scout_web/lib/block_scout_web/controllers/address_token_transfer_controller.ex
  15. 5
      apps/block_scout_web/lib/block_scout_web/controllers/address_transaction_controller.ex
  16. 3
      apps/block_scout_web/lib/block_scout_web/controllers/address_validation_controller.ex
  17. 5
      apps/block_scout_web/lib/block_scout_web/controllers/address_withdrawal_controller.ex
  18. 3
      apps/block_scout_web/lib/block_scout_web/controllers/address_write_contract_controller.ex
  19. 3
      apps/block_scout_web/lib/block_scout_web/controllers/address_write_proxy_controller.ex
  20. 6
      apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/stats_controller.ex
  21. 3
      apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex
  22. 21
      apps/block_scout_web/lib/block_scout_web/controllers/api/v2/stats_controller.ex
  23. 31
      apps/block_scout_web/lib/block_scout_web/controllers/chain/market_history_chart_controller.ex
  24. 3
      apps/block_scout_web/lib/block_scout_web/controllers/chain_controller.ex
  25. 5
      apps/block_scout_web/lib/block_scout_web/controllers/transaction_controller.ex
  26. 3
      apps/block_scout_web/lib/block_scout_web/controllers/transaction_internal_transaction_controller.ex
  27. 3
      apps/block_scout_web/lib/block_scout_web/controllers/transaction_log_controller.ex
  28. 3
      apps/block_scout_web/lib/block_scout_web/controllers/transaction_raw_trace_controller.ex
  29. 3
      apps/block_scout_web/lib/block_scout_web/controllers/transaction_state_controller.ex
  30. 3
      apps/block_scout_web/lib/block_scout_web/controllers/transaction_token_transfer_controller.ex
  31. 5
      apps/block_scout_web/lib/block_scout_web/notifier.ex
  32. 3
      apps/block_scout_web/lib/block_scout_web/views/account/watchlist_view.ex
  33. 3
      apps/block_scout_web/lib/block_scout_web/views/api/v2/address_view.ex
  34. 3
      apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex
  35. 1
      apps/block_scout_web/test/block_scout_web/controllers/api/v2/stats_controller_test.exs
  36. 3
      apps/explorer/lib/explorer/chain/csv_export/address_transaction_csv_exporter.ex
  37. 3
      apps/explorer/lib/explorer/chain/supply/exchange_rate.ex
  38. 20
      apps/explorer/lib/explorer/exchange_rates/token.ex
  39. 120
      apps/explorer/lib/explorer/market/history/cataloger.ex
  40. 18
      apps/explorer/lib/explorer/market/history/source/market_cap.ex
  41. 60
      apps/explorer/lib/explorer/market/history/source/market_cap/coin_gecko.ex
  42. 6
      apps/explorer/lib/explorer/market/history/source/price.ex
  43. 12
      apps/explorer/lib/explorer/market/history/source/price/crypto_compare.ex
  44. 42
      apps/explorer/lib/explorer/market/market.ex
  45. 5
      apps/explorer/lib/explorer/market/market_history.ex
  46. 9
      apps/explorer/priv/repo/migrations/20230530074105_market_history_add_market_cap.exs
  47. 55
      apps/explorer/test/explorer/market/history/cataloger_test.exs
  48. 12
      apps/explorer/test/explorer/market/history/source/price/crypto_compare_test.exs
  49. 2
      apps/explorer/test/test_helper.exs

@ -10,6 +10,7 @@
- [#7653](https://github.com/blockscout/blockscout/pull/7653) - Add support for DEPOSIT and WITHDRAW token transfer event in older contracts
- [#7628](https://github.com/blockscout/blockscout/pull/7628) - Support partially verified property from verifier MS; Add property to track contracts automatically verified via eth-bytecode-db
- [#7603](https://github.com/blockscout/blockscout/pull/7603) - Add Polygon Edge and optimism genesis files support
- [#7585](https://github.com/blockscout/blockscout/pull/7585) - Store and display native coin market cap from the DB
- [#7513](https://github.com/blockscout/blockscout/pull/7513) - Add Polygon Edge support
- [#7532](https://github.com/blockscout/blockscout/pull/7532) - Handle empty id in json rpc responses
- [#7544](https://github.com/blockscout/blockscout/pull/7544) - Add ERC-1155 signatures to uncataloged_token_transfer_block_numbers

@ -14,6 +14,7 @@ Chart.register(LineController, LineElement, PointElement, LinearScale, TimeScale
// @ts-ignore
const coinName = document.getElementById('js-coin-name').value
// @ts-ignore
const chainId = document.getElementById('js-chain-id').value
const priceDataKey = `priceData${coinName}`
const txHistoryDataKey = `txHistoryData${coinName}${chainId}`
@ -188,15 +189,12 @@ function getTxHistoryData (transactionHistory) {
return data
}
function getMarketCapData (marketHistoryData, availableSupply) {
function getMarketCapData (marketHistoryData) {
if (marketHistoryData.length === 0) {
return getDataFromLocalStorage(marketCapDataKey)
}
const data = marketHistoryData.map(({ date, closingPrice }) => {
const supply = (availableSupply !== null && typeof availableSupply === 'object')
? availableSupply[date]
: availableSupply
return { x: date, y: closingPrice * supply }
const data = marketHistoryData.map(({ date, marketCap }) => {
return { x: date, y: marketCap }
})
setDataToLocalStorage(marketCapDataKey, data)
return data
@ -207,7 +205,7 @@ const priceLineColor = getPriceChartColor()
const mcapLineColor = getMarketCapChartColor()
class MarketHistoryChart {
constructor (el, availableSupply, _marketHistoryData, dataConfig) {
constructor (el, _marketHistoryData, dataConfig) {
const axes = config.options.scales
let priceActivated = true
@ -271,8 +269,6 @@ class MarketHistoryChart {
axes.numTransactions.position = 'left'
}
this.availableSupply = availableSupply
const txChartTitle = 'Daily transactions'
const marketChartTitle = 'Daily price and market cap'
let chartTitle = ''
@ -297,15 +293,9 @@ class MarketHistoryChart {
this.chart = new Chart(el, config)
}
updateMarketHistory (availableSupply, marketHistoryData) {
updateMarketHistory (marketHistoryData) {
this.price.data = getPriceData(marketHistoryData)
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.marketCap.data = getMarketCapData(marketHistoryData)
this.chart.update()
}
@ -320,17 +310,16 @@ export function createMarketHistoryChart (el) {
const dataConfig = $(el).data('history_chart_config')
const $chartError = $('[data-chart-error-message]')
const chart = new MarketHistoryChart(el, 0, [], dataConfig)
const chart = new MarketHistoryChart(el, [], dataConfig)
Object.keys(dataPaths).forEach(function (historySource) {
$.getJSON(dataPaths[historySource], { type: 'JSON' })
.done(data => {
switch (historySource) {
case 'market': {
const availableSupply = JSON.parse(data.supply_data)
const marketHistoryData = humps.camelizeKeys(JSON.parse(data.history_data))
const marketHistoryData = humps.camelizeKeys(data.history_data)
$(el).show()
chart.updateMarketHistory(availableSupply, marketHistoryData)
chart.updateMarketHistory(marketHistoryData)
break
}
case 'transaction': {

@ -18,7 +18,6 @@ defmodule BlockScoutWeb.AddressChannel do
alias Explorer.{Chain, Market, Repo}
alias Explorer.Chain.{Hash, Transaction, Wei}
alias Explorer.Chain.Hash.Address, as: AddressHash
alias Explorer.ExchangeRates.Token
alias Phoenix.View
intercept([
@ -43,7 +42,7 @@ defmodule BlockScoutWeb.AddressChannel do
with {:ok, casted_address_hash} <- AddressHash.cast(socket.assigns.address_hash),
{:ok, address = %{fetched_coin_balance: balance}} when not is_nil(balance) <-
Chain.hash_to_address(casted_address_hash),
exchange_rate <- Market.get_exchange_rate(Explorer.coin()) || Token.null(),
exchange_rate <- Market.get_coin_exchange_rate(),
{:ok, rendered} <- render_balance_card(address, exchange_rate, socket) do
reply =
{:ok,
@ -233,7 +232,7 @@ defmodule BlockScoutWeb.AddressChannel do
) do
push(socket, "current_coin_balance", %{
coin_balance: (coin_balance && coin_balance.value) || %Wei{value: Decimal.new(0)},
exchange_rate: (Market.get_exchange_rate(Explorer.coin()) || Token.null()).usd_value,
exchange_rate: Market.get_coin_exchange_rate().usd_value,
block_number: block_number
})
end
@ -248,7 +247,7 @@ defmodule BlockScoutWeb.AddressChannel do
conn: socket,
address: Chain.hash_to_address(hash),
coin_balance: (coin_balance && coin_balance.value) || %Wei{value: Decimal.new(0)},
exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null()
exchange_rate: Market.get_coin_exchange_rate()
)
rendered_link =

@ -8,7 +8,6 @@ defmodule BlockScoutWeb.Account.Api.V1.UserController do
alias Explorer.Account.Api.Key, as: ApiKey
alias Explorer.Account.CustomABI
alias Explorer.Account.{Identity, PublicTagsRequest, TagAddress, TagTransaction, WatchlistAddress}
alias Explorer.ExchangeRates.Token
alias Explorer.{Market, Repo}
alias Plug.CSRFProtection
@ -34,7 +33,7 @@ defmodule BlockScoutWeb.Account.Api.V1.UserController do
conn
|> put_status(200)
|> render(:watchlist_addresses, %{
exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(),
exchange_rate: Market.get_coin_exchange_rate(),
watchlist_addresses: watchlist_with_addresses.watchlist_addresses
})
end
@ -103,7 +102,7 @@ defmodule BlockScoutWeb.Account.Api.V1.UserController do
|> put_status(200)
|> render(:watchlist_address, %{
watchlist_address: watchlist_address,
exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null()
exchange_rate: Market.get_coin_exchange_rate()
})
end
end
@ -160,7 +159,7 @@ defmodule BlockScoutWeb.Account.Api.V1.UserController do
|> put_status(200)
|> render(:watchlist_address, %{
watchlist_address: watchlist_address,
exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null()
exchange_rate: Market.get_coin_exchange_rate()
})
end
end

@ -12,7 +12,6 @@ defmodule BlockScoutWeb.AddressCoinBalanceController do
alias BlockScoutWeb.{AccessHelper, AddressCoinBalanceView, Controller}
alias Explorer.{Chain, Market}
alias Explorer.Chain.{Address, Wei}
alias Explorer.ExchangeRates.Token
alias Indexer.Fetcher.CoinBalanceOnDemand
alias Phoenix.View
@ -76,7 +75,7 @@ defmodule BlockScoutWeb.AddressCoinBalanceController do
render(conn, "index.html",
address: address,
coin_balance_status: CoinBalanceOnDemand.trigger_fetch(address),
exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(),
exchange_rate: Market.get_coin_exchange_rate(),
current_path: Controller.current_full_path(conn),
counters_path: address_path(conn, :address_counters, %{"id" => Address.checksum(address_hash)}),
tags: get_address_tags(address_hash, current_user(conn))
@ -102,7 +101,7 @@ defmodule BlockScoutWeb.AddressCoinBalanceController do
"index.html",
address: address,
coin_balance_status: nil,
exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(),
exchange_rate: Market.get_coin_exchange_rate(),
counters_path: address_path(conn, :address_counters, %{"id" => Address.checksum(address_hash)}),
current_path: Controller.current_full_path(conn),
tags: get_address_tags(address_hash, current_user(conn))

@ -7,7 +7,6 @@ defmodule BlockScoutWeb.AddressContractController do
alias BlockScoutWeb.AccessHelper
alias Explorer.{Chain, Market}
alias Explorer.ExchangeRates.Token
alias Explorer.SmartContract.Solidity.PublishHelper
alias Indexer.Fetcher.CoinBalanceOnDemand
@ -31,7 +30,7 @@ defmodule BlockScoutWeb.AddressContractController do
"index.html",
address: address,
coin_balance_status: CoinBalanceOnDemand.trigger_fetch(address),
exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(),
exchange_rate: Market.get_coin_exchange_rate(),
counters_path: address_path(conn, :address_counters, %{"id" => address_hash_string}),
tags: get_address_tags(address_hash, current_user(conn))
)

@ -16,7 +16,6 @@ defmodule BlockScoutWeb.AddressController do
alias Explorer.{Chain, Market}
alias Explorer.Chain.Wei
alias Explorer.ExchangeRates.Token
alias Indexer.Fetcher.CoinBalanceOnDemand
alias Phoenix.View
@ -41,7 +40,7 @@ defmodule BlockScoutWeb.AddressController do
)
end
exchange_rate = Market.get_exchange_rate(Explorer.coin()) || Token.null()
exchange_rate = Market.get_coin_exchange_rate()
total_supply = Chain.total_supply()
items_count_str = Map.get(params, "items_count")
@ -101,7 +100,7 @@ defmodule BlockScoutWeb.AddressController do
"_show_address_transactions.html",
address: address,
coin_balance_status: CoinBalanceOnDemand.trigger_fetch(address),
exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(),
exchange_rate: Market.get_coin_exchange_rate(),
filter: params["filter"],
counters_path: address_path(conn, :address_counters, %{"id" => address_hash_string}),
current_path: Controller.current_full_path(conn),
@ -131,7 +130,7 @@ defmodule BlockScoutWeb.AddressController do
"_show_address_transactions.html",
address: address,
coin_balance_status: nil,
exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(),
exchange_rate: Market.get_coin_exchange_rate(),
filter: params["filter"],
counters_path: address_path(conn, :address_counters, %{"id" => address_hash_string}),
current_path: Controller.current_full_path(conn),

@ -6,7 +6,6 @@ defmodule BlockScoutWeb.AddressDecompiledContractController do
alias BlockScoutWeb.AccessHelper
alias Explorer.{Chain, Market}
alias Explorer.ExchangeRates.Token
alias Indexer.Fetcher.CoinBalanceOnDemand
def index(conn, %{"address_id" => address_hash_string} = params) do
@ -18,7 +17,7 @@ defmodule BlockScoutWeb.AddressDecompiledContractController do
"index.html",
address: address,
coin_balance_status: CoinBalanceOnDemand.trigger_fetch(address),
exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(),
exchange_rate: Market.get_coin_exchange_rate(),
counters_path: address_path(conn, :address_counters, %{"id" => address_hash_string}),
tags: get_address_tags(address_hash, current_user(conn))
)

@ -12,7 +12,6 @@ defmodule BlockScoutWeb.AddressInternalTransactionController do
alias BlockScoutWeb.{AccessHelper, Controller, InternalTransactionView}
alias Explorer.{Chain, Market}
alias Explorer.Chain.{Address, Wei}
alias Explorer.ExchangeRates.Token
alias Indexer.Fetcher.CoinBalanceOnDemand
alias Phoenix.View
@ -86,7 +85,7 @@ defmodule BlockScoutWeb.AddressInternalTransactionController do
address: address,
coin_balance_status: CoinBalanceOnDemand.trigger_fetch(address),
current_path: Controller.current_full_path(conn),
exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(),
exchange_rate: Market.get_coin_exchange_rate(),
filter: params["filter"],
counters_path: address_path(conn, :address_counters, %{"id" => address_hash_string}),
tags: get_address_tags(address_hash, current_user(conn))
@ -113,7 +112,7 @@ defmodule BlockScoutWeb.AddressInternalTransactionController do
address: address,
filter: params["filter"],
coin_balance_status: nil,
exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(),
exchange_rate: Market.get_coin_exchange_rate(),
counters_path: address_path(conn, :address_counters, %{"id" => Address.checksum(address_hash)}),
current_path: Controller.current_full_path(conn),
tags: get_address_tags(address_hash, current_user(conn))

@ -11,7 +11,6 @@ defmodule BlockScoutWeb.AddressLogsController do
alias BlockScoutWeb.{AccessHelper, AddressLogsView, Controller}
alias Explorer.{Chain, Market}
alias Explorer.ExchangeRates.Token
alias Indexer.Fetcher.CoinBalanceOnDemand
alias Phoenix.View
@ -67,7 +66,7 @@ defmodule BlockScoutWeb.AddressLogsController do
address: address,
current_path: Controller.current_full_path(conn),
coin_balance_status: CoinBalanceOnDemand.trigger_fetch(address),
exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(),
exchange_rate: Market.get_coin_exchange_rate(),
counters_path: address_path(conn, :address_counters, %{"id" => address_hash_string}),
tags: get_address_tags(address_hash, current_user(conn))
)

@ -15,7 +15,6 @@ defmodule BlockScoutWeb.AddressReadContractController do
alias BlockScoutWeb.AddressView
alias Explorer.{Chain, Market}
alias Explorer.Chain.Address
alias Explorer.ExchangeRates.Token
alias Explorer.SmartContract.Reader
alias Indexer.Fetcher.CoinBalanceOnDemand
@ -40,7 +39,7 @@ defmodule BlockScoutWeb.AddressReadContractController do
type: :regular,
action: :read,
custom_abi: custom_abi?,
exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null()
exchange_rate: Market.get_coin_exchange_rate()
]
with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string),

@ -8,7 +8,6 @@ defmodule BlockScoutWeb.AddressReadProxyController do
alias BlockScoutWeb.AccessHelper
alias Explorer.{Chain, Market}
alias Explorer.Chain.Address
alias Explorer.ExchangeRates.Token
alias Indexer.Fetcher.CoinBalanceOnDemand
def index(conn, %{"address_id" => address_hash_string} = params) do
@ -33,7 +32,7 @@ defmodule BlockScoutWeb.AddressReadProxyController do
type: :proxy,
action: :read,
coin_balance_status: CoinBalanceOnDemand.trigger_fetch(address),
exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(),
exchange_rate: Market.get_coin_exchange_rate(),
counters_path: address_path(conn, :address_counters, %{"id" => Address.checksum(address_hash)}),
tags: get_address_tags(address_hash, current_user(conn))
)

@ -8,7 +8,6 @@ defmodule BlockScoutWeb.AddressTokenController do
alias BlockScoutWeb.{AccessHelper, AddressTokenView, Controller}
alias Explorer.{Chain, Market}
alias Explorer.Chain.Address
alias Explorer.ExchangeRates.Token
alias Indexer.Fetcher.CoinBalanceOnDemand
alias Phoenix.View
@ -73,7 +72,7 @@ defmodule BlockScoutWeb.AddressTokenController do
address: address,
current_path: Controller.current_full_path(conn),
coin_balance_status: CoinBalanceOnDemand.trigger_fetch(address),
exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(),
exchange_rate: Market.get_coin_exchange_rate(),
counters_path: address_path(conn, :address_counters, %{"id" => Address.checksum(address_hash)}),
tags: get_address_tags(address_hash, current_user(conn))
)

@ -5,7 +5,6 @@ defmodule BlockScoutWeb.AddressTokenTransferController do
import BlockScoutWeb.Models.GetAddressTags, only: [get_address_tags: 2]
alias BlockScoutWeb.{AccessHelper, Controller, TransactionView}
alias Explorer.ExchangeRates.Token
alias Explorer.{Chain, Market}
alias Explorer.Chain.Address
alias Indexer.Fetcher.CoinBalanceOnDemand
@ -109,7 +108,7 @@ defmodule BlockScoutWeb.AddressTokenTransferController do
"index.html",
address: address,
coin_balance_status: CoinBalanceOnDemand.trigger_fetch(address),
exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(),
exchange_rate: Market.get_coin_exchange_rate(),
filter: params["filter"],
current_path: Controller.current_full_path(conn),
token: token,
@ -202,7 +201,7 @@ defmodule BlockScoutWeb.AddressTokenTransferController do
"index.html",
address: address,
coin_balance_status: CoinBalanceOnDemand.trigger_fetch(address),
exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(),
exchange_rate: Market.get_coin_exchange_rate(),
filter: params["filter"],
current_path: Controller.current_full_path(conn),
counters_path: address_path(conn, :address_counters, %{"id" => Address.checksum(address_hash)}),

@ -23,7 +23,6 @@ defmodule BlockScoutWeb.AddressTransactionController do
alias Explorer.Chain.Wei
alias Explorer.ExchangeRates.Token
alias Indexer.Fetcher.CoinBalanceOnDemand
alias Phoenix.View
@ -124,7 +123,7 @@ defmodule BlockScoutWeb.AddressTransactionController do
"index.html",
address: address,
coin_balance_status: CoinBalanceOnDemand.trigger_fetch(address),
exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(),
exchange_rate: Market.get_coin_exchange_rate(),
filter: params["filter"],
counters_path: address_path(conn, :address_counters, %{"id" => address_hash_string}),
current_path: Controller.current_full_path(conn),
@ -154,7 +153,7 @@ defmodule BlockScoutWeb.AddressTransactionController do
"index.html",
address: address,
coin_balance_status: nil,
exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(),
exchange_rate: Market.get_coin_exchange_rate(),
filter: params["filter"],
counters_path: address_path(conn, :address_counters, %{"id" => address_hash_string}),
current_path: Controller.current_full_path(conn),

@ -12,7 +12,6 @@ defmodule BlockScoutWeb.AddressValidationController do
import BlockScoutWeb.Models.GetAddressTags, only: [get_address_tags: 2]
alias BlockScoutWeb.{AccessHelper, BlockView, Controller}
alias Explorer.ExchangeRates.Token
alias Explorer.{Chain, Market}
alias Indexer.Fetcher.CoinBalanceOnDemand
alias Phoenix.View
@ -83,7 +82,7 @@ defmodule BlockScoutWeb.AddressValidationController do
coin_balance_status: CoinBalanceOnDemand.trigger_fetch(address),
current_path: Controller.current_full_path(conn),
counters_path: address_path(conn, :address_counters, %{"id" => address_hash_string}),
exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(),
exchange_rate: Market.get_coin_exchange_rate(),
tags: get_address_tags(address_hash, current_user(conn))
)
else

@ -16,7 +16,6 @@ defmodule BlockScoutWeb.AddressWithdrawalController do
alias Explorer.Chain.Wei
alias Explorer.ExchangeRates.Token
alias Indexer.Fetcher.CoinBalanceOnDemand
alias Phoenix.View
@ -78,7 +77,7 @@ defmodule BlockScoutWeb.AddressWithdrawalController do
"index.html",
address: address,
coin_balance_status: CoinBalanceOnDemand.trigger_fetch(address),
exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(),
exchange_rate: Market.get_coin_exchange_rate(),
counters_path: address_path(conn, :address_counters, %{"id" => address_hash_string}),
current_path: Controller.current_full_path(conn),
tags: get_address_tags(address_hash, current_user(conn))
@ -107,7 +106,7 @@ defmodule BlockScoutWeb.AddressWithdrawalController do
"index.html",
address: address,
coin_balance_status: nil,
exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(),
exchange_rate: Market.get_coin_exchange_rate(),
counters_path: address_path(conn, :address_counters, %{"id" => address_hash_string}),
current_path: Controller.current_full_path(conn),
tags: get_address_tags(address_hash, current_user(conn))

@ -15,7 +15,6 @@ defmodule BlockScoutWeb.AddressWriteContractController do
alias BlockScoutWeb.AddressView
alias Explorer.{Chain, Market}
alias Explorer.Chain.Address
alias Explorer.ExchangeRates.Token
alias Indexer.Fetcher.CoinBalanceOnDemand
def index(conn, %{"address_id" => address_hash_string} = params) do
@ -35,7 +34,7 @@ defmodule BlockScoutWeb.AddressWriteContractController do
type: :regular,
action: :write,
custom_abi: custom_abi?,
exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null()
exchange_rate: Market.get_coin_exchange_rate()
]
with false <- AddressView.contract_interaction_disabled?(),

@ -8,7 +8,6 @@ defmodule BlockScoutWeb.AddressWriteProxyController do
alias BlockScoutWeb.{AccessHelper, AddressView}
alias Explorer.{Chain, Market}
alias Explorer.Chain.Address
alias Explorer.ExchangeRates.Token
alias Indexer.Fetcher.CoinBalanceOnDemand
def index(conn, %{"address_id" => address_hash_string} = params) do
@ -34,7 +33,7 @@ defmodule BlockScoutWeb.AddressWriteProxyController do
type: :proxy,
action: :write,
coin_balance_status: CoinBalanceOnDemand.trigger_fetch(address),
exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(),
exchange_rate: Market.get_coin_exchange_rate(),
counters_path: address_path(conn, :address_counters, %{"id" => Address.checksum(address_hash)}),
tags: get_address_tags(address_hash, current_user(conn))
)

@ -3,8 +3,7 @@ defmodule BlockScoutWeb.API.RPC.StatsController do
use Explorer.Schema
alias Explorer
alias Explorer.{Chain, Etherscan, ExchangeRates}
alias Explorer.{Chain, Etherscan, Market}
alias Explorer.Chain.Cache.{AddressSum, AddressSumMinusBurnt}
alias Explorer.Chain.Wei
@ -61,8 +60,7 @@ defmodule BlockScoutWeb.API.RPC.StatsController do
end
def coinprice(conn, _params) do
symbol = Explorer.coin()
rates = ExchangeRates.lookup(symbol)
rates = Market.get_coin_exchange_rate()
render(conn, "coinprice.json", rates: rates)
end

@ -15,7 +15,6 @@ defmodule BlockScoutWeb.API.V2.AddressController do
alias BlockScoutWeb.AccessHelper
alias BlockScoutWeb.API.V2.{BlockView, TransactionView, WithdrawalView}
alias Explorer.ExchangeRates.Token
alias Explorer.{Chain, Market}
alias Indexer.Fetcher.{CoinBalanceOnDemand, TokenBalanceOnDemand}
@ -401,7 +400,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do
next_page_params = next_page_params(next_page, addresses, params)
exchange_rate = Market.get_exchange_rate(Explorer.coin()) || Token.null()
exchange_rate = Market.get_coin_exchange_rate()
total_supply = Chain.total_supply()
conn

@ -2,6 +2,7 @@ defmodule BlockScoutWeb.API.V2.StatsController do
use Phoenix.Controller
alias BlockScoutWeb.API.V2.Helper
alias BlockScoutWeb.Chain.MarketHistoryChartController
alias Explorer.{Chain, Market}
alias Explorer.Chain.Cache.Block, as: BlockCache
alias Explorer.Chain.Cache.{GasPriceOracle, GasUsage}
@ -9,7 +10,6 @@ defmodule BlockScoutWeb.API.V2.StatsController do
alias Explorer.Chain.Supply.RSK
alias Explorer.Chain.Transaction.History.TransactionStats
alias Explorer.Counters.AverageBlockTime
alias Explorer.ExchangeRates.Token
alias Timex.Duration
@api_true [api?: true]
@ -24,7 +24,7 @@ defmodule BlockScoutWeb.API.V2.StatsController do
:standard
end
exchange_rate = Market.get_exchange_rate(Explorer.coin()) || Token.null()
exchange_rate = Market.get_coin_exchange_rate()
transaction_stats = Helper.get_transaction_stats()
@ -46,7 +46,7 @@ defmodule BlockScoutWeb.API.V2.StatsController do
"total_addresses" => @api_true |> Chain.address_estimated_count() |> to_string(),
"total_transactions" => TransactionCache.estimated_count() |> to_string(),
"average_block_time" => AverageBlockTime.average_block_time() |> Duration.to_milliseconds(),
"coin_price" => exchange_rate.usd_value || Market.get_native_coin_exchange_rate_from_db(),
"coin_price" => exchange_rate.usd_value,
"total_gas_used" => GasUsage.total() |> to_string(),
"transactions_today" => Enum.at(transaction_stats, 0).number_of_transactions |> to_string(),
"gas_used_today" => Enum.at(transaction_stats, 0).gas_used,
@ -91,18 +91,19 @@ defmodule BlockScoutWeb.API.V2.StatsController do
end
def market_chart(conn, _params) do
exchange_rate = Market.get_exchange_rate(Explorer.coin()) || Token.null()
exchange_rate = Market.get_coin_exchange_rate()
recent_market_history = Market.fetch_recent_history()
current_total_supply = available_supply(Chain.supply_for_days(), exchange_rate)
market_history_data =
price_history_data =
recent_market_history
|> case do
[today | the_rest] ->
[
%{
today
| closing_price: if(exchange_rate.usd_value, do: exchange_rate.usd_value, else: today.closing_price)
| closing_price: exchange_rate.usd_value
}
| the_rest
]
@ -110,11 +111,15 @@ defmodule BlockScoutWeb.API.V2.StatsController do
data ->
data
end
|> Enum.map(fn day -> Map.take(day, [:closing_price, :date]) end)
|> Enum.map(fn day -> Map.take(day, [:closing_price, :market_cap, :date]) end)
market_history_data =
MarketHistoryChartController.encode_market_history_data(price_history_data, current_total_supply)
json(conn, %{
chart_data: market_history_data,
available_supply: available_supply(Chain.supply_for_days(), exchange_rate)
# todo: remove when new frontend is ready to use data from chart_data property only
available_supply: current_total_supply
})
end

@ -2,26 +2,27 @@ defmodule BlockScoutWeb.Chain.MarketHistoryChartController do
use BlockScoutWeb, :controller
alias Explorer.{Chain, Market}
alias Explorer.ExchangeRates.Token
def show(conn, _params) do
if ajax?(conn) do
exchange_rate = Market.get_exchange_rate(Explorer.coin()) || Token.null()
exchange_rate = Market.get_coin_exchange_rate()
recent_market_history = Market.fetch_recent_history()
current_total_supply = available_supply(Chain.supply_for_days(), exchange_rate)
market_history_data =
price_history_data =
case recent_market_history do
[today | the_rest] ->
encode_market_history_data([%{today | closing_price: exchange_rate.usd_value} | the_rest])
[%{today | closing_price: exchange_rate.usd_value} | the_rest]
data ->
encode_market_history_data(data)
data
end
market_history_data = encode_market_history_data(price_history_data, current_total_supply)
json(conn, %{
history_data: market_history_data,
supply_data: available_supply(Chain.supply_for_days(), exchange_rate)
history_data: market_history_data
})
else
unprocessable_entity(conn)
@ -41,12 +42,22 @@ defmodule BlockScoutWeb.Chain.MarketHistoryChartController do
end
end
defp encode_market_history_data(market_history_data) do
def encode_market_history_data(market_history_data, current_total_supply) when is_binary(current_total_supply) do
encode_market_history_data(market_history_data, Decimal.new(current_total_supply))
end
def encode_market_history_data(market_history_data, current_total_supply) do
market_history_data
|> Enum.map(fn day -> Map.take(day, [:closing_price, :date]) end)
|> Enum.map(fn day ->
market_cap = if day.market_cap, do: day.market_cap, else: Decimal.mult(current_total_supply, day.closing_price)
day
|> Map.put(:market_cap, market_cap)
|> Map.take([:closing_price, :market_cap, :date])
end)
|> Jason.encode()
|> case do
{:ok, data} -> data
{:ok, data} -> Jason.decode!(data)
_ -> []
end
end

@ -12,7 +12,6 @@ defmodule BlockScoutWeb.ChainController do
alias Explorer.Chain.Cache.Transaction, as: TransactionCache
alias Explorer.Chain.Supply.RSK
alias Explorer.Counters.AverageBlockTime
alias Explorer.ExchangeRates.Token
alias Explorer.Market
alias Phoenix.View
@ -31,7 +30,7 @@ defmodule BlockScoutWeb.ChainController do
:standard
end
exchange_rate = Market.get_exchange_rate(Explorer.coin()) || Token.null()
exchange_rate = Market.get_coin_exchange_rate()
transaction_stats = Helper.get_transaction_stats()

@ -25,7 +25,6 @@ defmodule BlockScoutWeb.TransactionController do
alias Explorer.{Chain, Market}
alias Explorer.Chain.Cache.Transaction, as: TransactionCache
alias Explorer.ExchangeRates.Token
alias Phoenix.View
@necessity_by_association %{
@ -161,7 +160,7 @@ defmodule BlockScoutWeb.TransactionController do
render(
conn,
"show_token_transfers.html",
exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(),
exchange_rate: Market.get_coin_exchange_rate(),
block_height: Chain.block_height(),
current_path: Controller.current_full_path(conn),
current_user: current_user(conn),
@ -199,7 +198,7 @@ defmodule BlockScoutWeb.TransactionController do
render(
conn,
"show_internal_transactions.html",
exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(),
exchange_rate: Market.get_coin_exchange_rate(),
current_path: Controller.current_full_path(conn),
current_user: current_user(conn),
block_height: Chain.block_height(),

@ -8,7 +8,6 @@ defmodule BlockScoutWeb.TransactionInternalTransactionController do
alias BlockScoutWeb.{AccessHelper, Controller, InternalTransactionView, TransactionController}
alias Explorer.{Chain, Market}
alias Explorer.ExchangeRates.Token
alias Phoenix.View
def index(conn, %{"transaction_id" => transaction_hash_string, "type" => "JSON"} = params) do
@ -102,7 +101,7 @@ defmodule BlockScoutWeb.TransactionInternalTransactionController do
render(
conn,
"index.html",
exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(),
exchange_rate: Market.get_coin_exchange_rate(),
current_path: Controller.current_full_path(conn),
current_user: current_user(conn),
block_height: Chain.block_height(),

@ -8,7 +8,6 @@ defmodule BlockScoutWeb.TransactionLogController do
alias BlockScoutWeb.{AccessHelper, Controller, TransactionController, TransactionLogView}
alias Explorer.{Chain, Market}
alias Explorer.ExchangeRates.Token
alias Phoenix.View
def index(conn, %{"transaction_id" => transaction_hash_string, "type" => "JSON"} = params) do
@ -99,7 +98,7 @@ defmodule BlockScoutWeb.TransactionLogController do
current_path: Controller.current_full_path(conn),
current_user: current_user(conn),
transaction: transaction,
exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(),
exchange_rate: Market.get_coin_exchange_rate(),
from_tags: get_address_tags(transaction.from_address_hash, current_user(conn)),
to_tags: get_address_tags(transaction.to_address_hash, current_user(conn)),
tx_tags:

@ -8,7 +8,6 @@ defmodule BlockScoutWeb.TransactionRawTraceController do
alias BlockScoutWeb.{AccessHelper, TransactionController}
alias EthereumJSONRPC
alias Explorer.{Chain, Market}
alias Explorer.ExchangeRates.Token
alias Indexer.Fetcher.FirstTraceOnDemand
def index(conn, %{"transaction_id" => hash_string} = params) do
@ -59,7 +58,7 @@ defmodule BlockScoutWeb.TransactionRawTraceController do
render(
conn,
"index.html",
exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(),
exchange_rate: Market.get_coin_exchange_rate(),
internal_transactions: internal_transactions,
block_height: Chain.block_height(),
current_user: current_user(conn),

@ -10,7 +10,6 @@ defmodule BlockScoutWeb.TransactionStateController do
}
alias Explorer.{Chain, Market}
alias Explorer.ExchangeRates.Token
alias Phoenix.View
import BlockScoutWeb.Account.AuthController, only: [current_user: 1]
@ -107,7 +106,7 @@ defmodule BlockScoutWeb.TransactionStateController do
render(
conn,
"index.html",
exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(),
exchange_rate: Market.get_coin_exchange_rate(),
block_height: Chain.block_height(),
current_path: Controller.current_full_path(conn),
show_token_transfers: Chain.transaction_has_token_transfers?(transaction_hash),

@ -8,7 +8,6 @@ defmodule BlockScoutWeb.TransactionTokenTransferController do
alias BlockScoutWeb.{AccessHelper, Controller, TransactionController, TransactionTokenTransferView}
alias Explorer.{Chain, Market}
alias Explorer.ExchangeRates.Token
alias Phoenix.View
{:ok, burn_address_hash} = Chain.string_to_address_hash("0x0000000000000000000000000000000000000000")
@ -105,7 +104,7 @@ defmodule BlockScoutWeb.TransactionTokenTransferController do
render(
conn,
"index.html",
exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(),
exchange_rate: Market.get_coin_exchange_rate(),
block_height: Chain.block_height(),
current_path: Controller.current_full_path(conn),
current_user: current_user(conn),

@ -21,7 +21,6 @@ defmodule BlockScoutWeb.Notifier do
alias Explorer.Chain.Supply.RSK
alias Explorer.Chain.Transaction.History.TransactionStats
alias Explorer.Counters.{AverageBlockTime, Helper}
alias Explorer.ExchangeRates.Token
alias Explorer.SmartContract.{CompilerVersion, Solidity.CodeCompiler}
alias Phoenix.View
@ -115,7 +114,7 @@ defmodule BlockScoutWeb.Notifier do
end
def handle_event({:chain_event, :exchange_rate}) do
exchange_rate = Market.get_exchange_rate(Explorer.coin()) || Token.null()
exchange_rate = Market.get_coin_exchange_rate()
market_history_data =
case Market.fetch_recent_history() do
@ -323,7 +322,7 @@ defmodule BlockScoutWeb.Notifier do
"balance_update",
%{
address: address,
exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null()
exchange_rate: Market.get_coin_exchange_rate()
}
)
end

@ -3,7 +3,6 @@ defmodule BlockScoutWeb.Account.WatchlistView do
alias BlockScoutWeb.Account.WatchlistAddressView
alias Explorer.Account.WatchlistAddress
alias Explorer.ExchangeRates.Token
alias Explorer.Market
alias Indexer.Fetcher.CoinBalanceOnDemand
@ -12,6 +11,6 @@ defmodule BlockScoutWeb.Account.WatchlistView do
end
def exchange_rate do
Market.get_exchange_rate(Explorer.coin()) || Token.null()
Market.get_coin_exchange_rate()
end
end

@ -8,7 +8,6 @@ defmodule BlockScoutWeb.API.V2.AddressView do
alias BlockScoutWeb.API.V2.Helper
alias Explorer.{Chain, Market}
alias Explorer.Chain.{Address, SmartContract}
alias Explorer.ExchangeRates.Token
@api_true [api?: true]
@ -78,7 +77,7 @@ defmodule BlockScoutWeb.API.V2.AddressView do
end
balance = address.fetched_coin_balance && address.fetched_coin_balance.value
exchange_rate = (Market.get_exchange_rate(Explorer.coin()) || Token.null()).usd_value
exchange_rate = Market.get_coin_exchange_rate().usd_value
creator_hash = AddressView.from_address_hash(address)
creation_tx = creator_hash && AddressView.transaction_hash(address)

@ -7,7 +7,6 @@ defmodule BlockScoutWeb.API.V2.TransactionView do
alias BlockScoutWeb.Tokens.Helper, as: TokensHelper
alias BlockScoutWeb.TransactionStateView
alias Ecto.Association.NotLoaded
alias Explorer.ExchangeRates.Token, as: TokenRate
alias Explorer.{Chain, Market}
alias Explorer.Chain.{Address, Block, InternalTransaction, Log, Token, Transaction, Wei}
alias Explorer.Chain.Block.Reward
@ -399,7 +398,7 @@ defmodule BlockScoutWeb.API.V2.TransactionView do
"token_transfers" => token_transfers(transaction.token_transfers, conn, single_tx?),
"token_transfers_overflow" => token_transfers_overflow(transaction.token_transfers, single_tx?),
"actions" => transaction_actions(transaction.transaction_actions),
"exchange_rate" => (Market.get_exchange_rate(Explorer.coin()) || TokenRate.null()).usd_value,
"exchange_rate" => Market.get_coin_exchange_rate().usd_value,
"method" => method_name(transaction, decoded_input),
"tx_types" => tx_types(transaction),
"tx_tag" => GetTransactionTags.get_transaction_tags(transaction.hash, current_user(single_tx? && conn)),

@ -42,7 +42,6 @@ defmodule BlockScoutWeb.API.V2.StatsControllerTest do
assert response = json_response(request, 200)
assert response["chart_data"] == []
assert response["available_supply"] == 0
end
end

@ -12,7 +12,6 @@ defmodule Explorer.Chain.CSVExport.AddressTransactionCsvExporter do
alias Explorer.Market.MarketHistory
alias Explorer.Chain.{Address, Transaction, Wei}
alias Explorer.Chain.CSVExport.Helper
alias Explorer.ExchangeRates.Token
@necessity_by_association [
necessity_by_association: %{
@ -32,7 +31,7 @@ defmodule Explorer.Chain.CSVExport.AddressTransactionCsvExporter do
@spec export(Address.t(), String.t(), String.t(), String.t() | nil, String.t() | nil) :: Enumerable.t()
def export(address, from_period, to_period, filter_type \\ nil, filter_value \\ nil) do
{from_block, to_block} = Helper.block_from_period(from_period, to_period)
exchange_rate = Market.get_exchange_rate(Explorer.coin()) || Token.null()
exchange_rate = Market.get_coin_exchange_rate()
address.hash
|> fetch_all_transactions(from_block, to_block, filter_type, filter_value, @paging_options)

@ -5,7 +5,6 @@ defmodule Explorer.Chain.Supply.ExchangeRate do
use Explorer.Chain.Supply
alias Explorer.ExchangeRates.Token
alias Explorer.Market
def circulating do
@ -17,6 +16,6 @@ defmodule Explorer.Chain.Supply.ExchangeRate do
end
def exchange_rate do
Market.get_exchange_rate(Explorer.coin()) || Token.null()
Market.get_coin_exchange_rate()
end
end

@ -18,16 +18,16 @@ defmodule Explorer.ExchangeRates.Token do
* `:volume_24h_usd` - The volume from the last 24 hours in USD
"""
@type t :: %__MODULE__{
available_supply: Decimal.t(),
total_supply: Decimal.t(),
btc_value: Decimal.t(),
id: String.t(),
last_updated: DateTime.t(),
market_cap_usd: Decimal.t(),
name: String.t(),
symbol: String.t(),
usd_value: Decimal.t(),
volume_24h_usd: Decimal.t()
available_supply: Decimal.t() | nil,
total_supply: Decimal.t() | nil,
btc_value: Decimal.t() | nil,
id: String.t() | nil,
last_updated: DateTime.t() | nil,
market_cap_usd: Decimal.t() | nil,
name: String.t() | nil,
symbol: String.t() | nil,
usd_value: Decimal.t() | nil,
volume_24h_usd: Decimal.t() | nil
}
@derive Jason.Encoder

@ -28,38 +28,68 @@ defmodule Explorer.Market.History.Cataloger do
@typep milliseconds :: non_neg_integer()
@price_failed_attempts 10
@market_cap_failed_attempts 3
@impl GenServer
def init(:ok) do
send(self(), {:fetch_history, 365})
send(self(), {:fetch_price_history, 365})
{:ok, %{}}
end
@impl GenServer
def handle_info({:fetch_history, day_count}, state) do
fetch_history(day_count)
def handle_info({:fetch_price_history, day_count}, state) do
fetch_price_history(day_count)
{:noreply, state}
end
@impl GenServer
def handle_info(:fetch_market_cap_history, state) do
fetch_market_cap_history()
{:noreply, state}
end
@impl GenServer
# Record fetch successful.
def handle_info({_ref, {_, _, {:ok, records}}}, state) do
Market.bulk_insert_history(records)
def handle_info({_ref, {:price_history, {_, _, {:ok, records}}}}, state) do
Process.send(self(), :fetch_market_cap_history, [])
state = state |> Map.put_new(:price_records, records)
# Schedule next check for history
fetch_after = config_or_default(:history_fetch_interval, :timer.minutes(60))
Process.send_after(self(), {:fetch_history, 1}, fetch_after)
{:noreply, state |> Map.put_new(:price_records, state)}
end
@impl GenServer
# Record fetch successful.
def handle_info({_ref, {:market_cap_history, {_, {:ok, nil}}}}, state) do
market_cap_history(state.price_records, state)
end
@impl GenServer
# Record fetch successful.
def handle_info({_ref, {:market_cap_history, {_, {:ok, market_cap_record}}}}, state) do
records = compile_records(state.price_records, market_cap_record)
market_cap_history(records, state)
end
# Failed to get records. Try again.
@impl GenServer
def handle_info({_ref, {:price_history, {day_count, failed_attempts, :error}}}, state) do
Logger.warn(fn -> "Failed to fetch price history. Trying again." end)
fetch_price_history(day_count, failed_attempts + 1)
{:noreply, state}
end
# Failed to get records. Try again.
@impl GenServer
def handle_info({_ref, {day_count, failed_attempts, :error}}, state) do
Logger.warn(fn -> "Failed to fetch market history. Trying again." end)
def handle_info({_ref, {:market_cap_history, {failed_attempts, :error}}}, state) do
Logger.warn(fn -> "Failed to fetch market cap history. Trying again." end)
fetch_history(day_count, failed_attempts + 1)
fetch_market_cap_history(failed_attempts + 1)
{:noreply, state}
end
@ -78,6 +108,16 @@ defmodule Explorer.Market.History.Cataloger do
GenServer.start_link(__MODULE__, :ok, name: __MODULE__)
end
defp market_cap_history(records, state) do
Market.bulk_insert_history(records)
# Schedule next check for history
fetch_after = config_or_default(:history_fetch_interval, :timer.minutes(60))
Process.send_after(self(), {:fetch_price_history, 1}, fetch_after)
{:noreply, state}
end
@spec base_backoff :: milliseconds()
defp base_backoff do
config_or_default(:base_backoff, 100)
@ -88,19 +128,65 @@ defmodule Explorer.Market.History.Cataloger do
Application.get_env(:explorer, __MODULE__, [])[key] || default
end
@spec source() :: module()
defp source do
config_or_default(:source, Explorer.Market.History.Source.CryptoCompare)
@spec source_price() :: module()
defp source_price do
config_or_default(:source, Explorer.Market.History.Source.Price.CryptoCompare)
end
@spec source_market_cap() :: module()
defp source_market_cap do
config_or_default(:source_market_cap, Explorer.Market.History.Source.MarketCap.CoinGecko)
end
@spec fetch_price_history(non_neg_integer(), non_neg_integer()) :: Task.t()
defp fetch_price_history(day_count, failed_attempts \\ 0) do
Task.Supervisor.async_nolink(Explorer.MarketTaskSupervisor, fn ->
Process.sleep(delay(failed_attempts))
if failed_attempts < @price_failed_attempts do
{:price_history, {day_count, failed_attempts, source_price().fetch_price_history(day_count)}}
else
{:price_history, {day_count, failed_attempts, {:ok, []}}}
end
end)
end
@spec fetch_history(non_neg_integer(), non_neg_integer()) :: Task.t()
defp fetch_history(day_count, failed_attempts \\ 0) do
@spec fetch_market_cap_history(non_neg_integer()) :: Task.t()
defp fetch_market_cap_history(failed_attempts \\ 0) do
Task.Supervisor.async_nolink(Explorer.MarketTaskSupervisor, fn ->
Process.sleep(delay(failed_attempts))
{day_count, failed_attempts, source().fetch_history(day_count)}
if failed_attempts < @market_cap_failed_attempts do
{:market_cap_history, {failed_attempts, source_market_cap().fetch_market_cap()}}
else
{:market_cap_history, {failed_attempts, {:ok, nil}}}
end
end)
end
defp compile_records(price_records, market_cap_record) do
if market_cap_record do
if Enum.empty?(price_records) do
[market_cap_record]
else
today_index =
Enum.find_index(price_records, fn price ->
price.date == market_cap_record.date
end)
today =
price_records
|> Enum.at(today_index)
|> Map.put(:market_cap, market_cap_record.market_cap)
price_records
|> List.replace_at(today_index, today)
end
else
price_records
end
end
@spec delay(non_neg_integer()) :: milliseconds()
defp delay(0), do: 0
defp delay(1), do: base_backoff()

@ -0,0 +1,18 @@
defmodule Explorer.Market.History.Source.MarketCap do
@moduledoc """
Interface for a source that allows for fetching of market cap history.
"""
@typedoc """
Record of market values for a specific date.
"""
@type record :: %{
date: Date.t(),
market_cap: Decimal.t()
}
@doc """
Fetch history for a specified amount of days in the past.
"""
@callback fetch_market_cap() :: {:ok, [record()]} | :error
end

@ -0,0 +1,60 @@
defmodule Explorer.Market.History.Source.MarketCap.CoinGecko do
@moduledoc """
Adapter for fetching current market from CoinGecko.
The current market is fetched for the configured coin. You can specify a
different coin by changing the targeted coin.
# In config.exs
config :explorer, coin: "POA"
"""
alias Explorer.ExchangeRates.Source
alias Explorer.ExchangeRates.Source.CoinGecko, as: ExchangeRatesSourceCoinGecko
alias Explorer.Market.History.Source.MarketCap, as: SourceMarketCap
@behaviour SourceMarketCap
@impl SourceMarketCap
def fetch_market_cap do
url = ExchangeRatesSourceCoinGecko.source_url()
if url do
case Source.http_request(url, ExchangeRatesSourceCoinGecko.headers()) do
{:ok, data} ->
result =
data
|> format_data()
{:ok, result}
_ ->
:error
end
else
:error
end
end
@spec date(String.t()) :: Date.t()
defp date(date_time_string) do
with {:ok, datetime, _} <- DateTime.from_iso8601(date_time_string) do
datetime
|> DateTime.to_date()
end
end
@spec format_data(term()) :: SourceMarketCap.record() | nil
defp format_data(nil), do: nil
defp format_data(data) do
market_data = data["market_data"]
market_cap = market_data["market_cap"]
%{
market_cap: Decimal.new(to_string(market_cap["usd"])),
date: date(data["last_updated"])
}
end
end

@ -1,6 +1,6 @@
defmodule Explorer.Market.History.Source do
defmodule Explorer.Market.History.Source.Price do
@moduledoc """
Interface for a source that allows for fetching of market history.
Interface for a source that allows for fetching of coin price.
"""
@typedoc """
@ -15,5 +15,5 @@ defmodule Explorer.Market.History.Source do
@doc """
Fetch history for a specified amount of days in the past.
"""
@callback fetch_history(previous_days :: non_neg_integer()) :: {:ok, [record()]} | :error
@callback fetch_price_history(previous_days :: non_neg_integer()) :: {:ok, [record()]} | :error
end

@ -1,4 +1,4 @@
defmodule Explorer.Market.History.Source.CryptoCompare do
defmodule Explorer.Market.History.Source.Price.CryptoCompare do
@moduledoc """
Adapter for fetching market history from https://cryptocompare.com.
@ -10,15 +10,15 @@ defmodule Explorer.Market.History.Source.CryptoCompare do
"""
alias Explorer.Market.History.Source
alias Explorer.Market.History.Source.Price, as: SourcePrice
alias HTTPoison.Response
@behaviour Source
@behaviour SourcePrice
@typep unix_timestamp :: non_neg_integer()
@impl Source
def fetch_history(previous_days) do
@impl SourcePrice
def fetch_price_history(previous_days) do
url = history_url(previous_days)
headers = [{"Content-Type", "application/json"}]
@ -49,7 +49,7 @@ defmodule Explorer.Market.History.Source.CryptoCompare do
|> DateTime.to_date()
end
@spec format_data(String.t()) :: [Source.record()]
@spec format_data(String.t()) :: [SourcePrice.record()]
defp format_data(data) do
json = Jason.decode!(data)

@ -7,14 +7,6 @@ defmodule Explorer.Market do
alias Explorer.Market.{MarketHistory, MarketHistoryCache}
alias Explorer.{ExchangeRates, Repo}
@doc """
Get most recent exchange rate for the given symbol.
"""
@spec get_exchange_rate(String.t()) :: Token.t() | nil
def get_exchange_rate(symbol) do
ExchangeRates.lookup(symbol)
end
@doc """
Retrieves the history for the recent specified amount of days.
@ -28,7 +20,7 @@ defmodule Explorer.Market do
@doc """
Retrieves today's native coin exchange rate from the database.
"""
@spec get_native_coin_exchange_rate_from_db() :: Token.t() | nil
@spec get_native_coin_exchange_rate_from_db() :: Token.t()
def get_native_coin_exchange_rate_from_db do
today =
case fetch_recent_history() do
@ -37,22 +29,48 @@ defmodule Explorer.Market do
end
if today do
Map.get(today, :closing_price)
%Token{
usd_value: Map.get(today, :closing_price),
market_cap_usd: Map.get(today, :market_cap),
available_supply: nil,
total_supply: nil,
btc_value: nil,
id: nil,
last_updated: nil,
name: nil,
symbol: nil,
volume_24h_usd: nil
}
else
nil
Token.null()
end
end
@doc """
Get most recent exchange rate for the native coin from ETS or from DB.
"""
@spec get_coin_exchange_rate() :: Token.t() | nil
def get_coin_exchange_rate do
get_exchange_rate(Explorer.coin()) || get_native_coin_exchange_rate_from_db() || Token.null()
end
@doc false
def bulk_insert_history(records) do
records_without_zeroes =
records
|> Enum.reject(fn item ->
Decimal.equal?(item.closing_price, 0) && Decimal.equal?(item.opening_price, 0)
Map.has_key?(item, :opening_price) && Map.has_key?(item, :closing_price) &&
Decimal.equal?(item.closing_price, 0) &&
Decimal.equal?(item.opening_price, 0)
end)
# Enforce MarketHistory ShareLocks order (see docs: sharelocks.md)
|> Enum.sort_by(& &1.date)
Repo.insert_all(MarketHistory, records_without_zeroes, on_conflict: :nothing, conflict_target: [:date])
end
@spec get_exchange_rate(String.t()) :: Token.t() | nil
defp get_exchange_rate(symbol) do
ExchangeRates.lookup(symbol)
end
end

@ -9,6 +9,7 @@ defmodule Explorer.Market.MarketHistory do
field(:closing_price, :decimal)
field(:date, :date)
field(:opening_price, :decimal)
field(:market_cap, :decimal)
end
@typedoc """
@ -17,10 +18,12 @@ defmodule Explorer.Market.MarketHistory do
* `:closing_price` - Closing price in USD.
* `:date` - The date in UTC.
* `:opening_price` - Opening price in USD.
* `:market_cap` - Market cap in USD.
"""
@type t :: %__MODULE__{
closing_price: Decimal.t(),
date: Date.t(),
opening_price: Decimal.t()
opening_price: Decimal.t(),
market_cap: Decimal.t()
}
end

@ -0,0 +1,9 @@
defmodule Explorer.Repo.Migrations.MarketHistoryAddMarketCap do
use Ecto.Migration
def change do
alter table(:market_history) do
add(:market_cap, :decimal)
end
end
end

@ -5,7 +5,7 @@ defmodule Explorer.Market.History.CatalogerTest do
alias Explorer.Market.MarketHistory
alias Explorer.Market.History.Cataloger
alias Explorer.Market.History.Source.TestSource
alias Explorer.Market.History.Source.Price.TestSource
alias Explorer.Repo
setup do
@ -15,27 +15,58 @@ defmodule Explorer.Market.History.CatalogerTest do
test "init" do
assert {:ok, %{}} == Cataloger.init(:ok)
assert_received {:fetch_history, 365}
assert_received {:fetch_price_history, 365}
end
test "handle_info with `{:fetch_history, days}`" do
test "handle_info with `{:fetch_price_history, days}`" do
records = [%{date: ~D[2018-04-01], closing_price: Decimal.new(10), opening_price: Decimal.new(5)}]
expect(TestSource, :fetch_history, fn 1 -> {:ok, records} end)
expect(TestSource, :fetch_price_history, fn 1 -> {:ok, records} end)
set_mox_global()
state = %{}
assert {:noreply, state} == Cataloger.handle_info({:fetch_history, 1}, state)
assert_receive {_ref, {1, 0, {:ok, ^records}}}
assert {:noreply, state} == Cataloger.handle_info({:fetch_price_history, 1}, state)
assert_receive {_ref, {:price_history, {1, 0, {:ok, ^records}}}}
end
test "handle_info with successful task" do
test "handle_info with successful tasks (price and market cap)" do
Application.put_env(:explorer, Cataloger, history_fetch_interval: 1)
record = %{date: ~D[2018-04-01], closing_price: Decimal.new(10), opening_price: Decimal.new(5)}
state = %{}
record_price = %{date: ~D[2018-04-01], closing_price: Decimal.new(10), opening_price: Decimal.new(5)}
record_market_cap = %{date: ~D[2018-04-01], market_cap: Decimal.new(100_500)}
state = %{
price_records: [
record_price
]
}
assert {:noreply, state} == Cataloger.handle_info({nil, {:price_history, {1, 0, {:ok, [record_price]}}}}, state)
assert_receive :fetch_market_cap_history
assert {:noreply, state} ==
Cataloger.handle_info({nil, {:market_cap_history, {0, {:ok, record_market_cap}}}}, state)
assert Repo.get_by(MarketHistory, date: record_price.date)
end
test "handle_info with successful price task" do
Application.put_env(:explorer, Cataloger, history_fetch_interval: 1)
record_price = %{date: ~D[2018-04-01], closing_price: Decimal.new(10), opening_price: Decimal.new(5)}
record_market_cap = nil
state = %{
price_records: [
record_price
]
}
assert {:noreply, state} == Cataloger.handle_info({nil, {:price_history, {1, 0, {:ok, [record_price]}}}}, state)
assert_receive :fetch_market_cap_history
assert {:noreply, state} ==
Cataloger.handle_info({nil, {:market_cap_history, {0, {:ok, record_market_cap}}}}, state)
assert {:noreply, state} == Cataloger.handle_info({nil, {1, 0, {:ok, [record]}}}, state)
assert_receive {:fetch_history, 1}
assert Repo.get_by(MarketHistory, date: record.date)
assert record = Repo.get_by(MarketHistory, date: record_price.date)
assert record.market_cap == nil
end
test "handle info for DOWN message" do

@ -1,7 +1,7 @@
defmodule Explorer.Market.History.Source.CryptoCompareTest do
defmodule Explorer.Market.History.Source.Price.CryptoCompareTest do
use ExUnit.Case, async: false
alias Explorer.Market.History.Source.CryptoCompare
alias Explorer.Market.History.Source.Price.CryptoCompare
alias Plug.Conn
@json """
@ -48,7 +48,7 @@ defmodule Explorer.Market.History.Source.CryptoCompareTest do
}
"""
describe "fetch_history/1" do
describe "fetch_price_history/1" do
setup do
bypass = Bypass.open()
Application.put_env(:explorer, CryptoCompare, base_url: "http://localhost:#{bypass.port}")
@ -77,14 +77,14 @@ defmodule Explorer.Market.History.Source.CryptoCompareTest do
}
]
assert {:ok, expected} == CryptoCompare.fetch_history(3)
assert {:ok, expected} == CryptoCompare.fetch_price_history(3)
end
test "with errored request", %{bypass: bypass} do
error_text = ~S({"error": "server error"})
Bypass.expect(bypass, fn conn -> Conn.resp(conn, 500, error_text) end)
assert :error == CryptoCompare.fetch_history(3)
assert :error == CryptoCompare.fetch_price_history(3)
end
test "rejects empty prices", %{bypass: bypass} do
@ -138,7 +138,7 @@ defmodule Explorer.Market.History.Source.CryptoCompareTest do
%{closing_price: Decimal.from_float(8804.32), date: ~D[2018-04-26], opening_price: Decimal.from_float(8873.57)}
]
assert {:ok, expected} == CryptoCompare.fetch_history(3)
assert {:ok, expected} == CryptoCompare.fetch_price_history(3)
end
end
end

@ -15,7 +15,7 @@ Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo, :auto)
Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.Account, :auto)
Mox.defmock(Explorer.ExchangeRates.Source.TestSource, for: Explorer.ExchangeRates.Source)
Mox.defmock(Explorer.Market.History.Source.TestSource, for: Explorer.Market.History.Source)
Mox.defmock(Explorer.Market.History.Source.Price.TestSource, for: Explorer.Market.History.Source.Price)
Mox.defmock(Explorer.History.TestHistorian, for: Explorer.History.Historian)
Mox.defmock(EthereumJSONRPC.Mox, for: EthereumJSONRPC.Transport)

Loading…
Cancel
Save