get upstream from master

pull/2659/head
Victor Baranov 5 years ago
commit 8e70b83d65
  1. 9
      CHANGELOG.md
  2. 29
      README.md
  3. 6
      apps/block_scout_web/assets/js/lib/try_api.js
  4. 12
      apps/block_scout_web/assets/js/lib/try_eth_api.js
  5. 6
      apps/block_scout_web/assets/package-lock.json
  6. 2
      apps/block_scout_web/assets/package.json
  7. 2
      apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/block_controller.ex
  8. 4
      apps/block_scout_web/lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex
  9. 23
      apps/block_scout_web/lib/block_scout_web/views/api_docs_view.ex
  10. 9
      apps/block_scout_web/lib/block_scout_web/views/chain_view.ex
  11. 6
      apps/block_scout_web/lib/block_scout_web/views/transaction_view.ex
  12. 4
      apps/block_scout_web/lib/block_scout_web/views/wei_helpers.ex
  13. 1430
      apps/block_scout_web/priv/gettext/default.pot
  14. 1439
      apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po
  15. 4
      apps/block_scout_web/test/block_scout_web/controllers/block_controller_test.exs
  16. 4
      apps/block_scout_web/test/block_scout_web/controllers/chain_controller_test.exs
  17. 2
      apps/block_scout_web/test/block_scout_web/features/viewing_addresses_test.exs
  18. 4
      apps/block_scout_web/test/block_scout_web/features/viewing_chain_test.exs
  19. 8
      apps/block_scout_web/test/block_scout_web/views/address_coin_balance_view_test.exs
  20. 44
      apps/block_scout_web/test/block_scout_web/views/api_docs_view_test.exs
  21. 2
      apps/block_scout_web/test/block_scout_web/views/block_view_test.exs
  22. 7
      apps/block_scout_web/test/block_scout_web/views/transaction_view_test.exs
  23. 3
      apps/explorer/config/config.exs
  24. 9
      apps/explorer/lib/explorer/application.ex
  25. 38
      apps/explorer/lib/explorer/chain.ex
  26. 2
      apps/explorer/lib/explorer/chain/address.ex
  27. 146
      apps/explorer/lib/explorer/chain/cache/block_count.ex
  28. 87
      apps/explorer/lib/explorer/chain/cache/block_number.ex
  29. 47
      apps/explorer/lib/explorer/chain/cache/net_version.ex
  30. 154
      apps/explorer/lib/explorer/chain/cache/transaction_count.ex
  31. 217
      apps/explorer/lib/explorer/chain/map_cache.ex
  32. 4
      apps/explorer/lib/explorer/chain/supply/exchange_rate.ex
  33. 2
      apps/explorer/lib/explorer/chain/supply/rsk.ex
  34. 32
      apps/explorer/lib/explorer/exchange_rates/source.ex
  35. 39
      apps/explorer/lib/explorer/exchange_rates/source/coin_gecko.ex
  36. 52
      apps/explorer/lib/explorer/exchange_rates/source/coin_market_cap.ex
  37. 12
      apps/explorer/lib/explorer/exchange_rates/source/token_bridge.ex
  38. 26
      apps/explorer/test/explorer/chain/cache/block_count_test.exs
  39. 50
      apps/explorer/test/explorer/chain/cache/block_number_test.exs
  40. 4
      apps/explorer/test/explorer/chain/cache/blocks_test.exs
  41. 26
      apps/explorer/test/explorer/chain/cache/transaction_count_test.exs
  42. 24
      apps/explorer/test/explorer/chain_test.exs
  43. 57
      apps/explorer/test/explorer/exchange_rates/source/coin_gecko_test.exs
  44. 59
      apps/explorer/test/explorer/exchange_rates/source/coin_market_cap_test.exs
  45. 47
      apps/explorer/test/explorer/exchange_rates/source/token_bridge_test.exs
  46. 3
      apps/explorer/test/support/data_case.ex
  47. 1806
      apps/explorer/test/support/fixture/exchange_rates/coin_gecko.json
  48. 9
      apps/indexer/lib/indexer/block/fetcher.ex
  49. 2
      apps/indexer/lib/indexer/fetcher/coin_balance_on_demand.ex
  50. 103
      docs/env-variables.md

@ -1,13 +1,16 @@
## Current ## Current
### Features ### Features
- [#2555](https://github.com/poanetwork/blockscout/pull/2555) - find and show decoding candidates for logs
- [#2596](https://github.com/poanetwork/blockscout/pull/2596) - support AuRa's empty step reward type - [#2596](https://github.com/poanetwork/blockscout/pull/2596) - support AuRa's empty step reward type
- [#2581](https://github.com/poanetwork/blockscout/pull/2581) - Add generic Map-like Cache behaviour and implementation
- [#2561](https://github.com/poanetwork/blockscout/pull/2561) - Add token's type to the response of tokenlist method - [#2561](https://github.com/poanetwork/blockscout/pull/2561) - Add token's type to the response of tokenlist method
- [#2555](https://github.com/poanetwork/blockscout/pull/2555) - find and show decoding candidates for logs
- [#2499](https://github.com/poanetwork/blockscout/pull/2499) - import emission reward ranges - [#2499](https://github.com/poanetwork/blockscout/pull/2499) - import emission reward ranges
- [#2497](https://github.com/poanetwork/blockscout/pull/2497) - Add generic Ordered Cache behaviour and implementation - [#2497](https://github.com/poanetwork/blockscout/pull/2497) - Add generic Ordered Cache behaviour and implementation
### Fixes ### Fixes
- [#2468](https://github.com/poanetwork/blockscout/pull/2468) - fix confirmations for non consensus blocks
- [#2610](https://github.com/poanetwork/blockscout/pull/2610) - use CoinGecko instead of CoinMarketcap for exchange rates
- [#2640](https://github.com/poanetwork/blockscout/pull/2640) - SVG network icons - [#2640](https://github.com/poanetwork/blockscout/pull/2640) - SVG network icons
- [#2635](https://github.com/poanetwork/blockscout/pull/2635) - optimize ERC721 inventory query - [#2635](https://github.com/poanetwork/blockscout/pull/2635) - optimize ERC721 inventory query
- [#2626](https://github.com/poanetwork/blockscout/pull/2626) - Fixing 2 Mobile UI Issues - [#2626](https://github.com/poanetwork/blockscout/pull/2626) - Fixing 2 Mobile UI Issues
@ -15,6 +18,7 @@
- [#2616](https://github.com/poanetwork/blockscout/pull/2616) - deduplicate coin history records by delta - [#2616](https://github.com/poanetwork/blockscout/pull/2616) - deduplicate coin history records by delta
- [#2613](https://github.com/poanetwork/blockscout/pull/2613) - fix getminedblocks rpc endpoint - [#2613](https://github.com/poanetwork/blockscout/pull/2613) - fix getminedblocks rpc endpoint
- [#2592](https://github.com/poanetwork/blockscout/pull/2592) - process new metadata format for whisper - [#2592](https://github.com/poanetwork/blockscout/pull/2592) - process new metadata format for whisper
- [#2591](https://github.com/poanetwork/blockscout/pull/2591) - Fix url error in API page
- [#2572](https://github.com/poanetwork/blockscout/pull/2572) - Ease non-critical css - [#2572](https://github.com/poanetwork/blockscout/pull/2572) - Ease non-critical css
- [#2570](https://github.com/poanetwork/blockscout/pull/2570) - Network icons preload - [#2570](https://github.com/poanetwork/blockscout/pull/2570) - Network icons preload
- [#2569](https://github.com/poanetwork/blockscout/pull/2569) - do not fetch emission rewards for transactions csv exporter - [#2569](https://github.com/poanetwork/blockscout/pull/2569) - do not fetch emission rewards for transactions csv exporter
@ -24,9 +28,10 @@
- [#2538](https://github.com/poanetwork/blockscout/pull/2538) - fetch the last not empty coin balance records - [#2538](https://github.com/poanetwork/blockscout/pull/2538) - fetch the last not empty coin balance records
### Chore ### Chore
- [#2646](https://github.com/poanetwork/blockscout/pull/2646) - Added Xerom to list of Additional Chains using BlockScout
- [#2634](https://github.com/poanetwork/blockscout/pull/2634) - add Lukso to networks dropdown - [#2634](https://github.com/poanetwork/blockscout/pull/2634) - add Lukso to networks dropdown
- [#2611](https://github.com/poanetwork/blockscout/pull/2611) - fix js dependency vulnerabilities
- [#2617](https://github.com/poanetwork/blockscout/pull/2617) - skip cache update if there are no blocks inserted - [#2617](https://github.com/poanetwork/blockscout/pull/2617) - skip cache update if there are no blocks inserted
- [#2611](https://github.com/poanetwork/blockscout/pull/2611) - fix js dependency vulnerabilities
- [#2594](https://github.com/poanetwork/blockscout/pull/2594) - do not start genesis data fetching periodically - [#2594](https://github.com/poanetwork/blockscout/pull/2594) - do not start genesis data fetching periodically
- [#2590](https://github.com/poanetwork/blockscout/pull/2590) - restore backward compatablity with old releases - [#2590](https://github.com/poanetwork/blockscout/pull/2590) - restore backward compatablity with old releases
- [#2577](https://github.com/poanetwork/blockscout/pull/2577) - Need recompile column in the env vars table - [#2577](https://github.com/poanetwork/blockscout/pull/2577) - Need recompile column in the env vars table

@ -28,20 +28,21 @@ Currently available full-featured block explorers (Etherscan, Etherchain, Blockc
## Supported Projects ## Supported Projects
| **Hosted Mainnets** | **Hosted Testnets** | **Additional Chains using BlockScout** | | **Hosted Mainnets** | **Hosted Testnets** | **Additional Chains using BlockScout** | **Additional Chains using BlockScout** |
|--------------------------------------------------------|-------------------------------------------------------|----------------------------------------------------| |--------------------------------------------------------|-------------------------------------------------------|------------------------------------------------------|----------------------------------------------------------------|
| [Aerum](https://blockscout.com/aerum/mainnet) | [Goerli Testnet](https://blockscout.com/eth/goerli) | [ARTIS](https://explorer.sigma1.artis.network) | | [Callisto](https://blockscout.com/callisto/mainnet) | [Goerli Testnet](https://blockscout.com/eth/goerli) | [ARTIS](https://explorer.sigma1.artis.network) | [Celo Testnet](https://alfajores-blockscout.celo-testnet.org/) |
| [Callisto](https://blockscout.com/callisto/mainnet) | [Kovan Testnet](https://blockscout.com/eth/kovan) | [Ether-1](https://blocks.ether1.wattpool.net/) | | [Ethereum Classic](https://blockscout.com/etc/mainnet) | [Kovan Testnet](https://blockscout.com/eth/kovan) | [Ether-1](https://blocks.ether1.wattpool.net/) | [Matic Testnet](https://explorer.testnet2.matic.network/) |
| [Ethereum Classic](https://blockscout.com/etc/mainnet) | [POA Sokol Testnet](https://blockscout.com/poa/sokol) | [Fuse Network](https://explorer.fuse.io/) | | [Ethereum Mainnet](https://blockscout.com/eth/mainnet) | [LUKSO L14 Testnet](https://blockscout.com/lukso/l14) | [Kotti Testnet](https://kottiexplorer.ethernode.io/) | |
| [Ethereum Mainnet](https://blockscout.com/eth/mainnet) | [Rinkeby Testnet](https://blockscout.com/eth/rinkeby) | [Oasis Labs](https://blockexplorer.oasiscloud.io/) | | [POA Core Network](https://blockscout.com/poa/core) | [POA Sokol Testnet](https://blockscout.com/poa/sokol) | [Fuse Network](https://explorer.fuse.io/) | |
| [POA Core Network](https://blockscout.com/poa/core) | [Ropsten Testnet](https://blockscout.com/eth/ropsten) | [Petrichor](https://explorer.petrachor.com/) | | [RSK](https://blockscout.com/rsk/mainnet) | [Rinkeby Testnet](https://blockscout.com/eth/rinkeby) | [Oasis Labs](https://blockexplorer.oasiscloud.io/) | |
| [RSK](https://blockscout.com/rsk/mainnet) | | [PIRL](http://pirl.es/) | | [xDai Chain](https://blockscout.com/poa/dai) | [Ropsten Testnet](https://blockscout.com/eth/ropsten) | [Petrichor](https://explorer.petrachor.com/) | |
| [xDai Chain](https://blockscout.com/poa/dai) | | [SafeChain](https://explorer.safechain.io) | | | | [PIRL](http://pirl.es/) | |
| | | [SpringChain](https://explorer.springrole.com/) | | | | [SafeChain](https://explorer.safechain.io) | |
| | | [Kotti Testnet](https://kottiexplorer.ethernode.io/) | | | | [SpringChain](https://explorer.springrole.com/) | |
| | | [Loom](http://plasma-blockexplorer.dappchains.com/) | | | | [Tenda](https://tenda.network) | |
| | | [Tenda](https://tenda.network) | | | | [Loom](http://plasma-blockexplorer.dappchains.com/) | |
| | | [GoJoy Chain](https://gojoychain.com/) | | | | [GoJoy Chain](https://gojoychain.com/) | |
| | | [Xerom](https://blocks.xerom.org/) | |
Current BlockScout versions for hosted projects are available [on the forum](https://forum.poa.network/t/deployed-instances-on-blockscout-com/1938). Current BlockScout versions for hosted projects are available [on the forum](https://forum.poa.network/t/deployed-instances-on-blockscout-com/1938).

@ -55,6 +55,10 @@ function handleSuccess (query, xhr, clickedButton) {
clickedButton.prop('disabled', false) clickedButton.prop('disabled', false)
} }
function dropDomain (url) {
return new URL(url).pathname
}
// Show 'Try it out' UI for a module/action. // Show 'Try it out' UI for a module/action.
$('button[data-selector*="btn-try-api"]').click(event => { $('button[data-selector*="btn-try-api"]').click(event => {
const clickedButton = $(event.target) const clickedButton = $(event.target)
@ -124,7 +128,7 @@ $('button[data-try-api-ui-button-type="execute"]').click(event => {
} }
$.ajax({ $.ajax({
url: `/api${query}`, url: dropDomain(composeRequestUrl(query)),
success: (_data, _status, xhr) => { success: (_data, _status, xhr) => {
handleSuccess(query, xhr, clickedButton) handleSuccess(query, xhr, clickedButton)
}, },

@ -1,7 +1,8 @@
import $ from 'jquery' import $ from 'jquery'
function composeCurlCommand (data) { function composeCurlCommand (data) {
return `curl -H "content-type: application/json" -X POST --data '${JSON.stringify(data)}'` const url = $('[data-endpoint-url]').attr('data-endpoint-url')
return `curl -H "content-type: application/json" -X POST --data '${JSON.stringify(data)}' ${url}`
} }
function handleResponse (data, xhr, clickedButton) { function handleResponse (data, xhr, clickedButton) {
@ -43,6 +44,10 @@ function parseInput (input) {
} }
} }
function dropDomain (url) {
return new URL(url).pathname
}
$('button[data-try-eth-api-ui-button-type="execute"]').click(event => { $('button[data-try-eth-api-ui-button-type="execute"]').click(event => {
const clickedButton = $(event.target) const clickedButton = $(event.target)
const module = clickedButton.attr('data-module') const module = clickedButton.attr('data-module')
@ -50,7 +55,6 @@ $('button[data-try-eth-api-ui-button-type="execute"]').click(event => {
const inputs = $(`input[data-selector="${module}-${action}-try-api-ui"]`) const inputs = $(`input[data-selector="${module}-${action}-try-api-ui"]`)
const params = $.map(inputs, parseInput) const params = $.map(inputs, parseInput)
const formData = wrapJsonRpc(action, params) const formData = wrapJsonRpc(action, params)
console.log(formData)
const loadingText = '<span class="loading-spinner-small mr-2"><span class="loading-spinner-block-1"></span><span class="loading-spinner-block-2"></span></span> Loading...' const loadingText = '<span class="loading-spinner-small mr-2"><span class="loading-spinner-block-1"></span><span class="loading-spinner-block-2"></span></span> Loading...'
clickedButton.prop('disabled', true) clickedButton.prop('disabled', true)
@ -60,8 +64,10 @@ $('button[data-try-eth-api-ui-button-type="execute"]').click(event => {
clickedButton.html(loadingText) clickedButton.html(loadingText)
} }
const url = $('[data-endpoint-url]').attr('data-endpoint-url')
$.ajax({ $.ajax({
url: '/api/eth_rpc', url: dropDomain(url),
type: 'POST', type: 'POST',
data: JSON.stringify(formData), data: JSON.stringify(formData),
dataType: 'json', dataType: 'json',

@ -6958,9 +6958,9 @@
} }
}, },
"lodash": { "lodash": {
"version": "4.17.13", "version": "4.17.15",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.13.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
"integrity": "sha512-vm3/XWXfWtRua0FkUyEHBZy8kCPjErNBT9fJx8Zvs+U6zjqPbTUOpkaoum3O5uiA8sm+yNMHXfYkTUHFoMxFNA==" "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
}, },
"lodash.assign": { "lodash.assign": {
"version": "4.2.0", "version": "4.2.0",

@ -30,7 +30,7 @@
"highlightjs-solidity": "^1.0.6", "highlightjs-solidity": "^1.0.6",
"humps": "^2.0.1", "humps": "^2.0.1",
"jquery": "^3.4.0", "jquery": "^3.4.0",
"lodash": "^4.17.13", "lodash": "^4.17.15",
"moment": "^2.22.1", "moment": "^2.22.1",
"nanomorph": "^5.1.3", "nanomorph": "^5.1.3",
"numeral": "^2.0.6", "numeral": "^2.0.6",

@ -26,7 +26,7 @@ defmodule BlockScoutWeb.API.RPC.BlockController do
def eth_block_number(conn, params) do def eth_block_number(conn, params) do
id = Map.get(params, "id", 1) id = Map.get(params, "id", 1)
max_block_number = BlockNumber.max_number() max_block_number = BlockNumber.get_max()
render(conn, :eth_block_number, number: max_block_number, id: id) render(conn, :eth_block_number, number: max_block_number, id: id)
end end

@ -4,9 +4,9 @@
<h3 class="api-doc-list-item-title"><%= @action %></h3> <h3 class="api-doc-list-item-title"><%= @action %></h3>
<p class="api-doc-list-item-contents"><%= raw @info.notes %></p> <p class="api-doc-list-item-contents"><%= raw @info.notes %></p>
<span class="api-doc-list-item-query api-text-monospace api-text-monospace-background btn" <span class="api-doc-list-item-query api-text-monospace api-text-monospace-background btn"
data-clipboard-text="curl -X POST --data '{&quot;id&quot;:0,&quot;jsonrpc&quot;:&quot;2.0&quot;,&quot;method&quot;: &quot;<%= @action %>&quot;, params: []}'" data-clipboard-text="curl -X POST --data '{&quot;id&quot;:0,&quot;jsonrpc&quot;:&quot;2.0&quot;,&quot;method&quot;: &quot;<%= @action %>&quot;,&quot;params&quot;: []}'"
> >
curl -X POST --data '{"id":0,"jsonrpc":"2.0","method": "<%= @action %>", params: []}' curl -X POST --data '{"id":0,"jsonrpc":"2.0","method": "<%= @action %>", "params": []}'
</span> </span>
<p class="api-doc-list-item-text"> <p class="api-doc-list-item-text">
<div class="tile tile-muted p-1"> <div class="tile tile-muted p-1">

@ -1,7 +1,7 @@
defmodule BlockScoutWeb.APIDocsView do defmodule BlockScoutWeb.APIDocsView do
use BlockScoutWeb, :view use BlockScoutWeb, :view
alias BlockScoutWeb.{Endpoint, LayoutView} alias BlockScoutWeb.LayoutView
def action_tile_id(module, action) do def action_tile_id(module, action) do
"#{module}-#{action}" "#{module}-#{action}"
@ -34,30 +34,27 @@ defmodule BlockScoutWeb.APIDocsView do
end) end)
end end
def blockscout_url do defp blockscout_url do
url_params = Application.get_env(:block_scout_web, BlockScoutWeb.Endpoint)[:url] url_params = Application.get_env(:block_scout_web, BlockScoutWeb.Endpoint)[:url]
host = url_params[:host] host = url_params[:host]
path = url_params[:path] path = url_params[:path]
scheme = url_params[:scheme] scheme = Keyword.get(url_params, :scheme, "http")
if host != "localhost" do if host != "localhost" do
scheme <> "://" <> host <> path "#{scheme}://#{host}#{path}"
else else
Endpoint.url() port = Application.get_env(:block_scout_web, BlockScoutWeb.Endpoint)[:http][:port]
"#{scheme}://#{host}:#{to_string(port)}"
end end
end end
def api_url do def api_url do
handle_slash("api") blockscout_url()
|> Path.join("api")
end end
def eth_rpc_api_url do def eth_rpc_api_url do
handle_slash("api/eth_rpc") blockscout_url()
end |> Path.join("api/eth_rpc")
defp handle_slash(path) do
base_url = blockscout_url()
Path.join(base_url, path)
end end
end end

@ -3,6 +3,15 @@ defmodule BlockScoutWeb.ChainView do
alias BlockScoutWeb.LayoutView alias BlockScoutWeb.LayoutView
defp market_cap(:standard, %{available_supply: available_supply, usd_value: usd_value})
when is_nil(available_supply) or is_nil(usd_value) do
Decimal.new(0)
end
defp market_cap(:standard, %{available_supply: available_supply, usd_value: usd_value}) do
Decimal.mult(available_supply, usd_value)
end
defp market_cap(:standard, exchange_rate) do defp market_cap(:standard, exchange_rate) do
exchange_rate.market_cap_usd exchange_rate.market_cap_usd
end end

@ -135,12 +135,12 @@ defmodule BlockScoutWeb.TransactionView do
def confirmations(%Transaction{block: block}, named_arguments) when is_list(named_arguments) do def confirmations(%Transaction{block: block}, named_arguments) when is_list(named_arguments) do
case block do case block do
nil ->
0
%Block{consensus: true} -> %Block{consensus: true} ->
{:ok, confirmations} = Chain.confirmations(block, named_arguments) {:ok, confirmations} = Chain.confirmations(block, named_arguments)
BlockScoutWeb.Cldr.Number.to_string!(confirmations, format: "#,###") BlockScoutWeb.Cldr.Number.to_string!(confirmations, format: "#,###")
_ ->
0
end end
end end

@ -35,7 +35,7 @@ defmodule BlockScoutWeb.WeiHelpers do
"10,000 Gwei" "10,000 Gwei"
iex> format_wei_value(%Wei{value: Decimal.new(1, 10, 21)}, :ether) iex> format_wei_value(%Wei{value: Decimal.new(1, 10, 21)}, :ether)
"10,000 POA" "10,000 Ether"
# With formatting options # With formatting options
@ -43,7 +43,7 @@ defmodule BlockScoutWeb.WeiHelpers do
...> %Wei{value: Decimal.new(1000500000000000000)}, ...> %Wei{value: Decimal.new(1000500000000000000)},
...> :ether ...> :ether
...> ) ...> )
"1.0005 POA" "1.0005 Ether"
iex> format_wei_value( iex> format_wei_value(
...> %Wei{value: Decimal.new(10)}, ...> %Wei{value: Decimal.new(10)},

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -3,8 +3,8 @@ defmodule BlockScoutWeb.BlockControllerTest do
alias Explorer.Chain.Block alias Explorer.Chain.Block
setup do setup do
Supervisor.terminate_child(Explorer.Supervisor, {ConCache, :blocks}) Supervisor.terminate_child(Explorer.Supervisor, Explorer.Chain.Cache.Blocks.child_id())
Supervisor.restart_child(Explorer.Supervisor, {ConCache, :blocks}) Supervisor.restart_child(Explorer.Supervisor, Explorer.Chain.Cache.Blocks.child_id())
:ok :ok
end end

@ -9,8 +9,8 @@ defmodule BlockScoutWeb.ChainControllerTest do
alias Explorer.Counters.AddressesWithBalanceCounter alias Explorer.Counters.AddressesWithBalanceCounter
setup do setup do
Supervisor.terminate_child(Explorer.Supervisor, {ConCache, :blocks}) Supervisor.terminate_child(Explorer.Supervisor, Explorer.Chain.Cache.Blocks.child_id())
Supervisor.restart_child(Explorer.Supervisor, {ConCache, :blocks}) Supervisor.restart_child(Explorer.Supervisor, Explorer.Chain.Cache.Blocks.child_id())
start_supervised!(AddressesWithBalanceCounter) start_supervised!(AddressesWithBalanceCounter)
AddressesWithBalanceCounter.consolidate() AddressesWithBalanceCounter.consolidate()

@ -73,7 +73,7 @@ defmodule BlockScoutWeb.ViewingAddressesTest do
session session
|> AddressPage.visit_page(address) |> AddressPage.visit_page(address)
|> assert_text(AddressPage.balance(), "0.0000000000000005 POA") |> assert_text(AddressPage.balance(), "0.0000000000000005 Ether")
end end
describe "viewing contract creator" do describe "viewing contract creator" do

@ -10,8 +10,8 @@ defmodule BlockScoutWeb.ViewingChainTest do
alias Explorer.Counters.AddressesWithBalanceCounter alias Explorer.Counters.AddressesWithBalanceCounter
setup do setup do
Supervisor.terminate_child(Explorer.Supervisor, {ConCache, :blocks}) Supervisor.terminate_child(Explorer.Supervisor, Explorer.Chain.Cache.Blocks.child_id())
Supervisor.restart_child(Explorer.Supervisor, {ConCache, :blocks}) Supervisor.restart_child(Explorer.Supervisor, Explorer.Chain.Cache.Blocks.child_id())
Enum.map(401..404, &insert(:block, number: &1)) Enum.map(401..404, &insert(:block, number: &1))

@ -8,13 +8,13 @@ defmodule BlockScoutWeb.AddressCoinBalanceViewTest do
test "format the wei value in ether" do test "format the wei value in ether" do
wei = Wei.from(Decimal.new(1_340_000_000), :gwei) wei = Wei.from(Decimal.new(1_340_000_000), :gwei)
assert AddressCoinBalanceView.format(wei) == "1.34 POA" assert AddressCoinBalanceView.format(wei) == "1.34 Ether"
end end
test "format negative values" do test "format negative values" do
wei = Wei.from(Decimal.new(-1_340_000_000), :gwei) wei = Wei.from(Decimal.new(-1_340_000_000), :gwei)
assert AddressCoinBalanceView.format(wei) == "-1.34 POA" assert AddressCoinBalanceView.format(wei) == "-1.34 Ether"
end end
end end
@ -50,13 +50,13 @@ defmodule BlockScoutWeb.AddressCoinBalanceViewTest do
test "format positive values" do test "format positive values" do
value = Decimal.new(1_340_000_000_000_000_000) value = Decimal.new(1_340_000_000_000_000_000)
assert AddressCoinBalanceView.format_delta(value) == "1.34 POA" assert AddressCoinBalanceView.format_delta(value) == "1.34 Ether"
end end
test "format negative values" do test "format negative values" do
value = Decimal.new(-1_340_000_000_000_000_000) value = Decimal.new(-1_340_000_000_000_000_000)
assert AddressCoinBalanceView.format_delta(value) == "1.34 POA" assert AddressCoinBalanceView.format_delta(value) == "1.34 Ether"
end end
end end
end end

@ -3,32 +3,6 @@ defmodule BlockScoutWeb.ApiDocsViewTest do
alias BlockScoutWeb.APIDocsView alias BlockScoutWeb.APIDocsView
describe "blockscout_url/0" do
setup do
original = Application.get_env(:block_scout_web, BlockScoutWeb.Endpoint)
on_exit(fn -> Application.put_env(:block_scout_web, BlockScoutWeb.Endpoint, original) end)
:ok
end
test "returns url with scheme and host without port" do
Application.put_env(:block_scout_web, BlockScoutWeb.Endpoint,
url: [scheme: "https", host: "blockscout.com", port: 9999, path: "/"]
)
assert APIDocsView.blockscout_url() == "https://blockscout.com/"
end
test "returns url with scheme and host with path" do
Application.put_env(:block_scout_web, BlockScoutWeb.Endpoint,
url: [scheme: "https", host: "blockscout.com", port: 9999, path: "/chain/dog"]
)
assert APIDocsView.blockscout_url() == "https://blockscout.com/chain/dog"
end
end
describe "api_url/1" do describe "api_url/1" do
setup do setup do
original = Application.get_env(:block_scout_web, BlockScoutWeb.Endpoint) original = Application.get_env(:block_scout_web, BlockScoutWeb.Endpoint)
@ -53,6 +27,15 @@ defmodule BlockScoutWeb.ApiDocsViewTest do
assert APIDocsView.api_url() == "https://blockscout.com/api" assert APIDocsView.api_url() == "https://blockscout.com/api"
end end
test "localhost return with port" do
Application.put_env(:block_scout_web, BlockScoutWeb.Endpoint,
url: [scheme: "http", host: "localhost"],
http: [port: 9999]
)
assert APIDocsView.api_url() == "http://localhost:9999/api"
end
end end
describe "eth_rpc_api_url/1" do describe "eth_rpc_api_url/1" do
@ -79,5 +62,14 @@ defmodule BlockScoutWeb.ApiDocsViewTest do
assert APIDocsView.eth_rpc_api_url() == "https://blockscout.com/api/eth_rpc" assert APIDocsView.eth_rpc_api_url() == "https://blockscout.com/api/eth_rpc"
end end
test "localhost return with port" do
Application.put_env(:block_scout_web, BlockScoutWeb.Endpoint,
url: [scheme: "http", host: "localhost"],
http: [port: 9999]
)
assert APIDocsView.eth_rpc_api_url() == "http://localhost:9999/api/eth_rpc"
end
end end
end end

@ -91,7 +91,7 @@ defmodule BlockScoutWeb.BlockViewTest do
block = Repo.preload(block, :rewards) block = Repo.preload(block, :rewards)
assert BlockView.combined_rewards_value(block) == "3.000042 POA" assert BlockView.combined_rewards_value(block) == "3.000042 Ether"
end end
end end
end end

@ -136,7 +136,7 @@ defmodule BlockScoutWeb.TransactionViewTest do
gas_used: nil gas_used: nil
) )
expected_value = "Max of 0.009 POA" expected_value = "Max of 0.009 Ether"
assert expected_value == TransactionView.formatted_fee(transaction, denomination: :ether) assert expected_value == TransactionView.formatted_fee(transaction, denomination: :ether)
end end
@ -144,12 +144,9 @@ defmodule BlockScoutWeb.TransactionViewTest do
{:ok, gas_price} = Wei.cast(3_000_000_000) {:ok, gas_price} = Wei.cast(3_000_000_000)
transaction = build(:transaction, gas_price: gas_price, gas_used: Decimal.from_float(1_034_234.0)) transaction = build(:transaction, gas_price: gas_price, gas_used: Decimal.from_float(1_034_234.0))
expected_value = "0.003102702 POA" expected_value = "0.003102702 Ether"
assert expected_value == TransactionView.formatted_fee(transaction, denomination: :ether) assert expected_value == TransactionView.formatted_fee(transaction, denomination: :ether)
end end
test "with fee but no available exchange_rate" do
end
end end
describe "formatted_status/1" do describe "formatted_status/1" do

@ -31,8 +31,7 @@ config :explorer, Explorer.ChainSpec.GenesisData, enabled: false, chain_spec_pat
config :explorer, Explorer.Chain.Cache.BlockNumber, enabled: true config :explorer, Explorer.Chain.Cache.BlockNumber, enabled: true
config :explorer, Explorer.ExchangeRates.Source.CoinMarketCap, config :explorer, Explorer.ExchangeRates.Source.CoinGecko, coin_id: System.get_env("COIN_GECKO_ID", "poa-network")
pages: String.to_integer(System.get_env("COINMARKETCAP_PAGES") || "10")
balances_update_interval = balances_update_interval =
if System.get_env("ADDRESS_WITH_BALANCES_UPDATE_INTERVAL") do if System.get_env("ADDRESS_WITH_BALANCES_UPDATE_INTERVAL") do

@ -42,10 +42,11 @@ defmodule Explorer.Application do
Explorer.SmartContract.SolcDownloader, Explorer.SmartContract.SolcDownloader,
{Registry, keys: :duplicate, name: Registry.ChainEvents, id: Registry.ChainEvents}, {Registry, keys: :duplicate, name: Registry.ChainEvents, id: Registry.ChainEvents},
{Admin.Recovery, [[], [name: Admin.Recovery]]}, {Admin.Recovery, [[], [name: Admin.Recovery]]},
{TransactionCount, [[], []]}, TransactionCount,
{BlockCount, []}, BlockCount,
Blocks, Blocks,
con_cache_child_spec(NetVersion.cache_name()), NetVersion,
BlockNumber,
con_cache_child_spec(MarketHistoryCache.cache_name()), con_cache_child_spec(MarketHistoryCache.cache_name()),
con_cache_child_spec(RSK.cache_name(), ttl_check_interval: :timer.minutes(1), global_ttl: :timer.minutes(30)), con_cache_child_spec(RSK.cache_name(), ttl_check_interval: :timer.minutes(1), global_ttl: :timer.minutes(30)),
Transactions Transactions
@ -57,8 +58,6 @@ defmodule Explorer.Application do
res = Supervisor.start_link(children, opts) res = Supervisor.start_link(children, opts)
BlockNumber.setup()
res res
end end

@ -267,7 +267,7 @@ defmodule Explorer.Chain do
def address_to_logs(address_hash, options \\ []) when is_list(options) do def address_to_logs(address_hash, options \\ []) when is_list(options) do
paging_options = Keyword.get(options, :paging_options) || %PagingOptions{page_size: 50} paging_options = Keyword.get(options, :paging_options) || %PagingOptions{page_size: 50}
{block_number, transaction_index, log_index} = paging_options.key || {BlockNumber.max_number(), 0, 0} {block_number, transaction_index, log_index} = paging_options.key || {BlockNumber.get_max(), 0, 0}
base_query = base_query =
from(log in Log, from(log in Log,
@ -1176,7 +1176,7 @@ defmodule Explorer.Chain do
""" """
@spec indexed_ratio() :: Decimal.t() @spec indexed_ratio() :: Decimal.t()
def indexed_ratio do def indexed_ratio do
{min, max} = BlockNumber.min_and_max_numbers() %{min: min, max: max} = BlockNumber.get_all()
case {min, max} do case {min, max} do
{0, 0} -> {0, 0} ->
@ -1189,20 +1189,30 @@ defmodule Explorer.Chain do
end end
end end
@spec fetch_min_and_max_block_numbers() :: {non_neg_integer, non_neg_integer} @spec fetch_min_block_number() :: non_neg_integer
def fetch_min_and_max_block_numbers do def fetch_min_block_number do
query = query =
from(block in Block, from(block in Block,
select: {min(block.number), max(block.number)}, select: block.number,
where: block.consensus == true where: block.consensus == true,
order_by: [asc: block.number],
limit: 1
) )
result = Repo.one!(query) Repo.one(query) || 0
case result do
{nil, nil} -> {0, 0}
_ -> result
end end
@spec fetch_max_block_number() :: non_neg_integer
def fetch_max_block_number do
query =
from(block in Block,
select: block.number,
where: block.consensus == true,
order_by: [desc: block.number],
limit: 1
)
Repo.one(query) || 0
end end
@spec fetch_count_consensus_block() :: non_neg_integer @spec fetch_count_consensus_block() :: non_neg_integer
@ -2195,7 +2205,7 @@ defmodule Explorer.Chain do
""" """
@spec transaction_estimated_count() :: non_neg_integer() @spec transaction_estimated_count() :: non_neg_integer()
def transaction_estimated_count do def transaction_estimated_count do
cached_value = TransactionCount.value() cached_value = TransactionCount.get_count()
if is_nil(cached_value) do if is_nil(cached_value) do
%Postgrex.Result{rows: [[rows]]} = %Postgrex.Result{rows: [[rows]]} =
@ -2214,7 +2224,7 @@ defmodule Explorer.Chain do
""" """
@spec block_estimated_count() :: non_neg_integer() @spec block_estimated_count() :: non_neg_integer()
def block_estimated_count do def block_estimated_count do
cached_value = BlockCount.count() cached_value = BlockCount.get_count()
if is_nil(cached_value) do if is_nil(cached_value) do
%Postgrex.Result{rows: [[count]]} = Repo.query!("SELECT reltuples FROM pg_class WHERE relname = 'blocks';") %Postgrex.Result{rows: [[count]]} = Repo.query!("SELECT reltuples FROM pg_class WHERE relname = 'blocks';")
@ -2717,7 +2727,7 @@ defmodule Explorer.Chain do
end end
defp supply_module do defp supply_module do
Application.get_env(:explorer, :supply, Explorer.Chain.Supply.CoinMarketCap) Application.get_env(:explorer, :supply, Explorer.Chain.Supply.ExchangeRate)
end end
@doc """ @doc """

@ -170,7 +170,7 @@ defmodule Explorer.Chain.Address do
end end
def rsk_checksum(hash) do def rsk_checksum(hash) do
chain_id = NetVersion.version() chain_id = NetVersion.get_version()
string_hash = string_hash =
hash hash

@ -5,143 +5,61 @@ defmodule Explorer.Chain.Cache.BlockCount do
require Logger require Logger
use GenServer @default_cache_period :timer.minutes(10)
alias Explorer.Chain use Explorer.Chain.MapCache,
name: :block_count,
# 10 minutes key: :count,
@cache_period 1_000 * 60 * 10 key: :async_task,
@key "count" global_ttl: cache_period(),
@default_value nil ttl_check_interval: :timer.minutes(1),
@name __MODULE__ callback: &async_task_on_deletion(&1)
def start_link(params) do
name = params[:name] || @name
params_with_name = Keyword.put(params, :name, name)
GenServer.start_link(__MODULE__, params_with_name, name: name)
end
def init(params) do
cache_period = period_from_env_var() || params[:cache_period] || @cache_period
current_value = params[:default_value] || @default_value
name = params[:name]
init_ets_table(name) alias Explorer.Chain
{:ok, {{cache_period, current_value, name}, nil}} defp handle_fallback(:count) do
end # This will get the task PID if one exists and launch a new task if not
# See next `handle_fallback` definition
get_async_task()
def count(process_name \\ __MODULE__) do {:return, nil}
GenServer.call(process_name, :count)
end end
def handle_call(:count, _, {{cache_period, default_value, name}, task}) do defp handle_fallback(:async_task) do
{count, task} = # If this gets called it means an async task was requested, but none exists
case cached_values(name) do # so a new one needs to be launched
nil ->
{default_value, update_cache(task, name)}
{cached_value, timestamp} ->
task = task =
if current_time() - timestamp > cache_period do
update_cache(task, name)
end
{cached_value, task}
end
{:reply, count, {{cache_period, default_value, name}, task}}
end
def update_cache(nil, name) do
async_update_cache(name)
end
def update_cache(task, _) do
task
end
def handle_cast({:update_cache, value}, {{cache_period, default_value, name}, _}) do
current_time = current_time()
tuple = {value, current_time}
table_name = table_name(name)
:ets.insert(table_name, {@key, tuple})
{:noreply, {{cache_period, default_value, name}, nil}}
end
def handle_info({:DOWN, _, _, _, _}, {{cache_period, default_value, name}, _}) do
{:noreply, {{cache_period, default_value, name}, nil}}
end
def handle_info(_, {{cache_period, default_value, name}, _}) do
{:noreply, {{cache_period, default_value, name}, nil}}
end
# sobelow_skip ["DOS"]
defp table_name(name) do
name
|> Atom.to_string()
|> Macro.underscore()
|> String.to_atom()
end
def async_update_cache(name) do
Task.async(fn -> Task.async(fn ->
try do try do
result = Chain.fetch_count_consensus_block() result = Chain.fetch_count_consensus_block()
GenServer.cast(name, {:update_cache, result}) set_count(result)
rescue rescue
e -> e ->
Logger.debug([ Logger.debug([
"Coudn't update block count test #{inspect(e)}" "Coudn't update block count test #{inspect(e)}"
]) ])
end end
end)
end
defp init_ets_table(name) do set_async_task(nil)
table_name = table_name(name) end)
if :ets.whereis(table_name) == :undefined do
:ets.new(table_name, [
:set,
:named_table,
:public,
write_concurrency: true
])
end
end
defp cached_values(name) do
table_name = table_name(name)
case :ets.lookup(table_name, @key) do {:update, task}
[{_, cached_values}] -> cached_values
_ -> nil
end
end end
defp current_time do # By setting this as a `callback` an async task will be started each time the
utc_now = DateTime.utc_now() # `count` expires (unless there is one already running)
defp async_task_on_deletion({:delete, _, :count}), do: get_async_task()
DateTime.to_unix(utc_now, :millisecond) defp async_task_on_deletion(_data), do: nil
end
defp period_from_env_var do
case System.get_env("BLOCK_COUNT_CACHE_PERIOD") do
value when is_binary(value) ->
case Integer.parse(value) do
{integer, ""} -> integer * 1_000
_ -> nil
end
_ -> defp cache_period do
nil "BLOCK_COUNT_CACHE_PERIOD"
|> System.get_env("")
|> Integer.parse()
|> case do
{integer, ""} -> :timer.seconds(integer)
_ -> @default_cache_period
end end
end end
end end

@ -3,89 +3,36 @@ defmodule Explorer.Chain.Cache.BlockNumber do
Cache for max and min block numbers. Cache for max and min block numbers.
""" """
alias Explorer.Chain @type value :: non_neg_integer()
@tab :block_number_cache use Explorer.Chain.MapCache,
@key "min_max" name: :block_number,
keys: [:min, :max]
@spec setup() :: :ok alias Explorer.Chain
def setup do
if :ets.whereis(@tab) == :undefined do
:ets.new(@tab, [
:set,
:named_table,
:public,
write_concurrency: true
])
end
update_cache() defp handle_update(_key, nil, value), do: {:ok, value}
:ok defp handle_update(:min, old_value, new_value), do: {:ok, min(new_value, old_value)}
end
def max_number do defp handle_update(:max, old_value, new_value), do: {:ok, max(new_value, old_value)}
value(:max)
end
def min_number do defp handle_fallback(key) do
value(:min) result = fetch_from_db(key)
end
def min_and_max_numbers do
value(:all)
end
defp value(type) do
{min, max} =
if Application.get_env(:explorer, __MODULE__)[:enabled] do if Application.get_env(:explorer, __MODULE__)[:enabled] do
cached_values() {:update, result}
else else
min_and_max_from_db() {:return, result}
end end
case type do
:max -> max
:min -> min
:all -> {min, max}
end end
end
@spec update(non_neg_integer()) :: boolean()
def update(number) do
{old_min, old_max} = cached_values()
cond do defp fetch_from_db(key) do
number > old_max -> case key do
tuple = {old_min, number} :min -> Chain.fetch_min_block_number()
:ets.insert(@tab, {@key, tuple}) :max -> Chain.fetch_max_block_number()
number < old_min ->
tuple = {number, old_max}
:ets.insert(@tab, {@key, tuple})
true ->
false
end
end
defp update_cache do
{min, max} = min_and_max_from_db()
tuple = {min, max}
:ets.insert(@tab, {@key, tuple})
end end
defp cached_values do
[{_, cached_values}] = :ets.lookup(@tab, @key)
cached_values
end
defp min_and_max_from_db do
Chain.fetch_min_and_max_block_numbers()
rescue rescue
_e -> _e -> 0
{0, 0}
end end
end end

@ -3,42 +3,27 @@ defmodule Explorer.Chain.Cache.NetVersion do
Caches chain version. Caches chain version.
""" """
@cache_name :net_version @json_rpc_named_arguments Application.get_env(:explorer, :json_rpc_named_arguments)
@key :version
@spec version() :: non_neg_integer() | {:error, any()} require Logger
def version do
cached_value = fetch_from_cache()
if is_nil(cached_value) do use Explorer.Chain.MapCache,
fetch_from_node() name: :net_version,
else key: :version
cached_value
end
end
def cache_name do
@cache_name
end
defp fetch_from_cache do
ConCache.get(@cache_name, @key)
end
defp cache_value(value) do defp handle_fallback(:version) do
ConCache.put(@cache_name, @key, value) case EthereumJSONRPC.fetch_net_version(@json_rpc_named_arguments) do
end
defp fetch_from_node do
json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments)
case EthereumJSONRPC.fetch_net_version(json_rpc_named_arguments) do
{:ok, value} -> {:ok, value} ->
cache_value(value) {:update, value}
value
other -> {:error, reason} ->
other Logger.debug([
"Coudn't fetch net_version, reason: #{inspect(reason)}"
])
{:return, nil}
end end
end end
defp handle_fallback(_key), do: {:return, nil}
end end

@ -3,152 +3,64 @@ defmodule Explorer.Chain.Cache.TransactionCount do
Cache for estimated transaction count. Cache for estimated transaction count.
""" """
require Logger @default_cache_period :timer.hours(2)
use Explorer.Chain.MapCache,
name: :transaction_count,
key: :count,
key: :async_task,
global_ttl: cache_period(),
ttl_check_interval: :timer.minutes(10),
callback: &async_task_on_deletion(&1)
use GenServer require Logger
alias Explorer.Chain.Transaction alias Explorer.Chain.Transaction
alias Explorer.Repo alias Explorer.Repo
# 2 hours defp handle_fallback(:count) do
@cache_period 1_000 * 60 * 60 * 2 # This will get the task PID if one exists and launch a new task if not
@default_value nil # See next `handle_fallback` definition
@key "count" get_async_task()
@name __MODULE__
def start_link([params, gen_server_options]) do
name = gen_server_options[:name] || @name
params_with_name = Keyword.put(params, :name, name)
GenServer.start_link(__MODULE__, params_with_name, name: name)
end
def init(params) do
cache_period = period_from_env_var() || params[:cache_period] || @cache_period
current_value = params[:default_value] || @default_value
name = params[:name]
init_ets_table(name)
schedule_cache_update()
{:ok, {{cache_period, current_value, name}, nil}} {:return, nil}
end end
def value(process_name \\ __MODULE__) do defp handle_fallback(:async_task) do
GenServer.call(process_name, :value) # If this gets called it means an async task was requested, but none exists
end # so a new one needs to be launched
def handle_call(:value, _, {{cache_period, default_value, name}, task}) do
{value, task} =
case cached_values(name) do
nil ->
{default_value, update_cache(task, name)}
{cached_value, timestamp} ->
task = task =
if current_time() - timestamp > cache_period do
update_cache(task, name)
end
{cached_value, task}
end
{:reply, value, {{cache_period, default_value, name}, task}}
end
def update_cache(nil, name) do
async_update_cache(name)
end
def update_cache(task, _) do
task
end
def handle_cast({:update_cache, value}, {{cache_period, default_value, name}, _}) do
current_time = current_time()
tuple = {value, current_time}
table_name = table_name(name)
:ets.insert(table_name, {@key, tuple})
{:noreply, {{cache_period, default_value, name}, nil}}
end
def handle_info({:DOWN, _, _, _, _}, {{cache_period, default_value, name}, _}) do
{:noreply, {{cache_period, default_value, name}, nil}}
end
def handle_info(_, {{cache_period, default_value, name}, _}) do
{:noreply, {{cache_period, default_value, name}, nil}}
end
# sobelow_skip ["DOS"]
defp table_name(name) do
name
|> Atom.to_string()
|> Macro.underscore()
|> String.to_atom()
end
def async_update_cache(name) do
Task.async(fn -> Task.async(fn ->
try do try do
result = Repo.aggregate(Transaction, :count, :hash, timeout: :infinity) result = Repo.aggregate(Transaction, :count, :hash, timeout: :infinity)
GenServer.cast(name, {:update_cache, result}) set_count(result)
rescue rescue
e -> e ->
Logger.debug([ Logger.debug([
"Coudn't update transaction count test #{inspect(e)}" "Coudn't update transaction count test #{inspect(e)}"
]) ])
end end
end)
end
defp init_ets_table(name) do
table_name = table_name(name)
if :ets.whereis(table_name) == :undefined do set_async_task(nil)
:ets.new(table_name, [ end)
:set,
:named_table,
:public,
write_concurrency: true
])
end
end
defp cached_values(name) do
table_name = table_name(name)
case :ets.lookup(table_name, @key) do
[{_, cached_values}] -> cached_values
_ -> nil
end
end
defp schedule_cache_update do {:update, task}
Process.send_after(self(), :update_cache, 2_000)
end end
defp current_time do # By setting this as a `callback` an async task will be started each time the
utc_now = DateTime.utc_now() # `count` expires (unless there is one already running)
defp async_task_on_deletion({:delete, _, :count}), do: get_async_task()
DateTime.to_unix(utc_now, :millisecond) defp async_task_on_deletion(_data), do: nil
end
defp period_from_env_var do
case System.get_env("TXS_COUNT_CACHE_PERIOD") do
value when is_binary(value) ->
case Integer.parse(value) do
{integer, ""} -> integer * 1_000
_ -> nil
end
_ -> defp cache_period do
nil "TXS_COUNT_CACHE_PERIOD"
|> System.get_env("")
|> Integer.parse()
|> case do
{integer, ""} -> :timer.seconds(integer)
_ -> @default_cache_period
end end
end end
end end

@ -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 """ @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 use Explorer.Chain.Supply

@ -102,7 +102,7 @@ defmodule Explorer.Chain.Supply.RSK do
def cache_name, do: @cache_name def cache_name, do: @cache_name
defp fetch_circulating_value do defp fetch_circulating_value do
max_number = BlockNumber.max_number() max_number = BlockNumber.get_max()
params = [ params = [
%{block_quantity: integer_to_quantity(max_number), hash_data: "0x0000000000000000000000000000000001000006"} %{block_quantity: integer_to_quantity(max_number), hash_data: "0x0000000000000000000000000000000001000006"}

@ -2,8 +2,6 @@ defmodule Explorer.ExchangeRates.Source do
@moduledoc """ @moduledoc """
Behaviour for fetching exchange rates from external sources. Behaviour for fetching exchange rates from external sources.
""" """
alias Explorer.ExchangeRates.Source.CoinMarketCap
alias Explorer.ExchangeRates.Token alias Explorer.ExchangeRates.Token
alias HTTPoison.{Error, Response} alias HTTPoison.{Error, Response}
@ -12,34 +10,18 @@ defmodule Explorer.ExchangeRates.Source do
""" """
@spec fetch_exchange_rates(module) :: {:ok, [Token.t()]} | {:error, any} @spec fetch_exchange_rates(module) :: {:ok, [Token.t()]} | {:error, any}
def fetch_exchange_rates(source \\ exchange_rates_source()) do def fetch_exchange_rates(source \\ exchange_rates_source()) do
if(source == CoinMarketCap) do
fetch_exchange_rates_from_paginable_source(source)
else
fetch_exchange_rates_request(source) fetch_exchange_rates_request(source)
end end
end
defp fetch_exchange_rates_from_paginable_source(source, page \\ 1) do
case HTTPoison.get(source.source_url(page), headers()) do
{:ok, %Response{body: body, status_code: 200}} ->
cond do
body =~ Explorer.coin() -> {:ok, source.format_data(body)}
page == source.max_page_number -> {:error, "exchange rates not found for this network"}
true -> fetch_exchange_rates_from_paginable_source(source, page + 1)
end
{:ok, %Response{body: body, status_code: status_code}} when status_code in 400..502 ->
{:error, decode_json(body)["error"]}
{:error, %Error{reason: reason}} ->
{:error, reason}
end
end
defp fetch_exchange_rates_request(source) do defp fetch_exchange_rates_request(source) do
case HTTPoison.get(source.source_url(), headers()) do case HTTPoison.get(source.source_url(), headers()) do
{:ok, %Response{body: body, status_code: 200}} -> {:ok, %Response{body: body, status_code: 200}} ->
{:ok, source.format_data(body)} result =
body
|> decode_json()
|> source.format_data()
{:ok, result}
{:ok, %Response{body: body, status_code: status_code}} when status_code in 400..499 -> {:ok, %Response{body: body, status_code: status_code}} when status_code in 400..499 ->
{:error, decode_json(body)["error"]} {:error, decode_json(body)["error"]}
@ -83,7 +65,7 @@ defmodule Explorer.ExchangeRates.Source do
@spec exchange_rates_source() :: module() @spec exchange_rates_source() :: module()
defp exchange_rates_source do defp exchange_rates_source do
config(:source) || Explorer.ExchangeRates.Source.CoinMarketCap config(:source) || Explorer.ExchangeRates.Source.CoinGecko
end end
@spec config(atom()) :: term @spec config(atom()) :: term

@ -11,43 +11,50 @@ defmodule Explorer.ExchangeRates.Source.CoinGecko do
@behaviour Source @behaviour Source
@impl Source @impl Source
def format_data(data) do def format_data(%{"market_data" => _} = json_data) do
{:ok, price} = get_btc_price() {:ok, price} = get_btc_price()
btc_price = to_decimal(price) btc_price = to_decimal(price)
for item <- decode_json(data), market_data = json_data["market_data"]
not is_nil(item["total_supply"]) and not is_nil(item["current_price"]) do {:ok, last_updated, 0} = DateTime.from_iso8601(market_data["last_updated"])
{:ok, last_updated, 0} = DateTime.from_iso8601(item["last_updated"])
current_price = to_decimal(item["current_price"]) current_price = to_decimal(market_data["current_price"]["usd"])
id = item["id"] id = json_data["id"]
btc_value = if id != "btc", do: Decimal.div(current_price, btc_price), else: 1 btc_value = if id != "btc", do: Decimal.div(current_price, btc_price), else: 1
[
%Token{ %Token{
available_supply: to_decimal(item["total_supply"]), available_supply: to_decimal(market_data["circulating_supply"]),
total_supply: to_decimal(item["total_supply"]), total_supply: to_decimal(market_data["total_supply"]),
btc_value: btc_value, btc_value: btc_value,
id: id, id: json_data["id"],
last_updated: last_updated, last_updated: last_updated,
market_cap_usd: to_decimal(item["market_cap"]), market_cap_usd: to_decimal(market_data["market_cap"]["usd"]),
name: item["name"], name: json_data["name"],
symbol: item["symbol"], symbol: String.upcase(json_data["symbol"]),
usd_value: current_price, usd_value: current_price,
volume_24h_usd: to_decimal(item["total_volume"]) volume_24h_usd: to_decimal(market_data["total_volume"]["usd"])
} }
]
end end
end
@impl Source @impl Source
def source_url(currency \\ "usd") do def format_data(_), do: []
"#{base_url()}/coins/markets?vs_currency=#{currency}"
@impl Source
def source_url do
"#{base_url()}/coins/#{coin_id()}"
end end
defp base_url do defp base_url do
config(:base_url) || "https://api.coingecko.com/api/v3" config(:base_url) || "https://api.coingecko.com/api/v3"
end end
defp coin_id do
Application.get_env(:explorer, __MODULE__)[:coin_id]
end
defp get_btc_price(currency \\ "usd") do defp get_btc_price(currency \\ "usd") do
url = "#{base_url()}/exchange_rates" url = "#{base_url()}/exchange_rates"

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

@ -30,7 +30,7 @@ defmodule Explorer.ExchangeRates.Source.TokenBridge do
btc_value: original_token.btc_value, btc_value: original_token.btc_value,
id: original_token.id, id: original_token.id,
last_updated: original_token.last_updated, last_updated: original_token.last_updated,
market_cap_usd: Decimal.mult(to_decimal(Chain.circulating_supply()), original_token.usd_value), market_cap_usd: market_cap_usd(Chain.circulating_supply(), original_token),
name: original_token.name, name: original_token.name,
symbol: original_token.symbol, symbol: original_token.symbol,
usd_value: original_token.usd_value, usd_value: original_token.usd_value,
@ -38,6 +38,14 @@ defmodule Explorer.ExchangeRates.Source.TokenBridge do
} }
end end
defp market_cap_usd(nil, _original_token), do: Decimal.new(0)
defp market_cap_usd(supply, original_token) do
supply
|> to_decimal()
|> Decimal.mult(original_token.usd_value)
end
@impl Source @impl Source
def source_url do def source_url do
secondary_source().source_url() secondary_source().source_url()
@ -45,7 +53,7 @@ defmodule Explorer.ExchangeRates.Source.TokenBridge do
@spec secondary_source() :: module() @spec secondary_source() :: module()
defp secondary_source do defp secondary_source do
config(:secondary_source) || Explorer.ExchangeRates.Source.CoinMarketCap config(:secondary_source) || Explorer.ExchangeRates.Source.CoinGecko
end end
@spec config(atom()) :: term @spec config(atom()) :: term

@ -3,53 +3,53 @@ defmodule Explorer.Chain.Cache.BlockCountTest do
alias Explorer.Chain.Cache.BlockCount alias Explorer.Chain.Cache.BlockCount
test "returns default transaction count" do setup do
BlockCount.start_link(name: BlockTestCache) Supervisor.terminate_child(Explorer.Supervisor, BlockCount.child_id())
Supervisor.restart_child(Explorer.Supervisor, BlockCount.child_id())
:ok
end
result = BlockCount.count(BlockTestCache) test "returns default transaction count" do
result = BlockCount.get_count()
assert is_nil(result) assert is_nil(result)
end end
test "updates cache if initial value is zero" do test "updates cache if initial value is zero" do
BlockCount.start_link(name: BlockTestCache)
insert(:block, consensus: true) insert(:block, consensus: true)
insert(:block, consensus: true) insert(:block, consensus: true)
insert(:block, consensus: false) insert(:block, consensus: false)
_result = BlockCount.count(BlockTestCache) _result = BlockCount.get_count()
Process.sleep(1000) Process.sleep(1000)
updated_value = BlockCount.count(BlockTestCache) updated_value = BlockCount.get_count()
assert updated_value == 2 assert updated_value == 2
end end
test "does not update cache if cache period did not pass" do test "does not update cache if cache period did not pass" do
BlockCount.start_link(name: BlockTestCache)
insert(:block, consensus: true) insert(:block, consensus: true)
insert(:block, consensus: true) insert(:block, consensus: true)
insert(:block, consensus: false) insert(:block, consensus: false)
_result = BlockCount.count(BlockTestCache) _result = BlockCount.get_count()
Process.sleep(1000) Process.sleep(1000)
updated_value = BlockCount.count(BlockTestCache) updated_value = BlockCount.get_count()
assert updated_value == 2 assert updated_value == 2
insert(:block, consensus: true) insert(:block, consensus: true)
insert(:block, consensus: true) insert(:block, consensus: true)
_updated_value = BlockCount.count(BlockTestCache) _updated_value = BlockCount.get_count()
Process.sleep(1000) Process.sleep(1000)
updated_value = BlockCount.count(BlockTestCache) updated_value = BlockCount.get_count()
assert updated_value == 2 assert updated_value == 2
end end

@ -11,49 +11,63 @@ defmodule Explorer.Chain.Cache.BlockNumberTest do
end) end)
end end
describe "max_number/1" do describe "get_max/1" do
test "returns max number" do test "returns max number" do
insert(:block, number: 5) insert(:block, number: 5)
BlockNumber.setup() assert BlockNumber.get_max() == 5
assert BlockNumber.max_number() == 5
end end
end end
describe "min_number/1" do describe "get_min/1" do
test "returns max number" do test "returns min number" do
insert(:block, number: 2) insert(:block, number: 2)
BlockNumber.setup() assert BlockNumber.get_min() == 2
end
end
assert BlockNumber.max_number() == 2 describe "get_all/1" do
test "returns min and max number" do
insert(:block, number: 6)
assert BlockNumber.get_all() == %{min: 6, max: 6}
end end
end end
describe "update/1" do describe "update_all/1" do
test "updates max number" do test "updates max number" do
insert(:block, number: 2) insert(:block, number: 2)
BlockNumber.setup() assert BlockNumber.get_max() == 2
assert BlockNumber.max_number() == 2
assert BlockNumber.update(3) assert BlockNumber.update_all(3)
assert BlockNumber.max_number() == 3 assert BlockNumber.get_max() == 3
end end
test "updates min number" do test "updates min number" do
insert(:block, number: 2) insert(:block, number: 2)
BlockNumber.setup() assert BlockNumber.get_min() == 2
assert BlockNumber.update_all(1)
assert BlockNumber.get_min() == 1
end
test "updates min and number" do
insert(:block, number: 2)
assert BlockNumber.get_all() == %{min: 2, max: 2}
assert BlockNumber.update_all(1)
assert BlockNumber.min_number() == 2 assert BlockNumber.get_all() == %{min: 1, max: 2}
assert BlockNumber.update(1) assert BlockNumber.update_all(6)
assert BlockNumber.min_number() == 1 assert BlockNumber.get_all() == %{min: 1, max: 6}
end end
end end
end end

@ -5,8 +5,8 @@ defmodule Explorer.Chain.Cache.BlocksTest do
alias Explorer.Repo alias Explorer.Repo
setup do setup do
Supervisor.terminate_child(Explorer.Supervisor, {ConCache, :blocks}) Supervisor.terminate_child(Explorer.Supervisor, Blocks.child_id())
Supervisor.restart_child(Explorer.Supervisor, {ConCache, :blocks}) Supervisor.restart_child(Explorer.Supervisor, Blocks.child_id())
:ok :ok
end end

@ -3,51 +3,51 @@ defmodule Explorer.Chain.Cache.TransactionCountTest do
alias Explorer.Chain.Cache.TransactionCount alias Explorer.Chain.Cache.TransactionCount
test "returns default transaction count" do setup do
TransactionCount.start_link([[], [name: TestCache]]) Supervisor.terminate_child(Explorer.Supervisor, TransactionCount.child_id())
Supervisor.restart_child(Explorer.Supervisor, TransactionCount.child_id())
:ok
end
result = TransactionCount.value(TestCache) test "returns default transaction count" do
result = TransactionCount.get_count()
assert is_nil(result) assert is_nil(result)
end end
test "updates cache if initial value is zero" do test "updates cache if initial value is zero" do
TransactionCount.start_link([[], [name: TestCache]])
insert(:transaction) insert(:transaction)
insert(:transaction) insert(:transaction)
_result = TransactionCount.value(TestCache) _result = TransactionCount.get_count()
Process.sleep(1000) Process.sleep(1000)
updated_value = TransactionCount.value(TestCache) updated_value = TransactionCount.get_count()
assert updated_value == 2 assert updated_value == 2
end end
test "does not update cache if cache period did not pass" do test "does not update cache if cache period did not pass" do
TransactionCount.start_link([[], [name: TestCache]])
insert(:transaction) insert(:transaction)
insert(:transaction) insert(:transaction)
_result = TransactionCount.value(TestCache) _result = TransactionCount.get_count()
Process.sleep(1000) Process.sleep(1000)
updated_value = TransactionCount.value(TestCache) updated_value = TransactionCount.get_count()
assert updated_value == 2 assert updated_value == 2
insert(:transaction) insert(:transaction)
insert(:transaction) insert(:transaction)
_updated_value = TransactionCount.value(TestCache) _updated_value = TransactionCount.get_count()
Process.sleep(1000) Process.sleep(1000)
updated_value = TransactionCount.value(TestCache) updated_value = TransactionCount.get_count()
assert updated_value == 2 assert updated_value == 2
end end

@ -1121,23 +1121,31 @@ defmodule Explorer.ChainTest do
end end
end end
describe "fetch_min_and_max_block_numbers/0" do describe "fetch_min_block_number/0" do
test "fetches min and max block numbers" do test "fetches min block numbers" do
for index <- 5..9 do for index <- 5..9 do
insert(:block, number: index) insert(:block, number: index)
end end
assert {5, 9} = Chain.fetch_min_and_max_block_numbers() assert 5 = Chain.fetch_min_block_number()
end end
test "fetches min and max when there are no blocks" do test "fetches min when there are no blocks" do
assert {0, 0} = Chain.fetch_min_and_max_block_numbers() assert 0 = Chain.fetch_min_block_number()
end
end end
test "fetches min and max where there is only one block" do describe "fetch_max_block_number/0" do
insert(:block, number: 1) test "fetches max block numbers" do
for index <- 5..9 do
insert(:block, number: index)
end
assert 9 = Chain.fetch_max_block_number()
end
assert {1, 1} = Chain.fetch_min_and_max_block_numbers() test "fetches max when there are no blocks" do
assert 0 = Chain.fetch_max_block_number()
end end
end end

@ -18,34 +18,6 @@ defmodule Explorer.ExchangeRates.Source.CoinGeckoTest do
} }
""" """
@json_mkt_data """
[
{
"id": "poa-network",
"symbol": "poa",
"name": "POA Network",
"image": "https://assets.coingecko.com/coins/images/3157/large/poa.jpg?1520829019",
"current_price": 0.114782883773693,
"market_cap": 25248999.6735956,
"market_cap_rank": 185,
"total_volume": 2344442.13578437,
"high_24h": 0.115215129840519,
"low_24h": 0.101039753612939,
"price_change_24h": 0.0135970966607094,
"price_change_percentage_24h": 13.437753511298,
"market_cap_change_24h": 3058195.58191147,
"market_cap_change_percentage_24h": 13.7813644304017,
"circulating_supply": "219935174.0",
"total_supply": 252193195,
"ath": 0.935923393359191,
"ath_change_percentage": -87.731057963078,
"ath_date": "2018-05-10T09:45:31.809Z",
"roi": null,
"last_updated": "2018-10-23T01:25:31.764Z"
}
]
"""
describe "format_data/1" do describe "format_data/1" do
setup do setup do
bypass = Bypass.open() bypass = Bypass.open()
@ -59,31 +31,30 @@ defmodule Explorer.ExchangeRates.Source.CoinGeckoTest do
Conn.resp(conn, 200, @json_btc_price) Conn.resp(conn, 200, @json_btc_price)
end) end)
{:ok, expected_date, 0} = "2018-10-23T01:25:31.764Z" |> DateTime.from_iso8601() json_data =
"#{File.cwd!()}/test/support/fixture/exchange_rates/coin_gecko.json"
|> File.read!()
|> Jason.decode!()
expected = [ expected = [
%Token{ %Token{
available_supply: Decimal.new("252193195"), available_supply: Decimal.new("220167621.0"),
total_supply: Decimal.new("252193195"), total_supply: Decimal.new("252193195.0"),
btc_value: Decimal.new("0.00001753101509231471092879666458"), btc_value: Decimal.new("0.000002055310963802830367634997491"),
id: "poa-network", id: "poa-network",
last_updated: expected_date, last_updated: ~U[2019-08-21 08:36:49.371Z],
market_cap_usd: Decimal.new("25248999.6735956"), market_cap_usd: Decimal.new("2962791"),
name: "POA Network", name: "POA Network",
symbol: "poa", symbol: "POA",
usd_value: Decimal.new("0.114782883773693"), usd_value: Decimal.new("0.01345698"),
volume_24h_usd: Decimal.new("2344442.13578437") volume_24h_usd: Decimal.new("119946")
} }
] ]
assert expected == CoinGecko.format_data(@json_mkt_data) assert expected == CoinGecko.format_data(json_data)
end end
test "returns nothing when given bad data", %{bypass: bypass} do test "returns nothing when given bad data" do
Bypass.expect(bypass, "GET", "/exchange_rates", fn conn ->
Conn.resp(conn, 200, @json_btc_price)
end)
bad_data = """ bad_data = """
[{"id": "poa-network"}] [{"id": "poa-network"}]
""" """

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

@ -1,32 +1,41 @@
defmodule Explorer.ExchangeRates.Source.TokenBridgeTest do defmodule Explorer.ExchangeRates.Source.TokenBridgeTest do
use Explorer.DataCase use Explorer.DataCase
alias Explorer.ExchangeRates.Source.CoinGecko
alias Explorer.ExchangeRates.Source.TokenBridge alias Explorer.ExchangeRates.Source.TokenBridge
alias Explorer.ExchangeRates.Token alias Explorer.ExchangeRates.Token
alias Plug.Conn
@json "#{File.cwd!()}/test/support/fixture/exchange_rates/coin_gecko.json"
|> File.read!()
|> Jason.decode!()
@json """ @json_btc_price """
[
{ {
"id": "poa-network", "rates": {
"name": "POA Network", "usd": {
"symbol": "POA", "name": "US Dollar",
"rank": "103", "unit": "$",
"price_usd": "0.485053", "value": 6547.418,
"price_btc": "0.00007032", "type": "fiat"
"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 describe "format_data/1" do
test "bring a list with one %Token{}" do setup do
bypass = Bypass.open()
Application.put_env(:explorer, CoinGecko, base_url: "http://localhost:#{bypass.port}")
{:ok, bypass: bypass}
end
test "bring a list with one %Token{}", %{bypass: bypass} do
Bypass.expect(bypass, "GET", "/exchange_rates", fn conn ->
Conn.resp(conn, 200, @json_btc_price)
end)
assert [%Token{}] = TokenBridge.format_data(@json) assert [%Token{}] = TokenBridge.format_data(@json)
end end
end end

@ -39,7 +39,8 @@ defmodule Explorer.DataCase do
Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo, {:shared, self()}) Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo, {:shared, self()})
end end
Explorer.Chain.Cache.BlockNumber.setup() Supervisor.terminate_child(Explorer.Supervisor, Explorer.Chain.Cache.BlockNumber.child_id())
Supervisor.restart_child(Explorer.Supervisor, Explorer.Chain.Cache.BlockNumber.child_id())
Supervisor.terminate_child(Explorer.Supervisor, Explorer.Chain.Cache.Blocks.child_id()) Supervisor.terminate_child(Explorer.Supervisor, Explorer.Chain.Cache.Blocks.child_id())
Supervisor.restart_child(Explorer.Supervisor, Explorer.Chain.Cache.Blocks.child_id()) Supervisor.restart_child(Explorer.Supervisor, Explorer.Chain.Cache.Blocks.child_id())
Supervisor.terminate_child(Explorer.Supervisor, Explorer.Chain.Cache.Transactions.child_id()) Supervisor.terminate_child(Explorer.Supervisor, Explorer.Chain.Cache.Transactions.child_id())

@ -184,11 +184,10 @@ defmodule Indexer.Block.Fetcher do
end end
defp update_block_cache(blocks) when is_list(blocks) do defp update_block_cache(blocks) when is_list(blocks) do
max_block = Enum.max_by(blocks, fn block -> block.number end) {min_block, max_block} = Enum.min_max_by(blocks, & &1.number)
min_block = Enum.min_by(blocks, fn block -> block.number end)
BlockNumber.update(max_block.number) BlockNumber.update_all(max_block.number)
BlockNumber.update(min_block.number) BlockNumber.update_all(min_block.number)
BlocksCache.update(blocks) BlocksCache.update(blocks)
end end
@ -269,7 +268,7 @@ defmodule Indexer.Block.Fetcher do
end end
def async_import_internal_transactions(%{transactions: transactions}, EthereumJSONRPC.Geth) do def async_import_internal_transactions(%{transactions: transactions}, EthereumJSONRPC.Geth) do
{_, max_block_number} = Chain.fetch_min_and_max_block_numbers() max_block_number = Chain.fetch_max_block_number()
transactions transactions
|> Enum.flat_map(fn |> Enum.flat_map(fn

@ -162,7 +162,7 @@ defmodule Indexer.Fetcher.CoinBalanceOnDemand do
end end
defp latest_block_number do defp latest_block_number do
BlockNumber.max_number() BlockNumber.get_max()
end end
defp stale_balance_window(block_number) do defp stale_balance_window(block_number) do

@ -14,54 +14,55 @@ $ export NETWORK=POA
``` ```
| Variable | Required | Description | Default | Version | Need recompile | | Variable | Required | Description | Default | Version | Need recompile | Deprecated in Version |
| --- | --- | --- | ---| --- | --- | | --- | --- | --- | ---| --- | --- | --- |
| `NETWORK`| :white_check_mark: | Environment variable for the main EVM network such as Ethereum Network or POA Network | POA Network | all | | | `NETWORK`| :white_check_mark: | Environment variable for the main EVM network such as Ethereum Network or POA Network | POA Network | all | | |
| `SUBNETWORK` | :white_check_mark: | Environment variable for the subnetwork such as Core or Sokol Network | Sokol Testnet | all | | | `SUBNETWORK` | :white_check_mark: | Environment variable for the subnetwork such as Core or Sokol Network | Sokol Testnet | all | | |
| `NETWORK_ICON` | :white_check_mark: | Environment variable for the main network icon or testnet icon. Two options are `_test_network_icon.html` and `_network_icon.html` | `_test_network_icon.html` | all | | | `NETWORK_ICON` | :white_check_mark: | Environment variable for the main network icon or testnet icon. Two options are `_test_network_icon.html` and `_network_icon.html` | `_test_network_icon.html` | all | | |
| `LOGO` | :white_check_mark: | Environment variable for the logo image location. The logo files names for different chains can be found [here](https://github.com/poanetwork/blockscout/tree/master/apps/block_scout_web/assets/static/images) | /images/blockscout_logo.svg | all | | | `LOGO` | :white_check_mark: | Environment variable for the logo image location. The logo files names for different chains can be found [here](https://github.com/poanetwork/blockscout/tree/master/apps/block_scout_web/assets/static/images) | /images/blockscout_logo.svg | all | | |
| `ETHEREUM_JSONRPC_VARIANT` | :white_check_mark: | This environment variable is used to tell the application which RPC Client the node is using (i.e. Geth, Parity, or Ganache) | parity | all | | | `ETHEREUM_JSONRPC_VARIANT` | :white_check_mark: | This environment variable is used to tell the application which RPC Client the node is using (i.e. Geth, Parity, or Ganache) | parity | all | | |
| `ETHEREUM_JSONRPC_HTTP_URL` | :white_check_mark: | The RPC endpoint used to fetch blocks, transactions, receipts, tokens. | localhost:8545 | all | | | `ETHEREUM_JSONRPC_HTTP_URL` | :white_check_mark: | The RPC endpoint used to fetch blocks, transactions, receipts, tokens. | localhost:8545 | all | | |
| `ETHEREUM_JSONRPC_TRACE_URL` | | The RPC endpoint specifically for the Geth/Parity client used by trace_block and trace_replayTransaction. This can be used to designate a tracing node. | localhost:8545 | all | | | `ETHEREUM_JSONRPC_TRACE_URL` | | The RPC endpoint specifically for the Geth/Parity client used by trace_block and trace_replayTransaction. This can be used to designate a tracing node. | localhost:8545 | all | | |
| `ETHEREUM_JSONRPC_WS_URL` | :white_check_mark: | The WebSockets RPC endpoint used to subscribe to the `newHeads` subscription alerting the indexer to fetch new blocks. | ws://localhost:8546 | all | | | `ETHEREUM_JSONRPC_WS_URL` | :white_check_mark: | The WebSockets RPC endpoint used to subscribe to the `newHeads` subscription alerting the indexer to fetch new blocks. | ws://localhost:8546 | all | | |
| `NETWORK_PATH` | | Used to set a network path other than what is displayed in the root directory. An example would be to add /eth/mainnet/ to the root directory. | (empty) | all | | | `NETWORK_PATH` | | Used to set a network path other than what is displayed in the root directory. An example would be to add /eth/mainnet/ to the root directory. | (empty) | all | | |
| `SECRET_KEY_BASE` | :white_check_mark: | Use mix phx.gen.secret to generate a new Secret Key Base string to protect production assets. | (empty) | all | | | `SECRET_KEY_BASE` | :white_check_mark: | Use mix phx.gen.secret to generate a new Secret Key Base string to protect production assets. | (empty) | all | | |
| `CHECK_ORIGIN` | | Used to check the origin of requests when the origin header is present. It defaults to false. In case of true, it will check against the host value. | false | all | | | `CHECK_ORIGIN` | | Used to check the origin of requests when the origin header is present. It defaults to false. In case of true, it will check against the host value. | false | all | | |
| `PORT` | :white_check_mark: | Default port the application runs on is 4000 | 4000 | all | | | `PORT` | :white_check_mark: | Default port the application runs on is 4000 | 4000 | all | | |
| `COIN` | :white_check_mark: | The coin here is checked via the Coinmarketcap API to obtain USD prices on graphs and other areas of the UI | POA | all | | | `COIN` | :white_check_mark: | The coin here is checked via the CoinGecko API to obtain USD prices on graphs and other areas of the UI | POA | all | | |
| `METADATA_CONTRACT` | | This environment variable is specifically used by POA Network to obtain Validators information to display in the UI. | (empty) | all | | | `METADATA_CONTRACT` | | This environment variable is specifically used by POA Network to obtain Validators information to display in the UI. | (empty) | all | | |
| `VALIDATORS_CONTRACT` | | This environment variable is specifically used by POA Network to obtain the Emission Fund contract. | (empty) | all | | | `VALIDATORS_CONTRACT` | | This environment variable is specifically used by POA Network to obtain the Emission Fund contract. | (empty) | all | | |
| `SUPPLY_MODULE` | | This environment variable is used by the xDai Chain in order to tell the application how to calculate the total supply of the chain. | false | all | | | `SUPPLY_MODULE` | | This environment variable is used by the xDai Chain in order to tell the application how to calculate the total supply of the chain. | false | all | | |
| `SOURCE_MODULE` | | This environment variable is used to calculate the exchange rate and is specifically used by the xDai Chain. | false | all | | | `SOURCE_MODULE` | | This environment variable is used to calculate the exchange rate and is specifically used by the xDai Chain. | false | all | | |
| `DATABASE_URL` | | Production environment variable to define the Database endpoint. | (empty) | all | | | `DATABASE_URL` | | Production environment variable to define the Database endpoint. | (empty) | all | | |
| `POOL_SIZE` | | Production environment variable to define the number of database connections allowed. | 20 | all | | | `POOL_SIZE` | | Production environment variable to define the number of database connections allowed. | 20 | all | | |
| `ECTO_USE_SSL` | | Production environment variable to use SSL on Ecto queries. | true | all | | | `ECTO_USE_SSL` | | Production environment variable to use SSL on Ecto queries. | true | all | | |
| `DATADOG_HOST` | | Host configuration setting for [Datadog integration](https://docs.datadoghq.com/integrations/) | (empty) | all | | | `DATADOG_HOST` | | Host configuration setting for [Datadog integration](https://docs.datadoghq.com/integrations/) | (empty) | all | | |
| `DATADOG_PORT` | | Port configuration setting for [Datadog integration](https://docs.datadoghq.com/integrations/). | (empty} | all | | | `DATADOG_PORT` | | Port configuration setting for [Datadog integration](https://docs.datadoghq.com/integrations/). | (empty} | all | | |
| `SPANDEX_BATCH_SIZE` | | [Spandex](https://github.com/spandex-project/spandex) and Datadog configuration setting. | (empty) | all | | `SPANDEX_BATCH_SIZE` | | [Spandex](https://github.com/spandex-project/spandex) and Datadog configuration setting. | (empty) | all | |
| `SPANDEX_SYNC_THRESHOLD` | | [Spandex](https://github.com/spandex-project/spandex) and Datadog configuration setting. | (empty) | all | | | `SPANDEX_SYNC_THRESHOLD` | | [Spandex](https://github.com/spandex-project/spandex) and Datadog configuration setting. | (empty) | all | | |
| `HEART_BEAT_TIMEOUT` | | Production environment variable to restart the application in the event of a crash. | 30 | all | | | `HEART_BEAT_TIMEOUT` | | Production environment variable to restart the application in the event of a crash. | 30 | all | | |
| `HEART_COMMAND` | | Production environment variable to restart the application in the event of a crash. | systemctl restart explorer.service | all | | | `HEART_COMMAND` | | Production environment variable to restart the application in the event of a crash. | systemctl restart explorer.service | all | | |
| `BLOCKSCOUT_VERSION` | | Added to the footer to signify the current BlockScout version. | (empty) | v1.3.4+ | | | `BLOCKSCOUT_VERSION` | | Added to the footer to signify the current BlockScout version. | (empty) | v1.3.4+ | | |
| `RELEASE_LINK` | | The link to Blockscout release notes in the footer. | <u>https: //github.com/poanetwork/</u> <br /><u>blockscout/releases/</u> <br /> <u>tag/${BLOCKSCOUT_VERSION}</u> | v1.3.5+ | | | `RELEASE_LINK` | | The link to Blockscout release notes in the footer. | <u>https: //github.com/poanetwork/</u> <br /><u>blockscout/releases/</u> <br /> <u>tag/${BLOCKSCOUT_VERSION}</u> | v1.3.5+ | | |
| `ELIXIR_VERSION` | | Elixir version to install on the node before Blockscout deploy. | (empty) | all | | | `ELIXIR_VERSION` | | Elixir version to install on the node before Blockscout deploy. | (empty) | all | | |
| `BLOCK_TRANSFORMER` | | Transformer for blocks: base or clique. | base | v1.3.4+ | | | `BLOCK_TRANSFORMER` | | Transformer for blocks: base or clique. | base | v1.3.4+ | | |
| `GRAPHIQL_TRANSACTION` | | Default transaction in query to GraphiQL. | (empty) | v1.2.0+ | :white_check_mark: | | `GRAPHIQL_TRANSACTION` | | Default transaction in query to GraphiQL. | (empty) | v1.2.0+ | :white_check_mark: | |
| `FIRST_BLOCK` | | The block number, where indexing begins from. | 0 | v1.3.8+ | | | `FIRST_BLOCK` | | The block number, where indexing begins from. | 0 | v1.3.8+ | | |
| `LAST_BLOCK` | | The block number, where indexing stops. | (empty) | v2.0.3+ | | | `LAST_BLOCK` | | The block number, where indexing stops. | (empty) | v2.0.3+ | | |
| `TXS_COUNT_CACHE_PERIOD` | | Interval in seconds to restart the task, which calculates the total txs count. | 60 * 60 * 2 | v1.3.9+ | | | `TXS_COUNT_CACHE_PERIOD` | | Interval in seconds to restart the task, which calculates the total txs count. | 60 * 60 * 2 | v1.3.9+ | | |
| `ADDRESS_WITH_BALANCES` <br /> `_UPDATE_INTERVAL`| | Interval in seconds to restart the task, which calculates addresses with balances. | 30 * 60 | v1.3.9+ | | | `ADDRESS_WITH_BALANCES` <br /> `_UPDATE_INTERVAL`| | Interval in seconds to restart the task, which calculates addresses with balances. | 30 * 60 | v1.3.9+ | | |
| `LINK_TO_OTHER_EXPLORERS` | | true/false. If true, links to other explorers are added in the footer | (empty) | v1.3.0+ | | | `LINK_TO_OTHER_EXPLORERS` | | true/false. If true, links to other explorers are added in the footer | (empty) | v1.3.0+ | | |
| `COINMARKETCAP_PAGES` | | the number of pages on coinmarketcap to list in order to find token's price | 10 | v1.3.10+ | | | `COINMARKETCAP_PAGES` | | the number of pages on coinmarketcap to list in order to find token's price | 10 | v1.3.10+ | | master |
| `SUPPORTED_CHAINS` | | Array of supported chains that displays in the footer and in the chains dropdown. This var was introduced in this PR [#1900](https://github.com/poanetwork/blockscout/pull/1900) and looks like an array of JSON objects. | (empty) | v2.0.0+ | | | `SUPPORTED_CHAINS` | | Array of supported chains that displays in the footer and in the chains dropdown. This var was introduced in this PR [#1900](https://github.com/poanetwork/blockscout/pull/1900) and looks like an array of JSON objects. | (empty) | v2.0.0+ | | |
| `BLOCK_COUNT_CACHE_PERIOD ` | | time to live of cache in seconds. This var was introduced in [#1876](https://github.com/poanetwork/blockscout/pull/1876) | 600 | v2.0.0+ | | | `BLOCK_COUNT_CACHE_PERIOD ` | | time to live of cache in seconds. This var was introduced in [#1876](https://github.com/poanetwork/blockscout/pull/1876) | 600 | v2.0.0+ | | |
| `ALLOWED_EVM_VERSIONS ` | | the comma-separated list of allowed EVM versions for contracts verification. This var was introduced in [#1964](https://github.com/poanetwork/blockscout/pull/1964) | "homestead, tangerineWhistle, spuriousDragon, byzantium, constantinople, petersburg" | v2.0.0+ | | | `ALLOWED_EVM_VERSIONS ` | | the comma-separated list of allowed EVM versions for contracts verification. This var was introduced in [#1964](https://github.com/poanetwork/blockscout/pull/1964) | "homestead, tangerineWhistle, spuriousDragon, byzantium, constantinople, petersburg" | v2.0.0+ | | |
| `AVERAGE_BLOCK_CACHE_PERIOD` | | Update of average block cache, in seconds | 30 minutes | v2.0.2+ | | `AVERAGE_BLOCK_CACHE_PERIOD` | | Update of average block cache, in seconds | 30 minutes | v2.0.2+ | |
| `MARKET_HISTORY_CACHE_PERIOD` | | Update of market history cache, in seconds | 6 hours | v2.0.2+ | | `MARKET_HISTORY_CACHE_PERIOD` | | Update of market history cache, in seconds | 6 hours | v2.0.2+ | |
| `DISABLE_WEBAPP` | | If `true`, endpoints to webapp are hidden (compile-time) | `false` | v2.0.3+ | :white_check_mark: | | `DISABLE_WEBAPP` | | If `true`, endpoints to webapp are hidden (compile-time) | `false` | v2.0.3+ | :white_check_mark: | |
| `DISABLE_READ_API` | | If `true`, read-only endpoints to API are hidden (compile-time) | `false` | v2.0.3+ | :white_check_mark: | | `DISABLE_READ_API` | | If `true`, read-only endpoints to API are hidden (compile-time) | `false` | v2.0.3+ | :white_check_mark: | |
| `DISABLE_WRITE_API` | | If `true`, write endpoints to API are hidden (compile-time) | `false` | v2.0.3+ | :white_check_mark: | | `DISABLE_WRITE_API` | | If `true`, write endpoints to API are hidden (compile-time) | `false` | v2.0.3+ | :white_check_mark: | |
| `DISABLE_INDEXER` | | If `true`, indexer application doesn't run | `false` | v2.0.3+ | :white_check_mark: | | `DISABLE_INDEXER` | | If `true`, indexer application doesn't run | `false` | v2.0.3+ | :white_check_mark: | |
| `WEBAPP_URL` | | Link to web application instance, e.g. `http://host/path` | (empty) | v2.0.3+ | | | `WEBAPP_URL` | | Link to web application instance, e.g. `http://host/path` | (empty) | v2.0.3+ | | |
| `API_URL` | | Link to API instance, e.g. `http://host/path` | (empty) | v2.0.3+ | | | `API_URL` | | Link to API instance, e.g. `http://host/path` | (empty) | v2.0.3+ | | |
| `CHAIN_SPEC_PATH` | | Chain specification path (absolute file system path or url) to import block emission reward ranges and genesis account balances from | (empty) | master | | | `CHAIN_SPEC_PATH` | | Chain specification path (absolute file system path or url) to import block emission reward ranges and genesis account balances from | (empty) | master | | |
| `COIN_GECKO_ID` | | CoinGecko coin id required for fetching an exchange rate | poa-network | master | | |

Loading…
Cancel
Save