Add /smart-contracts and /state-changes endpoints (#6973)

* Add /smart-contracts endpoint to API v2

* Add transactions/{hash}/state-changes endpoint; State changes code refactoring

* Add tests

* Disable CheckBytecodeMatchingOnDemand fetcher in test environment

* CHANGELOG.md
pull/6981/head
nikitosing 2 years ago committed by GitHub
parent d63eb813ca
commit 1a232e11f4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      CHANGELOG.md
  2. 1
      apps/block_scout_web/lib/block_scout_web/api_router.ex
  3. 2
      apps/block_scout_web/lib/block_scout_web/controllers/api/v2/block_controller.ex
  4. 25
      apps/block_scout_web/lib/block_scout_web/controllers/api/v2/smart_contract_controller.ex
  5. 7
      apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex
  6. 19
      apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex
  7. 2
      apps/block_scout_web/lib/block_scout_web/controllers/block_transaction_controller.ex
  8. 375
      apps/block_scout_web/lib/block_scout_web/controllers/transaction_state_controller.ex
  9. 20
      apps/block_scout_web/lib/block_scout_web/controllers/verified_contracts_controller.ex
  10. 402
      apps/block_scout_web/lib/block_scout_web/models/transaction_state_helper.ex
  11. 25
      apps/block_scout_web/lib/block_scout_web/paging_helper.ex
  12. 1
      apps/block_scout_web/lib/block_scout_web/smart_contracts_api_v2_router.ex
  13. 102
      apps/block_scout_web/lib/block_scout_web/templates/transaction_state/_state_change.html.eex
  14. 42
      apps/block_scout_web/lib/block_scout_web/views/api/v2/smart_contract_view.ex
  15. 55
      apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex
  16. 10
      apps/block_scout_web/lib/block_scout_web/views/transaction_state_view.ex
  17. 8
      apps/block_scout_web/priv/gettext/default.pot
  18. 8
      apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po
  19. 6
      apps/block_scout_web/test/block_scout_web/controllers/address_read_contract_controller_test.exs
  20. 6
      apps/block_scout_web/test/block_scout_web/controllers/address_read_proxy_controller_test.exs
  21. 6
      apps/block_scout_web/test/block_scout_web/controllers/address_write_contract_controller_test.exs
  22. 6
      apps/block_scout_web/test/block_scout_web/controllers/address_write_proxy_controller_test.exs
  23. 94
      apps/block_scout_web/test/block_scout_web/controllers/api/v2/smart_contract_controller_test.exs
  24. 79
      apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs
  25. 16
      apps/block_scout_web/test/block_scout_web/controllers/smart_contract_controller_test.exs
  26. 13
      apps/block_scout_web/test/block_scout_web/controllers/transaction_state_controller_test.exs
  27. 2
      apps/explorer/config/test.exs
  28. 6
      apps/explorer/lib/explorer/chain/transaction/state_change.ex
  29. 2
      apps/explorer/lib/explorer/paging_options.ex
  30. 12
      apps/explorer/test/explorer/chain/log_test.exs

@ -4,6 +4,7 @@
### Features
- [#6973](https://github.com/blockscout/blockscout/pull/6973) - API v2: `/smart-contracts` and `/state-changes` endpoints
- [#6897](https://github.com/blockscout/blockscout/pull/6897) - Support basic auth in JSON RPC endpoint
- [#6908](https://github.com/blockscout/blockscout/pull/6908) - Allow disable API rate limit
- [#6951](https://github.com/blockscout/blockscout/pull/6951), [#6958](https://github.com/blockscout/blockscout/pull/6958) - Set poll: true for TokenInstance fetcher

@ -114,6 +114,7 @@ defmodule BlockScoutWeb.ApiRouter do
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)
get("/:transaction_hash/state-changes", V2.TransactionController, :state_changes)
end
scope "/blocks" do

@ -28,7 +28,7 @@ defmodule BlockScoutWeb.API.V2.BlockController do
with {:ok, block} <-
BlockTransactionController.param_block_hash_or_number_to_block(block_hash_or_number,
necessity_by_association: %{
[miner: :names] => :required,
[miner: :names] => :optional,
:uncles => :optional,
:nephews => :optional,
:rewards => :optional,

@ -1,6 +1,11 @@
defmodule BlockScoutWeb.API.V2.SmartContractController do
use BlockScoutWeb, :controller
import BlockScoutWeb.Chain, only: [paging_options: 1, next_page_params: 3, split_list_by_page: 1]
import BlockScoutWeb.PagingHelper,
only: [current_filter: 1, delete_parameters_from_next_page_params: 1, search_query: 1]
import Explorer.SmartContract.Solidity.Verifier, only: [parse_boolean: 1]
alias BlockScoutWeb.{AccessHelpers, AddressView}
@ -169,6 +174,26 @@ defmodule BlockScoutWeb.API.V2.SmartContractController do
end
end
def smart_contracts_list(conn, params) do
full_options =
[necessity_by_association: %{[address: :token] => :optional, [address: :names] => :optional}]
|> Keyword.merge(paging_options(params))
|> Keyword.merge(current_filter(params))
|> Keyword.merge(search_query(params))
smart_contracts_plus_one = Chain.verified_contracts(full_options)
{smart_contracts, next_page} = split_list_by_page(smart_contracts_plus_one)
next_page_params =
next_page
|> next_page_params(smart_contracts, params)
|> delete_parameters_from_next_page_params()
conn
|> put_status(200)
|> render(:smart_contracts, %{smart_contracts: smart_contracts, next_page_params: next_page_params})
end
def prepare_args(list) when is_list(list), do: list
def prepare_args(other), do: [other]
end

@ -142,12 +142,7 @@ defmodule BlockScoutWeb.API.V2.TokenController do
end
def tokens_list(conn, params) do
filter =
if Map.has_key?(params, "filter") do
Map.get(params, "filter")
else
nil
end
filter = params["q"]
paging_params =
params

@ -14,6 +14,7 @@ defmodule BlockScoutWeb.API.V2.TransactionController do
]
alias BlockScoutWeb.AccessHelpers
alias BlockScoutWeb.Models.TransactionStateHelper
alias Explorer.Chain
alias Indexer.Fetcher.FirstTraceOnDemand
@ -221,4 +222,22 @@ defmodule BlockScoutWeb.API.V2.TransactionController do
})
end
end
def state_changes(conn, %{"transaction_hash" => transaction_hash_string} = params) 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:
Map.merge(@transaction_necessity_by_association, %{[block: [miner: :names]] => :optional})
)},
{:ok, false} <- AccessHelpers.restricted_access?(to_string(transaction.from_address_hash), params),
{:ok, false} <- AccessHelpers.restricted_access?(to_string(transaction.to_address_hash), params) do
state_changes = TransactionStateHelper.state_changes(transaction)
conn
|> put_status(200)
|> render(:state_changes, %{state_changes: state_changes})
end
end
end

@ -99,7 +99,7 @@ defmodule BlockScoutWeb.BlockTransactionController do
def index(conn, %{"block_hash_or_number" => formatted_block_hash_or_number}) do
case param_block_hash_or_number_to_block(formatted_block_hash_or_number,
necessity_by_association: %{
[miner: :names] => :required,
[miner: :names] => :optional,
:uncles => :optional,
:nephews => :optional,
:rewards => :optional

@ -4,31 +4,31 @@ defmodule BlockScoutWeb.TransactionStateController do
alias BlockScoutWeb.{
AccessHelpers,
Controller,
Models.TransactionStateHelper,
TransactionController,
TransactionStateView
}
alias Explorer.{Chain, Chain.Wei, Market, PagingOptions}
alias Explorer.{Chain, Market}
alias Explorer.ExchangeRates.Token
alias Phoenix.View
alias Indexer.Fetcher.{CoinBalance, TokenBalance}
import BlockScoutWeb.Account.AuthController, only: [current_user: 1]
import BlockScoutWeb.Models.GetAddressTags, only: [get_address_tags: 2]
import BlockScoutWeb.Models.GetTransactionTags, only: [get_transaction_with_addresses_tags: 2]
{:ok, burn_address_hash} = Chain.string_to_address_hash("0x0000000000000000000000000000000000000000")
@burn_address_hash burn_address_hash
def index(conn, %{"transaction_id" => transaction_hash_string, "type" => "JSON"} = params) do
with {:ok, transaction_hash} <- Chain.string_to_transaction_hash(transaction_hash_string),
:ok <- Chain.check_transaction_exists(transaction_hash),
{:ok, transaction} <-
Chain.hash_to_transaction(
transaction_hash,
necessity_by_association: %{
[block: :miner] => :required,
from_address: :required,
[block: :miner] => :optional,
from_address: :optional,
to_address: :optional
}
),
@ -36,117 +36,26 @@ defmodule BlockScoutWeb.TransactionStateController do
AccessHelpers.restricted_access?(to_string(transaction.from_address_hash), params),
{:ok, false} <-
AccessHelpers.restricted_access?(to_string(transaction.to_address_hash), params) do
full_options = [
necessity_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
},
# we need to consider all token transfers in block to show whole state change of transaction
paging_options: %PagingOptions{key: nil, page_size: nil}
]
token_transfers = Chain.transaction_to_token_transfers(transaction_hash, full_options)
block = transaction.block
block_txs =
Chain.block_to_transactions(block.hash,
necessity_by_association: %{},
paging_options: %PagingOptions{key: nil, page_size: nil}
)
{from_before, to_before, miner_before} = coin_balances_before(transaction, block_txs)
from_hash = transaction.from_address_hash
to_hash = transaction.to_address_hash
miner_hash = block.miner_hash
from_coin_entry =
if from_hash not in [to_hash, miner_hash] do
from = transaction.from_address
from_after = do_update_coin_balance_from_tx(from_hash, transaction, from_before, block)
state_changes = TransactionStateHelper.state_changes(transaction)
rendered_changes =
Enum.map(state_changes, fn state_change ->
View.render_to_string(
TransactionStateView,
"_state_change.html",
coin_or_token_transfers: :coin,
address: from,
coin_or_token_transfers: state_change.coin_or_token_transfers,
address: state_change.address,
burn_address_hash: @burn_address_hash,
balance_before: from_before,
balance_after: from_after,
balance_diff: Wei.sub(from_after, from_before),
conn: conn
balance_before: state_change.balance_before,
balance_after: state_change.balance_after,
balance_diff: state_change.balance_diff,
conn: conn,
miner: state_change.miner?
)
end
to_coin_entry =
if not is_nil(to_hash) and to_hash != miner_hash do
to = transaction.to_address
to_after = do_update_coin_balance_from_tx(to_hash, transaction, to_before, block)
View.render_to_string(
TransactionStateView,
"_state_change.html",
coin_or_token_transfers: :coin,
address: to,
burn_address_hash: @burn_address_hash,
balance_before: to_before,
balance_after: to_after,
balance_diff: Wei.sub(to_after, to_before),
conn: conn
)
end
miner = block.miner
miner_after = do_update_coin_balance_from_tx(miner_hash, transaction, miner_before, block)
miner_entry =
View.render_to_string(
TransactionStateView,
"_state_change.html",
coin_or_token_transfers: :coin,
address: miner,
burn_address_hash: @burn_address_hash,
balance_before: miner_before,
balance_after: miner_after,
balance_diff: Wei.sub(miner_after, miner_before),
miner: true,
conn: conn
)
token_balances_before = token_balances_before(token_transfers, transaction, block_txs)
token_balances_after =
do_update_token_balances_from_token_transfers(
token_transfers,
token_balances_before,
:include_transfers
)
items =
for {address, balances} <- token_balances_after,
{token_hash, {balance, transfers}} <- balances do
balance_before = token_balances_before[address][token_hash]
View.render_to_string(
TransactionStateView,
"_state_change.html",
coin_or_token_transfers: transfers,
address: address,
burn_address_hash: @burn_address_hash,
balance_before: balance_before,
balance_after: balance,
balance_diff: Decimal.sub(balance, balance_before),
conn: conn
)
end
end)
json(conn, %{
items: [from_coin_entry, to_coin_entry, miner_entry | items] |> Enum.reject(&is_nil/1) |> Enum.sort()
items: rendered_changes
})
else
{:restricted_access, _} ->
@ -212,254 +121,4 @@ defmodule BlockScoutWeb.TransactionStateController do
TransactionController.set_not_found_view(conn, transaction_hash_string)
end
end
def coin_balance(address_hash, _block_number) when is_nil(address_hash) do
%Wei{value: Decimal.new(0)}
end
def coin_balance(address_hash, block_number) do
case Chain.get_coin_balance(address_hash, block_number) do
%{value: val} when not is_nil(val) ->
val
_ ->
json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments)
CoinBalance.run([{address_hash.bytes, block_number}], json_rpc_named_arguments)
# after CoinBalance.run balance is fetched and imported, so we can call coin_balance again
coin_balance(address_hash, block_number)
end
end
def coin_balances_before(tx, block_txs) do
block = tx.block
from_before = coin_balance(tx.from_address_hash, block.number - 1)
to_before = coin_balance(tx.to_address_hash, block.number - 1)
miner_before = coin_balance(block.miner_hash, block.number - 1)
block_txs
|> Enum.reduce_while(
{from_before, to_before, miner_before},
fn block_tx, {block_from, block_to, block_miner} = state ->
if block_tx.index < tx.index do
{:cont,
{do_update_coin_balance_from_tx(tx.from_address_hash, block_tx, block_from, block),
do_update_coin_balance_from_tx(tx.to_address_hash, block_tx, block_to, block),
do_update_coin_balance_from_tx(tx.block.miner_hash, block_tx, block_miner, block)}}
else
# txs ordered by index ascending, so we can halt after facing index greater or equal than index of our tx
{:halt, state}
end
end
)
end
defp do_update_coin_balance_from_tx(address_hash, tx, balance, block) do
from = tx.from_address_hash
to = tx.to_address_hash
miner = block.miner_hash
balance
|> (&if(address_hash == from, do: Wei.sub(&1, from_loss(tx)), else: &1)).()
|> (&if(address_hash == to, do: Wei.sum(&1, to_profit(tx)), else: &1)).()
|> (&if(address_hash == miner, do: Wei.sum(&1, miner_profit(tx, block)), else: &1)).()
end
def token_balance(@burn_address_hash, _token_transfer, _block_number) do
Decimal.new(0)
end
def token_balance(address_hash, token_transfer, block_number) do
token = token_transfer.token
token_contract_address_hash = token.contract_address_hash
case Chain.get_token_balance(address_hash, token_contract_address_hash, block_number) do
%{value: val} when not is_nil(val) ->
val
# we haven't fetched this balance yet
_ ->
json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments)
token_id_int =
case token_transfer.token_id do
%Decimal{} -> Decimal.to_integer(token_transfer.token_id)
id_int when is_integer(id_int) -> id_int
_ -> token_transfer.token_id
end
TokenBalance.run(
[
{address_hash.bytes, token_contract_address_hash.bytes, block_number, token.type, token_id_int, 0}
],
json_rpc_named_arguments
)
# after TokenBalance.run balance is fetched and imported, so we can call token_balance again
token_balance(address_hash, token_transfer, block_number)
end
end
def token_balances_before(token_transfers, tx, block_txs) do
balances_before =
token_transfers
|> Enum.reduce(%{}, fn transfer, balances_map ->
from = transfer.from_address
to = transfer.to_address
token_hash = transfer.token_contract_address_hash
prev_block = transfer.block_number - 1
balances_with_from =
case balances_map do
# from address already in the map
%{^from => %{^token_hash => _}} ->
balances_map
# we need to add from address into the map
_ ->
put_in(
balances_map,
Enum.map([from, token_hash], &Access.key(&1, %{})),
token_balance(from.hash, transfer, prev_block)
)
end
case balances_with_from do
# to address already in the map
%{^to => %{^token_hash => _}} ->
balances_with_from
# we need to add to address into the map
_ ->
put_in(
balances_with_from,
Enum.map([to, token_hash], &Access.key(&1, %{})),
token_balance(to.hash, transfer, prev_block)
)
end
end)
block_txs
|> Enum.reduce_while(
balances_before,
fn block_tx, state ->
if block_tx.index < tx.index do
{:cont, do_update_token_balances_from_token_transfers(block_tx.token_transfers, state)}
else
# txs ordered by index ascending, so we can halt after facing index greater or equal than index of our tx
{:halt, state}
end
end
)
end
defp do_update_token_balances_from_token_transfers(
token_transfers,
balances_map,
include_transfers \\ :no
) do
Enum.reduce(
token_transfers,
balances_map,
&token_transfers_balances_reducer(&1, &2, include_transfers)
)
end
defp token_transfers_balances_reducer(transfer, state_balances_map, include_transfers) do
from = transfer.from_address
to = transfer.to_address
token = transfer.token_contract_address_hash
balances_map_from_included =
case state_balances_map do
# from address is needed to be updated in our map
%{^from => %{^token => val}} ->
put_in(
state_balances_map,
Enum.map([from, token], &Access.key(&1, %{})),
do_update_balance(val, :from, transfer, include_transfers)
)
# we are not interested in this address
_ ->
state_balances_map
end
case balances_map_from_included do
# to address is needed to be updated in our map
%{^to => %{^token => val}} ->
put_in(
balances_map_from_included,
Enum.map([to, token], &Access.key(&1, %{})),
do_update_balance(val, :to, transfer, include_transfers)
)
# we are not interested in this address
_ ->
balances_map_from_included
end
end
# point of this function is to include all transfers for frontend if option :include_transfer is passed
defp do_update_balance(old_val, type, transfer, include_transfers) do
transfer_amount = if is_nil(transfer.amount), do: 1, else: transfer.amount
case {include_transfers, old_val, type} do
{:include_transfers, {val, transfers}, :from} ->
{Decimal.sub(val, transfer_amount), [{type, transfer} | transfers]}
{:include_transfers, {val, transfers}, :to} ->
{Decimal.add(val, transfer_amount), [{type, transfer} | transfers]}
{:include_transfers, val, :from} ->
{Decimal.sub(val, transfer_amount), [{type, transfer}]}
{:include_transfers, val, :to} ->
{Decimal.add(val, transfer_amount), [{type, transfer}]}
{_, val, :from} ->
Decimal.sub(val, transfer_amount)
{_, val, :to} ->
Decimal.add(val, transfer_amount)
end
end
def from_loss(tx) do
{_, fee} = Chain.fee(tx, :wei)
if error?(tx) do
%Wei{value: fee}
else
Wei.sum(tx.value, %Wei{value: fee})
end
end
def to_profit(tx) do
if error?(tx) do
%Wei{value: 0}
else
tx.value
end
end
def miner_profit(tx, block) do
base_fee_per_gas = block.base_fee_per_gas || %Wei{value: Decimal.new(0)}
max_priority_fee_per_gas = tx.max_priority_fee_per_gas || tx.gas_price
max_fee_per_gas = tx.max_fee_per_gas || tx.gas_price
priority_fee_per_gas =
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)
Wei.mult(priority_fee_per_gas, tx.gas_used)
end
defp error?(tx) do
case Chain.transaction_to_status(tx) do
{:error, _} -> true
_ -> false
end
end
end

@ -4,6 +4,8 @@ defmodule BlockScoutWeb.VerifiedContractsController do
import BlockScoutWeb.Chain,
only: [paging_options: 1, next_page_params: 3, split_list_by_page: 1, fetch_page_number: 1]
import BlockScoutWeb.PagingHelper, only: [current_filter: 1, search_query: 1]
alias BlockScoutWeb.{Controller, VerifiedContractsView}
alias Explorer.{Chain, Market}
alias Explorer.ExchangeRates.Token
@ -54,22 +56,4 @@ defmodule BlockScoutWeb.VerifiedContractsController do
new_verified_contracts_count: Chain.count_new_verified_contracts_from_cache()
)
end
defp current_filter(%{"filter" => "solidity"}) do
[filter: :solidity]
end
defp current_filter(%{"filter" => "vyper"}) do
[filter: :vyper]
end
defp current_filter(_), do: []
defp search_query(%{"search" => ""}), do: []
defp search_query(%{"search" => search_string}) do
[search: search_string]
end
defp search_query(_), do: []
end

@ -0,0 +1,402 @@
defmodule BlockScoutWeb.Models.TransactionStateHelper do
@moduledoc """
Module includes functions needed for BlockScoutWeb.TransactionStateController
"""
alias Explorer.{Chain, Chain.Wei, PagingOptions}
alias Explorer.Chain
alias Explorer.Chain.Transaction.StateChange
alias Indexer.Fetcher.{CoinBalance, TokenBalance}
{:ok, burn_address_hash} = Chain.string_to_address_hash("0x0000000000000000000000000000000000000000")
@burn_address_hash burn_address_hash
# credo:disable-for-next-line /Complexity/
def state_changes(transaction) do
transaction_hash = transaction.hash
full_options = [
necessity_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
},
# we need to consider all token transfers in block to show whole state change of transaction
paging_options: %PagingOptions{key: nil, page_size: nil}
]
token_transfers = Chain.transaction_to_token_transfers(transaction_hash, full_options)
block = transaction.block
block_txs =
Chain.block_to_transactions(block.hash,
necessity_by_association: %{},
paging_options: %PagingOptions{key: nil, page_size: nil}
)
{from_before, to_before, miner_before} = coin_balances_before(transaction, block_txs)
from_hash = transaction.from_address_hash
to_hash = transaction.to_address_hash
miner_hash = block.miner_hash
from_coin_entry =
if from_hash not in [to_hash, miner_hash] do
from = transaction.from_address
from_after = do_update_coin_balance_from_tx(from_hash, transaction, from_before, block)
balance_diff = Wei.sub(from_after, from_before)
if has_diff?(balance_diff) do
%StateChange{
coin_or_token_transfers: :coin,
address: from,
balance_before: from_before,
balance_after: from_after,
balance_diff: balance_diff,
miner?: false
}
end
end
to_coin_entry =
if not is_nil(to_hash) and to_hash != miner_hash do
to = transaction.to_address
to_after = do_update_coin_balance_from_tx(to_hash, transaction, to_before, block)
balance_diff = Wei.sub(to_after, to_before)
if has_diff?(balance_diff) do
%StateChange{
coin_or_token_transfers: :coin,
address: to,
balance_before: to_before,
balance_after: to_after,
balance_diff: balance_diff,
miner?: false
}
end
end
miner = block.miner
miner_after = do_update_coin_balance_from_tx(miner_hash, transaction, miner_before, block)
miner_diff = Wei.sub(miner_after, miner_before)
miner_entry =
if has_diff?(miner_diff) do
%StateChange{
coin_or_token_transfers: :coin,
address: miner,
balance_before: miner_before,
balance_after: miner_after,
balance_diff: miner_diff,
miner?: true
}
end
token_balances_before = token_balances_before(token_transfers, transaction, block_txs)
token_balances_after =
do_update_token_balances_from_token_transfers(
token_transfers,
token_balances_before,
:include_transfers
)
items =
for {address, balances} <- token_balances_after,
{token_hash, {balance, transfers}} <- balances do
balance_before = token_balances_before[address][token_hash]
balance_diff = Decimal.sub(balance, balance_before)
transfer = elem(List.first(transfers), 1)
if transfer.token.type != "ERC-20" or has_diff?(balance_diff) do
%StateChange{
coin_or_token_transfers: transfers,
address: address,
balance_before: balance_before,
balance_after: balance,
balance_diff: balance_diff,
miner?: false
}
end
end
[from_coin_entry, to_coin_entry, miner_entry | items]
|> Enum.reject(&is_nil/1)
|> Enum.sort_by(fn state_change -> to_string(state_change.address && state_change.address.hash) end)
end
defp coin_balance(address_hash, block_number, retry? \\ false)
defp coin_balance(address_hash, _block_number, _retry?) when is_nil(address_hash) do
%Wei{value: Decimal.new(0)}
end
defp coin_balance(address_hash, block_number, retry?) do
case Chain.get_coin_balance(address_hash, block_number) do
%{value: val} when not is_nil(val) ->
val
_ ->
if retry? do
%Wei{value: Decimal.new(0)}
else
json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments)
CoinBalance.run([{address_hash.bytes, block_number}], json_rpc_named_arguments)
# after CoinBalance.run balance is fetched and imported, so we can call coin_balance again
coin_balance(address_hash, block_number, true)
end
end
end
defp coin_balances_before(tx, block_txs) do
block = tx.block
from_before = coin_balance(tx.from_address_hash, block.number - 1)
to_before = coin_balance(tx.to_address_hash, block.number - 1)
miner_before = coin_balance(block.miner_hash, block.number - 1)
block_txs
|> Enum.reduce_while(
{from_before, to_before, miner_before},
fn block_tx, {block_from, block_to, block_miner} = state ->
if block_tx.index < tx.index do
{:cont,
{do_update_coin_balance_from_tx(tx.from_address_hash, block_tx, block_from, block),
do_update_coin_balance_from_tx(tx.to_address_hash, block_tx, block_to, block),
do_update_coin_balance_from_tx(tx.block.miner_hash, block_tx, block_miner, block)}}
else
# txs ordered by index ascending, so we can halt after facing index greater or equal than index of our tx
{:halt, state}
end
end
)
end
defp do_update_coin_balance_from_tx(address_hash, tx, balance, block) do
from = tx.from_address_hash
to = tx.to_address_hash
miner = block.miner_hash
balance
|> (&if(address_hash == from, do: Wei.sub(&1, from_loss(tx)), else: &1)).()
|> (&if(address_hash == to, do: Wei.sum(&1, to_profit(tx)), else: &1)).()
|> (&if(address_hash == miner, do: Wei.sum(&1, miner_profit(tx, block)), else: &1)).()
end
defp token_balance(address_hash, token_transfer, block_number, retry? \\ false)
defp token_balance(@burn_address_hash, _token_transfer, _block_number, _retry?) do
Decimal.new(0)
end
defp token_balance(address_hash, token_transfer, block_number, retry?) do
token = token_transfer.token
token_contract_address_hash = token.contract_address_hash
case Chain.get_token_balance(address_hash, token_contract_address_hash, block_number) do
%{value: val} when not is_nil(val) ->
val
# we haven't fetched this balance yet
_ ->
if retry? do
Decimal.new(0)
else
json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments)
token_id_int =
case token_transfer.token_id do
%Decimal{} -> Decimal.to_integer(token_transfer.token_id)
id_int when is_integer(id_int) -> id_int
_ -> token_transfer.token_id
end
TokenBalance.run(
[
{address_hash.bytes, token_contract_address_hash.bytes, block_number, token.type, token_id_int, 0}
],
json_rpc_named_arguments
)
# after TokenBalance.run balance is fetched and imported, so we can call token_balance again
token_balance(address_hash, token_transfer, block_number, true)
end
end
end
defp token_balances_before(token_transfers, tx, block_txs) do
balances_before =
token_transfers
|> Enum.reduce(%{}, fn transfer, balances_map ->
from = transfer.from_address
to = transfer.to_address
token_hash = transfer.token_contract_address_hash
prev_block = transfer.block_number - 1
balances_with_from =
case balances_map do
# from address already in the map
%{^from => %{^token_hash => _}} ->
balances_map
# we need to add from address into the map
_ ->
put_in(
balances_map,
Enum.map([from, token_hash], &Access.key(&1, %{})),
token_balance(from.hash, transfer, prev_block)
)
end
case balances_with_from do
# to address already in the map
%{^to => %{^token_hash => _}} ->
balances_with_from
# we need to add to address into the map
_ ->
put_in(
balances_with_from,
Enum.map([to, token_hash], &Access.key(&1, %{})),
token_balance(to.hash, transfer, prev_block)
)
end
end)
block_txs
|> Enum.reduce_while(
balances_before,
fn block_tx, state ->
if block_tx.index < tx.index do
{:cont, do_update_token_balances_from_token_transfers(block_tx.token_transfers, state)}
else
# txs ordered by index ascending, so we can halt after facing index greater or equal than index of our tx
{:halt, state}
end
end
)
end
defp do_update_token_balances_from_token_transfers(
token_transfers,
balances_map,
include_transfers \\ :no
) do
Enum.reduce(
token_transfers,
balances_map,
&token_transfers_balances_reducer(&1, &2, include_transfers)
)
end
defp token_transfers_balances_reducer(transfer, state_balances_map, include_transfers) do
from = transfer.from_address
to = transfer.to_address
token = transfer.token_contract_address_hash
balances_map_from_included =
case state_balances_map do
# from address is needed to be updated in our map
%{^from => %{^token => val}} ->
put_in(
state_balances_map,
Enum.map([from, token], &Access.key(&1, %{})),
do_update_balance(val, :from, transfer, include_transfers)
)
# we are not interested in this address
_ ->
state_balances_map
end
case balances_map_from_included do
# to address is needed to be updated in our map
%{^to => %{^token => val}} ->
put_in(
balances_map_from_included,
Enum.map([to, token], &Access.key(&1, %{})),
do_update_balance(val, :to, transfer, include_transfers)
)
# we are not interested in this address
_ ->
balances_map_from_included
end
end
# point of this function is to include all transfers for frontend if option :include_transfer is passed
defp do_update_balance(old_val, type, transfer, include_transfers) do
transfer_amount = if is_nil(transfer.amount), do: 1, else: transfer.amount
case {include_transfers, old_val, type} do
{:include_transfers, {val, transfers}, :from} ->
{Decimal.sub(val, transfer_amount), [{type, transfer} | transfers]}
{:include_transfers, {val, transfers}, :to} ->
{Decimal.add(val, transfer_amount), [{type, transfer} | transfers]}
{:include_transfers, val, :from} ->
{Decimal.sub(val, transfer_amount), [{type, transfer}]}
{:include_transfers, val, :to} ->
{Decimal.add(val, transfer_amount), [{type, transfer}]}
{_, val, :from} ->
Decimal.sub(val, transfer_amount)
{_, val, :to} ->
Decimal.add(val, transfer_amount)
end
end
def from_loss(tx) do
{_, fee} = Chain.fee(tx, :wei)
if error?(tx) do
%Wei{value: fee}
else
Wei.sum(tx.value, %Wei{value: fee})
end
end
def to_profit(tx) do
if error?(tx) do
%Wei{value: 0}
else
tx.value
end
end
defp miner_profit(tx, block) do
base_fee_per_gas = block.base_fee_per_gas || %Wei{value: Decimal.new(0)}
max_priority_fee_per_gas = tx.max_priority_fee_per_gas || tx.gas_price
max_fee_per_gas = tx.max_fee_per_gas || tx.gas_price
priority_fee_per_gas =
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)
Wei.mult(priority_fee_per_gas, tx.gas_used)
end
defp error?(tx) do
case Chain.transaction_to_status(tx) do
{:error, _} -> true
_ -> false
end
end
def has_diff?(%Wei{value: val}) do
not Decimal.eq?(val, Decimal.new(0))
end
def has_diff?(val) do
not Decimal.eq?(val, Decimal.new(0))
end
end

@ -134,7 +134,32 @@ defmodule BlockScoutWeb.PagingHelper do
|> Map.delete("method")
|> Map.delete("filter")
|> Map.delete("token_address_hash")
|> Map.delete("q")
end
def delete_parameters_from_next_page_params(_), do: nil
def current_filter(%{"filter" => "solidity"}) do
[filter: :solidity]
end
def current_filter(%{"filter" => "vyper"}) do
[filter: :vyper]
end
def current_filter(_), do: []
def search_query(%{"search" => ""}), do: []
def search_query(%{"search" => search_string}) do
[search: search_string]
end
def search_query(%{"q" => ""}), do: []
def search_query(%{"q" => search_string}) do
[search: search_string]
end
def search_query(_), do: []
end

@ -21,6 +21,7 @@ defmodule BlockScoutWeb.SmartContractsApiV2Router do
alias BlockScoutWeb.API.V2
get("/", V2.SmartContractController, :smart_contracts_list)
get("/:address_hash", V2.SmartContractController, :smart_contract)
get("/:address_hash/methods-read", V2.SmartContractController, :methods_read)
get("/:address_hash/methods-write", V2.SmartContractController, :methods_write)

@ -1,66 +1,64 @@
<% coin_or_transfer = if @coin_or_token_transfers == :coin, do: :coin, else: elem(List.first(@coin_or_token_transfers), 1)%>
<%= if coin_or_transfer != :coin and coin_or_transfer.token.type != "ERC-20" or has_diff?(@balance_diff) do %>
<tr data-identifier-hash="<%= @address && @address.hash %>">
<%= if @address.hash == @burn_address_hash do %>
<% coin_or_transfer = if @coin_or_token_transfers == :coin, do: :coin, else: elem(List.first(@coin_or_token_transfers), 1) %>
<tr data-identifier-hash="<%= @address && @address.hash %>">
<%= if @address.hash == @burn_address_hash do %>
<td class="stakes-td">
<dt class="text-muted">
<%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html",
text: gettext("Address used in token mintings and burnings.") %>
<%= gettext("Burn address") %>
</dt>
</td>
<td class="stakes-td">
<%= render BlockScoutWeb.AddressView, "_link.html", address: @address, contract: BlockScoutWeb.AddressView.contract?(@address), use_custom_tooltip: false %>
</td>
<td class="stakes-td"></td>
<td class="stakes-td"></td>
<% else %>
<%= if Map.get(assigns, :miner) do %>
<td class="stakes-td">
<dt class="text-muted">
<%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html",
text: gettext("Address used in token mintings and burnings.") %>
<%= gettext("Burn address") %>
text: gettext("A block producer who successfully included the block onto the blockchain.") %>
<%= gettext("Miner") %>
</dt>
</td>
<td class="stakes-td">
<%= render BlockScoutWeb.AddressView, "_link.html", address: @address, contract: false, use_custom_tooltip: false %>
</td>
<% else %>
<td class="stakes-td"></td>
<td class="stakes-td">
<%= render BlockScoutWeb.AddressView, "_link.html", address: @address, contract: BlockScoutWeb.AddressView.contract?(@address), use_custom_tooltip: false %>
</td>
<% end %>
<%= if not_negative?(@balance_before) and not_negative?(@balance_after) do %>
<td class="stakes-td">
<span><%= display_value(@balance_before, coin_or_transfer) %></span>
</td>
<td class="stakes-td">
<span><%= display_value(@balance_after, coin_or_transfer) %></span>
</td>
<% else %>
<td class="stakes-td"></td>
<td class="stakes-td"></td>
<% else %>
<%= if Map.get(assigns, :miner) do %>
<td class="stakes-td">
<dt class="text-muted">
<%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html",
text: gettext("A block producer who successfully included the block onto the blockchain.") %>
<%= gettext("Miner") %>
</dt>
</td>
<td class="stakes-td">
<%= render BlockScoutWeb.AddressView, "_link.html", address: @address, contract: false, use_custom_tooltip: false %>
</td>
<% else %>
<td class="stakes-td"></td>
<td class="stakes-td">
<%= render BlockScoutWeb.AddressView, "_link.html", address: @address, contract: BlockScoutWeb.AddressView.contract?(@address), use_custom_tooltip: false %>
</td>
<% end %>
<%= if not_negative?(@balance_before) and not_negative?(@balance_after) do %>
<td class="stakes-td">
<span><%= display_value(@balance_before, coin_or_transfer) %></span>
</td>
<td class="stakes-td">
<span><%= display_value(@balance_after, coin_or_transfer) %></span>
</td>
<% else %>
<td class="stakes-td"></td>
<td class="stakes-td"></td>
<% end %>
<% end %>
<td class="stakes-td">
<%= if is_list(@coin_or_token_transfers) and elem(List.first(@coin_or_token_transfers), 1).token.type != "ERC-20" do %>
<%= for {type, transfer} <- @coin_or_token_transfers do %>
<%= case type do %>
<% :from -> %>
<div class="py-1 mr-4 text-danger">▼ <%= display_nft(transfer) %></div>
<% :to -> %>
<div class="py-1 mr-4 text-success">▲ <%= display_nft(transfer) %></div>
<% end %>
<% end %>
<td class="stakes-td">
<%= if is_list(@coin_or_token_transfers) and coin_or_transfer.token.type != "ERC-20" do %>
<%= for {type, transfer} <- @coin_or_token_transfers do %>
<%= case type do %>
<% :from -> %>
<div class="py-1 mr-4 text-danger">▼ <%= display_nft(transfer) %></div>
<% :to -> %>
<div class="py-1 mr-4 text-success">▲ <%= display_nft(transfer) %></div>
<% end %>
<% end %>
<% else %>
<%= if not_negative?(@balance_diff) do %>
<span class="mr-4 text-success">▲ <%= display_value(@balance_diff, coin_or_transfer) %></span>
<% else %>
<%= if not_negative?(@balance_diff) do %>
<span class="mr-4 text-success">▲ <%= display_value(@balance_diff, coin_or_transfer) %></span>
<% else %>
<span class="mr-4 text-danger">▼ <%= display_value(absolute_value_of(@balance_diff), coin_or_transfer) %></span>
<% end %>
<span class="mr-4 text-danger">▼ <%= display_value(absolute_value_of(@balance_diff), coin_or_transfer) %></span>
<% end %>
</td>
</tr>
<% end %>
<% end %>
</td>
</tr>

@ -2,16 +2,21 @@ defmodule BlockScoutWeb.API.V2.SmartContractView do
use BlockScoutWeb, :view
alias ABI.FunctionSelector
alias BlockScoutWeb.API.V2.TransactionView
alias BlockScoutWeb.API.V2.{Helper, TransactionView}
alias BlockScoutWeb.SmartContractView
alias BlockScoutWeb.{ABIEncodedValueView, AddressContractView, AddressView}
alias Ecto.Changeset
alias Explorer.Chain
alias Explorer.{Chain, Market}
alias Explorer.Chain.{Address, SmartContract}
alias Explorer.ExchangeRates.Token
alias Explorer.Visualize.Sol2uml
require Logger
def render("smart_contracts.json", %{smart_contracts: smart_contracts, next_page_params: next_page_params}) do
%{"items" => Enum.map(smart_contracts, &prepare_smart_contract_for_list/1), "next_page_params" => next_page_params}
end
def render("smart_contract.json", %{address: address}) do
prepare_smart_contract(address)
end
@ -237,4 +242,37 @@ defmodule BlockScoutWeb.API.V2.SmartContractView do
nil
end
defp prepare_smart_contract_for_list(%SmartContract{} = smart_contract) do
token =
if smart_contract.address.token,
do: Market.get_exchange_rate(smart_contract.address.token.symbol),
else: Token.null()
%{
"address" => Helper.address_with_info(nil, smart_contract.address, smart_contract.address.hash),
"compiler_version" => smart_contract.compiler_version,
"optimization_enabled" => if(smart_contract.is_vyper_contract, do: nil, else: smart_contract.optimization),
"tx_count" => smart_contract.address.transactions_count,
"language" => smart_contract_language(smart_contract),
"verified_at" => smart_contract.inserted_at,
"market_cap" => token && token.market_cap_usd,
"has_constructor_args" => !is_nil(smart_contract.constructor_arguments),
"coin_balance" =>
if(smart_contract.address.fetched_coin_balance, do: smart_contract.address.fetched_coin_balance.value)
}
end
defp smart_contract_language(smart_contract) do
cond do
smart_contract.is_vyper_contract ->
"vyper"
is_nil(smart_contract.abi) ->
"yul"
true ->
"solidity"
end
end
end

@ -5,11 +5,13 @@ defmodule BlockScoutWeb.API.V2.TransactionView do
alias BlockScoutWeb.{ABIEncodedValueView, TransactionView}
alias BlockScoutWeb.Models.GetTransactionTags
alias BlockScoutWeb.Tokens.Helpers
alias BlockScoutWeb.TransactionStateView
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.Chain.Transaction.StateChange
alias Explorer.Counters.AverageBlockTime
alias Timex.Duration
@ -85,6 +87,10 @@ defmodule BlockScoutWeb.API.V2.TransactionView do
}
end
def render("state_changes.json", %{state_changes: state_changes, conn: conn}) do
Enum.map(state_changes, &prepare_state_change(&1, conn))
end
def prepare_token_transfer(token_transfer, conn) do
decoded_input = token_transfer.transaction |> Transaction.decoded_input_data() |> format_decoded_input()
@ -486,4 +492,53 @@ defmodule BlockScoutWeb.API.V2.TransactionView do
defp block_timestamp(%Transaction{block: %Block{} = block}), do: block.timestamp
defp block_timestamp(%Block{} = block), do: block.timestamp
defp block_timestamp(_), do: nil
defp prepare_state_change(%StateChange{} = state_change, conn) do
coin_or_transfer =
if state_change.coin_or_token_transfers == :coin,
do: :coin,
else: elem(List.first(state_change.coin_or_token_transfers), 1)
type = if coin_or_transfer == :coin, do: "coin", else: "token"
%{
"address" =>
Helper.address_with_info(conn, state_change.address, state_change.address && state_change.address.hash),
"is_miner" => state_change.miner?,
"type" => type,
"token" => if(type == "token", do: TokenView.render("token.json", %{token: coin_or_transfer.token}))
}
|> append_balances(state_change.balance_before, state_change.balance_after)
|> append_balance_change(state_change, coin_or_transfer)
end
defp append_balances(map, balance_before, balance_after) do
balances =
if TransactionStateView.not_negative?(balance_before) and TransactionStateView.not_negative?(balance_after) do
%{
"balance_before" => balance_before,
"balance_after" => balance_after
}
else
%{
"balance_before" => nil,
"balance_after" => nil
}
end
Map.merge(map, balances)
end
defp append_balance_change(map, state_change, coin_or_transfer) do
change =
if is_list(state_change.coin_or_token_transfers) and coin_or_transfer.token.type != "ERC-20" do
for {direction, token_transfer} <- state_change.coin_or_token_transfers do
%{"total" => prepare_token_transfer_total(token_transfer), "direction" => direction}
end
else
state_change.balance_diff
end
Map.merge(map, %{"change" => change})
end
end

@ -4,15 +4,7 @@ defmodule BlockScoutWeb.TransactionStateView do
alias Explorer.Chain
alias Explorer.Chain.Wei
import BlockScoutWeb.TransactionStateController, only: [from_loss: 1, to_profit: 1]
def has_diff?(%Wei{value: val}) do
not Decimal.eq?(val, Decimal.new(0))
end
def has_diff?(val) do
not Decimal.eq?(val, Decimal.new(0))
end
import BlockScoutWeb.Models.TransactionStateHelper, only: [from_loss: 1, has_diff?: 1, to_profit: 1]
def not_negative?(%Wei{value: val}) do
not Decimal.negative?(val)

@ -92,7 +92,7 @@ msgid "64-bit hash of value verifying proof-of-work (note: null for POA chains).
msgstr ""
#: lib/block_scout_web/templates/block/overview.html.eex:97
#: lib/block_scout_web/templates/transaction_state/_state_change.html.eex:22
#: lib/block_scout_web/templates/transaction_state/_state_change.html.eex:21
#, elixir-autogen, elixir-format
msgid "A block producer who successfully included the block onto the blockchain."
msgstr ""
@ -266,7 +266,7 @@ msgstr ""
msgid "Address of the token contract"
msgstr ""
#: lib/block_scout_web/templates/transaction_state/_state_change.html.eex:8
#: lib/block_scout_web/templates/transaction_state/_state_change.html.eex:7
#, elixir-autogen, elixir-format
msgid "Address used in token mintings and burnings."
msgstr ""
@ -502,7 +502,7 @@ msgstr ""
msgid "Blockscout is a tool for inspecting and analyzing EVM based blockchains. Blockchain explorer for Ethereum Networks."
msgstr ""
#: lib/block_scout_web/templates/transaction_state/_state_change.html.eex:9
#: lib/block_scout_web/templates/transaction_state/_state_change.html.eex:8
#, elixir-autogen, elixir-format
msgid "Burn address"
msgstr ""
@ -1685,7 +1685,7 @@ msgstr ""
#: lib/block_scout_web/templates/block/_tile.html.eex:41
#: lib/block_scout_web/templates/block/overview.html.eex:98
#: lib/block_scout_web/templates/chain/_block.html.eex:16
#: lib/block_scout_web/templates/transaction_state/_state_change.html.eex:23
#: lib/block_scout_web/templates/transaction_state/_state_change.html.eex:22
#, elixir-autogen, elixir-format
msgid "Miner"
msgstr ""

@ -92,7 +92,7 @@ msgid "64-bit hash of value verifying proof-of-work (note: null for POA chains).
msgstr ""
#: lib/block_scout_web/templates/block/overview.html.eex:97
#: lib/block_scout_web/templates/transaction_state/_state_change.html.eex:22
#: lib/block_scout_web/templates/transaction_state/_state_change.html.eex:21
#, elixir-autogen, elixir-format
msgid "A block producer who successfully included the block onto the blockchain."
msgstr ""
@ -266,7 +266,7 @@ msgstr ""
msgid "Address of the token contract"
msgstr ""
#: lib/block_scout_web/templates/transaction_state/_state_change.html.eex:8
#: lib/block_scout_web/templates/transaction_state/_state_change.html.eex:7
#, elixir-autogen, elixir-format
msgid "Address used in token mintings and burnings."
msgstr ""
@ -502,7 +502,7 @@ msgstr ""
msgid "Blockscout is a tool for inspecting and analyzing EVM based blockchains. Blockchain explorer for Ethereum Networks."
msgstr ""
#: lib/block_scout_web/templates/transaction_state/_state_change.html.eex:9
#: lib/block_scout_web/templates/transaction_state/_state_change.html.eex:8
#, elixir-autogen, elixir-format
msgid "Burn address"
msgstr ""
@ -1685,7 +1685,7 @@ msgstr ""
#: lib/block_scout_web/templates/block/_tile.html.eex:41
#: lib/block_scout_web/templates/block/overview.html.eex:98
#: lib/block_scout_web/templates/chain/_block.html.eex:16
#: lib/block_scout_web/templates/transaction_state/_state_change.html.eex:23
#: lib/block_scout_web/templates/transaction_state/_state_change.html.eex:22
#, elixir-autogen, elixir-format
msgid "Miner"
msgstr ""

@ -84,12 +84,6 @@ defmodule BlockScoutWeb.AddressReadContractControllerTest do
def get_eip1967_implementation do
EthereumJSONRPC.Mox
|> expect(
:json_rpc,
fn [%{id: id, method: "eth_getCode", params: [_, _]}], _options ->
{:ok, [%{id: id, jsonrpc: "2.0", result: "0x0"}]}
end
)
|> expect(:json_rpc, fn %{
id: 0,
method: "eth_getStorageAt",

@ -82,12 +82,6 @@ defmodule BlockScoutWeb.AddressReadProxyControllerTest do
def get_eip1967_implementation do
EthereumJSONRPC.Mox
|> expect(
:json_rpc,
fn [%{id: id, method: "eth_getCode", params: [_, _]}], _options ->
{:ok, [%{id: id, jsonrpc: "2.0", result: "0x0"}]}
end
)
|> expect(:json_rpc, fn %{
id: 0,
method: "eth_getStorageAt",

@ -86,12 +86,6 @@ defmodule BlockScoutWeb.AddressWriteContractControllerTest do
def get_eip1967_implementation do
EthereumJSONRPC.Mox
|> expect(
:json_rpc,
fn [%{id: id, method: "eth_getCode", params: [_, _]}], _options ->
{:ok, [%{id: id, jsonrpc: "2.0", result: "0x0"}]}
end
)
|> expect(:json_rpc, fn %{
id: 0,
method: "eth_getStorageAt",

@ -84,12 +84,6 @@ defmodule BlockScoutWeb.AddressWriteProxyControllerTest do
def get_eip1967_implementation do
EthereumJSONRPC.Mox
|> expect(
:json_rpc,
fn [%{id: id, method: "eth_getCode", params: [_, _]}], _options ->
{:ok, [%{id: id, jsonrpc: "2.0", result: "0x0"}]}
end
)
|> expect(:json_rpc, fn %{
id: 0,
method: "eth_getStorageAt",

@ -5,7 +5,7 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do
alias BlockScoutWeb.AddressContractView
alias BlockScoutWeb.Models.UserFromAuth
alias Explorer.Chain.Address
alias Explorer.Chain.{Address, SmartContract}
setup :set_mox_from_context
@ -109,7 +109,6 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do
"abi" => target_contract.abi
}
blockchain_get_code_mock()
request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(target_contract.address_hash)}")
response = json_response(request, 200)
@ -199,7 +198,6 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do
"abi" => target_contract.abi
}
blockchain_get_code_mock()
request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(target_contract.address_hash)}")
response = json_response(request, 200)
@ -292,7 +290,6 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do
"abi" => target_contract.abi
}
blockchain_get_code_mock()
request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(address.hash)}")
response = json_response(request, 200)
@ -507,8 +504,6 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do
}
]
blockchain_get_code_mock()
expect(
EthereumJSONRPC.Mox,
:json_rpc,
@ -573,8 +568,6 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do
}
]
blockchain_get_code_mock()
target_contract = insert(:smart_contract, abi: abi)
request =
@ -617,8 +610,6 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do
}
]
blockchain_get_code_mock()
expect(
EthereumJSONRPC.Mox,
:json_rpc,
@ -673,8 +664,6 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do
}
]
blockchain_get_code_mock()
expect(
EthereumJSONRPC.Mox,
:json_rpc,
@ -728,8 +717,6 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do
}
]
blockchain_get_code_mock()
expect(
EthereumJSONRPC.Mox,
:json_rpc,
@ -1072,7 +1059,6 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do
]
target_contract = insert(:smart_contract, abi: abi)
blockchain_get_code_mock()
expect(EthereumJSONRPC.Mox, :json_rpc, fn %{
id: 0,
@ -1185,8 +1171,6 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do
target_contract = insert(:smart_contract, abi: abi)
blockchain_get_code_mock()
expect(EthereumJSONRPC.Mox, :json_rpc, fn %{
id: 0,
method: "eth_getStorageAt",
@ -1272,8 +1256,6 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do
target_contract = insert(:smart_contract, abi: abi)
blockchain_get_code_mock()
expect(EthereumJSONRPC.Mox, :json_rpc, fn %{
id: 0,
method: "eth_getStorageAt",
@ -1343,8 +1325,6 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do
target_contract = insert(:smart_contract, abi: abi)
blockchain_get_code_mock()
expect(EthereumJSONRPC.Mox, :json_rpc, fn %{
id: 0,
method: "eth_getStorageAt",
@ -1413,8 +1393,6 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do
target_contract = insert(:smart_contract, abi: abi)
blockchain_get_code_mock()
expect(EthereumJSONRPC.Mox, :json_rpc, fn %{
id: 0,
method: "eth_getStorageAt",
@ -1506,7 +1484,6 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do
]
target_contract = insert(:smart_contract, abi: abi)
blockchain_get_code_mock()
expect(EthereumJSONRPC.Mox, :json_rpc, fn %{
id: 0,
@ -1538,22 +1515,59 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do
end
end
defp blockchain_get_code_mock do
expect(
EthereumJSONRPC.Mox,
:json_rpc,
fn [%{id: id, method: "eth_getCode", params: [_, _]}], _options ->
{:ok,
[
%{
id: id,
jsonrpc: "2.0",
result:
"0x6080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a7230582061b7676067d537e410bb704932a9984739a959416170ea17bda192ac1218d2790029"
}
]}
end
)
describe "/smart-contracts" do
test "get [] on empty db", %{conn: conn} do
request = get(conn, "/api/v2/smart-contracts")
assert %{"items" => [], "next_page_params" => nil} = json_response(request, 200)
end
test "get correct smart contract", %{conn: conn} do
smart_contract = insert(:smart_contract)
request = get(conn, "/api/v2/smart-contracts")
assert %{"items" => [sc], "next_page_params" => nil} = json_response(request, 200)
compare_item(smart_contract, sc)
end
test "check pagination", %{conn: conn} do
smart_contracts =
for _ <- 0..50 do
insert(:smart_contract)
end
request = get(conn, "/api/v2/smart-contracts")
assert response = json_response(request, 200)
request_2nd_page = get(conn, "/api/v2/smart-contracts", response["next_page_params"])
assert response_2nd_page = json_response(request_2nd_page, 200)
check_paginated_response(response, response_2nd_page, smart_contracts)
end
end
defp compare_item(%SmartContract{} = smart_contract, json) do
assert smart_contract.compiler_version == json["compiler_version"]
assert if(smart_contract.is_vyper_contract, do: nil, else: smart_contract.optimization) ==
json["optimization_enabled"]
assert json["language"] == if(smart_contract.is_vyper_contract, do: "vyper", else: "solidity")
assert json["verified_at"]
assert !is_nil(smart_contract.constructor_arguments) == json["has_constructor_args"]
assert Address.checksum(smart_contract.address_hash) == json["address"]["hash"]
end
defp check_paginated_response(first_page_resp, second_page_resp, list) do
assert Enum.count(first_page_resp["items"]) == 50
assert first_page_resp["next_page_params"] != nil
compare_item(Enum.at(list, 50), Enum.at(first_page_resp["items"], 0))
compare_item(Enum.at(list, 1), Enum.at(first_page_resp["items"], 49))
assert Enum.count(second_page_resp["items"]) == 1
assert second_page_resp["next_page_params"] == nil
compare_item(Enum.at(list, 0), Enum.at(second_page_resp["items"], 0))
end
defp blockchain_eth_call_mock do

@ -1,6 +1,9 @@
defmodule BlockScoutWeb.API.V2.TransactionControllerTest do
use BlockScoutWeb.ConnCase
import EthereumJSONRPC, only: [integer_to_quantity: 1]
import Mox
alias Explorer.Chain.{Address, InternalTransaction, Log, TokenTransfer, Transaction}
setup do
@ -534,6 +537,82 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do
end
end
describe "/transactions/{tx_hash}/state-changes" do
test "return 404 on non existing tx", %{conn: conn} do
tx = build(:transaction)
request = get(conn, "/api/v2/transactions/#{to_string(tx.hash)}/state-changes")
assert %{"message" => "Not found"} = json_response(request, 404)
end
test "return 422 on invalid tx hash", %{conn: conn} do
request = get(conn, "/api/v2/transactions/0x/state-changes")
assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422)
end
test "return existing tx", %{conn: conn} do
EthereumJSONRPC.Mox
|> stub(:json_rpc, fn
[%{id: id, method: "eth_getBalance", params: _}], _options ->
{:ok, [%{id: id, result: integer_to_quantity(123)}]}
[%{id: _id, method: "eth_getBlockByNumber", params: _}], _options ->
{:ok,
[
%{
id: 0,
jsonrpc: "2.0",
result: %{
"author" => "0x0000000000000000000000000000000000000000",
"difficulty" => "0x20000",
"extraData" => "0x",
"gasLimit" => "0x663be0",
"gasUsed" => "0x0",
"hash" => "0x5b28c1bfd3a15230c9a46b399cd0f9a6920d432e85381cc6a140b06e8410112f",
"logsBloom" =>
"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"miner" => "0x0000000000000000000000000000000000000000",
"number" => integer_to_quantity(1),
"parentHash" => "0x0000000000000000000000000000000000000000000000000000000000000000",
"receiptsRoot" => "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
"sealFields" => [
"0x80",
"0xb8410000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
],
"sha3Uncles" => "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
"signature" =>
"0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"size" => "0x215",
"stateRoot" => "0xfad4af258fd11939fae0c6c6eec9d340b1caac0b0196fd9a1bc3f489c5bf00b3",
"step" => "0",
"timestamp" => "0x0",
"totalDifficulty" => "0x20000",
"transactions" => [],
"transactionsRoot" => "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
"uncles" => []
}
}
]}
end)
insert(:block)
insert(:block)
address_a = insert(:address)
address_b = insert(:address)
transaction =
:transaction
|> insert(from_address: address_a, to_address: address_b, value: 1000)
|> with_block(status: :ok)
request = get(conn, "/api/v2/transactions/#{to_string(transaction.hash)}/state-changes")
assert response = json_response(request, 200)
assert Enum.count(response) == 3
end
end
defp compare_item(%Transaction{} = transaction, json) do
assert to_string(transaction.hash) == json["hash"]
assert transaction.block_number == json["block"]

@ -49,7 +49,6 @@ defmodule BlockScoutWeb.SmartContractControllerTest do
insert(:smart_contract, address_hash: token_contract_address.hash, contract_code_md5: "123")
blockchain_get_code_mock()
blockchain_get_function_mock()
path =
@ -87,8 +86,6 @@ defmodule BlockScoutWeb.SmartContractControllerTest do
contract_code_md5: "123"
)
blockchain_get_code_mock()
path =
smart_contract_path(BlockScoutWeb.Endpoint, :index,
hash: token_contract_address.hash,
@ -124,7 +121,6 @@ defmodule BlockScoutWeb.SmartContractControllerTest do
contract_code_md5: "123"
)
blockchain_get_code_mock()
blockchain_get_implementation_mock()
path =
@ -162,7 +158,6 @@ defmodule BlockScoutWeb.SmartContractControllerTest do
contract_code_md5: "123"
)
blockchain_get_code_mock()
blockchain_get_implementation_mock_2()
path =
@ -242,7 +237,6 @@ defmodule BlockScoutWeb.SmartContractControllerTest do
address = insert(:contract_address)
smart_contract = insert(:smart_contract, address_hash: address.hash, contract_code_md5: "123")
blockchain_get_code_mock()
blockchain_get_function_mock()
path =
@ -281,16 +275,6 @@ defmodule BlockScoutWeb.SmartContractControllerTest do
)
end
defp blockchain_get_code_mock do
expect(
EthereumJSONRPC.Mox,
:json_rpc,
fn [%{id: id, method: "eth_getCode", params: [_, _]}], _options ->
{:ok, [%{id: id, jsonrpc: "2.0", result: "0x0"}]}
end
)
end
defp blockchain_get_implementation_mock do
expect(
EthereumJSONRPC.Mox,

@ -31,7 +31,7 @@ defmodule BlockScoutWeb.TransactionStateControllerTest do
test "with duplicated from, to or miner fields", %{conn: conn} do
address = insert(:address)
to_address = insert(:address)
insert(:block)
block = insert(:block, miner: address)
@ -41,12 +41,19 @@ defmodule BlockScoutWeb.TransactionStateControllerTest do
block_number: block.number - 1
)
transaction = insert(:transaction, from_address: address, to_address: address) |> with_block(block, status: :ok)
insert(:fetched_balance,
address_hash: to_address.hash,
value: 1_000_000,
block_number: block.number - 1
)
transaction =
insert(:transaction, from_address: address, to_address: to_address) |> with_block(block, status: :ok)
conn = get(conn, transaction_state_path(conn, :index, transaction), %{type: "JSON"})
{:ok, %{"items" => items}} = conn.resp_body |> Poison.decode()
assert(items |> Enum.filter(fn item -> item != nil end) |> length() == 1)
assert(items |> Enum.filter(fn item -> item != nil end) |> length() == 2)
end
test "returns state changes for the transaction with contract creation", %{conn: conn} do

@ -44,3 +44,5 @@ config :logger, :explorer,
config :explorer, Explorer.ExchangeRates.Source.TransactionAndLog,
secondary_source: Explorer.ExchangeRates.Source.OneCoinSource
config :explorer, Explorer.Chain.Fetcher.CheckBytecodeMatchingOnDemand, enabled: false

@ -0,0 +1,6 @@
defmodule Explorer.Chain.Transaction.StateChange do
@moduledoc """
Struct for storing state changes
"""
defstruct [:coin_or_token_transfers, :address, :balance_before, :balance_after, :balance_diff, :miner?]
end

@ -14,7 +14,7 @@ defmodule Explorer.PagingOptions do
}
@typep key :: any()
@typep page_size :: non_neg_integer()
@typep page_size :: non_neg_integer() | nil
@typep page_number :: pos_integer()
@typep is_pending_tx :: atom()
@typep is_index_in_asc_order :: atom()

@ -104,8 +104,6 @@ defmodule Explorer.Chain.LogTest do
data: data
)
blockchain_get_code_mock()
get_eip1967_implementation()
assert Log.decode(log, transaction) ==
@ -177,16 +175,6 @@ defmodule Explorer.Chain.LogTest do
end
end
defp blockchain_get_code_mock do
expect(
EthereumJSONRPC.Mox,
:json_rpc,
fn [%{id: id, method: "eth_getCode", params: [_, _]}], _options ->
{:ok, [%{id: id, jsonrpc: "2.0", result: "0x0"}]}
end
)
end
def get_eip1967_implementation do
EthereumJSONRPC.Mox
|> expect(:json_rpc, fn %{

Loading…
Cancel
Save