+
<%= gettext "Token Transfers" %>
<%= gettext("Something went wrong, click to reload.") %>
diff --git a/apps/block_scout_web/lib/block_scout_web/views/address_contract_view.ex b/apps/block_scout_web/lib/block_scout_web/views/address_contract_view.ex
index 3c51c70342..e772781277 100644
--- a/apps/block_scout_web/lib/block_scout_web/views/address_contract_view.ex
+++ b/apps/block_scout_web/lib/block_scout_web/views/address_contract_view.ex
@@ -14,4 +14,20 @@ defmodule BlockScoutWeb.AddressContractView do
"""
def format_optimization_text(true), do: gettext("true")
def format_optimization_text(false), do: gettext("false")
+
+ def contract_lines_with_index(contract_source_code) do
+ contract_lines = String.split(contract_source_code, "\n")
+
+ max_digits =
+ contract_lines
+ |> Enum.count()
+ |> Integer.digits()
+ |> Enum.count()
+
+ contract_lines
+ |> Enum.with_index(1)
+ |> Enum.map(fn {value, line} ->
+ {value, String.pad_leading(to_string(line), max_digits, " ")}
+ end)
+ end
end
diff --git a/apps/block_scout_web/priv/gettext/default.pot b/apps/block_scout_web/priv/gettext/default.pot
index d0367b6bd4..d73265cc8d 100644
--- a/apps/block_scout_web/priv/gettext/default.pot
+++ b/apps/block_scout_web/priv/gettext/default.pot
@@ -267,7 +267,7 @@ msgid "Connection Lost, click to load newer validations"
msgstr ""
#, elixir-format
-#: lib/block_scout_web/templates/address_contract/index.html.eex:50
+#: lib/block_scout_web/templates/address_contract/index.html.eex:49
msgid "Contract ABI"
msgstr ""
@@ -300,7 +300,7 @@ msgid "Contract Name"
msgstr ""
#, elixir-format
-#: lib/block_scout_web/templates/address_contract/index.html.eex:64
+#: lib/block_scout_web/templates/address_contract/index.html.eex:63
msgid "Contract creation code"
msgstr ""
@@ -324,8 +324,8 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_contract/index.html.eex:39
-#: lib/block_scout_web/templates/address_contract/index.html.eex:52
-#: lib/block_scout_web/templates/address_contract/index.html.eex:66
+#: lib/block_scout_web/templates/address_contract/index.html.eex:51
+#: lib/block_scout_web/templates/address_contract/index.html.eex:65
msgid "Copy Code"
msgstr ""
diff --git a/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po b/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po
index dbc529460e..2b41acfd7f 100644
--- a/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po
+++ b/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po
@@ -267,7 +267,7 @@ msgid "Connection Lost, click to load newer validations"
msgstr ""
#, elixir-format
-#: lib/block_scout_web/templates/address_contract/index.html.eex:50
+#: lib/block_scout_web/templates/address_contract/index.html.eex:49
msgid "Contract ABI"
msgstr ""
@@ -300,7 +300,7 @@ msgid "Contract Name"
msgstr ""
#, elixir-format
-#: lib/block_scout_web/templates/address_contract/index.html.eex:64
+#: lib/block_scout_web/templates/address_contract/index.html.eex:63
msgid "Contract creation code"
msgstr ""
@@ -324,8 +324,8 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_contract/index.html.eex:39
-#: lib/block_scout_web/templates/address_contract/index.html.eex:52
-#: lib/block_scout_web/templates/address_contract/index.html.eex:66
+#: lib/block_scout_web/templates/address_contract/index.html.eex:51
+#: lib/block_scout_web/templates/address_contract/index.html.eex:65
msgid "Copy Code"
msgstr ""
diff --git a/apps/block_scout_web/test/block_scout_web/controllers/tokens/inventory_controller_test.ex b/apps/block_scout_web/test/block_scout_web/controllers/tokens/inventory_controller_test.ex
new file mode 100644
index 0000000000..7ce68af4d1
--- /dev/null
+++ b/apps/block_scout_web/test/block_scout_web/controllers/tokens/inventory_controller_test.ex
@@ -0,0 +1,121 @@
+defmodule BlockScoutWeb.Tokens.InventoryControllerTest do
+ use BlockScoutWeb.ConnCase
+
+ describe "GET index/3" do
+ test "with invalid address hash", %{conn: conn} do
+ conn = get(conn, token_inventory_path(conn, :index, "invalid_address"))
+
+ assert html_response(conn, 404)
+ end
+
+ test "with a token that doesn't exist", %{conn: conn} do
+ address = build(:address)
+ conn = get(conn, token_inventory_path(conn, :index, address.hash))
+
+ assert html_response(conn, 404)
+ end
+
+ test "successfully renders the page", %{conn: conn} do
+ token_contract_address = insert(:contract_address)
+ token = insert(:token, type: "ERC-721", contract_address: token_contract_address)
+
+ transaction =
+ :transaction
+ |> insert()
+ |> with_block()
+
+ insert(
+ :token_transfer,
+ transaction: transaction,
+ token_contract_address: token_contract_address,
+ token: token
+ )
+
+ conn =
+ get(
+ conn,
+ token_inventory_path(conn, :index, token_contract_address.hash)
+ )
+
+ assert html_response(conn, 200)
+ end
+
+ test "returns next page of results based on last seen token balance", %{conn: conn} do
+ token = insert(:token, type: "ERC-721")
+
+ transaction =
+ :transaction
+ |> insert()
+ |> with_block()
+
+ second_page_token_balances =
+ Enum.map(
+ 1..50,
+ &insert(
+ :token_transfer,
+ transaction: transaction,
+ token_contract_address: token.contract_address,
+ token: token,
+ token_id: &1 + 1000
+ )
+ )
+
+ conn =
+ get(conn, token_inventory_path(conn, :index, token.contract_address_hash), %{
+ "token_id" => "999"
+ })
+
+ assert Enum.map(conn.assigns.unique_tokens, & &1.token_id) == Enum.map(second_page_token_balances, & &1.token_id)
+ end
+
+ test "next_page_params exists if not on last page", %{conn: conn} do
+ token = insert(:token, type: "ERC-721")
+
+ transaction =
+ :transaction
+ |> insert()
+ |> with_block()
+
+ Enum.each(
+ 1..51,
+ &insert(
+ :token_transfer,
+ transaction: transaction,
+ token_contract_address: token.contract_address,
+ token: token,
+ token_id: &1 + 1000
+ )
+ )
+
+ expected_next_page_params = %{
+ "token_id" => to_string(token.contract_address_hash),
+ "unique_token" => 1050
+ }
+
+ conn = get(conn, token_inventory_path(conn, :index, token.contract_address_hash))
+
+ assert conn.assigns.next_page_params == expected_next_page_params
+ end
+
+ test "next_page_params are empty if on last page", %{conn: conn} do
+ token = insert(:token, type: "ERC-721")
+
+ transaction =
+ :transaction
+ |> insert()
+ |> with_block()
+
+ insert(
+ :token_transfer,
+ transaction: transaction,
+ token_contract_address: token.contract_address,
+ token: token,
+ token_id: 1000
+ )
+
+ conn = get(conn, token_inventory_path(conn, :index, token.contract_address_hash))
+
+ refute conn.assigns.next_page_params
+ end
+ end
+end
diff --git a/apps/block_scout_web/test/block_scout_web/views/address_contract_view_test.exs b/apps/block_scout_web/test/block_scout_web/views/address_contract_view_test.exs
index e483bfc7ea..4bcb1eb2bf 100644
--- a/apps/block_scout_web/test/block_scout_web/views/address_contract_view_test.exs
+++ b/apps/block_scout_web/test/block_scout_web/views/address_contract_view_test.exs
@@ -14,4 +14,59 @@ defmodule BlockScoutWeb.AddressContractViewTest do
assert AddressContractView.format_optimization_text(false) == "false"
end
end
+
+ describe "contract_lines_with_index/1" do
+ test "returns a list of tuples containing two strings each" do
+ code = """
+ pragma solidity >=0.4.22 <0.6.0;
+
+ struct Proposal {
+ uint voteCount;
+ }
+
+ address chairperson;
+ mapping(address => Voter) voters;
+ Proposal[] proposals;
+
+ constructor(uint8 _numProposals) public {
+ chairperson = msg.sender;
+ voters[chairperson].weight = 1;
+ proposals.length = _numProposals;
+ }
+ """
+
+ result = AddressContractView.contract_lines_with_index(code)
+
+ assert result == [
+ {"pragma solidity >=0.4.22 <0.6.0;", " 1"},
+ {"", " 2"},
+ {"struct Proposal {", " 3"},
+ {" uint voteCount;", " 4"},
+ {"}", " 5"},
+ {"", " 6"},
+ {"address chairperson;", " 7"},
+ {"mapping(address => Voter) voters;", " 8"},
+ {"Proposal[] proposals;", " 9"},
+ {"", "10"},
+ {"constructor(uint8 _numProposals) public {", "11"},
+ {" chairperson = msg.sender;", "12"},
+ {" voters[chairperson].weight = 1;", "13"},
+ {" proposals.length = _numProposals;", "14"},
+ {"}", "15"},
+ {"", "16"}
+ ]
+ end
+
+ test "returns a list of tuples and the second element always has n chars with x lines" do
+ chars = 3
+ lines = 100
+ result = AddressContractView.contract_lines_with_index(Enum.join(1..lines, "\n"))
+ assert Enum.all?(result, fn {_, number} -> String.length(number) == chars end)
+ end
+
+ test "returns a list of tuples and the first element is just a line from the original string" do
+ result = AddressContractView.contract_lines_with_index("a\nb\nc\nd\ne")
+ assert Enum.map(result, fn {line, _number} -> line end) == ["a", "b", "c", "d", "e"]
+ end
+ end
end
diff --git a/apps/explorer/lib/explorer/chain/internal_transaction/type.ex b/apps/explorer/lib/explorer/chain/internal_transaction/type.ex
index 3f7f1eee81..4133dcf45f 100644
--- a/apps/explorer/lib/explorer/chain/internal_transaction/type.ex
+++ b/apps/explorer/lib/explorer/chain/internal_transaction/type.ex
@@ -38,6 +38,13 @@ defmodule Explorer.Chain.InternalTransaction.Type do
iex> Explorer.Chain.InternalTransaction.Type.cast("selfdestruct")
{:ok, :selfdestruct}
+ Deprecated values are not allowed for incoming data.
+
+ iex> Explorer.Chain.InternalTransaction.Type.cast(:suicide)
+ :error
+ iex> Explorer.Chain.InternalTransaction.Type.cast("suicide")
+ :error
+
Unsupported `String.t` return an `:error`.
iex> Explorer.Chain.InternalTransaction.Type.cast("hard-fork")
@@ -65,6 +72,11 @@ defmodule Explorer.Chain.InternalTransaction.Type do
iex> Explorer.Chain.InternalTransaction.Type.dump(:selfdestruct)
{:ok, "selfdestruct"}
+ Deprecated values are not allowed to be dumped to the database as old values should only be read, not written.
+
+ iex> Explorer.Chain.InternalTransaction.Type.dump(:suicide)
+ :error
+
Other atoms return an error
iex> Explorer.Chain.InternalTransaction.Type.dump(:other)
@@ -91,6 +103,11 @@ defmodule Explorer.Chain.InternalTransaction.Type do
iex> Explorer.Chain.InternalTransaction.Type.load("selfdestruct")
{:ok, :selfdestruct}
+ Converts deprecated value on load to the corresponding `t:t/0`.
+
+ iex> Explorer.Chain.InternalTransaction.Type.load("suicide")
+ {:ok, :selfdestruct}
+
Other `t:String.t/0` return `:error`
iex> Explorer.Chain.InternalTransaction.Type.load("other")
@@ -103,6 +120,8 @@ defmodule Explorer.Chain.InternalTransaction.Type do
def load("create"), do: {:ok, :create}
def load("reward"), do: {:ok, :reward}
def load("selfdestruct"), do: {:ok, :selfdestruct}
+ # deprecated
+ def load("suicide"), do: {:ok, :selfdestruct}
def load(_), do: :error
@doc """
diff --git a/apps/explorer/lib/explorer/chain/token_transfer.ex b/apps/explorer/lib/explorer/chain/token_transfer.ex
index c50bc23c64..80c46fa64d 100644
--- a/apps/explorer/lib/explorer/chain/token_transfer.ex
+++ b/apps/explorer/lib/explorer/chain/token_transfer.ex
@@ -135,6 +135,10 @@ defmodule Explorer.Chain.TokenTransfer do
def page_token_transfer(query, %PagingOptions{key: nil}), do: query
+ def page_token_transfer(query, %PagingOptions{key: {token_id}}) do
+ where(query, [token_transfer], token_transfer.token_id > ^token_id)
+ end
+
def page_token_transfer(query, %PagingOptions{key: {block_number, log_index}}) do
where(
query,
diff --git a/apps/explorer/lib/release_tasks.ex b/apps/explorer/lib/release_tasks.ex
new file mode 100644
index 0000000000..27e2566c37
--- /dev/null
+++ b/apps/explorer/lib/release_tasks.ex
@@ -0,0 +1,93 @@
+defmodule Explorer.ReleaseTasks do
+ @moduledoc """
+ Release tasks used to migrate or generate seeds.
+ """
+
+ alias Ecto.Migrator
+
+ @start_apps [
+ :crypto,
+ :ssl,
+ :postgrex,
+ :ecto,
+ # If using Ecto 3.0 or higher
+ :ecto_sql
+ ]
+
+ @repos Application.get_env(:blockscout, :ecto_repos, [Explorer.Repo])
+
+ def migrate(_argv) do
+ start_services()
+
+ run_migrations()
+
+ stop_services()
+ end
+
+ def seed(_argv) do
+ start_services()
+
+ run_migrations()
+
+ run_seeds()
+
+ stop_services()
+ end
+
+ defp start_services do
+ IO.puts("Starting dependencies..")
+ # Start apps necessary for executing migrations
+ Enum.each(@start_apps, &Application.ensure_all_started/1)
+
+ # Start the Repo(s) for app
+ IO.puts("Starting repos..")
+
+ # Switch pool_size to 2 for ecto > 3.0
+ Enum.each(@repos, & &1.start_link(pool_size: 1))
+ end
+
+ defp stop_services do
+ IO.puts("Success!")
+ :init.stop()
+ end
+
+ defp run_migrations do
+ Enum.each(@repos, &run_migrations_for/1)
+ end
+
+ defp run_migrations_for(repo) do
+ app = Keyword.get(repo.config, :otp_app)
+ IO.puts("Running migrations for #{app}")
+ migrations_path = priv_path_for(repo, "migrations")
+ Migrator.run(repo, migrations_path, :up, all: true)
+ end
+
+ defp run_seeds do
+ Enum.each(@repos, &run_seeds_for/1)
+ end
+
+ # sobelow_skip ["RCE.CodeModule"]
+ defp run_seeds_for(repo) do
+ # Run the seed script if it exists
+ seed_script = priv_path_for(repo, "seeds.exs")
+
+ if File.exists?(seed_script) do
+ IO.puts("Running seed script..")
+ Code.eval_file(seed_script)
+ end
+ end
+
+ defp priv_path_for(repo, filename) do
+ app = Keyword.get(repo.config, :otp_app)
+
+ repo_underscore =
+ repo
+ |> Module.split()
+ |> List.last()
+ |> Macro.underscore()
+
+ priv_dir = "#{:code.priv_dir(app)}"
+
+ Path.join([priv_dir, repo_underscore, filename])
+ end
+end
diff --git a/apps/explorer/priv/repo/migrations/20181107164103_eip6.exs b/apps/explorer/priv/repo/migrations/20181107164103_eip6.exs
index 099262696e..783561be1f 100644
--- a/apps/explorer/priv/repo/migrations/20181107164103_eip6.exs
+++ b/apps/explorer/priv/repo/migrations/20181107164103_eip6.exs
@@ -1,28 +1,31 @@
defmodule Explorer.Repo.Migrations.EIP6 do
+ @moduledoc """
+ Use `priv/repo/migrations/scripts/20181107164103_eip6.sql` to migrate data and validate constraint.
+
+ ```sh
+ mix ecto.migrate
+ psql -d $DATABASE -a -f priv/repo/migrations/scripts/20181107164103_eip6.sql
+ ```
+ """
+
use Ecto.Migration
def up do
execute("ALTER TABLE internal_transactions DROP CONSTRAINT suicide_has_from_and_to_address_hashes")
- execute("UPDATE internal_transactions SET type = 'selfdestruct' WHERE type = 'suicide'")
-
- create(
- constraint(
- :internal_transactions,
- :selfdestruct_has_from_and_to_address_hashes,
- check: """
- type != 'selfdestruct' OR
- (from_address_hash IS NOT NULL AND gas IS NULL AND to_address_hash IS NOT NULL)
- """
- )
- )
+ # `NOT VALID` skips checking pre-existing rows. Use `priv/repo/migrations/scripts/20181107164103_eip6.sql` to
+ # migrate data and validate constraints
+ execute("""
+ ALTER TABLE internal_transactions
+ ADD CONSTRAINT selfdestruct_has_from_and_to_address
+ CHECK (type != 'selfdestruct' OR (from_address_hash IS NOT NULL AND gas IS NULL AND to_address_hash IS NOT NULL))
+ NOT VALID
+ """)
end
def down do
execute("ALTER TABLE internal_transactions DROP CONSTRAINT selfdestruct_has_from_and_to_address_hashes")
- execute("UPDATE internal_transactions SET type = 'suicide' WHERE type = 'selfdestruct'")
-
create(
constraint(
:internal_transactions,
diff --git a/apps/explorer/priv/repo/migrations/20181108205650_additional_internal_transaction_constraints.exs b/apps/explorer/priv/repo/migrations/20181108205650_additional_internal_transaction_constraints.exs
index 295923b9c4..de554e5acf 100644
--- a/apps/explorer/priv/repo/migrations/20181108205650_additional_internal_transaction_constraints.exs
+++ b/apps/explorer/priv/repo/migrations/20181108205650_additional_internal_transaction_constraints.exs
@@ -1,10 +1,37 @@
defmodule Explorer.Repo.Migrations.AdditionalInternalTransactionConstraints do
+ @moduledoc """
+ Use `priv/repo/migrations/scripts/20181108205650_additional_internal_transaction_constraints.sql` to migrate data and
+ validate constraint.
+
+ ```sh
+ mix ecto.migrate
+ psql -d $DATABASE -a -f priv/repo/migrations/scripts/20181108205650_additional_internal_transaction_constraints.sql
+ ```
+ """
+
use Ecto.Migration
def up do
- create(constraint(:internal_transactions, :call_has_call_type, check: "type != 'call' OR call_type IS NOT NULL"))
- create(constraint(:internal_transactions, :call_has_input, check: "type != 'call' OR input IS NOT NULL"))
- create(constraint(:internal_transactions, :create_has_init, check: "type != 'create' OR init IS NOT NULL"))
+ execute("""
+ ALTER TABLE internal_transactions
+ ADD CONSTRAINT call_has_call_type
+ CHECK (type != 'call' OR call_type IS NOT NULL)
+ NOT VALID
+ """)
+
+ execute("""
+ ALTER TABLE internal_transactions
+ ADD CONSTRAINT call_has_input
+ CHECK (type != 'call' OR input IS NOT NULL)
+ NOT VALID
+ """)
+
+ execute("""
+ ALTER TABLE internal_transactions
+ ADD CONSTRAINT create_has_init
+ CHECK (type != 'create' OR init IS NOT NULL)
+ NOT VALID
+ """)
end
def down do
diff --git a/apps/explorer/priv/repo/migrations/scripts/20181107164103_eip6.sql b/apps/explorer/priv/repo/migrations/scripts/20181107164103_eip6.sql
new file mode 100644
index 0000000000..998b12ad02
--- /dev/null
+++ b/apps/explorer/priv/repo/migrations/scripts/20181107164103_eip6.sql
@@ -0,0 +1,64 @@
+DO $$
+DECLARE
+ row_count integer := 1;
+ batch_size integer := 50000; -- HOW MANY ITEMS WILL BE UPDATED AT TIME
+ iterator integer := batch_size;
+ max_row_number integer;
+ next_iterator integer;
+ updated_row_count integer;
+ deleted_row_count integer;
+BEGIN
+ DROP TABLE IF EXISTS current_suicide_internal_transactions_temp;
+ -- CREATES TEMP TABLE TO STORE DATA TO BE UPDATED
+ CREATE TEMP TABLE current_suicide_internal_transactions_temp(
+ transaction_hash bytea NOT NULL,
+ index bigint NOT NULL,
+ row_number integer
+ );
+ INSERT INTO current_suicide_internal_transactions_temp
+ SELECT DISTINCT ON (transaction_hash, index)
+ transaction_hash,
+ index,
+ ROW_NUMBER () OVER ()
+ FROM internal_transactions
+ WHERE type = 'suicide'
+ ORDER BY transaction_hash, index DESC;
+
+ max_row_number := (SELECT MAX(row_number) FROM current_suicide_internal_transactions_temp);
+ RAISE NOTICE '% items to be updated', max_row_number + 1;
+
+ -- ITERATES THROUGH THE ITEMS UNTIL THE TEMP TABLE IS EMPTY
+ WHILE iterator <= max_row_number LOOP
+ next_iterator := iterator + batch_size;
+
+ RAISE NOTICE '-> suicide internal transactions % to % to be updated', iterator, next_iterator - 1;
+
+ UPDATE internal_transactions
+ SET type = 'selfdestruct'
+ FROM current_suicide_internal_transactions_temp
+ WHERE internal_transactions.transaction_hash = current_suicide_internal_transactions_temp.transaction_hash AND
+ internal_transactions.index = current_suicide_internal_transactions_temp.index AND
+ current_suicide_internal_transactions_temp.row_number < next_iterator;
+
+ GET DIAGNOSTICS updated_row_count = ROW_COUNT;
+
+ RAISE NOTICE '-> % internal transactions updated from suicide to selfdesruct', updated_row_count;
+
+ DELETE FROM current_suicide_internal_transactions_temp
+ WHERE row_number < next_iterator;
+
+ GET DIAGNOSTICS deleted_row_count = ROW_COUNT;
+
+ ASSERT updated_row_count = deleted_row_count;
+
+ -- COMMITS THE BATCH UPDATES
+ CHECKPOINT;
+
+ -- UPDATES THE COUNTER SO IT DOESN'T TURN INTO AN INFINITE LOOP
+ iterator := next_iterator;
+ END LOOP;
+
+ RAISE NOTICE 'All suicide type internal transactions updated to selfdestruct. Validating constraint.';
+
+ ALTER TABLE internal_transactions VALIDATE CONSTRAINT selfdestruct_has_from_and_to_address;
+END $$;
diff --git a/apps/explorer/priv/repo/migrations/scripts/20181108205650_additional_internal_transaction_constraints.sql b/apps/explorer/priv/repo/migrations/scripts/20181108205650_additional_internal_transaction_constraints.sql
new file mode 100644
index 0000000000..69a6fa24dd
--- /dev/null
+++ b/apps/explorer/priv/repo/migrations/scripts/20181108205650_additional_internal_transaction_constraints.sql
@@ -0,0 +1,80 @@
+DO $$
+DECLARE
+ row_count integer := 1;
+ batch_size integer := 50000; -- HOW MANY ITEMS WILL BE UPDATED AT TIME
+ iterator integer := batch_size;
+ max_row_number integer;
+ next_iterator integer;
+ updated_transaction_count integer;
+ deleted_internal_transaction_count integer;
+ deleted_row_count integer;
+BEGIN
+ DROP TABLE IF EXISTS transactions_with_deprecated_internal_transactions;
+ -- CREATES TEMP TABLE TO STORE DATA TO BE UPDATED
+ CREATE TEMP TABLE transactions_with_deprecated_internal_transactions(
+ hash bytea NOT NULL,
+ row_number integer
+ );
+ INSERT INTO transactions_with_deprecated_internal_transactions
+ SELECT DISTINCT ON (transaction_hash)
+ transaction_hash,
+ ROW_NUMBER () OVER ()
+ FROM internal_transactions
+ WHERE
+ -- call_has_call_type CONSTRAINT
+ (type = 'call' AND call_type IS NULL) OR
+ -- call_has_input CONSTRAINT
+ (type = 'call' AND input IS NULL) OR
+ -- create_has_init CONSTRAINT
+ (type = 'create' AND init is NULL)
+ ORDER BY transaction_hash DESC;
+
+ max_row_number := (SELECT MAX(row_number) FROM transactions_with_deprecated_internal_transactions);
+ RAISE NOTICE '% transactions to be updated', max_row_number + 1;
+
+ -- ITERATES THROUGH THE ITEMS UNTIL THE TEMP TABLE IS EMPTY
+ WHILE iterator <= max_row_number LOOP
+ next_iterator := iterator + batch_size;
+
+ RAISE NOTICE '-> transactions with deprecated internal transactions % to % to be updated', iterator, next_iterator - 1;
+
+ UPDATE transactions
+ SET internal_transactions_indexed_at = NULL,
+ error = NULL
+ FROM transactions_with_deprecated_internal_transactions
+ WHERE transactions.hash = transactions_with_deprecated_internal_transactions.hash AND
+ transactions_with_deprecated_internal_transactions.row_number < next_iterator;
+
+ GET DIAGNOSTICS updated_transaction_count = ROW_COUNT;
+
+ RAISE NOTICE '-> % transactions updated to refetch internal transactions', updated_transaction_count;
+
+ DELETE FROM internal_transactions
+ USING transactions_with_deprecated_internal_transactions
+ WHERE internal_transactions.transaction_hash = transactions_with_deprecated_internal_transactions.hash AND
+ transactions_with_deprecated_internal_transactions.row_number < next_iterator;
+
+ GET DIAGNOSTICS deleted_internal_transaction_count = ROW_COUNT;
+
+ RAISE NOTICE '-> % internal transactions deleted', deleted_internal_transaction_count;
+
+ DELETE FROM transactions_with_deprecated_internal_transactions
+ WHERE row_number < next_iterator;
+
+ GET DIAGNOSTICS deleted_row_count = ROW_COUNT;
+
+ ASSERT updated_transaction_count = deleted_row_count;
+
+ -- COMMITS THE BATCH UPDATES
+ CHECKPOINT;
+
+ -- UPDATES THE COUNTER SO IT DOESN'T TURN INTO AN INFINITE LOOP
+ iterator := next_iterator;
+ END LOOP;
+
+ RAISE NOTICE 'All deprecated internal transactions will be refetched. Validating constraints.';
+
+ ALTER TABLE internal_transactions VALIDATE CONSTRAINT call_has_call_type;
+ ALTER TABLE internal_transactions VALIDATE CONSTRAINT call_has_input;
+ ALTER TABLE internal_transactions VALIDATE CONSTRAINT create_has_init;
+END $$;
diff --git a/apps/explorer/test/explorer/chain_test.exs b/apps/explorer/test/explorer/chain_test.exs
index 7c4dbdf8e0..fb6994fe9e 100644
--- a/apps/explorer/test/explorer/chain_test.exs
+++ b/apps/explorer/test/explorer/chain_test.exs
@@ -3131,7 +3131,7 @@ defmodule Explorer.ChainTest do
token_id: 29
)
- paging_options = %PagingOptions{key: {first_page.block_number, first_page.log_index}, page_size: 1}
+ paging_options = %PagingOptions{key: {first_page.token_id}, page_size: 1}
unique_tokens_ids_paginated =
token_contract_address.hash
diff --git a/mix.exs b/mix.exs
index bf60713de1..f76a8cfe00 100644
--- a/mix.exs
+++ b/mix.exs
@@ -63,7 +63,9 @@ defmodule BlockScout.Mixfile do
# Documentation
{:ex_doc, "~> 0.19.0", only: [:dev]},
# Code coverage
- {:excoveralls, "~> 0.10.0", only: [:test], github: "KronicDeth/excoveralls", branch: "circle-workflows"}
+ {:excoveralls, "~> 0.10.0", only: [:test], github: "KronicDeth/excoveralls", branch: "circle-workflows"},
+ # Release
+ {:distillery, "~> 2.0", runtime: false}
]
end
end
diff --git a/mix.lock b/mix.lock
index 3a6bf7bd26..68baad2790 100644
--- a/mix.lock
+++ b/mix.lock
@@ -6,6 +6,7 @@
"absinthe_plug": {:hex, :absinthe_plug, "1.4.6", "ac5d2d3d02acf52fda0f151b294017ab06e2ed1c6c15334e06aac82c94e36e08", [:mix], [{:absinthe, "~> 1.4.11", [hex: :absinthe, repo: "hexpm", optional: false]}, {:plug, "~> 1.3.2 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
"absinthe_relay": {:hex, :absinthe_relay, "1.4.4", "d0a6d8e71375a6026974d227456c8a73ea8eea7c7b00e698603ab5a96066c333", [:mix], [{:absinthe, "~> 1.4.0", [hex: :absinthe, repo: "hexpm", optional: false]}, {:ecto, "~> 2.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm"},
"accept": {:hex, :accept, "0.3.3", "548ebb6fb2e8b0d170e75bb6123aea6ceecb0189bb1231eeadf52eac08384a97", [:rebar3], [], "hexpm"},
+ "artificery": {:hex, :artificery, "0.2.6", "f602909757263f7897130cbd006b0e40514a541b148d366ad65b89236b93497a", [:mix], [], "hexpm"},
"bcrypt_elixir": {:hex, :bcrypt_elixir, "1.1.1", "6b5560e47a02196ce5f0ab3f1d8265db79a23868c137e973b27afef928ed8006", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm"},
"benchee": {:hex, :benchee, "0.13.2", "30cd4ff5f593fdd218a9b26f3c24d580274f297d88ad43383afe525b1543b165", [:mix], [{:deep_merge, "~> 0.1", [hex: :deep_merge, repo: "hexpm", optional: false]}], "hexpm"},
"benchee_csv": {:hex, :benchee_csv, "0.8.0", "0ca094677d6e2b2f601b7ee7864b754789ef9d24d079432e5e3d6f4fb83a4d80", [:mix], [{:benchee, "~> 0.12", [hex: :benchee, optional: false]}, {:csv, "~> 2.0", [hex: :csv, optional: false]}]},
@@ -25,6 +26,7 @@
"decimal": {:hex, :decimal, "1.5.0", "b0433a36d0e2430e3d50291b1c65f53c37d56f83665b43d79963684865beab68", [:mix], []},
"deep_merge": {:hex, :deep_merge, "0.2.0", "c1050fa2edf4848b9f556fba1b75afc66608a4219659e3311d9c9427b5b680b3", [:mix], [], "hexpm"},
"dialyxir": {:hex, :dialyxir, "0.5.1", "b331b091720fd93e878137add264bac4f644e1ddae07a70bf7062c7862c4b952", [:mix], []},
+ "distillery": {:hex, :distillery, "2.0.12", "6e78fe042df82610ac3fa50bd7d2d8190ad287d120d3cd1682d83a44e8b34dfb", [:mix], [{:artificery, "~> 0.2", [hex: :artificery, repo: "hexpm", optional: false]}], "hexpm"},
"earmark": {:hex, :earmark, "1.2.6", "b6da42b3831458d3ecc57314dff3051b080b9b2be88c2e5aa41cd642a5b044ed", [:mix], [], "hexpm"},
"ecto": {:hex, :ecto, "2.2.11", "4bb8f11718b72ba97a2696f65d247a379e739a0ecabf6a13ad1face79844791c", [:mix], [{:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: true]}, {:decimal, "~> 1.2", [hex: :decimal, repo: "hexpm", optional: false]}, {:mariaex, "~> 0.8.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.13.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm"},
"elixir_make": {:hex, :elixir_make, "0.4.2", "332c649d08c18bc1ecc73b1befc68c647136de4f340b548844efc796405743bf", [:mix], [], "hexpm"},
diff --git a/rel/commands/migrate.sh b/rel/commands/migrate.sh
new file mode 100644
index 0000000000..9a3261ce75
--- /dev/null
+++ b/rel/commands/migrate.sh
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+release_ctl eval --mfa "Explorer.ReleaseTasks.migrate/1" --argv -- "$@"
\ No newline at end of file
diff --git a/rel/commands/seed.sh b/rel/commands/seed.sh
new file mode 100644
index 0000000000..69bfb8815d
--- /dev/null
+++ b/rel/commands/seed.sh
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+release_ctl eval --mfa "Explorer.ReleaseTasks.seed/1" --argv -- "$@"
diff --git a/rel/config.exs b/rel/config.exs
new file mode 100644
index 0000000000..1a390a369a
--- /dev/null
+++ b/rel/config.exs
@@ -0,0 +1,85 @@
+# Import all plugins from `rel/plugins`
+# They can then be used by adding `plugin MyPlugin` to
+# either an environment, or release definition, where
+# `MyPlugin` is the name of the plugin module.
+~w(rel plugins *.exs)
+|> Path.join()
+|> Path.wildcard()
+|> Enum.map(&Code.eval_file(&1))
+
+defer = fn fun ->
+ apply(fun, [])
+end
+
+app_root = fn ->
+ if String.contains?(System.cwd!(), "apps") do
+ Path.join([System.cwd!(), "/../../"])
+ else
+ System.cwd!()
+ end
+end
+
+cookie =
+ defer.(fn ->
+ cookie_bytes =
+ :crypto.strong_rand_bytes(32)
+ |> Base.encode32()
+
+ :ok = File.write!(Path.join(app_root.(), ".erlang_cookie"), cookie_bytes)
+ :erlang.binary_to_atom(cookie_bytes, :utf8)
+ end)
+
+use Mix.Releases.Config,
+ # This sets the default release built by `mix release`
+ default_release: :default,
+ # This sets the default environment used by `mix release`
+ default_environment: Mix.env()
+
+# For a full list of config options for both releases
+# and environments, visit https://hexdocs.pm/distillery/config/distillery.html
+
+
+# You may define one or more environments in this file,
+# an environment's settings will override those of a release
+# when building in that environment, this combination of release
+# and environment configuration is called a profile
+
+environment :dev do
+ # If you are running Phoenix, you should make sure that
+ # server: true is set and the code reloader is disabled,
+ # even in dev mode.
+ # It is recommended that you build with MIX_ENV=prod and pass
+ # the --env flag to Distillery explicitly if you want to use
+ # dev mode.
+ set dev_mode: true
+ set include_erts: false
+ set cookie: :"i6E,!mJ6|E&|.VPaDywo@N.o}BgmC$UdKXW[aK,(@U0Asfpp/NergA;CR%YW4;i6"
+end
+
+environment :prod do
+ set include_erts: true
+ set include_src: false
+ set cookie: cookie
+ set vm_args: "rel/vm.args"
+end
+
+# You may define one or more releases in this file.
+# If you have not set a default release, or selected one
+# when running `mix release`, the first release in the file
+# will be used by default
+
+release :blockscout do
+ set version: "1.2.0-beta"
+ set applications: [
+ :runtime_tools,
+ block_scout_web: :permanent,
+ ethereum_jsonrpc: :permanent,
+ explorer: :permanent,
+ indexer: :permanent
+ ]
+ set commands: [
+ migrate: "rel/commands/migrate.sh",
+ seed: "rel/commands/seed.sh",
+ ]
+end
+
diff --git a/rel/plugins/.gitignore b/rel/plugins/.gitignore
new file mode 100644
index 0000000000..4fa3b5c29b
--- /dev/null
+++ b/rel/plugins/.gitignore
@@ -0,0 +1,3 @@
+*.*
+!*.exs
+!.gitignore
\ No newline at end of file
diff --git a/rel/vm.args b/rel/vm.args
new file mode 100644
index 0000000000..93f34bb37a
--- /dev/null
+++ b/rel/vm.args
@@ -0,0 +1,30 @@
+## This file provide the arguments provided to the VM at startup
+## You can find a full list of flags and their behaviours at
+## http://erlang.org/doc/man/erl.html
+
+## Name of the node
+-name <%= release_name %>@127.0.0.1
+
+## Cookie for distributed erlang
+-setcookie <%= release.profile.cookie %>
+
+## Heartbeat management; auto-restarts VM if it dies or becomes unresponsive
+## (Disabled by default..use with caution!)
+##-heart
+
+## Enable kernel poll and a few async threads
+##+K true
+##+A 5
+## For OTP21+, the +A flag is not used anymore,
+## +SDio replace it to use dirty schedulers
+##+SDio 5
+
+## Increase number of concurrent ports/sockets
+##-env ERL_MAX_PORTS 4096
+
+## Tweak GC to run more often
+##-env ERL_FULLSWEEP_AFTER 10
+
+# Enable SMP automatically based on availability
+# On OTP21+, this is not needed anymore.
+-smp auto