Merge branch 'master' into vb-graphql-xss

pull/3577/head
Victor Baranov 4 years ago committed by GitHub
commit a6a5a4703c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      .dialyzer-ignore
  2. 5
      CHANGELOG.md
  3. 45
      apps/block_scout_web/lib/block_scout_web/controllers/address_transaction_controller.ex
  4. 31
      apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/token_controller.ex
  5. 84
      apps/block_scout_web/lib/block_scout_web/etherscan.ex
  6. 7
      apps/block_scout_web/lib/block_scout_web/templates/address_token/index.html.eex
  7. 9
      apps/block_scout_web/lib/block_scout_web/templates/address_transaction/index.html.eex
  8. 6
      apps/block_scout_web/lib/block_scout_web/templates/smart_contract/_function_response.html.eex
  9. 2
      apps/block_scout_web/lib/block_scout_web/templates/smart_contract/_functions.html.eex
  10. 10
      apps/block_scout_web/lib/block_scout_web/templates/stakes/_stakes_modal_delegators_list.html.eex
  11. 2
      apps/block_scout_web/lib/block_scout_web/templates/stakes/_warning.html.eex
  12. 2
      apps/block_scout_web/lib/block_scout_web/templates/stakes/index.html.eex
  13. 2
      apps/block_scout_web/lib/block_scout_web/views/address_token_view.ex
  14. 1
      apps/block_scout_web/lib/block_scout_web/views/address_transaction_view.ex
  15. 12
      apps/block_scout_web/lib/block_scout_web/views/api/rpc/token_view.ex
  16. 251
      apps/block_scout_web/lib/block_scout_web/views/smart_contract_view.ex
  17. 4
      apps/block_scout_web/lib/block_scout_web/web_router.ex
  18. 68
      apps/block_scout_web/priv/gettext/default.pot
  19. 68
      apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po
  20. 36
      apps/block_scout_web/test/block_scout_web/controllers/address_transaction_controller_test.exs
  21. 48
      apps/block_scout_web/test/block_scout_web/views/tokens/smart_contract_view_test.exs
  22. 4
      apps/explorer/config/config.exs
  23. 2
      apps/explorer/lib/explorer/chain.ex
  24. 119
      apps/explorer/lib/explorer/chain/address_token_transfer_csv_exporter.ex
  25. 139
      apps/explorer/lib/explorer/chain/address_transaction_csv_exporter.ex
  26. 2
      apps/explorer/lib/explorer/chain/events/subscriber.ex
  27. 8
      apps/explorer/lib/explorer/staking/contract_reader.ex
  28. 280
      apps/explorer/lib/explorer/staking/contract_state.ex
  29. 3
      apps/explorer/lib/explorer/staking/stake_snapshotting.ex
  30. 7
      apps/explorer/lib/explorer/validator/metadata_importer.ex
  31. 14
      apps/explorer/priv/contracts_abi/posdao/StakingAuRa.json
  32. 14
      apps/explorer/priv/contracts_abi/posdao/ValidatorSetAuRa.json
  33. 72
      apps/explorer/test/explorer/chain/address_token_transfer_csv_exporter_test.exs
  34. 105
      apps/explorer/test/explorer/chain/address_transaction_csv_exporter_test.exs
  35. 108
      apps/explorer/test/explorer/staking/contract_state_test.exs
  36. 4
      apps/indexer/lib/indexer/block/realtime/fetcher.ex

@ -24,5 +24,5 @@ lib/explorer/exchange_rates/source.ex:104
lib/explorer/exchange_rates/source.ex:107 lib/explorer/exchange_rates/source.ex:107
lib/explorer/smart_contract/verifier.ex:89 lib/explorer/smart_contract/verifier.ex:89
lib/block_scout_web/templates/address_contract/index.html.eex:118 lib/block_scout_web/templates/address_contract/index.html.eex:118
lib/explorer/staking/stake_snapshotting.ex:14: Function do_snapshotting/6 has no local return lib/explorer/staking/stake_snapshotting.ex:15: Function do_snapshotting/6 has no local return
lib/explorer/staking/stake_snapshotting.ex:183 lib/explorer/staking/stake_snapshotting.ex:184

@ -1,12 +1,17 @@
## Current ## Current
### Features ### Features
- [#3584](https://github.com/poanetwork/blockscout/pull/3584) - Token holders API endpoint
- [#3564](https://github.com/poanetwork/blockscout/pull/3564) - Staking welcome message - [#3564](https://github.com/poanetwork/blockscout/pull/3564) - Staking welcome message
### Fixes ### Fixes
- [#3600](https://github.com/poanetwork/blockscout/pull/3600) - Prevent update validator metadata with empty name from contract
- [#3592](https://github.com/poanetwork/blockscout/pull/3592), [#3601](https://github.com/poanetwork/blockscout/pull/3601) - Contract interaction: fix nested tuples in the output view, add formatting
- [#3583](https://github.com/poanetwork/blockscout/pull/3583) - Reduce RPC requests and DB changes by Staking DApp
- [#3577](https://github.com/poanetwork/blockscout/pull/3577) - Eliminate GraphiQL page XSS attack - [#3577](https://github.com/poanetwork/blockscout/pull/3577) - Eliminate GraphiQL page XSS attack
### Chore ### Chore
- [#3585](https://github.com/poanetwork/blockscout/pull/3585) - Add autoswitching from eth_subscribe to eth_blockNumber in Staking DApp
- [#3574](https://github.com/poanetwork/blockscout/pull/3574) - Correct UNI token price - [#3574](https://github.com/poanetwork/blockscout/pull/3574) - Correct UNI token price
- [#3567](https://github.com/poanetwork/blockscout/pull/3567) - Force to show filter at the page where filtered items list is empty - [#3567](https://github.com/poanetwork/blockscout/pull/3567) - Force to show filter at the page where filtered items list is empty
- [#3565](https://github.com/poanetwork/blockscout/pull/3565) - Staking dapp: unhealthy state alert message - [#3565](https://github.com/poanetwork/blockscout/pull/3565) - Staking dapp: unhealthy state alert message

@ -9,7 +9,6 @@ defmodule BlockScoutWeb.AddressTransactionController do
alias BlockScoutWeb.{AccessHelpers, TransactionView} alias BlockScoutWeb.{AccessHelpers, TransactionView}
alias Explorer.{Chain, Market} alias Explorer.{Chain, Market}
alias Explorer.Chain.{AddressTokenTransferCsvExporter, AddressTransactionCsvExporter}
alias Explorer.ExchangeRates.Token alias Explorer.ExchangeRates.Token
alias Indexer.Fetcher.CoinBalanceOnDemand alias Indexer.Fetcher.CoinBalanceOnDemand
alias Phoenix.View alias Phoenix.View
@ -140,48 +139,4 @@ defmodule BlockScoutWeb.AddressTransactionController do
end end
end end
end end
def token_transfers_csv(conn, %{"address_id" => address_hash_string}) when is_binary(address_hash_string) do
with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string),
{:ok, address} <- Chain.hash_to_address(address_hash) do
address
|> AddressTokenTransferCsvExporter.export()
|> Enum.into(
conn
|> put_resp_content_type("application/csv")
|> put_resp_header("content-disposition", "attachment; filename=token_transfers.csv")
|> send_chunked(200)
)
else
:error ->
unprocessable_entity(conn)
{:error, :not_found} ->
not_found(conn)
end
end
def token_transfers_csv(conn, _), do: not_found(conn)
def transactions_csv(conn, %{"address_id" => address_hash_string}) do
with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string),
{:ok, address} <- Chain.hash_to_address(address_hash) do
address
|> AddressTransactionCsvExporter.export()
|> Enum.into(
conn
|> put_resp_content_type("application/csv")
|> put_resp_header("content-disposition", "attachment; filename=transactions.csv")
|> send_chunked(200)
)
else
:error ->
unprocessable_entity(conn)
{:error, :not_found} ->
not_found(conn)
end
end
def transactions_csv(conn, _), do: not_found(conn)
end end

@ -1,7 +1,8 @@
defmodule BlockScoutWeb.API.RPC.TokenController do defmodule BlockScoutWeb.API.RPC.TokenController do
use BlockScoutWeb, :controller use BlockScoutWeb, :controller
alias Explorer.Chain alias BlockScoutWeb.API.RPC.Helpers
alias Explorer.{Chain, PagingOptions}
def gettoken(conn, params) do def gettoken(conn, params) do
with {:contractaddress_param, {:ok, contractaddress_param}} <- fetch_contractaddress(params), with {:contractaddress_param, {:ok, contractaddress_param}} <- fetch_contractaddress(params),
@ -20,6 +21,34 @@ defmodule BlockScoutWeb.API.RPC.TokenController do
end end
end end
def gettokenholders(conn, params) do
with pagination_options <- Helpers.put_pagination_options(%{}, params),
{:contractaddress_param, {:ok, contractaddress_param}} <- fetch_contractaddress(params),
{:format, {:ok, address_hash}} <- to_address_hash(contractaddress_param) do
options_with_defaults =
pagination_options
|> Map.put_new(:page_number, 0)
|> Map.put_new(:page_size, 10)
options = [
paging_options: %PagingOptions{
key: nil,
page_number: options_with_defaults.page_number,
page_size: options_with_defaults.page_size
}
]
token_holders = Chain.fetch_token_holders_from_token_hash(address_hash, options)
render(conn, "gettokenholders.json", %{token_holders: token_holders})
else
{:contractaddress_param, :error} ->
render(conn, :error, error: "Query parameter contract address is required")
{:format, :error} ->
render(conn, :error, error: "Invalid contract address hash")
end
end
defp fetch_contractaddress(params) do defp fetch_contractaddress(params) do
{:contractaddress_param, Map.fetch(params, "contractaddress")} {:contractaddress_param, Map.fetch(params, "contractaddress")}
end end

@ -276,6 +276,23 @@ defmodule BlockScoutWeb.Etherscan do
"result" => nil "result" => nil
} }
@token_gettokenholders_example_value %{
"status" => "1",
"message" => "OK",
"result" => [
%{
"address" => "0x0000000000000000000000000000000000000000",
"value" => "965208500001258757122850"
}
]
}
@token_gettokenholders_example_value_error %{
"status" => "0",
"message" => "Invalid contract address format",
"result" => nil
}
@stats_tokensupply_example_value %{ @stats_tokensupply_example_value %{
"status" => "1", "status" => "1",
"message" => "OK", "message" => "OK",
@ -664,6 +681,18 @@ defmodule BlockScoutWeb.Etherscan do
} }
} }
@token_holder_details %{
name: "Token holder Detail",
fields: %{
address: @address_hash_type,
value: %{
type: "value",
definition: "A nonnegative number used to identify the balance of the target token.",
example: ~s("1000000000000000000")
}
}
}
@address_balance %{ @address_balance %{
name: "AddressBalance", name: "AddressBalance",
fields: %{ fields: %{
@ -1825,6 +1854,56 @@ defmodule BlockScoutWeb.Etherscan do
] ]
} }
@token_gettokenholders_action %{
name: "getTokenHolders",
description: "Get token holders by contract address.",
required_params: [
%{
key: "contractaddress",
placeholder: "contractAddressHash",
type: "string",
description: "A 160-bit code used for identifying contracts."
}
],
optional_params: [
%{
key: "page",
type: "integer",
description:
"A nonnegative integer that represents the page number to be used for pagination. 'offset' must be provided in conjunction."
},
%{
key: "offset",
type: "integer",
description:
"A nonnegative integer that represents the maximum number of records to return when paginating. 'page' must be provided in conjunction."
}
],
responses: [
%{
code: "200",
description: "successful operation",
example_value: Jason.encode!(@token_gettokenholders_example_value),
model: %{
name: "Result",
fields: %{
status: @status_type,
message: @message_type,
result: %{
type: "array",
array_type: @token_holder_details
}
}
}
},
%{
code: "200",
description: "error",
example_value: Jason.encode!(@token_gettokenholders_example_value_error)
}
]
}
@stats_tokensupply_action %{ @stats_tokensupply_action %{
name: "tokensupply", name: "tokensupply",
description: description:
@ -2446,7 +2525,10 @@ defmodule BlockScoutWeb.Etherscan do
@token_module %{ @token_module %{
name: "token", name: "token",
actions: [@token_gettoken_action] actions: [
@token_gettoken_action,
@token_gettokenholders_action
]
} }
@stats_module %{ @stats_module %{

@ -26,13 +26,6 @@
</div> </div>
<div class="transaction-bottom-panel"> <div class="transaction-bottom-panel">
<div csv-download class="download-all-transactions">
Download <a class="download-all-transactions-link" href=<%= address_transaction_path(@conn, :token_transfers_csv, %{"address_id" => Address.checksum(@address.hash)}) %>><%= gettext("CSV") %></span>
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="16">
<path fill="#333333" fill-rule="evenodd" d="M13 16H1c-.999 0-1-1-1-1V1s-.004-1 1-1h6l7 7v8s-.032 1-1 1zm-1-8c0-.99-1-1-1-1H8s-1 .001-1-1V3c0-.999-1-1-1-1H2v12h10V8z"/>
</svg>
</a>
</div>
<%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "bottom", cur_page_number: "1", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %> <%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "bottom", cur_page_number: "1", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %>
</div> </div>

@ -59,15 +59,6 @@
</div> </div>
<div class="transaction-bottom-panel"> <div class="transaction-bottom-panel">
<div class="download-all-transactions">
Download <a class="download-all-transactions-link" href=<%= address_transaction_path(@conn, :transactions_csv, %{"address_id" => Address.checksum(@address.hash)}) %>><%= gettext("CSV") %></span>
<div class="csv-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="16">
<path fill="#333333" fill-rule="evenodd" d="M13 16H1c-.999 0-1-1-1-1V1s-.004-1 1-1h6l7 7v8s-.032 1-1 1zm-1-8c0-.99-1-1-1-1H8s-1 .001-1-1V3c0-.999-1-1-1-1H2v12h10V8z"/>
</svg>
</div>
</a>
</div>
<%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "bottom", cur_page_number: "1", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %> <%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "bottom", cur_page_number: "1", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %>
</div> </div>

@ -3,8 +3,8 @@
[ <strong><%= @function_name %></strong> method Response ] [ <strong><%= @function_name %></strong> method Response ]
[<%= for item <- @outputs do %> [<%= for item <- @outputs do %>
<span class="function-response-item"><%= if named_argument?(item) do %><%= item["name"] %> <%= if named_argument?(item) do %><span class="function-response-item"><%= item["name"] %></span><% end %>
<% end %> <span class="text-muted"><%= raw(values_with_type(item["value"], item["type"])) %></span>
<span class="text-muted">(<%= item["type"] %>)</span> : <%= values(item["value"], item["type"]) %></span><% end %>] <% end %>]
</pre> </pre>
</div> </div>

@ -121,7 +121,7 @@ to: address_contract_path(@conn, :index, metadata_for_verification.address_hash)
</span> </span>
</div> </div>
<% else %> <% else %>
<%= values(output["value"], output["type"]) %> <%= raw(values_only(output["value"], output["type"], output["components"])) %>
<% end %> <% end %>
<% end %> <% end %>
<% end %> <% end %>

@ -2,7 +2,7 @@
<div class="modal-dialog modal-dialog-centered modal-delegators-info" role="document"> <div class="modal-dialog modal-dialog-centered modal-delegators-info" role="document">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title"><%= gettext("Delegators") %></h5> <h5 class="modal-title"><%= gettext("Delegators of ") %><%= @pool.staking_address_hash %></h5>
</div> </div>
<%= render BlockScoutWeb.CommonComponentsView, "_modal_close_button.html" %> <%= render BlockScoutWeb.CommonComponentsView, "_modal_close_button.html" %>
<div class="modal-body"> <div class="modal-body">
@ -49,15 +49,15 @@
<%= <%=
title = title =
if @show_snapshotted_data do if @show_snapshotted_data do
gettext("Potential Reward Percent") <> "<br />(" <> gettext("Current Reward Percent") <> ")" gettext("Potential Reward Share") <> "<br />(" <> gettext("Current Reward Share") <> ")"
else else
gettext("Potential Reward Percent") gettext("Potential Reward Share")
end end
tooltip = tooltip =
gettext("Reward distribution is based on stake amount. Validator receives a minimum of %{min}%.", min: @validator_min_reward_percent) <> gettext("Reward distribution is based on stake amount. Validator receives at least %{min}% of the pool reward.", min: @validator_min_reward_percent) <>
if @show_snapshotted_data do if @show_snapshotted_data do
" " <> gettext("Current Reward Percent is calculated based on the Working Stake Amount.") " " <> gettext("Current Reward Share is calculated based on the Working Stake Amount.")
else else
"" ""
end end

@ -8,7 +8,7 @@
</button> </button>
<span style="font-size: 1.3em;"> <span style="font-size: 1.3em;">
<i style="color: #f7b32b;" class="fa fa-info-circle ml-1"></i> <i style="color: #f7b32b;" class="fa fa-info-circle ml-1"></i>
Due to high loading, Staking Dapp may show data with delay. Because of that after making transactions the result in UI will be visible not right away. We're working on optimization. Due to high load volumes, current staking data display may lag behind actual transactions. Transactions are being processed correctly on-chain. We are currently working to address this UI display issue.
</span> </span>
</div> </div>
</div> </div>

@ -2,7 +2,7 @@
<%= raw(@top) %> <%= raw(@top) %>
</div> </div>
<%= render BlockScoutWeb.StakesView, "_learn-more.html", conn: @conn %> <%= render BlockScoutWeb.StakesView, "_learn-more.html", conn: @conn %>
<%= render BlockScoutWeb.StakesView, "_warning.html", conn: @conn %> <%= # render BlockScoutWeb.StakesView, "_warning.html", conn: @conn %>
<section data-page="stakes" class="container" data-refresh-interval="<%= @refresh_interval %>"> <section data-page="stakes" class="container" data-refresh-interval="<%= @refresh_interval %>">
<div class="card" data-async-load data-async-listing="<%= @current_path %>" data-no-first-loading> <div class="card" data-async-load data-async-listing="<%= @current_path %>" data-no-first-loading>
<%= render BlockScoutWeb.StakesView, "_stakes_tabs.html", conn: @conn %> <%= render BlockScoutWeb.StakesView, "_stakes_tabs.html", conn: @conn %>

@ -1,5 +1,3 @@
defmodule BlockScoutWeb.AddressTokenView do defmodule BlockScoutWeb.AddressTokenView do
use BlockScoutWeb, :view use BlockScoutWeb, :view
alias Explorer.Chain.Address
end end

@ -2,7 +2,6 @@ defmodule BlockScoutWeb.AddressTransactionView do
use BlockScoutWeb, :view use BlockScoutWeb, :view
alias BlockScoutWeb.AccessHelpers alias BlockScoutWeb.AccessHelpers
alias Explorer.Chain.Address
def format_current_filter(filter) do def format_current_filter(filter) do
case filter do case filter do

@ -7,6 +7,11 @@ defmodule BlockScoutWeb.API.RPC.TokenView do
RPCView.render("show.json", data: prepare_token(token)) RPCView.render("show.json", data: prepare_token(token))
end end
def render("gettokenholders.json", %{token_holders: token_holders}) do
data = Enum.map(token_holders, &prepare_token_holder/1)
RPCView.render("show.json", data: data)
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
@ -22,4 +27,11 @@ defmodule BlockScoutWeb.API.RPC.TokenView do
"cataloged" => token.cataloged "cataloged" => token.cataloged
} }
end end
defp prepare_token_holder(token_holder) do
%{
"address" => to_string(token_holder.address_hash),
"value" => token_holder.value
}
end
end end

@ -2,6 +2,7 @@ defmodule BlockScoutWeb.SmartContractView do
use BlockScoutWeb, :view use BlockScoutWeb, :view
alias Explorer.Chain alias Explorer.Chain
alias Explorer.Chain.Hash.Address
def queryable?(inputs) when not is_nil(inputs), do: Enum.any?(inputs) def queryable?(inputs) when not is_nil(inputs), do: Enum.any?(inputs)
@ -41,46 +42,256 @@ defmodule BlockScoutWeb.SmartContractView do
def named_argument?(%{"name" => _}), do: true def named_argument?(%{"name" => _}), do: true
def named_argument?(_), do: false def named_argument?(_), do: false
def values(addresses, type) when is_list(addresses) and type == "address[]" do def values_with_type(value, type, components \\ nil)
addresses
|> Enum.map(&values(&1, "address")) def values_with_type(value, type, components) when is_list(value) do
cond do
String.starts_with?(type, "tuple") ->
tuple_types =
type
|> String.slice(0..-3)
|> supplement_type_with_components(components)
values =
value
|> tuple_array_to_array(tuple_types)
|> Enum.join(", ")
render_array_type_value(type, values)
String.starts_with?(type, "address") ->
values =
value
|> Enum.map(&binary_to_utf_string(&1))
|> Enum.join(", ") |> Enum.join(", ")
render_array_type_value(type, values)
String.starts_with?(type, "bytes") ->
values =
value
|> Enum.map(&binary_to_utf_string(&1))
|> Enum.join(", ")
render_array_type_value(type, values)
true ->
values =
value
|> Enum.join(", ")
render_array_type_value(type, values)
end end
end
def values_with_type(value, type, _components) when is_tuple(value) do
values =
value
|> tuple_to_array(type)
|> Enum.join(", ")
render_type_value(type, values)
end
def values_with_type(value, type, _components) when type in [:address, "address", "address payable"] do
{:ok, address} = Address.cast(value)
render_type_value("address", to_string(address))
end
def values_with_type(value, "string", _components), do: render_type_value("string", value)
def values(values, type) when is_list(values) and type == "tuple[]" do def values_with_type(value, "bool", _components), do: render_type_value("bool", to_string(value))
array_from_tuple = tuple_array_to_array(values)
array_from_tuple def values_with_type(value, type, _components) do
if String.starts_with?(type, "uint") do
render_type_value(type, to_string(value))
else
render_type_value(type, binary_to_utf_string(value))
end
end
def values_only(value, type, components) when is_list(value) do
cond do
String.starts_with?(type, "tuple") ->
tuple_types =
type
|> String.slice(0..-3)
|> supplement_type_with_components(components)
values =
value
|> tuple_array_to_array(tuple_types)
|> Enum.join(", ")
render_array_value(values)
String.starts_with?(type, "address") ->
values =
value
|> Enum.map(&binary_to_utf_string(&1))
|> Enum.join(", ")
render_array_value(values)
String.starts_with?(type, "bytes") ->
values =
value
|> Enum.map(&binary_to_utf_string(&1))
|> Enum.join(", ")
render_array_value(values)
true ->
values =
value
|> Enum.join(", ") |> Enum.join(", ")
render_array_value(values)
end
end end
def values(value, _type) when is_tuple(value) do def values_only(value, type, _components) when is_tuple(value) do
tuple_to_array(value) values =
value
|> tuple_to_array(type)
|> Enum.join(", ")
values
end end
def values(value, type) when type in ["address", "address payable"] do def values_only(value, type, _components) when type in [:address, "address", "address payable"] do
{:ok, address} = Explorer.Chain.Hash.Address.cast(value) {:ok, address} = Address.cast(value)
to_string(address) to_string(address)
end end
def values(values, _) when is_list(values), do: Enum.join(values, ",") def values_only(value, "string", _components), do: value
def values(value, _), do: value
defp tuple_array_to_array(values) do def values_only(value, "bool", _components), do: to_string(value)
values
|> Enum.map(fn value -> def values_only(value, type, _components) do
tuple_to_array(value) if String.starts_with?(type, "uint") do
to_string(value)
else
binary_to_utf_string(value)
end
end
defp tuple_array_to_array(value, type) do
value
|> Enum.map(fn item ->
tuple_to_array(item, type)
end) end)
end end
defp tuple_to_array(value) do defp tuple_to_array(value, type) do
types_string =
type
|> String.slice(6..-2)
types =
if String.trim(types_string) == "" do
[]
else
types_string
|> String.split(",")
end
{tuple_types, _} =
types
|> Enum.reduce({[], nil}, fn val, acc ->
{arr, to_merge} = acc
if to_merge do
if count_string_symbols(val)["]"] > count_string_symbols(val)["["] do
updated_arr = update_last_list_item(arr, val)
{updated_arr, !to_merge}
else
updated_arr = update_last_list_item(arr, val)
{updated_arr, to_merge}
end
else
if count_string_symbols(val)["["] > count_string_symbols(val)["]"] do
# credo:disable-for-next-line
{arr ++ [val], !to_merge}
else
# credo:disable-for-next-line
{arr ++ [val], to_merge}
end
end
end)
values_list =
value value
|> Tuple.to_list() |> Tuple.to_list()
|> Enum.map(&binary_to_utf_string(&1))
|> Enum.join(",") values_types_list = Enum.zip(tuple_types, values_list)
values_types_list
|> Enum.map(fn {type, value} ->
values_with_type(value, type)
end)
end
defp update_last_list_item(arr, new_val) do
arr
|> Enum.with_index()
|> Enum.map(fn {item, index} ->
if index == Enum.count(arr) - 1 do
item <> "," <> new_val
else
item
end
end)
end
defp count_string_symbols(str) do
str
|> String.graphemes()
|> Enum.reduce(%{"[" => 0, "]" => 0}, fn char, acc ->
Map.update(acc, char, 1, &(&1 + 1))
end)
end end
defp binary_to_utf_string(item) do defp binary_to_utf_string(item) do
if is_binary(item), do: "0x" <> Base.encode16(item, case: :lower), else: item if is_binary(item) do
if String.starts_with?(item, "0x") do
item
else
"0x" <> Base.encode16(item, case: :lower)
end
else
item
end
end
defp render_type_value(type, value) do
"<div style=\"padding-left: 20px\">(#{type}) : #{value}</div>"
end
defp render_array_type_value(type, values) do
value_to_display = "[" <> values <> "]"
render_type_value(type, value_to_display)
end
defp render_array_value(values) do
value_to_display = "[" <> values <> "]"
value_to_display
end
defp supplement_type_with_components(type, components) do
if type == "tuple" && components do
types =
components
|> Enum.map(fn component ->
Map.get(component, "type")
end)
|> Enum.join(",")
"tuple[" <> types <> "]"
else
type
end
end end
end end

@ -256,12 +256,8 @@ defmodule BlockScoutWeb.WebRouter do
get("/search-logs", AddressLogsController, :search_logs) get("/search-logs", AddressLogsController, :search_logs)
get("/transactions_csv", AddressTransactionController, :transactions_csv)
get("/token-autocomplete", ChainController, :token_autocomplete) get("/token-autocomplete", ChainController, :token_autocomplete)
get("/token-transfers-csv", AddressTransactionController, :token_transfers_csv)
get("/chain-blocks", ChainController, :chain_blocks, as: :chain_blocks) get("/chain-blocks", ChainController, :chain_blocks, as: :chain_blocks)
get("/token-counters", Tokens.TokenController, :token_counters) get("/token-counters", Tokens.TokenController, :token_counters)

@ -141,7 +141,7 @@ msgstr ""
#: lib/block_scout_web/templates/layout/_topnav.html.eex:91 #: lib/block_scout_web/templates/layout/_topnav.html.eex:91
#: lib/block_scout_web/views/address_internal_transaction_view.ex:10 #: lib/block_scout_web/views/address_internal_transaction_view.ex:10
#: lib/block_scout_web/views/address_token_transfer_view.ex:10 #: lib/block_scout_web/views/address_token_transfer_view.ex:10
#: lib/block_scout_web/views/address_transaction_view.ex:11 #: lib/block_scout_web/views/address_transaction_view.ex:10
msgid "All" msgid "All"
msgstr "" msgstr ""
@ -252,12 +252,6 @@ msgstr ""
msgid "Blockscout is a tool for inspecting and analyzing EVM based blockchains. Blockchain explorer for Ethereum Networks." msgid "Blockscout is a tool for inspecting and analyzing EVM based blockchains. Blockchain explorer for Ethereum Networks."
msgstr "" msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_token/index.html.eex:30
#: lib/block_scout_web/templates/address_transaction/index.html.eex:63
msgid "CSV"
msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/views/internal_transaction_view.ex:21 #: lib/block_scout_web/views/internal_transaction_view.ex:21
msgid "Call" msgid "Call"
@ -706,7 +700,7 @@ msgstr ""
#: lib/block_scout_web/templates/address_transaction/index.html.eex:36 #: lib/block_scout_web/templates/address_transaction/index.html.eex:36
#: lib/block_scout_web/views/address_internal_transaction_view.ex:9 #: lib/block_scout_web/views/address_internal_transaction_view.ex:9
#: lib/block_scout_web/views/address_token_transfer_view.ex:9 #: lib/block_scout_web/views/address_token_transfer_view.ex:9
#: lib/block_scout_web/views/address_transaction_view.ex:10 #: lib/block_scout_web/views/address_transaction_view.ex:9
msgid "From" msgid "From"
msgstr "" msgstr ""
@ -856,7 +850,7 @@ msgstr ""
#: lib/block_scout_web/templates/address_transaction/index.html.eex:30 #: lib/block_scout_web/templates/address_transaction/index.html.eex:30
#: lib/block_scout_web/views/address_internal_transaction_view.ex:8 #: lib/block_scout_web/views/address_internal_transaction_view.ex:8
#: lib/block_scout_web/views/address_token_transfer_view.ex:8 #: lib/block_scout_web/views/address_token_transfer_view.ex:8
#: lib/block_scout_web/views/address_transaction_view.ex:9 #: lib/block_scout_web/views/address_transaction_view.ex:8
msgid "To" msgid "To"
msgstr "" msgstr ""
@ -2132,18 +2126,6 @@ msgstr ""
msgid "Claim the Amount" msgid "Claim the Amount"
msgstr "" msgstr ""
#, elixir-format
#:
#: lib/block_scout_web/templates/stakes/_stakes_modal_delegators_list.html.eex:52
msgid "Current Reward Percent"
msgstr ""
#, elixir-format
#:
#: lib/block_scout_web/templates/stakes/_stakes_modal_delegators_list.html.eex:60
msgid "Current Reward Percent is calculated based on the Working Stake Amount."
msgstr ""
#, elixir-format #, elixir-format
#: #:
#: lib/block_scout_web/templates/stakes/_stakes_modal_delegators_list.html.eex:32 #: lib/block_scout_web/templates/stakes/_stakes_modal_delegators_list.html.eex:32
@ -2162,7 +2144,6 @@ msgid "DApp for Staking %{symbol} tokens"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/stakes/_stakes_modal_delegators_list.html.eex:5
#: lib/block_scout_web/templates/stakes/_stakes_progress.html.eex:31 #: lib/block_scout_web/templates/stakes/_stakes_progress.html.eex:31
#: lib/block_scout_web/templates/stakes/_table.html.eex:38 #: lib/block_scout_web/templates/stakes/_table.html.eex:38
msgid "Delegators" msgid "Delegators"
@ -2290,13 +2271,6 @@ msgstr ""
msgid "Pools searching is already in progress for this address" msgid "Pools searching is already in progress for this address"
msgstr "" msgstr ""
#, elixir-format
#:
#: lib/block_scout_web/templates/stakes/_stakes_modal_delegators_list.html.eex:52
#: lib/block_scout_web/templates/stakes/_stakes_modal_delegators_list.html.eex:54
msgid "Potential Reward Percent"
msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/stakes/_stakes_modal_pool_info.html.eex:16 #: lib/block_scout_web/templates/stakes/_stakes_modal_pool_info.html.eex:16
msgid "Reason for Ban: %{ban_reason}" msgid "Reason for Ban: %{ban_reason}"
@ -2323,12 +2297,6 @@ msgstr ""
msgid "Reward calculating is already in progress for this address" msgid "Reward calculating is already in progress for this address"
msgstr "" msgstr ""
#, elixir-format
#:
#: lib/block_scout_web/templates/stakes/_stakes_modal_delegators_list.html.eex:58
msgid "Reward distribution is based on stake amount. Validator receives a minimum of %{min}%."
msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/stakes/_stakes_modal_claim_reward.html.eex:9 #: lib/block_scout_web/templates/stakes/_stakes_modal_claim_reward.html.eex:9
msgid "Searching for pools you have ever staked into. Please, wait..." msgid "Searching for pools you have ever staked into. Please, wait..."
@ -2664,3 +2632,33 @@ msgstr ""
#: lib/block_scout_web/templates/layout/_topnav.html.eex:247 #: lib/block_scout_web/templates/layout/_topnav.html.eex:247
msgid "Press / and focus will be moved to the search field" msgid "Press / and focus will be moved to the search field"
msgstr "" msgstr ""
#, elixir-format
#:
#: lib/block_scout_web/templates/stakes/_stakes_modal_delegators_list.html.eex:52
msgid "Current Reward Share"
msgstr ""
#, elixir-format
#:
#: lib/block_scout_web/templates/stakes/_stakes_modal_delegators_list.html.eex:60
msgid "Current Reward Share is calculated based on the Working Stake Amount."
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/stakes/_stakes_modal_delegators_list.html.eex:5
msgid "Delegators of "
msgstr ""
#, elixir-format
#:
#: lib/block_scout_web/templates/stakes/_stakes_modal_delegators_list.html.eex:52
#: lib/block_scout_web/templates/stakes/_stakes_modal_delegators_list.html.eex:54
msgid "Potential Reward Share"
msgstr ""
#, elixir-format
#:
#: lib/block_scout_web/templates/stakes/_stakes_modal_delegators_list.html.eex:58
msgid "Reward distribution is based on stake amount. Validator receives at least %{min}% of the pool reward."
msgstr ""

@ -141,7 +141,7 @@ msgstr ""
#: lib/block_scout_web/templates/layout/_topnav.html.eex:91 #: lib/block_scout_web/templates/layout/_topnav.html.eex:91
#: lib/block_scout_web/views/address_internal_transaction_view.ex:10 #: lib/block_scout_web/views/address_internal_transaction_view.ex:10
#: lib/block_scout_web/views/address_token_transfer_view.ex:10 #: lib/block_scout_web/views/address_token_transfer_view.ex:10
#: lib/block_scout_web/views/address_transaction_view.ex:11 #: lib/block_scout_web/views/address_transaction_view.ex:10
msgid "All" msgid "All"
msgstr "" msgstr ""
@ -252,12 +252,6 @@ msgstr ""
msgid "Blockscout is a tool for inspecting and analyzing EVM based blockchains. Blockchain explorer for Ethereum Networks." msgid "Blockscout is a tool for inspecting and analyzing EVM based blockchains. Blockchain explorer for Ethereum Networks."
msgstr "" msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_token/index.html.eex:30
#: lib/block_scout_web/templates/address_transaction/index.html.eex:63
msgid "CSV"
msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/views/internal_transaction_view.ex:21 #: lib/block_scout_web/views/internal_transaction_view.ex:21
msgid "Call" msgid "Call"
@ -706,7 +700,7 @@ msgstr ""
#: lib/block_scout_web/templates/address_transaction/index.html.eex:36 #: lib/block_scout_web/templates/address_transaction/index.html.eex:36
#: lib/block_scout_web/views/address_internal_transaction_view.ex:9 #: lib/block_scout_web/views/address_internal_transaction_view.ex:9
#: lib/block_scout_web/views/address_token_transfer_view.ex:9 #: lib/block_scout_web/views/address_token_transfer_view.ex:9
#: lib/block_scout_web/views/address_transaction_view.ex:10 #: lib/block_scout_web/views/address_transaction_view.ex:9
msgid "From" msgid "From"
msgstr "" msgstr ""
@ -856,7 +850,7 @@ msgstr ""
#: lib/block_scout_web/templates/address_transaction/index.html.eex:30 #: lib/block_scout_web/templates/address_transaction/index.html.eex:30
#: lib/block_scout_web/views/address_internal_transaction_view.ex:8 #: lib/block_scout_web/views/address_internal_transaction_view.ex:8
#: lib/block_scout_web/views/address_token_transfer_view.ex:8 #: lib/block_scout_web/views/address_token_transfer_view.ex:8
#: lib/block_scout_web/views/address_transaction_view.ex:9 #: lib/block_scout_web/views/address_transaction_view.ex:8
msgid "To" msgid "To"
msgstr "" msgstr ""
@ -2132,18 +2126,6 @@ msgstr ""
msgid "Claim the Amount" msgid "Claim the Amount"
msgstr "" msgstr ""
#, elixir-format
#:
#: lib/block_scout_web/templates/stakes/_stakes_modal_delegators_list.html.eex:52
msgid "Current Reward Percent"
msgstr ""
#, elixir-format
#:
#: lib/block_scout_web/templates/stakes/_stakes_modal_delegators_list.html.eex:60
msgid "Current Reward Percent is calculated based on the Working Stake Amount."
msgstr ""
#, elixir-format #, elixir-format
#: #:
#: lib/block_scout_web/templates/stakes/_stakes_modal_delegators_list.html.eex:32 #: lib/block_scout_web/templates/stakes/_stakes_modal_delegators_list.html.eex:32
@ -2162,7 +2144,6 @@ msgid "DApp for Staking %{symbol} tokens"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/stakes/_stakes_modal_delegators_list.html.eex:5
#: lib/block_scout_web/templates/stakes/_stakes_progress.html.eex:31 #: lib/block_scout_web/templates/stakes/_stakes_progress.html.eex:31
#: lib/block_scout_web/templates/stakes/_table.html.eex:38 #: lib/block_scout_web/templates/stakes/_table.html.eex:38
msgid "Delegators" msgid "Delegators"
@ -2290,13 +2271,6 @@ msgstr ""
msgid "Pools searching is already in progress for this address" msgid "Pools searching is already in progress for this address"
msgstr "" msgstr ""
#, elixir-format
#:
#: lib/block_scout_web/templates/stakes/_stakes_modal_delegators_list.html.eex:52
#: lib/block_scout_web/templates/stakes/_stakes_modal_delegators_list.html.eex:54
msgid "Potential Reward Percent"
msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/stakes/_stakes_modal_pool_info.html.eex:16 #: lib/block_scout_web/templates/stakes/_stakes_modal_pool_info.html.eex:16
msgid "Reason for Ban: %{ban_reason}" msgid "Reason for Ban: %{ban_reason}"
@ -2323,12 +2297,6 @@ msgstr ""
msgid "Reward calculating is already in progress for this address" msgid "Reward calculating is already in progress for this address"
msgstr "" msgstr ""
#, elixir-format
#:
#: lib/block_scout_web/templates/stakes/_stakes_modal_delegators_list.html.eex:58
msgid "Reward distribution is based on stake amount. Validator receives a minimum of %{min}%."
msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/stakes/_stakes_modal_claim_reward.html.eex:9 #: lib/block_scout_web/templates/stakes/_stakes_modal_claim_reward.html.eex:9
msgid "Searching for pools you have ever staked into. Please, wait..." msgid "Searching for pools you have ever staked into. Please, wait..."
@ -2664,3 +2632,33 @@ msgstr ""
#: lib/block_scout_web/templates/layout/_topnav.html.eex:247 #: lib/block_scout_web/templates/layout/_topnav.html.eex:247
msgid "Press / and focus will be moved to the search field" msgid "Press / and focus will be moved to the search field"
msgstr "" msgstr ""
#, elixir-format, fuzzy
#:
#: lib/block_scout_web/templates/stakes/_stakes_modal_delegators_list.html.eex:52
msgid "Current Reward Share"
msgstr ""
#, elixir-format, fuzzy
#:
#: lib/block_scout_web/templates/stakes/_stakes_modal_delegators_list.html.eex:60
msgid "Current Reward Share is calculated based on the Working Stake Amount."
msgstr ""
#, elixir-format, fuzzy
#: lib/block_scout_web/templates/stakes/_stakes_modal_delegators_list.html.eex:5
msgid "Delegators of "
msgstr ""
#, elixir-format, fuzzy
#:
#: lib/block_scout_web/templates/stakes/_stakes_modal_delegators_list.html.eex:52
#: lib/block_scout_web/templates/stakes/_stakes_modal_delegators_list.html.eex:54
msgid "Potential Reward Share"
msgstr ""
#, elixir-format, fuzzy
#:
#: lib/block_scout_web/templates/stakes/_stakes_modal_delegators_list.html.eex:58
msgid "Reward distribution is based on stake amount. Validator receives at least %{min}% of the pool reward."
msgstr ""

@ -157,40 +157,4 @@ defmodule BlockScoutWeb.AddressTransactionControllerTest do
end) end)
end end
end end
describe "GET token-transfers-csv/2" do
test "exports token transfers to csv", %{conn: conn} do
address = insert(:address)
transaction =
:transaction
|> insert(from_address: address)
|> with_block()
insert(:token_transfer, transaction: transaction, from_address: address)
insert(:token_transfer, transaction: transaction, to_address: address)
conn = get(conn, "/token-transfers-csv", %{"address_id" => Address.checksum(address.hash)})
assert conn.resp_body |> String.split("\n") |> Enum.count() == 4
end
end
describe "GET transactions_csv/2" do
test "download csv file with transactions", %{conn: conn} do
address = insert(:address)
:transaction
|> insert(from_address: address)
|> with_block()
:transaction
|> insert(from_address: address)
|> with_block()
conn = get(conn, "/transactions_csv", %{"address_id" => Address.checksum(address.hash)})
assert conn.resp_body |> String.split("\n") |> Enum.count() == 4
end
end
end end

@ -240,21 +240,28 @@ defmodule BlockScoutWeb.SmartContractViewTest do
end end
end end
describe "values/2" do describe "values_only/2" do
test "joins the values when it is a list of a given type" do test "joins the values when it is a list of a given type" do
values = [8, 6, 9, 2, 2, 37] values = [8, 6, 9, 2, 2, 37]
assert SmartContractView.values(values, "type") == "8,6,9,2,2,37" assert SmartContractView.values_only(values, "type", nil) == "[8, 6, 9, 2, 2, 37]"
end end
test "convert the value to string receiving a value and the 'address' type" do test "convert the value to string receiving a value and the 'address' type" do
value = <<95, 38, 9, 115, 52, 182, 163, 43, 121, 81, 223, 97, 253, 12, 88, 3, 236, 93, 131, 84>> value = <<95, 38, 9, 115, 52, 182, 163, 43, 121, 81, 223, 97, 253, 12, 88, 3, 236, 93, 131, 84>>
assert SmartContractView.values(value, "address") == "0x5f26097334b6a32b7951df61fd0c5803ec5d8354" assert SmartContractView.values_only(value, "address", nil) == "0x5f26097334b6a32b7951df61fd0c5803ec5d8354"
end
test "convert the value to string receiving a value and the :address type" do
value = <<95, 38, 9, 115, 52, 182, 163, 43, 121, 81, 223, 97, 253, 12, 88, 3, 236, 93, 131, 84>>
assert SmartContractView.values_only(value, :address, nil) == "0x5f26097334b6a32b7951df61fd0c5803ec5d8354"
end end
test "convert the value to string receiving a value and the 'address payable' type" do test "convert the value to string receiving a value and the 'address payable' type" do
value = <<95, 38, 9, 115, 52, 182, 163, 43, 121, 81, 223, 97, 253, 12, 88, 3, 236, 93, 131, 84>> value = <<95, 38, 9, 115, 52, 182, 163, 43, 121, 81, 223, 97, 253, 12, 88, 3, 236, 93, 131, 84>>
assert SmartContractView.values(value, "address payable") == "0x5f26097334b6a32b7951df61fd0c5803ec5d8354"
assert SmartContractView.values_only(value, "address payable", nil) ==
"0x5f26097334b6a32b7951df61fd0c5803ec5d8354"
end end
test "convert each value to string and join them when receiving 'address[]' as the type" do test "convert each value to string and join them when receiving 'address[]' as the type" do
@ -263,14 +270,41 @@ defmodule BlockScoutWeb.SmartContractViewTest do
<<207, 38, 14, 163, 23, 85, 86, 55, 197, 95, 112, 229, 93, 186, 141, 90, 216, 65, 76, 176>> <<207, 38, 14, 163, 23, 85, 86, 55, 197, 95, 112, 229, 93, 186, 141, 90, 216, 65, 76, 176>>
] ]
assert SmartContractView.values(value, "address[]") == assert SmartContractView.values_only(value, "address[]", nil) ==
"0x5f26097334b6a32b7951df61fd0c5803ec5d8354, 0xcf260ea317555637c55f70e55dba8d5ad8414cb0" "[0x5f26097334b6a32b7951df61fd0c5803ec5d8354, 0xcf260ea317555637c55f70e55dba8d5ad8414cb0]"
end end
test "returns the value when the type is neither 'address' nor 'address payable'" do test "returns the value when the type is neither 'address' nor 'address payable'" do
value = "POA" value = "POA"
assert SmartContractView.values(value, "not address") == "POA" assert SmartContractView.values_only(value, "string", nil) == "POA"
end
test "returns the value when the type is boolean" do
value = "true"
assert SmartContractView.values_only(value, "bool", nil) == "true"
end
test "returns the value when the type is bytes4" do
value = <<228, 184, 12, 77>>
assert SmartContractView.values_only(value, "bytes4", nil) == "0xe4b80c4d"
end
test "returns the value when the type is bytes32" do
value =
<<156, 209, 70, 119, 249, 170, 85, 105, 179, 187, 179, 81, 252, 214, 125, 17, 21, 170, 86, 58, 225, 98, 66, 118,
211, 212, 230, 127, 179, 214, 249, 38>>
assert SmartContractView.values_only(value, "bytes32", nil) ==
"0x9cd14677f9aa5569b3bbb351fcd67d1115aa563ae1624276d3d4e67fb3d6f926"
end
test "returns the value when the type is uint(n) and value is 0" do
value = "0"
assert SmartContractView.values_only(value, "uint64", nil) == "0"
end end
end end
end end

@ -224,7 +224,9 @@ config :explorer, Explorer.Chain.Block.Reward,
if System.get_env("POS_STAKING_CONTRACT") do if System.get_env("POS_STAKING_CONTRACT") do
config :explorer, Explorer.Staking.ContractState, config :explorer, Explorer.Staking.ContractState,
enabled: true, enabled: true,
staking_contract_address: System.get_env("POS_STAKING_CONTRACT") staking_contract_address: System.get_env("POS_STAKING_CONTRACT"),
eth_subscribe_max_delay: System.get_env("POS_ETH_SUBSCRIBE_MAX_DELAY", "60"),
eth_blocknumber_pull_interval: System.get_env("POS_ETH_BLOCKNUMBER_PULL_INTERVAL", "500")
else else
config :explorer, Explorer.Staking.ContractState, enabled: false config :explorer, Explorer.Staking.ContractState, enabled: false
end end

@ -4601,7 +4601,7 @@ defmodule Explorer.Chain do
end end
@spec fetch_token_holders_from_token_hash(Hash.Address.t(), [paging_options]) :: [TokenBalance.t()] @spec fetch_token_holders_from_token_hash(Hash.Address.t(), [paging_options]) :: [TokenBalance.t()]
def fetch_token_holders_from_token_hash(contract_address_hash, options) do def fetch_token_holders_from_token_hash(contract_address_hash, options \\ []) do
contract_address_hash contract_address_hash
|> CurrentTokenBalance.token_holders_ordered_by_value(options) |> CurrentTokenBalance.token_holders_ordered_by_value(options)
|> Repo.all() |> Repo.all()

@ -1,119 +0,0 @@
defmodule Explorer.Chain.AddressTokenTransferCsvExporter do
@moduledoc """
Exports token transfers to a csv file.
"""
alias Explorer.{Chain, PagingOptions}
alias Explorer.Chain.{TokenTransfer, Transaction}
alias NimbleCSV.RFC4180
@necessity_by_association [
necessity_by_association: %{
[created_contract_address: :names] => :optional,
[from_address: :names] => :optional,
[to_address: :names] => :optional,
[token_transfers: :token] => :optional,
[token_transfers: :to_address] => :optional,
[token_transfers: :from_address] => :optional,
[token_transfers: :token_contract_address] => :optional,
:block => :required
}
]
@page_size 150
@paging_options %PagingOptions{page_size: @page_size + 1}
def export(address) do
address.hash
|> fetch_all_transactions(@paging_options)
|> to_token_transfers()
|> to_csv_format(address)
|> dump_to_stream()
end
defp fetch_all_transactions(address_hash, paging_options, acc \\ []) do
options = Keyword.merge(@necessity_by_association, paging_options: paging_options)
transactions =
address_hash
|> Chain.address_to_mined_transactions_with_rewards(options)
|> Enum.filter(fn transaction -> Enum.count(transaction.token_transfers) > 0 end)
new_acc = transactions ++ acc
case Enum.split(transactions, @page_size) do
{_transactions, [%Transaction{block_number: block_number, index: index}]} ->
new_paging_options = %{@paging_options | key: {block_number, index}}
fetch_all_transactions(address_hash, new_paging_options, new_acc)
{_, []} ->
new_acc
end
end
defp to_token_transfers(transactions) do
transactions
|> Enum.flat_map(fn transaction ->
transaction.token_transfers
|> Enum.map(fn transfer -> %{transfer | transaction: transaction} end)
end)
end
defp dump_to_stream(transactions) do
transactions
|> RFC4180.dump_to_stream()
end
defp to_csv_format(token_transfers, address) do
row_names = [
"TxHash",
"BlockNumber",
"UnixTimestamp",
"FromAddress",
"ToAddress",
"TokenContractAddress",
"Type",
"TokenSymbol",
"TokensTransferred",
"TransactionFee",
"Status",
"ErrCode"
]
token_transfer_lists =
token_transfers
|> Stream.map(fn token_transfer ->
[
to_string(token_transfer.transaction_hash),
token_transfer.transaction.block_number,
token_transfer.transaction.block.timestamp,
token_transfer.from_address |> to_string() |> String.downcase(),
token_transfer.to_address |> to_string() |> String.downcase(),
token_transfer.token_contract_address |> to_string() |> String.downcase(),
type(token_transfer, address.hash),
token_transfer.token.symbol,
token_transfer.amount,
fee(token_transfer.transaction),
token_transfer.transaction.status,
token_transfer.transaction.error
]
end)
Stream.concat([row_names], token_transfer_lists)
end
defp type(%TokenTransfer{from_address_hash: address_hash}, address_hash), do: "OUT"
defp type(%TokenTransfer{to_address_hash: address_hash}, address_hash), do: "IN"
defp type(_, _), do: ""
defp fee(transaction) do
transaction
|> Chain.fee(:wei)
|> case do
{:actual, value} -> value
{:maximum, value} -> "Max of #{value}"
end
end
end

@ -1,139 +0,0 @@
defmodule Explorer.Chain.AddressTransactionCsvExporter do
@moduledoc """
Exports transactions to a csv file.
"""
import Ecto.Query,
only: [
from: 2
]
alias Explorer.{Chain, Market, PagingOptions, Repo}
alias Explorer.Market.MarketHistory
alias Explorer.Chain.{Address, Transaction, Wei}
alias Explorer.ExchangeRates.Token
alias NimbleCSV.RFC4180
@necessity_by_association [
necessity_by_association: %{
[created_contract_address: :names] => :optional,
[from_address: :names] => :optional,
[to_address: :names] => :optional,
[token_transfers: :token] => :optional,
[token_transfers: :to_address] => :optional,
[token_transfers: :from_address] => :optional,
[token_transfers: :token_contract_address] => :optional,
:block => :required
}
]
@page_size 150
@paging_options %PagingOptions{page_size: @page_size + 1}
@spec export(Address.t()) :: Enumerable.t()
def export(address) do
exchange_rate = Market.get_exchange_rate(Explorer.coin()) || Token.null()
address.hash
|> fetch_all_transactions(@paging_options)
|> to_csv_format(address, exchange_rate)
|> dump_to_stream()
end
defp fetch_all_transactions(address_hash, paging_options, acc \\ []) do
options = Keyword.put(@necessity_by_association, :paging_options, paging_options)
transactions = Chain.address_to_transactions_without_rewards(address_hash, options)
new_acc = transactions ++ acc
case Enum.split(transactions, @page_size) do
{_transactions, [%Transaction{block_number: block_number, index: index}]} ->
new_paging_options = %{@paging_options | key: {block_number, index}}
fetch_all_transactions(address_hash, new_paging_options, new_acc)
{_, []} ->
new_acc
end
end
defp dump_to_stream(transactions) do
transactions
|> RFC4180.dump_to_stream()
end
defp to_csv_format(transactions, address, exchange_rate) do
row_names = [
"TxHash",
"BlockNumber",
"UnixTimestamp",
"FromAddress",
"ToAddress",
"ContractAddress",
"Type",
"Value",
"Fee",
"Status",
"ErrCode",
"CurrentPrice",
"TxDateOpeningPrice",
"TxDateClosingPrice"
]
transaction_lists =
transactions
|> Stream.map(fn transaction ->
{opening_price, closing_price} = price_at_date(transaction.block.timestamp)
[
to_string(transaction.hash),
transaction.block_number,
transaction.block.timestamp,
to_string(transaction.from_address),
to_string(transaction.to_address),
to_string(transaction.created_contract_address),
type(transaction, address.hash),
Wei.to(transaction.value, :wei),
fee(transaction),
transaction.status,
transaction.error,
exchange_rate.usd_value,
opening_price,
closing_price
]
end)
Stream.concat([row_names], transaction_lists)
end
defp type(%Transaction{from_address_hash: address_hash}, address_hash), do: "OUT"
defp type(%Transaction{to_address_hash: address_hash}, address_hash), do: "IN"
defp type(_, _), do: ""
defp fee(transaction) do
transaction
|> Chain.fee(:wei)
|> case do
{:actual, value} -> value
{:maximum, value} -> "Max of #{value}"
end
end
defp price_at_date(datetime) do
date = DateTime.to_date(datetime)
query =
from(
mh in MarketHistory,
where: mh.date == ^date
)
case Repo.one(query) do
nil -> {nil, nil}
price -> {price.opening_price, price.closing_price}
end
end
end

@ -7,7 +7,7 @@ defmodule Explorer.Chain.Events.Subscriber do
@allowed_broadcast_types ~w(catchup realtime on_demand contract_verification_result)a @allowed_broadcast_types ~w(catchup realtime on_demand contract_verification_result)a
@allowed_events ~w(exchange_rate transaction_stats)a @allowed_events ~w(exchange_rate stake_snapshotting_finished transaction_stats)a
@type broadcast_type :: :realtime | :catchup | :on_demand @type broadcast_type :: :realtime | :catchup | :on_demand

@ -19,6 +19,8 @@ defmodule Explorer.Staking.ContractReader do
inactive_pools: {:staking, "df6f55f5", [], block_number}, inactive_pools: {:staking, "df6f55f5", [], block_number},
# f0786096 = keccak256(MAX_CANDIDATES()) # f0786096 = keccak256(MAX_CANDIDATES())
max_candidates: {:staking, "f0786096", [], block_number}, max_candidates: {:staking, "f0786096", [], block_number},
# 714897df = keccak256(MAX_VALIDATORS())
max_validators: {:validator_set, "714897df", [], block_number},
# 5fef7643 = keccak256(candidateMinStake()) # 5fef7643 = keccak256(candidateMinStake())
min_candidate_stake: {:staking, "5fef7643", [], block_number}, min_candidate_stake: {:staking, "5fef7643", [], block_number},
# da7a9b6a = keccak256(delegatorMinStake()) # da7a9b6a = keccak256(delegatorMinStake())
@ -29,6 +31,8 @@ defmodule Explorer.Staking.ContractReader do
pools_to_be_elected: {:staking, "a5d54f65", [], block_number}, pools_to_be_elected: {:staking, "a5d54f65", [], block_number},
# f4942501 = keccak256(areStakeAndWithdrawAllowed()) # f4942501 = keccak256(areStakeAndWithdrawAllowed())
staking_allowed: {:staking, "f4942501", [], block_number}, staking_allowed: {:staking, "f4942501", [], block_number},
# 74bdb372 = keccak256(lastChangeBlock())
staking_last_change_block: {:staking, "74bdb372", [], block_number},
# 2d21d217 = keccak256(erc677TokenContract()) # 2d21d217 = keccak256(erc677TokenContract())
token_contract_address: {:staking, "2d21d217", [], block_number}, token_contract_address: {:staking, "2d21d217", [], block_number},
# 704189ca = keccak256(unremovableValidator()) # 704189ca = keccak256(unremovableValidator())
@ -36,7 +40,9 @@ defmodule Explorer.Staking.ContractReader do
# b7ab4db5 = keccak256(getValidators()) # b7ab4db5 = keccak256(getValidators())
validators: {:validator_set, "b7ab4db5", [], block_number}, validators: {:validator_set, "b7ab4db5", [], block_number},
# b927ef43 = keccak256(validatorSetApplyBlock()) # b927ef43 = keccak256(validatorSetApplyBlock())
validator_set_apply_block: {:validator_set, "b927ef43", [], block_number} validator_set_apply_block: {:validator_set, "b927ef43", [], block_number},
# 74bdb372 = keccak256(lastChangeBlock())
validator_set_last_change_block: {:validator_set, "74bdb372", [], block_number}
] ]
end end

@ -23,9 +23,11 @@ defmodule Explorer.Staking.ContractState do
:epoch_number, :epoch_number,
:epoch_start_block, :epoch_start_block,
:is_snapshotting, :is_snapshotting,
:last_change_block,
:max_candidates, :max_candidates,
:min_candidate_stake, :min_candidate_stake,
:min_delegator_stake, :min_delegator_stake,
:seen_block,
:snapshotted_epoch_number, :snapshotted_epoch_number,
:staking_allowed, :staking_allowed,
:staking_contract, :staking_contract,
@ -36,11 +38,14 @@ defmodule Explorer.Staking.ContractState do
:validator_set_contract :validator_set_contract
] ]
# frequency in blocks # token renewal frequency in blocks
@token_renew_frequency 10 @token_renew_frequency 10
defstruct [ defstruct [
:seen_block, :eth_blocknumber_pull_interval,
:eth_subscribe_max_delay,
:snapshotting_finished,
:timer,
:contracts, :contracts,
:abi :abi
] ]
@ -69,13 +74,22 @@ defmodule Explorer.Staking.ContractState do
]) ])
Subscriber.to(:last_block_number, :realtime) Subscriber.to(:last_block_number, :realtime)
Subscriber.to(:stake_snapshotting_finished)
staking_abi = abi("StakingAuRa") staking_abi = abi("StakingAuRa")
validator_set_abi = abi("ValidatorSetAuRa") validator_set_abi = abi("ValidatorSetAuRa")
block_reward_abi = abi("BlockRewardAuRa") block_reward_abi = abi("BlockRewardAuRa")
token_abi = abi("Token") token_abi = abi("Token")
staking_contract_address = Application.get_env(:explorer, __MODULE__)[:staking_contract_address] module_env = Application.get_env(:explorer, __MODULE__)
# eth_blockNumber pull interval, in milliseconds
eth_blocknumber_pull_interval = String.to_integer(module_env[:eth_blocknumber_pull_interval])
# eth_subscribe max delay to switch to eth_blockNumber, in seconds
eth_subscribe_max_delay = String.to_integer(module_env[:eth_subscribe_max_delay])
staking_contract_address = module_env[:staking_contract_address]
# 2d21d217 = keccak256(erc677TokenContract()) # 2d21d217 = keccak256(erc677TokenContract())
erc_677_token_contract_signature = "2d21d217" erc_677_token_contract_signature = "2d21d217"
@ -100,7 +114,10 @@ defmodule Explorer.Staking.ContractState do
}) })
state = %__MODULE__{ state = %__MODULE__{
seen_block: 0, eth_blocknumber_pull_interval: eth_blocknumber_pull_interval,
eth_subscribe_max_delay: eth_subscribe_max_delay,
snapshotting_finished: false,
timer: nil,
contracts: %{ contracts: %{
staking: staking_contract_address, staking: staking_contract_address,
validator_set: validator_set_contract_address, validator_set: validator_set_contract_address,
@ -112,6 +129,8 @@ defmodule Explorer.Staking.ContractState do
:ets.insert(@table_name, :ets.insert(@table_name,
block_reward_contract: %{abi: block_reward_abi, address: block_reward_contract_address}, block_reward_contract: %{abi: block_reward_abi, address: block_reward_contract_address},
is_snapshotting: false, is_snapshotting: false,
last_change_block: 0,
seen_block: 0,
snapshotted_epoch_number: -1, snapshotted_epoch_number: -1,
staking_contract: %{abi: staking_abi, address: staking_contract_address}, staking_contract: %{abi: staking_abi, address: staking_contract_address},
token_contract: %{abi: token_abi, address: token_contract_address}, token_contract: %{abi: token_abi, address: token_contract_address},
@ -123,58 +142,135 @@ defmodule Explorer.Staking.ContractState do
end end
def handle_continue(_, state) do def handle_continue(_, state) do
# if eth_subscribe doesn't work during the first eth_subscribe_max_delay seconds
# after server start, use eth_blockNumber
timer = Process.send_after(self(), :eth_subscribe_stopped, state.eth_subscribe_max_delay * 1000)
{:noreply, %{state | timer: timer}}
end
# handles an event about snapshotting finishing
def handle_info({:chain_event, :stake_snapshotting_finished}, state) do
{:noreply, %{state | snapshotting_finished: true}}
end
# received when eth_subscribe is stopped
def handle_info(:eth_subscribe_stopped, state) do
state =
if Process.read_timer(state.timer) == false do
{microseconds, state} =
:timer.tc(
fn st -> fetch_state(st, get_current_block_number()) end,
[state]
)
# sleep up to eth_blocknumber_pull_interval milliseconds before the next eth_blockNumber request
Process.send_after(
self(),
:eth_subscribe_stopped,
max(state.eth_blocknumber_pull_interval - round(microseconds / 1000), 0)
)
state
else
state
end
{:noreply, state} {:noreply, state}
end end
@doc "Handles new blocks and decides to fetch fresh chain info" # catches a new block number from eth_subscribe
def handle_info({:chain_event, :last_block_number, :realtime, block_number}, state) do def handle_info({:chain_event, :last_block_number, :realtime, block_number}, state) do
if block_number > state.seen_block do if state.timer != nil do
Process.cancel_timer(state.timer)
end
state = fetch_state(state, block_number)
timer = Process.send_after(self(), :eth_subscribe_stopped, state.eth_subscribe_max_delay * 1000)
{:noreply, %{state | timer: timer}}
end
# handles new block and decides to fetch fresh chain info
defp fetch_state(state, block_number) do
if block_number <= get(:seen_block) do
state
else
fetch_state_internal(state, block_number)
end
end
defp fetch_state_internal(state, block_number) do
# read general info from the contracts (including pool list and validator list) # read general info from the contracts (including pool list and validator list)
global_responses = global_responses =
ContractReader.perform_requests(ContractReader.global_requests(block_number), state.contracts, state.abi) ContractReader.perform_requests(ContractReader.global_requests(block_number), state.contracts, state.abi)
epoch_very_beginning = global_responses.epoch_start_block == block_number + 1 epoch_very_beginning = global_responses.epoch_start_block == block_number + 1
if global_responses.epoch_number > get(:epoch_number) and not epoch_very_beginning and state.seen_block > 0 do start_snapshotting = start_snapshotting?(global_responses)
# if the previous staking epoch finished and we have blocks gap,
# call fetch_state in a loop until the blocks gap is closed
loop_block_start = state.seen_block + 1
loop_block_end = block_number - 1
if loop_block_end >= loop_block_start do # determine if something changed in contracts state since the previous seen block.
for bn <- loop_block_start..loop_block_end do # if something changed or the `fetch_state` function is called for the first time
gr = ContractReader.perform_requests(ContractReader.global_requests(bn), state.contracts, state.abi) # or we are at the beginning of staking epoch or snapshotting recently finished
fetch_state(state.contracts, state.abi, gr, bn, gr.epoch_start_block == bn + 1) # then we should update database
end last_change_block =
max(global_responses.staking_last_change_block, global_responses.validator_set_last_change_block)
first_fetch = get(:epoch_end_block, 0) == 0
should_update_db =
start_snapshotting or state.snapshotting_finished or first_fetch or last_change_block > get(:last_change_block)
# save the general info to ETS (excluding pool list and validator list)
set_settings(global_responses, state, block_number, last_change_block)
if epoch_very_beginning or start_snapshotting do
# if the block_number is the latest block of the finished staking epoch
# or we are starting Blockscout server, the BlockRewardAuRa contract balance
# could increase before (without Mint/Transfer events),
# so we need to update its balance in database
update_block_reward_balance(block_number)
end end
# we should update database as something changed in contracts state
if should_update_db do
update_database(
epoch_very_beginning,
start_snapshotting,
global_responses,
state.contracts,
state.abi,
block_number
)
end end
fetch_state(state.contracts, state.abi, global_responses, block_number, epoch_very_beginning) # notify the UI about a new block
{:noreply, %{state | seen_block: block_number}} data = %{
active_pools_length: get(:active_pools_length),
block_number: block_number,
epoch_end_block: global_responses.epoch_end_block,
epoch_number: global_responses.epoch_number,
max_candidates: global_responses.max_candidates,
staking_allowed: global_responses.staking_allowed,
staking_token_defined: get(:token, nil) != nil,
validator_set_apply_block: global_responses.validator_set_apply_block
}
Publisher.broadcast([{:staking_update, data}], :realtime)
if state.snapshotting_finished do
%{state | snapshotting_finished: false}
else else
{:noreply, state} state
end end
end end
defp fetch_state(contracts, abi, global_responses, block_number, epoch_very_beginning) do defp start_snapshotting?(global_responses) do
validator_min_reward_percent =
get_validator_min_reward_percent(global_responses.epoch_number, block_number, contracts, abi)
is_validator = Enum.into(global_responses.validators, %{}, &{address_bytes_to_string(&1), true})
start_snapshotting =
global_responses.epoch_number > get(:snapshotted_epoch_number) && global_responses.epoch_number > 0 && global_responses.epoch_number > get(:snapshotted_epoch_number) && global_responses.epoch_number > 0 &&
not get(:is_snapshotting) not get(:is_snapshotting)
end
active_pools_length = Enum.count(global_responses.active_pools) defp update_database(epoch_very_beginning, start_snapshotting, global_responses, contracts, abi, block_number) do
is_validator = Enum.into(global_responses.validators, %{}, &{address_bytes_to_string(&1), true})
# save the general info to ETS (excluding pool list and validator list)
settings =
global_responses
|> get_settings(validator_min_reward_percent, block_number)
|> Enum.concat(active_pools_length: active_pools_length)
:ets.insert(@table_name, settings)
# form the list of validator pools # form the list of validator pools
validators = validators =
@ -220,14 +316,7 @@ defmodule Explorer.Staking.ContractState do
# call `BlockReward.delegatorShare` function for each delegator # call `BlockReward.delegatorShare` function for each delegator
# to get their reward share of the pool (needed for the `Delegators` list in UI) # to get their reward share of the pool (needed for the `Delegators` list in UI)
delegator_responses = delegator_responses = get_delegator_responses(staker_responses)
Enum.reduce(staker_responses, %{}, fn {{pool_staking_address, staker_address, _is_active} = key, value}, acc ->
if pool_staking_address != staker_address do
Map.put(acc, key, value)
else
acc
end
end)
delegator_reward_responses = delegator_reward_responses =
get_delegator_reward_responses( get_delegator_reward_responses(
@ -239,9 +328,6 @@ defmodule Explorer.Staking.ContractState do
block_number block_number
) )
# calculate total amount staked into all active pools
staked_total = Enum.sum(for {_, pool} <- pool_staking_responses, pool.is_active, do: pool.total_staked_amount)
# calculate likelihood of becoming a validator on the next epoch # calculate likelihood of becoming a validator on the next epoch
[likelihood_values, total_likelihood] = global_responses.pools_likelihood [likelihood_values, total_likelihood] = global_responses.pools_likelihood
# array of pool addresses (staking addresses) # array of pool addresses (staking addresses)
@ -252,7 +338,20 @@ defmodule Explorer.Staking.ContractState do
snapshotted_epoch_number = get(:snapshotted_epoch_number) snapshotted_epoch_number = get(:snapshotted_epoch_number)
# form entries for writing to the `staking_pools` table in DB # form entries for writing to the `staking_pools_delegators` table in DB
delegator_entries = get_delegator_entries(staker_responses, delegator_reward_responses)
# perform SQL queries
{:ok, _} =
Chain.import(%{
staking_pools_delegators: %{params: delegator_entries},
timeout: :infinity
})
# form entries for writing to the `staking_pools` table in DB.
# !!! it's important to do this AFTER updating `staking_pools_delegators`
# !!! table because the `get_pool_entries` function requires fresh
# !!! info about delegators of validators from the `staking_pools_delegators` table
pool_entries = pool_entries =
get_pool_entries(%{ get_pool_entries(%{
pools: pools, pools: pools,
@ -263,25 +362,16 @@ defmodule Explorer.Staking.ContractState do
global_responses: global_responses, global_responses: global_responses,
snapshotted_epoch_number: snapshotted_epoch_number, snapshotted_epoch_number: snapshotted_epoch_number,
likelihood: likelihood, likelihood: likelihood,
total_likelihood: total_likelihood, total_likelihood: total_likelihood
staked_total: staked_total
}) })
# form entries for writing to the `staking_pools_delegators` table in DB
delegator_entries = get_delegator_entries(staker_responses, delegator_reward_responses)
# perform SQL queries # perform SQL queries
{:ok, _} = {:ok, _} =
Chain.import(%{ Chain.import(%{
staking_pools: %{params: pool_entries}, staking_pools: %{params: pool_entries},
staking_pools_delegators: %{params: delegator_entries},
timeout: :infinity timeout: :infinity
}) })
if epoch_very_beginning or start_snapshotting do
at_start_snapshotting(block_number)
end
if start_snapshotting do if start_snapshotting do
do_start_snapshotting( do_start_snapshotting(
epoch_very_beginning, epoch_very_beginning,
@ -293,23 +383,31 @@ defmodule Explorer.Staking.ContractState do
mining_to_staking_address mining_to_staking_address
) )
end end
end
# notify the UI about a new block defp get_current_block_number do
data = %{ json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments)
active_pools_length: active_pools_length,
block_number: block_number,
epoch_end_block: global_responses.epoch_end_block,
epoch_number: global_responses.epoch_number,
max_candidates: global_responses.max_candidates,
staking_allowed: global_responses.staking_allowed,
staking_token_defined: get(:token, nil) != nil,
validator_set_apply_block: global_responses.validator_set_apply_block
}
Publisher.broadcast([{:staking_update, data}], :realtime) result =
%{id: 0, method: "eth_blockNumber", params: []}
|> EthereumJSONRPC.request()
|> EthereumJSONRPC.json_rpc(json_rpc_named_arguments)
case result do
{:ok, response} ->
response
|> String.replace_leading("0x", "")
|> String.to_integer(16)
_ ->
0
end end
end
defp get_settings(global_responses, state, block_number) do
validator_min_reward_percent =
get_validator_min_reward_percent(global_responses.epoch_number, block_number, state.contracts, state.abi)
defp get_settings(global_responses, validator_min_reward_percent, block_number) do
base_settings = get_base_settings(global_responses, validator_min_reward_percent) base_settings = get_base_settings(global_responses, validator_min_reward_percent)
update_token = update_token =
@ -324,6 +422,17 @@ defmodule Explorer.Staking.ContractState do
end end
end end
defp set_settings(global_responses, state, block_number, last_change_block) do
settings =
global_responses
|> get_settings(state, block_number)
|> Enum.concat(active_pools_length: Enum.count(global_responses.active_pools))
|> Enum.concat(last_change_block: last_change_block)
|> Enum.concat(seen_block: block_number)
:ets.insert(@table_name, settings)
end
defp get_mining_to_staking_address(validators, contracts, abi, block_number) do defp get_mining_to_staking_address(validators, contracts, abi, block_number) do
validators.all validators.all
|> Enum.map(&ContractReader.staking_by_mining_request(&1, block_number)) |> Enum.map(&ContractReader.staking_by_mining_request(&1, block_number))
@ -390,6 +499,16 @@ defmodule Explorer.Staking.ContractState do
|> ContractReader.perform_grouped_requests(pool_staking_keys, contracts, abi) |> ContractReader.perform_grouped_requests(pool_staking_keys, contracts, abi)
end end
defp get_delegator_responses(staker_responses) do
Enum.reduce(staker_responses, %{}, fn {{pool_staking_address, staker_address, _is_active} = key, value}, acc ->
if pool_staking_address != staker_address do
Map.put(acc, key, value)
else
acc
end
end)
end
defp get_delegator_reward_responses( defp get_delegator_reward_responses(
delegator_responses, delegator_responses,
pool_staking_responses, pool_staking_responses,
@ -543,9 +662,11 @@ defmodule Explorer.Staking.ContractState do
global_responses: global_responses, global_responses: global_responses,
snapshotted_epoch_number: snapshotted_epoch_number, snapshotted_epoch_number: snapshotted_epoch_number,
likelihood: likelihood, likelihood: likelihood,
total_likelihood: total_likelihood, total_likelihood: total_likelihood
staked_total: staked_total
}) do }) do
# total amount staked into all active pools
staked_total = Enum.sum(for {_, pool} <- pool_staking_responses, pool.is_active, do: pool.total_staked_amount)
Enum.map(pools, fn pool_staking_address -> Enum.map(pools, fn pool_staking_address ->
staking_resp = pool_staking_responses[pool_staking_address] staking_resp = pool_staking_responses[pool_staking_address]
mining_resp = pool_mining_responses[pool_staking_address] mining_resp = pool_mining_responses[pool_staking_address]
@ -565,6 +686,15 @@ defmodule Explorer.Staking.ContractState do
0 0
end end
is_unremovable = address_bytes_to_string(pool_staking_address) == global_responses.unremovable_validator
likelihood_value =
if get(:active_pools_length) > global_responses.max_validators and not is_unremovable do
ratio(likelihood[pool_staking_address] || 0, total_likelihood)
else
100
end
%{ %{
staking_address_hash: pool_staking_address, staking_address_hash: pool_staking_address,
delegators_count: delegators_count, delegators_count: delegators_count,
@ -575,11 +705,11 @@ defmodule Explorer.Staking.ContractState do
0 0
end, end,
validator_reward_ratio: Float.floor(candidate_reward_resp.validator_share / 10_000, 2), validator_reward_ratio: Float.floor(candidate_reward_resp.validator_share / 10_000, 2),
likelihood: ratio(likelihood[pool_staking_address] || 0, total_likelihood), likelihood: likelihood_value,
validator_reward_percent: staking_resp.validator_reward_percent / 10_000, validator_reward_percent: staking_resp.validator_reward_percent / 10_000,
is_deleted: false, is_deleted: false,
is_validator: is_validator, is_validator: is_validator,
is_unremovable: address_bytes_to_string(pool_staking_address) == global_responses.unremovable_validator, is_unremovable: is_unremovable,
ban_reason: binary_to_string(mining_resp.ban_reason) ban_reason: binary_to_string(mining_resp.ban_reason)
} }
|> Map.merge( |> Map.merge(
@ -603,7 +733,7 @@ defmodule Explorer.Staking.ContractState do
end) end)
end end
defp at_start_snapshotting(block_number) do defp update_block_reward_balance(block_number) do
# update ERC balance of the BlockReward contract # update ERC balance of the BlockReward contract
token = get(:token) token = get(:token)

@ -8,6 +8,7 @@ defmodule Explorer.Staking.StakeSnapshotting do
require Logger require Logger
alias Explorer.Chain alias Explorer.Chain
alias Explorer.Chain.Events.Publisher
alias Explorer.Chain.{StakingPool, StakingPoolsDelegator} alias Explorer.Chain.{StakingPool, StakingPoolsDelegator}
alias Explorer.Staking.ContractReader alias Explorer.Staking.ContractReader
@ -194,6 +195,8 @@ defmodule Explorer.Staking.StakeSnapshotting do
end end
:ets.insert(ets_table_name, is_snapshotting: false) :ets.insert(ets_table_name, is_snapshotting: false)
Publisher.broadcast(:stake_snapshotting_finished)
end end
defp address_bytes_to_string(hash), do: "0x" <> Base.encode16(hash, case: :lower) defp address_bytes_to_string(hash), do: "0x" <> Base.encode16(hash, case: :lower)

@ -9,7 +9,12 @@ defmodule Explorer.Validator.MetadataImporter do
def import_metadata(metadata_maps) do def import_metadata(metadata_maps) do
# Enforce Name ShareLocks order (see docs: sharelocks.md) # Enforce Name ShareLocks order (see docs: sharelocks.md)
ordered_metadata_maps = Enum.sort_by(metadata_maps, &{&1.address_hash, &1.name}) ordered_metadata_maps =
metadata_maps
|> Enum.filter(fn metadata ->
String.trim(metadata.name) !== ""
end)
|> Enum.sort_by(&{&1.address_hash, &1.name})
Repo.transaction(fn -> Enum.each(ordered_metadata_maps, &upsert_validator_metadata(&1)) end) Repo.transaction(fn -> Enum.each(ordered_metadata_maps, &upsert_validator_metadata(&1)) end)
end end

@ -1074,5 +1074,19 @@
"payable": false, "payable": false,
"stateMutability": "view", "stateMutability": "view",
"type": "function" "type": "function"
},
{
"constant": true,
"inputs": [],
"name": "lastChangeBlock",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
} }
] ]

@ -694,5 +694,19 @@
"payable": false, "payable": false,
"stateMutability": "view", "stateMutability": "view",
"type": "function" "type": "function"
},
{
"constant": true,
"inputs": [],
"name": "lastChangeBlock",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
} }
] ]

@ -1,72 +0,0 @@
defmodule Explorer.Chain.AddressTokenTransferCsvExporterTest do
use Explorer.DataCase
alias Explorer.Chain.AddressTokenTransferCsvExporter
describe "export/1" do
test "exports token transfers to csv" do
address = insert(:address)
transaction =
:transaction
|> insert(from_address: address)
|> with_block()
token_transfer = insert(:token_transfer, transaction: transaction, from_address: address)
[result] =
address
|> AddressTokenTransferCsvExporter.export()
|> Enum.to_list()
|> Enum.drop(1)
|> Enum.map(fn [
tx_hash,
_,
block_number,
_,
timestamp,
_,
from_address,
_,
to_address,
_,
token_contract_address,
_,
type,
_,
token_symbol,
_,
tokens_transferred,
_,
transaction_fee,
_,
status,
_,
err_code,
_
] ->
%{
tx_hash: tx_hash,
block_number: block_number,
timestamp: timestamp,
from_address: from_address,
to_address: to_address,
token_contract_address: token_contract_address,
type: type,
token_symbol: token_symbol,
tokens_transferred: tokens_transferred,
transaction_fee: transaction_fee,
status: status,
err_code: err_code
}
end)
assert result.block_number == to_string(transaction.block_number)
assert result.tx_hash == to_string(transaction.hash)
assert result.from_address == token_transfer.from_address_hash |> to_string() |> String.downcase()
assert result.to_address == token_transfer.to_address_hash |> to_string() |> String.downcase()
assert result.timestamp == to_string(transaction.block.timestamp)
assert result.type == "OUT"
end
end
end

@ -1,105 +0,0 @@
defmodule Explorer.Chain.AddressTransactionCsvExporterTest do
use Explorer.DataCase
alias Explorer.Chain.{AddressTransactionCsvExporter, Wei}
describe "export/1" do
test "exports address transactions to csv" do
address = insert(:address)
transaction =
:transaction
|> insert(from_address: address)
|> with_block()
|> Repo.preload(:token_transfers)
[result] =
address
|> AddressTransactionCsvExporter.export()
|> Enum.to_list()
|> Enum.drop(1)
|> Enum.map(fn [
hash,
_,
block_number,
_,
timestamp,
_,
from_address,
_,
to_address,
_,
created_address,
_,
type,
_,
value,
_,
fee,
_,
status,
_,
error,
_,
cur_price,
_,
op_price,
_,
cl_price,
_
] ->
%{
hash: hash,
block_number: block_number,
timestamp: timestamp,
from_address: from_address,
to_address: to_address,
created_address: created_address,
type: type,
value: value,
fee: fee,
status: status,
error: error,
current_price: cur_price,
opening_price: op_price,
closing_price: cl_price
}
end)
assert result.block_number == to_string(transaction.block_number)
assert result.timestamp
assert result.created_address == to_string(transaction.created_contract_address_hash)
assert result.from_address == to_string(transaction.from_address)
assert result.to_address == to_string(transaction.to_address)
assert result.hash == to_string(transaction.hash)
assert result.type == "OUT"
assert result.value == transaction.value |> Wei.to(:wei) |> to_string()
assert result.fee
assert result.status == to_string(transaction.status)
assert result.error == to_string(transaction.error)
assert result.current_price
assert result.opening_price
assert result.closing_price
end
test "fetches all transactions" do
address = insert(:address)
1..200
|> Enum.map(fn _ ->
:transaction
|> insert(from_address: address)
|> with_block()
end)
|> Enum.count()
result =
address
|> AddressTransactionCsvExporter.export()
|> Enum.to_list()
|> Enum.drop(1)
assert Enum.count(result) == 200
end
end
end

@ -25,7 +25,9 @@ defmodule Explorer.Staking.ContractStateTest do
Application.put_env(:explorer, ContractState, Application.put_env(:explorer, ContractState,
enabled: true, enabled: true,
staking_contract_address: "0x1100000000000000000000000000000000000001" staking_contract_address: "0x1100000000000000000000000000000000000001",
eth_blocknumber_pull_interval: "500",
eth_subscribe_max_delay: "60"
) )
start_supervised!(ContractState) start_supervised!(ContractState)
@ -99,7 +101,7 @@ defmodule Explorer.Staking.ContractStateTest do
EthereumJSONRPC.Mox, EthereumJSONRPC.Mox,
:json_rpc, :json_rpc,
fn requests, _opts -> fn requests, _opts ->
assert length(requests) == 15 assert length(requests) == 18
{:ok, {:ok,
format_responses([ format_responses([
@ -114,24 +116,30 @@ defmodule Explorer.Staking.ContractStateTest do
# 5 StakingAuRa.getPoolsInactive # 5 StakingAuRa.getPoolsInactive
"0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000", "0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000",
# 6 StakingAuRa.MAX_CANDIDATES # 6 StakingAuRa.MAX_CANDIDATES
"0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000bb8", "0x0000000000000000000000000000000000000000000000000000000000000bb8",
# 7 StakingAuRa.candidateMinStake # 7 StakingAuRa.MAX_VALIDATORS
"0x0000000000000000000000000000000000000000000000000000000000000013",
# 8 StakingAuRa.candidateMinStake
"0x0000000000000000000000000000000000000000000000000de0b6b3a7640000", "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000",
# 8 StakingAuRa.delegatorMinStake # 9 StakingAuRa.delegatorMinStake
"0x0000000000000000000000000000000000000000000000000de0b6b3a7640000", "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000",
# 9 StakingAuRa.getPoolsLikelihood # 10 StakingAuRa.getPoolsLikelihood
"0x0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000001bc16d674ec8000000000000000000000000000000000000000000000000000098a7d9b8314c000000000000000000000000000000000000000000000000000029a2241af62c0000", "0x0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000001bc16d674ec8000000000000000000000000000000000000000000000000000098a7d9b8314c000000000000000000000000000000000000000000000000000029a2241af62c0000",
# 10 StakingAuRa.getPoolsToBeElected # 11 StakingAuRa.getPoolsToBeElected
"0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003000000000000000000000000b916e7e1f4bcb13549602ed042d36746fd0d96c9000000000000000000000000db9cb2478d917719c53862008672166808258577000000000000000000000000b6695f5c2e3f5eff8036b5f5f3a9d83a5310e51e", "0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003000000000000000000000000b916e7e1f4bcb13549602ed042d36746fd0d96c9000000000000000000000000db9cb2478d917719c53862008672166808258577000000000000000000000000b6695f5c2e3f5eff8036b5f5f3a9d83a5310e51e",
# 11 StakingAuRa.areStakeAndWithdrawAllowed # 12 StakingAuRa.areStakeAndWithdrawAllowed
"0x0000000000000000000000000000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000000000000000000000000000",
# 12 StakingAuRa.erc677TokenContract # 13 StakingAuRa.lastChangeBlock
"0x0000000000000000000000000000000000000000000000000000000000000000",
# 14 StakingAuRa.erc677TokenContract
"0x0000000000000000000000006f7a73c96bd56f8b0debc795511eda135e105ea3", "0x0000000000000000000000006f7a73c96bd56f8b0debc795511eda135e105ea3",
# 13 ValidatorSetAuRa.unremovableValidator # 15 ValidatorSetAuRa.unremovableValidator
"0x0000000000000000000000000b2f5e2f3cbd864eaa2c642e3769c1582361caf6", "0x0000000000000000000000000b2f5e2f3cbd864eaa2c642e3769c1582361caf6",
# 14 ValidatorSetAuRa.getValidators # 16 ValidatorSetAuRa.getValidators
"0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003000000000000000000000000bbcaa8d48289bb1ffcf9808d9aa4b1d215054c7800000000000000000000000075df42383afe6bf5194aa8fa0e9b3d5f9e869441000000000000000000000000522df396ae70a058bd69778408630fdb023389b2", "0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003000000000000000000000000bbcaa8d48289bb1ffcf9808d9aa4b1d215054c7800000000000000000000000075df42383afe6bf5194aa8fa0e9b3d5f9e869441000000000000000000000000522df396ae70a058bd69778408630fdb023389b2",
# 15 ValidatorSetAuRa.validatorSetApplyBlock # 17 ValidatorSetAuRa.validatorSetApplyBlock
"0x0000000000000000000000000000000000000000000000000000000000000000",
# 18 ValidatorSetAuRa.lastChangeBlock
"0x0000000000000000000000000000000000000000000000000000000000000000" "0x0000000000000000000000000000000000000000000000000000000000000000"
])} ])}
end end
@ -152,6 +160,44 @@ defmodule Explorer.Staking.ContractStateTest do
end end
) )
# invoke update_block_reward_balance()
## BalanceReader.get_balances_of
expect(
EthereumJSONRPC.Mox,
:json_rpc,
fn requests, _opts ->
assert length(requests) == 1
{:ok,
format_responses([
# ERC677BridgeTokenRewardable.balanceOf(BlockRewardAuRa)
"0x0000000000000000000000000000000000000000000000000000000000000000"
])}
end
)
## MetadataRetriever.get_functions_of
expect(
EthereumJSONRPC.Mox,
:json_rpc,
fn requests, _opts ->
assert length(requests) == 4
{:ok,
format_responses([
# ERC677BridgeTokenRewardable.decimals
"0x0000000000000000000000000000000000000000000000000000000000000012",
# ERC677BridgeTokenRewardable.name
"0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000055354414b45000000000000000000000000000000000000000000000000000000",
# ERC677BridgeTokenRewardable.symbol
"0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000055354414b45000000000000000000000000000000000000000000000000000000",
# ERC677BridgeTokenRewardable.totalSupply
"0x000000000000000000000000000000000000000000000001f399b1438a100000"
])}
end
)
# get_validators # get_validators
expect( expect(
EthereumJSONRPC.Mox, EthereumJSONRPC.Mox,
@ -644,44 +690,6 @@ defmodule Explorer.Staking.ContractStateTest do
end end
) )
# invoke at_start_snapshotting()
## BalanceReader.get_balances_of
expect(
EthereumJSONRPC.Mox,
:json_rpc,
fn requests, _opts ->
assert length(requests) == 1
{:ok,
format_responses([
# ERC677BridgeTokenRewardable.balanceOf(BlockRewardAuRa)
"0x0000000000000000000000000000000000000000000000000000000000000000"
])}
end
)
## MetadataRetriever.get_functions_of
expect(
EthereumJSONRPC.Mox,
:json_rpc,
fn requests, _opts ->
assert length(requests) == 4
{:ok,
format_responses([
# ERC677BridgeTokenRewardable.decimals
"0x0000000000000000000000000000000000000000000000000000000000000012",
# ERC677BridgeTokenRewardable.name
"0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000055354414b45000000000000000000000000000000000000000000000000000000",
# ERC677BridgeTokenRewardable.symbol
"0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000055354414b45000000000000000000000000000000000000000000000000000000",
# ERC677BridgeTokenRewardable.totalSupply
"0x000000000000000000000000000000000000000000000001f399b1438a100000"
])}
end
)
# invoke do_snapshotting() # invoke do_snapshotting()
## 1 snapshotted_pool_amounts_requests ## 1 snapshotted_pool_amounts_requests

@ -86,7 +86,11 @@ defmodule Indexer.Block.Realtime.Fetcher do
) )
when is_binary(quantity) do when is_binary(quantity) do
number = quantity_to_integer(quantity) number = quantity_to_integer(quantity)
if number > 0 do
Publisher.broadcast([{:last_block_number, number}], :realtime) Publisher.broadcast([{:last_block_number, number}], :realtime)
end
# Subscriptions don't support getting all the blocks and transactions data, # Subscriptions don't support getting all the blocks and transactions data,
# so we need to go back and get the full block # so we need to go back and get the full block
start_fetch_and_import(number, block_fetcher, previous_number, max_number_seen) start_fetch_and_import(number, block_fetcher, previous_number, max_number_seen)

Loading…
Cancel
Save