API v2 for frontend (#6379)

* Core BlockScout API V2 (#6164)

API endpoints:

/transactions
/transactions/{tx_hash}
/transactions/{tx_hash}/token-transfers
/transactions/{tx_hash}/internal-transactions
/transactions/{tx_hash}/logs
/transactions/{tx_hash}/raw-trace

* Block initial commit

* Finish Block API

* Address API initial commit

* Fix some issue with API

* Add tags

* transaction -> coin_transfer

* Some changes

* Add /transactions and /token-transfers for addresses

* Fix test

* Fix tests

* Fix block rewards

* Add /json-rpc-url API endpoint

* Create method_id index concurrently

* Fix for concurrent index creation

* BS core API V2: addresses, stats, main page, websockets (#6361)

* socket/v2

* Refactor: reuse fees counter fucntion, remove unecessary clause of do_token_transfer_amount_for_api

* Improve token transfers preload

* Done with channels

* Add some endpoints to /address

* Fix credo

* Add main page controller

* Add stats controller

* Move api search to API v2

* Fix some addresses methods; Rename gas_price

* Improve logs view

* Fix tests

* Brush and finalize websockets

* Add API_V2_ENABLED env

* Fix credo

* Add is_smart_contract clause

* Add CHANGELOG entry

Co-authored-by: nikitosing <32202610+nikitosing@users.noreply.github.com>
Co-authored-by: Никита Поздняков <nikitosing4@mail.ru>
pull/5944/head
Victor Baranov 2 years ago committed by GitHub
parent 2c438e6077
commit 3cac89f15c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      .github/workflows/config.yml
  2. 1
      CHANGELOG.md
  3. 69
      apps/block_scout_web/lib/block_scout_web/api_router.ex
  4. 83
      apps/block_scout_web/lib/block_scout_web/channels/address_channel.ex
  5. 17
      apps/block_scout_web/lib/block_scout_web/channels/block_channel.ex
  6. 14
      apps/block_scout_web/lib/block_scout_web/channels/exchange_rate_channel.ex
  7. 10
      apps/block_scout_web/lib/block_scout_web/channels/reward_channel.ex
  8. 12
      apps/block_scout_web/lib/block_scout_web/channels/token_channel.ex
  9. 20
      apps/block_scout_web/lib/block_scout_web/channels/transaction_channel.ex
  10. 19
      apps/block_scout_web/lib/block_scout_web/channels/user_socket_v2.ex
  11. 238
      apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex
  12. 81
      apps/block_scout_web/lib/block_scout_web/controllers/api/v2/block_controller.ex
  13. 11
      apps/block_scout_web/lib/block_scout_web/controllers/api/v2/config_controller.ex
  14. 38
      apps/block_scout_web/lib/block_scout_web/controllers/api/v2/fallback_controller.ex
  15. 40
      apps/block_scout_web/lib/block_scout_web/controllers/api/v2/main_page_controller.ex
  16. 24
      apps/block_scout_web/lib/block_scout_web/controllers/api/v2/search_controller.ex
  17. 104
      apps/block_scout_web/lib/block_scout_web/controllers/api/v2/stats_controller.ex
  18. 221
      apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex
  19. 6
      apps/block_scout_web/lib/block_scout_web/controllers/block_transaction_controller.ex
  20. 2
      apps/block_scout_web/lib/block_scout_web/controllers/chain/transaction_history_chart_controller.ex
  21. 29
      apps/block_scout_web/lib/block_scout_web/controllers/chain_controller.ex
  22. 20
      apps/block_scout_web/lib/block_scout_web/controllers/pagination_helpers.ex
  23. 2
      apps/block_scout_web/lib/block_scout_web/controllers/pending_transaction_controller.ex
  24. 2
      apps/block_scout_web/lib/block_scout_web/controllers/recent_transactions_controller.ex
  25. 22
      apps/block_scout_web/lib/block_scout_web/controllers/search_controller.ex
  26. 1
      apps/block_scout_web/lib/block_scout_web/endpoint.ex
  27. 11
      apps/block_scout_web/lib/block_scout_web/models/get_address_tags.ex
  28. 4
      apps/block_scout_web/lib/block_scout_web/notifier.ex
  29. 117
      apps/block_scout_web/lib/block_scout_web/paging_helper.ex
  30. 21
      apps/block_scout_web/lib/block_scout_web/plug/check_api_v2.ex
  31. 10
      apps/block_scout_web/lib/block_scout_web/templates/address_token/_tokens.html.eex
  32. 6
      apps/block_scout_web/lib/block_scout_web/templates/address_token_balance/_token_balances.html.eex
  33. 20
      apps/block_scout_web/lib/block_scout_web/templates/address_token_balance/_tokens.html.eex
  34. 64
      apps/block_scout_web/lib/block_scout_web/views/abi_encoded_value_view.ex
  35. 58
      apps/block_scout_web/lib/block_scout_web/views/address_token_balance_view.ex
  36. 59
      apps/block_scout_web/lib/block_scout_web/views/api/v2/address_view.ex
  37. 9
      apps/block_scout_web/lib/block_scout_web/views/api/v2/api_v2.ex
  38. 7
      apps/block_scout_web/lib/block_scout_web/views/api/v2/api_view.ex
  39. 106
      apps/block_scout_web/lib/block_scout_web/views/api/v2/block_view.ex
  40. 7
      apps/block_scout_web/lib/block_scout_web/views/api/v2/config_view.ex
  41. 108
      apps/block_scout_web/lib/block_scout_web/views/api/v2/helper.ex
  42. 53
      apps/block_scout_web/lib/block_scout_web/views/api/v2/search_view.ex
  43. 13
      apps/block_scout_web/lib/block_scout_web/views/api/v2/token_view.ex
  44. 447
      apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex
  45. 2
      apps/block_scout_web/lib/block_scout_web/views/block_view.ex
  46. 18
      apps/block_scout_web/lib/block_scout_web/views/chain_view.ex
  47. 48
      apps/block_scout_web/lib/block_scout_web/views/search_view.ex
  48. 50
      apps/block_scout_web/lib/block_scout_web/views/tokens/helpers.ex
  49. 12
      apps/block_scout_web/priv/gettext/default.pot
  50. 12
      apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po
  51. 2
      apps/block_scout_web/test/block_scout_web/features/viewing_chain_test.exs
  52. 6
      apps/block_scout_web/test/block_scout_web/views/address_token_balance_view_test.exs
  53. 3
      apps/block_scout_web/test/block_scout_web/views/block_view_test.exs
  54. 2
      apps/explorer/benchmarks/explorer/chain/recent_collated_transactions.exs
  55. 4
      apps/explorer/config/dev.exs
  56. 3
      apps/explorer/config/prod.exs
  57. 3
      apps/explorer/config/test.exs
  58. 6
      apps/explorer/lib/explorer/account/tag_transaction.ex
  59. 336
      apps/explorer/lib/explorer/chain.ex
  60. 1
      apps/explorer/lib/explorer/chain/address/current_token_balance.ex
  61. 23
      apps/explorer/lib/explorer/chain/transaction.ex
  62. 5
      apps/explorer/lib/explorer/chain/wei.ex
  63. 6
      apps/explorer/lib/explorer/counters/address_tokens_usd_sum.ex
  64. 2
      apps/explorer/lib/explorer/market/market.ex
  65. 15
      apps/explorer/priv/repo/migrations/20220919105140_add_method_id_index.exs
  66. 11
      apps/explorer/test/explorer/chain_test.exs
  67. 2
      config/runtime.exs

@ -7,6 +7,7 @@ on:
pull_request: pull_request:
branches: branches:
- master - master
- api
env: env:
MIX_ENV: test MIX_ENV: test

@ -2,6 +2,7 @@
### Features ### Features
- [#6379](https://github.com/blockscout/blockscout/pull/6379) - API v2 for frontend
- [#6351](https://github.com/blockscout/blockscout/pull/6351) - Enable forum link env var - [#6351](https://github.com/blockscout/blockscout/pull/6351) - Enable forum link env var
- [#6316](https://github.com/blockscout/blockscout/pull/6316) - Copy public tags functionality to master - [#6316](https://github.com/blockscout/blockscout/pull/6316) - Copy public tags functionality to master
- [#6196](https://github.com/blockscout/blockscout/pull/6196) - INDEXER_CATCHUP_BLOCKS_BATCH_SIZE and INDEXER_CATCHUP_BLOCKS_CONCURRENCY env varaibles - [#6196](https://github.com/blockscout/blockscout/pull/6196) - INDEXER_CATCHUP_BLOCKS_BATCH_SIZE and INDEXER_CATCHUP_BLOCKS_CONCURRENCY env varaibles

@ -13,7 +13,7 @@ defmodule BlockScoutWeb.ApiRouter do
Router for API Router for API
""" """
use BlockScoutWeb, :router use BlockScoutWeb, :router
alias BlockScoutWeb.Plug.CheckAccountAPI alias BlockScoutWeb.Plug.{CheckAccountAPI, CheckApiV2}
pipeline :api do pipeline :api do
plug(:accepts, ["json"]) plug(:accepts, ["json"])
@ -25,9 +25,15 @@ defmodule BlockScoutWeb.ApiRouter do
plug(CheckAccountAPI) plug(CheckAccountAPI)
end end
pipeline :api_v2 do
plug(CheckApiV2)
plug(:fetch_session)
plug(:protect_from_forgery)
end
alias BlockScoutWeb.Account.Api.V1.{TagsController, UserController} alias BlockScoutWeb.Account.Api.V1.{TagsController, UserController}
scope "/account/v1" do scope "/account/v1", as: :account_v1 do
pipe_through(:api) pipe_through(:api)
pipe_through(:account_api) pipe_through(:account_api)
@ -83,13 +89,68 @@ defmodule BlockScoutWeb.ApiRouter do
end end
end end
scope "/v2", as: :api_v2 do
pipe_through(:api)
pipe_through(:api_v2)
alias BlockScoutWeb.API.V2
get("/search", V2.SearchController, :search)
scope "/config" do
get("/json-rpc-url", V2.ConfigController, :json_rpc_url)
end
scope "/transactions" do
get("/", V2.TransactionController, :transactions)
get("/:transaction_hash", V2.TransactionController, :transaction)
get("/:transaction_hash/token-transfers", V2.TransactionController, :token_transfers)
get("/:transaction_hash/internal-transactions", V2.TransactionController, :internal_transactions)
get("/:transaction_hash/logs", V2.TransactionController, :logs)
get("/:transaction_hash/raw-trace", V2.TransactionController, :raw_trace)
end
scope "/blocks" do
get("/", V2.BlockController, :blocks)
get("/:block_hash_or_number", V2.BlockController, :block)
get("/:block_hash_or_number/transactions", V2.BlockController, :transactions)
end
scope "/addresses" do
get("/:address_hash", V2.AddressController, :address)
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)
get("/:address_hash/internal-transactions", V2.AddressController, :internal_transactions)
get("/:address_hash/logs", V2.AddressController, :logs)
get("/:address_hash/blocks-validated", V2.AddressController, :blocks_validated)
get("/:address_hash/coin-balance-history", V2.AddressController, :coin_balance_history)
get("/:address_hash/coin-balance-history-by-day", V2.AddressController, :coin_balance_history_by_day)
end
scope "/main-page" do
get("/blocks", V2.MainPageController, :blocks)
get("/transactions", V2.MainPageController, :transactions)
end
scope "/stats" do
get("/", V2.StatsController, :stats)
scope "/charts" do
get("/transactions", V2.StatsController, :transactions_chart)
get("/market", V2.StatsController, :market_chart)
end
end
end
scope "/v1", as: :api_v1 do scope "/v1", as: :api_v1 do
pipe_through(:api) pipe_through(:api)
alias BlockScoutWeb.API.{EthRPC, RPC, V1} alias BlockScoutWeb.API.{EthRPC, RPC, V1}
alias BlockScoutWeb.API.V1.HealthController alias BlockScoutWeb.API.V1.HealthController
alias BlockScoutWeb.SearchController alias BlockScoutWeb.API.V2.SearchController
get("/search", SearchController, :api_search_result) # leave the same endpoint in v1 in order to keep backward compatibility
get("/search", SearchController, :search)
get("/health", HealthController, :health) get("/health", HealthController, :health)
get("/gas-price-oracle", V1.GasPriceOracleController, :gas_price_oracle) get("/gas-price-oracle", V1.GasPriceOracleController, :gas_price_oracle)

@ -4,6 +4,8 @@ defmodule BlockScoutWeb.AddressChannel do
""" """
use BlockScoutWeb, :channel use BlockScoutWeb, :channel
alias BlockScoutWeb.API.V2.AddressView, as: AddressViewAPI
alias BlockScoutWeb.{ alias BlockScoutWeb.{
AddressCoinBalanceView, AddressCoinBalanceView,
AddressView, AddressView,
@ -56,6 +58,20 @@ defmodule BlockScoutWeb.AddressChannel do
end end
end end
def handle_out(
"balance_update",
%{address: address, exchange_rate: exchange_rate},
%Phoenix.Socket{handler: BlockScoutWeb.UserSocketV2} = socket
) do
push(socket, "balance", %{
balance: address.fetched_coin_balance.value,
block_number: address.fetched_coin_balance_block_number,
exchange_rate: exchange_rate.usd_value
})
{:noreply, socket}
end
def handle_out( def handle_out(
"balance_update", "balance_update",
%{address: address, exchange_rate: exchange_rate}, %{address: address, exchange_rate: exchange_rate},
@ -88,6 +104,12 @@ defmodule BlockScoutWeb.AddressChannel do
end end
end end
def handle_out("count", %{count: count}, %Phoenix.Socket{handler: BlockScoutWeb.UserSocketV2} = socket) do
push(socket, "count", %{count: to_string(count)})
{:noreply, socket}
end
def handle_out("count", %{count: count}, socket) do def handle_out("count", %{count: count}, socket) do
Gettext.put_locale(BlockScoutWeb.Gettext, socket.assigns.locale) Gettext.put_locale(BlockScoutWeb.Gettext, socket.assigns.locale)
@ -96,6 +118,16 @@ defmodule BlockScoutWeb.AddressChannel do
{:noreply, socket} {:noreply, socket}
end end
def handle_out(
"internal_transaction",
%{address: _address, internal_transaction: _internal_transaction},
%Phoenix.Socket{handler: BlockScoutWeb.UserSocketV2} = socket
) do
push(socket, "internal_transaction", %{internal_transaction: 1})
{:noreply, socket}
end
def handle_out("internal_transaction", %{address: address, internal_transaction: internal_transaction}, socket) do def handle_out("internal_transaction", %{address: address, internal_transaction: internal_transaction}, socket) do
Gettext.put_locale(BlockScoutWeb.Gettext, socket.assigns.locale) Gettext.put_locale(BlockScoutWeb.Gettext, socket.assigns.locale)
@ -120,6 +152,22 @@ defmodule BlockScoutWeb.AddressChannel do
def handle_out("token_transfer", data, socket), do: handle_token_transfer(data, socket, "token_transfer") def handle_out("token_transfer", data, socket), do: handle_token_transfer(data, socket, "token_transfer")
def handle_out(
"coin_balance",
%{block_number: block_number},
%Phoenix.Socket{handler: BlockScoutWeb.UserSocketV2} = socket
) do
coin_balance = Chain.get_coin_balance(socket.assigns.address_hash, block_number)
rendered_coin_balance = AddressViewAPI.render("coin_balance.json", %{coin_balance: coin_balance})
push(socket, "coin_balance", %{coin_balance: rendered_coin_balance})
push_current_coin_balance(socket, block_number, coin_balance)
{:noreply, socket}
end
def handle_out("coin_balance", %{block_number: block_number}, socket) do def handle_out("coin_balance", %{block_number: block_number}, socket) do
coin_balance = Chain.get_coin_balance(socket.assigns.address_hash, block_number) coin_balance = Chain.get_coin_balance(socket.assigns.address_hash, block_number)
@ -142,8 +190,23 @@ defmodule BlockScoutWeb.AddressChannel do
{:noreply, socket} {:noreply, socket}
end end
def handle_out("pending_transaction", data, %Phoenix.Socket{handler: BlockScoutWeb.UserSocketV2} = socket),
do: handle_transaction(data, socket, "pending_transaction")
def handle_out("pending_transaction", data, socket), do: handle_transaction(data, socket, "transaction") def handle_out("pending_transaction", data, socket), do: handle_transaction(data, socket, "transaction")
def push_current_coin_balance(
%Phoenix.Socket{handler: BlockScoutWeb.UserSocketV2} = socket,
block_number,
coin_balance
) do
push(socket, "current_coin_balance", %{
coin_balance: (coin_balance && coin_balance.value) || %Wei{value: Decimal.new(0)},
exchange_rate: (Market.get_exchange_rate(Explorer.coin()) || Token.null()).usd_value,
block_number: block_number
})
end
def push_current_coin_balance(socket, block_number, coin_balance) do def push_current_coin_balance(socket, block_number, coin_balance) do
{:ok, hash} = Chain.string_to_address_hash(socket.assigns.address_hash) {:ok, hash} = Chain.string_to_address_hash(socket.assigns.address_hash)
@ -172,6 +235,16 @@ defmodule BlockScoutWeb.AddressChannel do
}) })
end end
def handle_transaction(
%{address: _address, transaction: _transaction},
%Phoenix.Socket{handler: BlockScoutWeb.UserSocketV2} = socket,
event
) do
push(socket, event, %{transaction: 1})
{:noreply, socket}
end
def handle_transaction(%{address: address, transaction: transaction}, socket, event) do def handle_transaction(%{address: address, transaction: transaction}, socket, event) do
Gettext.put_locale(BlockScoutWeb.Gettext, socket.assigns.locale) Gettext.put_locale(BlockScoutWeb.Gettext, socket.assigns.locale)
@ -195,6 +268,16 @@ defmodule BlockScoutWeb.AddressChannel do
{:noreply, socket} {:noreply, socket}
end end
def handle_token_transfer(
%{address: _address, token_transfer: _token_transfer},
%Phoenix.Socket{handler: BlockScoutWeb.UserSocketV2} = socket,
event
) do
push(socket, event, %{token_transfer: 1})
{:noreply, socket}
end
def handle_token_transfer(%{address: address, token_transfer: token_transfer}, socket, event) do def handle_token_transfer(%{address: address, token_transfer: token_transfer}, socket, event) do
Gettext.put_locale(BlockScoutWeb.Gettext, socket.assigns.locale) Gettext.put_locale(BlockScoutWeb.Gettext, socket.assigns.locale)

@ -4,8 +4,10 @@ defmodule BlockScoutWeb.BlockChannel do
""" """
use BlockScoutWeb, :channel use BlockScoutWeb, :channel
alias BlockScoutWeb.API.V2.BlockView, as: BlockViewAPI
alias BlockScoutWeb.{BlockView, ChainView} alias BlockScoutWeb.{BlockView, ChainView}
alias Phoenix.View alias Phoenix.View
alias Timex.Duration
intercept(["new_block"]) intercept(["new_block"])
@ -17,6 +19,21 @@ defmodule BlockScoutWeb.BlockChannel do
{:ok, %{}, socket} {:ok, %{}, socket}
end end
def handle_out(
"new_block",
%{block: block, average_block_time: average_block_time},
%Phoenix.Socket{handler: BlockScoutWeb.UserSocketV2} = socket
) do
rendered_block = BlockViewAPI.render("block.json", %{block: block, socket: nil})
push(socket, "new_block", %{
average_block_time: to_string(Duration.to_milliseconds(average_block_time)),
block: rendered_block
})
{:noreply, socket}
end
def handle_out("new_block", %{block: block, average_block_time: average_block_time}, socket) do def handle_out("new_block", %{block: block, average_block_time: average_block_time}, socket) do
Gettext.put_locale(BlockScoutWeb.Gettext, socket.assigns.locale) Gettext.put_locale(BlockScoutWeb.Gettext, socket.assigns.locale)

@ -10,6 +10,20 @@ defmodule BlockScoutWeb.ExchangeRateChannel do
{:ok, %{}, socket} {:ok, %{}, socket}
end end
def handle_out(
"new_rate",
%{exchange_rate: exchange_rate, market_history_data: market_history_data},
%Phoenix.Socket{handler: BlockScoutWeb.UserSocketV2} = socket
) do
push(socket, "new_rate", %{
exchange_rate: exchange_rate.usd_value,
available_supply: exchange_rate.available_supply,
chart_data: market_history_data
})
{:noreply, socket}
end
def handle_out("new_rate", %{exchange_rate: exchange_rate, market_history_data: market_history_data}, socket) do def handle_out("new_rate", %{exchange_rate: exchange_rate, market_history_data: market_history_data}, socket) do
push(socket, "new_rate", %{ push(socket, "new_rate", %{
exchange_rate: exchange_rate, exchange_rate: exchange_rate,

@ -17,6 +17,16 @@ defmodule BlockScoutWeb.RewardChannel do
end end
end end
def handle_out(
"new_reward",
%{emission_funds: _emission_funds, validator: _validator},
%Phoenix.Socket{handler: BlockScoutWeb.UserSocketV2} = socket
) do
push(socket, "new_reward", %{reward: 1})
{:noreply, socket}
end
def handle_out("new_reward", %{emission_funds: emission_funds, validator: validator}, socket) do def handle_out("new_reward", %{emission_funds: emission_funds, validator: validator}, socket) do
Gettext.put_locale(BlockScoutWeb.Gettext, socket.assigns.locale) Gettext.put_locale(BlockScoutWeb.Gettext, socket.assigns.locale)

@ -14,12 +14,18 @@ defmodule BlockScoutWeb.TokenChannel do
{:ok, burn_address_hash} = Chain.string_to_address_hash("0x0000000000000000000000000000000000000000") {:ok, burn_address_hash} = Chain.string_to_address_hash("0x0000000000000000000000000000000000000000")
@burn_address_hash burn_address_hash @burn_address_hash burn_address_hash
def join("tokens:new_token_transfer", _params, socket) do def join("tokens:" <> _transaction_hash, _params, socket) do
{:ok, %{}, socket} {:ok, %{}, socket}
end end
def join("tokens:" <> _transaction_hash, _params, socket) do def handle_out(
{:ok, %{}, socket} "token_transfer",
%{token_transfer: _token_transfer},
%Phoenix.Socket{handler: BlockScoutWeb.UserSocketV2} = socket
) do
push(socket, "token_transfer", %{token_transfer: 1})
{:noreply, socket}
end end
def handle_out("token_transfer", %{token_transfer: token_transfer}, socket) do def handle_out("token_transfer", %{token_transfer: token_transfer}, socket) do

@ -30,6 +30,16 @@ defmodule BlockScoutWeb.TransactionChannel do
{:ok, %{}, socket} {:ok, %{}, socket}
end end
def handle_out(
"pending_transaction",
%{transaction: _transaction},
%Phoenix.Socket{handler: BlockScoutWeb.UserSocketV2} = socket
) do
push(socket, "pending_transaction", %{pending_transaction: 1})
{:noreply, socket}
end
def handle_out("pending_transaction", %{transaction: transaction}, socket) do def handle_out("pending_transaction", %{transaction: transaction}, socket) do
Gettext.put_locale(BlockScoutWeb.Gettext, socket.assigns.locale) Gettext.put_locale(BlockScoutWeb.Gettext, socket.assigns.locale)
@ -50,6 +60,16 @@ defmodule BlockScoutWeb.TransactionChannel do
{:noreply, socket} {:noreply, socket}
end end
def handle_out(
"transaction",
%{transaction: _transaction},
%Phoenix.Socket{handler: BlockScoutWeb.UserSocketV2} = socket
) do
push(socket, "transaction", %{transaction: 1})
{:noreply, socket}
end
def handle_out("transaction", %{transaction: transaction}, socket) do def handle_out("transaction", %{transaction: transaction}, socket) do
Gettext.put_locale(BlockScoutWeb.Gettext, socket.assigns.locale) Gettext.put_locale(BlockScoutWeb.Gettext, socket.assigns.locale)

@ -0,0 +1,19 @@
defmodule BlockScoutWeb.UserSocketV2 do
@moduledoc """
Module to distinct new and old UI websocket connections
"""
use Phoenix.Socket
channel("addresses:*", BlockScoutWeb.AddressChannel)
channel("blocks:*", BlockScoutWeb.BlockChannel)
channel("exchange_rate:*", BlockScoutWeb.ExchangeRateChannel)
channel("rewards:*", BlockScoutWeb.RewardChannel)
channel("transactions:*", BlockScoutWeb.TransactionChannel)
channel("tokens:*", BlockScoutWeb.TokenChannel)
def connect(_params, socket) do
{:ok, socket}
end
def id(_socket), do: nil
end

@ -0,0 +1,238 @@
defmodule BlockScoutWeb.API.V2.AddressController do
use BlockScoutWeb, :controller
import BlockScoutWeb.Chain,
only: [
next_page_params: 3,
paging_options: 1,
split_list_by_page: 1,
current_filter: 1
]
alias BlockScoutWeb.API.V2.{AddressView, BlockView, TransactionView}
alias Explorer.{Chain, Market}
alias Indexer.Fetcher.TokenBalanceOnDemand
@transaction_necessity_by_association [
necessity_by_association: %{
[created_contract_address: :names] => :optional,
[from_address: :names] => :optional,
[to_address: :names] => :optional,
:block => :optional,
[created_contract_address: :smart_contract] => :optional,
[from_address: :smart_contract] => :optional,
[to_address: :smart_contract] => :optional
}
]
@transaction_with_tt_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
}
]
action_fallback(BlockScoutWeb.API.V2.FallbackController)
def address(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
conn
|> put_status(200)
|> render(:address, %{address: address})
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 =
address_hash
|> Chain.fetch_last_token_balances()
Task.start_link(fn ->
TokenBalanceOnDemand.trigger_fetch(address_hash, token_balances)
end)
token_balances_with_price =
token_balances
|> Market.add_price()
conn
|> put_status(200)
|> render(:token_balances, %{token_balances: token_balances_with_price})
end
end
def transactions(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_necessity_by_association
|> Keyword.merge(paging_options(params))
|> Keyword.merge(current_filter(params))
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)
conn
|> put_status(200)
|> put_view(TransactionView)
|> render(:transactions, %{transactions: transactions, next_page_params: next_page_params})
end
end
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
|> Keyword.merge(paging_options(params))
|> Keyword.merge(current_filter(params))
results_plus_one =
Chain.address_hash_to_token_transfers(
address_hash,
options
)
{transactions, next_page} = split_list_by_page(results_plus_one)
next_page_params = next_page_params(next_page, transactions, params)
conn
|> put_status(200)
|> put_view(TransactionView)
|> render(:transactions, %{transactions: transactions, next_page_params: next_page_params})
end
end
def internal_transactions(conn, %{"address_hash" => address_hash_string} = params) do
with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)} do
full_options =
[
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
}
]
|> Keyword.merge(paging_options(params))
|> Keyword.merge(current_filter(params))
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)
conn
|> put_status(200)
|> put_view(TransactionView)
|> render(:internal_transactions, %{
internal_transactions: internal_transactions,
next_page_params: next_page_params
})
end
end
def logs(conn, %{"address_hash" => address_hash_string, "topic" => topic} = params) do
with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)} do
prepared_topic = String.trim(topic)
formatted_topic = if String.starts_with?(prepared_topic, "0x"), do: prepared_topic, else: "0x" <> prepared_topic
results_plus_one = Chain.address_to_logs(address_hash, topic: formatted_topic)
{logs, next_page} = split_list_by_page(results_plus_one)
next_page_params = next_page_params(next_page, logs, params)
conn
|> put_status(200)
|> put_view(TransactionView)
|> render(:logs, %{logs: logs, next_page_params: next_page_params})
end
end
def logs(conn, %{"address_hash" => address_hash_string} = params) do
with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)} 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)
conn
|> put_status(200)
|> put_view(TransactionView)
|> render(:logs, %{logs: logs, next_page_params: next_page_params})
end
end
def blocks_validated(conn, %{"address_hash" => address_hash_string} = params) do
with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)} do
full_options =
Keyword.merge(
[
necessity_by_association: %{
miner: :required,
nephews: :optional,
transactions: :optional,
rewards: :optional
}
],
paging_options(params)
)
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)
conn
|> put_status(200)
|> put_view(BlockView)
|> render(:blocks, %{blocks: blocks, next_page_params: next_page_params})
end
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
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)
conn
|> put_status(200)
|> put_view(AddressView)
|> render(:coin_balances, %{coin_balances: coin_balances, next_page_params: next_page_params})
end
end
def coin_balance_history_by_day(conn, %{"address_hash" => address_hash_string}) do
with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)} do
balances_by_day =
address_hash
|> Chain.address_to_balances_by_day(true)
conn
|> put_status(200)
|> put_view(AddressView)
|> render(:coin_balances_by_day, %{coin_balances_by_day: balances_by_day})
end
end
end

@ -0,0 +1,81 @@
defmodule BlockScoutWeb.API.V2.BlockController do
use BlockScoutWeb, :controller
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]
alias BlockScoutWeb.API.V2.TransactionView
alias BlockScoutWeb.BlockTransactionController
alias Explorer.Chain
@transaction_necessity_by_association [
necessity_by_association: %{
[created_contract_address: :names] => :optional,
[from_address: :names] => :optional,
[to_address: :names] => :optional,
:block => :optional,
[created_contract_address: :smart_contract] => :optional,
[from_address: :smart_contract] => :optional,
[to_address: :smart_contract] => :optional
}
]
action_fallback(BlockScoutWeb.API.V2.FallbackController)
def block(conn, %{"block_hash_or_number" => block_hash_or_number}) do
with {:ok, block} <-
BlockTransactionController.param_block_hash_or_number_to_block(block_hash_or_number,
necessity_by_association: %{
[miner: :names] => :required,
:uncles => :optional,
:nephews => :optional,
:rewards => :optional,
:transactions => :optional
}
) do
conn
|> put_status(200)
|> render(:block, %{block: block})
end
end
def blocks(conn, params) do
full_options = select_block_type(params)
blocks_plus_one =
full_options
|> Keyword.merge(paging_options(params))
|> Chain.list_blocks()
{blocks, next_page} = split_list_by_page(blocks_plus_one)
next_page_params = next_page_params(next_page, blocks, params)
conn
|> put_status(200)
|> render(:blocks, %{blocks: blocks, next_page_params: next_page_params})
end
def transactions(conn, %{"block_hash_or_number" => block_hash_or_number} = params) do
with {:ok, block} <- BlockTransactionController.param_block_hash_or_number_to_block(block_hash_or_number, []) do
full_options =
Keyword.merge(
@transaction_necessity_by_association,
put_key_value_to_paging_options(paging_options(params), :is_index_in_asc_order, true)
)
transactions_plus_one = Chain.block_to_transactions(block.hash, full_options, false)
{transactions, next_page} = split_list_by_page(transactions_plus_one)
next_page_params = next_page_params(next_page, transactions, params)
conn
|> put_status(200)
|> put_view(TransactionView)
|> render(:transactions, %{transactions: transactions, next_page_params: next_page_params})
end
end
end

@ -0,0 +1,11 @@
defmodule BlockScoutWeb.API.V2.ConfigController do
use BlockScoutWeb, :controller
def json_rpc_url(conn, _params) do
json_rpc_url = Application.get_env(:block_scout_web, :json_rpc)
conn
|> put_status(200)
|> render(:json_rpc_url, %{url: json_rpc_url})
end
end

@ -0,0 +1,38 @@
defmodule BlockScoutWeb.API.V2.FallbackController do
use Phoenix.Controller
alias BlockScoutWeb.API.V2.ApiView
def call(conn, {:format, _}) do
conn
|> put_status(:unprocessable_entity)
|> put_view(ApiView)
|> render(:message, %{message: "Invalid parameter(s)"})
end
def call(conn, {:not_found, _}) do
conn
|> put_status(:not_found)
|> put_view(ApiView)
|> render(:message, %{message: "Not found"})
end
def call(conn, {:error, {:invalid, :hash}}) do
conn
|> put_status(:unprocessable_entity)
|> put_view(ApiView)
|> render(:message, %{message: "Invalid hash"})
end
def call(conn, {:error, {:invalid, :number}}) do
conn
|> put_status(:unprocessable_entity)
|> put_view(ApiView)
|> render(:message, %{message: "Invalid number"})
end
def call(conn, {:error, :not_found}) do
conn
|> call({:not_found, nil})
end
end

@ -0,0 +1,40 @@
defmodule BlockScoutWeb.API.V2.MainPageController do
use Phoenix.Controller
alias Explorer.{Chain, PagingOptions}
alias BlockScoutWeb.API.V2.{BlockView, TransactionView}
alias Explorer.{Chain, Repo}
def blocks(conn, _params) do
blocks =
[paging_options: %PagingOptions{page_size: 4}]
|> Chain.list_blocks()
|> Repo.preload([[miner: :names], :transactions, :rewards])
conn
|> put_status(200)
|> put_view(BlockView)
|> render(:blocks, %{blocks: blocks})
end
def transactions(conn, _params) do
recent_transactions =
Chain.recent_collated_transactions(false,
necessity_by_association: %{
:block => :required,
[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
},
paging_options: %PagingOptions{page_size: 5}
)
conn
|> put_status(200)
|> put_view(TransactionView)
|> render(:transactions, %{transactions: recent_transactions})
end
end

@ -0,0 +1,24 @@
defmodule BlockScoutWeb.API.V2.SearchController do
use Phoenix.Controller
import BlockScoutWeb.Chain, only: [paging_options: 1, next_page_params: 3, split_list_by_page: 1]
alias Explorer.Chain
def search(conn, %{"q" => query} = params) do
[paging_options: paging_options] = paging_options(params)
offset = (max(paging_options.page_number, 1) - 1) * paging_options.page_size
search_results_plus_one =
paging_options
|> Chain.joint_search(offset, query)
{search_results, next_page} = split_list_by_page(search_results_plus_one)
next_page_params = next_page_params(next_page, search_results, params)
conn
|> put_status(200)
|> render(:search_results, %{search_results: search_results, next_page_params: next_page_params})
end
end

@ -0,0 +1,104 @@
defmodule BlockScoutWeb.API.V2.StatsController do
use Phoenix.Controller
alias BlockScoutWeb.API.V2.Helper
alias Explorer.{Chain, Market}
alias Explorer.Chain.Cache.Block, as: BlockCache
alias Explorer.Chain.Cache.{GasPriceOracle, GasUsage}
alias Explorer.Chain.Cache.Transaction, as: TransactionCache
alias Explorer.Chain.Supply.RSK
alias Explorer.Chain.Transaction.History.TransactionStats
alias Explorer.Counters.AverageBlockTime
alias Explorer.ExchangeRates.Token
alias Timex.Duration
def stats(conn, _params) do
market_cap_type =
case Application.get_env(:explorer, :supply) do
RSK ->
RSK
_ ->
:standard
end
exchange_rate = Market.get_exchange_rate(Explorer.coin()) || Token.null()
transaction_stats = Helper.get_transaction_stats()
gas_prices =
case GasPriceOracle.get_gas_prices() do
{:ok, gas_prices} ->
gas_prices
_ ->
nil
end
gas_price = Application.get_env(:block_scout_web, :gas_price)
json(
conn,
%{
"total_blocks" => BlockCache.estimated_count() |> to_string(),
"total_addresses" => Chain.address_estimated_count() |> to_string(),
"total_transactions" => TransactionCache.estimated_count() |> to_string(),
"average_block_time" => AverageBlockTime.average_block_time() |> Duration.to_milliseconds(),
"coin_price" => exchange_rate.usd_value,
"total_gas_used" => GasUsage.total() |> to_string(),
"transactions_today" => Enum.at(transaction_stats, 0).number_of_transactions |> to_string(),
"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)
}
)
end
def transactions_chart(conn, _params) do
[{:history_size, history_size}] =
Application.get_env(:block_scout_web, BlockScoutWeb.Chain.TransactionHistoryChartController, [{:history_size, 30}])
today = Date.utc_today()
latest = Date.add(today, -1)
earliest = Date.add(latest, -1 * history_size)
date_range = TransactionStats.by_date_range(earliest, latest)
transaction_history_data =
date_range
|> Enum.map(fn row ->
%{date: row.date, tx_count: row.number_of_transactions}
end)
json(conn, %{
chart_data: transaction_history_data
})
end
def market_chart(conn, _params) do
exchange_rate = Market.get_exchange_rate(Explorer.coin()) || Token.null()
recent_market_history = Market.fetch_recent_history()
market_history_data =
recent_market_history
|> case do
[today | the_rest] ->
[%{today | closing_price: exchange_rate.usd_value} | the_rest]
data ->
data
end
|> Enum.map(fn day -> Map.take(day, [:closing_price, :date]) end)
json(conn, %{
chart_data: market_history_data,
available_supply: available_supply(Chain.supply_for_days(), exchange_rate)
})
end
defp available_supply(:ok, exchange_rate), do: exchange_rate.available_supply || 0
defp available_supply({:ok, supply_for_days}, _exchange_rate), do: supply_for_days
end

@ -0,0 +1,221 @@
defmodule BlockScoutWeb.API.V2.TransactionController do
use BlockScoutWeb, :controller
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]
alias Explorer.Chain
alias Explorer.Chain.Import
alias Explorer.Chain.Import.Runner.InternalTransactions
action_fallback(BlockScoutWeb.API.V2.FallbackController)
@transaction_necessity_by_association %{
:block => :optional,
[created_contract_address: :names] => :optional,
[created_contract_address: :token] => :optional,
[from_address: :names] => :optional,
[to_address: :names] => :optional,
[to_address: :smart_contract] => :optional
}
@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: :required
}
@internal_transaction_neccessity_by_association [
necessity_by_association: %{
[created_contract_address: :names] => :optional,
[from_address: :names] => :optional,
[to_address: :names] => :optional,
[transaction: :block] => :optional,
[created_contract_address: :smart_contract] => :optional,
[from_address: :smart_contract] => :optional,
[to_address: :smart_contract] => :optional
}
]
def transaction(conn, %{"transaction_hash" => transaction_hash_string}) do
with {:format, {:ok, transaction_hash}} <- {:format, Chain.string_to_transaction_hash(transaction_hash_string)},
{:not_found, {:ok, transaction}} <-
{:not_found,
Chain.hash_to_transaction(
transaction_hash,
necessity_by_association: @transaction_necessity_by_association
)},
preloaded <- Chain.preload_token_transfers(transaction, @token_transfers_neccessity_by_association, false) do
conn
|> put_status(200)
|> render(:transaction, %{transaction: preloaded})
end
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)
full_options =
Keyword.merge(
[
necessity_by_association: @transaction_necessity_by_association
],
paging_options(params, filter_options)
)
transactions_plus_one =
Chain.recent_transactions(full_options, filter_options, method_filter_options, type_filter_options)
{transactions, next_page} = split_list_by_page(transactions_plus_one)
next_page_params = next_page_params(next_page, transactions, params)
conn
|> put_status(200)
|> render(:transactions, %{transactions: transactions, next_page_params: next_page_params})
end
def raw_trace(conn, %{"transaction_hash" => transaction_hash_string}) do
with {:format, {:ok, transaction_hash}} <- {:format, Chain.string_to_transaction_hash(transaction_hash_string)},
{:not_found, {:ok, transaction}} <-
{:not_found, Chain.hash_to_transaction(transaction_hash)} do
if is_nil(transaction.block_number) do
conn
|> put_status(200)
|> render(:raw_trace, %{internal_transactions: []})
else
internal_transactions = Chain.all_transaction_to_internal_transactions(transaction_hash)
first_trace_exists =
Enum.find_index(internal_transactions, fn trace ->
trace.index == 0
end)
json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments)
internal_transactions =
if first_trace_exists do
internal_transactions
else
response =
Chain.fetch_first_trace(
[
%{
block_hash: transaction.block_hash,
block_number: transaction.block_number,
hash_data: transaction_hash_string,
transaction_index: transaction.index
}
],
json_rpc_named_arguments
)
case response do
{:ok, first_trace_params} ->
InternalTransactions.run_insert_only(first_trace_params, %{
timeout: :infinity,
timestamps: Import.timestamps(),
internal_transactions: %{params: first_trace_params}
})
Chain.all_transaction_to_internal_transactions(transaction_hash)
{:error, _} ->
internal_transactions
:ignore ->
internal_transactions
end
end
conn
|> put_status(200)
|> render(:raw_trace, %{internal_transactions: internal_transactions})
end
end
end
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)
)
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)
conn
|> put_status(200)
|> render(:token_transfers, %{token_transfers: token_transfers, next_page_params: next_page_params})
end
end
def internal_transactions(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(
@internal_transaction_neccessity_by_association,
paging_options(params)
)
internal_transactions_plus_one = Chain.transaction_to_internal_transactions(transaction_hash, full_options)
{internal_transactions, next_page} = split_list_by_page(internal_transactions_plus_one)
next_page_params = next_page_params(next_page, internal_transactions, params)
conn
|> put_status(200)
|> render(:internal_transactions, %{
internal_transactions: internal_transactions,
next_page_params: next_page_params
})
end
end
def logs(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: %{
[address: :names] => :optional,
[address: :smart_contract] => :optional,
address: :optional
}
],
paging_options(params)
)
from_api = true
logs_plus_one = Chain.transaction_to_logs(transaction_hash, from_api, full_options)
{logs, next_page} = split_list_by_page(logs_plus_one)
next_page_params = next_page_params(next_page, logs, params)
conn
|> put_status(200)
|> render(:logs, %{
tx_hash: transaction_hash,
logs: logs,
next_page_params: next_page_params
})
end
end
end

@ -133,7 +133,7 @@ defmodule BlockScoutWeb.BlockTransactionController do
end end
end end
defp param_block_hash_or_number_to_block("0x" <> _ = param, options) do def param_block_hash_or_number_to_block("0x" <> _ = param, options) do
case string_to_block_hash(param) do case string_to_block_hash(param) do
{:ok, hash} -> {:ok, hash} ->
hash_to_block(hash, options) hash_to_block(hash, options)
@ -143,8 +143,8 @@ defmodule BlockScoutWeb.BlockTransactionController do
end end
end end
defp param_block_hash_or_number_to_block(number_string, options) def param_block_hash_or_number_to_block(number_string, options)
when is_binary(number_string) do when is_binary(number_string) do
case BlockScoutWeb.Chain.param_to_block_number(number_string) do case BlockScoutWeb.Chain.param_to_block_number(number_string) do
{:ok, number} -> {:ok, number} ->
number_to_block(number, options) number_to_block(number, options)

@ -5,7 +5,7 @@ defmodule BlockScoutWeb.Chain.TransactionHistoryChartController do
def show(conn, _params) do def show(conn, _params) do
if ajax?(conn) do if ajax?(conn) do
[{:history_size, history_size}] = Application.get_env(:block_scout_web, __MODULE__, 30) [{:history_size, history_size}] = Application.get_env(:block_scout_web, __MODULE__, [{:history_size, 30}])
today = Date.utc_today() today = Date.utc_today()
latest = Date.add(today, -1) latest = Date.add(today, -1)

@ -3,6 +3,7 @@ defmodule BlockScoutWeb.ChainController do
import BlockScoutWeb.Chain, only: [paging_options: 1] import BlockScoutWeb.Chain, only: [paging_options: 1]
alias BlockScoutWeb.API.V2.Helper
alias BlockScoutWeb.{ChainView, Controller} alias BlockScoutWeb.{ChainView, Controller}
alias Explorer.{Chain, PagingOptions, Repo} alias Explorer.{Chain, PagingOptions, Repo}
alias Explorer.Chain.{Address, Block, Transaction} alias Explorer.Chain.{Address, Block, Transaction}
@ -10,7 +11,6 @@ defmodule BlockScoutWeb.ChainController do
alias Explorer.Chain.Cache.GasUsage alias Explorer.Chain.Cache.GasUsage
alias Explorer.Chain.Cache.Transaction, as: TransactionCache alias Explorer.Chain.Cache.Transaction, as: TransactionCache
alias Explorer.Chain.Supply.RSK alias Explorer.Chain.Supply.RSK
alias Explorer.Chain.Transaction.History.TransactionStats
alias Explorer.Counters.AverageBlockTime alias Explorer.Counters.AverageBlockTime
alias Explorer.ExchangeRates.Token alias Explorer.ExchangeRates.Token
alias Explorer.Market alias Explorer.Market
@ -33,7 +33,7 @@ defmodule BlockScoutWeb.ChainController do
exchange_rate = Market.get_exchange_rate(Explorer.coin()) || Token.null() exchange_rate = Market.get_exchange_rate(Explorer.coin()) || Token.null()
transaction_stats = get_transaction_stats() transaction_stats = Helper.get_transaction_stats()
chart_data_paths = %{ chart_data_paths = %{
market: market_history_chart_path(conn, :show), market: market_history_chart_path(conn, :show),
@ -61,25 +61,6 @@ defmodule BlockScoutWeb.ChainController do
) )
end end
def get_transaction_stats do
stats_scale = date_range(1)
transaction_stats = TransactionStats.by_date_range(stats_scale.earliest, stats_scale.latest)
# Need datapoint for legend if none currently available.
if Enum.empty?(transaction_stats) do
[%{number_of_transactions: 0, gas_used: 0}]
else
transaction_stats
end
end
def date_range(num_days) do
today = Date.utc_today()
latest = Date.add(today, -1)
x_days_back = Date.add(latest, -1 * (num_days - 1))
%{earliest: x_days_back, latest: latest}
end
def search(conn, %{"q" => ""}) do def search(conn, %{"q" => ""}) do
show(conn, []) show(conn, [])
end end
@ -110,7 +91,7 @@ defmodule BlockScoutWeb.ChainController do
results = results =
paging_options paging_options
|> search_by(offset, term) |> Chain.joint_search(offset, term)
encoded_results = encoded_results =
results results
@ -144,10 +125,6 @@ defmodule BlockScoutWeb.ChainController do
json(conn, "{}") json(conn, "{}")
end end
def search_by(paging_options, offset, term) do
Chain.joint_search(paging_options, offset, term)
end
def chain_blocks(conn, _params) do def chain_blocks(conn, _params) do
if ajax?(conn) do if ajax?(conn) do
blocks = blocks =

@ -1,20 +0,0 @@
defmodule BlockScoutWeb.PaginationHelpers do
@moduledoc """
Common pagination logic helpers.
"""
def current_page_number(params) do
cond do
!params["prev_page_number"] -> 1
params["next_page"] -> String.to_integer(params["prev_page_number"]) + 1
params["prev_page"] -> String.to_integer(params["prev_page_number"]) - 1
end
end
def add_navigation_params(params, current_page_path, current_page_number) do
params
|> Map.put("prev_page_path", current_page_path)
|> Map.put("next_page", true)
|> Map.put("prev_page_number", current_page_number)
end
end

@ -63,7 +63,7 @@ defmodule BlockScoutWeb.PendingTransactionController do
end end
defp get_pending_transactions_and_next_page(options) do defp get_pending_transactions_and_next_page(options) do
transactions_plus_one = Chain.recent_pending_transactions(options) transactions_plus_one = Chain.recent_pending_transactions(options, true)
split_list_by_page(transactions_plus_one) split_list_by_page(transactions_plus_one)
end end
end end

@ -11,7 +11,7 @@ defmodule BlockScoutWeb.RecentTransactionsController do
def index(conn, _params) do def index(conn, _params) do
if ajax?(conn) do if ajax?(conn) do
recent_transactions = recent_transactions =
Chain.recent_collated_transactions( Chain.recent_collated_transactions(true,
necessity_by_association: %{ necessity_by_association: %{
:block => :required, :block => :required,
[created_contract_address: :names] => :optional, [created_contract_address: :names] => :optional,

@ -3,7 +3,8 @@ defmodule BlockScoutWeb.SearchController do
import BlockScoutWeb.Chain, only: [paging_options: 1, next_page_params: 3, split_list_by_page: 1] import BlockScoutWeb.Chain, only: [paging_options: 1, next_page_params: 3, split_list_by_page: 1]
alias BlockScoutWeb.{ChainController, Controller, SearchView} alias BlockScoutWeb.{Controller, SearchView}
alias Explorer.Chain
alias Phoenix.View alias Phoenix.View
def search_results(conn, %{"q" => query, "type" => "JSON"} = params) do def search_results(conn, %{"q" => query, "type" => "JSON"} = params) do
@ -12,7 +13,7 @@ defmodule BlockScoutWeb.SearchController do
search_results_plus_one = search_results_plus_one =
paging_options paging_options
|> ChainController.search_by(offset, query) |> Chain.joint_search(offset, query)
{search_results, next_page} = split_list_by_page(search_results_plus_one) {search_results, next_page} = split_list_by_page(search_results_plus_one)
@ -73,21 +74,4 @@ defmodule BlockScoutWeb.SearchController do
current_path: Controller.current_full_path(conn) current_path: Controller.current_full_path(conn)
) )
end end
def api_search_result(conn, %{"q" => query} = params) do
[paging_options: paging_options] = paging_options(params)
offset = (max(paging_options.page_number, 1) - 1) * paging_options.page_size
search_results_plus_one =
paging_options
|> ChainController.search_by(offset, query)
{search_results, next_page} = split_list_by_page(search_results_plus_one)
next_page_params = next_page_params(next_page, search_results, params)
conn
|> put_status(200)
|> render(:search_results, %{search_results: search_results, next_page_params: next_page_params})
end
end end

@ -7,6 +7,7 @@ defmodule BlockScoutWeb.Endpoint do
end end
socket("/socket", BlockScoutWeb.UserSocket, websocket: [timeout: 45_000]) socket("/socket", BlockScoutWeb.UserSocket, websocket: [timeout: 45_000])
socket("/socket/v2", BlockScoutWeb.UserSocketV2, websocket: [timeout: 45_000])
# Serve at "/" the static files from "priv/static" directory. # Serve at "/" the static files from "priv/static" directory.
# #

@ -6,14 +6,13 @@ defmodule BlockScoutWeb.Models.GetAddressTags do
import Ecto.Query, only: [from: 2] import Ecto.Query, only: [from: 2]
alias Explorer.Account.{TagAddress, WatchlistAddress} alias Explorer.Account.{TagAddress, WatchlistAddress}
alias Explorer.Chain.Hash
alias Explorer.Repo alias Explorer.Repo
alias Explorer.Tags.{AddressTag, AddressToTag} alias Explorer.Tags.{AddressTag, AddressToTag}
def get_address_tags(nil, nil), def get_address_tags(nil, nil),
do: %{common_tags: [], personal_tags: [], watchlist_names: []} do: %{common_tags: [], personal_tags: [], watchlist_names: []}
def get_address_tags(%Hash{} = address_hash, current_user) do def get_address_tags(address_hash, current_user) when not is_nil(address_hash) do
%{ %{
common_tags: get_tags_on_address(address_hash), common_tags: get_tags_on_address(address_hash),
personal_tags: get_personal_tags(address_hash, current_user), personal_tags: get_personal_tags(address_hash, current_user),
@ -23,13 +22,13 @@ defmodule BlockScoutWeb.Models.GetAddressTags do
def get_address_tags(_, _), do: %{common_tags: [], personal_tags: [], watchlist_names: []} def get_address_tags(_, _), do: %{common_tags: [], personal_tags: [], watchlist_names: []}
def get_public_tags(%Hash{} = address_hash) do def get_public_tags(address_hash) when not is_nil(address_hash) do
%{ %{
common_tags: get_tags_on_address(address_hash) common_tags: get_tags_on_address(address_hash)
} }
end end
def get_tags_on_address(%Hash{} = address_hash) do def get_tags_on_address(address_hash) when not is_nil(address_hash) do
query = query =
from( from(
tt in AddressTag, tt in AddressTag,
@ -45,7 +44,7 @@ defmodule BlockScoutWeb.Models.GetAddressTags do
def get_tags_on_address(_), do: [] def get_tags_on_address(_), do: []
def get_personal_tags(%Hash{} = address_hash, %{id: id}) do def get_personal_tags(address_hash, %{id: id}) when not is_nil(address_hash) do
query = query =
from( from(
ta in TagAddress, ta in TagAddress,
@ -59,7 +58,7 @@ defmodule BlockScoutWeb.Models.GetAddressTags do
def get_personal_tags(_, _), do: [] def get_personal_tags(_, _), do: []
def get_watchlist_names_on_address(%Hash{} = address_hash, %{watchlist_id: watchlist_id}) do def get_watchlist_names_on_address(address_hash, %{watchlist_id: watchlist_id}) when not is_nil(address_hash) do
query = query =
from( from(
wa in WatchlistAddress, wa in WatchlistAddress,

@ -184,7 +184,7 @@ defmodule BlockScoutWeb.Notifier do
today = Date.utc_today() today = Date.utc_today()
[{:history_size, history_size}] = [{:history_size, history_size}] =
Application.get_env(:block_scout_web, BlockScoutWeb.Chain.TransactionHistoryChartController, 30) Application.get_env(:block_scout_web, BlockScoutWeb.Chain.TransactionHistoryChartController, {:history_size, 30})
x_days_back = Date.add(today, -1 * history_size) x_days_back = Date.add(today, -1 * history_size)
@ -371,8 +371,6 @@ defmodule BlockScoutWeb.Notifier do
end end
defp broadcast_token_transfer(token_transfer, event) do defp broadcast_token_transfer(token_transfer, event) do
Endpoint.broadcast("token_transfers:#{token_transfer.transaction_hash}", event, %{})
Endpoint.broadcast("addresses:#{token_transfer.from_address_hash}", event, %{ Endpoint.broadcast("addresses:#{token_transfer.from_address_hash}", event, %{
address: token_transfer.from_address, address: token_transfer.from_address,
token_transfer: token_transfer token_transfer: token_transfer

@ -0,0 +1,117 @@
defmodule BlockScoutWeb.PagingHelper do
@moduledoc """
Helper for fetching filters and other url query paramters
"""
import Explorer.Chain, only: [string_to_transaction_hash: 1]
alias Explorer.PagingOptions
@page_size 50
@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"]
def paging_options(%{"block_number" => block_number_string, "index" => index_string}, [:validated | _]) do
with {block_number, ""} <- Integer.parse(block_number_string),
{index, ""} <- Integer.parse(index_string) do
[paging_options: %{@default_paging_options | key: {block_number, index}}]
else
_ ->
[paging_options: @default_paging_options]
end
end
def paging_options(%{"inserted_at" => inserted_at_string, "hash" => hash_string}, [:pending | _]) do
with {:ok, inserted_at, _} <- DateTime.from_iso8601(inserted_at_string),
{:ok, hash} <- string_to_transaction_hash(hash_string) do
[paging_options: %{@default_paging_options | key: {inserted_at, hash}, is_pending_tx: true}]
else
_ ->
[paging_options: @default_paging_options]
end
end
def paging_options(_params, _filter), do: [paging_options: @default_paging_options]
def filter_options(%{"filter" => filter}) do
parse_filter(filter, @allowed_filter_labels)
end
def filter_options(_params), do: []
def type_filter_options(%{"type" => type}) do
parse_filter(type, @allowed_type_labels)
end
def type_filter_options(_params), do: []
def method_filter_options(%{"method" => method}) do
parse_method_filter(method)
end
def method_filter_options(_params), do: []
def parse_filter("[" <> filter, allowed_labels) do
filter
|> String.trim_trailing("]")
|> 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
filter
|> String.trim_trailing("]")
|> parse_method_filter()
end
def parse_method_filter(filter) do
filter
|> String.split(",")
|> Enum.uniq()
end
def select_block_type(%{"type" => type}) do
case String.downcase(type) do
"uncle" ->
[
necessity_by_association: %{
:transactions => :optional,
[miner: :names] => :optional,
:nephews => :required,
:rewards => :optional
},
block_type: "Uncle"
]
"reorg" ->
[
necessity_by_association: %{
:transactions => :optional,
[miner: :names] => :optional,
:rewards => :optional
},
block_type: "Reorg"
]
_ ->
select_block_type(nil)
end
end
def select_block_type(_),
do: [
necessity_by_association: %{
:transactions => :optional,
[miner: :names] => :optional,
:rewards => :optional
},
block_type: "Block"
]
end

@ -0,0 +1,21 @@
defmodule BlockScoutWeb.Plug.CheckApiV2 do
@moduledoc """
Checks if the API V2 enabled.
"""
import Plug.Conn
alias BlockScoutWeb.API.V2, as: API_V2
def init(opts), do: opts
def call(conn, _opts) do
if API_V2.enabled?() do
conn
else
conn
|> put_resp_content_type("application/json")
|> send_resp(404, Jason.encode!(%{message: "API V2 is disabled"}))
|> halt()
end
end
end

@ -17,7 +17,7 @@
<%= link( <%= link(
to: address_token_transfers_path(@conn, :index, to_string(@address.hash), to_string(@token.contract_address_hash)), to: address_token_transfers_path(@conn, :index, to_string(@address.hash), to_string(@token.contract_address_hash)),
class: "tile-title-lg", class: "tile-title-lg",
"data-test": "token_transfers_#{@token_balance.token.contract_address_hash}" "data-test": "token_transfers_#{@token.contract_address_hash}"
) do %> ) do %>
<%= token_name(@token) %> <%= token_name(@token) %>
<% end %> <% end %>
@ -33,14 +33,14 @@
</td> </td>
<td class="stakes-td"> <td class="stakes-td">
<p class="mb-0 col-md-6 text-right text-muted"> <p class="mb-0 col-md-6 text-right text-muted">
<% token_price = if @token_balance.token.usd_value, do: @token_balance.token.usd_value, else: nil %> <% token_price = if @token.usd_value, do: @token.usd_value, else: nil %>
<span data-selector="token-price" data-token-usd-value="<%= @token_balance.token.usd_value %>"><%= ChainView.format_currency_value(token_price, "@") %></span> <span data-selector="token-price" data-token-usd-value="<%= @token.usd_value %>"><%= ChainView.format_currency_value(token_price, "@") %></span>
</p> </p>
</td> </td>
<td class="stakes-td"> <td class="stakes-td">
<%= if @token_balance.token.usd_value do %> <%= if @token.usd_value do %>
<p class="mb-0 col-md-6 text-right"> <p class="mb-0 col-md-6 text-right">
<span data-selector="token-balance-usd" data-usd-value="<%= Chain.balance_in_usd(@token_balance) %>"><%= ChainView.format_usd_value(Chain.balance_in_usd(@token_balance)) %></span> <span data-selector="token-balance-usd" data-usd-value="<%= Chain.balance_in_usd(@token_balance, @token) %>"><%= ChainView.format_usd_value(Chain.balance_in_usd(@token_balance, @token)) %></span>
</p> </p>
<% end %> <% end %>
</td> </td>

@ -47,7 +47,7 @@
placeholder: gettext("Search tokens") placeholder: gettext("Search tokens")
) %> ) %>
</div> </div>
<%= if Enum.any?(@token_balances, fn {token_balance, _} -> token_balance.token.type == "ERC-721" end) do %> <%= if Enum.any?(@token_balances, fn {_token_balance, token} -> token.type == "ERC-721" end) do %>
<%= render( <%= render(
"_tokens.html", "_tokens.html",
conn: @conn, conn: @conn,
@ -56,7 +56,7 @@
) %> ) %>
<% end %> <% end %>
<%= if Enum.any?(@token_balances, fn {token_balance, _} -> token_balance.token.type == "ERC-1155" end) do %> <%= if Enum.any?(@token_balances, fn {_token_balance, token} -> token.type == "ERC-1155" end) do %>
<%= render( <%= render(
"_tokens.html", "_tokens.html",
conn: @conn, conn: @conn,
@ -65,7 +65,7 @@
) %> ) %>
<% end %> <% end %>
<%= if Enum.any?(@token_balances, fn {token_balance, _} -> token_balance.token.type == "ERC-20" end) do %> <%= if Enum.any?(@token_balances, fn {_token_balance, token} -> token.type == "ERC-20" end) do %>
<%= render( <%= render(
"_tokens.html", "_tokens.html",
conn: @conn, conn: @conn,

@ -7,13 +7,13 @@
<div <div
class="border-bottom" class="border-bottom"
data-dropdown-token-balance-test data-dropdown-token-balance-test
data-token-name="<%= token_name(token_balance.token) %>" data-token-name="<%= token_name(token) %>"
data-token-symbol="<%= token_symbol(token_balance.token) %>" data-token-symbol="<%= token_symbol(token) %>"
> >
<% path = cond do <% path = cond do
token_balance.token_type == "ERC-721" && !is_nil(token_balance.token_id) -> token_instance_path(@conn, :show, token_balance.token.contract_address_hash, to_string(token_balance.token_id)) token_balance.token_type == "ERC-721" && !is_nil(token_balance.token_id) -> token_instance_path(@conn, :show, token.contract_address_hash, to_string(token_balance.token_id))
token_balance.token_type == "ERC-1155" && !is_nil(token_balance.token_id) -> token_instance_path(@conn, :show, token_balance.token.contract_address_hash, to_string(token_balance.token_id)) token_balance.token_type == "ERC-1155" && !is_nil(token_balance.token_id) -> token_instance_path(@conn, :show, token.contract_address_hash, to_string(token_balance.token_id))
true -> token_path(@conn, :show, to_string(token_balance.token.contract_address_hash)) true -> token_path(@conn, :show, to_string(token.contract_address_hash))
end end
%> %>
<%= link( <%= link(
@ -34,16 +34,16 @@
<% end %> <% end %>
<p class="mb-0 col-md-6 pl-0 pr-0 el-1 flex-grow-2 <%= if System.get_env("DISPLAY_TOKEN_ICONS") !== "true", do: "ml-5px" %>"><%= token_name(token) %> <p class="mb-0 col-md-6 pl-0 pr-0 el-1 flex-grow-2 <%= if System.get_env("DISPLAY_TOKEN_ICONS") !== "true", do: "ml-5px" %>"><%= token_name(token) %>
</p> </p>
<%= if token_balance.token.usd_value do %> <%= if token.usd_value do %>
<p class="mb-0 col-md-6 text-right usd-total"> <p class="mb-0 col-md-6 text-right usd-total">
<span data-selector="token-balance-usd" data-usd-value="<%= Chain.balance_in_usd(token_balance) %>"></span> <span data-selector="token-balance-usd" data-usd-value="<%= Chain.balance_in_usd(token_balance, token) %>"></span>
</p> </p>
<% end %> <% end %>
</div> </div>
<div class="row dropdown-row wh-sp"> <div class="row dropdown-row wh-sp">
<%= if token_balance.token.usd_value do %> <%= if token.usd_value do %>
<p class="mb-0 text-right text-muted usd-rate"> <p class="mb-0 text-right text-muted usd-rate">
<span data-selector="token-price" data-token-usd-value="<%= token_balance.token.usd_value %>"></span> <span data-selector="token-price" data-token-usd-value="<%= token.usd_value %>"></span>
</p> </p>
<% end %> <% end %>
</div> </div>
@ -52,7 +52,7 @@
<%= if token_balance.token_type == "ERC-721" && !is_nil(token_balance.token_id) do %> <%= if token_balance.token_type == "ERC-721" && !is_nil(token_balance.token_id) do %>
1 1
<% else %> <% else %>
<%= format_according_to_decimals(token_balance.value, token_balance.token.decimals) %> <%= token_symbol(token_balance.token) %> <%= format_according_to_decimals(token_balance.value, token.decimals) %> <%= token_symbol(token) %>
<% end %> <% end %>
<%= if (token_balance.token_type == "ERC-721" && !is_nil(token_balance.token_id)) or token_balance.token_type == "ERC-1155" do %> <%= if (token_balance.token_type == "ERC-721" && !is_nil(token_balance.token_id)) or token_balance.token_type == "ERC-1155" do %>
<%= " TokenID " <> to_string(token_balance.token_id) %> <%= " TokenID " <> to_string(token_balance.token_id) %>

@ -27,6 +27,19 @@ defmodule BlockScoutWeb.ABIEncodedValueView do
:error :error
end end
def value_json(type, value) do
decoded_type = FunctionSelector.decode_type(type)
do_value_json(decoded_type, value)
rescue
exception ->
Logger.warn(fn ->
["Error determining value json for #{inspect(type)}: ", Exception.format(:error, exception)]
end)
nil
end
def copy_text(type, value) do def copy_text(type, value) do
decoded_type = FunctionSelector.decode_type(type) decoded_type = FunctionSelector.decode_type(type)
@ -145,5 +158,56 @@ defmodule BlockScoutWeb.ABIEncodedValueView do
defp base_value_html(_, value, _no_links), do: HTML.html_escape(value) defp base_value_html(_, value, _no_links), do: HTML.html_escape(value)
defp do_value_json({:bytes, _}, value) do
do_value_json(:bytes, value)
end
defp do_value_json({:array, type, _}, value) do
do_value_json({:array, type}, value)
end
defp do_value_json({:array, type}, value) do
values =
Enum.map(value, fn inner_value ->
do_value_json(type, inner_value)
end)
values
end
defp do_value_json({:tuple, types}, values) do
values_list =
values
|> Tuple.to_list()
|> Enum.with_index()
|> Enum.map(fn {value, i} ->
do_value_json(Enum.at(types, i), value)
end)
values_list
end
defp do_value_json(type, value) do
base_value_json(type, value)
end
defp base_value_json(_, {:dynamic, value}) do
hex(value)
end
defp base_value_json(:address, value) do
hex(value)
end
defp base_value_json(:address_text, value) do
hex(value)
end
defp base_value_json(:bytes, value) do
hex(value)
end
defp base_value_json(_, value), do: value
defp hex(value), do: "0x" <> Base.encode16(value, case: :lower) defp hex(value), do: "0x" <> Base.encode16(value, case: :lower)
end end

@ -11,7 +11,7 @@ defmodule BlockScoutWeb.AddressTokenBalanceView do
end end
def filter_by_type(token_balances, type) do def filter_by_type(token_balances, type) do
Enum.filter(token_balances, fn {token_balance, _} -> token_balance.token.type == type end) Enum.filter(token_balances, fn {_token_balance, token} -> token.type == type end)
end end
@doc """ @doc """
@ -30,15 +30,23 @@ defmodule BlockScoutWeb.AddressTokenBalanceView do
def sort_by_usd_value_and_name(token_balances) do def sort_by_usd_value_and_name(token_balances) do
token_balances token_balances
|> Enum.sort(fn {token_balance1, token1}, {token_balance2, token2} -> |> Enum.sort(fn {token_balance1, token1}, {token_balance2, token2} ->
usd_value1 = token_balance1.token.usd_value usd_value1 = token1.usd_value
usd_value2 = token_balance2.token.usd_value usd_value2 = token2.usd_value
token_name1 = token1.name token_name1 = token1.name
token_name2 = token2.name token_name2 = token2.name
sort_by_name = sort_2_tokens_by_name(token_name1, token_name2) sort_by_name = sort_2_tokens_by_name(token_name1, token_name2)
sort_2_tokens_by_value_desc_and_name(token_balance1, token_balance2, usd_value1, usd_value2, sort_by_name) sort_2_tokens_by_value_desc_and_name(
token_balance1,
token_balance2,
usd_value1,
usd_value2,
sort_by_name,
token1,
token2
)
end) end)
end end
@ -58,9 +66,17 @@ defmodule BlockScoutWeb.AddressTokenBalanceView do
end end
end end
defp sort_2_tokens_by_value_desc_and_name(token_balance1, token_balance2, usd_value1, usd_value2, sort_by_name) defp sort_2_tokens_by_value_desc_and_name(
token_balance1,
token_balance2,
usd_value1,
usd_value2,
sort_by_name,
token1,
token2
)
when not is_nil(usd_value1) and not is_nil(usd_value2) do when not is_nil(usd_value1) and not is_nil(usd_value2) do
case Decimal.compare(Chain.balance_in_usd(token_balance1), Chain.balance_in_usd(token_balance2)) do case Decimal.compare(Chain.balance_in_usd(token_balance1, token1), Chain.balance_in_usd(token_balance2, token2)) do
:gt -> :gt ->
true true
@ -72,17 +88,41 @@ defmodule BlockScoutWeb.AddressTokenBalanceView do
end end
end end
defp sort_2_tokens_by_value_desc_and_name(_token_balance1, _token_balance2, usd_value1, usd_value2, _sort_by_name) defp sort_2_tokens_by_value_desc_and_name(
_token_balance1,
_token_balance2,
usd_value1,
usd_value2,
_sort_by_name,
_token1,
_token2
)
when not is_nil(usd_value1) and is_nil(usd_value2) do when not is_nil(usd_value1) and is_nil(usd_value2) do
true true
end end
defp sort_2_tokens_by_value_desc_and_name(_token_balance1, _token_balance2, usd_value1, usd_value2, _sort_by_name) defp sort_2_tokens_by_value_desc_and_name(
_token_balance1,
_token_balance2,
usd_value1,
usd_value2,
_sort_by_name,
_token1,
_token2
)
when is_nil(usd_value1) and not is_nil(usd_value2) do when is_nil(usd_value1) and not is_nil(usd_value2) do
false false
end end
defp sort_2_tokens_by_value_desc_and_name(_token_balance1, _token_balance2, usd_value1, usd_value2, sort_by_name) defp sort_2_tokens_by_value_desc_and_name(
_token_balance1,
_token_balance2,
usd_value1,
usd_value2,
sort_by_name,
_token1,
_token2
)
when is_nil(usd_value1) and is_nil(usd_value2) do when is_nil(usd_value1) and is_nil(usd_value2) do
sort_by_name sort_by_name
end end

@ -0,0 +1,59 @@
defmodule BlockScoutWeb.API.V2.AddressView do
use BlockScoutWeb, :view
alias BlockScoutWeb.API.V2.{ApiView, Helper, TokenView}
alias BlockScoutWeb.API.V2.Helper
def render("message.json", assigns) do
ApiView.render("message.json", assigns)
end
def render("address.json", %{address: address, conn: conn}) do
prepare_address(address, conn)
end
def render("token_balances.json", %{token_balances: token_balances}) do
Enum.map(token_balances, &prepare_token_balance/1)
end
def render("coin_balance.json", %{coin_balance: coin_balance}) do
prepare_coin_balance_history_entry(coin_balance)
end
def render("coin_balances.json", %{coin_balances: coin_balances, next_page_params: next_page_params}) do
%{"items" => Enum.map(coin_balances, &prepare_coin_balance_history_entry/1), "next_page_params" => next_page_params}
end
def render("coin_balances_by_day.json", %{coin_balances_by_day: coin_balances_by_day}) do
Enum.map(coin_balances_by_day, &prepare_coin_balance_history_by_day_entry/1)
end
def prepare_address(address, conn \\ nil) do
Helper.address_with_info(conn, address, address.hash)
end
def prepare_token_balance({token_balance, token}) do
%{
"value" => token_balance.value,
"token" => TokenView.render("token.json", %{token: token}),
"token_id" => token_balance.token_id
}
end
def prepare_coin_balance_history_entry(coin_balance) do
%{
"transaction_hash" => coin_balance.transaction_hash,
"block_number" => coin_balance.block_number,
"delta" => coin_balance.delta,
"value" => coin_balance.value,
"block_timestamp" => coin_balance.block_timestamp
}
end
def prepare_coin_balance_history_by_day_entry(coin_balance_by_day) do
%{
"date" => coin_balance_by_day.date,
"value" => coin_balance_by_day.value
}
end
end

@ -0,0 +1,9 @@
defmodule BlockScoutWeb.API.V2 do
@moduledoc """
API V2 context
"""
def enabled? do
Application.get_env(:block_scout_web, __MODULE__)[:enabled]
end
end

@ -0,0 +1,7 @@
defmodule BlockScoutWeb.API.V2.ApiView do
def render("message.json", %{message: message}) do
%{
"message" => message
}
end
end

@ -0,0 +1,106 @@
defmodule BlockScoutWeb.API.V2.BlockView do
use BlockScoutWeb, :view
alias BlockScoutWeb.BlockView
alias BlockScoutWeb.API.V2.{ApiView, Helper}
alias Explorer.Chain
alias Explorer.Chain.Block
alias Explorer.Counters.BlockPriorityFeeCounter
def render("message.json", assigns) do
ApiView.render("message.json", assigns)
end
def render("blocks.json", %{blocks: blocks, next_page_params: next_page_params}) do
%{"items" => Enum.map(blocks, &prepare_block(&1, nil)), "next_page_params" => next_page_params}
end
def render("blocks.json", %{blocks: blocks}) do
Enum.map(blocks, &prepare_block(&1, nil))
end
def render("block.json", %{block: block, conn: conn}) do
prepare_block(block, conn, true)
end
def render("block.json", %{block: block, socket: _socket}) do
# single_block? set to true in order to prevent heavy fetching of reward type
prepare_block(block, nil, false)
end
def prepare_block(block, conn, single_block? \\ false) do
burned_fee = Chain.burned_fees(block.transactions, block.base_fee_per_gas)
priority_fee = block.base_fee_per_gas && BlockPriorityFeeCounter.fetch(block.hash)
tx_fees = Chain.txn_fees(block.transactions)
%{
"height" => block.number,
"timestamp" => block.timestamp,
"tx_count" => count_transactions(block),
"miner" => Helper.address_with_info(conn, block.miner, block.miner_hash),
"size" => block.size,
"hash" => block.hash,
"parent_hash" => block.parent_hash,
"difficulty" => block.difficulty,
"total_difficulty" => block.total_difficulty,
"gas_used" => block.gas_used,
"gas_limit" => block.gas_limit,
"nonce" => block.nonce,
"base_fee_per_gas" => block.base_fee_per_gas,
"burnt_fees" => burned_fee,
"priority_fee" => priority_fee,
"extra_data" => "TODO",
"uncles_hashes" => prepare_uncles(block.uncle_relations),
"state_root" => "TODO",
"rewards" => prepare_rewards(block.rewards, block, single_block?),
"gas_target_percentage" => gas_target(block),
"gas_used_percentage" => gas_used_percentage(block),
"burnt_fees_percentage" => burnt_fees_percentage(burned_fee, tx_fees),
"type" => block |> BlockView.block_type() |> String.downcase(),
"tx_fees" => tx_fees
}
end
def prepare_rewards(rewards, block, single_block?) do
Enum.map(rewards, &prepare_reward(&1, block, single_block?))
end
def prepare_reward(reward, block, single_block?) do
%{
"reward" => reward.reward,
"type" => if(single_block?, do: BlockView.block_reward_text(reward, block.miner.hash), else: reward.address_type)
}
end
def prepare_uncles(uncles_relations) when is_list(uncles_relations) do
Enum.map(uncles_relations, &prepare_uncle/1)
end
def prepare_uncles(_), do: []
def prepare_uncle(uncle_relation) do
%{"hash" => uncle_relation.uncle_hash}
end
def gas_target(block) do
elasticity_multiplier = 2
ratio = Decimal.div(block.gas_used, Decimal.div(block.gas_limit, elasticity_multiplier))
ratio |> Decimal.sub(1) |> Decimal.mult(100) |> Decimal.to_float()
end
def gas_used_percentage(block) do
block.gas_used |> Decimal.div(block.gas_limit) |> Decimal.mult(100) |> Decimal.to_float()
end
def burnt_fees_percentage(_, %Decimal{coef: 0}), do: nil
def burnt_fees_percentage(burnt_fees, tx_fees) when not is_nil(tx_fees) and not is_nil(burnt_fees) do
burnt_fees.value |> Decimal.div(tx_fees) |> Decimal.mult(100) |> Decimal.to_float()
end
def burnt_fees_percentage(_, _), do: nil
def count_transactions(%Block{transactions: txs}) when is_list(txs), do: Enum.count(txs)
def count_transactions(_), do: nil
end

@ -0,0 +1,7 @@
defmodule BlockScoutWeb.API.V2.ConfigView do
def render("json_rpc_url.json", %{url: url}) do
%{
"json_rpc_url" => url
}
end
end

@ -0,0 +1,108 @@
defmodule BlockScoutWeb.API.V2.Helper do
@moduledoc """
API V2 helper
"""
alias Ecto.Association.NotLoaded
alias Explorer.Chain.Address
alias Explorer.Chain.Transaction.History.TransactionStats
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(conn, address, address_hash) do
%{
personal_tags: private_tags,
watchlist_names: watchlist_names
} = get_address_tags(address_hash, current_user(conn))
public_tags = get_tags_on_address(address_hash)
Map.merge(address_with_info(address, address_hash), %{
"private_tags" => private_tags,
"watchlist_names" => watchlist_names,
"public_tags" => public_tags
})
end
def address_with_info(%Address{} = address, _address_hash) do
%{
"hash" => to_string(address),
"is_contract" => is_smart_contract(address),
"name" => address_name(address),
"implementation_name" => implementation_name(address),
"is_verified" => is_verified(address)
}
end
def address_with_info(%NotLoaded{}, address_hash) do
address_with_info(nil, address_hash)
end
def address_with_info(nil, address_hash) do
%{"hash" => address_hash, "is_contract" => false, "name" => nil, "implementation_name" => nil, "is_verified" => nil}
end
def address_name(%Address{names: [_ | _] = address_names}) do
case Enum.find(address_names, &(&1.primary == true)) do
nil ->
%Address.Name{name: name} = Enum.at(address_names, 0)
name
%Address.Name{name: name} ->
name
end
end
def address_name(_), do: nil
def implementation_name(%Address{smart_contract: %{implementation_name: implementation_name}}),
do: implementation_name
def implementation_name(_), do: nil
def is_smart_contract(%Address{contract_code: nil}), do: false
def is_smart_contract(%Address{contract_code: _}), do: true
def is_smart_contract(%NotLoaded{}), do: nil
def is_smart_contract(_), do: false
def is_verified(%Address{smart_contract: nil}), do: false
def is_verified(%Address{smart_contract: %NotLoaded{}}), do: nil
def is_verified(%Address{smart_contract: _}), do: true
def market_cap(:standard, %{available_supply: available_supply, usd_value: usd_value})
when is_nil(available_supply) or is_nil(usd_value) do
Decimal.new(0)
end
def market_cap(:standard, %{available_supply: available_supply, usd_value: usd_value}) do
Decimal.mult(available_supply, usd_value)
end
def market_cap(:standard, exchange_rate) do
exchange_rate.market_cap_usd
end
def market_cap(module, exchange_rate) do
module.market_cap(exchange_rate)
end
def get_transaction_stats do
stats_scale = date_range(1)
transaction_stats = TransactionStats.by_date_range(stats_scale.earliest, stats_scale.latest)
# Need datapoint for legend if none currently available.
if Enum.empty?(transaction_stats) do
[%{number_of_transactions: 0, gas_used: 0}]
else
transaction_stats
end
end
def date_range(num_days) do
today = Date.utc_today()
latest = Date.add(today, -1)
x_days_back = Date.add(latest, -1 * (num_days - 1))
%{earliest: x_days_back, latest: latest}
end
end

@ -0,0 +1,53 @@
defmodule BlockScoutWeb.API.V2.SearchView do
use BlockScoutWeb, :view
alias BlockScoutWeb.Endpoint
def render("search_results.json", %{search_results: search_results, next_page_params: next_page_params}) do
%{"items" => Enum.map(search_results, &prepare_search_result/1), "next_page_params" => next_page_params}
end
def prepare_search_result(%{type: "token"} = search_result) do
%{
"type" => search_result.type,
"name" => search_result.name,
"symbol" => search_result.symbol,
"address" => search_result.address_hash,
"token_url" => token_path(Endpoint, :show, search_result.address_hash),
"address_url" => address_path(Endpoint, :show, search_result.address_hash)
}
end
def prepare_search_result(%{type: address_or_contract} = search_result)
when address_or_contract in ["address", "contract"] do
%{
"type" => search_result.type,
"name" => search_result.name,
"address" => search_result.address_hash,
"url" => address_path(Endpoint, :show, search_result.address_hash)
}
end
def prepare_search_result(%{type: "block"} = search_result) do
block_hash = hash_to_string(search_result.block_hash)
%{
"type" => search_result.type,
"block_number" => search_result.block_number,
"block_hash" => block_hash,
"url" => block_path(Endpoint, :show, block_hash)
}
end
def prepare_search_result(%{type: "transaction"} = search_result) do
tx_hash = hash_to_string(search_result.tx_hash)
%{
"type" => search_result.type,
"tx_hash" => tx_hash,
"url" => transaction_path(Endpoint, :show, tx_hash)
}
end
defp hash_to_string(hash), do: "0x" <> Base.encode16(hash, case: :lower)
end

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

@ -0,0 +1,447 @@
defmodule BlockScoutWeb.API.V2.TransactionView do
use BlockScoutWeb, :view
alias BlockScoutWeb.API.V2.{ApiView, Helper, TokenView}
alias BlockScoutWeb.{ABIEncodedValueView, TransactionView}
alias BlockScoutWeb.Models.GetTransactionTags
alias BlockScoutWeb.Tokens.Helpers
alias Ecto.Association.NotLoaded
alias Explorer.ExchangeRates.Token, as: TokenRate
alias Explorer.{Chain, Market}
alias Explorer.Chain.{Address, Block, InternalTransaction, Log, Token, Transaction, Wei}
alias Explorer.Chain.Block.Reward
alias Explorer.Counters.AverageBlockTime
alias Timex.Duration
import BlockScoutWeb.Account.AuthController, only: [current_user: 1]
def render("message.json", assigns) do
ApiView.render("message.json", assigns)
end
def render("transactions.json", %{transactions: transactions, conn: conn}) do
Enum.map(transactions, &prepare_transaction(&1, conn, false))
end
def render("transactions.json", %{transactions: transactions, next_page_params: next_page_params, conn: conn}) do
%{"items" => Enum.map(transactions, &prepare_transaction(&1, conn, false)), "next_page_params" => next_page_params}
end
def render("transaction.json", %{transaction: transaction, conn: conn}) do
prepare_transaction(transaction, conn, true)
end
def render("raw_trace.json", %{internal_transactions: internal_transactions}) do
InternalTransaction.internal_transactions_to_raw(internal_transactions)
end
def render("decoded_log_input.json", %{method_id: method_id, text: text, mapping: mapping}) do
%{"method_id" => method_id, "method_call" => text, "parameters" => prepare_log_mapping(mapping)}
end
def render("decoded_input.json", %{method_id: method_id, text: text, mapping: mapping, error?: _error}) 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}
end
def render("token_transfers.json", %{token_transfers: token_transfers, next_page_params: next_page_params, conn: conn}) do
%{"items" => Enum.map(token_transfers, &prepare_token_transfer(&1, conn)), "next_page_params" => next_page_params}
end
def render("token_transfers.json", %{token_transfers: token_transfers, conn: conn}) do
Enum.map(token_transfers, &prepare_token_transfer(&1, conn))
end
def render("token_transfer.json", %{token_transfer: token_transfer, conn: conn}) do
prepare_token_transfer(token_transfer, conn)
end
def render("internal_transactions.json", %{
internal_transactions: internal_transactions,
next_page_params: next_page_params,
conn: conn
}) do
%{
"items" => Enum.map(internal_transactions, &prepare_internal_transaction(&1, conn)),
"next_page_params" => next_page_params
}
end
def render("logs.json", %{logs: logs, next_page_params: next_page_params, tx_hash: tx_hash}) do
%{"items" => Enum.map(logs, fn log -> prepare_log(log, tx_hash) end), "next_page_params" => next_page_params}
end
def render("logs.json", %{logs: logs, next_page_params: next_page_params}) do
%{
"items" => Enum.map(logs, fn log -> prepare_log(log, log.transaction) end),
"next_page_params" => next_page_params
}
end
def prepare_token_transfer(token_transfer, conn) do
%{
"tx_hash" => token_transfer.transaction_hash,
"from" => Helper.address_with_info(conn, token_transfer.from_address, token_transfer.from_address_hash),
"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)
}
end
def prepare_token_transfer_total(token_transfer) do
case Helpers.token_transfer_amount_for_api(token_transfer) do
{:ok, :erc721_instance} ->
%{"token_id" => token_transfer.token_id}
{:ok, :erc1155_instance, value, decimals} ->
%{"token_id" => token_transfer.token_id, "value" => value, "decimals" => decimals}
{:ok, :erc1155_instance, values, token_ids, decimals} ->
Enum.map(Enum.zip(values, token_ids), fn {value, token_id} ->
%{"value" => value, "token_id" => token_id, "decimals" => decimals}
end)
{:ok, value, decimals} ->
%{"value" => value, "decimals" => decimals}
_ ->
nil
end
end
def prepare_internal_transaction(internal_transaction, conn) do
%{
"error" => internal_transaction.error,
"success" => is_nil(internal_transaction.error),
"type" => internal_transaction.call_type,
"transaction_hash" => internal_transaction.transaction_hash,
"from" =>
Helper.address_with_info(
conn,
internal_transaction.from_address,
internal_transaction.from_address_hash
),
"to" => Helper.address_with_info(conn, internal_transaction.to_address, internal_transaction.to_address_hash),
"created_contract" =>
Helper.address_with_info(
conn,
internal_transaction.created_contract_address,
internal_transaction.created_contract_address_hash
),
"value" => internal_transaction.value,
"block" => internal_transaction.block_number,
"timestamp" => internal_transaction.transaction.block.timestamp,
"index" => internal_transaction.index,
"gas_limit" => internal_transaction.gas
}
end
def prepare_log(log, transaction_or_hash) do
decoded = decode_log(log, transaction_or_hash)
%{
"address" => Helper.address_with_info(log.address, log.address_hash),
"topics" => [
log.first_topic,
log.second_topic,
log.third_topic,
log.fourth_topic
],
"data" => log.data,
"index" => log.index,
"decoded" => decoded,
"smart_contract" => smart_contract_info(transaction_or_hash)
}
end
defp smart_contract_info(%Transaction{} = tx), do: Helper.address_with_info(tx.to_address, tx.to_address_hash)
defp smart_contract_info(_), do: nil
defp decode_log(log, %Transaction{} = tx) do
case log |> Log.decode(tx) |> format_decoded_log_input() do
{:ok, method_id, text, mapping} ->
render(__MODULE__, "decoded_log_input.json", method_id: method_id, text: text, mapping: mapping)
_ ->
nil
end
end
defp decode_log(log, transaction_hash), do: decode_log(log, %Transaction{hash: transaction_hash})
defp prepare_transaction({%Reward{} = emission_reward, %Reward{} = validator_reward}, conn, _single_tx?) do
%{
"emission_reward" => emission_reward.reward,
"block_hash" => validator_reward.block_hash,
"from" => Helper.address_with_info(conn, emission_reward.address, emission_reward.address_hash),
"to" => Helper.address_with_info(conn, validator_reward.address, validator_reward.address_hash),
"types" => [:reward]
}
end
defp prepare_transaction(%Transaction{} = transaction, conn, single_tx?) do
base_fee_per_gas = transaction.block && transaction.block.base_fee_per_gas
max_priority_fee_per_gas = transaction.max_priority_fee_per_gas
max_fee_per_gas = transaction.max_fee_per_gas
priority_fee_per_gas = priority_fee_per_gas(max_priority_fee_per_gas, base_fee_per_gas, max_fee_per_gas)
burned_fee = burned_fee(transaction, max_fee_per_gas, base_fee_per_gas)
status = transaction |> Chain.transaction_to_status() |> format_status()
revert_reason = revert_reason(status, transaction)
decoded_input = transaction |> Transaction.decoded_input_data() |> format_decoded_input()
decoded_input_data = decoded_input(decoded_input)
%{
"hash" => transaction.hash,
"result" => status,
"status" => transaction.status,
"block" => transaction.block_number,
"timestamp" => transaction.block && transaction.block.timestamp,
"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" =>
Helper.address_with_info(conn, transaction.created_contract_address, transaction.created_contract_address_hash),
"confirmations" =>
transaction.block |> Chain.confirmations(block_height: Chain.block_height()) |> format_confirmations(),
"confirmation_duration" => processing_time_duration(transaction),
"value" => transaction.value,
"fee" => transaction |> Chain.fee(:wei) |> format_fee(),
"gas_price" => transaction.gas_price,
"type" => transaction.type,
"gas_used" => transaction.gas_used,
"gas_limit" => transaction.gas,
"max_fee_per_gas" => transaction.max_fee_per_gas,
"max_priority_fee_per_gas" => transaction.max_priority_fee_per_gas,
"base_fee_per_gas" => base_fee_per_gas,
"priority_fee" => priority_fee_per_gas && Wei.mult(priority_fee_per_gas, transaction.gas_used),
"tx_burnt_fee" => burned_fee,
"nonce" => transaction.nonce,
"position" => transaction.index,
"revert_reason" => revert_reason,
"raw_input" => transaction.input,
"decoded_input" => decoded_input_data,
"token_transfers" => token_transfers(transaction.token_transfers, conn, single_tx?),
"token_transfers_overflow" => token_transfers_overflow(transaction.token_transfers, single_tx?),
"exchange_rate" => (Market.get_exchange_rate(Explorer.coin()) || TokenRate.null()).usd_value,
"method" => method_name(transaction, decoded_input),
"tx_types" => tx_types(transaction),
"tx_tag" => GetTransactionTags.get_transaction_tags(transaction.hash, current_user(conn))
}
end
def token_transfers(_, _conn, false), do: nil
def token_transfers(%NotLoaded{}, _conn, _), do: nil
def token_transfers(token_transfers, conn, _) do
render("token_transfers.json", %{
token_transfers: Enum.take(token_transfers, Chain.get_token_transfers_per_transaction_preview_count()),
conn: conn
})
end
def token_transfers_overflow(_, false), do: nil
def token_transfers_overflow(%NotLoaded{}, _), do: false
def token_transfers_overflow(token_transfers, _),
do: Enum.count(token_transfers) > Chain.get_token_transfers_per_transaction_preview_count()
defp priority_fee_per_gas(max_priority_fee_per_gas, base_fee_per_gas, max_fee_per_gas) do
if is_nil(max_priority_fee_per_gas) or is_nil(base_fee_per_gas),
do: nil,
else:
Enum.min_by([max_priority_fee_per_gas, Wei.sub(max_fee_per_gas, base_fee_per_gas)], fn x ->
Wei.to(x, :wei)
end)
end
defp burned_fee(transaction, max_fee_per_gas, base_fee_per_gas) do
if !is_nil(max_fee_per_gas) and !is_nil(transaction.gas_used) and !is_nil(base_fee_per_gas) do
if Decimal.compare(max_fee_per_gas.value, 0) == :eq do
%Wei{value: Decimal.new(0)}
else
Wei.mult(base_fee_per_gas, transaction.gas_used)
end
else
nil
end
end
defp revert_reason(status, transaction) do
if is_binary(status) && status |> String.downcase() |> String.contains?("reverted") do
case TransactionView.transaction_revert_reason(transaction) do
{:error, _contract_not_verified, candidates} when candidates != [] ->
{:ok, method_id, text, mapping} = Enum.at(candidates, 0)
render(__MODULE__, "decoded_input.json", method_id: method_id, text: text, mapping: mapping, error?: true)
{:ok, method_id, text, mapping} ->
render(__MODULE__, "decoded_input.json", method_id: method_id, text: text, mapping: mapping, error?: true)
_ ->
hex = TransactionView.get_pure_transaction_revert_reason(transaction)
utf8 = TransactionView.decoded_revert_reason(transaction)
render(__MODULE__, "revert_reason.json", raw: hex, decoded: utf8)
end
end
end
defp decoded_input(decoded_input) do
case decoded_input do
{:ok, method_id, text, mapping} ->
render(__MODULE__, "decoded_input.json", method_id: method_id, text: text, mapping: mapping, error?: false)
_ ->
nil
end
end
def prepare_method_mapping(mapping) do
Enum.map(mapping, fn {name, type, value} ->
%{"name" => name, "type" => type, "value" => ABIEncodedValueView.value_json(type, value)}
end)
end
def prepare_log_mapping(mapping) do
Enum.map(mapping, fn {name, type, indexed?, value} ->
%{"name" => name, "type" => type, "indexed" => indexed?, "value" => ABIEncodedValueView.value_json(type, value)}
end)
end
defp format_status({:error, reason}), do: reason
defp format_status(status), do: status
defp format_decoded_input({:error, _, []}), do: nil
defp format_decoded_input({:error, _, candidates}), do: Enum.at(candidates, 0)
defp format_decoded_input({:ok, _identifier, _text, _mapping} = decoded), do: decoded
defp format_decoded_input(_), do: nil
defp format_decoded_log_input({:error, :could_not_decode}), do: nil
defp format_decoded_log_input({:error, :no_matching_function}), do: nil
defp format_decoded_log_input({:ok, _method_id, _text, _mapping} = decoded), do: decoded
defp format_decoded_log_input({:error, _, candidates}), do: Enum.at(candidates, 0)
def format_confirmations({:ok, confirmations}), do: confirmations
def format_confirmations(_), do: 0
def format_fee({type, value}), do: %{"type" => type, "value" => value}
def processing_time_duration(%Transaction{block: nil}) do
[]
end
def processing_time_duration(%Transaction{earliest_processing_start: nil}) do
avg_time = AverageBlockTime.average_block_time()
if avg_time == {:error, :disabled} do
[]
else
[
0,
avg_time
|> Duration.to_milliseconds()
]
end
end
def processing_time_duration(%Transaction{
block: %Block{timestamp: end_time},
earliest_processing_start: earliest_processing_start,
inserted_at: inserted_at
}) do
long_interval = abs(diff(earliest_processing_start, end_time))
short_interval = abs(diff(inserted_at, end_time))
merge_intervals(short_interval, long_interval)
end
def merge_intervals(short, long) when short == long, do: [short]
def merge_intervals(short, long) do
[short, long]
end
def diff(left, right) do
left
|> Timex.diff(right, :milliseconds)
end
defp method_name(_, {:ok, _method_id, text, _mapping}) do
Transaction.parse_method_name(text, false)
end
defp method_name(%Transaction{to_address: to_address, input: %{bytes: <<method_id::binary-size(4), _::binary>>}}, _) do
if Helper.is_smart_contract(to_address) do
"0x" <> Base.encode16(method_id, case: :lower)
else
nil
end
end
defp method_name(_, _) do
nil
end
defp tx_types(tx, types \\ [], stage \\ :token_transfer)
defp tx_types(%Transaction{token_transfers: token_transfers} = tx, types, :token_transfer) do
types =
if !is_nil(token_transfers) && token_transfers != [] && !match?(%NotLoaded{}, token_transfers) do
[:token_transfer | types]
else
types
end
tx_types(tx, types, :token_creation)
end
defp tx_types(%Transaction{created_contract_address: created_contract_address} = tx, types, :token_creation) do
types =
if match?(%Address{}, created_contract_address) && match?(%Token{}, created_contract_address.token) do
[:token_creation | types]
else
types
end
tx_types(tx, types, :contract_creation)
end
defp tx_types(
%Transaction{created_contract_address_hash: created_contract_address_hash} = tx,
types,
:contract_creation
) do
types =
if is_nil(created_contract_address_hash) do
types
else
[:contract_creation | types]
end
tx_types(tx, types, :contract_call)
end
defp tx_types(%Transaction{to_address: to_address} = tx, types, :contract_call) do
types =
if Helper.is_smart_contract(to_address) do
[:contract_call | types]
else
types
end
tx_types(tx, types, :coin_transfer)
end
defp tx_types(%Transaction{value: value}, types, :coin_transfer) do
if Decimal.compare(value.value, 0) == :gt do
[:coin_transfer | types]
else
types
end
end
end

@ -3,6 +3,7 @@ defmodule BlockScoutWeb.BlockView do
import Math.Enum, only: [mean: 1] import Math.Enum, only: [mean: 1]
alias Ecto.Association.NotLoaded
alias Explorer.Chain alias Explorer.Chain
alias Explorer.Chain.{Block, Wei} alias Explorer.Chain.{Block, Wei}
alias Explorer.Chain.Block.Reward alias Explorer.Chain.Block.Reward
@ -23,6 +24,7 @@ defmodule BlockScoutWeb.BlockView do
"#{average} #{unit_text}" "#{average} #{unit_text}"
end end
def block_type(%Block{consensus: false, nephews: %NotLoaded{}}), do: "Reorg"
def block_type(%Block{consensus: false, nephews: []}), do: "Reorg" def block_type(%Block{consensus: false, nephews: []}), do: "Reorg"
def block_type(%Block{consensus: false}), do: "Uncle" def block_type(%Block{consensus: false}), do: "Uncle"
def block_type(_block), do: "Block" def block_type(_block), do: "Block"

@ -3,27 +3,11 @@ defmodule BlockScoutWeb.ChainView do
require Decimal require Decimal
import Number.Currency, only: [number_to_currency: 2] import Number.Currency, only: [number_to_currency: 2]
import BlockScoutWeb.API.V2.Helper, only: [market_cap: 2]
alias BlockScoutWeb.LayoutView alias BlockScoutWeb.LayoutView
alias Explorer.Chain.Cache.GasPriceOracle alias Explorer.Chain.Cache.GasPriceOracle
defp market_cap(:standard, %{available_supply: available_supply, usd_value: usd_value})
when is_nil(available_supply) or is_nil(usd_value) do
Decimal.new(0)
end
defp market_cap(:standard, %{available_supply: available_supply, usd_value: usd_value}) do
Decimal.mult(available_supply, usd_value)
end
defp market_cap(:standard, exchange_rate) do
exchange_rate.market_cap_usd
end
defp market_cap(module, exchange_rate) do
module.market_cap(exchange_rate)
end
def format_usd_value(nil), do: "" def format_usd_value(nil), do: ""
def format_usd_value(value) do def format_usd_value(value) do

@ -16,52 +16,4 @@ defmodule BlockScoutWeb.SearchView do
|> Regex.replace(safe_result, "<mark class=\'autoComplete_highlight\'>\\g{0}</mark>", global: true) |> Regex.replace(safe_result, "<mark class=\'autoComplete_highlight\'>\\g{0}</mark>", global: true)
|> raw() |> raw()
end end
def render("search_results.json", %{search_results: search_results, next_page_params: next_page_params}) do
%{"items" => Enum.map(search_results, &prepare_search_result/1), "next_page_params" => next_page_params}
end
def prepare_search_result(%{type: "token"} = search_result) do
%{
"type" => search_result.type,
"name" => search_result.name,
"symbol" => search_result.symbol,
"address" => search_result.address_hash,
"token_url" => token_path(BlockScoutWeb.Endpoint, :show, search_result.address_hash),
"address_url" => address_path(BlockScoutWeb.Endpoint, :show, search_result.address_hash)
}
end
def prepare_search_result(%{type: address_or_contract} = search_result)
when address_or_contract in ["address", "contract"] do
%{
"type" => search_result.type,
"name" => search_result.name,
"address" => search_result.address_hash,
"url" => address_path(BlockScoutWeb.Endpoint, :show, search_result.address_hash)
}
end
def prepare_search_result(%{type: "block"} = search_result) do
block_hash = hash_to_string(search_result.block_hash)
%{
"type" => search_result.type,
"block_number" => search_result.block_number,
"block_hash" => block_hash,
"url" => block_path(BlockScoutWeb.Endpoint, :show, block_hash)
}
end
def prepare_search_result(%{type: "transaction"} = search_result) do
tx_hash = hash_to_string(search_result.tx_hash)
%{
"type" => search_result.type,
"tx_hash" => tx_hash,
"url" => transaction_path(BlockScoutWeb.Endpoint, :show, tx_hash)
}
end
defp hash_to_string(hash), do: "0x" <> Base.encode16(hash, case: :lower)
end end

@ -52,6 +52,56 @@ defmodule BlockScoutWeb.Tokens.Helpers do
nil nil
end end
def token_transfer_amount_for_api(%{
token: token,
amount: amount,
amounts: amounts,
token_id: token_id,
token_ids: token_ids
}) do
do_token_transfer_amount_for_api(token, amount, amounts, token_id, token_ids)
end
def token_transfer_amount_for_api(%{token: token, amount: amount, token_id: token_id}) do
do_token_transfer_amount_for_api(token, amount, nil, token_id, nil)
end
defp do_token_transfer_amount_for_api(%Token{type: "ERC-20"}, nil, nil, _token_id, _token_ids) do
{:ok, nil}
end
defp do_token_transfer_amount_for_api(
%Token{type: "ERC-20", decimals: decimals},
amount,
_amounts,
_token_id,
_token_ids
) do
{:ok, amount, decimals}
end
defp do_token_transfer_amount_for_api(%Token{type: "ERC-721"}, _amount, _amounts, _token_id, _token_ids) do
{:ok, :erc721_instance}
end
defp do_token_transfer_amount_for_api(
%Token{type: "ERC-1155", decimals: decimals},
amount,
amounts,
_token_id,
token_ids
) do
if amount do
{:ok, :erc1155_instance, amount, decimals}
else
{:ok, :erc1155_instance, amounts, token_ids, decimals}
end
end
defp do_token_transfer_amount_for_api(_token, _amount, _amounts, _token_id, _token_ids) do
nil
end
@doc """ @doc """
Returns the token's symbol. Returns the token's symbol.

@ -512,7 +512,7 @@ msgstr ""
msgid "Chat (#blockscout)" msgid "Chat (#blockscout)"
msgstr "" msgstr ""
#: lib/block_scout_web/views/block_view.ex:63 #: lib/block_scout_web/views/block_view.ex:65
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Chore Reward" msgid "Chore Reward"
msgstr "" msgstr ""
@ -1105,7 +1105,7 @@ msgstr ""
msgid "Emission Contract" msgid "Emission Contract"
msgstr "" msgstr ""
#: lib/block_scout_web/views/block_view.ex:71 #: lib/block_scout_web/views/block_view.ex:73
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Emission Reward" msgid "Emission Reward"
msgstr "" msgstr ""
@ -1323,7 +1323,7 @@ msgstr ""
#: lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex:21 #: lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex:21
#: lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex:22 #: lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex:22
#: lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex:38 #: lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex:38
#: lib/block_scout_web/views/block_view.ex:21 #: lib/block_scout_web/views/block_view.ex:22
#: lib/block_scout_web/views/wei_helpers.ex:77 #: lib/block_scout_web/views/wei_helpers.ex:77
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Gwei" msgid "Gwei"
@ -1596,8 +1596,8 @@ msgstr ""
msgid "Miner" msgid "Miner"
msgstr "" msgstr ""
#: lib/block_scout_web/views/block_view.ex:61 #: lib/block_scout_web/views/block_view.ex:63
#: lib/block_scout_web/views/block_view.ex:66 #: lib/block_scout_web/views/block_view.ex:68
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Miner Reward" msgid "Miner Reward"
msgstr "" msgstr ""
@ -2823,7 +2823,7 @@ msgstr ""
msgid "UTF-8" msgid "UTF-8"
msgstr "" msgstr ""
#: lib/block_scout_web/views/block_view.ex:75 #: lib/block_scout_web/views/block_view.ex:77
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Uncle Reward" msgid "Uncle Reward"
msgstr "" msgstr ""

@ -512,7 +512,7 @@ msgstr ""
msgid "Chat (#blockscout)" msgid "Chat (#blockscout)"
msgstr "" msgstr ""
#: lib/block_scout_web/views/block_view.ex:63 #: lib/block_scout_web/views/block_view.ex:65
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Chore Reward" msgid "Chore Reward"
msgstr "" msgstr ""
@ -1105,7 +1105,7 @@ msgstr ""
msgid "Emission Contract" msgid "Emission Contract"
msgstr "" msgstr ""
#: lib/block_scout_web/views/block_view.ex:71 #: lib/block_scout_web/views/block_view.ex:73
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Emission Reward" msgid "Emission Reward"
msgstr "" msgstr ""
@ -1323,7 +1323,7 @@ msgstr ""
#: lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex:21 #: lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex:21
#: lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex:22 #: lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex:22
#: lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex:38 #: lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex:38
#: lib/block_scout_web/views/block_view.ex:21 #: lib/block_scout_web/views/block_view.ex:22
#: lib/block_scout_web/views/wei_helpers.ex:77 #: lib/block_scout_web/views/wei_helpers.ex:77
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Gwei" msgid "Gwei"
@ -1596,8 +1596,8 @@ msgstr ""
msgid "Miner" msgid "Miner"
msgstr "" msgstr ""
#: lib/block_scout_web/views/block_view.ex:61 #: lib/block_scout_web/views/block_view.ex:63
#: lib/block_scout_web/views/block_view.ex:66 #: lib/block_scout_web/views/block_view.ex:68
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Miner Reward" msgid "Miner Reward"
msgstr "" msgstr ""
@ -2823,7 +2823,7 @@ msgstr ""
msgid "UTF-8" msgid "UTF-8"
msgstr "" msgstr ""
#: lib/block_scout_web/views/block_view.ex:75 #: lib/block_scout_web/views/block_view.ex:77
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Uncle Reward" msgid "Uncle Reward"
msgstr "" msgstr ""

@ -133,7 +133,7 @@ defmodule BlockScoutWeb.ViewingChainTest do
transaction = transaction =
:transaction :transaction
|> insert(to_address: contract_token_address) |> insert(to_address: contract_token_address)
|> with_block(block) |> with_block(block, status: :ok)
insert_list( insert_list(
3, 3,

@ -20,9 +20,11 @@ defmodule BlockScoutWeb.AddressTokenBalanceViewTest do
token_balance_a = build(:token_balance, token: build(:token, type: "ERC-20")) token_balance_a = build(:token_balance, token: build(:token, type: "ERC-20"))
token_balance_b = build(:token_balance, token: build(:token, type: "ERC-721")) token_balance_b = build(:token_balance, token: build(:token, type: "ERC-721"))
token_balances = [{token_balance_a, %{}}, {token_balance_b, %{}}] token_balances = [{token_balance_a, token_balance_a.token}, {token_balance_b, token_balance_b.token}]
assert AddressTokenBalanceView.filter_by_type(token_balances, "ERC-20") == [{token_balance_a, %{}}] assert AddressTokenBalanceView.filter_by_type(token_balances, "ERC-20") == [
{token_balance_a, token_balance_a.token}
]
end end
end end

@ -34,8 +34,9 @@ defmodule BlockScoutWeb.BlockViewTest do
test "returns Uncle" do test "returns Uncle" do
uncle = insert(:block, consensus: false) uncle = insert(:block, consensus: false)
insert(:block_second_degree_relation, uncle_hash: uncle.hash) insert(:block_second_degree_relation, uncle_hash: uncle.hash)
preloaded = Repo.preload(uncle, :nephews)
assert BlockView.block_type(uncle) == "Uncle" assert BlockView.block_type(preloaded) == "Uncle"
end end
end end

@ -8,7 +8,7 @@ alias Explorer.Chain.Block
Benchee.run( Benchee.run(
%{ %{
"Explorer.Chain.recent_collated_transactions" => fn _ -> "Explorer.Chain.recent_collated_transactions" => fn _ ->
Chain.recent_collated_transactions() Chain.recent_collated_transactions(true)
end end
}, },
inputs: %{ inputs: %{

@ -1,7 +1,9 @@
import Config import Config
# Configure your database # Configure your database
config :explorer, Explorer.Repo, timeout: :timer.seconds(80) config :explorer, Explorer.Repo,
timeout: :timer.seconds(80),
migration_lock: nil
# Configure API database # Configure API database
config :explorer, Explorer.Repo.Replica1, timeout: :timer.seconds(80) config :explorer, Explorer.Repo.Replica1, timeout: :timer.seconds(80)

@ -3,7 +3,8 @@ import Config
# Configures the database # Configures the database
config :explorer, Explorer.Repo, config :explorer, Explorer.Repo,
prepare: :unnamed, prepare: :unnamed,
timeout: :timer.seconds(60) timeout: :timer.seconds(60),
migration_lock: nil
# Configures API the database # Configures API the database
config :explorer, Explorer.Repo.Replica1, config :explorer, Explorer.Repo.Replica1,

@ -11,7 +11,8 @@ config :explorer, Explorer.Repo,
# Default of `5_000` was too low for `BlockFetcher` test # Default of `5_000` was too low for `BlockFetcher` test
ownership_timeout: :timer.minutes(7), ownership_timeout: :timer.minutes(7),
timeout: :timer.seconds(60), timeout: :timer.seconds(60),
queue_target: 1000 queue_target: 1000,
migration_lock: nil
# Configure API database # Configure API database
config :explorer, Explorer.Repo.Replica1, config :explorer, Explorer.Repo.Replica1,

@ -153,3 +153,9 @@ defmodule Explorer.Account.TagTransaction do
def get_max_tags_count, do: @max_tag_transaction_per_account def get_max_tags_count, do: @max_tag_transaction_per_account
end end
defimpl Jason.Encoder, for: Explorer.Account.TagTransaction do
def encode(tx_tag, opts) do
Jason.Encode.string(tx_tag.name, opts)
end
end

@ -5,8 +5,11 @@ defmodule Explorer.Chain do
import Ecto.Query, import Ecto.Query,
only: [ only: [
dynamic: 1,
dynamic: 2,
from: 2, from: 2,
join: 4, join: 4,
join: 5,
limit: 2, limit: 2,
lock: 2, lock: 2,
offset: 2, offset: 2,
@ -81,6 +84,23 @@ defmodule Explorer.Chain do
@default_paging_options %PagingOptions{page_size: 50} @default_paging_options %PagingOptions{page_size: 50}
@token_transfers_per_transaction_preview 10
@token_transfers_neccessity_by_association %{
[from_address: :smart_contract] => :optional,
[to_address: :smart_contract] => :optional,
[from_address: :names] => :optional,
[to_address: :names] => :optional,
token: :required
}
@method_name_to_id_map %{
"approve" => "095ea7b3",
"transfer" => "a9059cbb",
"multicall" => "5ae401dc",
"mint" => "40c10f19",
"commit" => "f14fcbc8"
}
@max_incoming_transactions_count 10_000 @max_incoming_transactions_count 10_000
@revert_msg_prefix_1 "Revert: " @revert_msg_prefix_1 "Revert: "
@ -656,9 +676,7 @@ defmodule Explorer.Chain do
end end
def where_block_number_in_period(base_query, from_block, to_block) when is_nil(from_block) and is_nil(to_block) do def where_block_number_in_period(base_query, from_block, to_block) when is_nil(from_block) and is_nil(to_block) do
from(q in base_query, base_query
where: 1
)
end end
def where_block_number_in_period(base_query, from_block, to_block) do def where_block_number_in_period(base_query, from_block, to_block) do
@ -768,19 +786,40 @@ defmodule Explorer.Chain do
end end
end end
@uncle_reward_coef 1 / 32 def txn_fees(transactions) do
def block_reward_by_parts(block, transactions) do Enum.reduce(transactions, Decimal.new(0), fn %{gas_used: gas_used, gas_price: gas_price}, acc ->
%{hash: block_hash, number: block_number} = block gas_used
base_fee_per_gas = Map.get(block, :base_fee_per_gas) |> Decimal.new()
|> Decimal.mult(gas_price_to_decimal(gas_price))
|> Decimal.add(acc)
end)
end
txn_fees = defp gas_price_to_decimal(%Wei{} = wei), do: wei.value
Enum.reduce(transactions, Decimal.new(0), fn %{gas_used: gas_used, gas_price: gas_price}, acc -> defp gas_price_to_decimal(gas_price), do: Decimal.new(gas_price)
def burned_fees(transactions, base_fee_per_gas) do
burned_fee_counter =
transactions
|> Enum.reduce(Decimal.new(0), fn %{gas_used: gas_used}, acc ->
gas_used gas_used
|> Decimal.new() |> Decimal.new()
|> Decimal.mult(Decimal.new(gas_price))
|> Decimal.add(acc) |> Decimal.add(acc)
end) end)
base_fee_per_gas && Wei.mult(base_fee_per_gas_to_wei(base_fee_per_gas), burned_fee_counter)
end
defp base_fee_per_gas_to_wei(%Wei{} = wei), do: wei
defp base_fee_per_gas_to_wei(base_fee_per_gas), do: %Wei{value: Decimal.new(base_fee_per_gas)}
@uncle_reward_coef 1 / 32
def block_reward_by_parts(block, transactions) do
%{hash: block_hash, number: block_number} = block
base_fee_per_gas = Map.get(block, :base_fee_per_gas)
txn_fees = txn_fees(transactions)
static_reward = static_reward =
Repo.one( Repo.one(
from( from(
@ -790,17 +829,9 @@ defmodule Explorer.Chain do
) )
) || %Wei{value: Decimal.new(0)} ) || %Wei{value: Decimal.new(0)}
burned_fee_counter =
transactions
|> Enum.reduce(Decimal.new(0), fn %{gas_used: gas_used}, acc ->
gas_used
|> Decimal.new()
|> Decimal.add(acc)
end)
has_uncles? = is_list(block.uncles) and not Enum.empty?(block.uncles) has_uncles? = is_list(block.uncles) and not Enum.empty?(block.uncles)
burned_fees = base_fee_per_gas && Wei.mult(%Wei{value: Decimal.new(base_fee_per_gas)}, burned_fee_counter) burned_fees = burned_fees(transactions, base_fee_per_gas)
uncle_reward = (has_uncles? && Wei.mult(static_reward, Decimal.from_float(@uncle_reward_coef))) || nil uncle_reward = (has_uncles? && Wei.mult(static_reward, Decimal.from_float(@uncle_reward_coef))) || nil
%{ %{
@ -861,8 +892,10 @@ defmodule Explorer.Chain do
`:key` (a tuple of the lowest/oldest `{index}`) and. Results will be the transactions older than `:key` (a tuple of the lowest/oldest `{index}`) and. Results will be the transactions older than
the `index` that are passed. the `index` that are passed.
""" """
@spec block_to_transactions(Hash.Full.t(), [paging_options | necessity_by_association_option]) :: [Transaction.t()] @spec block_to_transactions(Hash.Full.t(), [paging_options | necessity_by_association_option], true | false) :: [
def block_to_transactions(block_hash, options \\ []) when is_list(options) do Transaction.t()
]
def block_to_transactions(block_hash, options \\ [], old_ui? \\ true) when is_list(options) do
necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) necessity_by_association = Keyword.get(options, :necessity_by_association, %{})
options options
@ -871,8 +904,12 @@ defmodule Explorer.Chain do
|> join(:inner, [transaction], block in assoc(transaction, :block)) |> join(:inner, [transaction], block in assoc(transaction, :block))
|> where([_, block], block.hash == ^block_hash) |> where([_, block], block.hash == ^block_hash)
|> join_associations(necessity_by_association) |> join_associations(necessity_by_association)
|> preload([{:token_transfers, [:token, :from_address, :to_address]}]) |> (&if(old_ui?, do: preload(&1, [{:token_transfers, [:token, :from_address, :to_address]}]), else: &1)).()
|> Repo.all() |> Repo.all()
|> (&if(old_ui?,
do: &1,
else: Enum.map(&1, fn tx -> preload_token_transfers(tx, @token_transfers_neccessity_by_association) end)
)).()
end end
@doc """ @doc """
@ -1031,8 +1068,8 @@ defmodule Explorer.Chain do
iex> Explorer.Chain.confirmations(block, block_height: 0) iex> Explorer.Chain.confirmations(block, block_height: 0)
{:ok, 1} {:ok, 1}
""" """
@spec confirmations(Block.t(), [{:block_height, block_height()}]) :: @spec confirmations(Block.t() | nil, [{:block_height, block_height()}]) ::
{:ok, non_neg_integer()} | {:error, :non_consensus} {:ok, non_neg_integer()} | {:error, :non_consensus | :pending}
def confirmations(%Block{consensus: true, number: number}, named_arguments) when is_list(named_arguments) do def confirmations(%Block{consensus: true, number: number}, named_arguments) when is_list(named_arguments) do
max_consensus_block_number = Keyword.fetch!(named_arguments, :block_height) max_consensus_block_number = Keyword.fetch!(named_arguments, :block_height)
@ -1042,6 +1079,8 @@ defmodule Explorer.Chain do
def confirmations(%Block{consensus: false}, _), do: {:error, :non_consensus} def confirmations(%Block{consensus: false}, _), do: {:error, :non_consensus}
def confirmations(nil, _), do: {:error, :pending}
@doc """ @doc """
Creates an address. Creates an address.
@ -2019,6 +2058,41 @@ defmodule Explorer.Chain do
end end
end end
# preload_to_detect_tt?: we don't need to preload more than one token transfer in case the tx inside the list (we dont't show any token transfers on tx tile in new UI)
def preload_token_transfers(
%Transaction{hash: tx_hash, block_hash: block_hash} = transaction,
necessity_by_association,
preload_to_detect_tt? \\ true
) do
token_transfers =
TokenTransfer
|> (&if(is_nil(block_hash),
do: where(&1, [token_transfer], token_transfer.transaction_hash == ^tx_hash),
else:
where(
&1,
[token_transfer],
token_transfer.transaction_hash == ^tx_hash and token_transfer.block_hash == ^block_hash
)
)).()
|> limit(^if(preload_to_detect_tt?, do: 1, else: @token_transfers_per_transaction_preview + 1))
|> order_by([token_transfer], asc: token_transfer.log_index)
|> join_associations(necessity_by_association)
|> Repo.all()
%Transaction{transaction | token_transfers: token_transfers}
end
def get_token_transfers_per_transaction_preview_count, do: @token_transfers_per_transaction_preview
defp debug(value, key) do
require Logger
Logger.configure(truncate: :infinity)
Logger.info(key)
Logger.info(Kernel.inspect(value, limit: :infinity, printable_limit: :infinity))
value
end
@doc """ @doc """
Converts list of `t:Explorer.Chain.Transaction.t/0` `hashes` to the list of `t:Explorer.Chain.Transaction.t/0`s for Converts list of `t:Explorer.Chain.Transaction.t/0` `hashes` to the list of `t:Explorer.Chain.Transaction.t/0`s for
those `hashes`. those `hashes`.
@ -2504,6 +2578,15 @@ defmodule Explorer.Chain do
@doc """ @doc """
Return the balance in usd corresponding to this token. Return nil if the usd_value of the token is not present. Return the balance in usd corresponding to this token. Return nil if the usd_value of the token is not present.
""" """
def balance_in_usd(_token_balance, %{usd_value: nil}) do
nil
end
def balance_in_usd(token_balance, %{usd_value: usd_value, decimals: decimals}) do
tokens = CurrencyHelpers.divide_decimals(token_balance.value, decimals)
Decimal.mult(tokens, usd_value)
end
def balance_in_usd(%{token: %{usd_value: nil}}) do def balance_in_usd(%{token: %{usd_value: nil}}) do
nil nil
end end
@ -3171,7 +3254,7 @@ defmodule Explorer.Chain do
iex> newest_first_transactions = 50 |> insert_list(:transaction) |> with_block() |> Enum.reverse() iex> newest_first_transactions = 50 |> insert_list(:transaction) |> with_block() |> Enum.reverse()
iex> oldest_seen = Enum.at(newest_first_transactions, 9) iex> oldest_seen = Enum.at(newest_first_transactions, 9)
iex> paging_options = %Explorer.PagingOptions{page_size: 10, key: {oldest_seen.block_number, oldest_seen.index}} iex> paging_options = %Explorer.PagingOptions{page_size: 10, key: {oldest_seen.block_number, oldest_seen.index}}
iex> recent_collated_transactions = Explorer.Chain.recent_collated_transactions(paging_options: paging_options) iex> recent_collated_transactions = Explorer.Chain.recent_collated_transactions(true, paging_options: paging_options)
iex> length(recent_collated_transactions) iex> length(recent_collated_transactions)
10 10
iex> hd(recent_collated_transactions).hash == Enum.at(newest_first_transactions, 10).hash iex> hd(recent_collated_transactions).hash == Enum.at(newest_first_transactions, 10).hash
@ -3187,26 +3270,17 @@ defmodule Explorer.Chain do
the `block_number` and `index` that are passed. the `block_number` and `index` that are passed.
""" """
@spec recent_collated_transactions([paging_options | necessity_by_association_option]) :: [Transaction.t()] @spec recent_collated_transactions(true | false, [paging_options | necessity_by_association_option], [String.t()], [
def recent_collated_transactions(options \\ []) when is_list(options) do :atom
]) :: [
Transaction.t()
]
def recent_collated_transactions(old_ui?, options \\ [], method_id_filter \\ [], type_filter \\ [])
when is_list(options) do
necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) necessity_by_association = Keyword.get(options, :necessity_by_association, %{})
paging_options = Keyword.get(options, :paging_options, @default_paging_options) paging_options = Keyword.get(options, :paging_options, @default_paging_options)
if is_nil(paging_options.key) do fetch_recent_collated_transactions(old_ui?, paging_options, necessity_by_association, method_id_filter, type_filter)
paging_options.page_size
|> Transactions.take_enough()
|> case do
nil ->
transactions = fetch_recent_collated_transactions(paging_options, necessity_by_association)
Transactions.update(transactions)
transactions
transactions ->
transactions
end
else
fetch_recent_collated_transactions(paging_options, necessity_by_association)
end
end end
# RAP - random access pagination # RAP - random access pagination
@ -3264,13 +3338,26 @@ defmodule Explorer.Chain do
|> Repo.aggregate(:count, :hash) |> Repo.aggregate(:count, :hash)
end end
def fetch_recent_collated_transactions(paging_options, necessity_by_association) do def fetch_recent_collated_transactions(
old_ui?,
paging_options,
necessity_by_association,
method_id_filter,
type_filter
) do
paging_options paging_options
|> fetch_transactions() |> fetch_transactions()
|> where([transaction], not is_nil(transaction.block_number) and not is_nil(transaction.index)) |> where([transaction], not is_nil(transaction.block_number) and not is_nil(transaction.index))
|> apply_filter_by_method_id_to_transactions(method_id_filter)
|> apply_filter_by_tx_type_to_transactions(type_filter)
|> join_associations(necessity_by_association) |> join_associations(necessity_by_association)
|> preload([{:token_transfers, [:token, :from_address, :to_address]}]) |> (&if(old_ui?, do: preload(&1, [{:token_transfers, [:token, :from_address, :to_address]}]), else: &1)).()
|> debug("result collated query")
|> Repo.all() |> Repo.all()
|> (&if(old_ui?,
do: &1,
else: Enum.map(&1, fn tx -> preload_token_transfers(tx, @token_transfers_neccessity_by_association) end)
)).()
end end
@doc """ @doc """
@ -3297,8 +3384,11 @@ defmodule Explorer.Chain do
Results will be the transactions older than the `inserted_at` and `hash` that are passed. Results will be the transactions older than the `inserted_at` and `hash` that are passed.
""" """
@spec recent_pending_transactions([paging_options | necessity_by_association_option]) :: [Transaction.t()] @spec recent_pending_transactions([paging_options | necessity_by_association_option], true | false, [String.t()], [
def recent_pending_transactions(options \\ []) when is_list(options) do :atom
]) :: [Transaction.t()]
def recent_pending_transactions(options \\ [], old_ui? \\ true, method_id_filter \\ [], type_filter \\ [])
when is_list(options) do
necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) necessity_by_association = Keyword.get(options, :necessity_by_association, %{})
paging_options = Keyword.get(options, :paging_options, @default_paging_options) paging_options = Keyword.get(options, :paging_options, @default_paging_options)
@ -3306,10 +3396,17 @@ defmodule Explorer.Chain do
|> page_pending_transaction(paging_options) |> page_pending_transaction(paging_options)
|> limit(^paging_options.page_size) |> limit(^paging_options.page_size)
|> pending_transactions_query() |> pending_transactions_query()
|> apply_filter_by_method_id_to_transactions(method_id_filter)
|> apply_filter_by_tx_type_to_transactions(type_filter)
|> order_by([transaction], desc: transaction.inserted_at, desc: transaction.hash) |> order_by([transaction], desc: transaction.inserted_at, desc: transaction.hash)
|> join_associations(necessity_by_association) |> join_associations(necessity_by_association)
|> preload([{:token_transfers, [:token, :from_address, :to_address]}]) |> (&if(old_ui?, do: preload(&1, [{:token_transfers, [:token, :from_address, :to_address]}]), else: &1)).()
|> debug("result pendging query")
|> Repo.all() |> Repo.all()
|> (&if(old_ui?,
do: &1,
else: Enum.map(&1, fn tx -> preload_token_transfers(tx, @token_transfers_neccessity_by_association) end)
)).()
end end
def pending_transactions_query(query) do def pending_transactions_query(query) do
@ -4334,6 +4431,16 @@ defmodule Explorer.Chain do
end end
end end
defp join_association(query, association, necessity) do
case necessity do
:optional ->
preload(query, ^association)
:required ->
from(q in query, inner_join: a in assoc(q, ^association), preload: [{^association, a}])
end
end
defp join_associations(query, necessity_by_association) when is_map(necessity_by_association) do defp join_associations(query, necessity_by_association) when is_map(necessity_by_association) do
Enum.reduce(necessity_by_association, query, fn {association, join}, acc_query -> Enum.reduce(necessity_by_association, query, fn {association, join}, acc_query ->
join_association(acc_query, association, join) join_association(acc_query, association, join)
@ -5040,8 +5147,8 @@ defmodule Explorer.Chain do
Repo.one(query) Repo.one(query)
end end
@spec address_to_balances_by_day(Hash.Address.t()) :: [balance_by_day] @spec address_to_balances_by_day(Hash.Address.t(), true | false) :: [balance_by_day]
def address_to_balances_by_day(address_hash) do def address_to_balances_by_day(address_hash, api? \\ false) do
latest_block_timestamp = latest_block_timestamp =
address_hash address_hash
|> CoinBalance.last_coin_balance_timestamp() |> CoinBalance.last_coin_balance_timestamp()
@ -5052,7 +5159,7 @@ defmodule Explorer.Chain do
|> Repo.all() |> Repo.all()
|> Enum.sort_by(fn %{date: d} -> {d.year, d.month, d.day} end) |> Enum.sort_by(fn %{date: d} -> {d.year, d.month, d.day} end)
|> replace_last_value(latest_block_timestamp) |> replace_last_value(latest_block_timestamp)
|> normalize_balances_by_day() |> normalize_balances_by_day(api?)
end end
# https://github.com/blockscout/blockscout/issues/2658 # https://github.com/blockscout/blockscout/issues/2658
@ -5062,12 +5169,12 @@ defmodule Explorer.Chain do
defp replace_last_value(items, _), do: items defp replace_last_value(items, _), do: items
defp normalize_balances_by_day(balances_by_day) do defp normalize_balances_by_day(balances_by_day, api?) do
result = result =
balances_by_day balances_by_day
|> Enum.filter(fn day -> day.value end) |> Enum.filter(fn day -> day.value end)
|> Enum.map(fn day -> Map.update!(day, :date, &to_string(&1)) end) |> (&if(api?, do: &1, else: Enum.map(&1, fn day -> Map.update!(day, :date, fn x -> to_string(x) end) end))).()
|> Enum.map(fn day -> Map.update!(day, :value, &Wei.to(&1, :ether)) end) |> (&if(api?, do: &1, else: Enum.map(&1, fn day -> Map.update!(day, :value, fn x -> Wei.to(x, :ether) end) end))).()
today = Date.to_string(NaiveDateTime.utc_now()) today = Date.to_string(NaiveDateTime.utc_now())
@ -6241,4 +6348,127 @@ defmodule Explorer.Chain do
|> to_string() |> to_string()
|> String.downcase() |> String.downcase()
end end
def recent_transactions(options, [:pending | _], method_id_filter, type_filter_options) do
recent_pending_transactions(options, false, method_id_filter, type_filter_options)
end
def recent_transactions(options, _, method_id_filter, type_filter_options) do
recent_collated_transactions(false, options, method_id_filter, type_filter_options)
end
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)
if method_ids != [] do
query
|> where([tx], fragment("SUBSTRING(? FOR 4)", tx.input) in ^method_ids)
else
query
end
end
def apply_filter_by_method_id_to_transactions(query, filter),
do: apply_filter_by_method_id_to_transactions(query, [filter])
defp map_name_or_method_id_to_method_id(string) when is_binary(string) do
if id = @method_name_to_id_map[string] do
decode_method_id(id)
else
trimmed =
string
|> String.replace("0x", "", global: false)
decode_method_id(trimmed)
end
end
defp decode_method_id(method_id) when is_binary(method_id) do
case String.length(method_id) == 8 && Base.decode16(method_id, case: :mixed) do
{:ok, bytes} ->
[bytes]
_ ->
[]
end
end
def apply_filter_by_tx_type_to_transactions(query, [_ | _] = filter) do
{dynamic, modified_query} = apply_filter_by_tx_type_to_transactions_inner(filter, query)
modified_query
|> where(^dynamic)
end
def apply_filter_by_tx_type_to_transactions(query, _filter), do: query
def apply_filter_by_tx_type_to_transactions_inner(dynamic \\ dynamic(false), filter, query)
def apply_filter_by_tx_type_to_transactions_inner(dynamic, [type | remain], query) do
case type do
:contract_call ->
dynamic
|> filter_contract_call_dynamic()
|> apply_filter_by_tx_type_to_transactions_inner(
remain,
join(query, :inner, [tx], address in assoc(tx, :to_address), as: :to_address)
)
:contract_creation ->
dynamic
|> filter_contract_creation_dynamic()
|> apply_filter_by_tx_type_to_transactions_inner(remain, query)
:coin_transfer ->
dynamic
|> filter_transaction_dynamic()
|> apply_filter_by_tx_type_to_transactions_inner(remain, query)
:token_transfer ->
dynamic
|> filter_token_transfer_dynamic()
|> apply_filter_by_tx_type_to_transactions_inner(remain, query)
:token_creation ->
dynamic
|> filter_token_creation_dynamic()
|> apply_filter_by_tx_type_to_transactions_inner(
remain,
join(query, :inner, [tx], token in Token,
on: token.contract_address_hash == tx.created_contract_address_hash,
as: :created_token
)
)
end
end
def apply_filter_by_tx_type_to_transactions_inner(dynamic_query, _, query), do: {dynamic_query, query}
def filter_contract_creation_dynamic(dynamic) do
dynamic([tx], ^dynamic or is_nil(tx.to_address_hash))
end
def filter_transaction_dynamic(dynamic) do
dynamic([tx], ^dynamic or tx.value > ^0)
end
def filter_contract_call_dynamic(dynamic) do
dynamic([tx, to_address: to_address], ^dynamic or not is_nil(to_address.contract_code))
end
def filter_token_transfer_dynamic(dynamic) do
# TokenTransfer.__struct__.__meta__.source
dynamic(
[tx],
^dynamic or
fragment(
"NOT (SELECT transaction_hash FROM token_transfers WHERE transaction_hash = ? LIMIT 1) IS NULL",
tx.hash
)
)
end
def filter_token_creation_dynamic(dynamic) do
dynamic([tx, created_token: created_token], ^dynamic or (is_nil(tx.to_address_hash) and not is_nil(created_token)))
end
end end

@ -165,7 +165,6 @@ defmodule Explorer.Chain.Address.CurrentTokenBalance do
where: ctb.value > 0, where: ctb.value > 0,
left_join: t in Token, left_join: t in Token,
on: ctb.token_contract_address_hash == t.contract_address_hash, on: ctb.token_contract_address_hash == t.contract_address_hash,
preload: :token,
select: {ctb, t}, select: {ctb, t},
order_by: [desc: ctb.value, asc: t.type, asc: t.name] order_by: [desc: ctb.value, asc: t.type, asc: t.name]
) )

@ -9,6 +9,7 @@ defmodule Explorer.Chain.Transaction do
alias ABI.FunctionSelector alias ABI.FunctionSelector
alias Ecto.Association.NotLoaded
alias Ecto.Changeset alias Ecto.Changeset
alias Explorer.{Chain, Repo} alias Explorer.{Chain, Repo}
@ -464,6 +465,18 @@ defmodule Explorer.Chain.Transaction do
def decoded_input_data(%__MODULE__{input: %{bytes: bytes}}) when bytes in [nil, <<>>], do: {:error, :no_input_data} def decoded_input_data(%__MODULE__{input: %{bytes: bytes}}) when bytes in [nil, <<>>], do: {:error, :no_input_data}
def decoded_input_data(%__MODULE__{to_address: %{contract_code: nil}}), do: {:error, :not_a_contract_call} def decoded_input_data(%__MODULE__{to_address: %{contract_code: nil}}), do: {:error, :not_a_contract_call}
def decoded_input_data(%__MODULE__{
to_address: %{smart_contract: %NotLoaded{}},
input: input,
hash: hash
}) do
decoded_input_data(%__MODULE__{
to_address: %{smart_contract: nil},
input: input,
hash: hash
})
end
def decoded_input_data(%__MODULE__{ def decoded_input_data(%__MODULE__{
to_address: %{smart_contract: nil}, to_address: %{smart_contract: nil},
input: %{bytes: <<method_id::binary-size(4), _::binary>> = data}, input: %{bytes: <<method_id::binary-size(4), _::binary>> = data},
@ -499,7 +512,7 @@ defmodule Explorer.Chain.Transaction do
hash: hash hash: hash
}) do }) do
case do_decoded_input_data(data, abi, address_hash, hash) do case do_decoded_input_data(data, abi, address_hash, hash) do
# In some cases transactions use methods of some unpredictadle contracts, so we can try to look up for method in a whole DB # In some cases transactions use methods of some unpredictable contracts, so we can try to look up for method in a whole DB
{:error, :could_not_decode} -> {:error, :could_not_decode} ->
case decoded_input_data(%__MODULE__{ case decoded_input_data(%__MODULE__{
to_address: %{smart_contract: nil}, to_address: %{smart_contract: nil},
@ -558,14 +571,16 @@ defmodule Explorer.Chain.Transaction do
def get_method_name(_), do: "Transfer" def get_method_name(_), do: "Transfer"
defp parse_method_name(method_desc) do def parse_method_name(method_desc, need_upcase \\ true) do
method_desc method_desc
|> String.split("(") |> String.split("(")
|> Enum.at(0) |> Enum.at(0)
|> upcase_first |> upcase_first(need_upcase)
end end
defp upcase_first(<<first::utf8, rest::binary>>), do: String.upcase(<<first::utf8>>) <> rest defp upcase_first(string, false), do: string
defp upcase_first(<<first::utf8, rest::binary>>, true), do: String.upcase(<<first::utf8>>) <> rest
defp function_call(name, mapping) do defp function_call(name, mapping) do
text = text =

@ -268,7 +268,8 @@ defimpl Inspect, for: Explorer.Chain.Wei do
end end
defimpl Jason.Encoder, for: Explorer.Chain.Wei do defimpl Jason.Encoder, for: Explorer.Chain.Wei do
def encode(wei, _) do def encode(wei, opts) do
Decimal.to_string(wei.value) # changed since it's needed to return wei value (which is big number) as string
Jason.Encode.struct(wei.value, opts)
end end
end end

@ -53,9 +53,9 @@ defmodule Explorer.Counters.AddressTokenUsdSum do
@spec address_tokens_usd_sum([{Address.CurrentTokenBalance, Explorer.Chain.Token}]) :: Decimal.t() @spec address_tokens_usd_sum([{Address.CurrentTokenBalance, Explorer.Chain.Token}]) :: Decimal.t()
defp address_tokens_usd_sum(token_balances) do defp address_tokens_usd_sum(token_balances) do
token_balances token_balances
|> Enum.reduce(Decimal.new(0), fn {token_balance, _}, acc -> |> Enum.reduce(Decimal.new(0), fn {token_balance, token}, acc ->
if token_balance.value && token_balance.token.usd_value do if token_balance.value && token.usd_value do
Decimal.add(acc, Chain.balance_in_usd(token_balance)) Decimal.add(acc, Chain.balance_in_usd(token_balance, token))
else else
acc acc
end end

@ -76,7 +76,7 @@ defmodule Explorer.Market do
Enum.map(tokens, fn item -> Enum.map(tokens, fn item ->
case item do case item do
{token_balance, token} -> {token_balance, token} ->
{add_price(token_balance), token} {token_balance, add_price(token)}
token_balance -> token_balance ->
add_price(token_balance) add_price(token_balance)

@ -0,0 +1,15 @@
defmodule Explorer.Repo.Migrations.AddMethodIdIndex do
use Ecto.Migration
@disable_ddl_transaction true
def up do
execute("""
CREATE INDEX CONCURRENTLY method_id ON public.transactions USING btree (substring(input for 4));
""")
end
def down do
execute("DROP INDEX method_id")
end
end

@ -3829,12 +3829,12 @@ defmodule Explorer.ChainTest do
describe "recent_collated_transactions/1" do describe "recent_collated_transactions/1" do
test "with no collated transactions it returns an empty list" do test "with no collated transactions it returns an empty list" do
assert [] == Explorer.Chain.recent_collated_transactions() assert [] == Explorer.Chain.recent_collated_transactions(true)
end end
test "it excludes pending transactions" do test "it excludes pending transactions" do
insert(:transaction) insert(:transaction)
assert [] == Explorer.Chain.recent_collated_transactions() assert [] == Explorer.Chain.recent_collated_transactions(true)
end end
test "returns a list of recent collated transactions" do test "returns a list of recent collated transactions" do
@ -3846,7 +3846,7 @@ defmodule Explorer.ChainTest do
oldest_seen = Enum.at(newest_first_transactions, 9) oldest_seen = Enum.at(newest_first_transactions, 9)
paging_options = %Explorer.PagingOptions{page_size: 10, key: {oldest_seen.block_number, oldest_seen.index}} paging_options = %Explorer.PagingOptions{page_size: 10, key: {oldest_seen.block_number, oldest_seen.index}}
recent_collated_transactions = Explorer.Chain.recent_collated_transactions(paging_options: paging_options) recent_collated_transactions = Explorer.Chain.recent_collated_transactions(true, paging_options: paging_options)
assert length(recent_collated_transactions) == 10 assert length(recent_collated_transactions) == 10
assert hd(recent_collated_transactions).hash == Enum.at(newest_first_transactions, 10).hash assert hd(recent_collated_transactions).hash == Enum.at(newest_first_transactions, 10).hash
@ -3868,10 +3868,11 @@ defmodule Explorer.ChainTest do
to_address: address, to_address: address,
transaction: transaction, transaction: transaction,
token_contract_address: token_contract_address, token_contract_address: token_contract_address,
token: token token: token,
block: transaction.block
) )
fetched_transaction = List.first(Explorer.Chain.recent_collated_transactions()) fetched_transaction = List.first(Explorer.Chain.recent_collated_transactions(true))
assert fetched_transaction.hash == transaction.hash assert fetched_transaction.hash == transaction.hash
assert length(fetched_transaction.token_transfers) == 2 assert length(fetched_transaction.token_transfers) == 2
end end

@ -170,6 +170,8 @@ config :block_scout_web, BlockScoutWeb.Chain.Address.CoinBalance,
# days # days
coin_balance_history_days: System.get_env("COIN_BALANCE_HISTORY_DAYS", "10") coin_balance_history_days: System.get_env("COIN_BALANCE_HISTORY_DAYS", "10")
config :block_scout_web, BlockScoutWeb.API.V2, enabled: System.get_env("API_V2_ENABLED") == "true"
######################## ########################
### Ethereum JSONRPC ### ### Ethereum JSONRPC ###
######################## ########################

Loading…
Cancel
Save