fix: include internal transactions in state change (#10210)

* fix: include internal transactions in state change

* Process review comments

* Do not take into account first trace

* Process review comment
pull/10462/head
Maxim Filonov 4 months ago committed by GitHub
parent cfd3da5403
commit 23adf21200
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 9
      apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex
  2. 9
      apps/block_scout_web/lib/block_scout_web/controllers/transaction_state_controller.ex
  3. 106
      apps/block_scout_web/lib/block_scout_web/models/transaction_state_helper.ex
  4. 138
      apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs
  5. 134
      apps/explorer/lib/explorer/chain/transaction/state_change.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))

@ -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} <-

@ -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

@ -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

@ -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(

Loading…
Cancel
Save