Merge pull request #985 from poanetwork/978

Composite primary keys and eliminating use of on_conflict: replace_all
pull/987/head
Luke Imhoff 6 years ago committed by GitHub
commit b26745c993
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      apps/block_scout_web/lib/block_scout_web/templates/internal_transaction/_tile.html.eex
  2. 121
      apps/block_scout_web/test/block_scout_web/channels/address_channel_test.exs
  3. 16
      apps/block_scout_web/test/block_scout_web/controllers/address_internal_transaction_controller_test.exs
  4. 7
      apps/block_scout_web/test/block_scout_web/controllers/transaction_internal_transaction_controller_test.exs
  5. 14
      apps/block_scout_web/test/block_scout_web/controllers/transaction_token_transfer_controller_test.exs
  6. 28
      apps/block_scout_web/test/block_scout_web/features/pages/address_page.ex
  7. 24
      apps/block_scout_web/test/block_scout_web/features/viewing_app_test.exs
  8. 1
      apps/block_scout_web/test/block_scout_web/features/viewing_transactions_test.exs
  9. 14
      apps/explorer/lib/explorer/chain.ex
  10. 13
      apps/explorer/lib/explorer/chain/import/address/coin_balances.ex
  11. 13
      apps/explorer/lib/explorer/chain/import/address/token_balances.ex
  12. 13
      apps/explorer/lib/explorer/chain/import/addresses.ex
  13. 10
      apps/explorer/lib/explorer/chain/import/block/second_degree_relations.ex
  14. 48
      apps/explorer/lib/explorer/chain/import/blocks.ex
  15. 57
      apps/explorer/lib/explorer/chain/import/internal_transactions.ex
  16. 43
      apps/explorer/lib/explorer/chain/import/logs.ex
  17. 35
      apps/explorer/lib/explorer/chain/import/token_transfers.ex
  18. 42
      apps/explorer/lib/explorer/chain/import/tokens.ex
  19. 13
      apps/explorer/lib/explorer/chain/import/transaction/forks.ex
  20. 52
      apps/explorer/lib/explorer/chain/import/transactions.ex
  21. 18
      apps/explorer/lib/explorer/chain/internal_transaction.ex
  22. 25
      apps/explorer/lib/explorer/chain/log.ex
  23. 12
      apps/explorer/lib/explorer/chain/token_transfer.ex
  24. 22
      apps/explorer/priv/repo/migrations/20181024141113_internal_transactions_composite_primary_key.exs
  25. 22
      apps/explorer/priv/repo/migrations/20181024164623_logs_composite_primary_key.exs
  26. 22
      apps/explorer/priv/repo/migrations/20181024172010_token_transfers_composite_primary_key.exs
  27. 27
      apps/explorer/test/explorer/chain/import_test.exs
  28. 15
      apps/explorer/test/explorer/chain/token_transfer_test.exs
  29. 169
      apps/explorer/test/explorer/chain_test.exs
  30. 2
      apps/indexer/lib/indexer/block/fetcher.ex
  31. 2
      apps/indexer/test/indexer/block/fetcher_test.exs

@ -1,4 +1,4 @@
<div class="tile tile-type-internal-transaction fade-in" data-test="internal_transaction" data-internal-transaction-id="<%= @internal_transaction.id %>">
<div class="tile tile-type-internal-transaction fade-in" data-test="internal_transaction" data-internal-transaction-transaction-hash="<%= @internal_transaction.transaction_hash %>" data-internal-transaction-index="<%= @internal_transaction.index %>">
<div class="row">
<div class="col-md-2 d-flex flex-row flex-md-column align-items-left justify-content-start justify-content-lg-center mb-1 mb-md-0 pl-md-4">
<%= gettext("Internal Transaction") %>

@ -11,13 +11,7 @@ defmodule BlockScoutWeb.AddressChannelTest do
Notifier.handle_event({:chain_event, :addresses, :realtime, [address]})
receive do
%Phoenix.Socket.Broadcast{topic: ^topic, event: "count", payload: %{count: _}} ->
assert true
after
5_000 ->
assert false, "Expected message received nothing."
end
assert_receive %Phoenix.Socket.Broadcast{topic: ^topic, event: "count", payload: %{count: _}}, 5_000
end
describe "user subscribed to address" do
@ -32,23 +26,14 @@ defmodule BlockScoutWeb.AddressChannelTest do
address_with_balance = %{address | fetched_coin_balance: 1}
Notifier.handle_event({:chain_event, :addresses, :realtime, [address_with_balance]})
receive do
%Phoenix.Socket.Broadcast{topic: ^topic, event: "balance_update", payload: payload} ->
assert_receive %Phoenix.Socket.Broadcast{topic: ^topic, event: "balance_update", payload: payload}, 5_000
assert payload.address.hash == address_with_balance.hash
after
5_000 ->
assert false, "Expected message received nothing."
end
end
test "not notified of balance_update if fetched_coin_balance is nil", %{address: address} do
Notifier.handle_event({:chain_event, :addresses, :realtime, [address]})
receive do
_ -> assert false, "Message was broadcast for nil fetched_coin_balance."
after
100 -> assert true
end
refute_receive _, 100, "Message was broadcast for nil fetched_coin_balance."
end
test "notified of new_pending_transaction for matching from_address", %{address: address, topic: topic} do
@ -56,14 +41,9 @@ defmodule BlockScoutWeb.AddressChannelTest do
Notifier.handle_event({:chain_event, :transactions, :realtime, [pending.hash]})
receive do
%Phoenix.Socket.Broadcast{topic: ^topic, event: "pending_transaction", payload: payload} ->
assert_receive %Phoenix.Socket.Broadcast{topic: ^topic, event: "pending_transaction", payload: payload}, 5_000
assert payload.address.hash == address.hash
assert payload.transaction.hash == pending.hash
after
5_000 ->
assert false, "Expected message received nothing."
end
end
test "notified of new_transaction for matching from_address", %{address: address, topic: topic} do
@ -74,14 +54,9 @@ defmodule BlockScoutWeb.AddressChannelTest do
Notifier.handle_event({:chain_event, :transactions, :realtime, [transaction.hash]})
receive do
%Phoenix.Socket.Broadcast{topic: ^topic, event: "transaction", payload: payload} ->
assert_receive %Phoenix.Socket.Broadcast{topic: ^topic, event: "transaction", payload: payload}, 5_000
assert payload.address.hash == address.hash
assert payload.transaction.hash == transaction.hash
after
5_000 ->
assert false, "Expected message received nothing."
end
end
test "notified of new_transaction for matching to_address", %{address: address, topic: topic} do
@ -92,14 +67,9 @@ defmodule BlockScoutWeb.AddressChannelTest do
Notifier.handle_event({:chain_event, :transactions, :realtime, [transaction.hash]})
receive do
%Phoenix.Socket.Broadcast{topic: ^topic, event: "transaction", payload: payload} ->
assert_receive %Phoenix.Socket.Broadcast{topic: ^topic, event: "transaction", payload: payload}, 5_000
assert payload.address.hash == address.hash
assert payload.transaction.hash == transaction.hash
after
5_000 ->
assert false, "Expected message received nothing."
end
end
test "not notified twice of new_transaction if to and from address are equal", %{address: address, topic: topic} do
@ -110,20 +80,11 @@ defmodule BlockScoutWeb.AddressChannelTest do
Notifier.handle_event({:chain_event, :transactions, :realtime, [transaction.hash]})
receive do
%Phoenix.Socket.Broadcast{topic: ^topic, event: "transaction", payload: payload} ->
assert_receive %Phoenix.Socket.Broadcast{topic: ^topic, event: "transaction", payload: payload}, 5_000
assert payload.address.hash == address.hash
assert payload.transaction.hash == transaction.hash
after
5_000 ->
assert false, "Expected message received nothing."
end
receive do
_ -> assert false, "Received duplicate broadcast."
after
100 -> assert true
end
refute_receive _, 100, "Received duplicate broadcast."
end
test "notified of new_internal_transaction for matching from_address", %{address: address, topic: topic} do
@ -136,14 +97,18 @@ defmodule BlockScoutWeb.AddressChannelTest do
Notifier.handle_event({:chain_event, :internal_transactions, :realtime, [internal_transaction]})
receive do
%Phoenix.Socket.Broadcast{topic: ^topic, event: "internal_transaction", payload: payload} ->
assert payload.address.hash == address.hash
assert payload.internal_transaction.id == internal_transaction.id
after
5_000 ->
assert false, "Expected message received nothing."
end
assert_receive %Phoenix.Socket.Broadcast{
topic: ^topic,
event: "internal_transaction",
payload: %{
address: %{hash: address_hash},
internal_transaction: %{transaction_hash: transaction_hash, index: index}
}
},
5_000
assert address_hash == address.hash
assert {transaction_hash, index} == {internal_transaction.transaction_hash, internal_transaction.index}
end
test "notified of new_internal_transaction for matching to_address", %{address: address, topic: topic} do
@ -156,14 +121,18 @@ defmodule BlockScoutWeb.AddressChannelTest do
Notifier.handle_event({:chain_event, :internal_transactions, :realtime, [internal_transaction]})
receive do
%Phoenix.Socket.Broadcast{topic: ^topic, event: "internal_transaction", payload: payload} ->
assert payload.address.hash == address.hash
assert payload.internal_transaction.id == internal_transaction.id
after
5_000 ->
assert false, "Expected message received nothing."
end
assert_receive %Phoenix.Socket.Broadcast{
topic: ^topic,
event: "internal_transaction",
payload: %{
address: %{hash: address_hash},
internal_transaction: %{transaction_hash: transaction_hash, index: index}
}
},
5_000
assert address_hash == address.hash
assert {transaction_hash, index} == {internal_transaction.transaction_hash, internal_transaction.index}
end
test "not notified twice of new_internal_transaction if to and from address are equal", %{
@ -180,20 +149,20 @@ defmodule BlockScoutWeb.AddressChannelTest do
Notifier.handle_event({:chain_event, :internal_transactions, :realtime, [internal_transaction]})
receive do
%Phoenix.Socket.Broadcast{topic: ^topic, event: "internal_transaction", payload: payload} ->
assert payload.address.hash == address.hash
assert payload.internal_transaction.id == internal_transaction.id
after
5_000 ->
assert false, "Expected message received nothing."
end
assert_receive %Phoenix.Socket.Broadcast{
topic: ^topic,
event: "internal_transaction",
payload: %{
address: %{hash: address_hash},
internal_transaction: %{transaction_hash: transaction_hash, index: index}
}
},
5_000
receive do
_ -> assert false, "Received duplicate broadcast."
after
100 -> assert true
end
assert address_hash == address.hash
assert {transaction_hash, index} == {internal_transaction.transaction_hash, internal_transaction.index}
refute_receive _, 100, "Received duplicate broadcast."
end
end
end

@ -50,12 +50,18 @@ defmodule BlockScoutWeb.AddressInternalTransactionControllerTest do
path = address_internal_transaction_path(conn, :index, address)
conn = get(conn, path)
actual_transaction_ids =
conn.assigns.internal_transactions
|> Enum.map(fn internal_transaction -> internal_transaction.id end)
actual_internal_transaction_primary_keys =
Enum.map(conn.assigns.internal_transactions, &{&1.transaction_hash, &1.index})
assert Enum.member?(
actual_internal_transaction_primary_keys,
{from_internal_transaction.transaction_hash, from_internal_transaction.index}
)
assert Enum.member?(actual_transaction_ids, from_internal_transaction.id)
assert Enum.member?(actual_transaction_ids, to_internal_transaction.id)
assert Enum.member?(
actual_internal_transaction_primary_keys,
{to_internal_transaction.transaction_hash, to_internal_transaction.index}
)
end
test "includes USD exchange rate value for address in assigns", %{conn: conn} do

@ -59,13 +59,12 @@ defmodule BlockScoutWeb.TransactionInternalTransactionControllerTest do
conn = get(conn, path)
actual_internal_transaction_ids =
conn.assigns.internal_transactions
|> Enum.map(fn it -> it.id end)
actual_internal_transaction_primary_keys =
Enum.map(conn.assigns.internal_transactions, &{&1.transaction_hash, &1.index})
assert html_response(conn, 200)
assert Enum.member?(actual_internal_transaction_ids, expected_internal_transaction.id)
assert {expected_internal_transaction.transaction_hash, expected_internal_transaction.index} in actual_internal_transaction_primary_keys
end
test "includes USD exchange rate value for address in assigns", %{conn: conn} do

@ -12,7 +12,10 @@ defmodule BlockScoutWeb.TransactionTokenTransferControllerTest do
conn = get(conn, transaction_token_transfer_path(BlockScoutWeb.Endpoint, :index, transaction.hash))
assert List.first(conn.assigns.transaction.token_transfers).id == token_transfer.id
assigned_token_transfer = List.first(conn.assigns.transaction.token_transfers)
assert {assigned_token_transfer.transaction_hash, assigned_token_transfer.log_index} ==
{token_transfer.transaction_hash, token_transfer.log_index}
end
test "with missing transaction", %{conn: conn} do
@ -53,13 +56,16 @@ defmodule BlockScoutWeb.TransactionTokenTransferControllerTest do
conn = get(conn, path)
actual_token_transfer_ids =
actual_token_transfer_primary_keys =
conn.assigns.token_transfers
|> Enum.map(fn it -> it.id end)
|> Enum.map(&{&1.transaction_hash, &1.log_index})
assert html_response(conn, 200)
assert Enum.member?(actual_token_transfer_ids, expected_token_transfer.id)
assert Enum.member?(
actual_token_transfer_primary_keys,
{expected_token_transfer.transaction_hash, expected_token_transfer.log_index}
)
end
test "includes USD exchange rate value for address in assigns", %{conn: conn} do

@ -75,20 +75,36 @@ defmodule BlockScoutWeb.AddressPage do
css("[data-test='address_detail_hash']", text: to_string(address_hash))
end
def internal_transaction(%InternalTransaction{id: id}) do
css("[data-test='internal_transaction'][data-internal-transaction-id='#{id}']")
def internal_transaction(%InternalTransaction{transaction_hash: transaction_hash, index: index}) do
css(
"[data-test='internal_transaction']" <>
"[data-internal-transaction-transaction-hash='#{transaction_hash}']" <>
"[data-internal-transaction-index='#{index}']"
)
end
def internal_transactions(count: count) do
css("[data-test='internal_transaction']", count: count)
end
def internal_transaction_address_link(%InternalTransaction{id: id, from_address_hash: address_hash}, :from) do
css("[data-internal-transaction-id='#{id}'] [data-test='address_hash_link'] [data-address-hash='#{address_hash}']")
def internal_transaction_address_link(
%InternalTransaction{transaction_hash: transaction_hash, index: index, from_address_hash: address_hash},
:from
) do
css(
"[data-internal-transaction-transaction-hash='#{transaction_hash}'][data-internal-transaction-index='#{index}']" <>
" [data-test='address_hash_link']" <> " [data-address-hash='#{address_hash}']"
)
end
def internal_transaction_address_link(%InternalTransaction{id: id, to_address_hash: address_hash}, :to) do
css("[data-internal-transaction-id='#{id}'] [data-test='address_hash_link'] [data-address-hash='#{address_hash}']")
def internal_transaction_address_link(
%InternalTransaction{transaction_hash: transaction_hash, index: index, to_address_hash: address_hash},
:to
) do
css(
"[data-internal-transaction-transaction-hash='#{transaction_hash}'][data-internal-transaction-index='#{index}']" <>
" [data-test='address_hash_link']" <> " [data-address-hash='#{address_hash}']"
)
end
def pending_transaction(%Transaction{hash: transaction_hash}), do: pending_transaction(transaction_hash)

@ -7,50 +7,58 @@ defmodule BlockScoutWeb.ViewingAppTest do
describe "loading bar when indexing" do
test "shows blocks indexed percentage", %{session: session} do
for index <- 6..10 do
for index <- 5..9 do
insert(:block, number: index)
end
assert Explorer.Chain.indexed_ratio() == 0.5
session
|> AppPage.visit_page()
|> assert_has(AppPage.indexed_status("50% Blocks Indexed"))
end
test "shows tokens loading", %{session: session} do
for index <- 1..10 do
for index <- 0..9 do
insert(:block, number: index)
end
assert Explorer.Chain.indexed_ratio() == 1.0
session
|> AppPage.visit_page()
|> assert_has(AppPage.indexed_status("Indexing Tokens"))
end
test "live updates blocks indexed percentage", %{session: session} do
for index <- 6..10 do
for index <- 5..9 do
insert(:block, number: index)
end
assert Explorer.Chain.indexed_ratio() == 0.5
session
|> AppPage.visit_page()
|> assert_has(AppPage.indexed_status("50% Blocks Indexed"))
insert(:block, number: 5)
insert(:block, number: 4)
Notifier.handle_event({:chain_event, :blocks, :catchup, []})
assert_has(session, AppPage.indexed_status("60% Blocks Indexed"))
end
test "live updates when blocks are fully indexed", %{session: session} do
for index <- 2..10 do
for index <- 1..9 do
insert(:block, number: index)
end
assert Explorer.Chain.indexed_ratio() == 0.9
session
|> AppPage.visit_page()
|> assert_has(AppPage.indexed_status("90% Blocks Indexed"))
insert(:block, number: 1)
insert(:block, number: 0)
Notifier.handle_event({:chain_event, :blocks, :catchup, []})
assert_has(session, AppPage.indexed_status("Indexing Tokens"))
@ -58,10 +66,12 @@ defmodule BlockScoutWeb.ViewingAppTest do
test "live removes message when chain is indexed", %{session: session} do
[block | _] =
for index <- 1..10 do
for index <- 0..9 do
insert(:block, number: index)
end
assert Explorer.Chain.indexed_ratio() == 1.0
session
|> AppPage.visit_page()
|> assert_has(AppPage.indexed_status("Indexing Tokens"))

@ -9,7 +9,6 @@ defmodule BlockScoutWeb.ViewingTransactionsTest do
setup do
block =
insert(:block, %{
number: 555,
timestamp: Timex.now() |> Timex.shift(hours: -2),
gas_used: 123_987
})

@ -842,7 +842,7 @@ defmodule Explorer.Chain do
@doc """
The percentage of indexed blocks on the chain.
iex> for index <- 6..10 do
iex> for index <- 5..9 do
...> insert(:block, number: index)
...> end
iex> Explorer.Chain.indexed_ratio()
@ -859,7 +859,7 @@ defmodule Explorer.Chain do
with {:ok, min_block_number} <- min_block_number(),
{:ok, max_block_number} <- max_block_number() do
indexed_blocks = max_block_number - min_block_number + 1
indexed_blocks / max_block_number
indexed_blocks / (max_block_number + 1)
else
{:error, _} -> 0
end
@ -883,7 +883,7 @@ defmodule Explorer.Chain do
"""
def internal_transaction_count do
Repo.aggregate(InternalTransaction, :count, :id)
Repo.one!(from(it in "internal_transactions", select: fragment("COUNT(*)")))
end
@doc """
@ -1167,7 +1167,7 @@ defmodule Explorer.Chain do
"""
def log_count do
Repo.aggregate(Log, :count, :id)
Repo.one!(from(log in "logs", select: fragment("COUNT(*)")))
end
@doc """
@ -1894,7 +1894,7 @@ defmodule Explorer.Chain do
internal_transaction.type != ^:call or
fragment(
"""
(SELECT COUNT(sibling.id)
(SELECT COUNT(sibling.*)
FROM internal_transactions AS sibling
WHERE sibling.transaction_hash = ?
LIMIT 2
@ -1967,7 +1967,7 @@ defmodule Explorer.Chain do
left_join: tf in TokenTransfer,
on: tf.transaction_hash == l.transaction_hash and tf.log_index == l.index,
where: l.first_topic == unquote(TokenTransfer.constant()),
where: is_nil(tf.id),
where: is_nil(tf.transaction_hash) and is_nil(tf.log_index),
select: t.block_number,
distinct: t.block_number
)
@ -2038,7 +2038,7 @@ defmodule Explorer.Chain do
token_changeset = Token.changeset(token, params)
address_name_changeset = Address.Name.changeset(%Address.Name{}, Map.put(params, :address_hash, address_hash))
token_opts = [on_conflict: :replace_all, conflict_target: :contract_address_hash]
token_opts = [on_conflict: Import.Tokens.default_on_conflict(), conflict_target: :contract_address_hash]
address_name_opts = [on_conflict: :nothing, conflict_target: [:address_hash, :name]]
insert_result =

@ -35,12 +35,16 @@ defmodule Explorer.Chain.Import.Address.CoinBalances do
end
@impl Import.Runner
def run(multi, changes_list, options) when is_map(options) do
timestamps = Map.fetch!(options, :timestamps)
timeout = options[option_key()][:timeout] || @timeout
def run(multi, changes_list, %{timestamps: timestamps} = options) do
insert_options =
options
|> Map.get(option_key(), %{})
|> Map.take(~w(on_conflict timeout)a)
|> Map.put_new(:timeout, @timeout)
|> Map.put(:timestamps, timestamps)
Multi.run(multi, :address_coin_balances, fn _ ->
insert(changes_list, %{timeout: timeout, timestamps: timestamps})
insert(changes_list, insert_options)
end)
end
@ -56,6 +60,7 @@ defmodule Explorer.Chain.Import.Address.CoinBalances do
}
],
%{
optional(:on_conflict) => Import.Runner.on_conflict(),
required(:timeout) => timeout,
required(:timestamps) => Import.timestamps()
}

@ -33,12 +33,16 @@ defmodule Explorer.Chain.Import.Address.TokenBalances do
end
@impl Import.Runner
def run(multi, changes_list, options) when is_map(options) do
timestamps = Map.fetch!(options, :timestamps)
timeout = options[option_key()][:timeout] || @timeout
def run(multi, changes_list, %{timestamps: timestamps} = options) do
insert_options =
options
|> Map.get(option_key(), %{})
|> Map.take(~w(on_conflict timeout)a)
|> Map.put_new(:timeout, @timeout)
|> Map.put(:timestamps, timestamps)
Multi.run(multi, :address_token_balances, fn _ ->
insert(changes_list, %{timeout: timeout, timestamps: timestamps})
insert(changes_list, insert_options)
end)
end
@ -46,6 +50,7 @@ defmodule Explorer.Chain.Import.Address.TokenBalances do
def timeout, do: @timeout
@spec insert([map()], %{
optional(:on_conflict) => Import.Runner.on_conflict(),
required(:timeout) => timeout(),
required(:timestamps) => Import.timestamps()
}) ::

@ -32,12 +32,16 @@ defmodule Explorer.Chain.Import.Addresses do
end
@impl Import.Runner
def run(multi, changes_list, options) when is_map(options) do
timestamps = Map.fetch!(options, :timestamps)
timeout = options[:addresses][:timeout] || @timeout
def run(multi, changes_list, %{timestamps: timestamps} = options) do
insert_options =
options
|> Map.get(option_key(), %{})
|> Map.take(~w(on_conflict timeout)a)
|> Map.put_new(:timeout, @timeout)
|> Map.put(:timestamps, timestamps)
Multi.run(multi, :addresses, fn _ ->
insert(changes_list, %{timeout: timeout, timestamps: timestamps})
insert(changes_list, insert_options)
end)
end
@ -47,6 +51,7 @@ defmodule Explorer.Chain.Import.Addresses do
## Private Functions
@spec insert([%{hash: Hash.Address.t()}], %{
optional(:on_conflict) => Import.Runner.on_conflict(),
required(:timeout) => timeout,
required(:timestamps) => Import.timestamps()
}) :: {:ok, [Address.t()]}

@ -34,17 +34,21 @@ defmodule Explorer.Chain.Import.Block.SecondDegreeRelations do
@impl Import.Runner
def run(multi, changes_list, options) when is_map(options) do
timeout = options[:block_second_degree_relations][:timeout] || @timeout
insert_options =
options
|> Map.get(option_key(), %{})
|> Map.take(~w(on_conflict timeout)a)
|> Map.put_new(:timeout, @timeout)
Multi.run(multi, :block_second_degree_relations, fn _ ->
insert(changes_list, %{timeout: timeout})
insert(changes_list, insert_options)
end)
end
@impl Import.Runner
def timeout, do: @timeout
@spec insert([map()], %{required(:timeout) => timeout}) ::
@spec insert([map()], %{optional(:on_conflict) => Import.Runner.on_conflict(), required(:timeout) => timeout}) ::
{:ok, %{nephew_hash: Hash.Full.t(), uncle_hash: Hash.Full.t()}} | {:error, [Changeset.t()]}
defp insert(changes_list, %{timeout: timeout} = options) when is_list(changes_list) do
on_conflict = Map.get_lazy(options, :on_conflict, &default_on_conflict/0)

@ -34,9 +34,14 @@ defmodule Explorer.Chain.Import.Blocks do
end
@impl Import.Runner
def run(multi, changes_list, options) when is_map(options) do
timestamps = Map.fetch!(options, :timestamps)
blocks_timeout = options[option_key()][:timeout] || @timeout
def run(multi, changes_list, %{timestamps: timestamps} = options) do
insert_options =
options
|> Map.get(option_key(), %{})
|> Map.take(~w(on_conflict timeout)a)
|> Map.put_new(:timeout, @timeout)
|> Map.put(:timestamps, timestamps)
where_forked = where_forked(changes_list)
multi
@ -56,10 +61,10 @@ defmodule Explorer.Chain.Import.Blocks do
})
end)
|> Multi.run(:lose_consenus, fn _ ->
lose_consensus(changes_list, %{timeout: blocks_timeout, timestamps: timestamps})
lose_consensus(changes_list, insert_options)
end)
|> Multi.run(:blocks, fn _ ->
insert(changes_list, %{timeout: blocks_timeout, timestamps: timestamps})
insert(changes_list, insert_options)
end)
|> Multi.run(:uncle_fetched_block_second_degree_relations, fn %{blocks: blocks} when is_list(blocks) ->
update_block_second_degree_relations(
@ -167,10 +172,13 @@ defmodule Explorer.Chain.Import.Blocks do
end
end
@spec insert([map()], %{required(:timeout) => timeout, required(:timestamps) => Import.timestamps()}) ::
{:ok, [Block.t()]} | {:error, [Changeset.t()]}
@spec insert([map()], %{
optional(:on_conflict) => Import.Runner.on_conflict(),
required(:timeout) => timeout,
required(:timestamps) => Import.timestamps()
}) :: {:ok, [Block.t()]} | {:error, [Changeset.t()]}
defp insert(changes_list, %{timeout: timeout, timestamps: timestamps} = options) when is_list(changes_list) do
on_conflict = Map.get(options, :on_conflict, :replace_all)
on_conflict = Map.get_lazy(options, :on_conflict, &default_on_conflict/0)
# order so that row ShareLocks are grabbed in a consistent order
ordered_changes_list = Enum.sort_by(changes_list, &{&1.number, &1.hash})
@ -186,6 +194,30 @@ defmodule Explorer.Chain.Import.Blocks do
)
end
defp default_on_conflict do
from(
block in Block,
update: [
set: [
consensus: fragment("EXCLUDED.consensus"),
difficulty: fragment("EXCLUDED.difficulty"),
gas_limit: fragment("EXCLUDED.gas_limit"),
gas_used: fragment("EXCLUDED.gas_used"),
miner_hash: fragment("EXCLUDED.miner_hash"),
nonce: fragment("EXCLUDED.nonce"),
number: fragment("EXCLUDED.number"),
parent_hash: fragment("EXCLUDED.parent_hash"),
size: fragment("EXCLUDED.size"),
timestamp: fragment("EXCLUDED.timestamp"),
total_difficulty: fragment("EXCLUDED.total_difficulty"),
# Don't update `hash` as it is used for the conflict target
inserted_at: fragment("LEAST(?, EXCLUDED.inserted_at)", block.inserted_at),
updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", block.updated_at)
]
]
)
end
defp lose_consensus(blocks_changes, %{timeout: timeout, timestamps: %{updated_at: updated_at}})
when is_list(blocks_changes) do
ordered_consensus_block_number =

@ -35,30 +35,41 @@ defmodule Explorer.Chain.Import.InternalTransactions do
end
@impl Import.Runner
def run(multi, changes_list, options) when is_map(options) do
timestamps = Map.fetch!(options, :timestamps)
internal_transactions_timeout = options[option_key()][:timeout] || @timeout
def run(multi, changes_list, %{timestamps: timestamps} = options) when is_map(options) do
insert_options =
options
|> Map.get(option_key(), %{})
|> Map.take(~w(on_conflict timeout)a)
|> Map.put_new(:timeout, @timeout)
|> Map.put(:timestamps, timestamps)
transactions_timeout = options[Import.Transactions.option_key()][:timeout] || Import.Transactions.timeout()
update_transactions_options = %{timeout: transactions_timeout, timestamps: timestamps}
multi
|> Multi.run(:internal_transactions, fn _ ->
insert(changes_list, %{timeout: internal_transactions_timeout, timestamps: timestamps})
insert(changes_list, insert_options)
end)
|> Multi.run(:internal_transactions_indexed_at_transactions, fn %{internal_transactions: internal_transactions}
when is_list(internal_transactions) ->
update_transactions(internal_transactions, %{timeout: transactions_timeout, timestamps: timestamps})
update_transactions(internal_transactions, update_transactions_options)
end)
end
@impl Import.Runner
def timeout, do: @timeout
@spec insert([map], %{required(:timeout) => timeout, required(:timestamps) => Import.timestamps()}) ::
@spec insert([map], %{
optional(:on_conflict) => Import.Runner.on_conflict(),
required(:timeout) => timeout,
required(:timestamps) => Import.timestamps()
}) ::
{:ok, [%{index: non_neg_integer, transaction_hash: Hash.t()}]}
| {:error, [Changeset.t()]}
defp insert(changes_list, %{timeout: timeout, timestamps: timestamps} = options)
when is_list(changes_list) do
on_conflict = Map.get(options, :on_conflict, :replace_all)
on_conflict = Map.get_lazy(options, :on_conflict, &default_on_conflict/0)
# order so that row ShareLocks are grabbed in a consistent order
ordered_changes_list = Enum.sort_by(changes_list, &{&1.transaction_hash, &1.index})
@ -69,7 +80,7 @@ defmodule Explorer.Chain.Import.InternalTransactions do
conflict_target: [:transaction_hash, :index],
for: InternalTransaction,
on_conflict: on_conflict,
returning: [:id, :index, :transaction_hash],
returning: [:transaction_hash, :index],
timeout: timeout,
timestamps: timestamps
)
@ -81,6 +92,36 @@ defmodule Explorer.Chain.Import.InternalTransactions do
)}
end
defp default_on_conflict do
from(
internal_transaction in InternalTransaction,
update: [
set: [
block_number: fragment("EXCLUDED.block_number"),
call_type: fragment("EXCLUDED.call_type"),
created_contract_address_hash: fragment("EXCLUDED.created_contract_address_hash"),
created_contract_code: fragment("EXCLUDED.created_contract_code"),
error: fragment("EXCLUDED.error"),
from_address_hash: fragment("EXCLUDED.from_address_hash"),
gas: fragment("EXCLUDED.gas"),
gas_used: fragment("EXCLUDED.gas_used"),
# Don't update `index` as it is part of the composite primary key and used for the conflict target
init: fragment("EXCLUDED.init"),
input: fragment("EXCLUDED.input"),
output: fragment("EXCLUDED.output"),
to_address_hash: fragment("EXCLUDED.to_address_hash"),
trace_address: fragment("EXCLUDED.trace_address"),
# Don't update `transaction_hash` as it is part of the composite primary key and used for the conflict target
transaction_index: fragment("EXCLUDED.transaction_index"),
type: fragment("EXCLUDED.type"),
value: fragment("EXCLUDED.value"),
inserted_at: fragment("LEAST(?, EXCLUDED.inserted_at)", internal_transaction.inserted_at),
updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", internal_transaction.updated_at)
]
]
)
end
defp update_transactions(internal_transactions, %{
timeout: timeout,
timestamps: timestamps

@ -8,6 +8,8 @@ defmodule Explorer.Chain.Import.Logs do
alias Ecto.{Changeset, Multi}
alias Explorer.Chain.{Import, Log}
import Ecto.Query, only: [from: 2]
@behaviour Import.Runner
# milliseconds
@ -30,23 +32,31 @@ defmodule Explorer.Chain.Import.Logs do
end
@impl Import.Runner
def run(multi, changes_list, options) when is_map(options) do
timestamps = Map.fetch!(options, :timestamps)
timeout = options[option_key()][:timeout] || @timeout
def run(multi, changes_list, %{timestamps: timestamps} = options) do
insert_options =
options
|> Map.get(option_key(), %{})
|> Map.take(~w(on_conflict timeout)a)
|> Map.put_new(:timeout, @timeout)
|> Map.put(:timestamps, timestamps)
Multi.run(multi, :logs, fn _ ->
insert(changes_list, %{timeout: timeout, timestamps: timestamps})
insert(changes_list, insert_options)
end)
end
@impl Import.Runner
def timeout, do: @timeout
@spec insert([map()], %{required(:timeout) => timeout, required(:timestamps) => Import.timestamps()}) ::
@spec insert([map()], %{
optional(:on_conflict) => Import.Runner.on_conflict(),
required(:timeout) => timeout,
required(:timestamps) => Import.timestamps()
}) ::
{:ok, [Log.t()]}
| {:error, [Changeset.t()]}
defp insert(changes_list, %{timeout: timeout, timestamps: timestamps} = options) when is_list(changes_list) do
on_conflict = Map.get(options, :on_conflict, :replace_all)
on_conflict = Map.get_lazy(options, :on_conflict, &default_on_conflict/0)
# order so that row ShareLocks are grabbed in a consistent order
ordered_changes_list = Enum.sort_by(changes_list, &{&1.transaction_hash, &1.index})
@ -62,4 +72,25 @@ defmodule Explorer.Chain.Import.Logs do
timestamps: timestamps
)
end
defp default_on_conflict do
from(
log in Log,
update: [
set: [
address_hash: fragment("EXCLUDED.address_hash"),
data: fragment("EXCLUDED.data"),
first_topic: fragment("EXCLUDED.first_topic"),
second_topic: fragment("EXCLUDED.second_topic"),
third_topic: fragment("EXCLUDED.third_topic"),
fourth_topic: fragment("EXCLUDED.fourth_topic"),
# Don't update `index` as it is part of the composite primary key and used for the conflict target
type: fragment("EXCLUDED.type"),
# Don't update `transaction_hash` as it is part of the composite primary key and used for the conflict target
inserted_at: fragment("LEAST(?, EXCLUDED.inserted_at)", log.inserted_at),
updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", log.updated_at)
]
]
)
end
end

@ -5,6 +5,8 @@ defmodule Explorer.Chain.Import.TokenTransfers do
require Ecto.Query
import Ecto.Query, only: [from: 2]
alias Ecto.{Changeset, Multi}
alias Explorer.Chain.{Import, TokenTransfer}
@ -30,12 +32,16 @@ defmodule Explorer.Chain.Import.TokenTransfers do
end
@impl Import.Runner
def run(multi, changes_list, options) when is_map(options) do
timestamps = Map.fetch!(options, :timestamps)
timeout = options[option_key()][:timeout] || @timeout
def run(multi, changes_list, %{timestamps: timestamps} = options) do
insert_options =
options
|> Map.get(option_key(), %{})
|> Map.take(~w(on_conflict timeout)a)
|> Map.put_new(:timeout, @timeout)
|> Map.put(:timestamps, timestamps)
Multi.run(multi, :token_transfers, fn _ ->
insert(changes_list, %{timeout: timeout, timestamps: timestamps})
insert(changes_list, insert_options)
end)
end
@ -46,7 +52,7 @@ defmodule Explorer.Chain.Import.TokenTransfers do
{:ok, [TokenTransfer.t()]}
| {:error, [Changeset.t()]}
def insert(changes_list, %{timeout: timeout, timestamps: timestamps} = options) when is_list(changes_list) do
on_conflict = Map.get(options, :on_conflict, :replace_all)
on_conflict = Map.get_lazy(options, :on_conflict, &default_on_conflict/0)
# order so that row ShareLocks are grabbed in a consistent order
ordered_changes_list = Enum.sort_by(changes_list, &{&1.transaction_hash, &1.log_index})
@ -62,4 +68,23 @@ defmodule Explorer.Chain.Import.TokenTransfers do
timestamps: timestamps
)
end
defp default_on_conflict do
from(
token_transfer in TokenTransfer,
update: [
set: [
# Don't update `transaction_hash` as it is part of the composite primary key and used for the conflict target
# Don't update `log_index` as it is part of the composite primary key and used for the conflict target
amount: fragment("EXCLUDED.amount"),
from_address_hash: fragment("EXCLUDED.from_address_hash"),
to_address_hash: fragment("EXCLUDED.to_address_hash"),
token_contract_address_hash: fragment("EXCLUDED.token_contract_address_hash"),
token_id: fragment("EXCLUDED.token_id"),
inserted_at: fragment("LEAST(?, EXCLUDED.inserted_at)", token_transfer.inserted_at),
updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", token_transfer.updated_at)
]
]
)
end
end

@ -5,6 +5,8 @@ defmodule Explorer.Chain.Import.Tokens do
require Ecto.Query
import Ecto.Query, only: [from: 2]
alias Ecto.Multi
alias Explorer.Chain.{Import, Token}
@ -30,12 +32,16 @@ defmodule Explorer.Chain.Import.Tokens do
end
@impl Import.Runner
def run(multi, changes_list, options) when is_map(options) do
%{timestamps: timestamps, tokens: %{on_conflict: on_conflict}} = options
timeout = options[option_key()][:timeout] || @timeout
def run(multi, changes_list, %{timestamps: timestamps} = options) do
insert_options =
options
|> Map.get(option_key(), %{})
|> Map.take(~w(on_conflict timeout)a)
|> Map.put_new(:timeout, @timeout)
|> Map.put(:timestamps, timestamps)
Multi.run(multi, :tokens, fn _ ->
insert(changes_list, %{on_conflict: on_conflict, timeout: timeout, timestamps: timestamps})
insert(changes_list, insert_options)
end)
end
@ -46,12 +52,9 @@ defmodule Explorer.Chain.Import.Tokens do
required(:on_conflict) => Import.Runner.on_conflict(),
required(:timeout) => timeout(),
required(:timestamps) => Import.timestamps()
}) ::
{:ok, [Token.t()]}
| {:error, {:required, :on_conflict}}
}) :: {:ok, [Token.t()]}
def insert(changes_list, %{timeout: timeout, timestamps: timestamps} = options) when is_list(changes_list) do
case options do
%{on_conflict: on_conflict} ->
on_conflict = Map.get_lazy(options, :on_conflict, &default_on_conflict/0)
# order so that row ShareLocks are grabbed in a consistent order
ordered_changes_list = Enum.sort_by(changes_list, & &1.contract_address_hash)
@ -65,9 +68,24 @@ defmodule Explorer.Chain.Import.Tokens do
timeout: timeout,
timestamps: timestamps
)
_ ->
{:error, {:required, :on_conflict}}
end
def default_on_conflict do
from(
token in Token,
update: [
set: [
name: fragment("EXCLUDED.name"),
symbol: fragment("EXCLUDED.symbol"),
total_supply: fragment("EXCLUDED.total_supply"),
decimals: fragment("EXCLUDED.decimals"),
type: fragment("EXCLUDED.type"),
cataloged: fragment("EXCLUDED.cataloged"),
# Don't update `contract_address_hash` as it is the primary key and used for the conflict target
inserted_at: fragment("LEAST(?, EXCLUDED.inserted_at)", token.inserted_at),
updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", token.updated_at)
]
]
)
end
end

@ -34,12 +34,16 @@ defmodule Explorer.Chain.Import.Transaction.Forks do
end
@impl Import.Runner
def run(multi, changes_list, options) when is_map(options) do
%{timestamps: timestamps} = options
timeout = options[option_key()][:timeout] || @timeout
def run(multi, changes_list, %{timestamps: timestamps} = options) do
insert_options =
options
|> Map.get(option_key(), %{})
|> Map.take(~w(on_conflict timeout)a)
|> Map.put_new(:timeout, @timeout)
|> Map.put(:timestamps, timestamps)
Multi.run(multi, :transaction_forks, fn _ ->
insert(changes_list, %{timeout: timeout, timestamps: timestamps})
insert(changes_list, insert_options)
end)
end
@ -47,6 +51,7 @@ defmodule Explorer.Chain.Import.Transaction.Forks do
def timeout, do: @timeout
@spec insert([map()], %{
optional(:on_conflict) => Import.Runner.on_conflict(),
required(:timeout) => timeout,
required(:timestamps) => Import.timestamps()
}) :: {:ok, [%{uncle_hash: Hash.t(), hash: Hash.t()}]}

@ -5,6 +5,8 @@ defmodule Explorer.Chain.Import.Transactions do
require Ecto.Query
import Ecto.Query, only: [from: 2]
alias Ecto.Multi
alias Explorer.Chain.{Hash, Import, Transaction}
@ -30,12 +32,16 @@ defmodule Explorer.Chain.Import.Transactions do
end
@impl Import.Runner
def run(multi, changes_list, options) when is_map(options) do
%{timestamps: timestamps, transactions: %{on_conflict: on_conflict} = transactions_options} = options
timeout = transactions_options[:timeout] || @timeout
def run(multi, changes_list, %{timestamps: timestamps} = options) do
insert_options =
options
|> Map.get(option_key(), %{})
|> Map.take(~w(on_conflict timeout)a)
|> Map.put_new(:timeout, @timeout)
|> Map.put(:timestamps, timestamps)
Multi.run(multi, :transactions, fn _ ->
insert(changes_list, %{on_conflict: on_conflict, timeout: timeout, timestamps: timestamps})
insert(changes_list, insert_options)
end)
end
@ -43,12 +49,14 @@ defmodule Explorer.Chain.Import.Transactions do
def timeout, do: @timeout
@spec insert([map()], %{
required(:on_conflict) => Import.Runner.on_conflict(),
optional(:on_conflict) => Import.Runner.on_conflict(),
required(:timeout) => timeout,
required(:timestamps) => Import.timestamps()
}) :: {:ok, [Hash.t()]}
defp insert(changes_list, %{on_conflict: on_conflict, timeout: timeout, timestamps: timestamps})
defp insert(changes_list, %{timeout: timeout, timestamps: timestamps} = options)
when is_list(changes_list) do
on_conflict = Map.get_lazy(options, :on_conflict, &default_on_conflict/0)
# order so that row ShareLocks are grabbed in a consistent order
ordered_changes_list = Enum.sort_by(changes_list, & &1.hash)
@ -65,4 +73,36 @@ defmodule Explorer.Chain.Import.Transactions do
{:ok, for(transaction <- transactions, do: transaction.hash)}
end
defp default_on_conflict do
from(
transaction in Transaction,
update: [
set: [
block_hash: fragment("EXCLUDED.block_hash"),
block_number: fragment("EXCLUDED.block_number"),
created_contract_address_hash: fragment("EXCLUDED.created_contract_address_hash"),
cumulative_gas_used: fragment("EXCLUDED.cumulative_gas_used"),
error: fragment("EXCLUDED.error"),
from_address_hash: fragment("EXCLUDED.from_address_hash"),
gas: fragment("EXCLUDED.gas"),
gas_price: fragment("EXCLUDED.gas_price"),
gas_used: fragment("EXCLUDED.gas_used"),
index: fragment("EXCLUDED.index"),
internal_transactions_indexed_at: fragment("EXCLUDED.internal_transactions_indexed_at"),
input: fragment("EXCLUDED.input"),
nonce: fragment("EXCLUDED.nonce"),
r: fragment("EXCLUDED.r"),
s: fragment("EXCLUDED.s"),
status: fragment("EXCLUDED.status"),
to_address_hash: fragment("EXCLUDED.to_address_hash"),
v: fragment("EXCLUDED.v"),
value: fragment("EXCLUDED.value"),
# Don't update `hash` as it is part of the primary key and used for the conflict target
inserted_at: fragment("LEAST(?, EXCLUDED.inserted_at)", transaction.inserted_at),
updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", transaction.updated_at)
]
]
)
end
end

@ -7,6 +7,7 @@ defmodule Explorer.Chain.InternalTransaction do
alias Explorer.Chain.InternalTransaction.{CallType, Type}
@typedoc """
* `block_number` - the `t:Explorer.Chain.Block.t/0` `number` that the `transaction` is collated into.
* `call_type` - the type of call. `nil` when `type` is not `:call`.
* `created_contract_code` - the code of the contract that was created when `type` is `:create`.
* `error` - error message when `:call` or `:create` `type` errors
@ -23,13 +24,15 @@ defmodule Explorer.Chain.InternalTransaction do
* `trace_address` - list of traces
* `transaction` - transaction in which this transaction occurred
* `transaction_hash` - foreign key for `transaction`
* `transaction_index` - the `t:Explorer.Chain.Transaction.t/0` `index` of `transaction` in `block_number`.
* `type` - type of internal transaction
* `value` - value of transferred from `from_address` to `to_address`
"""
@type t :: %__MODULE__{
block_number: Explorer.Chain.Block.block_number() | nil,
call_type: CallType.t() | nil,
created_contract_address: %Ecto.Association.NotLoaded{} | Address.t() | nil,
created_contract_address_hash: Explorer.Chain.Hash.t() | nil,
created_contract_address_hash: Hash.t() | nil,
created_contract_code: Data.t() | nil,
error: String.t(),
from_address: %Ecto.Association.NotLoaded{} | Address.t(),
@ -44,18 +47,20 @@ defmodule Explorer.Chain.InternalTransaction do
to_address_hash: Hash.Address.t() | nil,
trace_address: [non_neg_integer()],
transaction: %Ecto.Association.NotLoaded{} | Transaction.t(),
transaction_hash: Explorer.Chain.Hash.t(),
transaction_hash: Hash.t(),
transaction_index: Transaction.transaction_index() | nil,
type: Type.t(),
value: Wei.t()
}
@primary_key false
schema "internal_transactions" do
field(:call_type, CallType)
field(:created_contract_code, Data)
field(:error, :string)
field(:gas, :decimal)
field(:gas_used, :decimal)
field(:index, :integer)
field(:index, :integer, primary_key: true)
field(:init, Data)
field(:input, Data)
field(:output, Data)
@ -91,7 +96,12 @@ defmodule Explorer.Chain.InternalTransaction do
type: Hash.Address
)
belongs_to(:transaction, Transaction, foreign_key: :transaction_hash, references: :hash, type: Hash.Full)
belongs_to(:transaction, Transaction,
foreign_key: :transaction_hash,
primary_key: true,
references: :hash,
type: Hash.Full
)
end
@doc """

@ -13,12 +13,12 @@ defmodule Explorer.Chain.Log do
* `address_hash` - foreign key for `address`
* `data` - non-indexed log parameters.
* `first_topic` - `topics[0]`
* `fourth_topic` - `topics[3]`
* `index` - index of the log entry in all logs for the `transaction`
* `second_topic` - `topics[1]`
* `third_topic` - `topics[2]`
* `fourth_topic` - `topics[3]`
* `transaction` - transaction for which `log` is
* `transaction_hash` - foreign key for `transaction`.
* `third_topic` - `topics[2]`
* `index` - index of the log entry in all logs for the `transaction`
* `type` - type of event. *Parity-only*
"""
@type t :: %__MODULE__{
@ -26,28 +26,35 @@ defmodule Explorer.Chain.Log do
address_hash: Hash.Address.t(),
data: Data.t(),
first_topic: String.t(),
fourth_topic: String.t(),
index: non_neg_integer(),
second_topic: String.t(),
third_topic: String.t(),
fourth_topic: String.t(),
transaction: %Ecto.Association.NotLoaded{} | Transaction.t(),
transaction_hash: Hash.Full.t(),
third_topic: String.t(),
index: non_neg_integer(),
type: String.t() | nil
}
@primary_key false
schema "logs" do
field(:data, Data)
field(:first_topic, :string)
field(:fourth_topic, :string)
field(:index, :integer)
field(:second_topic, :string)
field(:third_topic, :string)
field(:fourth_topic, :string)
field(:index, :integer, primary_key: true)
field(:type, :string)
timestamps()
belongs_to(:address, Address, foreign_key: :address_hash, references: :hash, type: Hash.Address)
belongs_to(:transaction, Transaction, foreign_key: :transaction_hash, references: :hash, type: Hash.Full)
belongs_to(:transaction, Transaction,
foreign_key: :transaction_hash,
primary_key: true,
references: :hash,
type: Hash.Full
)
end
@doc """

@ -62,9 +62,10 @@ defmodule Explorer.Chain.TokenTransfer do
@constant "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"
@primary_key false
schema "token_transfers" do
field(:amount, :decimal)
field(:log_index, :integer)
field(:log_index, :integer, primary_key: true)
field(:token_id, :decimal)
belongs_to(:from_address, Address, foreign_key: :from_address_hash, references: :hash, type: Hash.Address)
@ -78,7 +79,12 @@ defmodule Explorer.Chain.TokenTransfer do
type: Hash.Address
)
belongs_to(:transaction, Transaction, foreign_key: :transaction_hash, references: :hash, type: Hash.Full)
belongs_to(:transaction, Transaction,
foreign_key: :transaction_hash,
primary_key: true,
references: :hash,
type: Hash.Full
)
has_one(:token, through: [:token_contract_address, :token])
@ -195,7 +201,7 @@ defmodule Explorer.Chain.TokenTransfer do
tt in TokenTransfer,
join: t in Token,
on: tt.token_contract_address_hash == t.contract_address_hash,
select: {tt.token_contract_address_hash, count(tt.id)},
select: {tt.token_contract_address_hash, fragment("COUNT(*)")},
group_by: tt.token_contract_address_hash
)

@ -0,0 +1,22 @@
defmodule Explorer.Repo.Migrations.InternalTransactionsCompositePrimaryKey do
use Ecto.Migration
def up do
# Remove old id
alter table(:internal_transactions) do
remove(:id)
end
# Don't use `modify` as it requires restating the whole column description
execute("ALTER TABLE internal_transactions ADD PRIMARY KEY (transaction_hash, index)")
end
def down do
execute("ALTER TABLE internal_transactions DROP CONSTRAINT internal_transactions_pkey")
# Add back old id
alter table(:internal_transactions) do
add(:id, :bigserial, primary_key: true)
end
end
end

@ -0,0 +1,22 @@
defmodule Explorer.Repo.Migrations.LogsCompositePrimaryKey do
use Ecto.Migration
def up do
# Remove old id
alter table(:logs) do
remove(:id)
end
# Don't use `modify` as it requires restating the whole column description
execute("ALTER TABLE logs ADD PRIMARY KEY (transaction_hash, index)")
end
def down do
execute("ALTER TABLE logs DROP CONSTRAINT logs_pkey")
# Add back old id
alter table(:logs) do
add(:id, :bigserial, primary_key: true)
end
end
end

@ -0,0 +1,22 @@
defmodule Explorer.Repo.Migrations.TokenTransfersCompositePrimaryKey do
use Ecto.Migration
def up do
# Remove old id
alter table(:token_transfers) do
remove(:id)
end
# Don't use `modify` as it requires restating the whole column description
execute("ALTER TABLE token_transfers ADD PRIMARY KEY (transaction_hash, log_index)")
end
def down do
execute("ALTER TABLE token_transfers DROP CONSTRAINT token_transfers_pkey")
# Add back old id
alter table(:token_transfers) do
add(:id, :bigserial, primary_key: true)
end
end
end

@ -95,7 +95,6 @@ defmodule Explorer.Chain.ImportTest do
timeout: 5
},
transactions: %{
on_conflict: :replace_all,
params: [
%{
block_hash: "0xf6b4b8c88df3ebd252ec476328334dc026cf66606a84fb769b3d3cbccc8471bd",
@ -429,7 +428,9 @@ defmodule Explorer.Chain.ImportTest do
test "publishes internal_transaction data to subscribers on insert" do
Chain.subscribe_to_events(:internal_transactions)
Import.all(@import_data)
assert_received {:chain_event, :internal_transactions, :realtime, [%{id: _}, %{id: _}]}
assert_received {:chain_event, :internal_transactions, :realtime,
[%{transaction_hash: _, index: _}, %{transaction_hash: _, index: _}]}
end
test "publishes log data to subscribers on insert" do
@ -568,8 +569,7 @@ defmodule Explorer.Chain.ImportTest do
v: 0,
value: 0
}
],
on_conflict: :replace_all
]
},
internal_transactions: %{
params: [
@ -660,8 +660,7 @@ defmodule Explorer.Chain.ImportTest do
v: 0,
value: 0
}
],
on_conflict: :replace_all
]
},
internal_transactions: %{
params: [
@ -731,7 +730,6 @@ defmodule Explorer.Chain.ImportTest do
timeout: 5
},
transactions: %{
on_conflict: :replace_all,
params: [
%{
block_hash: "0xf6b4b8c88df3ebd252ec476328334dc026cf66606a84fb769b3d3cbccc8471bd",
@ -878,7 +876,6 @@ defmodule Explorer.Chain.ImportTest do
},
broadcast: false,
transactions: %{
on_conflict: :replace_all,
params: [
%{
block_hash: "0x1f8cde8bd326702c49e065d56b08bdc82caa0c4820d914e27026c9c68ca1cf09",
@ -1038,8 +1035,7 @@ defmodule Explorer.Chain.ImportTest do
v: 0,
value: 0
}
],
on_conflict: :replace_all
]
},
transaction_forks: %{
params: [
@ -1228,8 +1224,7 @@ defmodule Explorer.Chain.ImportTest do
value: 0,
status: :ok
}
],
on_conflict: :replace_all
]
}
})
@ -1341,8 +1336,7 @@ defmodule Explorer.Chain.ImportTest do
value: 0,
status: :ok
}
],
on_conflict: :replace_all
]
}
})
@ -1482,7 +1476,6 @@ defmodule Explorer.Chain.ImportTest do
},
tokens: %{
params: [params_for(:token, contract_address_hash: token_contract_address_hash)],
on_conflict: :replace_all,
timeout: 1
},
transactions: %{
@ -1498,7 +1491,6 @@ defmodule Explorer.Chain.ImportTest do
cumulative_gas_used: 0
)
],
on_conflict: :replace_all,
timeout: 1
},
transaction_forks: %{
@ -1711,8 +1703,7 @@ defmodule Explorer.Chain.ImportTest do
value: 0,
status: :error
}
],
on_conflict: :replace_all
]
},
internal_transactions: %{
params: [

@ -50,12 +50,15 @@ defmodule Explorer.Chain.TokenTransferTest do
token: token
)
transfers_ids =
transfers_primary_keys =
token_contract_address.hash
|> TokenTransfer.fetch_token_transfers_from_token_hash([])
|> Enum.map(& &1.id)
|> Enum.map(&{&1.transaction_hash, &1.log_index})
assert transfers_ids == [another_transfer.id, token_transfer.id]
assert transfers_primary_keys == [
{another_transfer.transaction_hash, another_transfer.log_index},
{token_transfer.transaction_hash, token_transfer.log_index}
]
end
test "when there isn't token transfers won't show anything" do
@ -101,14 +104,14 @@ defmodule Explorer.Chain.TokenTransferTest do
paging_options = %PagingOptions{key: first_page.inserted_at, page_size: 1}
token_transfers_ids_paginated =
token_transfers_primary_keys_paginated =
TokenTransfer.fetch_token_transfers_from_token_hash(
token_contract_address.hash,
paging_options: paging_options
)
|> Enum.map(& &1.id)
|> Enum.map(&{&1.transaction_hash, &1.log_index})
assert token_transfers_ids_paginated == [second_page.id]
assert token_transfers_primary_keys_paginated == [{second_page.transaction_hash, second_page.log_index}]
end
end

@ -235,7 +235,10 @@ defmodule Explorer.ChainTest do
insert(:token_transfer, to_address: build(:address), transaction: transaction)
transaction = Chain.address_to_transactions(address) |> List.first()
assert transaction.token_transfers |> Enum.map(& &1.id) == [token_transfer.id]
assert transaction.token_transfers |> Enum.map(&{&1.transaction_hash, &1.log_index}) == [
{token_transfer.transaction_hash, token_transfer.log_index}
]
end
test "returns just the token transfers related to the given contract address" do
@ -250,7 +253,10 @@ defmodule Explorer.ChainTest do
insert(:token_transfer, to_address: build(:address), transaction: transaction)
transaction = Chain.address_to_transactions(contract_address) |> List.first()
assert Enum.map(transaction.token_transfers, & &1.id) == [token_transfer.id]
assert Enum.map(transaction.token_transfers, &{&1.transaction_hash, &1.log_index}) == [
{token_transfer.transaction_hash, token_transfer.log_index}
]
end
test "returns all token transfers when the given address is the token contract address" do
@ -572,12 +578,17 @@ defmodule Explorer.ChainTest do
|> insert()
|> with_block()
%TokenTransfer{id: token_transfer_id, token_contract_address_hash: token_contract_address_hash} =
insert(:token_transfer, to_address: address, transaction: transaction)
%TokenTransfer{
transaction_hash: token_transfer_transaction_hash,
log_index: token_transfer_log_index,
token_contract_address_hash: token_contract_address_hash
} = insert(:token_transfer, to_address: address, transaction: transaction)
assert token_contract_address_hash
|> Chain.fetch_token_transfers_from_token_hash()
|> Enum.map(& &1.id) == [token_transfer_id]
|> Enum.map(&{&1.transaction_hash, &1.log_index}) == [
{token_transfer_transaction_hash, token_transfer_log_index}
]
end
end
@ -778,7 +789,7 @@ defmodule Explorer.ChainTest do
|> insert_list(:transaction)
|> with_block()
%TokenTransfer{id: id1} =
%TokenTransfer{transaction_hash: transaction_hash1, log_index: log_index1} =
insert(
:token_transfer,
to_address: address,
@ -787,7 +798,7 @@ defmodule Explorer.ChainTest do
token: token
)
%TokenTransfer{id: id2} =
%TokenTransfer{transaction_hash: transaction_hash2, log_index: log_index2} =
insert(
:token_transfer,
to_address: address,
@ -799,14 +810,17 @@ defmodule Explorer.ChainTest do
fetched_transactions = Explorer.Chain.hashes_to_transactions([transaction1.hash, transaction2.hash])
assert Enum.all?(fetched_transactions, fn transaction ->
hd(transaction.token_transfers).id in [id1, id2]
%TokenTransfer{transaction_hash: transaction_hash, log_index: log_index} =
hd(transaction.token_transfers)
{transaction_hash, log_index} in [{transaction_hash1, log_index1}, {transaction_hash2, log_index2}]
end)
end
end
describe "indexed_ratio/0" do
test "returns indexed ratio" do
for index <- 6..10 do
for index <- 5..9 do
insert(:block, number: index)
end
@ -818,7 +832,7 @@ defmodule Explorer.ChainTest do
end
test "returns 1.0 if fully indexed blocks" do
for index <- 1..10 do
for index <- 0..9 do
insert(:block, number: index)
end
@ -889,7 +903,6 @@ defmodule Explorer.ChainTest do
]
},
transactions: %{
on_conflict: :replace_all,
params: [
%{
block_hash: "0xf6b4b8c88df3ebd252ec476328334dc026cf66606a84fb769b3d3cbccc8471bd",
@ -1270,7 +1283,7 @@ defmodule Explorer.ChainTest do
|> insert()
|> with_block(block)
%InternalTransaction{id: first_id} =
%InternalTransaction{transaction_hash: first_transaction_hash, index: first_index} =
insert(:internal_transaction,
index: 1,
transaction: transaction,
@ -1279,7 +1292,7 @@ defmodule Explorer.ChainTest do
transaction_index: transaction.index
)
%InternalTransaction{id: second_id} =
%InternalTransaction{transaction_hash: second_transaction_hash, index: second_index} =
insert(:internal_transaction,
index: 2,
transaction: transaction,
@ -1291,10 +1304,10 @@ defmodule Explorer.ChainTest do
result =
address
|> Chain.address_to_internal_transactions()
|> Enum.map(& &1.id)
|> Enum.map(&{&1.transaction_hash, &1.index})
assert Enum.member?(result, first_id)
assert Enum.member?(result, second_id)
assert Enum.member?(result, {first_transaction_hash, first_index})
assert Enum.member?(result, {second_transaction_hash, second_index})
end
test "loads associations in necessity_by_association" do
@ -1359,7 +1372,7 @@ defmodule Explorer.ChainTest do
|> insert()
|> with_block(block)
%InternalTransaction{id: first_pending} =
%InternalTransaction{transaction_hash: first_pending_transaction_hash, index: first_pending_index} =
insert(
:internal_transaction,
transaction: pending_transaction,
@ -1369,7 +1382,7 @@ defmodule Explorer.ChainTest do
transaction_index: pending_transaction.index
)
%InternalTransaction{id: second_pending} =
%InternalTransaction{transaction_hash: second_pending_transaction_hash, index: second_pending_index} =
insert(
:internal_transaction,
transaction: pending_transaction,
@ -1386,7 +1399,7 @@ defmodule Explorer.ChainTest do
|> insert()
|> with_block(a_block)
%InternalTransaction{id: first} =
%InternalTransaction{transaction_hash: first_transaction_hash, index: first_index} =
insert(
:internal_transaction,
transaction: first_a_transaction,
@ -1396,7 +1409,7 @@ defmodule Explorer.ChainTest do
transaction_index: first_a_transaction.index
)
%InternalTransaction{id: second} =
%InternalTransaction{transaction_hash: second_transaction_hash, index: second_index} =
insert(
:internal_transaction,
transaction: first_a_transaction,
@ -1411,7 +1424,7 @@ defmodule Explorer.ChainTest do
|> insert()
|> with_block(a_block)
%InternalTransaction{id: third} =
%InternalTransaction{transaction_hash: third_transaction_hash, index: third_index} =
insert(
:internal_transaction,
transaction: second_a_transaction,
@ -1421,7 +1434,7 @@ defmodule Explorer.ChainTest do
transaction_index: second_a_transaction.index
)
%InternalTransaction{id: fourth} =
%InternalTransaction{transaction_hash: fourth_transaction_hash, index: fourth_index} =
insert(
:internal_transaction,
transaction: second_a_transaction,
@ -1438,7 +1451,7 @@ defmodule Explorer.ChainTest do
|> insert()
|> with_block(b_block)
%InternalTransaction{id: fifth} =
%InternalTransaction{transaction_hash: fifth_transaction_hash, index: fifth_index} =
insert(
:internal_transaction,
transaction: first_b_transaction,
@ -1448,7 +1461,7 @@ defmodule Explorer.ChainTest do
transaction_index: first_b_transaction.index
)
%InternalTransaction{id: sixth} =
%InternalTransaction{transaction_hash: sixth_transaction_hash, index: sixth_index} =
insert(
:internal_transaction,
transaction: first_b_transaction,
@ -1461,9 +1474,18 @@ defmodule Explorer.ChainTest do
result =
address
|> Chain.address_to_internal_transactions()
|> Enum.map(& &1.id)
|> Enum.map(&{&1.transaction_hash, &1.index})
assert [second_pending, first_pending, sixth, fifth, fourth, third, second, first] == result
assert [
{second_pending_transaction_hash, second_pending_index},
{first_pending_transaction_hash, first_pending_index},
{sixth_transaction_hash, sixth_index},
{fifth_transaction_hash, fifth_index},
{fourth_transaction_hash, fourth_index},
{third_transaction_hash, third_index},
{second_transaction_hash, second_index},
{first_transaction_hash, first_index}
] == result
end
test "pages by {block_number, transaction_index, index}" do
@ -1492,7 +1514,7 @@ defmodule Explorer.ChainTest do
|> insert()
|> with_block(a_block)
%InternalTransaction{id: first} =
%InternalTransaction{transaction_hash: first_transaction_hash, index: first_index} =
insert(
:internal_transaction,
transaction: first_a_transaction,
@ -1502,7 +1524,7 @@ defmodule Explorer.ChainTest do
transaction_index: first_a_transaction.index
)
%InternalTransaction{id: second} =
%InternalTransaction{transaction_hash: second_transaction_hash, index: second_index} =
insert(
:internal_transaction,
transaction: first_a_transaction,
@ -1517,7 +1539,7 @@ defmodule Explorer.ChainTest do
|> insert()
|> with_block(a_block)
%InternalTransaction{id: third} =
%InternalTransaction{transaction_hash: third_transaction_hash, index: third_index} =
insert(
:internal_transaction,
transaction: second_a_transaction,
@ -1527,7 +1549,7 @@ defmodule Explorer.ChainTest do
transaction_index: second_a_transaction.index
)
%InternalTransaction{id: fourth} =
%InternalTransaction{transaction_hash: fourth_transaction_hash, index: fourth_index} =
insert(
:internal_transaction,
transaction: second_a_transaction,
@ -1544,7 +1566,7 @@ defmodule Explorer.ChainTest do
|> insert()
|> with_block(b_block)
%InternalTransaction{id: fifth} =
%InternalTransaction{transaction_hash: fifth_transaction_hash, index: fifth_index} =
insert(
:internal_transaction,
transaction: first_b_transaction,
@ -1554,7 +1576,7 @@ defmodule Explorer.ChainTest do
transaction_index: first_b_transaction.index
)
%InternalTransaction{id: sixth} =
%InternalTransaction{transaction_hash: sixth_transaction_hash, index: sixth_index} =
insert(
:internal_transaction,
transaction: first_b_transaction,
@ -1566,28 +1588,45 @@ defmodule Explorer.ChainTest do
# When paged, internal transactions need an associated block number, so `second_pending` and `first_pending` are
# excluded.
assert [sixth, fifth, fourth, third, second, first] ==
assert [
{sixth_transaction_hash, sixth_index},
{fifth_transaction_hash, fifth_index},
{fourth_transaction_hash, fourth_index},
{third_transaction_hash, third_index},
{second_transaction_hash, second_index},
{first_transaction_hash, first_index}
] ==
address
|> Chain.address_to_internal_transactions(
paging_options: %PagingOptions{key: {6001, 3, 2}, page_size: 8}
)
|> Enum.map(& &1.id)
|> Enum.map(&{&1.transaction_hash, &1.index})
# block number ==, transaction index ==, internal transaction index <
assert [fourth, third, second, first] ==
assert [
{fourth_transaction_hash, fourth_index},
{third_transaction_hash, third_index},
{second_transaction_hash, second_index},
{first_transaction_hash, first_index}
] ==
address
|> Chain.address_to_internal_transactions(
paging_options: %PagingOptions{key: {6000, 0, 1}, page_size: 8}
)
|> Enum.map(& &1.id)
|> Enum.map(&{&1.transaction_hash, &1.index})
# block number ==, transaction index <
assert [fourth, third, second, first] ==
assert [
{fourth_transaction_hash, fourth_index},
{third_transaction_hash, third_index},
{second_transaction_hash, second_index},
{first_transaction_hash, first_index}
] ==
address
|> Chain.address_to_internal_transactions(
paging_options: %PagingOptions{key: {6000, -1, -1}, page_size: 8}
)
|> Enum.map(& &1.id)
|> Enum.map(&{&1.transaction_hash, &1.index})
# block number <
assert [] ==
@ -1595,7 +1634,7 @@ defmodule Explorer.ChainTest do
|> Chain.address_to_internal_transactions(
paging_options: %PagingOptions{key: {2000, -1, -1}, page_size: 8}
)
|> Enum.map(& &1.id)
|> Enum.map(&{&1.transaction_hash, &1.index})
end
test "excludes internal transactions of type `call` when they are alone in the parent transaction" do
@ -1637,7 +1676,7 @@ defmodule Explorer.ChainTest do
actual = Enum.at(Chain.address_to_internal_transactions(address), 0)
assert actual.id == expected.id
assert {actual.transaction_hash, actual.index} == {expected.transaction_hash, expected.index}
end
end
@ -1708,7 +1747,15 @@ defmodule Explorer.ChainTest do
results = [internal_transaction | _] = Chain.transaction_to_internal_transactions(transaction)
assert 2 == length(results)
assert Enum.all?(results, &(&1.id in [first.id, second.id]))
assert Enum.all?(
results,
&({&1.transaction_hash, &1.index} in [
{first.transaction_hash, first.index},
{second.transaction_hash, second.index}
])
)
assert internal_transaction.transaction.block.number == block.number
end
@ -1781,7 +1828,7 @@ defmodule Explorer.ChainTest do
actual = Enum.at(Chain.transaction_to_internal_transactions(transaction), 0)
assert actual.id == expected.id
assert {actual.transaction_hash, actual.index} == {expected.transaction_hash, expected.index}
end
test "includes internal transactions of type `reward` even when they are alone in the parent transaction" do
@ -1801,7 +1848,7 @@ defmodule Explorer.ChainTest do
actual = Enum.at(Chain.transaction_to_internal_transactions(transaction), 0)
assert actual.id == expected.id
assert {actual.transaction_hash, actual.index} == {expected.transaction_hash, expected.index}
end
test "includes internal transactions of type `suicide` even when they are alone in the parent transaction" do
@ -1822,7 +1869,7 @@ defmodule Explorer.ChainTest do
actual = Enum.at(Chain.transaction_to_internal_transactions(transaction), 0)
assert actual.id == expected.id
assert {actual.transaction_hash, actual.index} == {expected.transaction_hash, expected.index}
end
test "returns the internal transactions in ascending index order" do
@ -1831,7 +1878,7 @@ defmodule Explorer.ChainTest do
|> insert()
|> with_block()
%InternalTransaction{id: first_id} =
%InternalTransaction{transaction_hash: first_transaction_hash, index: first_index} =
insert(:internal_transaction,
transaction: transaction,
index: 0,
@ -1839,7 +1886,7 @@ defmodule Explorer.ChainTest do
transaction_index: transaction.index
)
%InternalTransaction{id: second_id} =
%InternalTransaction{transaction_hash: second_transaction_hash, index: second_index} =
insert(:internal_transaction,
transaction: transaction,
index: 1,
@ -1850,9 +1897,9 @@ defmodule Explorer.ChainTest do
result =
transaction
|> Chain.transaction_to_internal_transactions()
|> Enum.map(& &1.id)
|> Enum.map(&{&1.transaction_hash, &1.index})
assert [first_id, second_id] == result
assert [{first_transaction_hash, first_index}, {second_transaction_hash, second_index}] == result
end
test "pages by index" do
@ -1861,7 +1908,7 @@ defmodule Explorer.ChainTest do
|> insert()
|> with_block()
%InternalTransaction{id: first_id} =
%InternalTransaction{transaction_hash: first_transaction_hash, index: first_index} =
insert(:internal_transaction,
transaction: transaction,
index: 0,
@ -1869,7 +1916,7 @@ defmodule Explorer.ChainTest do
transaction_index: transaction.index
)
%InternalTransaction{id: second_id} =
%InternalTransaction{transaction_hash: second_transaction_hash, index: second_index} =
insert(:internal_transaction,
transaction: transaction,
index: 1,
@ -1877,20 +1924,20 @@ defmodule Explorer.ChainTest do
transaction_index: transaction.index
)
assert [^first_id, ^second_id] =
assert [{first_transaction_hash, first_index}, {second_transaction_hash, second_index}] ==
transaction
|> Chain.transaction_to_internal_transactions(paging_options: %PagingOptions{key: {-1}, page_size: 2})
|> Enum.map(& &1.id)
|> Enum.map(&{&1.transaction_hash, &1.index})
assert [^first_id] =
assert [{first_transaction_hash, first_index}] ==
transaction
|> Chain.transaction_to_internal_transactions(paging_options: %PagingOptions{key: {-1}, page_size: 1})
|> Enum.map(& &1.id)
|> Enum.map(&{&1.transaction_hash, &1.index})
assert [^second_id] =
assert [{second_transaction_hash, second_index}] ==
transaction
|> Chain.transaction_to_internal_transactions(paging_options: %PagingOptions{key: {0}, page_size: 2})
|> Enum.map(& &1.id)
|> Enum.map(&{&1.transaction_hash, &1.index})
end
end
@ -1907,9 +1954,9 @@ defmodule Explorer.ChainTest do
|> insert()
|> with_block()
%Log{id: id} = insert(:log, transaction: transaction)
%Log{transaction_hash: transaction_hash, index: index} = insert(:log, transaction: transaction)
assert [%Log{id: ^id}] = Chain.transaction_to_logs(transaction)
assert [%Log{transaction_hash: ^transaction_hash, index: ^index}] = Chain.transaction_to_logs(transaction)
end
test "with logs can be paginated" do
@ -1970,9 +2017,11 @@ defmodule Explorer.ChainTest do
|> insert()
|> with_block()
%TokenTransfer{id: id} = insert(:token_transfer, transaction: transaction)
%TokenTransfer{transaction_hash: transaction_hash, log_index: log_index} =
insert(:token_transfer, transaction: transaction)
assert [%TokenTransfer{id: ^id}] = Chain.transaction_to_token_transfers(transaction)
assert [%TokenTransfer{transaction_hash: ^transaction_hash, log_index: ^log_index}] =
Chain.transaction_to_token_transfers(transaction)
end
test "token transfers necessity_by_association loads associations" do

@ -132,7 +132,7 @@ defmodule Indexer.Block.Fetcher do
logs: %{params: logs},
token_transfers: %{params: token_transfers},
tokens: %{on_conflict: :nothing, params: tokens},
transactions: %{params: transactions_with_receipts, on_conflict: :replace_all}
transactions: %{params: transactions_with_receipts}
}
) do
{:ok, {inserted, next}}

@ -564,7 +564,7 @@ defmodule Indexer.Block.FetcherTest do
assert Repo.aggregate(Chain.Block, :count, :hash) == 1
assert Repo.aggregate(Address, :count, :hash) == 2
assert Repo.aggregate(Log, :count, :id) == 1
assert Chain.log_count() == 1
assert Repo.aggregate(Transaction, :count, :hash) == 1
first_address = Repo.get!(Address, first_address_hash)

Loading…
Cancel
Save