Merge branch 'master' into ab-show-only-the-last-decompiled-version

pull/2118/head
Ayrat Badykov 6 years ago committed by GitHub
commit 843eb4efce
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 10
      CHANGELOG.md
  2. 7
      apps/block_scout_web/assets/css/components/_footer.scss
  3. 20
      apps/block_scout_web/assets/css/components/_verify_other_explorers.scss
  4. 5
      apps/block_scout_web/lib/block_scout_web/controller.ex
  5. 43
      apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/address_controller.ex
  6. 118
      apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/eth_controller.ex
  7. 8
      apps/block_scout_web/lib/block_scout_web/controllers/page_not_found_controller.ex
  8. 50
      apps/block_scout_web/lib/block_scout_web/etherscan.ex
  9. 4
      apps/block_scout_web/lib/block_scout_web/router.ex
  10. 2
      apps/block_scout_web/lib/block_scout_web/templates/address/_tile.html.eex
  11. 2
      apps/block_scout_web/lib/block_scout_web/templates/common_components/_pagination_container.html.eex
  12. 4
      apps/block_scout_web/lib/block_scout_web/templates/page_not_found/index.html.eex
  13. 6
      apps/block_scout_web/lib/block_scout_web/views/api/rpc/address_view.ex
  14. 36
      apps/block_scout_web/lib/block_scout_web/views/api/rpc/eth_rpc_view.ex
  15. 13
      apps/block_scout_web/lib/block_scout_web/views/api/rpc/eth_view.ex
  16. 5
      apps/block_scout_web/lib/block_scout_web/views/page_not_found.ex
  17. 199
      apps/block_scout_web/test/block_scout_web/controllers/api/rpc/eth_controller_test.exs
  18. 7
      apps/block_scout_web/test/block_scout_web/controllers/block_controller_test.exs
  19. 2
      apps/block_scout_web/test/block_scout_web/controllers/chain_controller_test.exs
  20. 3
      apps/block_scout_web/test/block_scout_web/features/viewing_chain_test.exs
  21. 5
      apps/explorer/lib/explorer/application.ex
  22. 99
      apps/explorer/lib/explorer/chain.ex
  23. 89
      apps/explorer/lib/explorer/chain/blocks_cache.ex
  24. 11
      apps/explorer/lib/explorer/chain/wei.ex
  25. 3
      apps/explorer/mix.exs
  26. 84
      apps/explorer/test/explorer/chain/blocks_cache_test.exs
  27. 2
      apps/explorer/test/support/data_case.ex
  28. 3
      apps/indexer/lib/indexer/block/fetcher.ex
  29. 2
      apps/indexer/lib/indexer/block/realtime/fetcher.ex
  30. 33
      apps/indexer/lib/indexer/temporary/blocks_transactions_mismatch.ex
  31. 2
      bin/install_chrome_headless.sh
  32. 1
      mix.lock

@ -1,8 +1,13 @@
## Current ## Current
### Features ### Features
- [#2109](https://github.com/poanetwork/blockscout/pull/2109) - use bigger updates instead of `Multi` transactions in BlocksTransactionsMismatch
- [#2075](https://github.com/poanetwork/blockscout/pull/2075) - add blocks cache
### Fixes ### Fixes
- [#2129](https://github.com/poanetwork/blockscout/pull/2129) - Fix for width of explorer elements
- [#2121](https://github.com/poanetwork/blockscout/pull/2121) - Binding of 404 page
- [#2120](https://github.com/poanetwork/blockscout/pull/2120) - footer links and socials focus color issue
- [#2113](https://github.com/poanetwork/blockscout/pull/2113) - renewed logos for rsk, dai, blockscout; themes color changes for lukso; error images for lukso - [#2113](https://github.com/poanetwork/blockscout/pull/2113) - renewed logos for rsk, dai, blockscout; themes color changes for lukso; error images for lukso
- [#2112](https://github.com/poanetwork/blockscout/pull/2112) - themes color improvements, dropdown color issue - [#2112](https://github.com/poanetwork/blockscout/pull/2112) - themes color improvements, dropdown color issue
- [#2110](https://github.com/poanetwork/blockscout/pull/2110) - themes colors issues, ui issues - [#2110](https://github.com/poanetwork/blockscout/pull/2110) - themes colors issues, ui issues
@ -11,8 +16,12 @@
- [#2096](https://github.com/poanetwork/blockscout/pull/2096) - RSK theme fixes - [#2096](https://github.com/poanetwork/blockscout/pull/2096) - RSK theme fixes
- [#2093](https://github.com/poanetwork/blockscout/pull/2093) - detect token transfer type for deprecated erc721 spec - [#2093](https://github.com/poanetwork/blockscout/pull/2093) - detect token transfer type for deprecated erc721 spec
- [#2108](https://github.com/poanetwork/blockscout/pull/2108) - fix uncle fetching without full transactions - [#2108](https://github.com/poanetwork/blockscout/pull/2108) - fix uncle fetching without full transactions
- [#2123](https://github.com/poanetwork/blockscout/pull/2123) - fix coins percentage view
- [#2119](https://github.com/poanetwork/blockscout/pull/2119) - fix map logging
- [#2130](https://github.com/poanetwork/blockscout/pull/2130) - fix navigation
### Chore ### Chore
- [#2127](https://github.com/poanetwork/blockscout/pull/2127) - use previouse chromedriver version
- [#2118](https://github.com/poanetwork/blockscout/pull/2118) - show only the last decompiled contract - [#2118](https://github.com/poanetwork/blockscout/pull/2118) - show only the last decompiled contract
### Chore ### Chore
@ -42,6 +51,7 @@
- [#2037](https://github.com/poanetwork/blockscout/pull/2037) - add address logs search functionality - [#2037](https://github.com/poanetwork/blockscout/pull/2037) - add address logs search functionality
- [#2012](https://github.com/poanetwork/blockscout/pull/2012) - make all pages pagination async - [#2012](https://github.com/poanetwork/blockscout/pull/2012) - make all pages pagination async
- [#2064](https://github.com/poanetwork/blockscout/pull/2064) - feat: add fields to tx apis, small cleanups - [#2064](https://github.com/poanetwork/blockscout/pull/2064) - feat: add fields to tx apis, small cleanups
- [#2100](https://github.com/poanetwork/blockscout/pull/2100) - feat: eth_get_balance rpc endpoint
### Fixes ### Fixes
- [#2099](https://github.com/poanetwork/blockscout/pull/2099) - logs search input width - [#2099](https://github.com/poanetwork/blockscout/pull/2099) - logs search input width

@ -52,6 +52,10 @@ $footer-logo-width: auto !default;
color: #fff; color: #fff;
} }
&:focus {
color: darken($footer-text-color, 5);
}
&:last-child { &:last-child {
margin-right: 0; margin-right: 0;
} }
@ -98,6 +102,9 @@ $footer-logo-width: auto !default;
text-decoration: none; text-decoration: none;
color: #fff; color: #fff;
} }
&:focus {
color: darken($footer-text-color, 5);
}
} }
&::before { &::before {

@ -18,9 +18,13 @@
line-height: 1.25; line-height: 1.25;
display: inline-flex; display: inline-flex;
margin-bottom: 12px; margin-bottom: 12px;
@media (min-width: 1200px) { width: 100%;
@media (min-width: 768px) {
margin-right: 10px; margin-right: 10px;
}
@media (min-width: 1200px) {
margin-bottom: 0; margin-bottom: 0;
width: auto;
} }
} }
} }
@ -31,6 +35,8 @@
flex-grow: 2; flex-grow: 2;
@media (min-width: 768px) { @media (min-width: 768px) {
flex-direction: row; flex-direction: row;
position: relative;
padding-right: 44px;
} }
} }
@ -41,6 +47,7 @@
flex-grow: 2; flex-grow: 2;
@media (min-width: 768px) { @media (min-width: 768px) {
margin-top: 0; margin-top: 0;
max-width: 188px;
} }
@media (min-width: 1200px) { @media (min-width: 1200px) {
min-width: 145px; min-width: 145px;
@ -76,7 +83,7 @@
} }
.exp-content { .exp-content {
padding: 6px 9px 4px 9px; padding: 6px 9px 5px 9px;
h3, div { h3, div {
font-size: 10px; font-size: 10px;
line-height: 1; line-height: 1;
@ -117,7 +124,7 @@
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
border: 1px solid $secondary; border: 1px solid $btn-line-color;
border-radius: 2px; border-radius: 2px;
margin-top: 10px; margin-top: 10px;
transition: .1s ease-in; transition: .1s ease-in;
@ -125,12 +132,15 @@
@media (min-width: 768px) { @media (min-width: 768px) {
margin-left: 10px; margin-left: 10px;
margin-top: 0; margin-top: 0;
position: absolute;
top: 0;
right: 0;
} }
svg path { svg path {
fill: $secondary; fill: $btn-line-color;
} }
&:hover { &:hover {
background-color: $secondary; background-color: $btn-line-color;
svg path { svg path {
fill: #fff; fill: #fff;
} }

@ -12,8 +12,9 @@ defmodule BlockScoutWeb.Controller do
def not_found(conn) do def not_found(conn) do
conn conn
|> put_status(:not_found) |> put_status(:not_found)
|> put_view(BlockScoutWeb.ErrorView) |> put_view(BlockScoutWeb.PageNotFoundView)
|> render("404.html") |> render(:index)
|> halt()
end end
def unprocessable_entity(conn) do def unprocessable_entity(conn) do

@ -20,6 +20,35 @@ defmodule BlockScoutWeb.API.RPC.AddressController do
|> render(:listaccounts, %{accounts: accounts}) |> render(:listaccounts, %{accounts: accounts})
end end
def eth_get_balance(conn, params) do
with {:address_param, {:ok, address_param}} <- fetch_address(params),
{:block_param, {:ok, block}} <- {:block_param, fetch_block_param(params)},
{:format, {:ok, address_hash}} <- to_address_hash(address_param),
{:balance, {:ok, balance}} <- {:balance, Chain.get_balance_as_of_block(address_hash, block)} do
render(conn, :eth_get_balance, %{balance: Wei.hex_format(balance)})
else
{:address_param, :error} ->
conn
|> put_status(400)
|> render(:eth_get_balance_error, %{message: "Query parameter 'address' is required"})
{:format, :error} ->
conn
|> put_status(400)
|> render(:eth_get_balance_error, %{error: "Invalid address hash"})
{:block_param, :error} ->
conn
|> put_status(400)
|> render(:eth_get_balance_error, %{error: "Invalid block"})
{:balance, {:error, :not_found}} ->
conn
|> put_status(404)
|> render(:eth_get_balance_error, %{error: "Balance not found"})
end
end
def balance(conn, params, template \\ :balance) do def balance(conn, params, template \\ :balance) do
with {:address_param, {:ok, address_param}} <- fetch_address(params), with {:address_param, {:ok, address_param}} <- fetch_address(params),
{:format, {:ok, address_hashes}} <- to_address_hashes(address_param) do {:format, {:ok, address_hashes}} <- to_address_hashes(address_param) do
@ -217,6 +246,20 @@ defmodule BlockScoutWeb.API.RPC.AddressController do
{:required_params, result} {:required_params, result}
end end
defp fetch_block_param(%{"block" => "latest"}), do: {:ok, :latest}
defp fetch_block_param(%{"block" => "earliest"}), do: {:ok, :earliest}
defp fetch_block_param(%{"block" => "pending"}), do: {:ok, :pending}
defp fetch_block_param(%{"block" => string_integer}) when is_bitstring(string_integer) do
case Integer.parse(string_integer) do
{integer, ""} -> {:ok, integer}
_ -> :error
end
end
defp fetch_block_param(%{"block" => _block}), do: :error
defp fetch_block_param(_), do: {:ok, :latest}
defp to_valid_format(params, :tokenbalance) do defp to_valid_format(params, :tokenbalance) do
result = result =
with {:ok, contract_address_hash} <- to_address_hash(params, "contractaddress"), with {:ok, contract_address_hash} <- to_address_hash(params, "contractaddress"),

@ -0,0 +1,118 @@
defmodule BlockScoutWeb.API.RPC.EthController do
use BlockScoutWeb, :controller
alias Explorer.Chain
alias Explorer.Chain.Wei
def eth_request(%{body_params: %{"_json" => requests}} = conn, _) when is_list(requests) do
responses = responses(requests)
conn
|> put_status(200)
|> render("responses.json", %{responses: responses})
end
def eth_request(%{body_params: %{"_json" => request}} = conn, _) do
[response] = responses([request])
conn
|> put_status(200)
|> render("response.json", %{response: response})
end
def eth_request(conn, request) do
# In the case that the JSON body is sent up w/o a json content type,
# Phoenix encodes it as a single key value pair, with the value being
# nil and the body being the key (as in a CURL request w/ no content type header)
decoded_request =
with [{single_key, nil}] <- Map.to_list(request),
{:ok, decoded} <- Jason.decode(single_key) do
decoded
else
_ -> request
end
[response] = responses([decoded_request])
conn
|> put_status(200)
|> render("response.json", %{response: response})
end
defp responses(requests) do
Enum.map(requests, fn request ->
with {:id, {:ok, id}} <- {:id, Map.fetch(request, "id")},
{:request, {:ok, result}} <- {:request, do_eth_request(request)} do
format_success(result, id)
else
{:id, :error} -> format_error("id is a required field", 0)
{:request, {:error, message}} -> format_error(message, Map.get(request, "id"))
end
end)
end
defp format_success(result, id) do
%{result: result, id: id}
end
defp format_error(message, id) do
%{error: message, id: id}
end
defp do_eth_request(%{"jsonrpc" => rpc_version}) when rpc_version != "2.0" do
{:error, "invalid rpc version"}
end
defp do_eth_request(%{"jsonrpc" => "2.0", "method" => method, "params" => params})
when is_list(params) do
with {:ok, action} <- get_action(method),
true <- :erlang.function_exported(__MODULE__, action, Enum.count(params)) do
apply(__MODULE__, action, params)
else
_ ->
{:error, "Action not found."}
end
end
defp do_eth_request(%{"params" => _params, "method" => _}) do
{:error, "Invalid params. Params must be a list."}
end
defp do_eth_request(_) do
{:error, "Method, params, and jsonrpc, are all required parameters."}
end
def eth_get_balance(address_param, block_param \\ nil) do
with {:address, {:ok, address}} <- {:address, Chain.string_to_address_hash(address_param)},
{:block, {:ok, block}} <- {:block, block_param(block_param)},
{:balance, {:ok, balance}} <- {:balance, Chain.get_balance_as_of_block(address, block)} do
{:ok, Wei.hex_format(balance)}
else
{:address, :error} ->
{:error, "Query parameter 'address' is invalid"}
{:block, :error} ->
{:error, "Query parameter 'block' is invalid"}
{:balance, {:error, :not_found}} ->
{:error, "Balance not found"}
end
end
defp get_action("eth_getBalance"), do: {:ok, :eth_get_balance}
defp get_action(_), do: :error
defp block_param("latest"), do: {:ok, :latest}
defp block_param("earliest"), do: {:ok, :earliest}
defp block_param("pending"), do: {:ok, :pending}
defp block_param(string_integer) when is_bitstring(string_integer) do
case Integer.parse(string_integer) do
{integer, ""} -> {:ok, integer}
_ -> :error
end
end
defp block_param(nil), do: {:ok, :latest}
defp block_param(_), do: :error
end

@ -0,0 +1,8 @@
defmodule BlockScoutWeb.PageNotFoundController do
use BlockScoutWeb, :controller
def index(conn, _params) do
conn
|> render("index.html")
end
end

@ -100,6 +100,12 @@ defmodule BlockScoutWeb.Etherscan do
"result" => [] "result" => []
} }
@account_eth_get_balance_example_value %{
"jsonrpc" => "2.0",
"result" => "0x0234c8a3397aab58",
"id" => 1
}
@account_tokentx_example_value %{ @account_tokentx_example_value %{
"status" => "1", "status" => "1",
"message" => "OK", "message" => "OK",
@ -1028,6 +1034,49 @@ defmodule BlockScoutWeb.Etherscan do
} }
} }
@account_eth_get_balance_action %{
name: "eth_get_balance",
description:
"Mimics Ethereum JSON RPC's eth_getBalance. Returns the balance as of the provided block (defaults to latest)",
required_params: [
%{
key: "address",
placeholder: "addressHash",
type: "string",
description: "The address of the account."
}
],
optional_params: [
%{
key: "block",
placeholder: "block",
type: "string",
description: """
Either the block number as a string, or one of latest, earliest or pending
latest will be the latest balance in a *consensus* block.
earliest will be the first recorded balance for the address.
pending will be the latest balance in consensus *or* nonconcensus blocks.
"""
}
],
responses: [
%{
code: "200",
description: "successful operation",
example_value: Jason.encode!(@account_eth_get_balance_example_value),
model: %{
name: "Result",
fields: %{
jsonrpc: @jsonrpc_version_type,
id: @id_type,
result: @hex_number_type
}
}
}
]
}
@account_balance_action %{ @account_balance_action %{
name: "balance", name: "balance",
description: """ description: """
@ -2203,6 +2252,7 @@ defmodule BlockScoutWeb.Etherscan do
@account_module %{ @account_module %{
name: "account", name: "account",
actions: [ actions: [
@account_eth_get_balance_action,
@account_balance_action, @account_balance_action,
@account_balancemulti_action, @account_balancemulti_action,
@account_txlist_action, @account_txlist_action,

@ -32,6 +32,8 @@ defmodule BlockScoutWeb.Router do
alias BlockScoutWeb.API.RPC alias BlockScoutWeb.API.RPC
post("/eth_rpc", EthController, :eth_request)
forward("/", RPCTranslator, %{ forward("/", RPCTranslator, %{
"block" => RPC.BlockController, "block" => RPC.BlockController,
"account" => RPC.AddressController, "account" => RPC.AddressController,
@ -245,5 +247,7 @@ defmodule BlockScoutWeb.Router do
get("/chain_blocks", ChainController, :chain_blocks, as: :chain_blocks) get("/chain_blocks", ChainController, :chain_blocks, as: :chain_blocks)
get("/api_docs", APIDocsController, :index) get("/api_docs", APIDocsController, :index)
get("/:page", PageNotFoundController, :index)
end end
end end

@ -39,7 +39,7 @@
</span> </span>
<!-- percentage of coins from total supply --> <!-- percentage of coins from total supply -->
<span class="ml-0 ml-md-2"> <span class="ml-0 ml-md-2">
<% if @total_supply do %> <%= if @total_supply do %>
(<%= balance_percentage(@address, @total_supply) %>) (<%= balance_percentage(@address, @total_supply) %>)
<% end %> <% end %>
</span> </span>

@ -1,4 +1,4 @@
<div class='pagination-container <%= if assigns[:position] == "top" do %>position-top<% end %> <%= if assigns[:position] == "bottom" do %>position-bottom<% end %>' <%= if !assigns[:next_page_path] do %>disabled<% end %>> <div class='pagination-container <%= if assigns[:position] == "top" do %>position-top<% end %> <%= if assigns[:position] == "bottom" do %>position-bottom<% end %>'>
<%= if false do %> <%= if false do %>
<!-- Pagination limit --> <!-- Pagination limit -->
<div class="pagination-limit"> <div class="pagination-limit">

@ -4,8 +4,8 @@
<img alt="Page Not Found" src="/images/errors-img/poa-page-not-found.png" srcset="/images/errors-img/poa-page-not-found@2x.png 2x"> <img alt="Page Not Found" src="/images/errors-img/poa-page-not-found.png" srcset="/images/errors-img/poa-page-not-found@2x.png 2x">
</div> </div>
<div class="block-not-found-content"> <div class="block-not-found-content">
<h1 class="card-title error-title">Lorem Ipsum Dolor</h1> <h1 class="card-title error-title">Page not found</h1>
<p class="error-descr">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua</p> <p class="error-descr">The requested path was not found on BlockScout.</p>
<a class="error-btn btn-line" href="/">Back Home</a> <a class="error-btn btn-line" href="/">Back Home</a>
</div> </div>
</div> </div>

@ -1,7 +1,7 @@
defmodule BlockScoutWeb.API.RPC.AddressView do defmodule BlockScoutWeb.API.RPC.AddressView do
use BlockScoutWeb, :view use BlockScoutWeb, :view
alias BlockScoutWeb.API.RPC.RPCView alias BlockScoutWeb.API.RPC.{EthRPCView, RPCView}
def render("listaccounts.json", %{accounts: accounts}) do def render("listaccounts.json", %{accounts: accounts}) do
accounts = Enum.map(accounts, &prepare_account/1) accounts = Enum.map(accounts, &prepare_account/1)
@ -51,6 +51,10 @@ defmodule BlockScoutWeb.API.RPC.AddressView do
RPCView.render("show.json", data: data) RPCView.render("show.json", data: data)
end end
def render("eth_get_balance_error.json", %{error: message}) do
EthRPCView.render("error.json", %{error: message, id: 0})
end
def render("error.json", assigns) do def render("error.json", assigns) do
RPCView.render("error.json", assigns) RPCView.render("error.json", assigns)
end end

@ -17,16 +17,48 @@ defmodule BlockScoutWeb.API.RPC.EthRPCView do
} }
end end
def render("response.json", %{response: %{error: error, id: id}}) do
%__MODULE__{
error: error,
id: id
}
end
def render("response.json", %{response: %{result: result, id: id}}) do
%__MODULE__{
result: result,
id: id
}
end
def render("responses.json", %{responses: responses}) do
Enum.map(responses, fn
%{error: error, id: id} ->
%__MODULE__{
error: error,
id: id
}
%{result: result, id: id} ->
%__MODULE__{
result: result,
id: id
}
end)
end
defimpl Poison.Encoder, for: BlockScoutWeb.API.RPC.EthRPCView do defimpl Poison.Encoder, for: BlockScoutWeb.API.RPC.EthRPCView do
def encode(%BlockScoutWeb.API.RPC.EthRPCView{result: result, id: id, error: error}, _options) when is_nil(error) do def encode(%BlockScoutWeb.API.RPC.EthRPCView{result: result, id: id, error: error}, _options) when is_nil(error) do
result = Poison.encode!(result)
""" """
{"jsonrpc":"2.0","result":"#{result}","id":#{id}} {"jsonrpc":"2.0","result":#{result},"id":#{id}}
""" """
end end
def encode(%BlockScoutWeb.API.RPC.EthRPCView{id: id, error: error}, _options) do def encode(%BlockScoutWeb.API.RPC.EthRPCView{id: id, error: error}, _options) do
""" """
{"jsonrpc":"2.0","error": #{error},"id": #{id}} {"jsonrpc":"2.0","error": "#{error}","id": #{id}}
""" """
end end
end end

@ -0,0 +1,13 @@
defmodule BlockScoutWeb.API.RPC.EthView do
use BlockScoutWeb, :view
alias BlockScoutWeb.API.RPC.EthRPCView
def render("responses.json", %{responses: responses}) do
EthRPCView.render("responses.json", %{responses: responses})
end
def render("response.json", %{response: response}) do
EthRPCView.render("response.json", %{response: response})
end
end

@ -0,0 +1,5 @@
defmodule BlockScoutWeb.PageNotFoundView do
use BlockScoutWeb, :view
@dialyzer :no_match
end

@ -0,0 +1,199 @@
defmodule BlockScoutWeb.API.RPC.EthControllerTest do
use BlockScoutWeb.ConnCase, async: false
alias Explorer.Counters.{AddressesWithBalanceCounter, AverageBlockTime}
alias Indexer.Fetcher.CoinBalanceOnDemand
setup do
mocked_json_rpc_named_arguments = [
transport: EthereumJSONRPC.Mox,
transport_options: []
]
start_supervised!({Task.Supervisor, name: Indexer.TaskSupervisor})
start_supervised!(AverageBlockTime)
start_supervised!({CoinBalanceOnDemand, [mocked_json_rpc_named_arguments, [name: CoinBalanceOnDemand]]})
start_supervised!(AddressesWithBalanceCounter)
Application.put_env(:explorer, AverageBlockTime, enabled: true)
on_exit(fn ->
Application.put_env(:explorer, AverageBlockTime, enabled: false)
end)
:ok
end
defp params(api_params, params), do: Map.put(api_params, "params", params)
describe "eth_get_balance" do
setup do
%{
api_params: %{
"method" => "eth_getBalance",
"jsonrpc" => "2.0",
"id" => 0
}
}
end
test "with an invalid address", %{conn: conn, api_params: api_params} do
assert response =
conn
|> post("/api/eth_rpc", params(api_params, ["badHash"]))
|> json_response(200)
assert %{"error" => "Query parameter 'address' is invalid"} = response
end
test "with a valid address that has no balance", %{conn: conn, api_params: api_params} do
address = insert(:address)
assert response =
conn
|> post("/api/eth_rpc", params(api_params, [to_string(address.hash)]))
|> json_response(200)
assert %{"error" => "Balance not found"} = response
end
test "with a valid address that has a balance", %{conn: conn, api_params: api_params} do
block = insert(:block)
address = insert(:address)
insert(:fetched_balance, block_number: block.number, address_hash: address.hash, value: 1)
assert response =
conn
|> post("/api/eth_rpc", params(api_params, [to_string(address.hash)]))
|> json_response(200)
assert %{"result" => "0x1"} = response
end
test "with a valid address that has no earliest balance", %{conn: conn, api_params: api_params} do
block = insert(:block, number: 1)
address = insert(:address)
insert(:fetched_balance, block_number: block.number, address_hash: address.hash, value: 1)
assert response =
conn
|> post("/api/eth_rpc", params(api_params, [to_string(address.hash), "earliest"]))
|> json_response(200)
assert response["error"] == "Balance not found"
end
test "with a valid address that has an earliest balance", %{conn: conn, api_params: api_params} do
block = insert(:block, number: 0)
address = insert(:address)
insert(:fetched_balance, block_number: block.number, address_hash: address.hash, value: 1)
assert response =
conn
|> post("/api/eth_rpc", params(api_params, [to_string(address.hash), "earliest"]))
|> json_response(200)
assert response["result"] == "0x1"
end
test "with a valid address and no pending balance", %{conn: conn, api_params: api_params} do
block = insert(:block, number: 1, consensus: true)
address = insert(:address)
insert(:fetched_balance, block_number: block.number, address_hash: address.hash, value: 1)
assert response =
conn
|> post("/api/eth_rpc", params(api_params, [to_string(address.hash), "pending"]))
|> json_response(200)
assert response["error"] == "Balance not found"
end
test "with a valid address and a pending balance", %{conn: conn, api_params: api_params} do
block = insert(:block, number: 1, consensus: false)
address = insert(:address)
insert(:fetched_balance, block_number: block.number, address_hash: address.hash, value: 1)
assert response =
conn
|> post("/api/eth_rpc", params(api_params, [to_string(address.hash), "pending"]))
|> json_response(200)
assert response["result"] == "0x1"
end
test "with a valid address and a pending balance after a consensus block", %{conn: conn, api_params: api_params} do
insert(:block, number: 1, consensus: true)
block = insert(:block, number: 2, consensus: false)
address = insert(:address)
insert(:fetched_balance, block_number: block.number, address_hash: address.hash, value: 1)
assert response =
conn
|> post("/api/eth_rpc", params(api_params, [to_string(address.hash), "pending"]))
|> json_response(200)
assert response["result"] == "0x1"
end
test "with a block provided", %{conn: conn, api_params: api_params} do
address = insert(:address)
insert(:fetched_balance, block_number: 1, address_hash: address.hash, value: 1)
insert(:fetched_balance, block_number: 2, address_hash: address.hash, value: 2)
insert(:fetched_balance, block_number: 3, address_hash: address.hash, value: 3)
assert response =
conn
|> post("/api/eth_rpc", params(api_params, [to_string(address.hash), "2"]))
|> json_response(200)
assert response["result"] == "0x2"
end
test "with a block provided and no balance", %{conn: conn, api_params: api_params} do
address = insert(:address)
insert(:fetched_balance, block_number: 3, address_hash: address.hash, value: 3)
assert response =
conn
|> post("/api/eth_rpc", params(api_params, [to_string(address.hash), "2"]))
|> json_response(200)
assert response["error"] == "Balance not found"
end
test "with a batch of requests", %{conn: conn} do
address = insert(:address)
insert(:fetched_balance, block_number: 1, address_hash: address.hash, value: 1)
insert(:fetched_balance, block_number: 2, address_hash: address.hash, value: 2)
insert(:fetched_balance, block_number: 3, address_hash: address.hash, value: 3)
params = [
%{"id" => 0, "params" => [to_string(address.hash), "1"], "jsonrpc" => "2.0", "method" => "eth_getBalance"},
%{"id" => 1, "params" => [to_string(address.hash), "2"], "jsonrpc" => "2.0", "method" => "eth_getBalance"},
%{"id" => 2, "params" => [to_string(address.hash), "3"], "jsonrpc" => "2.0", "method" => "eth_getBalance"}
]
assert response =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/eth_rpc", Jason.encode!(params))
|> json_response(200)
assert [
%{"id" => 0, "result" => "0x1"},
%{"id" => 1, "result" => "0x2"},
%{"id" => 2, "result" => "0x3"}
] = response
end
end
end

@ -2,6 +2,13 @@ defmodule BlockScoutWeb.BlockControllerTest do
use BlockScoutWeb.ConnCase use BlockScoutWeb.ConnCase
alias Explorer.Chain.Block alias Explorer.Chain.Block
setup do
Supervisor.terminate_child(Explorer.Supervisor, ConCache)
Supervisor.restart_child(Explorer.Supervisor, ConCache)
:ok
end
describe "GET show/2" do describe "GET show/2" do
test "with block redirects to block transactions route", %{conn: conn} do test "with block redirects to block transactions route", %{conn: conn} do
insert(:block, number: 3) insert(:block, number: 3)

@ -9,6 +9,8 @@ defmodule BlockScoutWeb.ChainControllerTest do
alias Explorer.Counters.AddressesWithBalanceCounter alias Explorer.Counters.AddressesWithBalanceCounter
setup do setup do
Supervisor.terminate_child(Explorer.Supervisor, ConCache)
Supervisor.restart_child(Explorer.Supervisor, ConCache)
start_supervised!(AddressesWithBalanceCounter) start_supervised!(AddressesWithBalanceCounter)
AddressesWithBalanceCounter.consolidate() AddressesWithBalanceCounter.consolidate()

@ -10,6 +10,9 @@ defmodule BlockScoutWeb.ViewingChainTest do
alias Explorer.Counters.AddressesWithBalanceCounter alias Explorer.Counters.AddressesWithBalanceCounter
setup do setup do
Supervisor.terminate_child(Explorer.Supervisor, ConCache)
Supervisor.restart_child(Explorer.Supervisor, ConCache)
Enum.map(401..404, &insert(:block, number: &1)) Enum.map(401..404, &insert(:block, number: &1))
block = insert(:block, number: 405) block = insert(:block, number: 405)

@ -6,7 +6,7 @@ defmodule Explorer.Application do
use Application use Application
alias Explorer.Admin alias Explorer.Admin
alias Explorer.Chain.{BlockCountCache, BlockNumberCache, TransactionCountCache} alias Explorer.Chain.{BlockCountCache, BlockNumberCache, BlocksCache, TransactionCountCache}
alias Explorer.Repo.PrometheusLogger alias Explorer.Repo.PrometheusLogger
@impl Application @impl Application
@ -30,7 +30,8 @@ defmodule Explorer.Application do
{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]]},
{TransactionCountCache, [[], []]}, {TransactionCountCache, [[], []]},
{BlockCountCache, []} {BlockCountCache, []},
{ConCache, [name: BlocksCache.cache_name(), ttl_check_interval: false]}
] ]
children = base_children ++ configurable_children() children = base_children ++ configurable_children()

@ -33,6 +33,7 @@ defmodule Explorer.Chain do
Block, Block,
BlockCountCache, BlockCountCache,
BlockNumberCache, BlockNumberCache,
BlocksCache,
Data, Data,
DecompiledSmartContract, DecompiledSmartContract,
Hash, Hash,
@ -823,6 +824,86 @@ defmodule Explorer.Chain do
Repo.all(query) Repo.all(query)
end end
@doc """
Returns the balance of the given address and block combination.
Returns `{:error, :not_found}` if there is no address by that hash present.
Returns `{:error, :no_balance}` if there is no balance for that address at that block.
"""
@spec get_balance_as_of_block(Hash.Address.t(), integer | :earliest | :latest | :pending) ::
{:ok, Wei.t()} | {:error, :no_balance} | {:error, :not_found}
def get_balance_as_of_block(address, block) when is_integer(block) do
coin_balance_query =
from(coin_balance in CoinBalance,
where: coin_balance.address_hash == ^address,
where: not is_nil(coin_balance.value),
where: coin_balance.block_number <= ^block,
order_by: [desc: coin_balance.block_number],
limit: 1,
select: coin_balance.value
)
case Repo.one(coin_balance_query) do
nil -> {:error, :not_found}
coin_balance -> {:ok, coin_balance}
end
end
def get_balance_as_of_block(address, :latest) do
case max_consensus_block_number() do
{:ok, latest_block_number} ->
get_balance_as_of_block(address, latest_block_number)
{:error, :not_found} ->
{:error, :not_found}
end
end
def get_balance_as_of_block(address, :earliest) do
query =
from(coin_balance in CoinBalance,
where: coin_balance.address_hash == ^address,
where: not is_nil(coin_balance.value),
where: coin_balance.block_number == 0,
limit: 1,
select: coin_balance.value
)
case Repo.one(query) do
nil -> {:error, :not_found}
coin_balance -> {:ok, coin_balance}
end
end
def get_balance_as_of_block(address, :pending) do
query =
case max_consensus_block_number() do
{:ok, latest_block_number} ->
from(coin_balance in CoinBalance,
where: coin_balance.address_hash == ^address,
where: not is_nil(coin_balance.value),
where: coin_balance.block_number > ^latest_block_number,
order_by: [desc: coin_balance.block_number],
limit: 1,
select: coin_balance.value
)
{:error, :not_found} ->
from(coin_balance in CoinBalance,
where: coin_balance.address_hash == ^address,
where: not is_nil(coin_balance.value),
order_by: [desc: coin_balance.block_number],
limit: 1,
select: coin_balance.value
)
end
case Repo.one(query) do
nil -> {:error, :not_found}
coin_balance -> {:ok, coin_balance}
end
end
@spec list_ordered_addresses(non_neg_integer(), non_neg_integer()) :: [Address.t()] @spec list_ordered_addresses(non_neg_integer(), non_neg_integer()) :: [Address.t()]
def list_ordered_addresses(offset, limit) do def list_ordered_addresses(offset, limit) do
query = query =
@ -1149,9 +1230,25 @@ defmodule Explorer.Chain do
@spec list_blocks([paging_options | necessity_by_association_option]) :: [Block.t()] @spec list_blocks([paging_options | necessity_by_association_option]) :: [Block.t()]
def list_blocks(options \\ []) when is_list(options) do def list_blocks(options \\ []) when is_list(options) do
necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) necessity_by_association = Keyword.get(options, :necessity_by_association, %{})
paging_options = Keyword.get(options, :paging_options, @default_paging_options) paging_options = Keyword.get(options, :paging_options) || @default_paging_options
block_type = Keyword.get(options, :block_type, "Block") block_type = Keyword.get(options, :block_type, "Block")
if block_type == "Block" && !paging_options.key do
if BlocksCache.enough_elements?(paging_options.page_size) do
BlocksCache.blocks(paging_options.page_size)
else
elements = fetch_blocks(block_type, paging_options, necessity_by_association)
BlocksCache.rewrite_cache(elements)
elements
end
else
fetch_blocks(block_type, paging_options, necessity_by_association)
end
end
defp fetch_blocks(block_type, paging_options, necessity_by_association) do
Block Block
|> Block.block_type_filter(block_type) |> Block.block_type_filter(block_type)
|> page_blocks(paging_options) |> page_blocks(paging_options)

@ -0,0 +1,89 @@
defmodule Explorer.Chain.BlocksCache do
@moduledoc """
Caches the last imported blocks
"""
alias Explorer.Repo
@block_numbers_key "block_numbers"
@cache_name :blocks
@number_of_elements 60
def update(block) do
numbers = block_numbers()
max_number = if numbers == [], do: -1, else: Enum.max(numbers)
min_number = if numbers == [], do: -1, else: Enum.min(numbers)
in_range? = block.number > min_number && Enum.all?(numbers, fn number -> number != block.number end)
not_too_far_away? = block.number > max_number - @number_of_elements - 1
if (block.number > max_number || Enum.count(numbers) == 1 || in_range?) && not_too_far_away? do
if Enum.count(numbers) >= @number_of_elements do
remove_block(numbers)
put_block(block, List.delete(numbers, Enum.min(numbers)))
else
put_block(block, numbers)
end
end
end
def rewrite_cache(elements) do
numbers = block_numbers()
ConCache.delete(@cache_name, @block_numbers_key)
numbers
|> Enum.each(fn number ->
ConCache.delete(@cache_name, number)
end)
elements
|> Enum.reduce([], fn element, acc ->
put_block(element, acc)
[element.number | acc]
end)
end
def enough_elements?(number) do
ConCache.size(@cache_name) > number
end
def update_blocks(blocks) do
Enum.each(blocks, fn block ->
update(block)
end)
end
def blocks(number \\ nil) do
numbers = block_numbers()
number = if is_nil(number), do: Enum.count(numbers), else: number
numbers
|> Enum.sort()
|> Enum.reverse()
|> Enum.slice(0, number)
|> Enum.map(fn number ->
ConCache.get(@cache_name, number)
end)
end
def cache_name, do: @cache_name
def block_numbers do
ConCache.get(@cache_name, @block_numbers_key) || []
end
defp remove_block(numbers) do
min_number = Enum.min(numbers)
ConCache.delete(@cache_name, min_number)
end
defp put_block(block, numbers) do
block_with_preloads = Repo.preload(block, [:transactions, [miner: :names], :rewards])
ConCache.put(@cache_name, block.number, block_with_preloads)
ConCache.put(@cache_name, @block_numbers_key, [block.number | numbers])
end
end

@ -113,6 +113,17 @@ defmodule Explorer.Chain.Wei do
@wei_per_ether Decimal.new(1_000_000_000_000_000_000) @wei_per_ether Decimal.new(1_000_000_000_000_000_000)
@wei_per_gwei Decimal.new(1_000_000_000) @wei_per_gwei Decimal.new(1_000_000_000)
@spec hex_format(Wei.t()) :: String.t()
def hex_format(%Wei{value: decimal}) do
hex =
decimal
|> Decimal.to_integer()
|> Integer.to_string(16)
|> String.downcase()
"0x" <> hex
end
@doc """ @doc """
Sums two Wei values. Sums two Wei values.

@ -114,7 +114,8 @@ defmodule Explorer.Mixfile do
# Attach `:prometheus_ecto` to `:ecto` # Attach `:prometheus_ecto` to `:ecto`
{:telemetry, "~> 0.3.0"}, {:telemetry, "~> 0.3.0"},
# `Timex.Duration` for `Explorer.Counters.AverageBlockTime.average_block_time/0` # `Timex.Duration` for `Explorer.Counters.AverageBlockTime.average_block_time/0`
{:timex, "~> 3.4"} {:timex, "~> 3.4"},
{:con_cache, "~> 0.13"}
] ]
end end

@ -0,0 +1,84 @@
defmodule Explorer.Chain.BlocksCacheTest do
use Explorer.DataCase
alias Explorer.Chain.BlocksCache
alias Explorer.Repo
setup do
Supervisor.terminate_child(Explorer.Supervisor, ConCache)
Supervisor.restart_child(Explorer.Supervisor, ConCache)
:ok
end
describe "update/1" do
test "adds a new value to cache" do
block = insert(:block) |> Repo.preload([:transactions, [miner: :names], :rewards])
BlocksCache.update(block)
assert BlocksCache.blocks() == [block]
end
test "adds a new elements removing the oldest one" do
blocks =
1..60
|> Enum.map(fn number ->
block = insert(:block, number: number)
BlocksCache.update(block)
block.number
end)
new_block = insert(:block, number: 70)
BlocksCache.update(new_block)
new_blocks = blocks |> List.replace_at(0, new_block.number) |> Enum.sort() |> Enum.reverse()
assert Enum.map(BlocksCache.blocks(), & &1.number) == new_blocks
end
test "does not add too old blocks" do
block = insert(:block, number: 100_000) |> Repo.preload([:transactions, [miner: :names], :rewards])
old_block = insert(:block, number: 1_000)
BlocksCache.update(block)
BlocksCache.update(old_block)
assert BlocksCache.blocks() == [block]
end
test "adds missing element" do
block1 = insert(:block, number: 10)
block2 = insert(:block, number: 4)
BlocksCache.update(block1)
BlocksCache.update(block2)
assert Enum.count(BlocksCache.blocks()) == 2
block3 = insert(:block, number: 6)
BlocksCache.update(block3)
assert Enum.map(BlocksCache.blocks(), & &1.number) == [10, 6, 4]
end
end
describe "rewrite_cache/1" do
test "updates cache" do
block = insert(:block)
BlocksCache.update(block)
block1 = insert(:block) |> Repo.preload([:transactions, [miner: :names], :rewards])
block2 = insert(:block) |> Repo.preload([:transactions, [miner: :names], :rewards])
new_blocks = [block1, block2]
BlocksCache.rewrite_cache(new_blocks)
assert BlocksCache.blocks() == [block2, block1]
end
end
end

@ -40,6 +40,8 @@ defmodule Explorer.DataCase do
end end
Explorer.Chain.BlockNumberCache.setup() Explorer.Chain.BlockNumberCache.setup()
Supervisor.terminate_child(Explorer.Supervisor, ConCache)
Supervisor.restart_child(Explorer.Supervisor, ConCache)
:ok :ok
end end

@ -11,7 +11,7 @@ defmodule Indexer.Block.Fetcher do
alias EthereumJSONRPC.{Blocks, FetchedBeneficiaries} alias EthereumJSONRPC.{Blocks, FetchedBeneficiaries}
alias Explorer.Chain alias Explorer.Chain
alias Explorer.Chain.{Address, Block, BlockNumberCache, Hash, Import, Transaction} alias Explorer.Chain.{Address, Block, BlockNumberCache, BlocksCache, Hash, Import, Transaction}
alias Indexer.Block.Fetcher.Receipts alias Indexer.Block.Fetcher.Receipts
alias Indexer.Fetcher.{ alias Indexer.Fetcher.{
@ -186,6 +186,7 @@ defmodule Indexer.Block.Fetcher do
BlockNumberCache.update(max_block.number) BlockNumberCache.update(max_block.number)
BlockNumberCache.update(min_block.number) BlockNumberCache.update(min_block.number)
BlocksCache.update_blocks(blocks)
end end
def import( def import(

@ -140,7 +140,7 @@ defmodule Indexer.Block.Realtime.Fetcher do
%__MODULE__{state | subscription: subscription} %__MODULE__{state | subscription: subscription}
{:error, reason} -> {:error, reason} ->
Logger.debug(fn -> ["Could not connect to websocket: ", reason, ". Continuing with polling."] end) Logger.debug(fn -> ["Could not connect to websocket: #{inspect(reason)}. Continuing with polling."] end)
state state
end end
end end

@ -13,7 +13,6 @@ defmodule Indexer.Temporary.BlocksTransactionsMismatch do
import Ecto.Query import Ecto.Query
alias Ecto.Multi
alias EthereumJSONRPC.Blocks alias EthereumJSONRPC.Blocks
alias Explorer.Chain.Block alias Explorer.Chain.Block
alias Explorer.Repo alias Explorer.Repo
@ -23,13 +22,14 @@ defmodule Indexer.Temporary.BlocksTransactionsMismatch do
@defaults [ @defaults [
flush_interval: :timer.seconds(3), flush_interval: :timer.seconds(3),
max_batch_size: 10, max_batch_size: 50,
max_concurrency: 1, max_concurrency: 1,
task_supervisor: Indexer.Temporary.BlocksTransactionsMismatch.TaskSupervisor, task_supervisor: Indexer.Temporary.BlocksTransactionsMismatch.TaskSupervisor,
metadata: [fetcher: :blocks_transactions_mismatch] metadata: [fetcher: :blocks_transactions_mismatch]
] ]
@doc false @doc false
# credo:disable-for-next-line Credo.Check.Design.DuplicatedCode
def child_spec([init_options, gen_server_options]) when is_list(init_options) do def child_spec([init_options, gen_server_options]) when is_list(init_options) do
{state, mergeable_init_options} = Keyword.pop(init_options, :json_rpc_named_arguments) {state, mergeable_init_options} = Keyword.pop(init_options, :json_rpc_named_arguments)
@ -99,17 +99,26 @@ defmodule Indexer.Temporary.BlocksTransactionsMismatch do
Map.has_key?(found_blocks_map, to_string(block.hash)) Map.has_key?(found_blocks_map, to_string(block.hash))
end) end)
{:ok, _} = {matching_blocks_data, unmatching_blocks_data} =
found_blocks_data Enum.split_with(found_blocks_data, fn {block, trans_num} ->
|> Enum.reduce(Multi.new(), fn {block, trans_num}, multi -> found_blocks_map[to_string(block.hash)] == trans_num
changes = %{
refetch_needed: false,
consensus: found_blocks_map[to_string(block.hash)] == trans_num
}
Multi.update(multi, block.hash, Block.changeset(block, changes))
end) end)
|> Repo.transaction()
unless Enum.empty?(matching_blocks_data) do
hashes = Enum.map(matching_blocks_data, fn {block, _trans_num} -> block.hash end)
Block
|> where([block], block.hash in ^hashes)
|> Repo.update_all(set: [refetch_needed: false])
end
unless Enum.empty?(unmatching_blocks_data) do
hashes = Enum.map(unmatching_blocks_data, fn {block, _trans_num} -> block.hash end)
Block
|> where([block], block.hash in ^hashes)
|> Repo.update_all(set: [refetch_needed: false, consensus: false])
end
if Enum.empty?(missing_blocks_data) do if Enum.empty?(missing_blocks_data) do
:ok :ok

@ -1,6 +1,6 @@
export DISPLAY=:99.0 export DISPLAY=:99.0
sh -e /etc/init.d/xvfb start sh -e /etc/init.d/xvfb start
export CHROMEDRIVER_VERSION=`curl -s http://chromedriver.storage.googleapis.com/LATEST_RELEASE` export CHROMEDRIVER_VERSION=74.0.3729.6
curl -L -O "http://chromedriver.storage.googleapis.com/${CHROMEDRIVER_VERSION}/chromedriver_linux64.zip" curl -L -O "http://chromedriver.storage.googleapis.com/${CHROMEDRIVER_VERSION}/chromedriver_linux64.zip"
unzip chromedriver_linux64.zip unzip chromedriver_linux64.zip
sudo chmod +x chromedriver sudo chmod +x chromedriver

@ -16,6 +16,7 @@
"certifi": {:hex, :certifi, "2.5.1", "867ce347f7c7d78563450a18a6a28a8090331e77fa02380b4a21962a65d36ee5", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"}, "certifi": {:hex, :certifi, "2.5.1", "867ce347f7c7d78563450a18a6a28a8090331e77fa02380b4a21962a65d36ee5", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"},
"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], []}, "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], []},
"comeonin": {:hex, :comeonin, "4.1.2", "3eb5620fd8e35508991664b4c2b04dd41e52f1620b36957be837c1d7784b7592", [:mix], [{:argon2_elixir, "~> 1.2", [hex: :argon2_elixir, repo: "hexpm", optional: true]}, {:bcrypt_elixir, "~> 0.12.1 or ~> 1.0", [hex: :bcrypt_elixir, repo: "hexpm", optional: true]}, {:pbkdf2_elixir, "~> 0.12", [hex: :pbkdf2_elixir, repo: "hexpm", optional: true]}], "hexpm"}, "comeonin": {:hex, :comeonin, "4.1.2", "3eb5620fd8e35508991664b4c2b04dd41e52f1620b36957be837c1d7784b7592", [:mix], [{:argon2_elixir, "~> 1.2", [hex: :argon2_elixir, repo: "hexpm", optional: true]}, {:bcrypt_elixir, "~> 0.12.1 or ~> 1.0", [hex: :bcrypt_elixir, repo: "hexpm", optional: true]}, {:pbkdf2_elixir, "~> 0.12", [hex: :pbkdf2_elixir, repo: "hexpm", optional: true]}], "hexpm"},
"con_cache": {:hex, :con_cache, "0.13.1", "047e097ab2a8c6876e12d0c29e29a86d487b592df97b98e3e2abedad574e215d", [:mix], [], "hexpm"},
"connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], []}, "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], []},
"cors_plug": {:hex, :cors_plug, "2.0.0", "238ddb479f92b38f6dc1ae44b8d81f0387f9519101a6da442d543ab70ee0e482", [:mix], [{:plug, "~> 1.3 or ~> 1.4 or ~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, "cors_plug": {:hex, :cors_plug, "2.0.0", "238ddb479f92b38f6dc1ae44b8d81f0387f9519101a6da442d543ab70ee0e482", [:mix], [{:plug, "~> 1.3 or ~> 1.4 or ~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
"cowboy": {:hex, :cowboy, "2.6.1", "f2e06f757c337b3b311f9437e6e072b678fcd71545a7b2865bdaa154d078593f", [:rebar3], [{:cowlib, "~> 2.7.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm"}, "cowboy": {:hex, :cowboy, "2.6.1", "f2e06f757c337b3b311f9437e6e072b678fcd71545a7b2865bdaa154d078593f", [:rebar3], [{:cowlib, "~> 2.7.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm"},

Loading…
Cancel
Save