Blockscout core API v2 (#6429)

* Add tests; Fix /transactions bug

* Add /internal-transactions and /logs tests

* Checksum all address hashes; Done transaction_controller_test.exs

* Add block_controller_test.exs

* Done block_controller_test.exs

* Add /counters, change /token-transfers for /addresses; Add timestamp to token transfer body

* Drop params from next_page_params; Add address_controller_test.exs

* Fix pending txs pagination; Add tests for address controller

* Tests for address in progress

* Add coin balances test for address

* Done address_controller_test.exs

* Done tests for API v2; Fix pagination for search; Add cache for transactions for API v2

* Add ERC filtering for transactions/0x../token-transfers; Add network utilization to /stats

* Return nil for nil address_hash instead of struct

* Fix token transfers

* Remove decoded field from revert_reason body
pull/6478/head
nikitosing 2 years ago committed by GitHub
parent 84f2b3ebf8
commit 0e0931e130
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      .github/workflows/config.yml
  2. 2
      CHANGELOG.md
  3. 1
      apps/block_scout_web/lib/block_scout_web/api_router.ex
  4. 56
      apps/block_scout_web/lib/block_scout_web/controllers/address_controller.ex
  5. 66
      apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex
  6. 9
      apps/block_scout_web/lib/block_scout_web/controllers/api/v2/block_controller.ex
  7. 5
      apps/block_scout_web/lib/block_scout_web/controllers/api/v2/fallback_controller.ex
  8. 2
      apps/block_scout_web/lib/block_scout_web/controllers/api/v2/main_page_controller.ex
  9. 14
      apps/block_scout_web/lib/block_scout_web/controllers/api/v2/stats_controller.ex
  10. 66
      apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex
  11. 3
      apps/block_scout_web/lib/block_scout_web/controllers/transaction_state_controller.ex
  12. 3
      apps/block_scout_web/lib/block_scout_web/controllers/transaction_token_transfer_controller.ex
  13. 40
      apps/block_scout_web/lib/block_scout_web/paging_helper.ex
  14. 18
      apps/block_scout_web/lib/block_scout_web/views/api/v2/helper.ex
  15. 6
      apps/block_scout_web/lib/block_scout_web/views/api/v2/token_view.ex
  16. 15
      apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex
  17. 1199
      apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs
  18. 329
      apps/block_scout_web/test/block_scout_web/controllers/api/v2/block_controller_test.exs
  19. 22
      apps/block_scout_web/test/block_scout_web/controllers/api/v2/config_controller_test.exs
  20. 65
      apps/block_scout_web/test/block_scout_web/controllers/api/v2/main_page_controller_test.exs
  21. 147
      apps/block_scout_web/test/block_scout_web/controllers/api/v2/search_controller_test.exs
  22. 57
      apps/block_scout_web/test/block_scout_web/controllers/api/v2/stats_controller_test.exs
  23. 582
      apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs
  24. 2
      apps/explorer/lib/explorer/application.ex
  25. 177
      apps/explorer/lib/explorer/chain.ex
  26. 27
      apps/explorer/lib/explorer/chain/cache/transactions_api_v2.ex
  27. 44
      apps/explorer/lib/explorer/chain/token_transfer.ex
  28. 2
      apps/explorer/test/explorer/chain_test.exs
  29. 35
      apps/explorer/test/support/factory.ex
  30. 4
      config/runtime.exs
  31. 2
      config/runtime/test.exs

@ -582,3 +582,4 @@ jobs:
ADMIN_PANEL_ENABLED: "true" ADMIN_PANEL_ENABLED: "true"
ACCOUNT_ENABLED: "true" ACCOUNT_ENABLED: "true"
ACCOUNT_REDIS_URL: "redis://localhost:6379" ACCOUNT_REDIS_URL: "redis://localhost:6379"
API_V2_ENABLED: "true"

@ -6,7 +6,7 @@
- [#6440](https://github.com/blockscout/blockscout/pull/6440) - Add support for base64 encoded NFT metadata - [#6440](https://github.com/blockscout/blockscout/pull/6440) - Add support for base64 encoded NFT metadata
- [#6407](https://github.com/blockscout/blockscout/pull/6407) - Indexed ratio for int txs fetching stage - [#6407](https://github.com/blockscout/blockscout/pull/6407) - Indexed ratio for int txs fetching stage
- [#6324](https://github.com/blockscout/blockscout/pull/6324) - Add verified contracts list page - [#6324](https://github.com/blockscout/blockscout/pull/6324) - Add verified contracts list page
- [#6379](https://github.com/blockscout/blockscout/pull/6379) - API v2 for frontend - [#6379](https://github.com/blockscout/blockscout/pull/6379), [#6429](https://github.com/blockscout/blockscout/pull/6429) - API v2 for frontend
- [#6351](https://github.com/blockscout/blockscout/pull/6351) - Enable forum link env var - [#6351](https://github.com/blockscout/blockscout/pull/6351) - Enable forum link env var
- [#6316](https://github.com/blockscout/blockscout/pull/6316) - Copy public tags functionality to master - [#6316](https://github.com/blockscout/blockscout/pull/6316) - Copy public tags functionality to master
- [#6196](https://github.com/blockscout/blockscout/pull/6196) - INDEXER_CATCHUP_BLOCKS_BATCH_SIZE and INDEXER_CATCHUP_BLOCKS_CONCURRENCY env varaibles - [#6196](https://github.com/blockscout/blockscout/pull/6196) - INDEXER_CATCHUP_BLOCKS_BATCH_SIZE and INDEXER_CATCHUP_BLOCKS_CONCURRENCY env varaibles

@ -118,6 +118,7 @@ defmodule BlockScoutWeb.ApiRouter do
scope "/addresses" do scope "/addresses" do
get("/:address_hash", V2.AddressController, :address) get("/:address_hash", V2.AddressController, :address)
get("/:address_hash/counters", V2.AddressController, :counters)
get("/:address_hash/token-balances", V2.AddressController, :token_balances) get("/:address_hash/token-balances", V2.AddressController, :token_balances)
get("/:address_hash/transactions", V2.AddressController, :transactions) get("/:address_hash/transactions", V2.AddressController, :transactions)
get("/:address_hash/token-transfers", V2.AddressController, :token_transfers) get("/:address_hash/token-transfers", V2.AddressController, :token_transfers)

@ -14,7 +14,6 @@ defmodule BlockScoutWeb.AddressController do
Controller Controller
} }
alias Explorer.Counters.{AddressTokenTransfersCounter, AddressTransactionsCounter, AddressTransactionsGasUsageCounter}
alias Explorer.{Chain, Market} alias Explorer.{Chain, Market}
alias Explorer.Chain.Wei alias Explorer.Chain.Wei
alias Explorer.ExchangeRates.Token alias Explorer.ExchangeRates.Token
@ -148,7 +147,7 @@ defmodule BlockScoutWeb.AddressController do
def address_counters(conn, %{"id" => address_hash_string}) do def address_counters(conn, %{"id" => address_hash_string}) do
with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string), with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string),
{:ok, address} <- Chain.hash_to_address(address_hash) do {:ok, address} <- Chain.hash_to_address(address_hash) do
{validation_count} = address_counters(address) {validation_count} = Chain.address_counters(address)
transactions_from_db = address.transactions_count || 0 transactions_from_db = address.transactions_count || 0
token_transfers_from_db = address.token_transfers_count || 0 token_transfers_from_db = address.token_transfers_count || 0
@ -170,57 +169,4 @@ defmodule BlockScoutWeb.AddressController do
}) })
end end
end end
defp address_counters(address) do
validation_count_task =
Task.async(fn ->
validation_count(address)
end)
Task.start_link(fn ->
transaction_count(address)
end)
Task.start_link(fn ->
token_transfers_count(address)
end)
Task.start_link(fn ->
gas_usage_count(address)
end)
[
validation_count_task
]
|> Task.yield_many(:infinity)
|> Enum.map(fn {_task, res} ->
case res do
{:ok, result} ->
result
{:exit, reason} ->
raise "Query fetching address counters terminated: #{inspect(reason)}"
nil ->
raise "Query fetching address counters timed out."
end
end)
|> List.to_tuple()
end
def transaction_count(address) do
AddressTransactionsCounter.fetch(address)
end
def token_transfers_count(address) do
AddressTokenTransfersCounter.fetch(address)
end
def gas_usage_count(address) do
AddressTransactionsGasUsageCounter.fetch(address)
end
defp validation_count(address) do
Chain.address_to_validation_count(address.hash)
end
end end

@ -9,6 +9,9 @@ defmodule BlockScoutWeb.API.V2.AddressController do
current_filter: 1 current_filter: 1
] ]
import BlockScoutWeb.PagingHelper,
only: [delete_parameters_from_next_page_params: 1, token_transfers_types_options: 1]
alias BlockScoutWeb.API.V2.{AddressView, BlockView, TransactionView} alias BlockScoutWeb.API.V2.{AddressView, BlockView, TransactionView}
alias Explorer.{Chain, Market} alias Explorer.{Chain, Market}
alias Indexer.Fetcher.TokenBalanceOnDemand alias Indexer.Fetcher.TokenBalanceOnDemand
@ -25,19 +28,11 @@ defmodule BlockScoutWeb.API.V2.AddressController do
} }
] ]
@transaction_with_tt_necessity_by_association [ @token_transfer_necessity_by_association [
necessity_by_association: %{ necessity_by_association: %{
[created_contract_address: :names] => :optional, :to_address => :optional,
[from_address: :names] => :optional, :from_address => :optional,
[to_address: :names] => :optional, :block => :optional
[created_contract_address: :smart_contract] => :optional,
[from_address: :smart_contract] => :optional,
[to_address: :smart_contract] => :optional,
[token_transfers: :token] => :optional,
[token_transfers: :to_address] => :optional,
[token_transfers: :from_address] => :optional,
[token_transfers: :token_contract_address] => :optional,
:block => :required
} }
] ]
@ -52,6 +47,24 @@ defmodule BlockScoutWeb.API.V2.AddressController do
end end
end end
def counters(conn, %{"address_hash" => address_hash_string}) do
with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)},
{:not_found, {:ok, address}} <- {:not_found, Chain.hash_to_address(address_hash)} do
{validation_count} = Chain.address_counters(address)
transactions_from_db = address.transactions_count || 0
token_transfers_from_db = address.token_transfers_count || 0
address_gas_usage_from_db = address.gas_used || 0
json(conn, %{
transaction_count: to_string(transactions_from_db),
token_transfer_count: to_string(token_transfers_from_db),
gas_usage_count: to_string(address_gas_usage_from_db),
validation_count: to_string(validation_count)
})
end
end
def token_balances(conn, %{"address_hash" => address_hash_string}) do def token_balances(conn, %{"address_hash" => address_hash_string}) do
with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)} do with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)} do
token_balances = token_balances =
@ -82,7 +95,8 @@ defmodule BlockScoutWeb.API.V2.AddressController do
results_plus_one = Chain.address_to_transactions_with_rewards(address_hash, options) results_plus_one = Chain.address_to_transactions_with_rewards(address_hash, options)
{transactions, next_page} = split_list_by_page(results_plus_one) {transactions, next_page} = split_list_by_page(results_plus_one)
next_page_params = next_page_params(next_page, transactions, params) next_page_params =
next_page |> next_page_params(transactions, params) |> delete_parameters_from_next_page_params()
conn conn
|> put_status(200) |> put_status(200)
@ -94,24 +108,26 @@ defmodule BlockScoutWeb.API.V2.AddressController do
def token_transfers(conn, %{"address_hash" => address_hash_string} = params) do def token_transfers(conn, %{"address_hash" => address_hash_string} = params) do
with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)} do with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)} do
options = options =
@transaction_with_tt_necessity_by_association @token_transfer_necessity_by_association
|> Keyword.merge(paging_options(params)) |> Keyword.merge(paging_options(params))
|> Keyword.merge(current_filter(params)) |> Keyword.merge(current_filter(params))
|> Keyword.merge(token_transfers_types_options(params))
results_plus_one = results_plus_one =
Chain.address_hash_to_token_transfers( Chain.address_hash_to_token_transfers_new(
address_hash, address_hash,
options options
) )
{transactions, next_page} = split_list_by_page(results_plus_one) {transactions, next_page} = split_list_by_page(results_plus_one)
next_page_params = next_page_params(next_page, transactions, params) next_page_params =
next_page |> next_page_params(transactions, params) |> delete_parameters_from_next_page_params()
conn conn
|> put_status(200) |> put_status(200)
|> put_view(TransactionView) |> put_view(TransactionView)
|> render(:transactions, %{transactions: transactions, next_page_params: next_page_params}) |> render(:token_transfers, %{token_transfers: transactions, next_page_params: next_page_params})
end end
end end
@ -134,7 +150,8 @@ defmodule BlockScoutWeb.API.V2.AddressController do
results_plus_one = Chain.address_to_internal_transactions(address_hash, full_options) results_plus_one = Chain.address_to_internal_transactions(address_hash, full_options)
{internal_transactions, next_page} = split_list_by_page(results_plus_one) {internal_transactions, next_page} = split_list_by_page(results_plus_one)
next_page_params = next_page_params(next_page, internal_transactions, params) next_page_params =
next_page |> next_page_params(internal_transactions, params) |> delete_parameters_from_next_page_params()
conn conn
|> put_status(200) |> put_status(200)
@ -156,7 +173,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do
{logs, next_page} = split_list_by_page(results_plus_one) {logs, next_page} = split_list_by_page(results_plus_one)
next_page_params = next_page_params(next_page, logs, params) next_page_params = next_page |> next_page_params(logs, params) |> delete_parameters_from_next_page_params()
conn conn
|> put_status(200) |> put_status(200)
@ -170,7 +187,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do
results_plus_one = Chain.address_to_logs(address_hash, paging_options(params)) results_plus_one = Chain.address_to_logs(address_hash, paging_options(params))
{logs, next_page} = split_list_by_page(results_plus_one) {logs, next_page} = split_list_by_page(results_plus_one)
next_page_params = next_page_params(next_page, logs, params) next_page_params = next_page |> next_page_params(logs, params) |> delete_parameters_from_next_page_params()
conn conn
|> put_status(200) |> put_status(200)
@ -197,7 +214,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do
results_plus_one = Chain.get_blocks_validated_by_address(full_options, address_hash) results_plus_one = Chain.get_blocks_validated_by_address(full_options, address_hash)
{blocks, next_page} = split_list_by_page(results_plus_one) {blocks, next_page} = split_list_by_page(results_plus_one)
next_page_params = next_page_params(next_page, blocks, params) next_page_params = next_page |> next_page_params(blocks, params) |> delete_parameters_from_next_page_params()
conn conn
|> put_status(200) |> put_status(200)
@ -207,14 +224,17 @@ defmodule BlockScoutWeb.API.V2.AddressController do
end end
def coin_balance_history(conn, %{"address_hash" => address_hash_string} = params) do def coin_balance_history(conn, %{"address_hash" => address_hash_string} = params) do
with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)} do with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)},
{:not_found, {:ok, _address}, _} <-
{:not_found, Chain.hash_to_address(address_hash), :empty_items_with_next_page_params} do
full_options = paging_options(params) full_options = paging_options(params)
results_plus_one = Chain.address_to_coin_balances(address_hash, full_options) results_plus_one = Chain.address_to_coin_balances(address_hash, full_options)
{coin_balances, next_page} = split_list_by_page(results_plus_one) {coin_balances, next_page} = split_list_by_page(results_plus_one)
next_page_params = next_page_params(next_page, coin_balances, params) next_page_params =
next_page |> next_page_params(coin_balances, params) |> delete_parameters_from_next_page_params()
conn conn
|> put_status(200) |> put_status(200)

@ -4,7 +4,7 @@ defmodule BlockScoutWeb.API.V2.BlockController do
import BlockScoutWeb.Chain, import BlockScoutWeb.Chain,
only: [next_page_params: 3, paging_options: 1, put_key_value_to_paging_options: 3, split_list_by_page: 1] only: [next_page_params: 3, paging_options: 1, put_key_value_to_paging_options: 3, split_list_by_page: 1]
import BlockScoutWeb.PagingHelper, only: [select_block_type: 1] import BlockScoutWeb.PagingHelper, only: [delete_parameters_from_next_page_params: 1, select_block_type: 1]
alias BlockScoutWeb.API.V2.TransactionView alias BlockScoutWeb.API.V2.TransactionView
alias BlockScoutWeb.BlockTransactionController alias BlockScoutWeb.BlockTransactionController
@ -51,7 +51,7 @@ defmodule BlockScoutWeb.API.V2.BlockController do
{blocks, next_page} = split_list_by_page(blocks_plus_one) {blocks, next_page} = split_list_by_page(blocks_plus_one)
next_page_params = next_page_params(next_page, blocks, params) next_page_params = next_page |> next_page_params(blocks, params) |> delete_parameters_from_next_page_params()
conn conn
|> put_status(200) |> put_status(200)
@ -70,7 +70,10 @@ defmodule BlockScoutWeb.API.V2.BlockController do
{transactions, next_page} = split_list_by_page(transactions_plus_one) {transactions, next_page} = split_list_by_page(transactions_plus_one)
next_page_params = next_page_params(next_page, transactions, params) next_page_params =
next_page
|> next_page_params(transactions, params)
|> delete_parameters_from_next_page_params()
conn conn
|> put_status(200) |> put_status(200)

@ -10,6 +10,11 @@ defmodule BlockScoutWeb.API.V2.FallbackController do
|> render(:message, %{message: "Invalid parameter(s)"}) |> render(:message, %{message: "Invalid parameter(s)"})
end end
def call(conn, {:not_found, _, :empty_items_with_next_page_params}) do
conn
|> json(%{"items" => [], "next_page_params" => nil})
end
def call(conn, {:not_found, _}) do def call(conn, {:not_found, _}) do
conn conn
|> put_status(:not_found) |> put_status(:not_found)

@ -29,7 +29,7 @@ defmodule BlockScoutWeb.API.V2.MainPageController do
[from_address: :smart_contract] => :optional, [from_address: :smart_contract] => :optional,
[to_address: :smart_contract] => :optional [to_address: :smart_contract] => :optional
}, },
paging_options: %PagingOptions{page_size: 5} paging_options: %PagingOptions{page_size: 6}
) )
conn conn

@ -50,11 +50,23 @@ defmodule BlockScoutWeb.API.V2.StatsController do
"gas_used_today" => Enum.at(transaction_stats, 0).gas_used, "gas_used_today" => Enum.at(transaction_stats, 0).gas_used,
"gas_prices" => gas_prices, "gas_prices" => gas_prices,
"static_gas_price" => gas_price, "static_gas_price" => gas_price,
"market_cap" => Helper.market_cap(market_cap_type, exchange_rate) "market_cap" => Helper.market_cap(market_cap_type, exchange_rate),
"network_utilization_percentage" => network_utilization_percentage()
} }
) )
end end
defp network_utilization_percentage do
{gas_used, gas_limit} =
Enum.reduce(Chain.list_blocks(), {Decimal.new(0), Decimal.new(0)}, fn block, {gas_used, gas_limit} ->
{Decimal.add(gas_used, block.gas_used), Decimal.add(gas_limit, block.gas_limit)}
end)
if Decimal.compare(gas_limit, 0) == :eq,
do: 0,
else: gas_used |> Decimal.div(gas_limit) |> Decimal.mult(100) |> Decimal.to_float()
end
def transactions_chart(conn, _params) do def transactions_chart(conn, _params) do
[{:history_size, history_size}] = [{:history_size, history_size}] =
Application.get_env(:block_scout_web, BlockScoutWeb.Chain.TransactionHistoryChartController, [{:history_size, 30}]) Application.get_env(:block_scout_web, BlockScoutWeb.Chain.TransactionHistoryChartController, [{:history_size, 30}])

@ -4,7 +4,14 @@ defmodule BlockScoutWeb.API.V2.TransactionController do
import BlockScoutWeb.Chain, only: [next_page_params: 3, paging_options: 1, split_list_by_page: 1] import BlockScoutWeb.Chain, only: [next_page_params: 3, paging_options: 1, split_list_by_page: 1]
import BlockScoutWeb.PagingHelper, import BlockScoutWeb.PagingHelper,
only: [paging_options: 2, filter_options: 1, method_filter_options: 1, type_filter_options: 1] only: [
delete_parameters_from_next_page_params: 1,
paging_options: 2,
filter_options: 2,
method_filter_options: 1,
token_transfers_types_options: 1,
type_filter_options: 1
]
alias Explorer.Chain alias Explorer.Chain
alias Explorer.Chain.Import alias Explorer.Chain.Import
@ -22,6 +29,15 @@ defmodule BlockScoutWeb.API.V2.TransactionController do
} }
@token_transfers_neccessity_by_association %{ @token_transfers_neccessity_by_association %{
[from_address: :smart_contract] => :optional,
[to_address: :smart_contract] => :optional,
[from_address: :names] => :optional,
[to_address: :names] => :optional,
from_address: :required,
to_address: :required
}
@token_transfers_in_tx_neccessity_by_association %{
[from_address: :smart_contract] => :optional, [from_address: :smart_contract] => :optional,
[to_address: :smart_contract] => :optional, [to_address: :smart_contract] => :optional,
[from_address: :names] => :optional, [from_address: :names] => :optional,
@ -51,7 +67,8 @@ defmodule BlockScoutWeb.API.V2.TransactionController do
transaction_hash, transaction_hash,
necessity_by_association: @transaction_necessity_by_association necessity_by_association: @transaction_necessity_by_association
)}, )},
preloaded <- Chain.preload_token_transfers(transaction, @token_transfers_neccessity_by_association, false) do preloaded <-
Chain.preload_token_transfers(transaction, @token_transfers_in_tx_neccessity_by_association, false) do
conn conn
|> put_status(200) |> put_status(200)
|> render(:transaction, %{transaction: preloaded}) |> render(:transaction, %{transaction: preloaded})
@ -59,24 +76,21 @@ defmodule BlockScoutWeb.API.V2.TransactionController do
end end
def transactions(conn, params) do def transactions(conn, params) do
filter_options = filter_options(params) filter_options = filter_options(params, :validated)
method_filter_options = method_filter_options(params)
type_filter_options = type_filter_options(params)
full_options = full_options =
Keyword.merge( [
[ necessity_by_association: @transaction_necessity_by_association
necessity_by_association: @transaction_necessity_by_association ]
], |> Keyword.merge(paging_options(params, filter_options))
paging_options(params, filter_options) |> Keyword.merge(method_filter_options(params))
) |> Keyword.merge(type_filter_options(params))
transactions_plus_one = transactions_plus_one = Chain.recent_transactions(full_options, filter_options)
Chain.recent_transactions(full_options, filter_options, method_filter_options, type_filter_options)
{transactions, next_page} = split_list_by_page(transactions_plus_one) {transactions, next_page} = split_list_by_page(transactions_plus_one)
next_page_params = next_page_params(next_page, transactions, params) next_page_params = next_page |> next_page_params(transactions, params) |> delete_parameters_from_next_page_params()
conn conn
|> put_status(200) |> put_status(200)
@ -146,18 +160,18 @@ defmodule BlockScoutWeb.API.V2.TransactionController do
def token_transfers(conn, %{"transaction_hash" => transaction_hash_string} = params) do def token_transfers(conn, %{"transaction_hash" => transaction_hash_string} = params) do
with {:format, {:ok, transaction_hash}} <- {:format, Chain.string_to_transaction_hash(transaction_hash_string)} do with {:format, {:ok, transaction_hash}} <- {:format, Chain.string_to_transaction_hash(transaction_hash_string)} do
full_options = full_options =
Keyword.merge( [necessity_by_association: @token_transfers_neccessity_by_association]
[ |> Keyword.merge(paging_options(params))
necessity_by_association: @token_transfers_neccessity_by_association |> Keyword.merge(token_transfers_types_options(params))
],
paging_options(params)
)
token_transfers_plus_one = Chain.transaction_to_token_transfers(transaction_hash, full_options) token_transfers_plus_one = Chain.transaction_to_token_transfers(transaction_hash, full_options)
{token_transfers, next_page} = split_list_by_page(token_transfers_plus_one) {token_transfers, next_page} = split_list_by_page(token_transfers_plus_one)
next_page_params = next_page_params(next_page, token_transfers, params) next_page_params =
next_page
|> next_page_params(token_transfers, params)
|> delete_parameters_from_next_page_params()
conn conn
|> put_status(200) |> put_status(200)
@ -177,7 +191,10 @@ defmodule BlockScoutWeb.API.V2.TransactionController do
{internal_transactions, next_page} = split_list_by_page(internal_transactions_plus_one) {internal_transactions, next_page} = split_list_by_page(internal_transactions_plus_one)
next_page_params = next_page_params(next_page, internal_transactions, params) next_page_params =
next_page
|> next_page_params(internal_transactions, params)
|> delete_parameters_from_next_page_params()
conn conn
|> put_status(200) |> put_status(200)
@ -207,7 +224,10 @@ defmodule BlockScoutWeb.API.V2.TransactionController do
{logs, next_page} = split_list_by_page(logs_plus_one) {logs, next_page} = split_list_by_page(logs_plus_one)
next_page_params = next_page_params(next_page, logs, params) next_page_params =
next_page
|> next_page_params(logs, params)
|> delete_parameters_from_next_page_params()
conn conn
|> put_status(200) |> put_status(200)

@ -43,8 +43,7 @@ defmodule BlockScoutWeb.TransactionStateController do
[from_address: :names] => :optional, [from_address: :names] => :optional,
[to_address: :names] => :optional, [to_address: :names] => :optional,
from_address: :required, from_address: :required,
to_address: :required, to_address: :required
token: :required
}, },
# we need to consider all token transfers in block to show whole state change of transaction # we need to consider all token transfers in block to show whole state change of transaction
paging_options: %PagingOptions{key: nil, page_size: nil} paging_options: %PagingOptions{key: nil, page_size: nil}

@ -33,8 +33,7 @@ defmodule BlockScoutWeb.TransactionTokenTransferController do
[from_address: :names] => :optional, [from_address: :names] => :optional,
[to_address: :names] => :optional, [to_address: :names] => :optional,
from_address: :required, from_address: :required,
to_address: :required, to_address: :required
token: :required
} }
], ],
paging_options(params) paging_options(params)

@ -9,6 +9,7 @@ defmodule BlockScoutWeb.PagingHelper do
@default_paging_options %PagingOptions{page_size: @page_size + 1} @default_paging_options %PagingOptions{page_size: @page_size + 1}
@allowed_filter_labels ["validated", "pending"] @allowed_filter_labels ["validated", "pending"]
@allowed_type_labels ["coin_transfer", "contract_call", "contract_creation", "token_transfer", "token_creation"] @allowed_type_labels ["coin_transfer", "contract_call", "contract_creation", "token_transfer", "token_creation"]
@allowed_token_transfer_type_labels ["ERC-20", "ERC-721", "ERC-1155"]
def paging_options(%{"block_number" => block_number_string, "index" => index_string}, [:validated | _]) do def paging_options(%{"block_number" => block_number_string, "index" => index_string}, [:validated | _]) do
with {block_number, ""} <- Integer.parse(block_number_string), with {block_number, ""} <- Integer.parse(block_number_string),
@ -32,23 +33,34 @@ defmodule BlockScoutWeb.PagingHelper do
def paging_options(_params, _filter), do: [paging_options: @default_paging_options] def paging_options(_params, _filter), do: [paging_options: @default_paging_options]
def filter_options(%{"filter" => filter}) do def token_transfers_types_options(%{"type" => filters}) do
parse_filter(filter, @allowed_filter_labels) [
token_type: filters |> String.upcase() |> parse_filter(@allowed_token_transfer_type_labels)
]
end
def token_transfers_types_options(_), do: [token_type: []]
# sobelow_skip ["DOS.StringToAtom"]
def filter_options(%{"filter" => filter}, fallback) do
filter = filter |> parse_filter(@allowed_filter_labels) |> Enum.map(&String.to_atom/1)
if(filter == [], do: [fallback], else: filter)
end end
def filter_options(_params), do: [] def filter_options(_params, fallback), do: [fallback]
# sobelow_skip ["DOS.StringToAtom"]
def type_filter_options(%{"type" => type}) do def type_filter_options(%{"type" => type}) do
parse_filter(type, @allowed_type_labels) [type: type |> parse_filter(@allowed_type_labels) |> Enum.map(&String.to_atom/1)]
end end
def type_filter_options(_params), do: [] def type_filter_options(_params), do: [type: []]
def method_filter_options(%{"method" => method}) do def method_filter_options(%{"method" => method}) do
parse_method_filter(method) [method: parse_method_filter(method)]
end end
def method_filter_options(_params), do: [] def method_filter_options(_params), do: [method: []]
def parse_filter("[" <> filter, allowed_labels) do def parse_filter("[" <> filter, allowed_labels) do
filter filter
@ -56,13 +68,11 @@ defmodule BlockScoutWeb.PagingHelper do
|> parse_filter(allowed_labels) |> parse_filter(allowed_labels)
end end
# sobelow_skip ["DOS.StringToAtom"]
def parse_filter(filter, allowed_labels) when is_binary(filter) do def parse_filter(filter, allowed_labels) when is_binary(filter) do
filter filter
|> String.split(",") |> String.split(",")
|> Enum.filter(fn label -> Enum.member?(allowed_labels, label) end) |> Enum.filter(fn label -> Enum.member?(allowed_labels, label) end)
|> Enum.uniq() |> Enum.uniq()
|> Enum.map(&String.to_atom/1)
end end
def parse_method_filter("[" <> filter) do def parse_method_filter("[" <> filter) do
@ -114,4 +124,16 @@ defmodule BlockScoutWeb.PagingHelper do
}, },
block_type: "Block" block_type: "Block"
] ]
def delete_parameters_from_next_page_params(params) when is_map(params) do
params
|> Map.delete("block_hash_or_number")
|> Map.delete("transaction_hash")
|> Map.delete("address_hash")
|> Map.delete("type")
|> Map.delete("method")
|> Map.delete("filter")
end
def delete_parameters_from_next_page_params(_), do: nil
end end

@ -10,6 +10,10 @@ defmodule BlockScoutWeb.API.V2.Helper do
import BlockScoutWeb.Account.AuthController, only: [current_user: 1] import BlockScoutWeb.Account.AuthController, only: [current_user: 1]
import BlockScoutWeb.Models.GetAddressTags, only: [get_address_tags: 2, get_tags_on_address: 1] import BlockScoutWeb.Models.GetAddressTags, only: [get_address_tags: 2, get_tags_on_address: 1]
def address_with_info(_, _, nil) do
nil
end
def address_with_info(conn, address, address_hash) do def address_with_info(conn, address, address_hash) do
%{ %{
personal_tags: private_tags, personal_tags: private_tags,
@ -27,7 +31,7 @@ defmodule BlockScoutWeb.API.V2.Helper do
def address_with_info(%Address{} = address, _address_hash) do def address_with_info(%Address{} = address, _address_hash) do
%{ %{
"hash" => to_string(address), "hash" => Address.checksum(address),
"is_contract" => is_smart_contract(address), "is_contract" => is_smart_contract(address),
"name" => address_name(address), "name" => address_name(address),
"implementation_name" => implementation_name(address), "implementation_name" => implementation_name(address),
@ -39,8 +43,18 @@ defmodule BlockScoutWeb.API.V2.Helper do
address_with_info(nil, address_hash) address_with_info(nil, address_hash)
end end
def address_with_info(nil, nil) do
nil
end
def address_with_info(nil, address_hash) do def address_with_info(nil, address_hash) do
%{"hash" => address_hash, "is_contract" => false, "name" => nil, "implementation_name" => nil, "is_verified" => nil} %{
"hash" => Address.checksum(address_hash),
"is_contract" => false,
"name" => nil,
"implementation_name" => nil,
"is_verified" => nil
}
end end
def address_name(%Address{names: [_ | _] = address_names}) do def address_name(%Address{names: [_ | _] = address_names}) do

@ -1,12 +1,14 @@
defmodule BlockScoutWeb.API.V2.TokenView do defmodule BlockScoutWeb.API.V2.TokenView do
alias Explorer.Chain.Address
def render("token.json", %{token: token}) do def render("token.json", %{token: token}) do
%{ %{
"address" => token.contract_address_hash, "address" => Address.checksum(token.contract_address_hash),
"symbol" => token.symbol, "symbol" => token.symbol,
"name" => token.name, "name" => token.name,
"decimals" => token.decimals, "decimals" => token.decimals,
"type" => token.type, "type" => token.type,
"holders" => to_string(token.holder_count), "holders" => token.holder_count && to_string(token.holder_count),
"exchange_rate" => token.usd_value && to_string(token.usd_value) "exchange_rate" => token.usd_value && to_string(token.usd_value)
} }
end end

@ -43,8 +43,8 @@ defmodule BlockScoutWeb.API.V2.TransactionView do
%{"method_id" => method_id, "method_call" => text, "parameters" => prepare_method_mapping(mapping)} %{"method_id" => method_id, "method_call" => text, "parameters" => prepare_method_mapping(mapping)}
end end
def render("revert_reason.json", %{raw: raw, decoded: decoded}) do def render("revert_reason.json", %{raw: raw}) do
%{"raw" => raw, "decoded" => decoded} %{"raw" => raw}
end end
def render("token_transfers.json", %{token_transfers: token_transfers, next_page_params: next_page_params, conn: conn}) do def render("token_transfers.json", %{token_transfers: token_transfers, next_page_params: next_page_params, conn: conn}) do
@ -88,7 +88,8 @@ defmodule BlockScoutWeb.API.V2.TransactionView do
"to" => Helper.address_with_info(conn, token_transfer.to_address, token_transfer.to_address_hash), "to" => Helper.address_with_info(conn, token_transfer.to_address, token_transfer.to_address_hash),
"total" => prepare_token_transfer_total(token_transfer), "total" => prepare_token_transfer_total(token_transfer),
"token" => TokenView.render("token.json", %{token: Market.add_price(token_transfer.token)}), "token" => TokenView.render("token.json", %{token: Market.add_price(token_transfer.token)}),
"type" => Chain.get_token_transfer_type(token_transfer) "type" => Chain.get_token_transfer_type(token_transfer),
"timestamp" => block_timestamp(token_transfer.block)
} }
end end
@ -204,7 +205,7 @@ defmodule BlockScoutWeb.API.V2.TransactionView do
"result" => status, "result" => status,
"status" => transaction.status, "status" => transaction.status,
"block" => transaction.block_number, "block" => transaction.block_number,
"timestamp" => transaction.block && transaction.block.timestamp, "timestamp" => block_timestamp(transaction.block),
"from" => Helper.address_with_info(conn, transaction.from_address, transaction.from_address_hash), "from" => Helper.address_with_info(conn, transaction.from_address, transaction.from_address_hash),
"to" => Helper.address_with_info(conn, transaction.to_address, transaction.to_address_hash), "to" => Helper.address_with_info(conn, transaction.to_address, transaction.to_address_hash),
"created_contract" => "created_contract" =>
@ -286,8 +287,7 @@ defmodule BlockScoutWeb.API.V2.TransactionView do
_ -> _ ->
hex = TransactionView.get_pure_transaction_revert_reason(transaction) hex = TransactionView.get_pure_transaction_revert_reason(transaction)
utf8 = TransactionView.decoded_revert_reason(transaction) render(__MODULE__, "revert_reason.json", raw: hex)
render(__MODULE__, "revert_reason.json", raw: hex, decoded: utf8)
end end
end end
end end
@ -444,4 +444,7 @@ defmodule BlockScoutWeb.API.V2.TransactionView do
types types
end end
end end
defp block_timestamp(%Block{} = block), do: block.timestamp
defp block_timestamp(_), do: nil
end end

@ -0,0 +1,329 @@
defmodule BlockScoutWeb.API.V2.BlockControllerTest do
use BlockScoutWeb.ConnCase
alias Explorer.Chain.{Address, Block, Transaction}
setup do
Supervisor.terminate_child(Explorer.Supervisor, Explorer.Chain.Cache.Blocks.child_id())
Supervisor.restart_child(Explorer.Supervisor, Explorer.Chain.Cache.Blocks.child_id())
Supervisor.terminate_child(Explorer.Supervisor, Explorer.Chain.Cache.Uncles.child_id())
Supervisor.restart_child(Explorer.Supervisor, Explorer.Chain.Cache.Uncles.child_id())
:ok
end
describe "/blocks" do
test "empty lists", %{conn: conn} do
request = get(conn, "/api/v2/blocks")
assert response = json_response(request, 200)
assert response["items"] == []
assert response["next_page_params"] == nil
request = get(conn, "/api/v2/blocks", %{"type" => "uncle"})
assert response = json_response(request, 200)
assert response["items"] == []
assert response["next_page_params"] == nil
request = get(conn, "/api/v2/blocks", %{"type" => "reorg"})
assert response = json_response(request, 200)
assert response["items"] == []
assert response["next_page_params"] == nil
request = get(conn, "/api/v2/blocks", %{"type" => "block"})
assert response = json_response(request, 200)
assert response["items"] == []
assert response["next_page_params"] == nil
end
test "get block", %{conn: conn} do
block = insert(:block)
request = get(conn, "/api/v2/blocks")
assert response = json_response(request, 200)
assert Enum.count(response["items"]) == 1
assert response["next_page_params"] == nil
compare_item(block, Enum.at(response["items"], 0))
end
test "type=block returns only consensus blocks", %{conn: conn} do
blocks =
4
|> insert_list(:block)
|> Enum.reverse()
for index <- 0..3 do
uncle = insert(:block, consensus: false)
insert(:block_second_degree_relation, uncle_hash: uncle.hash, nephew: Enum.at(blocks, index))
end
2
|> insert_list(:block, consensus: false)
request = get(conn, "/api/v2/blocks", %{"type" => "block"})
assert response = json_response(request, 200)
assert Enum.count(response["items"]) == 4
assert response["next_page_params"] == nil
for index <- 0..3 do
compare_item(Enum.at(blocks, index), Enum.at(response["items"], index))
end
end
test "type=block can paginate", %{conn: conn} do
blocks =
51
|> insert_list(:block)
filter = %{"type" => "block"}
request = get(conn, "/api/v2/blocks", filter)
assert response = json_response(request, 200)
request_2nd_page = get(conn, "/api/v2/blocks", Map.merge(response["next_page_params"], filter))
assert response_2nd_page = json_response(request_2nd_page, 200)
check_paginated_response(response, response_2nd_page, blocks)
end
test "type=reorg returns only non consensus blocks", %{conn: conn} do
blocks =
5
|> insert_list(:block)
for index <- 0..3 do
uncle = insert(:block, consensus: false)
insert(:block_second_degree_relation, uncle_hash: uncle.hash, nephew: Enum.at(blocks, index))
end
reorgs =
4
|> insert_list(:block, consensus: false)
|> Enum.reverse()
request = get(conn, "/api/v2/blocks", %{"type" => "reorg"})
assert response = json_response(request, 200)
assert Enum.count(response["items"]) == 4
assert response["next_page_params"] == nil
for index <- 0..3 do
compare_item(Enum.at(reorgs, index), Enum.at(response["items"], index))
end
end
test "type=reorg can paginate", %{conn: conn} do
reorgs =
51
|> insert_list(:block, consensus: false)
filter = %{"type" => "reorg"}
request = get(conn, "/api/v2/blocks", filter)
assert response = json_response(request, 200)
request_2nd_page = get(conn, "/api/v2/blocks", Map.merge(response["next_page_params"], filter))
assert response_2nd_page = json_response(request_2nd_page, 200)
check_paginated_response(response, response_2nd_page, reorgs)
end
test "type=uncle returns only uncle blocks", %{conn: conn} do
blocks =
4
|> insert_list(:block)
|> Enum.reverse()
uncles =
for index <- 0..3 do
uncle = insert(:block, consensus: false)
insert(:block_second_degree_relation, uncle_hash: uncle.hash, nephew: Enum.at(blocks, index))
uncle
end
|> Enum.reverse()
4
|> insert_list(:block, consensus: false)
request = get(conn, "/api/v2/blocks", %{"type" => "uncle"})
assert response = json_response(request, 200)
assert Enum.count(response["items"]) == 4
assert response["next_page_params"] == nil
for index <- 0..3 do
compare_item(Enum.at(uncles, index), Enum.at(response["items"], index))
end
end
test "type=uncle can paginate", %{conn: conn} do
blocks =
51
|> insert_list(:block)
uncles =
for index <- 0..50 do
uncle = insert(:block, consensus: false)
insert(:block_second_degree_relation, uncle_hash: uncle.hash, nephew: Enum.at(blocks, index))
uncle
end
filter = %{"type" => "uncle"}
request = get(conn, "/api/v2/blocks", filter)
assert response = json_response(request, 200)
request_2nd_page = get(conn, "/api/v2/blocks", Map.merge(response["next_page_params"], filter))
assert response_2nd_page = json_response(request_2nd_page, 200)
check_paginated_response(response, response_2nd_page, uncles)
end
end
describe "/blocks/{block_hash_or_number}" do
test "return 422 on invalid parameter", %{conn: conn} do
request_1 = get(conn, "/api/v2/blocks/0x123123")
assert %{"message" => "Invalid hash"} = json_response(request_1, 422)
request_2 = get(conn, "/api/v2/blocks/123qwe")
assert %{"message" => "Invalid number"} = json_response(request_2, 422)
end
test "return 404 on non existing block", %{conn: conn} do
block = build(:block)
request_1 = get(conn, "/api/v2/blocks/#{block.number}")
assert %{"message" => "Not found"} = json_response(request_1, 404)
request_2 = get(conn, "/api/v2/blocks/#{block.hash}")
assert %{"message" => "Not found"} = json_response(request_2, 404)
end
test "get the same blocks by hash and number", %{conn: conn} do
block = insert(:block)
request_1 = get(conn, "/api/v2/blocks/#{block.number}")
assert response_1 = json_response(request_1, 200)
request_2 = get(conn, "/api/v2/blocks/#{block.hash}")
assert response_2 = json_response(request_2, 200)
assert response_2 == response_1
compare_item(block, response_2)
end
end
describe "/blocks/{block_hash_or_number}/transactions" do
test "return 422 on invalid parameter", %{conn: conn} do
request_1 = get(conn, "/api/v2/blocks/0x123123/transactions")
assert %{"message" => "Invalid hash"} = json_response(request_1, 422)
request_2 = get(conn, "/api/v2/blocks/123qwe/transactions")
assert %{"message" => "Invalid number"} = json_response(request_2, 422)
end
test "return 404 on non existing block", %{conn: conn} do
block = build(:block)
request_1 = get(conn, "/api/v2/blocks/#{block.number}/transactions")
assert %{"message" => "Not found"} = json_response(request_1, 404)
request_2 = get(conn, "/api/v2/blocks/#{block.hash}/transactions")
assert %{"message" => "Not found"} = json_response(request_2, 404)
end
test "get empty list", %{conn: conn} do
block = insert(:block)
request = get(conn, "/api/v2/blocks/#{block.number}/transactions")
assert response = json_response(request, 200)
assert response["items"] == []
assert response["next_page_params"] == nil
request = get(conn, "/api/v2/blocks/#{block.hash}/transactions")
assert response = json_response(request, 200)
assert response["items"] == []
assert response["next_page_params"] == nil
end
test "get relevant tx", %{conn: conn} do
10
|> insert_list(:transaction)
|> with_block()
block = insert(:block)
tx =
:transaction
|> insert()
|> with_block(block)
request = get(conn, "/api/v2/blocks/#{block.number}/transactions")
assert response = json_response(request, 200)
assert Enum.count(response["items"]) == 1
assert response["next_page_params"] == nil
compare_item(tx, Enum.at(response["items"], 0))
request = get(conn, "/api/v2/blocks/#{block.hash}/transactions")
assert response_1 = json_response(request, 200)
assert response_1 == response
end
test "get txs with working next_page_params", %{conn: conn} do
2
|> insert_list(:transaction)
|> with_block()
block = insert(:block)
txs =
51
|> insert_list(:transaction)
|> with_block(block)
|> Enum.reverse()
request = get(conn, "/api/v2/blocks/#{block.number}/transactions")
assert response = json_response(request, 200)
request_2nd_page = get(conn, "/api/v2/blocks/#{block.number}/transactions", response["next_page_params"])
assert response_2nd_page = json_response(request_2nd_page, 200)
check_paginated_response(response, response_2nd_page, txs)
request_1 = get(conn, "/api/v2/blocks/#{block.hash}/transactions")
assert response_1 = json_response(request_1, 200)
assert response_1 == response
request_2 = get(conn, "/api/v2/blocks/#{block.hash}/transactions", response_1["next_page_params"])
assert response_2 = json_response(request_2, 200)
assert response_2 == response_2nd_page
end
end
defp compare_item(%Block{} = block, json) do
assert to_string(block.hash) == json["hash"]
assert block.number == json["height"]
end
defp compare_item(%Transaction{} = transaction, json) do
assert to_string(transaction.hash) == json["hash"]
assert transaction.block_number == json["block"]
assert to_string(transaction.value.value) == json["value"]
assert Address.checksum(transaction.from_address_hash) == json["from"]["hash"]
assert Address.checksum(transaction.to_address_hash) == json["to"]["hash"]
end
defp check_paginated_response(first_page_resp, second_page_resp, list) do
assert Enum.count(first_page_resp["items"]) == 50
assert first_page_resp["next_page_params"] != nil
compare_item(Enum.at(list, 50), Enum.at(first_page_resp["items"], 0))
compare_item(Enum.at(list, 1), Enum.at(first_page_resp["items"], 49))
assert Enum.count(second_page_resp["items"]) == 1
assert second_page_resp["next_page_params"] == nil
compare_item(Enum.at(list, 0), Enum.at(second_page_resp["items"], 0))
end
end

@ -0,0 +1,22 @@
defmodule BlockScoutWeb.API.V2.ConfigControllerTest do
use BlockScoutWeb.ConnCase
describe "/config/json-rpc-url" do
test "get json rps url if set", %{conn: conn} do
url = "http://rps.url:1234/v1"
Application.put_env(:block_scout_web, :json_rpc, url)
request = get(conn, "/api/v2/config/json-rpc-url")
assert %{"json_rpc_url" => ^url} = json_response(request, 200)
end
test "get empty json rps url if not set", %{conn: conn} do
Application.put_env(:block_scout_web, :json_rpc, nil)
request = get(conn, "/api/v2/config/json-rpc-url")
assert %{"json_rpc_url" => nil} = json_response(request, 200)
end
end
end

@ -0,0 +1,65 @@
defmodule BlockScoutWeb.API.V2.MainPageControllerTest do
use BlockScoutWeb.ConnCase
alias Explorer.Chain.{Address, Block, Transaction}
setup do
Supervisor.terminate_child(Explorer.Supervisor, Explorer.Chain.Cache.Blocks.child_id())
Supervisor.restart_child(Explorer.Supervisor, Explorer.Chain.Cache.Blocks.child_id())
Supervisor.terminate_child(Explorer.Supervisor, Explorer.Chain.Cache.TransactionsApiV2.child_id())
Supervisor.restart_child(Explorer.Supervisor, Explorer.Chain.Cache.TransactionsApiV2.child_id())
:ok
end
describe "/main-page/blocks" do
test "get empty list when no blocks", %{conn: conn} do
request = get(conn, "/api/v2/main-page/blocks")
assert [] = json_response(request, 200)
end
test "get last 4 blocks", %{conn: conn} do
blocks = insert_list(10, :block) |> Enum.take(-4) |> Enum.reverse()
request = get(conn, "/api/v2/main-page/blocks")
assert response = json_response(request, 200)
assert Enum.count(response) == 4
for i <- 0..3 do
compare_item(Enum.at(blocks, i), Enum.at(response, i))
end
end
end
describe "/main-page/transactions" do
test "get empty list when no txs", %{conn: conn} do
request = get(conn, "/api/v2/main-page/transactions")
assert [] = json_response(request, 200)
end
test "get last 5 txs", %{conn: conn} do
txs = insert_list(10, :transaction) |> with_block() |> Enum.take(-6) |> Enum.reverse()
request = get(conn, "/api/v2/main-page/transactions")
assert response = json_response(request, 200)
assert Enum.count(response) == 6
for i <- 0..5 do
compare_item(Enum.at(txs, i), Enum.at(response, i))
end
end
end
defp compare_item(%Block{} = block, json) do
assert to_string(block.hash) == json["hash"]
assert block.number == json["height"]
end
defp compare_item(%Transaction{} = transaction, json) do
assert to_string(transaction.hash) == json["hash"]
assert transaction.block_number == json["block"]
assert to_string(transaction.value.value) == json["value"]
assert Address.checksum(transaction.from_address_hash) == json["from"]["hash"]
assert Address.checksum(transaction.to_address_hash) == json["to"]["hash"]
end
end

@ -0,0 +1,147 @@
defmodule BlockScoutWeb.API.V2.SearchControllerTest do
use BlockScoutWeb.ConnCase
alias Explorer.Chain.Address
setup do
insert(:block)
insert(:unique_smart_contract)
insert(:unique_token)
insert(:transaction)
address = insert(:address)
insert(:unique_address_name, address: address)
:ok
end
describe "/search" do
test "search block", %{conn: conn} do
block = insert(:block)
request = get(conn, "/api/v2/search?q=#{block.hash}")
assert response = json_response(request, 200)
assert Enum.count(response["items"]) == 1
assert response["next_page_params"] == nil
item = Enum.at(response["items"], 0)
assert item["type"] == "block"
assert item["block_number"] == block.number
assert item["block_hash"] == to_string(block.hash)
assert item["url"] =~ to_string(block.hash)
request = get(conn, "/api/v2/search?q=#{block.number}")
assert response = json_response(request, 200)
assert Enum.count(response["items"]) == 1
assert response["next_page_params"] == nil
item = Enum.at(response["items"], 0)
assert item["type"] == "block"
assert item["block_number"] == block.number
assert item["block_hash"] == to_string(block.hash)
assert item["url"] =~ to_string(block.hash)
end
test "search address", %{conn: conn} do
address = insert(:address)
name = insert(:unique_address_name, address: address)
request = get(conn, "/api/v2/search?q=#{address.hash}")
assert response = json_response(request, 200)
assert Enum.count(response["items"]) == 1
assert response["next_page_params"] == nil
item = Enum.at(response["items"], 0)
assert item["type"] == "address"
assert item["name"] == name.name
assert item["address"] == Address.checksum(address.hash)
assert item["url"] =~ Address.checksum(address.hash)
end
test "search contract", %{conn: conn} do
contract = insert(:unique_smart_contract)
request = get(conn, "/api/v2/search?q=#{contract.name}")
assert response = json_response(request, 200)
assert Enum.count(response["items"]) == 1
assert response["next_page_params"] == nil
item = Enum.at(response["items"], 0)
assert item["type"] == "contract"
assert item["name"] == contract.name
assert item["address"] == Address.checksum(contract.address_hash)
assert item["url"] =~ Address.checksum(contract.address_hash)
end
test "check pagination", %{conn: conn} do
name = "contract"
contracts = insert_list(51, :smart_contract, name: name)
request = get(conn, "/api/v2/search?q=#{name}")
assert response = json_response(request, 200)
assert Enum.count(response["items"]) == 50
assert response["next_page_params"] != nil
item = Enum.at(response["items"], 0)
assert item["type"] == "contract"
assert item["name"] == name
request_2 = get(conn, "/api/v2/search", response["next_page_params"])
assert response_2 = json_response(request_2, 200)
assert Enum.count(response_2["items"]) == 1
assert response_2["next_page_params"] == nil
item = Enum.at(response_2["items"], 0)
assert item["type"] == "contract"
assert item["name"] == name
assert item not in response["items"]
end
test "search token", %{conn: conn} do
token = insert(:unique_token)
request = get(conn, "/api/v2/search?q=#{token.name}")
assert response = json_response(request, 200)
assert Enum.count(response["items"]) == 1
assert response["next_page_params"] == nil
item = Enum.at(response["items"], 0)
assert item["type"] == "token"
assert item["name"] == token.name
assert item["symbol"] == token.symbol
assert item["address"] == Address.checksum(token.contract_address_hash)
assert item["token_url"] =~ Address.checksum(token.contract_address_hash)
assert item["address_url"] =~ Address.checksum(token.contract_address_hash)
end
test "search transaction", %{conn: conn} do
tx = insert(:transaction)
request = get(conn, "/api/v2/search?q=#{tx.hash}")
assert response = json_response(request, 200)
assert Enum.count(response["items"]) == 1
assert response["next_page_params"] == nil
item = Enum.at(response["items"], 0)
assert item["type"] == "transaction"
assert item["tx_hash"] == to_string(tx.hash)
assert item["url"] =~ to_string(tx.hash)
end
end
end

@ -0,0 +1,57 @@
defmodule BlockScoutWeb.API.V2.StatsControllerTest do
use BlockScoutWeb.ConnCase
alias Explorer.Counters.{AddressesCounter, AverageBlockTime}
describe "/stats" do
setup do
start_supervised!(AddressesCounter)
start_supervised!(AverageBlockTime)
Application.put_env(:explorer, AverageBlockTime, enabled: true)
on_exit(fn ->
Application.put_env(:explorer, AverageBlockTime, enabled: false)
end)
:ok
end
test "get all fields", %{conn: conn} do
request = get(conn, "/api/v2/stats")
assert response = json_response(request, 200)
assert Map.has_key?(response, "total_blocks")
assert Map.has_key?(response, "total_addresses")
assert Map.has_key?(response, "total_transactions")
assert Map.has_key?(response, "average_block_time")
assert Map.has_key?(response, "coin_price")
assert Map.has_key?(response, "total_gas_used")
assert Map.has_key?(response, "transactions_today")
assert Map.has_key?(response, "gas_used_today")
assert Map.has_key?(response, "gas_prices")
assert Map.has_key?(response, "static_gas_price")
assert Map.has_key?(response, "market_cap")
assert Map.has_key?(response, "network_utilization_percentage")
end
end
describe "/stats/charts/market" do
test "get empty data", %{conn: conn} do
request = get(conn, "/api/v2/stats/charts/market")
assert response = json_response(request, 200)
assert response["chart_data"] == []
assert response["available_supply"] == 0
end
end
describe "/stats/charts/transactions" do
test "get empty data", %{conn: conn} do
request = get(conn, "/api/v2/stats/charts/transactions")
assert response = json_response(request, 200)
assert response["chart_data"] == []
end
end
end

@ -0,0 +1,582 @@
defmodule BlockScoutWeb.API.V2.TransactionControllerTest do
use BlockScoutWeb.ConnCase
alias Explorer.Chain.{Address, InternalTransaction, Log, TokenTransfer, Transaction}
setup do
Supervisor.terminate_child(Explorer.Supervisor, Explorer.Chain.Cache.TransactionsApiV2.child_id())
Supervisor.restart_child(Explorer.Supervisor, Explorer.Chain.Cache.TransactionsApiV2.child_id())
:ok
end
describe "/transactions" do
test "empty list", %{conn: conn} do
request = get(conn, "/api/v2/transactions")
assert response = json_response(request, 200)
assert response["items"] == []
assert response["next_page_params"] == nil
end
test "non empty list", %{conn: conn} do
1
|> insert_list(:transaction)
|> with_block()
request = get(conn, "/api/v2/transactions")
assert response = json_response(request, 200)
assert Enum.count(response["items"]) == 1
assert response["next_page_params"] == nil
end
test "txs with next_page_params", %{conn: conn} do
txs =
51
|> insert_list(:transaction)
|> with_block()
request = get(conn, "/api/v2/transactions")
assert response = json_response(request, 200)
request_2nd_page = get(conn, "/api/v2/transactions", response["next_page_params"])
assert response_2nd_page = json_response(request_2nd_page, 200)
check_paginated_response(response, response_2nd_page, txs)
end
test "filter=pending", %{conn: conn} do
pending_txs =
51
|> insert_list(:transaction)
_mined_txs =
51
|> insert_list(:transaction)
|> with_block()
filter = %{"filter" => "pending"}
request = get(conn, "/api/v2/transactions", filter)
assert response = json_response(request, 200)
request_2nd_page = get(conn, "/api/v2/transactions", Map.merge(response["next_page_params"], filter))
assert response_2nd_page = json_response(request_2nd_page, 200)
check_paginated_response(response, response_2nd_page, pending_txs)
end
test "filter=validated", %{conn: conn} do
_pending_txs =
51
|> insert_list(:transaction)
mined_txs =
51
|> insert_list(:transaction)
|> with_block()
filter = %{"filter" => "validated"}
request = get(conn, "/api/v2/transactions", filter)
assert response = json_response(request, 200)
request_2nd_page = get(conn, "/api/v2/transactions", Map.merge(response["next_page_params"], filter))
assert response_2nd_page = json_response(request_2nd_page, 200)
check_paginated_response(response, response_2nd_page, mined_txs)
end
end
describe "/transactions/{tx_hash}" do
test "return 404 on non existing tx", %{conn: conn} do
tx = build(:transaction)
request = get(conn, "/api/v2/transactions/#{to_string(tx.hash)}")
assert %{"message" => "Not found"} = json_response(request, 404)
end
test "return 422 on invalid tx hash", %{conn: conn} do
request = get(conn, "/api/v2/transactions/0x")
assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422)
end
test "return existing tx", %{conn: conn} do
tx =
:transaction
|> insert()
|> with_block()
request = get(conn, "/api/v2/transactions/" <> to_string(tx.hash))
assert response = json_response(request, 200)
compare_item(tx, response)
end
end
describe "/transactions/{tx_hash}/internal-transactions" do
test "return empty list on non existing tx", %{conn: conn} do
tx = build(:transaction)
request = get(conn, "/api/v2/transactions/#{to_string(tx.hash)}/internal-transactions")
assert response = json_response(request, 200)
assert response["items"] == []
assert response["next_page_params"] == nil
end
test "return 422 on invalid tx hash", %{conn: conn} do
request = get(conn, "/api/v2/transactions/0x/internal-transactions")
assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422)
end
test "return empty list", %{conn: conn} do
tx =
:transaction
|> insert()
|> with_block()
request = get(conn, "/api/v2/transactions/#{to_string(tx.hash)}/internal-transactions")
assert response = json_response(request, 200)
assert response["items"] == []
assert response["next_page_params"] == nil
end
test "return relevant internal transaction", %{conn: conn} do
tx =
:transaction
|> insert()
|> with_block()
insert(:internal_transaction,
transaction: tx,
index: 0,
block_number: tx.block_number,
transaction_index: tx.index,
block_hash: tx.block_hash,
block_index: 0
)
internal_tx =
insert(:internal_transaction,
transaction: tx,
index: 1,
block_number: tx.block_number,
transaction_index: tx.index,
block_hash: tx.block_hash,
block_index: 1
)
tx_1 =
:transaction
|> insert()
|> with_block()
0..5
|> Enum.map(fn index ->
insert(:internal_transaction,
transaction: tx_1,
index: index,
block_number: tx_1.block_number,
transaction_index: tx_1.index,
block_hash: tx_1.block_hash,
block_index: index
)
end)
request = get(conn, "/api/v2/transactions/#{to_string(tx.hash)}/internal-transactions")
assert response = json_response(request, 200)
assert Enum.count(response["items"]) == 1
assert response["next_page_params"] == nil
compare_item(internal_tx, Enum.at(response["items"], 0))
end
test "return list with next_page_params", %{conn: conn} do
tx =
:transaction
|> insert()
|> with_block()
insert(:internal_transaction,
transaction: tx,
index: 0,
block_number: tx.block_number,
transaction_index: tx.index,
block_hash: tx.block_hash,
block_index: 0
)
internal_txs =
51..1
|> Enum.map(fn index ->
insert(:internal_transaction,
transaction: tx,
index: index,
block_number: tx.block_number,
transaction_index: tx.index,
block_hash: tx.block_hash,
block_index: index
)
end)
request = get(conn, "/api/v2/transactions/#{to_string(tx.hash)}/internal-transactions")
assert response = json_response(request, 200)
request_2nd_page =
get(conn, "/api/v2/transactions/#{to_string(tx.hash)}/internal-transactions", response["next_page_params"])
assert response_2nd_page = json_response(request_2nd_page, 200)
check_paginated_response(response, response_2nd_page, internal_txs)
end
end
describe "/transactions/{tx_hash}/logs" do
test "return empty list on non existing tx", %{conn: conn} do
tx = build(:transaction)
request = get(conn, "/api/v2/transactions/#{to_string(tx.hash)}/logs")
assert response = json_response(request, 200)
assert response["items"] == []
assert response["next_page_params"] == nil
end
test "return 422 on invalid tx hash", %{conn: conn} do
request = get(conn, "/api/v2/transactions/0x/logs")
assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422)
end
test "return empty list", %{conn: conn} do
tx =
:transaction
|> insert()
|> with_block()
request = get(conn, "/api/v2/transactions/#{to_string(tx.hash)}/logs")
assert response = json_response(request, 200)
assert response["items"] == []
assert response["next_page_params"] == nil
end
test "return relevant log", %{conn: conn} do
tx =
:transaction
|> insert()
|> with_block()
log =
insert(:log,
transaction: tx,
index: 1,
block: tx.block,
block_number: tx.block_number
)
tx_1 =
:transaction
|> insert()
|> with_block()
0..5
|> Enum.map(fn index ->
insert(:log,
transaction: tx_1,
index: index,
block: tx_1.block,
block_number: tx_1.block_number
)
end)
request = get(conn, "/api/v2/transactions/#{to_string(tx.hash)}/logs")
assert response = json_response(request, 200)
assert Enum.count(response["items"]) == 1
assert response["next_page_params"] == nil
compare_item(log, Enum.at(response["items"], 0))
end
test "return list with next_page_params", %{conn: conn} do
tx =
:transaction
|> insert()
|> with_block()
logs =
50..0
|> Enum.map(fn index ->
insert(:log,
transaction: tx,
index: index,
block: tx.block,
block_number: tx.block_number
)
end)
request = get(conn, "/api/v2/transactions/#{to_string(tx.hash)}/logs")
assert response = json_response(request, 200)
request_2nd_page = get(conn, "/api/v2/transactions/#{to_string(tx.hash)}/logs", response["next_page_params"])
assert response_2nd_page = json_response(request_2nd_page, 200)
check_paginated_response(response, response_2nd_page, logs)
end
end
describe "/transactions/{tx_hash}/token-transfers" do
test "return empty list on non existing tx", %{conn: conn} do
tx = build(:transaction)
request = get(conn, "/api/v2/transactions/#{to_string(tx.hash)}/token-transfers")
assert response = json_response(request, 200)
assert response["items"] == []
assert response["next_page_params"] == nil
end
test "return 422 on invalid tx hash", %{conn: conn} do
request = get(conn, "/api/v2/transactions/0x/token-transfers")
assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422)
end
test "return empty list", %{conn: conn} do
tx =
:transaction
|> insert()
|> with_block()
request = get(conn, "/api/v2/transactions/#{to_string(tx.hash)}/token-transfers")
assert response = json_response(request, 200)
assert response["items"] == []
assert response["next_page_params"] == nil
end
test "return relevant token transfer", %{conn: conn} do
tx =
:transaction
|> insert()
|> with_block()
token_transfer = insert(:token_transfer, transaction: tx, block: tx.block, block_number: tx.block_number)
tx_1 =
:transaction
|> insert()
|> with_block()
insert_list(6, :token_transfer, transaction: tx_1, block: tx_1.block, block_number: tx_1.block_number)
request = get(conn, "/api/v2/transactions/#{to_string(tx.hash)}/token-transfers")
assert response = json_response(request, 200)
assert Enum.count(response["items"]) == 1
assert response["next_page_params"] == nil
compare_item(token_transfer, Enum.at(response["items"], 0))
end
test "return list with next_page_params", %{conn: conn} do
tx =
:transaction
|> insert()
|> with_block()
token_transfers =
insert_list(51, :token_transfer, transaction: tx, block: tx.block, block_number: tx.block_number)
|> Enum.reverse()
request = get(conn, "/api/v2/transactions/#{to_string(tx.hash)}/token-transfers")
assert response = json_response(request, 200)
request_2nd_page =
get(conn, "/api/v2/transactions/#{to_string(tx.hash)}/token-transfers", response["next_page_params"])
assert response_2nd_page = json_response(request_2nd_page, 200)
check_paginated_response(response, response_2nd_page, token_transfers)
end
test "check filters", %{conn: conn} do
tx =
:transaction
|> insert()
|> with_block()
erc_1155_token = insert(:token, type: "ERC-1155")
erc_1155_tt =
for x <- 0..50 do
insert(:token_transfer,
transaction: tx,
block: tx.block,
block_number: tx.block_number,
token_contract_address: erc_1155_token.contract_address,
token_ids: [x]
)
end
|> Enum.reverse()
erc_721_token = insert(:token, type: "ERC-721")
erc_721_tt =
for x <- 0..50 do
insert(:token_transfer,
transaction: tx,
block: tx.block,
block_number: tx.block_number,
token_contract_address: erc_721_token.contract_address,
token_ids: [x]
)
end
|> Enum.reverse()
erc_20_token = insert(:token, type: "ERC-20")
erc_20_tt =
for _ <- 0..50 do
insert(:token_transfer,
transaction: tx,
block: tx.block,
block_number: tx.block_number,
token_contract_address: erc_20_token.contract_address
)
end
|> Enum.reverse()
# -- ERC-20 --
filter = %{"type" => "ERC-20"}
request = get(conn, "/api/v2/transactions/#{to_string(tx.hash)}/token-transfers", filter)
assert response = json_response(request, 200)
request_2nd_page =
get(
conn,
"/api/v2/transactions/#{to_string(tx.hash)}/token-transfers",
Map.merge(response["next_page_params"], filter)
)
assert response_2nd_page = json_response(request_2nd_page, 200)
check_paginated_response(response, response_2nd_page, erc_20_tt)
# -- ------ --
# -- ERC-721 --
filter = %{"type" => "ERC-721"}
request = get(conn, "/api/v2/transactions/#{to_string(tx.hash)}/token-transfers", filter)
assert response = json_response(request, 200)
request_2nd_page =
get(
conn,
"/api/v2/transactions/#{to_string(tx.hash)}/token-transfers",
Map.merge(response["next_page_params"], filter)
)
assert response_2nd_page = json_response(request_2nd_page, 200)
check_paginated_response(response, response_2nd_page, erc_721_tt)
# -- ------ --
# -- ERC-1155 --
filter = %{"type" => "ERC-1155"}
request = get(conn, "/api/v2/transactions/#{to_string(tx.hash)}/token-transfers", filter)
assert response = json_response(request, 200)
request_2nd_page =
get(
conn,
"/api/v2/transactions/#{to_string(tx.hash)}/token-transfers",
Map.merge(response["next_page_params"], filter)
)
assert response_2nd_page = json_response(request_2nd_page, 200)
check_paginated_response(response, response_2nd_page, erc_1155_tt)
# -- ------ --
# two filters simultaneously
filter = %{"type" => "ERC-1155,ERC-20"}
request = get(conn, "/api/v2/transactions/#{to_string(tx.hash)}/token-transfers", filter)
assert response = json_response(request, 200)
request_2nd_page =
get(
conn,
"/api/v2/transactions/#{to_string(tx.hash)}/token-transfers",
Map.merge(response["next_page_params"], filter)
)
assert response_2nd_page = json_response(request_2nd_page, 200)
assert Enum.count(response["items"]) == 50
assert response["next_page_params"] != nil
compare_item(Enum.at(erc_1155_tt, 50), Enum.at(response["items"], 0))
compare_item(Enum.at(erc_1155_tt, 1), Enum.at(response["items"], 49))
assert Enum.count(response_2nd_page["items"]) == 50
assert response_2nd_page["next_page_params"] != nil
compare_item(Enum.at(erc_1155_tt, 0), Enum.at(response_2nd_page["items"], 0))
compare_item(Enum.at(erc_20_tt, 50), Enum.at(response_2nd_page["items"], 1))
compare_item(Enum.at(erc_20_tt, 2), Enum.at(response_2nd_page["items"], 49))
request_3rd_page =
get(
conn,
"/api/v2/transactions/#{to_string(tx.hash)}/token-transfers",
Map.merge(response_2nd_page["next_page_params"], filter)
)
assert response_3rd_page = json_response(request_3rd_page, 200)
assert Enum.count(response_3rd_page["items"]) == 2
assert response_3rd_page["next_page_params"] == nil
compare_item(Enum.at(erc_20_tt, 1), Enum.at(response_3rd_page["items"], 0))
compare_item(Enum.at(erc_20_tt, 0), Enum.at(response_3rd_page["items"], 1))
end
end
defp compare_item(%Transaction{} = transaction, json) do
assert to_string(transaction.hash) == json["hash"]
assert transaction.block_number == json["block"]
assert to_string(transaction.value.value) == json["value"]
assert Address.checksum(transaction.from_address_hash) == json["from"]["hash"]
assert Address.checksum(transaction.to_address_hash) == json["to"]["hash"]
end
defp compare_item(%InternalTransaction{} = internal_tx, json) do
assert internal_tx.block_number == json["block"]
assert to_string(internal_tx.gas) == json["gas_limit"]
assert internal_tx.index == json["index"]
assert to_string(internal_tx.transaction_hash) == json["transaction_hash"]
assert Address.checksum(internal_tx.from_address_hash) == json["from"]["hash"]
assert Address.checksum(internal_tx.to_address_hash) == json["to"]["hash"]
end
defp compare_item(%Log{} = log, json) do
assert to_string(log.data) == json["data"]
assert log.index == json["index"]
assert Address.checksum(log.address_hash) == json["address"]["hash"]
end
defp compare_item(%TokenTransfer{} = token_transfer, json) do
assert Address.checksum(token_transfer.from_address_hash) == json["from"]["hash"]
assert Address.checksum(token_transfer.to_address_hash) == json["to"]["hash"]
assert to_string(token_transfer.transaction_hash) == json["tx_hash"]
end
defp check_paginated_response(first_page_resp, second_page_resp, txs) do
assert Enum.count(first_page_resp["items"]) == 50
assert first_page_resp["next_page_params"] != nil
compare_item(Enum.at(txs, 50), Enum.at(first_page_resp["items"], 0))
compare_item(Enum.at(txs, 1), Enum.at(first_page_resp["items"], 49))
assert Enum.count(second_page_resp["items"]) == 1
assert second_page_resp["next_page_params"] == nil
compare_item(Enum.at(txs, 0), Enum.at(second_page_resp["items"], 0))
end
end

@ -20,6 +20,7 @@ defmodule Explorer.Application do
NetVersion, NetVersion,
Transaction, Transaction,
Transactions, Transactions,
TransactionsApiV2,
Uncles Uncles
} }
@ -66,6 +67,7 @@ defmodule Explorer.Application do
con_cache_child_spec(MarketHistoryCache.cache_name()), con_cache_child_spec(MarketHistoryCache.cache_name()),
con_cache_child_spec(RSK.cache_name(), ttl_check_interval: :timer.minutes(1), global_ttl: :timer.minutes(30)), con_cache_child_spec(RSK.cache_name(), ttl_check_interval: :timer.minutes(1), global_ttl: :timer.minutes(30)),
Transactions, Transactions,
TransactionsApiV2,
Accounts, Accounts,
Uncles, Uncles,
{Redix, redix_opts()} {Redix, redix_opts()}

@ -16,6 +16,7 @@ defmodule Explorer.Chain do
order_by: 2, order_by: 2,
order_by: 3, order_by: 3,
preload: 2, preload: 2,
preload: 3,
select: 2, select: 2,
select: 3, select: 3,
subquery: 1, subquery: 1,
@ -73,6 +74,7 @@ defmodule Explorer.Chain do
NewContractsCounter, NewContractsCounter,
NewVerifiedContractsCounter, NewVerifiedContractsCounter,
Transactions, Transactions,
TransactionsApiV2,
Uncles, Uncles,
VerifiedContractsCounter VerifiedContractsCounter
} }
@ -82,7 +84,10 @@ defmodule Explorer.Chain do
alias Explorer.Counters.{ alias Explorer.Counters.{
AddressesCounter, AddressesCounter,
AddressesWithBalanceCounter AddressesWithBalanceCounter,
AddressTokenTransfersCounter,
AddressTransactionsCounter,
AddressTransactionsGasUsageCounter
} }
alias Explorer.Market.MarketHistoryCache alias Explorer.Market.MarketHistoryCache
@ -551,6 +556,20 @@ defmodule Explorer.Chain do
|> Repo.all() |> Repo.all()
end end
@spec address_hash_to_token_transfers_new(Hash.Address.t() | String.t(), Keyword.t()) :: [TokenTransfer.t()]
def address_hash_to_token_transfers_new(address_hash, options \\ []) do
paging_options = Keyword.get(options, :paging_options, @default_paging_options)
direction = Keyword.get(options, :direction)
filters = Keyword.get(options, :token_type)
necessity_by_association = Keyword.get(options, :necessity_by_association)
direction
|> TokenTransfer.token_transfers_by_address_hash(address_hash, filters)
|> join_associations(necessity_by_association)
|> TokenTransfer.handle_paging_options(paging_options)
|> Repo.all()
end
@doc """ @doc """
address_hash_to_token_transfers_including_contract/2 function returns token transfers on address (to/from/contract). address_hash_to_token_transfers_including_contract/2 function returns token transfers on address (to/from/contract).
It is used by CSV export of token transfers button. It is used by CSV export of token transfers button.
@ -2101,14 +2120,6 @@ defmodule Explorer.Chain do
def get_token_transfers_per_transaction_preview_count, do: @token_transfers_per_transaction_preview def get_token_transfers_per_transaction_preview_count, do: @token_transfers_per_transaction_preview
defp debug(value, key) do
require Logger
Logger.configure(truncate: :infinity)
Logger.info(key)
Logger.info(Kernel.inspect(value, limit: :infinity, printable_limit: :infinity))
value
end
@doc """ @doc """
Converts list of `t:Explorer.Chain.Transaction.t/0` `hashes` to the list of `t:Explorer.Chain.Transaction.t/0`s for Converts list of `t:Explorer.Chain.Transaction.t/0` `hashes` to the list of `t:Explorer.Chain.Transaction.t/0`s for
those `hashes`. those `hashes`.
@ -3305,19 +3316,56 @@ defmodule Explorer.Chain do
the `block_number` and `index` that are passed. the `block_number` and `index` that are passed.
""" """
@spec recent_collated_transactions(true | false, [paging_options | necessity_by_association_option], [String.t()], [ @spec recent_collated_transactions(true | false, [paging_options | necessity_by_association_option]) :: [
:atom
]) :: [
Transaction.t() Transaction.t()
] ]
def recent_collated_transactions(old_ui?, options \\ [], method_id_filter \\ [], type_filter \\ []) def recent_collated_transactions(old_ui?, options \\ [])
when is_list(options) do 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)
method_id_filter = Keyword.get(options, :method)
type_filter = Keyword.get(options, :type)
fetch_recent_collated_transactions(old_ui?, paging_options, necessity_by_association, method_id_filter, type_filter) if is_nil(paging_options.key) and (method_id_filter == [] || is_nil(method_id_filter)) and
(type_filter == [] || is_nil(type_filter)) do
old_ui?
|> take_enough_from_txs_cache(paging_options.page_size)
|> case do
nil ->
transactions =
fetch_recent_collated_transactions(
old_ui?,
paging_options,
necessity_by_association,
method_id_filter,
type_filter
)
update_transactions_cache(old_ui?, transactions)
transactions
transactions ->
transactions
end
else
fetch_recent_collated_transactions(
old_ui?,
paging_options,
necessity_by_association,
method_id_filter,
type_filter
)
end
end end
defp take_enough_from_txs_cache(old_ui?, amount)
defp take_enough_from_txs_cache(true, amount), do: Transactions.take_enough(amount)
defp take_enough_from_txs_cache(false, amount), do: TransactionsApiV2.take_enough(amount)
defp update_transactions_cache(old_ui?, txs)
defp update_transactions_cache(true, txs), do: Transactions.update(txs)
defp update_transactions_cache(false, txs), do: TransactionsApiV2.update(txs)
# RAP - random access pagination # RAP - random access pagination
@spec recent_collated_transactions_for_rap([paging_options | necessity_by_association_option]) :: %{ @spec recent_collated_transactions_for_rap([paging_options | necessity_by_association_option]) :: %{
:total_transactions_count => non_neg_integer(), :total_transactions_count => non_neg_integer(),
@ -3387,7 +3435,6 @@ defmodule Explorer.Chain do
|> apply_filter_by_tx_type_to_transactions(type_filter) |> apply_filter_by_tx_type_to_transactions(type_filter)
|> join_associations(necessity_by_association) |> join_associations(necessity_by_association)
|> (&if(old_ui?, do: preload(&1, [{:token_transfers, [:token, :from_address, :to_address]}]), else: &1)).() |> (&if(old_ui?, do: preload(&1, [{:token_transfers, [:token, :from_address, :to_address]}]), else: &1)).()
|> debug("result collated query")
|> Repo.all() |> Repo.all()
|> (&if(old_ui?, |> (&if(old_ui?,
do: &1, do: &1,
@ -3419,13 +3466,15 @@ defmodule Explorer.Chain do
Results will be the transactions older than the `inserted_at` and `hash` that are passed. Results will be the transactions older than the `inserted_at` and `hash` that are passed.
""" """
@spec recent_pending_transactions([paging_options | necessity_by_association_option], true | false, [String.t()], [ @spec recent_pending_transactions([paging_options | necessity_by_association_option], true | false) :: [
:atom Transaction.t()
]) :: [Transaction.t()] ]
def recent_pending_transactions(options \\ [], old_ui? \\ true, method_id_filter \\ [], type_filter \\ []) def recent_pending_transactions(options \\ [], old_ui? \\ true)
when is_list(options) do 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)
method_id_filter = Keyword.get(options, :method)
type_filter = Keyword.get(options, :type)
Transaction Transaction
|> page_pending_transaction(paging_options) |> page_pending_transaction(paging_options)
@ -3436,7 +3485,6 @@ defmodule Explorer.Chain do
|> order_by([transaction], desc: transaction.inserted_at, desc: transaction.hash) |> order_by([transaction], desc: transaction.inserted_at, desc: transaction.hash)
|> join_associations(necessity_by_association) |> join_associations(necessity_by_association)
|> (&if(old_ui?, do: preload(&1, [{:token_transfers, [:token, :from_address, :to_address]}]), else: &1)).() |> (&if(old_ui?, do: preload(&1, [{:token_transfers, [:token, :from_address, :to_address]}]), else: &1)).()
|> debug("result pendging query")
|> Repo.all() |> Repo.all()
|> (&if(old_ui?, |> (&if(old_ui?,
do: &1, do: &1,
@ -3667,6 +3715,7 @@ defmodule Explorer.Chain do
def transaction_to_token_transfers(transaction_hash, options \\ []) when is_list(options) do def transaction_to_token_transfers(transaction_hash, 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 = options |> Keyword.get(:paging_options, @default_paging_options) |> Map.put(:asc_order, true) paging_options = options |> Keyword.get(:paging_options, @default_paging_options) |> Map.put(:asc_order, true)
token_type = Keyword.get(options, :token_type)
TokenTransfer TokenTransfer
|> join(:inner, [token_transfer], transaction in assoc(token_transfer, :transaction)) |> join(:inner, [token_transfer], transaction in assoc(token_transfer, :transaction))
@ -3675,6 +3724,9 @@ defmodule Explorer.Chain do
transaction.hash == ^transaction_hash and token_transfer.block_hash == transaction.block_hash and transaction.hash == ^transaction_hash and token_transfer.block_hash == transaction.block_hash and
token_transfer.block_number == transaction.block_number token_transfer.block_number == transaction.block_number
) )
|> join(:inner, [tt], token in assoc(tt, :token), as: :token)
|> preload([token: token], [{:token, token}])
|> TokenTransfer.filter_by_type(token_type)
|> TokenTransfer.page_token_transfer(paging_options) |> TokenTransfer.page_token_transfer(paging_options)
|> limit(^paging_options.page_size) |> limit(^paging_options.page_size)
|> order_by([token_transfer], asc: token_transfer.log_index) |> order_by([token_transfer], asc: token_transfer.log_index)
@ -4365,7 +4417,12 @@ defmodule Explorer.Chain do
defp fetch_transactions(paging_options \\ nil, from_block \\ nil, to_block \\ nil) do defp fetch_transactions(paging_options \\ nil, from_block \\ nil, to_block \\ nil) do
Transaction Transaction
|> order_by([transaction], desc: transaction.block_number, desc: transaction.index) |> order_by([transaction],
desc: transaction.block_number,
desc: transaction.index,
desc: transaction.inserted_at,
desc: transaction.hash
)
|> where_block_number_in_period(from_block, to_block) |> where_block_number_in_period(from_block, to_block)
|> handle_paging_options(paging_options) |> handle_paging_options(paging_options)
end end
@ -4578,7 +4635,10 @@ defmodule Explorer.Chain do
where( where(
query, query,
[transaction], [transaction],
transaction.inserted_at < ^inserted_at or (transaction.inserted_at == ^inserted_at and transaction.hash < ^hash) (is_nil(transaction.block_number) and
(transaction.inserted_at < ^inserted_at or
(transaction.inserted_at == ^inserted_at and transaction.hash < ^hash))) or
not is_nil(transaction.block_number)
) )
end end
@ -4611,6 +4671,20 @@ defmodule Explorer.Chain do
defp page_search_results(query, %PagingOptions{key: nil}), do: query defp page_search_results(query, %PagingOptions{key: nil}), do: query
defp page_search_results(query, %PagingOptions{
key: {_address_hash, _tx_hash, _block_hash, holder_count, name, inserted_at, item_type}
})
when holder_count in [nil, ""] do
where(
query,
[item],
(item.name > ^name and item.type == ^item_type) or
(item.name == ^name and item.inserted_at < ^inserted_at and
item.type == ^item_type) or
item.type != ^item_type
)
end
# credo:disable-for-next-line # credo:disable-for-next-line
defp page_search_results(query, %PagingOptions{ defp page_search_results(query, %PagingOptions{
key: {_address_hash, _tx_hash, _block_hash, holder_count, name, inserted_at, item_type} key: {_address_hash, _tx_hash, _block_hash, holder_count, name, inserted_at, item_type}
@ -6319,14 +6393,16 @@ defmodule Explorer.Chain do
|> String.downcase() |> String.downcase()
end end
def recent_transactions(options, [:pending | _], method_id_filter, type_filter_options) do def recent_transactions(options, [:pending | _]) do
recent_pending_transactions(options, false, method_id_filter, type_filter_options) recent_pending_transactions(options, false)
end end
def recent_transactions(options, _, method_id_filter, type_filter_options) do def recent_transactions(options, _) do
recent_collated_transactions(false, options, method_id_filter, type_filter_options) recent_collated_transactions(false, options)
end end
def apply_filter_by_method_id_to_transactions(query, nil), do: query
def apply_filter_by_method_id_to_transactions(query, filter) when is_list(filter) do def apply_filter_by_method_id_to_transactions(query, filter) when is_list(filter) do
method_ids = Enum.flat_map(filter, &map_name_or_method_id_to_method_id/1) method_ids = Enum.flat_map(filter, &map_name_or_method_id_to_method_id/1)
@ -6537,4 +6613,53 @@ defmodule Explorer.Chain do
def count_new_contracts_from_cache do def count_new_contracts_from_cache do
NewContractsCounter.fetch() NewContractsCounter.fetch()
end end
def address_counters(address) do
validation_count_task =
Task.async(fn ->
address_to_validation_count(address.hash)
end)
Task.start_link(fn ->
transaction_count(address)
end)
Task.start_link(fn ->
token_transfers_count(address)
end)
Task.start_link(fn ->
gas_usage_count(address)
end)
[
validation_count_task
]
|> Task.yield_many(:infinity)
|> Enum.map(fn {_task, res} ->
case res do
{:ok, result} ->
result
{:exit, reason} ->
raise "Query fetching address counters terminated: #{inspect(reason)}"
nil ->
raise "Query fetching address counters timed out."
end
end)
|> List.to_tuple()
end
def transaction_count(address) do
AddressTransactionsCounter.fetch(address)
end
def token_transfers_count(address) do
AddressTokenTransfersCounter.fetch(address)
end
def gas_usage_count(address) do
AddressTransactionsGasUsageCounter.fetch(address)
end
end end

@ -0,0 +1,27 @@
defmodule Explorer.Chain.Cache.TransactionsApiV2 do
@moduledoc """
Caches the latest imported transactions
"""
alias Explorer.Chain.Transaction
use Explorer.Chain.OrderedCache,
name: :transactions_api_v2,
max_size: 51,
preloads: [
:block,
created_contract_address: :names,
from_address: :names,
to_address: :names
],
ttl_check_interval: Application.get_env(:explorer, __MODULE__)[:ttl_check_interval],
global_ttl: Application.get_env(:explorer, __MODULE__)[:global_ttl]
@type element :: Transaction.t()
@type id :: {non_neg_integer(), non_neg_integer()}
def element_to_id(%Transaction{block_number: block_number, index: index}) do
{block_number, index}
end
end

@ -25,7 +25,7 @@ defmodule Explorer.Chain.TokenTransfer do
use Explorer.Schema use Explorer.Schema
import Ecto.Changeset import Ecto.Changeset
import Ecto.Query, only: [from: 2, limit: 2, where: 3] import Ecto.Query, only: [from: 2, limit: 2, where: 3, join: 5, order_by: 3, preload: 3]
alias Explorer.Chain.{Address, Block, Hash, TokenTransfer, Transaction} alias Explorer.Chain.{Address, Block, Hash, TokenTransfer, Transaction}
alias Explorer.Chain.Token.Instance alias Explorer.Chain.Token.Instance
@ -241,6 +241,16 @@ defmodule Explorer.Chain.TokenTransfer do
) )
end end
def handle_paging_options(query, nil), do: query
def handle_paging_options(query, %PagingOptions{key: nil, page_size: nil}), do: query
def handle_paging_options(query, paging_options) do
query
|> page_token_transfer(paging_options)
|> limit(^paging_options.page_size)
end
@doc """ @doc """
Fetches the transaction hashes from token transfers according Fetches the transaction hashes from token transfers according
to the address hash. to the address hash.
@ -304,4 +314,36 @@ defmodule Explorer.Chain.TokenTransfer do
tt.block_number < ^block_number tt.block_number < ^block_number
) )
end end
def token_transfers_by_address_hash(direction, address_hash, token_types) do
TokenTransfer
|> filter_by_direction(direction, address_hash)
|> order_by([tt], desc: tt.block_number, desc: tt.log_index)
|> join(:inner, [tt], token in assoc(tt, :token), as: :token)
|> preload([token: token], [{:token, token}])
|> filter_by_type(token_types)
end
def filter_by_direction(query, :to, address_hash) do
query
|> where([tt], tt.to_address_hash == ^address_hash)
end
def filter_by_direction(query, :from, address_hash) do
query
|> where([tt], tt.from_address_hash == ^address_hash)
end
def filter_by_direction(query, _, address_hash) do
query
|> where([tt], tt.from_address_hash == ^address_hash or tt.to_address_hash == ^address_hash)
end
def filter_by_type(query, []), do: query
def filter_by_type(query, token_types) when is_list(token_types) do
where(query, [token: token], token.type in ^token_types)
end
def filter_by_type(query, _), do: query
end end

@ -3353,7 +3353,7 @@ defmodule Explorer.ChainTest do
assert [ assert [
%TokenTransfer{ %TokenTransfer{
token: %Ecto.Association.NotLoaded{}, token: %Token{},
transaction: %Ecto.Association.NotLoaded{} transaction: %Ecto.Association.NotLoaded{}
} }
] = Chain.transaction_to_token_transfers(transaction.hash) ] = Chain.transaction_to_token_transfers(transaction.hash)

@ -170,6 +170,13 @@ defmodule Explorer.Factory do
} }
end end
def unique_address_name_factory do
%Address.Name{
address: build(:address),
name: sequence("FooContract")
}
end
def unfetched_balance_factory do def unfetched_balance_factory do
%CoinBalance{ %CoinBalance{
address_hash: address_hash(), address_hash: address_hash(),
@ -642,6 +649,10 @@ defmodule Explorer.Factory do
} }
end end
def unique_token_factory do
Map.replace(token_factory(), :name, sequence("Infinite Token"))
end
def token_transfer_log_factory do def token_transfer_log_factory do
token_contract_address = build(:address) token_contract_address = build(:address)
to_address = build(:address) to_address = build(:address)
@ -809,6 +820,10 @@ defmodule Explorer.Factory do
} }
end end
def unique_smart_contract_factory do
Map.replace(smart_contract_factory(), :name, sequence("SimpleStorage"))
end
def decompiled_smart_contract_factory do def decompiled_smart_contract_factory do
contract_code_info = contract_code_info() contract_code_info = contract_code_info()
@ -839,6 +854,15 @@ defmodule Explorer.Factory do
} }
end end
def address_coin_balance_factory do
%CoinBalance{
address: insert(:address),
block_number: insert(:block).number,
value: Enum.random(1..100_000_000),
value_fetched_at: DateTime.utc_now()
}
end
def address_current_token_balance_factory do def address_current_token_balance_factory do
%CurrentTokenBalance{ %CurrentTokenBalance{
address: build(:address), address: build(:address),
@ -849,6 +873,17 @@ defmodule Explorer.Factory do
} }
end end
def address_current_token_balance_with_token_id_factory do
%CurrentTokenBalance{
address: build(:address),
token_contract_address_hash: insert(:token).contract_address_hash,
block_number: block_number(),
value: Enum.random(1..100_000),
value_fetched_at: DateTime.utc_now(),
token_id: Enum.random([nil, Enum.random(1..100_000)])
}
end
defp block_hash_to_next_transaction_index(block_hash) do defp block_hash_to_next_transaction_index(block_hash) do
import Kernel, except: [+: 2] import Kernel, except: [+: 2]

@ -324,6 +324,10 @@ config :explorer, Explorer.Chain.Cache.Transactions,
ttl_check_interval: if(disable_indexer == "true", do: :timer.seconds(1), else: false), ttl_check_interval: if(disable_indexer == "true", do: :timer.seconds(1), else: false),
global_ttl: if(disable_indexer == "true", do: :timer.seconds(5)) global_ttl: if(disable_indexer == "true", do: :timer.seconds(5))
config :explorer, Explorer.Chain.Cache.TransactionsApiV2,
ttl_check_interval: if(disable_indexer == "true", do: :timer.seconds(1), else: false),
global_ttl: if(disable_indexer == "true", do: :timer.seconds(5))
config :explorer, Explorer.Chain.Cache.Accounts, config :explorer, Explorer.Chain.Cache.Accounts,
ttl_check_interval: if(disable_indexer == "true", do: :timer.seconds(1), else: false), ttl_check_interval: if(disable_indexer == "true", do: :timer.seconds(1), else: false),
global_ttl: if(disable_indexer == "true", do: :timer.seconds(5)) global_ttl: if(disable_indexer == "true", do: :timer.seconds(5))

@ -6,6 +6,8 @@ alias EthereumJSONRPC.Variant
### BlockScout Web ### ### BlockScout Web ###
###################### ######################
config :block_scout_web, BlockScoutWeb.API.V2, enabled: true
######################## ########################
### Ethereum JSONRPC ### ### Ethereum JSONRPC ###
######################## ########################

Loading…
Cancel
Save