From 0e0931e13047ceb4e5f6728d7c10ec78e0afe5ff Mon Sep 17 00:00:00 2001 From: nikitosing <32202610+nikitosing@users.noreply.github.com> Date: Tue, 22 Nov 2022 11:16:57 +0300 Subject: [PATCH] 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 --- .github/workflows/config.yml | 1 + CHANGELOG.md | 2 +- .../lib/block_scout_web/api_router.ex | 1 + .../controllers/address_controller.ex | 56 +- .../controllers/api/v2/address_controller.ex | 66 +- .../controllers/api/v2/block_controller.ex | 9 +- .../controllers/api/v2/fallback_controller.ex | 5 + .../api/v2/main_page_controller.ex | 2 +- .../controllers/api/v2/stats_controller.ex | 14 +- .../api/v2/transaction_controller.ex | 66 +- .../transaction_state_controller.ex | 3 +- .../transaction_token_transfer_controller.ex | 3 +- .../lib/block_scout_web/paging_helper.ex | 40 +- .../block_scout_web/views/api/v2/helper.ex | 18 +- .../views/api/v2/token_view.ex | 6 +- .../views/api/v2/transaction_view.ex | 15 +- .../api/v2/address_controller_test.exs | 1199 +++++++++++++++++ .../api/v2/block_controller_test.exs | 329 +++++ .../api/v2/config_controller_test.exs | 22 + .../api/v2/main_page_controller_test.exs | 65 + .../api/v2/search_controller_test.exs | 147 ++ .../api/v2/stats_controller_test.exs | 57 + .../api/v2/transaction_controller_test.exs | 582 ++++++++ apps/explorer/lib/explorer/application.ex | 2 + apps/explorer/lib/explorer/chain.ex | 177 ++- .../chain/cache/transactions_api_v2.ex | 27 + .../lib/explorer/chain/token_transfer.ex | 44 +- apps/explorer/test/explorer/chain_test.exs | 2 +- apps/explorer/test/support/factory.ex | 35 + config/runtime.exs | 4 + config/runtime/test.exs | 2 + 31 files changed, 2843 insertions(+), 158 deletions(-) create mode 100644 apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs create mode 100644 apps/block_scout_web/test/block_scout_web/controllers/api/v2/block_controller_test.exs create mode 100644 apps/block_scout_web/test/block_scout_web/controllers/api/v2/config_controller_test.exs create mode 100644 apps/block_scout_web/test/block_scout_web/controllers/api/v2/main_page_controller_test.exs create mode 100644 apps/block_scout_web/test/block_scout_web/controllers/api/v2/search_controller_test.exs create mode 100644 apps/block_scout_web/test/block_scout_web/controllers/api/v2/stats_controller_test.exs create mode 100644 apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs create mode 100644 apps/explorer/lib/explorer/chain/cache/transactions_api_v2.ex diff --git a/.github/workflows/config.yml b/.github/workflows/config.yml index 738095bec3..9f6a71bfa9 100644 --- a/.github/workflows/config.yml +++ b/.github/workflows/config.yml @@ -582,3 +582,4 @@ jobs: ADMIN_PANEL_ENABLED: "true" ACCOUNT_ENABLED: "true" ACCOUNT_REDIS_URL: "redis://localhost:6379" + API_V2_ENABLED: "true" diff --git a/CHANGELOG.md b/CHANGELOG.md index 326765ebad..ff0a8a0174 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ - [#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 - [#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 - [#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 diff --git a/apps/block_scout_web/lib/block_scout_web/api_router.ex b/apps/block_scout_web/lib/block_scout_web/api_router.ex index d850b6163e..0d78f6f870 100644 --- a/apps/block_scout_web/lib/block_scout_web/api_router.ex +++ b/apps/block_scout_web/lib/block_scout_web/api_router.ex @@ -118,6 +118,7 @@ defmodule BlockScoutWeb.ApiRouter do scope "/addresses" do 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/transactions", V2.AddressController, :transactions) get("/:address_hash/token-transfers", V2.AddressController, :token_transfers) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/address_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/address_controller.ex index b90321d16a..982ac6128f 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/address_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/address_controller.ex @@ -14,7 +14,6 @@ defmodule BlockScoutWeb.AddressController do Controller } - alias Explorer.Counters.{AddressTokenTransfersCounter, AddressTransactionsCounter, AddressTransactionsGasUsageCounter} alias Explorer.{Chain, Market} alias Explorer.Chain.Wei alias Explorer.ExchangeRates.Token @@ -148,7 +147,7 @@ defmodule BlockScoutWeb.AddressController do def address_counters(conn, %{"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 - {validation_count} = address_counters(address) + {validation_count} = Chain.address_counters(address) transactions_from_db = address.transactions_count || 0 token_transfers_from_db = address.token_transfers_count || 0 @@ -170,57 +169,4 @@ defmodule BlockScoutWeb.AddressController do }) 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 diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex index 717210dc50..5ee15da7ec 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex @@ -9,6 +9,9 @@ defmodule BlockScoutWeb.API.V2.AddressController do 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 Explorer.{Chain, Market} 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: %{ - [created_contract_address: :names] => :optional, - [from_address: :names] => :optional, - [to_address: :names] => :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 + :to_address => :optional, + :from_address => :optional, + :block => :optional } ] @@ -52,6 +47,24 @@ defmodule BlockScoutWeb.API.V2.AddressController do 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 with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)} do token_balances = @@ -82,7 +95,8 @@ defmodule BlockScoutWeb.API.V2.AddressController do results_plus_one = Chain.address_to_transactions_with_rewards(address_hash, options) {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 |> put_status(200) @@ -94,24 +108,26 @@ defmodule BlockScoutWeb.API.V2.AddressController 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 options = - @transaction_with_tt_necessity_by_association + @token_transfer_necessity_by_association |> Keyword.merge(paging_options(params)) |> Keyword.merge(current_filter(params)) + |> Keyword.merge(token_transfers_types_options(params)) results_plus_one = - Chain.address_hash_to_token_transfers( + Chain.address_hash_to_token_transfers_new( address_hash, options ) {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 |> put_status(200) |> 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 @@ -134,7 +150,8 @@ defmodule BlockScoutWeb.API.V2.AddressController do results_plus_one = Chain.address_to_internal_transactions(address_hash, full_options) {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 |> put_status(200) @@ -156,7 +173,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do {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 |> 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)) {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 |> 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) {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 |> put_status(200) @@ -207,14 +224,17 @@ defmodule BlockScoutWeb.API.V2.AddressController do end 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) results_plus_one = Chain.address_to_coin_balances(address_hash, full_options) {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 |> put_status(200) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/block_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/block_controller.ex index 24a3daf808..10533c1f0b 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/block_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/block_controller.ex @@ -4,7 +4,7 @@ defmodule BlockScoutWeb.API.V2.BlockController do import BlockScoutWeb.Chain, 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.BlockTransactionController @@ -51,7 +51,7 @@ defmodule BlockScoutWeb.API.V2.BlockController do {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 |> put_status(200) @@ -70,7 +70,10 @@ defmodule BlockScoutWeb.API.V2.BlockController do {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 |> put_status(200) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/fallback_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/fallback_controller.ex index 20f900066b..b61ef38777 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/fallback_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/fallback_controller.ex @@ -10,6 +10,11 @@ defmodule BlockScoutWeb.API.V2.FallbackController do |> render(:message, %{message: "Invalid parameter(s)"}) 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 conn |> put_status(:not_found) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/main_page_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/main_page_controller.ex index 417eaf4009..0d49281f7f 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/main_page_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/main_page_controller.ex @@ -29,7 +29,7 @@ defmodule BlockScoutWeb.API.V2.MainPageController do [from_address: :smart_contract] => :optional, [to_address: :smart_contract] => :optional }, - paging_options: %PagingOptions{page_size: 5} + paging_options: %PagingOptions{page_size: 6} ) conn diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/stats_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/stats_controller.ex index fa1a46a0c4..7624d3574c 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/stats_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/stats_controller.ex @@ -50,11 +50,23 @@ defmodule BlockScoutWeb.API.V2.StatsController do "gas_used_today" => Enum.at(transaction_stats, 0).gas_used, "gas_prices" => gas_prices, "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 + 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 [{:history_size, history_size}] = Application.get_env(:block_scout_web, BlockScoutWeb.Chain.TransactionHistoryChartController, [{:history_size, 30}]) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex index cce9bd7718..4b6a6c9599 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex @@ -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.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.Import @@ -22,6 +29,15 @@ defmodule BlockScoutWeb.API.V2.TransactionController do } @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, [to_address: :smart_contract] => :optional, [from_address: :names] => :optional, @@ -51,7 +67,8 @@ defmodule BlockScoutWeb.API.V2.TransactionController do transaction_hash, 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 |> put_status(200) |> render(:transaction, %{transaction: preloaded}) @@ -59,24 +76,21 @@ defmodule BlockScoutWeb.API.V2.TransactionController do end def transactions(conn, params) do - filter_options = filter_options(params) - method_filter_options = method_filter_options(params) - type_filter_options = type_filter_options(params) + filter_options = filter_options(params, :validated) full_options = - Keyword.merge( - [ - necessity_by_association: @transaction_necessity_by_association - ], - paging_options(params, filter_options) - ) + [ + necessity_by_association: @transaction_necessity_by_association + ] + |> Keyword.merge(paging_options(params, filter_options)) + |> Keyword.merge(method_filter_options(params)) + |> Keyword.merge(type_filter_options(params)) - transactions_plus_one = - Chain.recent_transactions(full_options, filter_options, method_filter_options, type_filter_options) + transactions_plus_one = Chain.recent_transactions(full_options, filter_options) {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 |> put_status(200) @@ -146,18 +160,18 @@ defmodule BlockScoutWeb.API.V2.TransactionController 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 full_options = - Keyword.merge( - [ - necessity_by_association: @token_transfers_neccessity_by_association - ], - paging_options(params) - ) + [necessity_by_association: @token_transfers_neccessity_by_association] + |> Keyword.merge(paging_options(params)) + |> Keyword.merge(token_transfers_types_options(params)) 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) - 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 |> 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) - 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 |> put_status(200) @@ -207,7 +224,10 @@ defmodule BlockScoutWeb.API.V2.TransactionController do {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 |> put_status(200) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/transaction_state_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/transaction_state_controller.ex index 14b07de040..4a7905d71c 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/transaction_state_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/transaction_state_controller.ex @@ -43,8 +43,7 @@ defmodule BlockScoutWeb.TransactionStateController do [from_address: :names] => :optional, [to_address: :names] => :optional, from_address: :required, - to_address: :required, - token: :required + to_address: :required }, # we need to consider all token transfers in block to show whole state change of transaction paging_options: %PagingOptions{key: nil, page_size: nil} diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/transaction_token_transfer_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/transaction_token_transfer_controller.ex index 931df3ce94..6ea80b6a03 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/transaction_token_transfer_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/transaction_token_transfer_controller.ex @@ -33,8 +33,7 @@ defmodule BlockScoutWeb.TransactionTokenTransferController do [from_address: :names] => :optional, [to_address: :names] => :optional, from_address: :required, - to_address: :required, - token: :required + to_address: :required } ], paging_options(params) diff --git a/apps/block_scout_web/lib/block_scout_web/paging_helper.ex b/apps/block_scout_web/lib/block_scout_web/paging_helper.ex index 3f44c6d62a..9b0b372240 100644 --- a/apps/block_scout_web/lib/block_scout_web/paging_helper.ex +++ b/apps/block_scout_web/lib/block_scout_web/paging_helper.ex @@ -9,6 +9,7 @@ defmodule BlockScoutWeb.PagingHelper do @default_paging_options %PagingOptions{page_size: @page_size + 1} @allowed_filter_labels ["validated", "pending"] @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 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 filter_options(%{"filter" => filter}) do - parse_filter(filter, @allowed_filter_labels) + def token_transfers_types_options(%{"type" => filters}) do + [ + 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 - def filter_options(_params), do: [] + def filter_options(_params, fallback), do: [fallback] + # sobelow_skip ["DOS.StringToAtom"] 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 - def type_filter_options(_params), do: [] + def type_filter_options(_params), do: [type: []] def method_filter_options(%{"method" => method}) do - parse_method_filter(method) + [method: parse_method_filter(method)] end - def method_filter_options(_params), do: [] + def method_filter_options(_params), do: [method: []] def parse_filter("[" <> filter, allowed_labels) do filter @@ -56,13 +68,11 @@ defmodule BlockScoutWeb.PagingHelper do |> parse_filter(allowed_labels) end - # sobelow_skip ["DOS.StringToAtom"] def parse_filter(filter, allowed_labels) when is_binary(filter) do filter |> String.split(",") |> Enum.filter(fn label -> Enum.member?(allowed_labels, label) end) |> Enum.uniq() - |> Enum.map(&String.to_atom/1) end def parse_method_filter("[" <> filter) do @@ -114,4 +124,16 @@ defmodule BlockScoutWeb.PagingHelper do }, 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 diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/helper.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/helper.ex index 57f914bc0c..6dc1808e31 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/helper.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/helper.ex @@ -10,6 +10,10 @@ defmodule BlockScoutWeb.API.V2.Helper do import BlockScoutWeb.Account.AuthController, only: [current_user: 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 %{ personal_tags: private_tags, @@ -27,7 +31,7 @@ defmodule BlockScoutWeb.API.V2.Helper do def address_with_info(%Address{} = address, _address_hash) do %{ - "hash" => to_string(address), + "hash" => Address.checksum(address), "is_contract" => is_smart_contract(address), "name" => address_name(address), "implementation_name" => implementation_name(address), @@ -39,8 +43,18 @@ defmodule BlockScoutWeb.API.V2.Helper do address_with_info(nil, address_hash) end + def address_with_info(nil, nil) do + nil + end + 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 def address_name(%Address{names: [_ | _] = address_names}) do diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/token_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/token_view.ex index e0bdae9c98..1806ec9d33 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/token_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/token_view.ex @@ -1,12 +1,14 @@ defmodule BlockScoutWeb.API.V2.TokenView do + alias Explorer.Chain.Address + def render("token.json", %{token: token}) do %{ - "address" => token.contract_address_hash, + "address" => Address.checksum(token.contract_address_hash), "symbol" => token.symbol, "name" => token.name, "decimals" => token.decimals, "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) } end diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex index c2f85b2ba6..651547ba7f 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex @@ -43,8 +43,8 @@ defmodule BlockScoutWeb.API.V2.TransactionView do %{"method_id" => method_id, "method_call" => text, "parameters" => prepare_method_mapping(mapping)} end - def render("revert_reason.json", %{raw: raw, decoded: decoded}) do - %{"raw" => raw, "decoded" => decoded} + def render("revert_reason.json", %{raw: raw}) do + %{"raw" => raw} end 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), "total" => prepare_token_transfer_total(token_transfer), "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 @@ -204,7 +205,7 @@ defmodule BlockScoutWeb.API.V2.TransactionView do "result" => status, "status" => transaction.status, "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), "to" => Helper.address_with_info(conn, transaction.to_address, transaction.to_address_hash), "created_contract" => @@ -286,8 +287,7 @@ defmodule BlockScoutWeb.API.V2.TransactionView do _ -> hex = TransactionView.get_pure_transaction_revert_reason(transaction) - utf8 = TransactionView.decoded_revert_reason(transaction) - render(__MODULE__, "revert_reason.json", raw: hex, decoded: utf8) + render(__MODULE__, "revert_reason.json", raw: hex) end end end @@ -444,4 +444,7 @@ defmodule BlockScoutWeb.API.V2.TransactionView do types end end + + defp block_timestamp(%Block{} = block), do: block.timestamp + defp block_timestamp(_), do: nil end diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs new file mode 100644 index 0000000000..7542666eca --- /dev/null +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs @@ -0,0 +1,1199 @@ +defmodule BlockScoutWeb.API.V2.AddressControllerTest do + use BlockScoutWeb.ConnCase + + alias Explorer.{Chain, Repo} + + alias Explorer.Chain.{ + Address, + Address.CoinBalance, + Block, + InternalTransaction, + Log, + Token, + TokenTransfer, + Transaction + } + + alias Explorer.Chain.Address.CurrentTokenBalance + + describe "/addresses/{address_hash}" do + test "get 404 on non existing address", %{conn: conn} do + address = build(:address) + + request = get(conn, "/api/v2/addresses/#{address.hash}") + + assert %{"message" => "Not found"} = json_response(request, 404) + end + + test "get 422 on invalid address", %{conn: conn} do + request = get(conn, "/api/v2/addresses/0x") + + assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + end + + test "get address & get the same response for checksummed and downcased parameter", %{conn: conn} do + address = insert(:address) + + correct_reponse = %{ + "hash" => Address.checksum(address.hash), + "implementation_name" => nil, + "is_contract" => false, + "is_verified" => false, + "name" => nil, + "private_tags" => [], + "public_tags" => [], + "watchlist_names" => [] + } + + request = get(conn, "/api/v2/addresses/#{Address.checksum(address.hash)}") + assert ^correct_reponse = json_response(request, 200) + + request = get(conn, "/api/v2/addresses/#{String.downcase(to_string(address.hash))}") + assert ^correct_reponse = json_response(request, 200) + end + end + + describe "/addresses/{address_hash}/counters" do + test "get 404 on non existing address", %{conn: conn} do + address = build(:address) + + request = get(conn, "/api/v2/addresses/#{address.hash}/counters") + + assert %{"message" => "Not found"} = json_response(request, 404) + end + + test "get 422 on invalid address", %{conn: conn} do + request = get(conn, "/api/v2/addresses/0x/counters") + + assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + end + + test "get counters with 0s", %{conn: conn} do + address = insert(:address) + + request = get(conn, "/api/v2/addresses/#{address.hash}/counters") + + assert %{ + "transaction_count" => "0", + "token_transfer_count" => "0", + "gas_usage_count" => "0", + "validation_count" => "0" + } = json_response(request, 200) + end + + test "get counters", %{conn: conn} do + address = insert(:address) + + tx_from = insert(:transaction, from_address: address) |> with_block() + insert(:transaction, to_address: address) |> with_block() + another_tx = insert(:transaction) |> with_block() + + insert(:token_transfer, + from_address: address, + transaction: another_tx, + block: another_tx.block, + block_number: another_tx.block_number + ) + + insert(:token_transfer, + to_address: address, + transaction: another_tx, + block: another_tx.block, + block_number: another_tx.block_number + ) + + insert(:block, miner: address) + + Chain.transaction_count(address) + Chain.token_transfers_count(address) + Chain.gas_usage_count(address) + + request = get(conn, "/api/v2/addresses/#{address.hash}/counters") + + gas_used = to_string(tx_from.gas_used) + + assert %{ + "transaction_count" => "2", + "token_transfer_count" => "2", + "gas_usage_count" => ^gas_used, + "validation_count" => "1" + } = json_response(request, 200) + end + end + + describe "/addresses/{address_hash}/transactions" do + test "get empty list on non existing address", %{conn: conn} do + address = build(:address) + + request = get(conn, "/api/v2/addresses/#{address.hash}/transactions") + + assert response = json_response(request, 200) + assert response["items"] == [] + assert response["next_page_params"] == nil + end + + test "get 422 on invalid address", %{conn: conn} do + request = get(conn, "/api/v2/addresses/0x/transactions") + + assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + end + + test "get relevant transaction", %{conn: conn} do + address = insert(:address) + + tx = insert(:transaction, from_address: address) |> with_block() + + insert(:transaction) |> with_block() + + request = get(conn, "/api/v2/addresses/#{address.hash}/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)) + end + + test "get pending transaction", %{conn: conn} do + address = insert(:address) + + tx = insert(:transaction, from_address: address) |> with_block() + pending_tx = insert(:transaction, from_address: address) + + insert(:transaction) |> with_block() + insert(:transaction) + + request = get(conn, "/api/v2/addresses/#{address.hash}/transactions") + + assert response = json_response(request, 200) + assert Enum.count(response["items"]) == 2 + assert response["next_page_params"] == nil + compare_item(pending_tx, Enum.at(response["items"], 0)) + compare_item(tx, Enum.at(response["items"], 1)) + end + + test "get only :to transaction", %{conn: conn} do + address = insert(:address) + + insert(:transaction, from_address: address) |> with_block() + tx = insert(:transaction, to_address: address) |> with_block() + + request = get(conn, "/api/v2/addresses/#{address.hash}/transactions", %{"filter" => "to"}) + + 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)) + end + + test "get only :from transactions", %{conn: conn} do + address = insert(:address) + + tx = insert(:transaction, from_address: address) |> with_block() + insert(:transaction, to_address: address) |> with_block() + + request = get(conn, "/api/v2/addresses/#{address.hash}/transactions", %{"filter" => "from"}) + + 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)) + end + + test "validated txs can paginate", %{conn: conn} do + address = insert(:address) + + txs = insert_list(51, :transaction, from_address: address) |> with_block() + + request = get(conn, "/api/v2/addresses/#{address.hash}/transactions") + assert response = json_response(request, 200) + + request_2nd_page = get(conn, "/api/v2/addresses/#{address.hash}/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 "pending txs can paginate", %{conn: conn} do + address = insert(:address) + + txs = insert_list(51, :transaction, from_address: address) + + request = get(conn, "/api/v2/addresses/#{address.hash}/transactions") + assert response = json_response(request, 200) + + request_2nd_page = get(conn, "/api/v2/addresses/#{address.hash}/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 "pending + validated txs can paginate", %{conn: conn} do + address = insert(:address) + + txs_pending = insert_list(51, :transaction, from_address: address) + txs_validated = insert_list(50, :transaction, to_address: address) |> with_block() + + request = get(conn, "/api/v2/addresses/#{address.hash}/transactions") + assert response = json_response(request, 200) + + request_2nd_page = get(conn, "/api/v2/addresses/#{address.hash}/transactions", response["next_page_params"]) + 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(txs_pending, 50), Enum.at(response["items"], 0)) + compare_item(Enum.at(txs_pending, 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(txs_pending, 0), Enum.at(response_2nd_page["items"], 0)) + compare_item(Enum.at(txs_validated, 49), Enum.at(response_2nd_page["items"], 1)) + compare_item(Enum.at(txs_validated, 1), Enum.at(response_2nd_page["items"], 49)) + + request = get(conn, "/api/v2/addresses/#{address.hash}/transactions", response_2nd_page["next_page_params"]) + assert response = json_response(request, 200) + + check_paginated_response(response_2nd_page, response, txs_validated ++ [Enum.at(txs_pending, 0)]) + end + + test ":to txs can paginate", %{conn: conn} do + address = insert(:address) + + txs = insert_list(51, :transaction, to_address: address) |> with_block() + insert_list(51, :transaction, from_address: address) |> with_block() + + filter = %{"filter" => "to"} + request = get(conn, "/api/v2/addresses/#{address.hash}/transactions", filter) + assert response = json_response(request, 200) + + request_2nd_page = + get(conn, "/api/v2/addresses/#{address.hash}/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, txs) + end + + test ":from txs can paginate", %{conn: conn} do + address = insert(:address) + + insert_list(51, :transaction, to_address: address) |> with_block() + txs = insert_list(51, :transaction, from_address: address) |> with_block() + + filter = %{"filter" => "from"} + request = get(conn, "/api/v2/addresses/#{address.hash}/transactions", filter) + assert response = json_response(request, 200) + + request_2nd_page = + get(conn, "/api/v2/addresses/#{address.hash}/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, txs) + end + + test ":from + :to txs can paginate", %{conn: conn} do + address = insert(:address) + + txs_from = insert_list(50, :transaction, from_address: address) |> with_block() + txs_to = insert_list(51, :transaction, to_address: address) |> with_block() + + request = get(conn, "/api/v2/addresses/#{address.hash}/transactions") + assert response = json_response(request, 200) + + request_2nd_page = get(conn, "/api/v2/addresses/#{address.hash}/transactions", response["next_page_params"]) + 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(txs_to, 50), Enum.at(response["items"], 0)) + compare_item(Enum.at(txs_to, 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(txs_to, 0), Enum.at(response_2nd_page["items"], 0)) + compare_item(Enum.at(txs_from, 49), Enum.at(response_2nd_page["items"], 1)) + compare_item(Enum.at(txs_from, 1), Enum.at(response_2nd_page["items"], 49)) + + request = get(conn, "/api/v2/addresses/#{address.hash}/transactions", response_2nd_page["next_page_params"]) + assert response = json_response(request, 200) + + check_paginated_response(response_2nd_page, response, txs_from ++ [Enum.at(txs_to, 0)]) + end + end + + describe "/addresses/{address_hash}/token-transfers" do + test "get empty list on non existing address", %{conn: conn} do + address = build(:address) + + request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers") + + assert response = json_response(request, 200) + assert response["items"] == [] + assert response["next_page_params"] == nil + end + + test "get 422 on invalid address", %{conn: conn} do + request = get(conn, "/api/v2/addresses/0x/token-transfers") + + assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + end + + test "get relevant token transfer", %{conn: conn} do + address = insert(:address) + + tx = insert(:transaction) |> with_block() + + insert(:token_transfer, transaction: tx, block: tx.block, block_number: tx.block_number) + + token_transfer = + insert(:token_transfer, transaction: tx, block: tx.block, block_number: tx.block_number, from_address: address) + + request = get(conn, "/api/v2/addresses/#{address.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 "get only :to token transfer", %{conn: conn} do + address = insert(:address) + + tx = insert(:transaction) |> with_block() + + insert(:token_transfer, transaction: tx, block: tx.block, block_number: tx.block_number, from_address: address) + + token_transfer = + insert(:token_transfer, transaction: tx, block: tx.block, block_number: tx.block_number, to_address: address) + + request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", %{"filter" => "to"}) + + 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 "get only :from token transfer", %{conn: conn} do + address = insert(:address) + + tx = insert(:transaction) |> with_block() + + token_transfer = + insert(:token_transfer, transaction: tx, block: tx.block, block_number: tx.block_number, from_address: address) + + insert(:token_transfer, transaction: tx, block: tx.block, block_number: tx.block_number, to_address: address) + request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", %{"filter" => "from"}) + + 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 "token transfers can paginate", %{conn: conn} do + address = insert(:address) + + token_tranfers = + for _ <- 0..50 do + tx = insert(:transaction) |> with_block() + + insert(:token_transfer, transaction: tx, block: tx.block, block_number: tx.block_number, from_address: address) + end + + request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers") + assert response = json_response(request, 200) + + request_2nd_page = get(conn, "/api/v2/addresses/#{address.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_tranfers) + end + + test ":to token transfers can paginate", %{conn: conn} do + address = insert(:address) + + for _ <- 0..50 do + tx = insert(:transaction) |> with_block() + insert(:token_transfer, transaction: tx, block: tx.block, block_number: tx.block_number, from_address: address) + end + + token_tranfers = + for _ <- 0..50 do + tx = insert(:transaction) |> with_block() + insert(:token_transfer, transaction: tx, block: tx.block, block_number: tx.block_number, to_address: address) + end + + filter = %{"filter" => "to"} + request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", filter) + assert response = json_response(request, 200) + + request_2nd_page = + get(conn, "/api/v2/addresses/#{address.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, token_tranfers) + end + + test ":from token transfers can paginate", %{conn: conn} do + address = insert(:address) + + token_tranfers = + for _ <- 0..50 do + tx = insert(:transaction) |> with_block() + + insert(:token_transfer, transaction: tx, block: tx.block, block_number: tx.block_number, from_address: address) + end + + for _ <- 0..50 do + tx = insert(:transaction) |> with_block() + insert(:token_transfer, transaction: tx, block: tx.block, block_number: tx.block_number, to_address: address) + end + + filter = %{"filter" => "from"} + request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", filter) + assert response = json_response(request, 200) + + request_2nd_page = + get(conn, "/api/v2/addresses/#{address.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, token_tranfers) + end + + test ":from + :to tt can paginate", %{conn: conn} do + address = insert(:address) + + tt_from = + for _ <- 0..49 do + tx = insert(:transaction) |> with_block() + + insert(:token_transfer, transaction: tx, block: tx.block, block_number: tx.block_number, from_address: address) + end + + tt_to = + for _ <- 0..50 do + tx = insert(:transaction) |> with_block() + insert(:token_transfer, transaction: tx, block: tx.block, block_number: tx.block_number, to_address: address) + end + + request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers") + assert response = json_response(request, 200) + + request_2nd_page = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", response["next_page_params"]) + 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(tt_to, 50), Enum.at(response["items"], 0)) + compare_item(Enum.at(tt_to, 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(tt_to, 0), Enum.at(response_2nd_page["items"], 0)) + compare_item(Enum.at(tt_from, 49), Enum.at(response_2nd_page["items"], 1)) + compare_item(Enum.at(tt_from, 1), Enum.at(response_2nd_page["items"], 49)) + + request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", response_2nd_page["next_page_params"]) + assert response = json_response(request, 200) + + check_paginated_response(response_2nd_page, response, tt_from ++ [Enum.at(tt_to, 0)]) + end + + test "check token type filters", %{conn: conn} do + address = insert(:address) + + erc_20_token = insert(:token, type: "ERC-20") + + erc_20_tt = + for _ <- 0..50 do + tx = insert(:transaction) |> with_block() + + insert(:token_transfer, + transaction: tx, + block: tx.block, + block_number: tx.block_number, + from_address: address, + token_contract_address: erc_20_token.contract_address + ) + end + + erc_721_token = insert(:token, type: "ERC-721") + + erc_721_tt = + for x <- 0..50 do + tx = insert(:transaction) |> with_block() + + insert(:token_transfer, + transaction: tx, + block: tx.block, + block_number: tx.block_number, + from_address: address, + token_contract_address: erc_721_token.contract_address, + token_ids: [x] + ) + end + + erc_1155_token = insert(:token, type: "ERC-1155") + + erc_1155_tt = + for x <- 0..50 do + tx = insert(:transaction) |> with_block() + + insert(:token_transfer, + transaction: tx, + block: tx.block, + block_number: tx.block_number, + from_address: address, + token_contract_address: erc_1155_token.contract_address, + token_ids: [x] + ) + end + + # -- ERC-20 -- + filter = %{"type" => "ERC-20"} + request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", filter) + assert response = json_response(request, 200) + + request_2nd_page = + get(conn, "/api/v2/addresses/#{address.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/addresses/#{address.hash}/token-transfers", filter) + assert response = json_response(request, 200) + + request_2nd_page = + get(conn, "/api/v2/addresses/#{address.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/addresses/#{address.hash}/token-transfers", filter) + assert response = json_response(request, 200) + + request_2nd_page = + get(conn, "/api/v2/addresses/#{address.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/addresses/#{address.hash}/token-transfers", filter) + assert response = json_response(request, 200) + + request_2nd_page = + get(conn, "/api/v2/addresses/#{address.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/addresses/#{address.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 + + test "type and direction filters at the same time", %{conn: conn} do + address = insert(:address) + + erc_20_token = insert(:token, type: "ERC-20") + + erc_20_tt = + for _ <- 0..50 do + tx = insert(:transaction) |> with_block() + + insert(:token_transfer, + transaction: tx, + block: tx.block, + block_number: tx.block_number, + from_address: address, + token_contract_address: erc_20_token.contract_address + ) + end + + erc_721_token = insert(:token, type: "ERC-721") + + erc_721_tt = + for x <- 0..50 do + tx = insert(:transaction) |> with_block() + + insert(:token_transfer, + transaction: tx, + block: tx.block, + block_number: tx.block_number, + to_address: address, + token_contract_address: erc_721_token.contract_address, + token_ids: [x] + ) + end + + filter = %{"type" => "ERC-721", "filter" => "from"} + request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", filter) + assert response = json_response(request, 200) + assert response["items"] == [] + assert response["next_page_params"] == nil + + filter = %{"type" => "ERC-721", "filter" => "to"} + request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", filter) + assert response = json_response(request, 200) + + request_2nd_page = + get(conn, "/api/v2/addresses/#{address.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) + + filter = %{"type" => "ERC-721,ERC-20", "filter" => "to"} + request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", filter) + assert response = json_response(request, 200) + + request_2nd_page = + get(conn, "/api/v2/addresses/#{address.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) + + filter = %{"type" => "ERC-721,ERC-20", "filter" => "from"} + request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", filter) + assert response = json_response(request, 200) + + request_2nd_page = + get(conn, "/api/v2/addresses/#{address.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) + end + end + + describe "/addresses/{address_hash}/internal-transactions" do + test "get empty list on non existing address", %{conn: conn} do + address = build(:address) + + request = get(conn, "/api/v2/addresses/#{address.hash}/internal-transactions") + + assert response = json_response(request, 200) + assert response["items"] == [] + assert response["next_page_params"] == nil + end + + test "get 422 on invalid address", %{conn: conn} do + request = get(conn, "/api/v2/addresses/0x/internal-transactions") + + assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + end + + test "get internal tx and filter working", %{conn: conn} do + address = insert(:address) + + tx = + :transaction + |> insert() + |> with_block() + + internal_tx_from = + insert(:internal_transaction, + transaction: tx, + index: 1, + block_number: tx.block_number, + transaction_index: tx.index, + block_hash: tx.block_hash, + block_index: 1, + from_address: address + ) + + internal_tx_to = + insert(:internal_transaction, + transaction: tx, + index: 2, + block_number: tx.block_number, + transaction_index: tx.index, + block_hash: tx.block_hash, + block_index: 2, + to_address: address + ) + + request = get(conn, "/api/v2/addresses/#{address.hash}/internal-transactions") + + assert response = json_response(request, 200) + assert Enum.count(response["items"]) == 2 + assert response["next_page_params"] == nil + + compare_item(internal_tx_from, Enum.at(response["items"], 1)) + compare_item(internal_tx_to, Enum.at(response["items"], 0)) + + request = get(conn, "/api/v2/addresses/#{address.hash}/internal-transactions", %{"filter" => "from"}) + + assert response = json_response(request, 200) + assert Enum.count(response["items"]) == 1 + assert response["next_page_params"] == nil + compare_item(internal_tx_from, Enum.at(response["items"], 0)) + + request = get(conn, "/api/v2/addresses/#{address.hash}/internal-transactions", %{"filter" => "to"}) + + assert response = json_response(request, 200) + assert Enum.count(response["items"]) == 1 + assert response["next_page_params"] == nil + compare_item(internal_tx_to, Enum.at(response["items"], 0)) + end + + test "internal txs can paginate", %{conn: conn} do + address = insert(:address) + + tx = + :transaction + |> insert() + |> with_block() + + itxs_from = + for i <- 1..51 do + insert(:internal_transaction, + transaction: tx, + index: i, + block_number: tx.block_number, + transaction_index: tx.index, + block_hash: tx.block_hash, + block_index: i, + from_address: address + ) + end + + request = get(conn, "/api/v2/addresses/#{address.hash}/internal-transactions") + assert response = json_response(request, 200) + + request_2nd_page = + get(conn, "/api/v2/addresses/#{address.hash}/internal-transactions", response["next_page_params"]) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, itxs_from) + + itxs_to = + for i <- 52..102 do + insert(:internal_transaction, + transaction: tx, + index: i, + block_number: tx.block_number, + transaction_index: tx.index, + block_hash: tx.block_hash, + block_index: i, + to_address: address + ) + end + + filter = %{"filter" => "to"} + request = get(conn, "/api/v2/addresses/#{address.hash}/internal-transactions", filter) + assert response = json_response(request, 200) + + request_2nd_page = + get( + conn, + "/api/v2/addresses/#{address.hash}/internal-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, itxs_to) + + filter = %{"filter" => "from"} + request = get(conn, "/api/v2/addresses/#{address.hash}/internal-transactions", filter) + assert response = json_response(request, 200) + + request_2nd_page = + get( + conn, + "/api/v2/addresses/#{address.hash}/internal-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, itxs_from) + end + end + + describe "/addresses/{address_hash}/blocks-validated" do + test "get empty list on non existing address", %{conn: conn} do + address = build(:address) + + request = get(conn, "/api/v2/addresses/#{address.hash}/blocks-validated") + + assert response = json_response(request, 200) + assert response["items"] == [] + assert response["next_page_params"] == nil + end + + test "get 422 on invalid address", %{conn: conn} do + request = get(conn, "/api/v2/addresses/0x/blocks-validated") + + assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + end + + test "get relevant block validated", %{conn: conn} do + address = insert(:address) + insert(:block) + block = insert(:block, miner: address) + + request = get(conn, "/api/v2/addresses/#{address.hash}/blocks-validated") + + 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 "blocks validated can be paginated", %{conn: conn} do + address = insert(:address) + insert(:block) + blocks = insert_list(51, :block, miner: address) + + request = get(conn, "/api/v2/addresses/#{address.hash}/blocks-validated") + assert response = json_response(request, 200) + + request_2nd_page = get(conn, "/api/v2/addresses/#{address.hash}/blocks-validated", response["next_page_params"]) + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, blocks) + end + end + + describe "/addresses/{address_hash}/token-balances" do + test "get empty list on non existing address", %{conn: conn} do + address = build(:address) + + request = get(conn, "/api/v2/addresses/#{address.hash}/token-balances") + + assert response = json_response(request, 200) + assert response == [] + end + + test "get 422 on invalid address", %{conn: conn} do + request = get(conn, "/api/v2/addresses/0x/token-balances") + + assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + end + + test "get token balance", %{conn: conn} do + address = insert(:address) + + ctbs = + for _ <- 0..50 do + insert(:address_current_token_balance_with_token_id, address: address) |> Repo.preload([:token]) + end + |> Enum.sort_by(fn x -> x.value end, :desc) + + request = get(conn, "/api/v2/addresses/#{address.hash}/token-balances") + + assert response = json_response(request, 200) + + for i <- 0..50 do + compare_item(Enum.at(ctbs, i), Enum.at(response, i)) + end + end + end + + describe "/addresses/{address_hash}/coin-balance-history" do + test "get empty list on non existing address", %{conn: conn} do + address = build(:address) + + request = get(conn, "/api/v2/addresses/#{address.hash}/coin-balance-history") + + assert response = json_response(request, 200) + assert response["items"] == [] + assert response["next_page_params"] == nil + end + + test "get 422 on invalid address", %{conn: conn} do + request = get(conn, "/api/v2/addresses/0x/coin-balance-history") + + assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + end + + test "get coin balance history", %{conn: conn} do + address = insert(:address) + + insert(:address_coin_balance) + acb = insert(:address_coin_balance, address: address) + + request = get(conn, "/api/v2/addresses/#{address.hash}/coin-balance-history") + + assert response = json_response(request, 200) + assert Enum.count(response["items"]) == 1 + assert response["next_page_params"] == nil + + compare_item(acb, Enum.at(response["items"], 0)) + end + + test "coin balance history can paginate", %{conn: conn} do + address = insert(:address) + + acbs = insert_list(51, :address_coin_balance, address: address) + + request = get(conn, "/api/v2/addresses/#{address.hash}/coin-balance-history") + assert response = json_response(request, 200) + + request_2nd_page = + get(conn, "/api/v2/addresses/#{address.hash}/coin-balance-history", response["next_page_params"]) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, acbs) + end + end + + describe "/addresses/{address_hash}/coin-balance-history-by-day" do + test "get empty list on non existing address", %{conn: conn} do + address = build(:address) + + request = get(conn, "/api/v2/addresses/#{address.hash}/coin-balance-history-by-day") + + assert response = json_response(request, 200) + assert response == [] + end + + test "get 422 on invalid address", %{conn: conn} do + request = get(conn, "/api/v2/addresses/0x/coin-balance-history-by-day") + + assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + end + + test "get coin balance history by day", %{conn: conn} do + address = insert(:address) + noon = Timex.now() |> Timex.beginning_of_day() |> Timex.set(hour: 12) + block = insert(:block, timestamp: noon, number: 2) + block_one_day_ago = insert(:block, timestamp: Timex.shift(noon, days: -1), number: 1) + insert(:fetched_balance, address_hash: address.hash, value: 1000, block_number: block.number) + insert(:fetched_balance, address_hash: address.hash, value: 2000, block_number: block_one_day_ago.number) + insert(:fetched_balance_daily, address_hash: address.hash, value: 1000, day: noon) + insert(:fetched_balance_daily, address_hash: address.hash, value: 2000, day: Timex.shift(noon, days: -1)) + + request = get(conn, "/api/v2/addresses/#{address.hash}/coin-balance-history-by-day") + + response = json_response(request, 200) + + assert [ + %{"date" => _, "value" => "2000"}, + %{"date" => _, "value" => "1000"}, + %{"date" => _, "value" => "1000"} + ] = response + end + end + + describe "/addresses/{address_hash}/logs" do + test "get empty list on non existing address", %{conn: conn} do + address = build(:address) + + request = get(conn, "/api/v2/addresses/#{address.hash}/logs") + + assert response = json_response(request, 200) + assert response["items"] == [] + assert response["next_page_params"] == nil + end + + test "get 422 on invalid address", %{conn: conn} do + request = get(conn, "/api/v2/addresses/0x/logs") + + assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + end + + test "get log", %{conn: conn} do + address = insert(:address) + + tx = + :transaction + |> insert() + |> with_block() + + log = + insert(:log, + transaction: tx, + index: 1, + block: tx.block, + block_number: tx.block_number, + address: address + ) + + request = get(conn, "/api/v2/addresses/#{address.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 + + # for some reasons test does not work if run as single test + test "logs can paginate", %{conn: conn} do + address = insert(:address) + + logs = + for x <- 0..50 do + tx = + :transaction + |> insert() + |> with_block() + + insert(:log, + transaction: tx, + index: x, + block: tx.block, + block_number: tx.block_number, + address: address + ) + end + + request = get(conn, "/api/v2/addresses/#{address.hash}/logs") + assert response = json_response(request, 200) + + request_2nd_page = get(conn, "/api/v2/addresses/#{address.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 + + test "logs can be filtered by topic", %{conn: conn} do + address = insert(:address) + + for x <- 0..20 do + tx = + :transaction + |> insert() + |> with_block() + + insert(:log, + transaction: tx, + index: x, + block: tx.block, + block_number: tx.block_number, + address: address + ) + end + + tx = + :transaction + |> insert() + |> with_block() + + log = + insert(:log, + transaction: tx, + block: tx.block, + block_number: tx.block_number, + address: address, + first_topic: "0x123456789123456789" + ) + + request = get(conn, "/api/v2/addresses/#{address.hash}/logs?topic=0x123456789123456789") + 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 + 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(%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 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(%Block{} = block, json) do + assert to_string(block.hash) == json["hash"] + assert block.number == json["height"] + end + + defp compare_item(%CurrentTokenBalance{} = ctb, json) do + assert to_string(ctb.value) == json["value"] + assert (ctb.token_id && to_string(ctb.token_id)) == json["token_id"] + compare_item(ctb.token, json["token"]) + end + + defp compare_item(%CoinBalance{} = cb, json) do + assert to_string(cb.value.value) == json["value"] + assert cb.block_number == json["block_number"] + + assert Jason.encode!(Repo.get_by(Block, number: cb.block_number).timestamp) =~ + String.replace(json["block_timestamp"], "Z", "") + end + + defp compare_item(%Token{} = token, json) do + assert Address.checksum(token.contract_address_hash) == json["address"] + assert to_string(token.symbol) == json["symbol"] + assert to_string(token.name) == json["name"] + assert to_string(token.type) == json["type"] + assert to_string(token.decimals) == json["decimals"] + assert (token.holder_count && to_string(token.holder_count)) == json["holders"] + assert Map.has_key?(json, "exchange_rate") + 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 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 diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/block_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/block_controller_test.exs new file mode 100644 index 0000000000..d16e457441 --- /dev/null +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/block_controller_test.exs @@ -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 diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/config_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/config_controller_test.exs new file mode 100644 index 0000000000..0c00722d58 --- /dev/null +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/config_controller_test.exs @@ -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 diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/main_page_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/main_page_controller_test.exs new file mode 100644 index 0000000000..75c5e21839 --- /dev/null +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/main_page_controller_test.exs @@ -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 diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/search_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/search_controller_test.exs new file mode 100644 index 0000000000..211c68dc79 --- /dev/null +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/search_controller_test.exs @@ -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 diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/stats_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/stats_controller_test.exs new file mode 100644 index 0000000000..803d5ad51c --- /dev/null +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/stats_controller_test.exs @@ -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 diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs new file mode 100644 index 0000000000..036b79dfc9 --- /dev/null +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs @@ -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 diff --git a/apps/explorer/lib/explorer/application.ex b/apps/explorer/lib/explorer/application.ex index 8529ef97cf..191840d0d3 100644 --- a/apps/explorer/lib/explorer/application.ex +++ b/apps/explorer/lib/explorer/application.ex @@ -20,6 +20,7 @@ defmodule Explorer.Application do NetVersion, Transaction, Transactions, + TransactionsApiV2, Uncles } @@ -66,6 +67,7 @@ defmodule Explorer.Application do 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)), Transactions, + TransactionsApiV2, Accounts, Uncles, {Redix, redix_opts()} diff --git a/apps/explorer/lib/explorer/chain.ex b/apps/explorer/lib/explorer/chain.ex index 30b5c2ecd3..5a06bf2f55 100644 --- a/apps/explorer/lib/explorer/chain.ex +++ b/apps/explorer/lib/explorer/chain.ex @@ -16,6 +16,7 @@ defmodule Explorer.Chain do order_by: 2, order_by: 3, preload: 2, + preload: 3, select: 2, select: 3, subquery: 1, @@ -73,6 +74,7 @@ defmodule Explorer.Chain do NewContractsCounter, NewVerifiedContractsCounter, Transactions, + TransactionsApiV2, Uncles, VerifiedContractsCounter } @@ -82,7 +84,10 @@ defmodule Explorer.Chain do alias Explorer.Counters.{ AddressesCounter, - AddressesWithBalanceCounter + AddressesWithBalanceCounter, + AddressTokenTransfersCounter, + AddressTransactionsCounter, + AddressTransactionsGasUsageCounter } alias Explorer.Market.MarketHistoryCache @@ -551,6 +556,20 @@ defmodule Explorer.Chain do |> Repo.all() 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 """ 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. @@ -2101,14 +2120,6 @@ defmodule Explorer.Chain do 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 """ Converts list of `t:Explorer.Chain.Transaction.t/0` `hashes` to the list of `t:Explorer.Chain.Transaction.t/0`s for those `hashes`. @@ -3305,19 +3316,56 @@ defmodule Explorer.Chain do the `block_number` and `index` that are passed. """ - @spec recent_collated_transactions(true | false, [paging_options | necessity_by_association_option], [String.t()], [ - :atom - ]) :: [ + @spec recent_collated_transactions(true | false, [paging_options | necessity_by_association_option]) :: [ 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 necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) 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 + 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 @spec recent_collated_transactions_for_rap([paging_options | necessity_by_association_option]) :: %{ :total_transactions_count => non_neg_integer(), @@ -3387,7 +3435,6 @@ defmodule Explorer.Chain do |> apply_filter_by_tx_type_to_transactions(type_filter) |> join_associations(necessity_by_association) |> (&if(old_ui?, do: preload(&1, [{:token_transfers, [:token, :from_address, :to_address]}]), else: &1)).() - |> debug("result collated query") |> Repo.all() |> (&if(old_ui?, 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. """ - @spec recent_pending_transactions([paging_options | necessity_by_association_option], true | false, [String.t()], [ - :atom - ]) :: [Transaction.t()] - def recent_pending_transactions(options \\ [], old_ui? \\ true, method_id_filter \\ [], type_filter \\ []) + @spec recent_pending_transactions([paging_options | necessity_by_association_option], true | false) :: [ + Transaction.t() + ] + def recent_pending_transactions(options \\ [], old_ui? \\ true) when is_list(options) do necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) paging_options = Keyword.get(options, :paging_options, @default_paging_options) + method_id_filter = Keyword.get(options, :method) + type_filter = Keyword.get(options, :type) Transaction |> page_pending_transaction(paging_options) @@ -3436,7 +3485,6 @@ defmodule Explorer.Chain do |> order_by([transaction], desc: transaction.inserted_at, desc: transaction.hash) |> join_associations(necessity_by_association) |> (&if(old_ui?, do: preload(&1, [{:token_transfers, [:token, :from_address, :to_address]}]), else: &1)).() - |> debug("result pendging query") |> Repo.all() |> (&if(old_ui?, do: &1, @@ -3667,6 +3715,7 @@ defmodule Explorer.Chain do def transaction_to_token_transfers(transaction_hash, options \\ []) when is_list(options) do necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) paging_options = options |> Keyword.get(:paging_options, @default_paging_options) |> Map.put(:asc_order, true) + token_type = Keyword.get(options, :token_type) TokenTransfer |> 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 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) |> limit(^paging_options.page_size) |> 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 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) |> handle_paging_options(paging_options) end @@ -4578,7 +4635,10 @@ defmodule Explorer.Chain do where( query, [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 @@ -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: {_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 defp page_search_results(query, %PagingOptions{ key: {_address_hash, _tx_hash, _block_hash, holder_count, name, inserted_at, item_type} @@ -6319,14 +6393,16 @@ defmodule Explorer.Chain do |> String.downcase() end - def recent_transactions(options, [:pending | _], method_id_filter, type_filter_options) do - recent_pending_transactions(options, false, method_id_filter, type_filter_options) + def recent_transactions(options, [:pending | _]) do + recent_pending_transactions(options, false) end - def recent_transactions(options, _, method_id_filter, type_filter_options) do - recent_collated_transactions(false, options, method_id_filter, type_filter_options) + def recent_transactions(options, _) do + recent_collated_transactions(false, options) 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 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 NewContractsCounter.fetch() 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 diff --git a/apps/explorer/lib/explorer/chain/cache/transactions_api_v2.ex b/apps/explorer/lib/explorer/chain/cache/transactions_api_v2.ex new file mode 100644 index 0000000000..0a5cf715c1 --- /dev/null +++ b/apps/explorer/lib/explorer/chain/cache/transactions_api_v2.ex @@ -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 diff --git a/apps/explorer/lib/explorer/chain/token_transfer.ex b/apps/explorer/lib/explorer/chain/token_transfer.ex index 152614b4da..e057bdaa51 100644 --- a/apps/explorer/lib/explorer/chain/token_transfer.ex +++ b/apps/explorer/lib/explorer/chain/token_transfer.ex @@ -25,7 +25,7 @@ defmodule Explorer.Chain.TokenTransfer do use Explorer.Schema 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.Token.Instance @@ -241,6 +241,16 @@ defmodule Explorer.Chain.TokenTransfer do ) 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 """ Fetches the transaction hashes from token transfers according to the address hash. @@ -304,4 +314,36 @@ defmodule Explorer.Chain.TokenTransfer do tt.block_number < ^block_number ) 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 diff --git a/apps/explorer/test/explorer/chain_test.exs b/apps/explorer/test/explorer/chain_test.exs index 892b185ac3..61b42a0e60 100644 --- a/apps/explorer/test/explorer/chain_test.exs +++ b/apps/explorer/test/explorer/chain_test.exs @@ -3353,7 +3353,7 @@ defmodule Explorer.ChainTest do assert [ %TokenTransfer{ - token: %Ecto.Association.NotLoaded{}, + token: %Token{}, transaction: %Ecto.Association.NotLoaded{} } ] = Chain.transaction_to_token_transfers(transaction.hash) diff --git a/apps/explorer/test/support/factory.ex b/apps/explorer/test/support/factory.ex index 7967d83b8d..7da1532025 100644 --- a/apps/explorer/test/support/factory.ex +++ b/apps/explorer/test/support/factory.ex @@ -170,6 +170,13 @@ defmodule Explorer.Factory do } end + def unique_address_name_factory do + %Address.Name{ + address: build(:address), + name: sequence("FooContract") + } + end + def unfetched_balance_factory do %CoinBalance{ address_hash: address_hash(), @@ -642,6 +649,10 @@ defmodule Explorer.Factory do } end + def unique_token_factory do + Map.replace(token_factory(), :name, sequence("Infinite Token")) + end + def token_transfer_log_factory do token_contract_address = build(:address) to_address = build(:address) @@ -809,6 +820,10 @@ defmodule Explorer.Factory do } end + def unique_smart_contract_factory do + Map.replace(smart_contract_factory(), :name, sequence("SimpleStorage")) + end + def decompiled_smart_contract_factory do contract_code_info = contract_code_info() @@ -839,6 +854,15 @@ defmodule Explorer.Factory do } 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 %CurrentTokenBalance{ address: build(:address), @@ -849,6 +873,17 @@ defmodule Explorer.Factory do } 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 import Kernel, except: [+: 2] diff --git a/config/runtime.exs b/config/runtime.exs index d4f6f958eb..4f4bad6979 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -324,6 +324,10 @@ config :explorer, Explorer.Chain.Cache.Transactions, 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.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, ttl_check_interval: if(disable_indexer == "true", do: :timer.seconds(1), else: false), global_ttl: if(disable_indexer == "true", do: :timer.seconds(5)) diff --git a/config/runtime/test.exs b/config/runtime/test.exs index f6ba58d424..ca3eed98e1 100644 --- a/config/runtime/test.exs +++ b/config/runtime/test.exs @@ -6,6 +6,8 @@ alias EthereumJSONRPC.Variant ### BlockScout Web ### ###################### +config :block_scout_web, BlockScoutWeb.API.V2, enabled: true + ######################## ### Ethereum JSONRPC ### ########################