diff --git a/apps/explorer/test/explorer/chain/import/runner/blocks_test.exs b/apps/explorer/test/explorer/chain/import/runner/blocks_test.exs index cba86e999a..b04d59895d 100644 --- a/apps/explorer/test/explorer/chain/import/runner/blocks_test.exs +++ b/apps/explorer/test/explorer/chain/import/runner/blocks_test.exs @@ -5,34 +5,34 @@ defmodule Explorer.Chain.Import.Runner.BlocksTest do alias Ecto.Multi alias Explorer.Chain.Import.Runner.{Blocks, Transaction} - alias Explorer.Chain.{Block, Transaction} + alias Explorer.Chain.{Address, Block, Transaction} alias Explorer.Repo describe "run/1" do setup do block = insert(:block, consensus: true) - transaction = - :transaction - |> insert() - |> with_block(block) + timestamp = DateTime.utc_now() + options = %{timestamps: %{inserted_at: timestamp, updated_at: timestamp}} - %{consensus_block: block, transaction: transaction} + %{consensus_block: block, options: options} end test "derive_transaction_forks replaces hash on conflicting (uncle_hash, index)", %{ - consensus_block: %Block{hash: block_hash, miner_hash: miner_hash, number: block_number}, - transaction: transaction + consensus_block: %Block{hash: block_hash, miner_hash: miner_hash, number: block_number} = consensus_block, + options: options } do + transaction = + :transaction + |> insert() + |> with_block(consensus_block) + block_params = params_for(:block, hash: block_hash, miner_hash: miner_hash, number: block_number, consensus: false) %Ecto.Changeset{valid?: true, changes: block_changes} = Block.changeset(%Block{}, block_params) changes_list = [block_changes] - timestamp = DateTime.utc_now() - options = %{timestamps: %{inserted_at: timestamp, updated_at: timestamp}} - assert Repo.aggregate(from(transaction in Transaction, where: is_nil(transaction.block_number)), :count, :hash) == 0 @@ -76,6 +76,139 @@ defmodule Explorer.Chain.Import.Runner.BlocksTest do assert Repo.one!(from(transaction_fork in Transaction.Fork, select: "ctid")) == ctid, "Tuple was written even though it is not distinct" end + + test "delete_address_current_token_balances deletes rows with matching block number when consensus is true", + %{consensus_block: %Block{hash: block_hash, miner_hash: miner_hash, number: block_number}, options: options} do + %Address.CurrentTokenBalance{address_hash: address_hash, token_contract_address_hash: token_contract_address_hash} = + insert(:address_current_token_balance, block_number: block_number) + + block_params = params_for(:block, hash: block_hash, miner_hash: miner_hash, number: block_number, consensus: true) + + %Ecto.Changeset{valid?: true, changes: block_changes} = Block.changeset(%Block{}, block_params) + changes_list = [block_changes] + + assert count(Address.CurrentTokenBalance) == 1 + + assert {:ok, + %{ + delete_address_current_token_balances: [ + %{address_hash: ^address_hash, token_contract_address_hash: ^token_contract_address_hash} + ] + }} = + Multi.new() + |> Blocks.run(changes_list, options) + |> Repo.transaction() + + assert count(Address.CurrentTokenBalance) == 0 + end + + test "delete_address_current_token_balances does not delete rows with matching block number when consensus is false", + %{consensus_block: %Block{hash: block_hash, miner_hash: miner_hash, number: block_number}, options: options} do + %Address.CurrentTokenBalance{} = insert(:address_current_token_balance, block_number: block_number) + + block_params = + params_for(:block, hash: block_hash, miner_hash: miner_hash, number: block_number, consensus: false) + + %Ecto.Changeset{valid?: true, changes: block_changes} = Block.changeset(%Block{}, block_params) + changes_list = [block_changes] + + count = 1 + + assert count(Address.CurrentTokenBalance) == count + + assert {:ok, + %{ + delete_address_current_token_balances: [] + }} = + Multi.new() + |> Blocks.run(changes_list, options) + |> Repo.transaction() + + assert count(Address.CurrentTokenBalance) == count + end + + test "derive_address_current_token_balances inserts rows if there is an address_token_balance left for the rows deleted by delete_address_current_token_balances", + %{consensus_block: %Block{hash: block_hash, miner_hash: miner_hash, number: block_number}, options: options} do + %Address.TokenBalance{ + address_hash: address_hash, + token_contract_address_hash: token_contract_address_hash, + value: previous_value, + block_number: previous_block_number + } = insert(:token_balance, block_number: block_number - 1) + + address = Repo.get(Address, address_hash) + + %Address.TokenBalance{ + address_hash: ^address_hash, + token_contract_address_hash: ^token_contract_address_hash, + value: current_value, + block_number: ^block_number + } = + insert(:token_balance, + address: address, + token_contract_address_hash: token_contract_address_hash, + block_number: block_number + ) + + refute current_value == previous_value + + %Address.CurrentTokenBalance{ + address_hash: ^address_hash, + token_contract_address_hash: ^token_contract_address_hash, + block_number: ^block_number, + value: ^current_value + } = + insert(:address_current_token_balance, + address: address, + token_contract_address_hash: token_contract_address_hash, + block_number: block_number, + value: current_value + ) + + block_params = params_for(:block, hash: block_hash, miner_hash: miner_hash, number: block_number, consensus: true) + + %Ecto.Changeset{valid?: true, changes: block_changes} = Block.changeset(%Block{}, block_params) + changes_list = [block_changes] + + assert count(Address.TokenBalance) == 2 + assert count(Address.CurrentTokenBalance) == 1 + + assert {:ok, + %{ + delete_address_current_token_balances: [ + %{ + address_hash: ^address_hash, + token_contract_address_hash: ^token_contract_address_hash + } + ], + delete_address_token_balances: [ + %{ + address_hash: ^address_hash, + token_contract_address_hash: ^token_contract_address_hash, + block_number: ^block_number + } + ], + derive_address_current_token_balances: [ + %{ + address_hash: ^address_hash, + token_contract_address_hash: ^token_contract_address_hash, + block_number: ^previous_block_number + } + ] + }} = + Multi.new() + |> Blocks.run(changes_list, options) + |> Repo.transaction() + + assert count(Address.TokenBalance) == 1 + assert count(Address.CurrentTokenBalance) == 1 + + assert %Address.CurrentTokenBalance{block_number: ^previous_block_number, value: ^previous_value} = + Repo.get_by(Address.CurrentTokenBalance, + address_hash: address_hash, + token_contract_address_hash: token_contract_address_hash + ) + end end defp count(schema) do diff --git a/apps/explorer/test/explorer/chain/import_test.exs b/apps/explorer/test/explorer/chain/import_test.exs index eebee1a76a..93a981648a 100644 --- a/apps/explorer/test/explorer/chain/import_test.exs +++ b/apps/explorer/test/explorer/chain/import_test.exs @@ -1828,5 +1828,256 @@ defmodule Explorer.Chain.ImportTest do assert transaction_after.error == nil assert transaction_after.status == nil end + + test "address_token_balances and address_current_token_balances are deleted during reorgs" do + %Block{number: block_number} = insert(:block, consensus: true) + value_before = Decimal.new(1) + + %Address{hash: address_hash} = address = insert(:address) + + %Address.TokenBalance{ + address_hash: ^address_hash, + token_contract_address_hash: token_contract_address_hash, + block_number: ^block_number + } = insert(:token_balance, address: address, block_number: block_number, value: value_before) + + %Address.CurrentTokenBalance{ + address_hash: ^address_hash, + token_contract_address_hash: ^token_contract_address_hash, + block_number: ^block_number + } = + insert(:address_current_token_balance, + address: address, + token_contract_address_hash: token_contract_address_hash, + block_number: block_number, + value: value_before + ) + + miner_hash_after = address_hash() + from_address_hash_after = address_hash() + block_hash_after = block_hash() + + assert {:ok, _} = + Import.all(%{ + addresses: %{ + params: [ + %{hash: miner_hash_after}, + %{hash: from_address_hash_after} + ] + }, + blocks: %{ + params: [ + %{ + consensus: true, + difficulty: 1, + gas_limit: 1, + gas_used: 1, + hash: block_hash_after, + miner_hash: miner_hash_after, + nonce: 1, + number: block_number, + parent_hash: block_hash(), + size: 1, + timestamp: Timex.parse!("2019-01-01T02:00:00Z", "{ISO:Extended:Z}"), + total_difficulty: 1 + } + ] + } + }) + + assert is_nil( + Repo.get_by(Address.CurrentTokenBalance, + address_hash: address_hash, + token_contract_address_hash: token_contract_address_hash + ) + ) + + assert is_nil( + Repo.get_by(Address.TokenBalance, + address_hash: address_hash, + token_contract_address_hash: token_contract_address_hash, + block_number: block_number + ) + ) + end + + test "address_current_token_balances is derived during reorgs" do + %Block{number: block_number} = insert(:block, consensus: true) + previous_block_number = block_number - 1 + + %Address.TokenBalance{ + address_hash: address_hash, + token_contract_address_hash: token_contract_address_hash, + value: previous_value, + block_number: previous_block_number + } = insert(:token_balance, block_number: previous_block_number) + + address = Repo.get(Address, address_hash) + + %Address.TokenBalance{ + address_hash: ^address_hash, + token_contract_address_hash: token_contract_address_hash, + value: current_value, + block_number: ^block_number + } = + insert(:token_balance, + address: address, + token_contract_address_hash: token_contract_address_hash, + block_number: block_number + ) + + refute current_value == previous_value + + %Address.CurrentTokenBalance{ + address_hash: ^address_hash, + token_contract_address_hash: ^token_contract_address_hash, + block_number: ^block_number + } = + insert(:address_current_token_balance, + address: address, + token_contract_address_hash: token_contract_address_hash, + block_number: block_number, + value: current_value + ) + + miner_hash_after = address_hash() + from_address_hash_after = address_hash() + block_hash_after = block_hash() + + assert {:ok, _} = + Import.all(%{ + addresses: %{ + params: [ + %{hash: miner_hash_after}, + %{hash: from_address_hash_after} + ] + }, + blocks: %{ + params: [ + %{ + consensus: true, + difficulty: 1, + gas_limit: 1, + gas_used: 1, + hash: block_hash_after, + miner_hash: miner_hash_after, + nonce: 1, + number: block_number, + parent_hash: block_hash(), + size: 1, + timestamp: Timex.parse!("2019-01-01T02:00:00Z", "{ISO:Extended:Z}"), + total_difficulty: 1 + } + ] + } + }) + + assert %Address.CurrentTokenBalance{block_number: ^previous_block_number, value: ^previous_value} = + Repo.get_by(Address.CurrentTokenBalance, + address_hash: address_hash, + token_contract_address_hash: token_contract_address_hash + ) + + assert is_nil( + Repo.get_by(Address.TokenBalance, + address_hash: address_hash, + token_contract_address_hash: token_contract_address_hash, + block_number: block_number + ) + ) + end + + test "address_token_balances and address_current_token_balances can be replaced during reorgs" do + %Block{number: block_number} = insert(:block, consensus: true) + value_before = Decimal.new(1) + + %Address{hash: address_hash} = address = insert(:address) + + %Address.TokenBalance{ + address_hash: ^address_hash, + token_contract_address_hash: token_contract_address_hash, + block_number: ^block_number + } = insert(:token_balance, address: address, block_number: block_number, value: value_before) + + %Address.CurrentTokenBalance{ + address_hash: ^address_hash, + token_contract_address_hash: ^token_contract_address_hash, + block_number: ^block_number + } = + insert(:address_current_token_balance, + address: address, + token_contract_address_hash: token_contract_address_hash, + block_number: block_number, + value: value_before + ) + + miner_hash_after = address_hash() + from_address_hash_after = address_hash() + block_hash_after = block_hash() + value_after = Decimal.add(value_before, 1) + + assert {:ok, _} = + Import.all(%{ + addresses: %{ + params: [ + %{hash: address_hash}, + %{hash: token_contract_address_hash}, + %{hash: miner_hash_after}, + %{hash: from_address_hash_after} + ] + }, + address_token_balances: %{ + params: [ + %{ + address_hash: address_hash, + token_contract_address_hash: token_contract_address_hash, + block_number: block_number, + value: value_after + } + ] + }, + address_current_token_balances: %{ + params: [ + %{ + address_hash: address_hash, + token_contract_address_hash: token_contract_address_hash, + block_number: block_number, + value: value_after + } + ] + }, + blocks: %{ + params: [ + %{ + consensus: true, + difficulty: 1, + gas_limit: 1, + gas_used: 1, + hash: block_hash_after, + miner_hash: miner_hash_after, + nonce: 1, + number: block_number, + parent_hash: block_hash(), + size: 1, + timestamp: Timex.parse!("2019-01-01T02:00:00Z", "{ISO:Extended:Z}"), + total_difficulty: 1 + } + ] + } + }) + + assert %Address.CurrentTokenBalance{value: ^value_after} = + Repo.get_by(Address.CurrentTokenBalance, + address_hash: address_hash, + token_contract_address_hash: token_contract_address_hash + ) + + assert %Address.TokenBalance{value: ^value_after} = + Repo.get_by(Address.TokenBalance, + address_hash: address_hash, + token_contract_address_hash: token_contract_address_hash, + block_number: block_number + ) + end end end