diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex index 21bc5e7cb1..485411b115 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex @@ -460,14 +460,7 @@ defmodule BlockScoutWeb.API.V2.TransactionController do """ @spec state_changes(Plug.Conn.t(), map()) :: Plug.Conn.t() | {atom(), any()} def state_changes(conn, %{"transaction_hash_param" => transaction_hash_string} = params) do - with {:ok, transaction, _transaction_hash} <- - validate_transaction(transaction_hash_string, params, - necessity_by_association: - Map.merge(@transaction_necessity_by_association, %{ - [block: [miner: [:names, :smart_contract, :proxy_implementations]]] => :optional - }), - api?: true - ) do + with {:ok, transaction, _transaction_hash} <- validate_transaction(transaction_hash_string, params, api?: true) do state_changes_plus_next_page = transaction |> TransactionStateHelper.state_changes(params |> paging_options() |> Keyword.merge(api?: true)) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/transaction_state_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/transaction_state_controller.ex index 7ea2d6e74e..f978e7647b 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/transaction_state_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/transaction_state_controller.ex @@ -25,14 +25,7 @@ defmodule BlockScoutWeb.TransactionStateController do 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, transaction} <- - Chain.hash_to_transaction( - transaction_hash, - necessity_by_association: %{ - [block: :miner] => :optional, - from_address: :optional, - to_address: :optional - } - ), + Chain.hash_to_transaction(transaction_hash), {:ok, false} <- AccessHelper.restricted_access?(to_string(transaction.from_address_hash), params), {:ok, false} <- diff --git a/apps/block_scout_web/lib/block_scout_web/models/transaction_state_helper.ex b/apps/block_scout_web/lib/block_scout_web/models/transaction_state_helper.ex index 1502e9b214..8d492ed184 100644 --- a/apps/block_scout_web/lib/block_scout_web/models/transaction_state_helper.ex +++ b/apps/block_scout_web/lib/block_scout_web/models/transaction_state_helper.ex @@ -7,8 +7,8 @@ defmodule BlockScoutWeb.Models.TransactionStateHelper do import Explorer.Chain.SmartContract, only: [burn_address_hash_string: 0] alias Explorer.Chain.Transaction.StateChange - alias Explorer.{Chain, PagingOptions} - alias Explorer.Chain.{Block, BlockNumberHelper, Transaction, Wei} + alias Explorer.{Chain, PagingOptions, Repo} + alias Explorer.Chain.{BlockNumberHelper, Transaction, Wei} alias Explorer.Chain.Cache.StateChanges alias Indexer.Fetcher.OnDemand.CoinBalance, as: CoinBalanceOnDemand alias Indexer.Fetcher.OnDemand.TokenBalance, as: TokenBalanceOnDemand @@ -16,9 +16,15 @@ defmodule BlockScoutWeb.Models.TransactionStateHelper do {:ok, burn_address_hash} = Chain.string_to_address_hash(burn_address_hash_string()) @burn_address_hash burn_address_hash + @doc """ + This function takes transaction, fetches all the transactions before this one from the same block + together with internal transactions and token transfers and calculates native coin and token + balances before and after this transaction. + """ + @spec state_changes(Transaction.t(), [Chain.paging_options() | Chain.api?()]) :: [StateChange.t()] def state_changes(transaction, options \\ []) - def state_changes(%Transaction{block: %Block{}} = transaction, options) do + def state_changes(%Transaction{} = transaction, options) do paging_options = Keyword.get(options, :paging_options, default_paging_options()) {offset} = paging_options.key || {0} @@ -48,53 +54,82 @@ defmodule BlockScoutWeb.Models.TransactionStateHelper do state_changes end - defp do_state_changes(%Transaction{block: %Block{} = block} = transaction, options) 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}, - api?: Keyword.get(options, :api?, false) - ] - - token_transfers = Chain.transaction_to_token_transfers(transaction_hash, full_options) - + defp do_state_changes(%Transaction{} = transaction, options) do block_txs = - Chain.block_to_transactions(block.hash, - necessity_by_association: %{}, + transaction.block_hash + |> Chain.block_to_transactions( paging_options: %PagingOptions{key: nil, page_size: nil}, api?: Keyword.get(options, :api?, false) ) + |> Enum.filter(&(&1.index <= transaction.index)) + |> Repo.preload([:token_transfers, :internal_transactions]) + + transaction = + block_txs + |> Enum.find(&(&1.hash == transaction.hash)) + |> Repo.preload( + token_transfers: [ + from_address: [:names, :smart_contract, :proxy_implementations], + to_address: [:names, :smart_contract, :proxy_implementations] + ], + internal_transactions: [ + from_address: [:names, :smart_contract, :proxy_implementations], + to_address: [:names, :smart_contract, :proxy_implementations] + ], + block: [miner: [:names, :smart_contract, :proxy_implementations]], + from_address: [:names, :smart_contract, :proxy_implementations], + to_address: [:names, :smart_contract, :proxy_implementations] + ) - previous_block_number = BlockNumberHelper.previous_block_number(block.number) + previous_block_number = BlockNumberHelper.previous_block_number(transaction.block_number) - from_before_block = coin_balance(transaction.from_address_hash, previous_block_number, options) - to_before_block = coin_balance(transaction.to_address_hash, previous_block_number, options) - miner_before_block = coin_balance(block.miner_hash, previous_block_number, options) + coin_balances_before_block = transaction_to_coin_balances(transaction, previous_block_number, options) - {from_before_tx, to_before_tx, miner_before_tx} = - StateChange.coin_balances_before(transaction, block_txs, from_before_block, to_before_block, miner_before_block) + coin_balances_before_tx = StateChange.coin_balances_before(transaction, block_txs, coin_balances_before_block) - native_coin_entries = StateChange.native_coin_entries(transaction, from_before_tx, to_before_tx, miner_before_tx) + native_coin_entries = StateChange.native_coin_entries(transaction, coin_balances_before_tx) token_balances_before = - token_transfers - |> Enum.reduce(%{}, &token_transfers_to_balances_reducer(&1, &2, options)) + transaction.token_transfers + |> Enum.reduce(%{}, &token_transfers_to_balances_reducer(&1, &2, previous_block_number, options)) |> StateChange.token_balances_before(transaction, block_txs) - tokens_entries = StateChange.token_entries(token_transfers, token_balances_before) + tokens_entries = StateChange.token_entries(transaction.token_transfers, token_balances_before) native_coin_entries ++ tokens_entries end + defp transaction_to_coin_balances(transaction, previous_block_number, options) do + Enum.reduce( + transaction.internal_transactions, + %{ + transaction.from_address_hash => + {transaction.from_address, coin_balance(transaction.from_address_hash, previous_block_number, options)}, + transaction.to_address_hash => + {transaction.to_address, coin_balance(transaction.to_address_hash, previous_block_number, options)}, + transaction.block.miner_hash => + {transaction.block.miner, coin_balance(transaction.block.miner_hash, previous_block_number, options)} + }, + &internal_transaction_to_coin_balances(&1, previous_block_number, options, &2) + ) + end + + defp internal_transaction_to_coin_balances(internal_transaction, previous_block_number, options, acc) do + if internal_transaction.value |> Wei.to(:wei) |> Decimal.positive?() do + acc + |> Map.put_new_lazy(internal_transaction.from_address_hash, fn -> + {internal_transaction.from_address, + coin_balance(internal_transaction.from_address_hash, previous_block_number, options)} + end) + |> Map.put_new_lazy(internal_transaction.to_address_hash, fn -> + {internal_transaction.to_address, + coin_balance(internal_transaction.to_address_hash, previous_block_number, options)} + end) + else + acc + end + end + defp coin_balance(address_hash, _block_number, _options) when is_nil(address_hash) do %Wei{value: Decimal.new(0)} end @@ -145,11 +180,10 @@ defmodule BlockScoutWeb.Models.TransactionStateHelper do end end - defp token_transfers_to_balances_reducer(transfer, balances, options) do + defp token_transfers_to_balances_reducer(transfer, balances, prev_block, options) do from = transfer.from_address to = transfer.to_address token_hash = transfer.token_contract_address_hash - prev_block = BlockNumberHelper.previous_block_number(transfer.block_number) balances |> case do diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs index 7b3a9652bb..f28b729e29 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs @@ -5,7 +5,7 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do alias BlockScoutWeb.Models.UserFromAuth alias Explorer.Account.WatchlistAddress - alias Explorer.Chain.{Address, InternalTransaction, Log, Token, TokenTransfer, Transaction} + alias Explorer.Chain.{Address, InternalTransaction, Log, Token, TokenTransfer, Transaction, Wei} alias Explorer.Repo setup do @@ -965,6 +965,142 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do assert response = json_response(request, 200) assert Enum.count(response["items"]) == 3 end + + test "does not include internal transaction with index 0", %{conn: conn} do + block_before = insert(:block) + + transaction = + :transaction + |> insert() + |> with_block(status: :ok) + + internal_transaction_from = insert(:address) + internal_transaction_to = insert(:address) + + insert(:internal_transaction, + transaction: transaction, + index: 0, + block_number: transaction.block_number, + transaction_index: transaction.index, + block_hash: transaction.block_hash, + block_index: 0, + value: %Wei{value: Decimal.new(7)}, + from_address_hash: internal_transaction_from.hash, + from_address: internal_transaction_from, + to_address_hash: internal_transaction_to.hash, + to_address: internal_transaction_to + ) + + insert(:address_coin_balance, + address: transaction.from_address, + address_hash: transaction.from_address_hash, + block_number: block_before.number + ) + + insert(:address_coin_balance, + address: transaction.to_address, + address_hash: transaction.to_address_hash, + block_number: block_before.number + ) + + insert(:address_coin_balance, + address: transaction.block.miner, + address_hash: transaction.block.miner_hash, + block_number: block_before.number + ) + + insert(:address_coin_balance, + address: internal_transaction_from, + address_hash: internal_transaction_from.hash, + block_number: block_before.number + ) + + insert(:address_coin_balance, + address: internal_transaction_to, + address_hash: internal_transaction_to.hash, + block_number: block_before.number + ) + + request = get(conn, "/api/v2/transactions/#{to_string(transaction.hash)}/state-changes") + + assert response = json_response(request, 200) + assert Enum.count(response["items"]) == 3 + end + + test "return entries from internal transaction", %{conn: conn} do + block_before = insert(:block) + + transaction = + :transaction + |> insert() + |> with_block(status: :ok) + + internal_transaction_from = insert(:address) + internal_transaction_to = insert(:address) + + insert(:internal_transaction, + transaction: transaction, + index: 0, + block_number: transaction.block_number, + transaction_index: transaction.index, + block_hash: transaction.block_hash, + block_index: 0, + value: %Wei{value: Decimal.new(7)}, + from_address_hash: internal_transaction_from.hash, + from_address: internal_transaction_from, + to_address_hash: internal_transaction_to.hash, + to_address: internal_transaction_to + ) + + insert(:internal_transaction, + transaction: transaction, + index: 1, + block_number: transaction.block_number, + transaction_index: transaction.index, + block_hash: transaction.block_hash, + block_index: 1, + value: %Wei{value: Decimal.new(7)}, + from_address_hash: internal_transaction_from.hash, + from_address: internal_transaction_from, + to_address_hash: internal_transaction_to.hash, + to_address: internal_transaction_to + ) + + insert(:address_coin_balance, + address: transaction.from_address, + address_hash: transaction.from_address_hash, + block_number: block_before.number + ) + + insert(:address_coin_balance, + address: transaction.to_address, + address_hash: transaction.to_address_hash, + block_number: block_before.number + ) + + insert(:address_coin_balance, + address: transaction.block.miner, + address_hash: transaction.block.miner_hash, + block_number: block_before.number + ) + + insert(:address_coin_balance, + address: internal_transaction_from, + address_hash: internal_transaction_from.hash, + block_number: block_before.number + ) + + insert(:address_coin_balance, + address: internal_transaction_to, + address_hash: internal_transaction_to.hash, + block_number: block_before.number + ) + + request = get(conn, "/api/v2/transactions/#{to_string(transaction.hash)}/state-changes") + + assert response = json_response(request, 200) + assert Enum.count(response["items"]) == 5 + end end if Application.compile_env(:explorer, :chain_type) == :stability do diff --git a/apps/explorer/lib/explorer/chain/transaction/state_change.ex b/apps/explorer/lib/explorer/chain/transaction/state_change.ex index 3d5f0f7f8b..9bdf9222b6 100644 --- a/apps/explorer/lib/explorer/chain/transaction/state_change.ex +++ b/apps/explorer/lib/explorer/chain/transaction/state_change.ex @@ -4,14 +4,14 @@ defmodule Explorer.Chain.Transaction.StateChange do """ alias Explorer.Chain - alias Explorer.Chain.{Hash, TokenTransfer, Transaction, Wei} + alias Explorer.Chain.{Address, Block, Hash, InternalTransaction, TokenTransfer, Transaction, Wei} alias Explorer.Chain.Transaction.StateChange defstruct [:coin_or_token_transfers, :address, :token_id, :balance_before, :balance_after, :balance_diff, :miner?] @type t :: %__MODULE__{ coin_or_token_transfers: :coin | [TokenTransfer.t()], - address: Hash.Address.t(), + address: Address.t(), token_id: nil | non_neg_integer(), balance_before: Wei.t() | Decimal.t(), balance_after: Wei.t() | Decimal.t(), @@ -19,35 +19,49 @@ defmodule Explorer.Chain.Transaction.StateChange do miner?: boolean() } - def coin_balances_before(tx, block_txs, from_before, to_before, miner_before) do + @type coin_balances_map :: %{Hash.Address.t() => {Address.t(), Wei.t()}} + + @zero_wei %Wei{value: Decimal.new(0)} + + @spec coin_balances_before(Transaction.t(), [Transaction.t()], coin_balances_map()) :: coin_balances_map() + def coin_balances_before(tx, block_txs, coin_balances_before_block) do block = tx.block block_txs |> Enum.reduce_while( - {from_before, to_before, miner_before}, - fn block_tx, {block_from, block_to, block_miner} = state -> + coin_balances_before_block, + fn block_tx, acc -> if block_tx.index < tx.index do - {:cont, - {update_coin_balance_from_tx(tx.from_address_hash, block_tx, block_from, block), - update_coin_balance_from_tx(tx.to_address_hash, block_tx, block_to, block), - update_coin_balance_from_tx(tx.block.miner_hash, block_tx, block_miner, block)}} + {:cont, update_coin_balances_from_tx(acc, block_tx, block)} else # txs ordered by index ascending, so we can halt after facing index greater or equal than index of our tx - {:halt, state} + {:halt, acc} end end ) end - def 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 + @spec update_coin_balances_from_tx(coin_balances_map(), Transaction.t(), Block.t()) :: coin_balances_map() + def update_coin_balances_from_tx(coin_balances, tx, block) do + coin_balances = + coin_balances + |> update_balance(tx.from_address_hash, &Wei.sub(&1, from_loss(tx))) + |> update_balance(tx.to_address_hash, &Wei.sum(&1, to_profit(tx))) + |> update_balance(block.miner_hash, &Wei.sum(&1, miner_profit(tx, block))) + + if error?(tx) do + coin_balances + else + tx.internal_transactions |> Enum.reduce(coin_balances, &update_coin_balances_from_internal_tx(&1, &2)) + end + end + + defp update_coin_balances_from_internal_tx(%InternalTransaction{index: 0}, coin_balances), do: coin_balances - 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)).() + defp update_coin_balances_from_internal_tx(internal_tx, coin_balances) do + coin_balances + |> update_balance(internal_tx.from_address_hash, &Wei.sub(&1, from_loss(internal_tx))) + |> update_balance(internal_tx.to_address_hash, &Wei.sum(&1, to_profit(internal_tx))) end def token_balances_before(balances_before, tx, block_txs) do @@ -65,11 +79,11 @@ defmodule Explorer.Chain.Transaction.StateChange do ) end - def do_update_token_balances_from_token_transfers( - token_transfers, - balances_map, - include_transfers \\ :no - ) do + defp do_update_token_balances_from_token_transfers( + token_transfers, + balances_map, + include_transfers \\ :no + ) do Enum.reduce( token_transfers, balances_map, @@ -139,7 +153,12 @@ defmodule Explorer.Chain.Transaction.StateChange do end) end - def from_loss(tx) do + @doc """ + Returns the balance change of from address of a transaction + or an internal transaction. + """ + @spec from_loss(Transaction.t() | InternalTransaction.t()) :: Wei.t() + def from_loss(%Transaction{} = tx) do {_, fee} = Transaction.fee(tx, :wei) if error?(tx) do @@ -149,7 +168,16 @@ defmodule Explorer.Chain.Transaction.StateChange do end end - def to_profit(tx) do + def from_loss(%InternalTransaction{} = tx) do + tx.value + end + + @doc """ + Returns the balance change of to address of a transaction + or an internal transaction. + """ + @spec to_profit(Transaction.t() | InternalTransaction.t()) :: Wei.t() + def to_profit(%Transaction{} = tx) do if error?(tx) do %Wei{value: 0} else @@ -157,6 +185,10 @@ defmodule Explorer.Chain.Transaction.StateChange do end end + def to_profit(%InternalTransaction{} = tx) do + tx.value + 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 @@ -196,35 +228,30 @@ defmodule Explorer.Chain.Transaction.StateChange do } end - def native_coin_entries(transaction, from_before_tx, to_before_tx, miner_before_tx) do + @doc """ + Returns the list of native coin state changes of a transaction, including state changes from the internal transactions, + taking into account state changes from previous transactions in the same block. + """ + @spec native_coin_entries(Transaction.t(), coin_balances_map()) :: [t()] + def native_coin_entries(transaction, coin_balances_before_tx) do block = transaction.block - from_hash = transaction.from_address_hash - to_hash = transaction.to_address_hash - miner_hash = block.miner_hash + coin_balances_after_tx = update_coin_balances_from_tx(coin_balances_before_tx, transaction, block) - from_coin_entry = - if from_hash not in [to_hash, miner_hash] do - from = transaction.from_address - from_after_tx = update_coin_balance_from_tx(from_hash, transaction, from_before_tx, block) - coin_entry(from, from_before_tx, from_after_tx) - end + coin_balances_before_tx + |> Enum.reduce([], fn {address_hash, {address, coin_balance_before}}, acc -> + {_, coin_balance_after} = coin_balances_after_tx[address_hash] + coin_entry = coin_entry(address, coin_balance_before, coin_balance_after, address_hash == block.miner_hash) - to_coin_entry = - if not is_nil(to_hash) and to_hash != miner_hash do - to = transaction.to_address - to_after = update_coin_balance_from_tx(to_hash, transaction, to_before_tx, block) - coin_entry(to, to_before_tx, to_after) + if coin_entry do + [coin_entry | acc] + else + acc end - - miner = block.miner - miner_after = update_coin_balance_from_tx(miner_hash, transaction, miner_before_tx, block) - miner_entry = coin_entry(miner, miner_before_tx, miner_after, true) - - [from_coin_entry, to_coin_entry, miner_entry] |> Enum.reject(&is_nil/1) + end) end - defp coin_entry(address, balance_before, balance_after, miner? \\ false) do + defp coin_entry(address, balance_before, balance_after, miner?) do diff = Wei.sub(balance_after, balance_before) if has_diff?(diff) do @@ -240,6 +267,19 @@ defmodule Explorer.Chain.Transaction.StateChange do end end + defp update_balance(coin_balances, address_hash, _update_function) when is_nil(address_hash), + do: coin_balances + + defp update_balance(coin_balances, address_hash, update_function) do + if Map.has_key?(coin_balances, address_hash) do + Map.update(coin_balances, address_hash, @zero_wei, fn {address, balance} -> + {address, update_function.(balance)} + end) + else + coin_balances + end + end + def token_entries(token_transfers, token_balances_before) do token_balances_after = do_update_token_balances_from_token_transfers(